diff --git a/.gitignore b/.gitignore index ffa792e2922ba5d56bf658ac2187f6336da277fd..07bd6c6a2058c5d83d81d1668c7341036ffb7178 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ inkscape_devel_source.tar.bz2 *.kdev4 .uuid .clangd/ +.cache compile_commands.json # Snap packaging specific rules diff --git a/share/icons/Tango/scalable/actions/snap-alignment-self.svg b/share/icons/Tango/scalable/actions/snap-alignment-self.svg new file mode 100644 index 0000000000000000000000000000000000000000..fc9d5892590873370fc4501d8e295ed5baac1808 --- /dev/null +++ b/share/icons/Tango/scalable/actions/snap-alignment-self.svg @@ -0,0 +1,138 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/Tango/scalable/actions/snap-alignment.svg b/share/icons/Tango/scalable/actions/snap-alignment.svg new file mode 100644 index 0000000000000000000000000000000000000000..7971954611563fe89edc8bfdc8e7f443b4fe1a2b --- /dev/null +++ b/share/icons/Tango/scalable/actions/snap-alignment.svg @@ -0,0 +1,116 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/Tango/scalable/actions/snap-distribution.svg b/share/icons/Tango/scalable/actions/snap-distribution.svg new file mode 100644 index 0000000000000000000000000000000000000000..83dd3ccc0bf1c9aae52a40a997c3446fce0d0b36 --- /dev/null +++ b/share/icons/Tango/scalable/actions/snap-distribution.svg @@ -0,0 +1,155 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/scalable/actions/snap-alignment-self.svg b/share/icons/hicolor/scalable/actions/snap-alignment-self.svg new file mode 100644 index 0000000000000000000000000000000000000000..fc9d5892590873370fc4501d8e295ed5baac1808 --- /dev/null +++ b/share/icons/hicolor/scalable/actions/snap-alignment-self.svg @@ -0,0 +1,138 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/scalable/actions/snap-alignment.svg b/share/icons/hicolor/scalable/actions/snap-alignment.svg new file mode 100644 index 0000000000000000000000000000000000000000..7971954611563fe89edc8bfdc8e7f443b4fe1a2b --- /dev/null +++ b/share/icons/hicolor/scalable/actions/snap-alignment.svg @@ -0,0 +1,116 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/scalable/actions/snap-distribution.svg b/share/icons/hicolor/scalable/actions/snap-distribution.svg new file mode 100644 index 0000000000000000000000000000000000000000..83dd3ccc0bf1c9aae52a40a997c3446fce0d0b36 --- /dev/null +++ b/share/icons/hicolor/scalable/actions/snap-distribution.svg @@ -0,0 +1,155 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/snap-alignment-self-symbolic.svg b/share/icons/hicolor/symbolic/actions/snap-alignment-self-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..f405486eaec94ce1715216cf649716966795baaf --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/snap-alignment-self-symbolic.svg @@ -0,0 +1,138 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/snap-alignment-symbolic.svg b/share/icons/hicolor/symbolic/actions/snap-alignment-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..ecb3eec51490c928c5adf232fb8e0553046f1df6 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/snap-alignment-symbolic.svg @@ -0,0 +1,116 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/snap-distribution-symbolic.svg b/share/icons/hicolor/symbolic/actions/snap-distribution-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..83dd3ccc0bf1c9aae52a40a997c3446fce0d0b36 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/snap-distribution-symbolic.svg @@ -0,0 +1,155 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/multicolor/symbolic/actions/snap-alignment-self-symbolic.svg b/share/icons/multicolor/symbolic/actions/snap-alignment-self-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..f405486eaec94ce1715216cf649716966795baaf --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/snap-alignment-self-symbolic.svg @@ -0,0 +1,138 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/multicolor/symbolic/actions/snap-alignment-symbolic.svg b/share/icons/multicolor/symbolic/actions/snap-alignment-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..ecb3eec51490c928c5adf232fb8e0553046f1df6 --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/snap-alignment-symbolic.svg @@ -0,0 +1,116 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/multicolor/symbolic/actions/snap-distribution-symbolic.svg b/share/icons/multicolor/symbolic/actions/snap-distribution-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..83dd3ccc0bf1c9aae52a40a997c3446fce0d0b36 --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/snap-distribution-symbolic.svg @@ -0,0 +1,155 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/share/ui/toolbar-snap.ui b/share/ui/toolbar-snap.ui index b287c8703365c8188c37a3a57febd2a4637bc719..42e6e551067d12a622963c3ace7e355661aa686e 100644 --- a/share/ui/toolbar-snap.ui +++ b/share/ui/toolbar-snap.ui @@ -212,6 +212,48 @@ + + + + + True + + + + + + True + doc.snap-alignment + snap-alignment + Alignment + + + + + + True + doc.snap-alignment-self + snap-alignment-self + Self Alignment + + + + + + + + True + + + + + + True + doc.snap-distribution + snap-distribution + Distribution + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cbb16235b7cb582c81a49edb4673fb88b735b9ba..52fe72825c6b0ace3a183a13bbbb94f423f7b8c0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,7 @@ # ----------------------------------------------------------------------------- set(inkscape_SRC + alignment-snapper.cpp attribute-rel-css.cpp attribute-rel-svg.cpp attribute-rel-util.cpp @@ -21,6 +22,7 @@ set(inkscape_SRC desktop-style.cpp desktop.cpp device-manager.cpp + distribution-snapper.cpp document-subset.cpp document-undo.cpp document.cpp @@ -88,6 +90,7 @@ set(inkscape_SRC # ------- # Headers + alignment-snapper.h attribute-rel-css.h attribute-rel-svg.h attribute-rel-util.h @@ -111,6 +114,7 @@ set(inkscape_SRC desktop-style.h desktop.h device-manager.h + distribution-snapper.h document-subset.h document-undo.h document.h diff --git a/src/actions/actions-canvas-snapping.cpp b/src/actions/actions-canvas-snapping.cpp index acef9532ed9389e1299b182a5086885cc4c1abe3..10daccbcd6b453c42c97ab18542b2c320d17a9c8 100644 --- a/src/actions/actions-canvas-snapping.cpp +++ b/src/actions/actions-canvas-snapping.cpp @@ -24,6 +24,7 @@ #include "document-undo.h" #include "object/sp-namedview.h" +#include "snap-enums.h" // There are four snapping lists that must be connected: // 1. The attribute name in NamedView: e.g. "inkscape:snap-bbox". @@ -63,6 +64,20 @@ canvas_snapping_toggle(SPDocument* document, const SPAttr option) repr->setAttributeBoolean("inkscape:snap-global", !v); break; + case SPAttr::INKSCAPE_SNAP_ALIGNMENT: + v = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_ALIGNMENT_CATEGORY); + repr->setAttributeBoolean("inkscape:snap-alignment", !v); + break; + case SPAttr::INKSCAPE_SNAP_ALIGNMENT_SELF: + v = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_ALIGNMENT_HANDLE); + repr->setAttributeBoolean("inkscape:snap-alignment-self", !v); + break; + + case SPAttr::INKSCAPE_SNAP_DISTRIBUTION: + v = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_DISTRIBUTION_CATEGORY); + repr->setAttributeBoolean("inkscape:snap-distribution", !v); + break; + // BBox case SPAttr::INKSCAPE_SNAP_BBOX: v = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_BBOX_CATEGORY); @@ -184,6 +199,11 @@ std::vector> raw_data_canvas_snapping = { {"doc.snap-global-toggle", N_("Snapping"), "Snap", N_("Toggle snapping on/off") }, + {"doc.snap-alignment", N_("Snap Objects that Align"), "Snap", N_("Toggle alignment snapping") }, + {"doc.snap-alignment-self", N_("Snap Nodes that Align"), "Snap", N_("Toggle alignment snapping to nodes in the same path")}, + + {"doc.snap-distribution", N_("Snap Objects at Equal Distances"), "Snap", N_("Toggle snapping objects at equal distances")}, + {"doc.snap-bbox", N_("Snap Bounding Boxes"), "Snap", N_("Toggle snapping to bounding boxes (global)") }, {"doc.snap-bbox-edge", N_("Snap Bounding Box Edges"), "Snap", N_("Toggle snapping to bounding-box edges") }, {"doc.snap-bbox-corner", N_("Snap Bounding Box Corners"), "Snap", N_("Toggle snapping to bounding-box corners") }, @@ -217,6 +237,11 @@ add_actions_canvas_snapping(SPDocument* document) map->add_action_bool( "snap-global-toggle", sigc::bind(sigc::ptr_fun(&canvas_snapping_toggle), document, SPAttr::INKSCAPE_SNAP_GLOBAL)); + map->add_action_bool( "snap-distribution", sigc::bind(sigc::ptr_fun(&canvas_snapping_toggle), document, SPAttr::INKSCAPE_SNAP_DISTRIBUTION)); + + map->add_action_bool( "snap-alignment", sigc::bind(sigc::ptr_fun(&canvas_snapping_toggle), document, SPAttr::INKSCAPE_SNAP_ALIGNMENT)); + map->add_action_bool( "snap-alignment-self", sigc::bind(sigc::ptr_fun(&canvas_snapping_toggle), document, SPAttr::INKSCAPE_SNAP_ALIGNMENT_SELF)); + map->add_action_bool( "snap-bbox", sigc::bind(sigc::ptr_fun(&canvas_snapping_toggle), document, SPAttr::INKSCAPE_SNAP_BBOX)); map->add_action_bool( "snap-bbox-edge", sigc::bind(sigc::ptr_fun(&canvas_snapping_toggle), document, SPAttr::INKSCAPE_SNAP_BBOX_EDGE)); map->add_action_bool( "snap-bbox-corner", sigc::bind(sigc::ptr_fun(&canvas_snapping_toggle), document, SPAttr::INKSCAPE_SNAP_BBOX_CORNER)); @@ -304,6 +329,13 @@ set_actions_canvas_snapping(SPDocument* document) bool global = nv->snap_manager.snapprefs.getSnapEnabledGlobally(); set_actions_canvas_snapping_helper(map, "snap-global-toggle", global, true); // Always enabled + bool alignment = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_ALIGNMENT_CATEGORY); + set_actions_canvas_snapping_helper(map, "snap-alignment", alignment, global); + set_actions_canvas_snapping_helper(map, "snap-alignment-self", nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_ALIGNMENT_HANDLE), global && alignment); + + bool distribution = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_DISTRIBUTION_CATEGORY); + set_actions_canvas_snapping_helper(map, "snap-distribution", distribution, global); + bool bbox = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_BBOX_CATEGORY); set_actions_canvas_snapping_helper(map, "snap-bbox", bbox, global); set_actions_canvas_snapping_helper(map, "snap-bbox-edge", nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_BBOX_EDGE), global && bbox); diff --git a/src/alignment-snapper.cpp b/src/alignment-snapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..db96327618bcf8ab204c5cabc8f36757c2a408f4 --- /dev/null +++ b/src/alignment-snapper.cpp @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Snapping things to on-canvas alignment guides. + * + * Authors: + * Parth Pant + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/circle.h> +#include <2geom/line.h> +#include <2geom/path-intersection.h> +#include <2geom/path-sink.h> +#include + +#include "desktop.h" +#include "display/curve.h" +#include "document.h" +#include "inkscape.h" +#include "live_effects/effect-enum.h" +#include "object/sp-clippath.h" +#include "object/sp-flowtext.h" +#include "object/sp-image.h" +#include "object/sp-item-group.h" +#include "object/sp-mask.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-use.h" +#include "path/path-util.h" // curve_for_item +#include "preferences.h" +#include "snap-enums.h" +#include "style.h" +#include "svg/svg.h" +#include "text-editing.h" + +Inkscape::AlignmentSnapper::AlignmentSnapper(SnapManager *sm, Geom::Coord const d) + : Snapper(sm, d) +{ + _points_to_snap_to = std::make_unique>(); +} + +Inkscape::AlignmentSnapper::~AlignmentSnapper() +{ + _points_to_snap_to->clear(); +} + +void Inkscape::AlignmentSnapper::_collectBBoxPoints(bool const &first_point) const +{ + if (!first_point) + return; + + _points_to_snap_to->clear(); + SPItem::BBoxType bbox_type = SPItem::GEOMETRIC_BBOX; + + Preferences *prefs = Preferences::get(); + bool prefs_bbox = prefs->getBool("/tools/bounding_box"); + bbox_type = !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + + // collect page corners and center + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PAGE_CORNER)) { + Geom::Coord w = (_snapmanager->getDocument())->getWidth().value("px"); + Geom::Coord h = (_snapmanager->getDocument())->getHeight().value("px"); + _points_to_snap_to->push_back(SnapCandidatePoint(Geom::Point(0,0), SNAPSOURCE_ALIGNMENT_PAGE_CORNER, SNAPTARGET_ALIGNMENT_PAGE_CORNER)); + _points_to_snap_to->push_back(SnapCandidatePoint(Geom::Point(0,h), SNAPSOURCE_ALIGNMENT_PAGE_CORNER, SNAPTARGET_ALIGNMENT_PAGE_CORNER)); + _points_to_snap_to->push_back(SnapCandidatePoint(Geom::Point(w,h), SNAPSOURCE_ALIGNMENT_PAGE_CORNER, SNAPTARGET_ALIGNMENT_PAGE_CORNER)); + _points_to_snap_to->push_back(SnapCandidatePoint(Geom::Point(w,0), SNAPSOURCE_ALIGNMENT_PAGE_CORNER, SNAPTARGET_ALIGNMENT_PAGE_CORNER)); + _points_to_snap_to->push_back(SnapCandidatePoint(Geom::Point(w/2.0f,h/2.0f), SNAPSOURCE_ALIGNMENT_PAGE_CENTER, SNAPTARGET_ALIGNMENT_PAGE_CENTER)); + } + + // collect bounding boxes of other objects + for (const auto & candidate : *(_snapmanager->align_snapper_candidates)) { + SPItem *root_item = candidate.item; + + // get the root item in case we have a duplicate at hand + SPUse *use = dynamic_cast(candidate.item); + if (use) { + root_item = use->root(); + } + g_return_if_fail(root_item); + + // if candidate is not a clip or a mask object then extract its BBox points + if (!candidate.clip_or_mask) { + Geom::OptRect b = root_item->desktopBounds(bbox_type); + getBBoxPoints(b, _points_to_snap_to.get(), true, true, false, true, true); + } + } + + // Debug log + //std::cout<<"----------"< *unselected_nodes, + SnapConstraint const &c, + Geom::Point const &p_proj_on_constraint) const +{ + + _collectBBoxPoints(p.getSourceNum() <= 0); + + if (unselected_nodes != nullptr && + unselected_nodes->size() > 0 && + _snapmanager->snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_ALIGNMENT_HANDLE)) { + g_assert(_points_to_snap_to != nullptr); + _points_to_snap_to->insert(_points_to_snap_to->end(), unselected_nodes->begin(), unselected_nodes->end()); + } + + SnappedPoint sx; + SnappedPoint sy; + SnappedPoint si; + + bool consider_x = true; + bool consider_y = true; + bool success_x = false; + bool success_y = false; + bool intersection = false; + //bool strict_snapping = _snapmanager->snapprefs.getStrictSnapping(); + + for (const auto & k : *_points_to_snap_to) { + // TODO: add strict snapping checks from ObjectSnapper::_allowSourceToSnapToTarget(...) + if (true) { + Geom::Point target_pt = k.getPoint(); + // (unconstrained) distance from HORIZONTAL guide + Geom::Point point_on_x(p.getPoint().x(), target_pt.y()); + Geom::Coord distX = Geom::L2(point_on_x - p.getPoint()); + + // (unconstrained) distance from VERTICAL guide + Geom::Point point_on_y(target_pt.x(), p.getPoint().y()); + Geom::Coord distY = Geom::L2(point_on_y - p.getPoint()); + + if (!c.isUndefined() && c.isLinear()) { + if (c.getDirection().x() == 0) + consider_y = false; // consider vertical snapping if moving vertically + else + consider_x = false; // consider horizontal snapping if moving horizontally + } + + bool is_target_node = k.getTargetType() & SNAPTARGET_NODE_CATEGORY; + if (consider_x && distX < getSnapperTolerance() && Geom::L2(target_pt - point_on_x) < sx.getDistanceToAlignTarget()) { + sx = SnappedPoint(point_on_x, + k.getPoint(), + source2alignment(p.getSourceType()), + p.getSourceNum(), + is_target_node ? SNAPTARGET_ALIGNMENT_HANDLE : k.getTargetType(), + distX, + getSnapperTolerance(), + getSnapperAlwaysSnap(), + false, + true, + k.getTargetBBox()); + success_x = true; + } + + if (consider_y && distY < getSnapperTolerance() && Geom::L2(target_pt - point_on_y) < sy.getDistanceToAlignTarget()) { + sy = SnappedPoint(point_on_y, + k.getPoint(), + source2alignment(p.getSourceType()), + p.getSourceNum(), + is_target_node ? SNAPTARGET_ALIGNMENT_HANDLE : k.getTargetType(), + distY, + getSnapperTolerance(), + getSnapperAlwaysSnap(), + false, + true, + k.getTargetBBox()); + success_y = true; + } + + if (consider_x && consider_y && success_x && success_y) { + Geom::Point intersection_p = Geom::Point(sy.getPoint().x(), sx.getPoint().y()); + Geom::Coord d = Geom::L2(intersection_p - p.getPoint()); + + if (d < sqrt(2)*getSnapperTolerance()) { + si = SnappedPoint(intersection_p, + *sy.getAlignmentTarget(), + *sx.getAlignmentTarget(), + source2alignment(p.getSourceType()), + p.getSourceNum(), + SNAPTARGET_ALIGNMENT_INTERSECTION, + d, + getSnapperTolerance(), + getSnapperAlwaysSnap(), + false, + true, + k.getTargetBBox()); + intersection = true; + } + } + } + } + + if (intersection) { + isr.points.push_back(si); + return; + } + + if (success_x || success_y) { + if (sx.getSnapDistance() < sy.getSnapDistance()) { + isr.points.push_back(sx); + } else { + isr.points.push_back(sy); + } + } + +} + +void Inkscape::AlignmentSnapper::freeSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector const *it, + std::vector *unselected_nodes) const +{ + bool p_is_bbox = p.getSourceType() & SNAPSOURCE_BBOX_CATEGORY; + bool p_is_node = p.getSourceType() & SNAPSOURCE_NODE_HANDLE; + + // toggle checks + if (!_snap_enabled || !_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_ALIGNMENT_CATEGORY)) + return; + + unsigned n = (unselected_nodes == nullptr) ? 0 : unselected_nodes->size(); + + // n > 0 : node tool is active + if (!(p_is_bbox || (n > 0 && p_is_node) || (p.considerForAlignment() && p_is_node))) + return; + + _snapBBoxPoints(isr, p, unselected_nodes); +} + +void Inkscape::AlignmentSnapper::constrainedSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + SnapConstraint const &c, + std::vector const *it, + std::vector *unselected_nodes) const +{ + bool p_is_bbox = p.getSourceType() & SNAPSOURCE_BBOX_CATEGORY; + bool p_is_node = p.getSourceType() & SNAPSOURCE_NODE_HANDLE; + + // project the mouse pointer onto the constraint. Only the projected point will be considered for snapping + Geom::Point pp = c.projection(p.getPoint()); + + // toggle checks + if (!_snap_enabled || !_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_ALIGNMENT_CATEGORY)) + return; + + unsigned n = (unselected_nodes == nullptr) ? 0 : unselected_nodes->size(); + + // n > 0 : node tool is active + if (!(p_is_bbox || (n > 0 && p_is_node) || (p.considerForAlignment() && p_is_node))) + return; + + _snapBBoxPoints(isr, p, unselected_nodes, c, pp); +} + +bool Inkscape::AlignmentSnapper::ThisSnapperMightSnap() const +{ + return true; +} + +bool Inkscape::AlignmentSnapper::getSnapperAlwaysSnap() const +{ + return _snapmanager->snapprefs.getAlignmentTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp +} + +Geom::Coord Inkscape::AlignmentSnapper::getSnapperTolerance() const +{ + SPDesktop const *dt = _snapmanager->getDesktop(); + double const zoom = dt ? dt->current_zoom() : 1; + return _snapmanager->snapprefs.getAlignmentTolerance() / zoom; +} + +Inkscape::SnapSourceType Inkscape::AlignmentSnapper::source2alignment(SnapSourceType s) const +{ + switch (s) { + case SNAPSOURCE_BBOX_CATEGORY: + return SNAPSOURCE_ALIGNMENT_CATEGORY; + case SNAPSOURCE_BBOX_CORNER: + return SNAPSOURCE_ALIGNMENT_BBOX_CORNER; + case SNAPSOURCE_BBOX_MIDPOINT: + return SNAPSOURCE_ALIGNMENT_BBOX_MIDPOINT; + case SNAPSOURCE_BBOX_EDGE_MIDPOINT: + return SNAPSOURCE_ALIGNMENT_BBOX_EDGE_MIDPOINT; + case SNAPSOURCE_NODE_CATEGORY: + case SNAPSOURCE_OTHER_HANDLE: + return SNAPSOURCE_ALIGNMENT_HANDLE; + default: + return SNAPSOURCE_UNDEFINED; + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + + + diff --git a/src/alignment-snapper.h b/src/alignment-snapper.h new file mode 100644 index 0000000000000000000000000000000000000000..30d3de515dabb8a994093de067c5b9b1ba55d40b --- /dev/null +++ b/src/alignment-snapper.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Authors: + * Parth Pant + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_ALIGNMENT_SNAPPER_H +#define SEEN_ALIGNMENT_SNAPPER_H + +#include <2geom/affine.h> +#include + +#include "snap-enums.h" +#include "snapper.h" +#include "snap-candidate.h" + +class SPDesktop; +class SPNamedView; +class SPItem; +class SPObject; +class SPPath; +class SPDesktop; + +namespace Inkscape +{ + +/** + * Snapping things to on-canvas alignment guides + */ +class AlignmentSnapper : public Snapper +{ + +public: + AlignmentSnapper(SnapManager *sm, Geom::Coord const d); + ~AlignmentSnapper() override; + + /** + * @return true if this Snapper will snap at least one kind of point. + */ + bool ThisSnapperMightSnap() const override; + + /** + * @return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels. + */ + Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom) + + bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance + + void freeSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector const *it, + std::vector *unselected_nodes) const override; + + void constrainedSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + SnapConstraint const &c, + std::vector const *it, + std::vector *unselected_nodes) const override; + +private: + std::unique_ptr> _points_to_snap_to; + + /** Collects and caches points on bounding boxes of the candidates + * @param is the point first point in the selection? + */ + void _collectBBoxPoints(bool const &first_point) const; + + void _snapBBoxPoints(IntermSnapResults &isr, + SnapCandidatePoint const &p, + std::vector *unselected_nodes, + SnapConstraint const &c = SnapConstraint(), + Geom::Point const &p_proj_on_constraint = Geom::Point()) const; + + SnapSourceType source2alignment(SnapSourceType s) const; +}; // end of AlignmentSnapper class + +} // end of namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/attributes.cpp b/src/attributes.cpp index 021205c2323300ee2d8da7de78331b8548677d59..f960d3d7a1548992d913d0e36f613b942c42a453 100644 --- a/src/attributes.cpp +++ b/src/attributes.cpp @@ -71,6 +71,8 @@ static SPStyleProp const props[] = { {SPAttr::GRIDTOLERANCE, "gridtolerance"}, {SPAttr::GUIDETOLERANCE, "guidetolerance"}, {SPAttr::OBJECTTOLERANCE, "objecttolerance"}, + {SPAttr::ALIGNMENTTOLERANCE, "alignmenttolerance"}, + {SPAttr::DISTRIBUTIONTOLERANCE, "distributiontolerance"}, {SPAttr::GUIDECOLOR, "guidecolor"}, {SPAttr::GUIDEOPACITY, "guideopacity"}, {SPAttr::GUIDEHICOLOR, "guidehicolor"}, @@ -98,6 +100,9 @@ static SPStyleProp const props[] = { {SPAttr::INKSCAPE_WINDOW_Y, "inkscape:window-y"}, {SPAttr::INKSCAPE_WINDOW_MAXIMIZED, "inkscape:window-maximized"}, {SPAttr::INKSCAPE_SNAP_GLOBAL, "inkscape:snap-global"}, + {SPAttr::INKSCAPE_SNAP_DISTRIBUTION, "inkscape:snap-distribution"}, + {SPAttr::INKSCAPE_SNAP_ALIGNMENT, "inkscape:snap-alignment"}, + {SPAttr::INKSCAPE_SNAP_ALIGNMENT_SELF, "inkscape:snap-alignment-self"}, {SPAttr::INKSCAPE_SNAP_PERP, "inkscape:snap-perpendicular"}, {SPAttr::INKSCAPE_SNAP_TANG, "inkscape:snap-tangential"}, {SPAttr::INKSCAPE_SNAP_BBOX, "inkscape:snap-bbox"}, diff --git a/src/attributes.h b/src/attributes.h index 2dc50d4c307f8fbd624e8b2279b9fe6a37d4f725..7eaea5fcdfc21de2124799b28664e0261e15b62b 100644 --- a/src/attributes.h +++ b/src/attributes.h @@ -70,6 +70,8 @@ enum class SPAttr { GRIDTOLERANCE, GUIDETOLERANCE, OBJECTTOLERANCE, + ALIGNMENTTOLERANCE, + DISTRIBUTIONTOLERANCE, GUIDECOLOR, GUIDEOPACITY, GUIDEHICOLOR, @@ -97,6 +99,9 @@ enum class SPAttr { INKSCAPE_WINDOW_Y, INKSCAPE_WINDOW_MAXIMIZED, INKSCAPE_SNAP_GLOBAL, + INKSCAPE_SNAP_DISTRIBUTION, + INKSCAPE_SNAP_ALIGNMENT, + INKSCAPE_SNAP_ALIGNMENT_SELF, INKSCAPE_SNAP_PERP, INKSCAPE_SNAP_TANG, INKSCAPE_SNAP_BBOX, diff --git a/src/context-fns.cpp b/src/context-fns.cpp index 974396ff118fba66d3d2f99abf6b63ae4288b696..edfc62d258e2bcf71a3f72c9ae620464a909be0f 100644 --- a/src/context-fns.cpp +++ b/src/context-fns.cpp @@ -89,6 +89,7 @@ bool Inkscape::have_viable_layer(SPDesktop *desktop, MessageStack *message) Geom::Rect Inkscape::snap_rectangular_box(SPDesktop const *desktop, SPItem *item, Geom::Point const &pt, Geom::Point const ¢er, int state) { + desktop->snapindicator->remove_snaptarget(); Geom::Point p[2]; auto confine = Modifiers::Modifier::get(Modifiers::Type::TRANS_CONFINE)->active(state); diff --git a/src/display/control/canvas-item-curve.cpp b/src/display/control/canvas-item-curve.cpp index 74cce44f85ff142b1409b09e924de385e6f8575c..e8ff9d325b89b16056faed972cbff533c7279d0b 100644 --- a/src/display/control/canvas-item-curve.cpp +++ b/src/display/control/canvas-item-curve.cpp @@ -84,6 +84,26 @@ void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1, G request_update(); } +/** + * Set stroke width. + */ +void CanvasItemCurve::set_width(int w) +{ + width = w; + + request_update(); +} + +/** + * Set background stroke alpha. + */ +void CanvasItemCurve::set_bg_alpha(float alpha) +{ + bg_alpha = alpha; + + request_update(); +} + /** * Returns distance between point in canvas units and nearest point on curve. */ @@ -182,13 +202,13 @@ void CanvasItemCurve::render(Inkscape::CanvasItemBuffer *buf) buf->cr->curve_to(curve[1].x(), curve[1].y(), curve[2].x(), curve[2].y(), curve[3].x(), curve[3].y()); } - buf->cr->set_source_rgba(1.0, 1.0, 1.0, 0.5); - buf->cr->set_line_width(2); + buf->cr->set_source_rgba(1.0, 1.0, 1.0, bg_alpha); + buf->cr->set_line_width(background_width); buf->cr->stroke_preserve(); buf->cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); - buf->cr->set_line_width(1); + buf->cr->set_line_width(width); buf->cr->stroke(); // Uncomment to show bounds diff --git a/src/display/control/canvas-item-curve.h b/src/display/control/canvas-item-curve.h index 37129aad2500b89146802c0a49fc36827ee26325..84d6511b4a25fbd1a3fc9a3e147f92c636ad730e 100644 --- a/src/display/control/canvas-item-curve.h +++ b/src/display/control/canvas-item-curve.h @@ -38,6 +38,8 @@ public: void set_coords(Geom::Point const &p0, Geom::Point const &p1); void set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3); void set(Geom::BezierCurve &curve); + void set_width(int w); + void set_bg_alpha(float alpha); bool is_line() { return _curve->size() == 2; } void update(Geom::Affine const &affine) override; @@ -58,10 +60,12 @@ public: int get_corner1() { return _corner1; } protected: - std::unique_ptr _curve; - + std::unique_ptr _curve; bool _is_fill = true; // Fill or stroke, used by meshes. + int width = 1; + int background_width = 3; // this should be an odd number so that the background appears on both the sides of the curve. + float bg_alpha = 0.5f; int _corner0 = -1; // For meshes int _corner1 = -1; // For meshes }; diff --git a/src/display/control/canvas-item-enums.h b/src/display/control/canvas-item-enums.h index baad84f5343b076a772d167e8c5de6b1b47c6ee6..0fddde0b0cd0ec1cc3fd93e7a2d2103127d9881c 100644 --- a/src/display/control/canvas-item-enums.h +++ b/src/display/control/canvas-item-enums.h @@ -36,7 +36,8 @@ enum CanvasItemCtrlShape { CANVAS_ITEM_CTRL_SHAPE_CALIGN, // Corner alignment. CANVAS_ITEM_CTRL_SHAPE_MALIGN, // Center (middle) alignment. CANVAS_ITEM_CTRL_SHAPE_BITMAP, - CANVAS_ITEM_CTRL_SHAPE_IMAGE + CANVAS_ITEM_CTRL_SHAPE_IMAGE, + CANVAS_ITEM_CTRL_SHAPE_LINE }; // Applies to control points. diff --git a/src/display/control/canvas-temporary-item-list.cpp b/src/display/control/canvas-temporary-item-list.cpp index b293e861d643c79e68d001d1858a70899f4986cf..4be97f994cca1c70e4153f5d8c28a2fcfb8ffec9 100644 --- a/src/display/control/canvas-temporary-item-list.cpp +++ b/src/display/control/canvas-temporary-item-list.cpp @@ -37,9 +37,15 @@ TemporaryItem * TemporaryItemList::add_item(CanvasItem *item, unsigned int lifetime) { // beware of strange things happening due to very short timeouts - TemporaryItem * tempitem = new TemporaryItem(item, lifetime); + TemporaryItem * tempitem; + if (lifetime == 0) + tempitem = new TemporaryItem(item, 0, true); + else { + tempitem = new TemporaryItem(item, lifetime); + tempitem->signal_timeout.connect( sigc::mem_fun(*this, &TemporaryItemList::_item_timeout) ); + } + itemlist.push_back(tempitem); - tempitem->signal_timeout.connect( sigc::mem_fun(*this, &TemporaryItemList::_item_timeout) ); return tempitem; } diff --git a/src/display/control/snap-indicator.cpp b/src/display/control/snap-indicator.cpp index 4e6adfbc780c089d3d06935609061cc1bb5e4c31..132f094b102173ad9ba88db3665f3cd2f9aba43f 100644 --- a/src/display/control/snap-indicator.cpp +++ b/src/display/control/snap-indicator.cpp @@ -13,23 +13,88 @@ */ #include +#include +#include #include "snap-indicator.h" #include "desktop.h" #include "enums.h" #include "preferences.h" +#include "util/units.h" +#include "document.h" #include "canvas-item-ctrl.h" #include "canvas-item-rect.h" #include "canvas-item-text.h" +#include "canvas-item-curve.h" #include "ui/tools/measure-tool.h" namespace Inkscape { namespace Display { +std::unordered_map SnapIndicator::source2string = { + {SNAPSOURCE_UNDEFINED, _("UNDEFINED")}, + {SNAPSOURCE_BBOX_CORNER, _("Bounding box corner")}, + {SNAPSOURCE_BBOX_MIDPOINT, _("Bounding box midpoint")}, + {SNAPSOURCE_BBOX_EDGE_MIDPOINT, _("Bounding box side midpoint")}, + {SNAPSOURCE_NODE_SMOOTH, _("Smooth node")}, + {SNAPSOURCE_NODE_CUSP, _("Cusp node")}, + {SNAPSOURCE_LINE_MIDPOINT, _("Line midpoint")}, + {SNAPSOURCE_PATH_INTERSECTION, _("Path intersection")}, + {SNAPSOURCE_RECT_CORNER, _("Corner")}, + {SNAPSOURCE_CONVEX_HULL_CORNER, _("Convex hull corner")}, + {SNAPSOURCE_ELLIPSE_QUADRANT_POINT, _("Quadrant point")}, + {SNAPSOURCE_NODE_HANDLE, _("Handle")}, + {SNAPSOURCE_GUIDE, _("Guide")}, + {SNAPSOURCE_GUIDE_ORIGIN, _("Guide origin")}, + {SNAPSOURCE_ROTATION_CENTER, _("Object rotation center")}, + {SNAPSOURCE_OBJECT_MIDPOINT, _("Object midpoint")}, + {SNAPSOURCE_IMG_CORNER, _("Corner")}, + {SNAPSOURCE_TEXT_ANCHOR, _("Text anchor")}, + {SNAPSOURCE_OTHER_HANDLE, _("Handle")}, + {SNAPSOURCE_GRID_PITCH, _("Multiple of grid spacing")}, +}; + +std::unordered_map SnapIndicator::target2string = { + {SNAPTARGET_UNDEFINED, _("UNDEFINED")}, + {SNAPTARGET_BBOX_CORNER, _("bounding box corner")}, + {SNAPTARGET_BBOX_EDGE, _("bounding box side")}, + {SNAPTARGET_BBOX_EDGE_MIDPOINT, _("bounding box side midpoint")}, + {SNAPTARGET_BBOX_MIDPOINT, _("bounding box midpoint")}, + {SNAPTARGET_NODE_SMOOTH, _("smooth node")}, + {SNAPTARGET_NODE_CUSP, _("cusp node")}, + {SNAPTARGET_LINE_MIDPOINT, _("line midpoint")}, + {SNAPTARGET_PATH, _("path")}, + {SNAPTARGET_PATH_PERPENDICULAR, _("path (perpendicular)")}, + {SNAPTARGET_PATH_TANGENTIAL, _("path (tangential)")}, + {SNAPTARGET_PATH_INTERSECTION, _("path intersection")}, + {SNAPTARGET_PATH_GUIDE_INTERSECTION, _("guide-path intersection")}, + {SNAPTARGET_PATH_CLIP, _("clip-path")}, + {SNAPTARGET_PATH_MASK, _("mask-path")}, + {SNAPTARGET_ELLIPSE_QUADRANT_POINT, _("quadrant point")}, + {SNAPTARGET_RECT_CORNER, _("corner")}, + {SNAPTARGET_GRID, _("grid line")}, + {SNAPTARGET_GRID_INTERSECTION, _("grid intersection")}, + {SNAPTARGET_GRID_PERPENDICULAR, _("grid line (perpendicular)")}, + {SNAPTARGET_GUIDE, _("guide")}, + {SNAPTARGET_GUIDE_INTERSECTION, _("guide intersection")}, + {SNAPTARGET_GUIDE_ORIGIN, _("guide origin")}, + {SNAPTARGET_GUIDE_PERPENDICULAR, _("guide (perpendicular)")}, + {SNAPTARGET_GRID_GUIDE_INTERSECTION, _("grid-guide intersection")}, + {SNAPTARGET_PAGE_BORDER, _("page border")}, + {SNAPTARGET_PAGE_CORNER, _("page corner")}, + {SNAPTARGET_OBJECT_MIDPOINT, _("object midpoint")}, + {SNAPTARGET_IMG_CORNER, _("corner")}, + {SNAPTARGET_ROTATION_CENTER, _("object rotation center")}, + {SNAPTARGET_TEXT_ANCHOR, _("text anchor")}, + {SNAPTARGET_TEXT_BASELINE, _("text baseline")}, + {SNAPTARGET_CONSTRAINED_ANGLE, _("constrained angle")}, + {SNAPTARGET_CONSTRAINT, _("constraint")}, +}; + SnapIndicator::SnapIndicator(SPDesktop * desktop) : _snaptarget(nullptr), _snaptarget_tooltip(nullptr), @@ -66,250 +131,116 @@ SnapIndicator::set_new_snaptarget(Inkscape::SnappedPoint const &p, bool pre_snap return; } + bool is_alignment = p.getAlignmentTarget().has_value(); + bool is_distribution = p.getTarget() & SNAPTARGET_DISTRIBUTION_CATEGORY; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0; + bool value = prefs->getBool("/options/snapindicator/value", true); if (value) { - // TRANSLATORS: undefined target for snapping Glib::ustring target_name = _("UNDEFINED"); - switch (p.getTarget()) { - case SNAPTARGET_UNDEFINED: - target_name = _("UNDEFINED"); - g_warning("Snap target has not been specified"); - break; - case SNAPTARGET_GRID: - target_name = _("grid line"); - break; - case SNAPTARGET_GRID_INTERSECTION: - target_name = _("grid intersection"); - break; - case SNAPTARGET_GRID_PERPENDICULAR: - target_name = _("grid line (perpendicular)"); - break; - case SNAPTARGET_GUIDE: - target_name = _("guide"); - break; - case SNAPTARGET_GUIDE_INTERSECTION: - target_name = _("guide intersection"); - break; - case SNAPTARGET_GUIDE_ORIGIN: - target_name = _("guide origin"); - break; - case SNAPTARGET_GUIDE_PERPENDICULAR: - target_name = _("guide (perpendicular)"); - break; - case SNAPTARGET_GRID_GUIDE_INTERSECTION: - target_name = _("grid-guide intersection"); - break; - case SNAPTARGET_NODE_CUSP: - target_name = _("cusp node"); - break; - case SNAPTARGET_NODE_SMOOTH: - target_name = _("smooth node"); - break; - case SNAPTARGET_PATH: - target_name = _("path"); - break; - case SNAPTARGET_PATH_PERPENDICULAR: - target_name = _("path (perpendicular)"); - break; - case SNAPTARGET_PATH_TANGENTIAL: - target_name = _("path (tangential)"); - break; - case SNAPTARGET_PATH_INTERSECTION: - target_name = _("path intersection"); - break; - case SNAPTARGET_PATH_GUIDE_INTERSECTION: - target_name = _("guide-path intersection"); - break; - case SNAPTARGET_PATH_CLIP: - target_name = _("clip-path"); - break; - case SNAPTARGET_PATH_MASK: - target_name = _("mask-path"); - break; - case SNAPTARGET_BBOX_CORNER: - target_name = _("bounding box corner"); - break; - case SNAPTARGET_BBOX_EDGE: - target_name = _("bounding box side"); - break; - case SNAPTARGET_PAGE_BORDER: - target_name = _("page border"); - break; - case SNAPTARGET_LINE_MIDPOINT: - target_name = _("line midpoint"); - break; - case SNAPTARGET_OBJECT_MIDPOINT: - target_name = _("object midpoint"); - break; - case SNAPTARGET_ROTATION_CENTER: - target_name = _("object rotation center"); - break; - case SNAPTARGET_BBOX_EDGE_MIDPOINT: - target_name = _("bounding box side midpoint"); - break; - case SNAPTARGET_BBOX_MIDPOINT: - target_name = _("bounding box midpoint"); - break; - case SNAPTARGET_PAGE_CORNER: - target_name = _("page corner"); - break; - case SNAPTARGET_ELLIPSE_QUADRANT_POINT: - target_name = _("quadrant point"); - break; - case SNAPTARGET_RECT_CORNER: - case SNAPTARGET_IMG_CORNER: - target_name = _("corner"); - break; - case SNAPTARGET_TEXT_ANCHOR: - target_name = _("text anchor"); - break; - case SNAPTARGET_TEXT_BASELINE: - target_name = _("text baseline"); - break; - case SNAPTARGET_CONSTRAINED_ANGLE: - target_name = _("constrained angle"); - break; - case SNAPTARGET_CONSTRAINT: - target_name = _("constraint"); - break; - default: - g_warning("Snap target not in SnapTargetType enum"); - break; - } - Glib::ustring source_name = _("UNDEFINED"); - switch (p.getSource()) { - case SNAPSOURCE_UNDEFINED: - source_name = _("UNDEFINED"); - g_warning("Snap source has not been specified"); - break; - case SNAPSOURCE_BBOX_CORNER: - source_name = _("Bounding box corner"); - break; - case SNAPSOURCE_BBOX_MIDPOINT: - source_name = _("Bounding box midpoint"); - break; - case SNAPSOURCE_BBOX_EDGE_MIDPOINT: - source_name = _("Bounding box side midpoint"); - break; - case SNAPSOURCE_NODE_SMOOTH: - source_name = _("Smooth node"); - break; - case SNAPSOURCE_NODE_CUSP: - source_name = _("Cusp node"); - break; - case SNAPSOURCE_LINE_MIDPOINT: - source_name = _("Line midpoint"); - break; - case SNAPSOURCE_OBJECT_MIDPOINT: - source_name = _("Object midpoint"); - break; - case SNAPSOURCE_ROTATION_CENTER: - source_name = _("Object rotation center"); - break; - case SNAPSOURCE_NODE_HANDLE: - case SNAPSOURCE_OTHER_HANDLE: - source_name = _("Handle"); - break; - case SNAPSOURCE_PATH_INTERSECTION: - source_name = _("Path intersection"); - break; - case SNAPSOURCE_GUIDE: - source_name = _("Guide"); - break; - case SNAPSOURCE_GUIDE_ORIGIN: - source_name = _("Guide origin"); - break; - case SNAPSOURCE_CONVEX_HULL_CORNER: - source_name = _("Convex hull corner"); - break; - case SNAPSOURCE_ELLIPSE_QUADRANT_POINT: - source_name = _("Quadrant point"); - break; - case SNAPSOURCE_RECT_CORNER: - case SNAPSOURCE_IMG_CORNER: - source_name = _("Corner"); - break; - case SNAPSOURCE_TEXT_ANCHOR: - source_name = _("Text anchor"); - break; - case SNAPSOURCE_GRID_PITCH: - source_name = _("Multiple of grid spacing"); - break; - default: - g_warning("Snap source not in SnapSourceType enum"); - break; + + if (!is_alignment && !is_distribution) { + if (target2string.find(p.getTarget()) == target2string.end()) + g_warning("Target type %i not present in target2string", p.getTarget()); + + if (source2string.find(p.getSource()) == source2string.end()) + g_warning("Source type %i not present in target2string", p.getSource()); + + target_name = target2string[p.getTarget()]; + source_name = source2string[p.getSource()]; } //std::cout << "Snapped " << source_name << " to " << target_name << std::endl; remove_snapsource(); // Don't set both the source and target indicators, as these will overlap - // Display the snap indicator (i.e. the cross) - auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CROSS); - ctrl->set_size(11); - ctrl->set_stroke( pre_snap ? 0x7f7f7fff : 0xff0000ff); - ctrl->set_position(p.getPoint()); - - // The snap indicator will be deleted after some time-out, and sp_canvas_item_dispose - // will be called. This will set canvas->current_item to NULL if the snap indicator was - // the current item, after which any events will go to the root handler instead of any - // item handler. Dragging an object which has just snapped might therefore not be possible - // without selecting / repicking it again. To avoid this, we make sure here that the - // snap indicator will never be picked, and will therefore never be the current item. - // Reported bugs: - // - scrolling when hovering above a pre-snap indicator won't work (for example) - // (https://bugs.launchpad.net/inkscape/+bug/522335/comments/8) - // - dragging doesn't work without repicking - // (https://bugs.launchpad.net/inkscape/+bug/1420301/comments/15) - ctrl->set_pickable(false); - double timeout_val = prefs->getDouble("/options/snapindicatorpersistence/value", 2.0); if (timeout_val < 0.1) { timeout_val = 0.1; // a zero value would mean infinite persistence (i.e. until new snap occurs) // Besides, negatives values would ....? } - _snaptarget = _desktop->add_temporary_canvasitem(ctrl, timeout_val*1000.0); - _snaptarget_is_presnap = pre_snap; + // TODO: should this be a constant or a separate prefrence + // we are using the preference of measure tool here. + double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); - // Display the tooltip, which reveals the type of snap source and the type of snap target - Glib::ustring tooltip_str; - if ( (p.getSource() != SNAPSOURCE_GRID_PITCH) && (p.getTarget() != SNAPTARGET_UNDEFINED) ) { - tooltip_str = source_name + _(" to ") + target_name; - } else if (p.getSource() != SNAPSOURCE_UNDEFINED) { - tooltip_str = source_name; + if (is_distribution) { + make_distribution_indicators(p, fontsize, scale); } - double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); + if (is_alignment) { + auto color = pre_snap ? 0x7f7f7fff : get_guide_color(p.getAlignmentTargetType()); + make_alignment_indicator(p.getPoint(), *p.getAlignmentTarget(), color, fontsize, scale); + if (p.getAlignmentTargetType() == SNAPTARGET_ALIGNMENT_INTERSECTION) { + make_alignment_indicator(p.getPoint(), *p.getAlignmentTarget2(), color, fontsize, scale); + } + } - if (!tooltip_str.empty()) { - Geom::Point tooltip_pos = p.getPoint(); - if (dynamic_cast(_desktop->event_context)) { - // Make sure that the snap tooltips do not overlap the ones from the measure tool - tooltip_pos += _desktop->w2d(Geom::Point(0, -3*fontsize)); - } else { - tooltip_pos += _desktop->w2d(Geom::Point(0, -2*fontsize)); + _snaptarget_is_presnap = pre_snap; + + // Display the snap indicator (i.e. the cross) + Inkscape::CanvasItemCtrl *ctrl; + + if (!is_alignment && !is_distribution) { + // Display snap indicator at snap target + ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CROSS); + ctrl->set_size(11); + ctrl->set_stroke( pre_snap ? 0x7f7f7fff : 0xff0000ff); + ctrl->set_position(p.getPoint()); + + _snaptarget = _desktop->add_temporary_canvasitem(ctrl, timeout_val*1000.0); + // The snap indicator will be deleted after some time-out, and sp_canvas_item_dispose + // will be called. This will set canvas->current_item to NULL if the snap indicator was + // the current item, after which any events will go to the root handler instead of any + // item handler. Dragging an object which has just snapped might therefore not be possible + // without selecting / repicking it again. To avoid this, we make sure here that the + // snap indicator will never be picked, and will therefore never be the current item. + // Reported bugs: + // - scrolling when hovering above a pre-snap indicator won't work (for example) + // (https://bugs.launchpad.net/inkscape/+bug/522335/comments/8) + // - dragging doesn't work without repicking + // (https://bugs.launchpad.net/inkscape/+bug/1420301/comments/15) + ctrl->set_pickable(false); + + // Display the tooltip, which reveals the type of snap source and the type of snap target + Glib::ustring tooltip_str; + if ( (p.getSource() != SNAPSOURCE_GRID_PITCH) && (p.getTarget() != SNAPTARGET_UNDEFINED) ) { + tooltip_str = source_name + _(" to ") + target_name; + } else if (p.getSource() != SNAPSOURCE_UNDEFINED) { + tooltip_str = source_name; } - auto canvas_tooltip = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), tooltip_pos, tooltip_str); - canvas_tooltip->set_fontsize(fontsize); - canvas_tooltip->set_fill(0xffffffff); - canvas_tooltip->set_background(pre_snap ? 0x33337f40 : 0x33337f7f); - _snaptarget_tooltip = _desktop->add_temporary_canvasitem(canvas_tooltip, timeout_val*1000.0); - } + if (!tooltip_str.empty()) { + Geom::Point tooltip_pos = p.getPoint(); + if (dynamic_cast(_desktop->event_context)) { + // Make sure that the snap tooltips do not overlap the ones from the measure tool + tooltip_pos += _desktop->w2d(Geom::Point(0, -3*fontsize)); + } else { + tooltip_pos += _desktop->w2d(Geom::Point(0, -2*fontsize)); + } + + auto canvas_tooltip = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), tooltip_pos, tooltip_str); + canvas_tooltip->set_fontsize(fontsize); + canvas_tooltip->set_fill(0xffffffff); + canvas_tooltip->set_background(pre_snap ? 0x33337f40 : 0x33337f7f); - // Display the bounding box, if we snapped to one - Geom::OptRect const bbox = p.getTargetBBox(); - if (bbox) { - auto box = new Inkscape::CanvasItemRect(_desktop->getCanvasTemp(), *bbox); - box->set_stroke(pre_snap ? 0x7f7f7fff : 0xff0000ff); - box->set_dashed(true); - box->set_pickable(false); // Is false by default. - box->set_z_position(0); - _snaptarget_bbox = _desktop->add_temporary_canvasitem(box, timeout_val*1000.0); + _snaptarget_tooltip = _desktop->add_temporary_canvasitem(canvas_tooltip, timeout_val*1000.0); + } + + // Display the bounding box, if we snapped to one + Geom::OptRect const bbox = p.getTargetBBox(); + if (bbox) { + auto box = new Inkscape::CanvasItemRect(_desktop->getCanvasTemp(), *bbox); + box->set_stroke(pre_snap ? 0x7f7f7fff : 0xff0000ff); + box->set_dashed(true); + box->set_pickable(false); // Is false by default. + box->set_z_position(0); + _snaptarget_bbox = _desktop->add_temporary_canvasitem(box, timeout_val*1000.0); + } } } } @@ -337,6 +268,15 @@ SnapIndicator::remove_snaptarget(bool only_if_presnap) _snaptarget_bbox = nullptr; } + for (auto *item : _alignment_snap_indicators) { + _desktop->remove_temporary_canvasitem(item); + } + _alignment_snap_indicators.clear(); + + for (auto *item : _distribution_snap_indicators) { + _desktop->remove_temporary_canvasitem(item); + } + _distribution_snap_indicators.clear(); } void @@ -387,6 +327,276 @@ SnapIndicator::remove_debugging_points() _debugging_points.clear(); } +guint32 SnapIndicator::get_guide_color(SnapTargetType t) +{ + switch(t) { + case SNAPTARGET_ALIGNMENT_BBOX_CORNER: + case SNAPTARGET_ALIGNMENT_BBOX_MIDPOINT: + case SNAPTARGET_ALIGNMENT_BBOX_EDGE_MIDPOINT: + return 0xff0000ff; + case SNAPTARGET_ALIGNMENT_PAGE_CENTER: + case SNAPTARGET_ALIGNMENT_PAGE_CORNER: + return 0x00ff00ff; + case SNAPTARGET_ALIGNMENT_HANDLE: + return 0x0000ffff; + case SNAPTARGET_ALIGNMENT_INTERSECTION: + return 0xd13bd1ff; + default: + g_warning("Alignment guide color not handled %i", t); + return 0x000000ff; + } +} + +Geom::Coord get_y(Geom::Rect const &source, Geom::Rect const &target) +{ + Geom::Coord y; + + if (source.max().y() < target.midpoint().y()) + y = target.min().y(); + else if(source.min().y() > target.midpoint().y()) + y = target.max().y(); + else + y = target.midpoint().y(); + + return y; +} + +Geom::Coord get_x(Geom::Rect const &source, Geom::Rect const &target) +{ + Geom::Coord x; + + if (source.max().x() < target.midpoint().x()) + x = target.min().x(); + else if(source.min().x() > target.midpoint().x()) + x = target.max().x(); + else + x = target.midpoint().x(); + + return x; +} + +void SnapIndicator::make_alignment_indicator(Geom::Point const &p1, Geom::Point const &p2, guint32 color, double fontsize, double scale) +{ + Preferences *prefs = Preferences::get(); + bool show_distance = prefs->getBool("/options/snapindicatordistance/value", false); + + Inkscape::CanvasItemCurve *line; + + auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + ctrl->set_size(7); + ctrl->set_mode(Inkscape::CanvasItemCtrlMode::CANVAS_ITEM_CTRL_MODE_COLOR); + ctrl->set_stroke(0xffffffff); + ctrl->set_fill(color); + ctrl->set_position(p1); + ctrl->set_pickable(false); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(ctrl, 0)); + + ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + ctrl->set_size(7); + ctrl->set_mode(Inkscape::CanvasItemCtrlMode::CANVAS_ITEM_CTRL_MODE_COLOR); + ctrl->set_stroke(0xffffffff); + ctrl->set_fill(color); + ctrl->set_position(p2); + ctrl->set_pickable(false); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(ctrl, 0)); + + if (show_distance) { + auto dist = Geom::L2(p2 - p1); + double offset = (fontsize + 5)/_desktop->current_zoom(); + auto direction = Geom::unit_vector(p1 - p2); + auto text_pos = (p1 + p2)/2; + + Glib::ustring unit_name = _desktop->doc()->getDisplayUnit()->abbr.c_str(); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + + dist = Inkscape::Util::Quantity::convert(dist, "px", unit_name); + + Glib::ustring distance = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*dist); + + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance); + text->set_fontsize(fontsize); + text->set_fill(color); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + text->set_background(0xffffff00); + + auto temp_point = text_pos + offset*direction; + line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, temp_point); + line->set_stroke(color); + line->set_bg_alpha(1.0f); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0)); + + temp_point = text_pos - offset*direction; + line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), temp_point, p2); + line->set_stroke(color); + line->set_bg_alpha(1.0f); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0)); + } else { + line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line->set_stroke(color); + line->set_bg_alpha(1.0f); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0)); + } +} + +Inkscape::CanvasItemCurve* SnapIndicator::make_stub_line_v(Geom::Point const & p) +{ + Geom::Coord length = 10/_desktop->current_zoom(); + auto line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p + Geom::Point(0, length/2), p - Geom::Point(0, length/2)); + line->set_stroke(0xff5f1fff); + return line; +} + +Inkscape::CanvasItemCurve* SnapIndicator::make_stub_line_h(Geom::Point const & p) +{ + Geom::Coord length = 10/_desktop->current_zoom(); + auto line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p + Geom::Point(length/2, 0), p - Geom::Point(length/2, 0)); + line->set_stroke(0xff5f1fff); + return line; +} + +void SnapIndicator::make_distribution_indicators(SnappedPoint const &p, + double fontsize, + double scale) +{ + Preferences *prefs = Preferences::get(); + bool show_distance = prefs->getBool("/options/snapindicatordistance/value", false); + + guint32 color = 0xff5f1fff; + guint32 text_fill = 0xffffffff; + guint32 text_bg = 0xff5f1fff; //0x33337f7f + Geom::Point text_pos; + double text_offset = (fontsize * 2); + double line_offset = 5/_desktop->current_zoom(); + + Glib::ustring unit_name = _desktop->doc()->getDisplayUnit()->abbr.c_str(); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + auto equal_dist = Inkscape::Util::Quantity::convert(p.getDistributionDistance(), "px", unit_name); + Glib::ustring distance = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*equal_dist); + + switch (p.getTarget()) { + case SNAPTARGET_DISTRIBUTION_Y: + case SNAPTARGET_DISTRIBUTION_X: + case SNAPTARGET_DISTRIBUTION_RIGHT: + case SNAPTARGET_DISTRIBUTION_LEFT: + case SNAPTARGET_DISTRIBUTION_UP: + case SNAPTARGET_DISTRIBUTION_DOWN: { + Geom::Coord x, y; + Geom::Point p1, p2; + Inkscape::CanvasItemCurve *point1, *point2; + + for (auto it = p.getBBoxes().begin(); it + 1 != p.getBBoxes().end(); it++) { + switch (p.getTarget()) { + case SNAPTARGET_DISTRIBUTION_RIGHT: + case SNAPTARGET_DISTRIBUTION_LEFT: + case SNAPTARGET_DISTRIBUTION_X: + y = get_y(*it,*std::next(it)); + p1 = Geom::Point(it->max().x() + line_offset, y); + p2 = Geom::Point(std::next(it)->min().x() - line_offset, y); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(0, -text_offset)); + + point1 = make_stub_line_v(p1); + point2 = make_stub_line_v(p2); + break; + + case SNAPTARGET_DISTRIBUTION_DOWN: + case SNAPTARGET_DISTRIBUTION_UP: + case SNAPTARGET_DISTRIBUTION_Y: + x = get_x(*it,*std::next(it)); + p1 = Geom::Point(x, it->max().y() + line_offset); + p2 = Geom::Point(x, std::next(it)->min().y() - line_offset); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(-text_offset, 0)); + + point1 = make_stub_line_h(p1); + point2 = make_stub_line_h(p2); + break; + } + + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0)); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0)); + + auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line1->set_stroke(color); + line1->set_width(2); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0)); + + if (show_distance) { + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance); + text->set_fontsize(fontsize); + text->set_fill(text_fill); + text->set_background(text_bg); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + } + } + break; + } + case SNAPTARGET_DISTRIBUTION_XY: { + Geom::Coord x, y; + Geom::Point p1, p2; + Inkscape::CanvasItemCurve *point1, *point2; + + auto equal_dist2 = Inkscape::Util::Quantity::convert(p.getDistributionDistance2(), "px", unit_name); + Glib::ustring distance2 = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*equal_dist2); + + for (auto it = p.getBBoxes().begin(); it + 1 != p.getBBoxes().end(); it++) { + y = get_y(*it,*std::next(it)); + p1 = Geom::Point(it->max().x() + line_offset, y); + p2 = Geom::Point(std::next(it)->min().x() - line_offset, y); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(0, -text_offset)); + + point1 = make_stub_line_v(p1); + point2 = make_stub_line_v(p2); + + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0)); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0)); + + auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line1->set_stroke(color); + line1->set_width(2); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0)); + + if (show_distance) { + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance); + text->set_fontsize(fontsize); + text->set_fill(text_fill); + text->set_background(text_bg); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + } + } + + for (auto it = p.getBBoxes2().begin(); it + 1 != p.getBBoxes2().end(); it++) { + x = get_x(*it,*std::next(it)); + p1 = Geom::Point(x, it->max().y() + line_offset); + p2 = Geom::Point(x, std::next(it)->min().y() - line_offset); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(-text_offset, 0)); + + point1 = make_stub_line_h(p1); + point2 = make_stub_line_h(p2); + + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0)); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0)); + + auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line1->set_stroke(color); + line1->set_width(2); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0)); + + if (show_distance) { + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance2); + text->set_fontsize(fontsize); + text->set_fill(text_fill); + text->set_background(text_bg); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + } + } + + break; + } + } +} } //namespace Display } /* namespace Inkscape */ diff --git a/src/display/control/snap-indicator.h b/src/display/control/snap-indicator.h index 463acaf9f2682601781a674a557b9d407666777c..76d083aeedc1c9580d2997bfcb218d67c0696dd5 100644 --- a/src/display/control/snap-indicator.h +++ b/src/display/control/snap-indicator.h @@ -17,7 +17,13 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ +#include "snap-enums.h" #include "snapped-point.h" +#include "display/control/canvas-item-curve.h" + +#include +#include +#include class SPDesktop; @@ -45,6 +51,9 @@ protected: TemporaryItem *_snaptarget_tooltip; TemporaryItem *_snaptarget_bbox; TemporaryItem *_snapsource; + + std::list _alignment_snap_indicators; + std::list _distribution_snap_indicators; std::list _debugging_points; bool _snaptarget_is_presnap; SPDesktop *_desktop; @@ -52,6 +61,15 @@ protected: private: SnapIndicator(const SnapIndicator&) = delete; SnapIndicator& operator=(const SnapIndicator&) = delete; + + void make_distribution_indicators(SnappedPoint const &p, double fontsize, double scale); + void make_alignment_indicator(Geom::Point const &p1, Geom::Point const &p2, guint32 color, double fontsize, double scale); + guint32 get_guide_color(SnapTargetType t); + Inkscape::CanvasItemCurve* make_stub_line_h(Geom::Point const &p); + Inkscape::CanvasItemCurve* make_stub_line_v(Geom::Point const &p); + + static std::unordered_map source2string; + static std::unordered_map target2string; }; } //namespace Display diff --git a/src/distribution-snapper.cpp b/src/distribution-snapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0cbc11c74586b4d1b9f38117b78a4f9a10f23ca4 --- /dev/null +++ b/src/distribution-snapper.cpp @@ -0,0 +1,652 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Snapping equidistant objects + * + * Authors: + * Parth Pant + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/circle.h> +#include <2geom/line.h> +#include <2geom/path-intersection.h> +#include <2geom/path-sink.h> +#include + +#include "desktop.h" +#include "display/curve.h" +#include "document.h" +#include "inkscape.h" +#include "live_effects/effect-enum.h" +#include "object/sp-clippath.h" +#include "object/sp-flowtext.h" +#include "object/sp-image.h" +#include "object/sp-item-group.h" +#include "object/sp-mask.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-use.h" +#include "path/path-util.h" // curve_for_item +#include "preferences.h" +#include "style.h" +#include "svg/svg.h" + +#define DISTRIBUTION_SNAPPING_EPSILON 0.5e-4f + +static bool compare_double(double x, double y, double epsilon = DISTRIBUTION_SNAPPING_EPSILON) +{ + if (abs(x - y) < epsilon) + return true; + return false; +} + +static int sortBoxesRight(Geom::Rect const &a, Geom::Rect const &b) +{ + if (a.midpoint().x() < b.midpoint().x()) + return 1; + return 0; +} + +static int sortBoxesLeft(Geom::Rect const &a, Geom::Rect const &b) +{ + if (a.midpoint().x() > b.midpoint().x()) + return 1; + return 0; +} + +static int sortBoxesUp(Geom::Rect const &a, Geom::Rect const &b) +{ + if (a.midpoint().y() > b.midpoint().y()) + return 1; + return 0; +} + +static int sortBoxesDown(Geom::Rect const &a, Geom::Rect const &b) +{ + if (a.midpoint().y() < b.midpoint().y()) + return 1; + return 0; +} + +Inkscape::DistributionSnapper::DistributionSnapper(SnapManager *sm, Geom::Coord const d) + : Snapper(sm, d) +{ + _bboxes_right = std::make_unique>(); + _bboxes_left = std::make_unique>(); + _bboxes_up = std::make_unique>(); + _bboxes_down = std::make_unique>(); +} + +Inkscape::DistributionSnapper::~DistributionSnapper() +{ + _bboxes_right->clear(); + _bboxes_left->clear(); + _bboxes_up->clear(); + _bboxes_down->clear(); +} + +Geom::Coord Inkscape::DistributionSnapper::distRight(Geom::Rect const &a, Geom::Rect const &b) +{ + return -a.max().x() + b.min().x(); +} + +Geom::Coord Inkscape::DistributionSnapper::distLeft(Geom::Rect const &a, Geom::Rect const &b) +{ + return a.min().x() - b.max().x(); +} + +Geom::Coord Inkscape::DistributionSnapper::distUp(Geom::Rect const &a, Geom::Rect const &b) +{ + return a.min().y() - b.max().y(); +} + +Geom::Coord Inkscape::DistributionSnapper::distDown(Geom::Rect const &a, Geom::Rect const &b) +{ + return -a.max().y() + b.min().y(); +} + +bool Inkscape::DistributionSnapper::_findSidewaysSnaps( + Geom::Rect const &source_bbox, + std::vector::iterator it, + std::vector::iterator end, + std::vector &vec, + Geom::Coord &dist, + Geom::Coord tol, + std::function const &distance_func, + int level) const +{ + std::vector::iterator next_bbox = it; + std::vector::iterator _next_bbox = it; + + if (level == 0) { + int max_length = 0; + + // check each consecutive box for a snap + Geom::Rect optimum_start; + while (std::next(next_bbox) != end) { + auto first_dist = distance_func(source_bbox, *next_bbox); + level = 0; + + // temporary result for this particular item + auto result = new std::vector; + if (_findSidewaysSnaps(*next_bbox, ++it, end, *result, first_dist, tol, distance_func, ++level)) { + if (result->size() > max_length) { + // if this item has the most number of items equidistant form each other + // then make this the final result + optimum_start = *next_bbox; + max_length = result->size(); + vec = *result; + dist = first_dist; + } + } + + result->clear(); + delete result; + + ++next_bbox; + } + + // if there is no snap, just add the first item and return false + // this is useful to find in-between snaps (see _snapEquidistantPoints()) + if (max_length == 0) { + vec.push_back(*_next_bbox); + return false; + } else { + // insert the first item to the list, this does not happen automatically if level==1 (see below) + vec.insert(vec.begin(), optimum_start); + return true; + } + } + + // if not the zeroth level + if (level != 1) + vec.push_back(source_bbox); + + if (it == end) + return true; + + int og_level = level; + std::vector best_result; + int max_length = 0; + while (next_bbox != end) { + level = og_level; + Geom::Coord this_dist; + Geom::Coord next_dist = distance_func(source_bbox, *next_bbox); + auto result = new std::vector; + + if (level == 1 && compare_double(dist, next_dist, tol)){ + // if this is the first level, check if the snap is within tolerance + // we cancel here if the possible snap in not whithing tolerance, saves us some time! + this_dist = next_dist; + if (_findSidewaysSnaps(*next_bbox, ++it, end, *result, this_dist, tol, distance_func, ++level)) { + if (result->size() > max_length) { + max_length = result->size(); + dist = this_dist; + best_result = *result; + } + } + result->clear(); + delete result; + + } else if (compare_double(dist, next_dist, level * DISTRIBUTION_SNAPPING_EPSILON)) { + + if (_findSidewaysSnaps(*next_bbox, ++it, end, *result, dist, tol, distance_func, ++level)) { + if (result->size() > max_length) { + max_length = result->size(); + best_result = *result; + } + } + result->clear(); + delete result; + } + + ++next_bbox; + } + + vec.insert(vec.end(), best_result.begin(), best_result.end()); + return true; +} + +void Inkscape::DistributionSnapper::_collectBBoxes(Geom::OptRect const &bbox_to_snap, bool const &first_point) const +{ + if (!first_point) + return; + + _bboxes_right->clear(); + _bboxes_left->clear(); + _bboxes_down->clear(); + _bboxes_up->clear(); + + SPItem::BBoxType bbox_type = SPItem::GEOMETRIC_BBOX; + + Preferences *prefs = Preferences::get(); + bool prefs_bbox = prefs->getBool("/tools/bounding_box"); + bbox_type = !prefs_bbox ? SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + + // collect bounding boxes of other objects + for (const auto &candidate : *(_snapmanager->align_snapper_candidates)) { + SPItem *root_item = candidate.item; + + // get the root item in case we have a duplicate at hand + SPUse *use = dynamic_cast(candidate.item); + if (use) { + root_item = use->root(); + } + g_return_if_fail(root_item); + + // if candidate is not a clip or a mask object then extract its BBox points + if (!candidate.clip_or_mask) { + Geom::OptRect b = root_item->desktopBounds(bbox_type); + if (!b.intersects(bbox_to_snap)) { + auto diff_vec = b->midpoint() - bbox_to_snap->midpoint(); + + Geom::Rect Xbounds = *bbox_to_snap; + Xbounds.expandBy(_snapmanager->_desktop->get_display_area().maxExtent(), 0); + + Geom::Rect Ybounds = *bbox_to_snap; + Ybounds.expandBy(0, _snapmanager->_desktop->get_display_area().maxExtent()); + + if (Xbounds.intersects(b)) { + if (diff_vec.x() > 0) { + _bboxes_right->push_back(*b); + } else { + _bboxes_left->push_back(*b); + } + } else if (Ybounds.intersects(b)) { + if (diff_vec.y() < 0) { + _bboxes_up->push_back(*b); + } else { + _bboxes_down->push_back(*b); + } + } + } + } + } + + std::stable_sort(_bboxes_right->begin(), _bboxes_right->end(), sortBoxesRight); + std::stable_sort(_bboxes_left->begin(), _bboxes_left->end(), sortBoxesLeft); + std::stable_sort(_bboxes_up->begin(), _bboxes_up->end(), sortBoxesUp); + std::stable_sort(_bboxes_down->begin(), _bboxes_down->end(), sortBoxesDown); + + _addBBoxForIntersectingBoxes(_bboxes_right.get(), Direction::RIGHT); + _addBBoxForIntersectingBoxes(_bboxes_left.get(), Direction::LEFT); + _addBBoxForIntersectingBoxes(_bboxes_up.get(), Direction::UP); + _addBBoxForIntersectingBoxes(_bboxes_down.get(), Direction::DOWN); +} + +void Inkscape::DistributionSnapper::_addBBoxForIntersectingBoxes(std::vector *vec, Direction dir) const { + if (vec->size() < 1) { + return; + } + + int count = 0; + std::vector> insertPositions; + + for (auto it = vec->begin(); it != vec->end(); it++, count++) { + Geom::Rect comb(*it); + int num = 0; + int insertPos = count; + + while (std::next(it) != vec->end() && it->intersects(*std::next(it))) { + comb.unionWith(*std::next(it)); + + if (dir == Direction::RIGHT && comb.midpoint().x() > it->midpoint().x()) { + ++insertPos; + } else if (dir == Direction::LEFT && comb.midpoint().x() < it->midpoint().x()){ + ++insertPos; + } else if (dir == Direction::UP && comb.midpoint().y() > it->midpoint().y()){ + ++insertPos; + } else if (dir == Direction::DOWN && comb.midpoint().y() < it->midpoint().y()){ + ++insertPos; + } + + ++it; + ++num; + ++count; + } + + if (num > 0) { + insertPositions.emplace_back(insertPos, comb); + } + } + + if (insertPositions.size() != 0) { + // TODO: Does this improve performance? + vec->reserve(vec->size() + insertPositions.size()); + + count = 0; + for (auto pair : insertPositions) { + vec->insert(vec->begin() + pair.first + count, pair.second); + ++count; + } + } +} + +void Inkscape::DistributionSnapper::_snapEquidistantPoints(IntermSnapResults &isr, + SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector *unselected_nodes, + SnapConstraint const &c, + Geom::Point const &p_proj_on_constraint) const +{ + bool consider_x = true; + bool consider_y = true; + if (!c.isUndefined() && c.isLinear()) { + if (c.getDirection().x() == 0) + consider_x = false; // consider horizontal snapping if moving vertically + else + consider_y = false; // consider vertical snapping if moving horizontally + } + + _collectBBoxes(bbox_to_snap, p.getSourceNum() <= 0); + + Geom::Coord offset; + + if (p.getSourceType() != SNAPSOURCE_BBOX_MIDPOINT) + return; + + Geom::Coord equal_dist; + + SnappedPoint sr, sl, sx, su, sd, sy; + Geom::Coord dist_x, dist_y; + bool snap_x = false, snap_y = false; + + // 1. look right + // if there is a snap then add right bboxes and look left, if there is a snap to the left then + // add those bboxes too + std::vector vecRight; + std::vector vecLeft; + if (consider_x && _bboxes_right->size() > 0) { + if (_findSidewaysSnaps(*bbox_to_snap, _bboxes_right->begin(), _bboxes_right->end(), vecRight, equal_dist, getSnapperTolerance(), &DistributionSnapper::distRight)) { + auto first_dist = distRight(*bbox_to_snap, vecRight.front()); + Geom::Coord offset = first_dist - equal_dist; + Geom::Point target = bbox_to_snap->midpoint() + Geom::Point(offset, 0); + + Geom::Affine translation = Geom::Translate(target - bbox_to_snap->midpoint()); + Geom::Rect bbox = *bbox_to_snap * translation; + vecRight.insert(vecRight.begin(), bbox); + + _correctSelectionBBox(target, p.getPoint(), *bbox_to_snap); + + if (_bboxes_left->size() > 0) { + first_dist = distLeft(bbox, _bboxes_left->front()); + Geom::Coord left_dist; + vecLeft.clear(); + if (_findSidewaysSnaps(*bbox_to_snap, _bboxes_left->begin(), _bboxes_left->end(), vecLeft, left_dist, getSnapperTolerance(), &DistributionSnapper::distLeft)) { + if (compare_double(left_dist, equal_dist)) { + std::reverse(vecLeft.begin(), vecLeft.end()); + vecRight.insert(vecRight.begin(), vecLeft.begin(), vecLeft.end()); + } + + } else if (compare_double(first_dist, equal_dist)) { + vecRight.insert(vecRight.begin(), vecLeft.front()); + } + } + + dist_x = abs(offset); + sx = SnappedPoint(target, vecRight, bbox, equal_dist, p.getSourceType(), p.getSourceNum(), SNAPTARGET_DISTRIBUTION_RIGHT, dist_x, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + snap_x = true; + } + } + + // 2. if no snap to right, look left + // if there is a snap then add left bboxes and right left, if there is a snap to the right then + // add those bboxes too + if (consider_x && !snap_x && _bboxes_left->size() > 0) { + vecLeft.clear(); + if (_findSidewaysSnaps(*bbox_to_snap, _bboxes_left->begin(), _bboxes_left->end(), vecLeft, equal_dist, getSnapperTolerance(), &DistributionSnapper::distLeft)) { + auto first_dist = distLeft(*bbox_to_snap, vecLeft.front()); + Geom::Coord offset = first_dist - equal_dist; + Geom::Point target = bbox_to_snap->midpoint() - Geom::Point(offset, 0); + + // translate the source bbox to the snap position + Geom::Affine translation = Geom::Translate(target - bbox_to_snap->midpoint()); + Geom::Rect bbox = *bbox_to_snap * translation; + std::reverse(vecLeft.begin(), vecLeft.end()); + vecLeft.push_back(bbox); + + _correctSelectionBBox(target, p.getPoint(), *bbox_to_snap); + + if (_bboxes_right->size() > 0) { + first_dist = distRight(bbox, _bboxes_right->front()); + Geom::Coord right_dist; + vecRight.clear(); + if (_findSidewaysSnaps(*bbox_to_snap, _bboxes_right->begin(), _bboxes_right->end(), vecRight, right_dist, getSnapperTolerance(), &DistributionSnapper::distRight)) { + if (compare_double(right_dist, equal_dist)) { + vecLeft.insert(vecLeft.end(), vecRight.begin(), vecRight.end()); + } + + } else if (compare_double(first_dist, equal_dist)) { + vecLeft.push_back(vecRight.front()); + } + } + + dist_x = abs(offset); + sx = SnappedPoint(target, vecLeft, bbox, equal_dist, p.getSourceType(), p.getSourceNum(), SNAPTARGET_DISTRIBUTION_LEFT, dist_x, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + snap_x = true; + } + } + + // 3. if no snap to right or left just add the center snap + if (consider_x && !snap_x && vecRight.size() > 0 && vecLeft.size() > 0) { + auto x = Geom::Point((vecRight.front().min() + vecLeft.front().max()) / 2).x(); + offset = abs(x - bbox_to_snap->midpoint().x()); + if (offset < getSnapperTolerance()) { + Geom::Point target = Geom::Point(x, bbox_to_snap->midpoint().y()); + // translate the source bbox to the snap position + Geom::Affine translation = Geom::Translate(target - bbox_to_snap->midpoint()); + Geom::Rect bbox = *bbox_to_snap * translation; + std::vector bboxes = {vecLeft.front(), bbox, vecRight.front()}; + + _correctSelectionBBox(target, p.getPoint(), *bbox_to_snap); + + equal_dist = bbox.min().x() - vecLeft.front().max().x(); + sx = SnappedPoint(target, bboxes, bbox, equal_dist, p.getSourceType(), p.getSourceNum(), SNAPTARGET_DISTRIBUTION_X, offset, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + snap_x = true; + } + } + + // 1. look Up + // if there is a snap then add top bboxes and look down, if there is a snap at the bottom then + // add those bboxes too + std::vector vecUp; + std::vector vecDown; + if (consider_y && _bboxes_up->size() > 0) { + if (_findSidewaysSnaps(*bbox_to_snap, _bboxes_up->begin(), _bboxes_up->end(), vecUp, equal_dist, getSnapperTolerance(), &DistributionSnapper::distUp)) { + auto first_dist = distUp(*bbox_to_snap, vecUp.front()); + Geom::Coord offset = first_dist - equal_dist; + Geom::Point target = bbox_to_snap->midpoint() - Geom::Point(0, offset); + + // translate the source bbox to the snap position + Geom::Affine translation = Geom::Translate(target - bbox_to_snap->midpoint()); + Geom::Rect bbox = *bbox_to_snap * translation; + std::reverse(vecUp.begin(), vecUp.end()); + vecUp.push_back(bbox); + + _correctSelectionBBox(target, p.getPoint(), *bbox_to_snap); + + if (_bboxes_down->size() > 0) { + first_dist = distDown(bbox, _bboxes_down->front()); + Geom::Coord down_dist; + vecDown.clear(); + if (_findSidewaysSnaps(*bbox_to_snap, _bboxes_down->begin(), _bboxes_down->end(), vecDown, down_dist, + getSnapperTolerance(), &DistributionSnapper::distDown)) { + if (abs(down_dist - equal_dist) < 1e-4) { + vecUp.insert(vecUp.end(), vecDown.begin(), vecDown.end()); + } + + } else if (abs(first_dist - equal_dist) < 1e-4) { + vecUp.insert(vecUp.end(), vecDown.front()); + } + } + + dist_y = abs(offset); + sy = SnappedPoint(target, vecUp, bbox, equal_dist, p.getSourceType(), p.getSourceNum(), SNAPTARGET_DISTRIBUTION_UP, dist_y, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + snap_y = true; + } + } + + // 2. if no snaps on top, look Down + // if there is a snap then add bottom bboxes and look Up, if there is a snap above then + // add those bboxes too + if (consider_y && !snap_y && _bboxes_down->size() > 0) { + vecDown.clear(); + if (_findSidewaysSnaps(*bbox_to_snap, _bboxes_down->begin(), _bboxes_down->end(), vecDown, equal_dist, getSnapperTolerance(), &DistributionSnapper::distDown)) { + auto first_dist = distDown(*bbox_to_snap, vecDown.front()); + Geom::Coord offset = first_dist - equal_dist; + Geom::Point target = bbox_to_snap->midpoint() + Geom::Point(0, offset); + + // translate the source bbox to the snap position + Geom::Affine translation = Geom::Translate(target - bbox_to_snap->midpoint()); + Geom::Rect bbox = *bbox_to_snap * translation; + vecDown.insert(vecDown.begin(), bbox); + + _correctSelectionBBox(target, p.getPoint(), *bbox_to_snap); + + if (_bboxes_up->size() > 0) { + first_dist = distUp(bbox, _bboxes_up->front()); + Geom::Coord up_dist; + vecUp.clear(); + + if (_findSidewaysSnaps(*bbox_to_snap, _bboxes_up->begin(), _bboxes_up->end(), vecUp, up_dist, getSnapperTolerance(), &DistributionSnapper::distUp)) { + if (compare_double(up_dist, equal_dist)) { + std::reverse(vecUp.begin(), vecUp.end()); + vecDown.insert(vecDown.begin(), vecUp.begin(), vecUp.end()); + } + } else if (compare_double(first_dist, equal_dist)) { + vecDown.insert(vecDown.begin(), vecUp.front()); + } + } + + dist_y = abs(offset); + sy = SnappedPoint(target, vecDown, bbox, equal_dist, p.getSourceType(), p.getSourceNum(), SNAPTARGET_DISTRIBUTION_DOWN, dist_y, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + snap_y = true; + } + } + + // 3. if no snap to right or left just add the center snap + if (consider_y && !snap_y && vecUp.size() > 0 && vecDown.size() > 0) { + auto y = Geom::Point((vecUp.front().max() + vecDown.front().min()) / 2).y(); + offset = abs(y - bbox_to_snap->midpoint().y()); + if (consider_y && offset < getSnapperTolerance()) { + Geom::Point target = Geom::Point(bbox_to_snap->midpoint().x(), y); + // translate the source bbox to the snap position + Geom::Affine translation = Geom::Translate(target - bbox_to_snap->midpoint()); + Geom::Rect bbox = *bbox_to_snap * translation; + std::vector bboxes = {vecUp.front(), bbox, vecDown.front()}; + + _correctSelectionBBox(target, p.getPoint(), *bbox_to_snap); + + equal_dist = bbox.min().y() - vecUp.front().max().y(); + sy = SnappedPoint(target, bboxes, bbox, equal_dist, p.getSourceType(), p.getSourceNum(), SNAPTARGET_DISTRIBUTION_Y, offset, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + snap_y = true; + } + } + + if (snap_x && snap_y) { + Geom::Point target = Geom::Point(sx.getPoint().x(), sy.getPoint().y()); + Geom::Affine translation = Geom::Translate(target - bbox_to_snap->midpoint()); + Geom::Rect bbox = *bbox_to_snap * translation; + std::vector bboxes_x = sx.getBBoxes(); + std::vector bboxes_y = sy.getBBoxes(); + + // Do not need to correct here, already did that earlier for each direction separately + //_correctSelectionBBox(target, p.getPoint(), *bbox_to_snap); + auto si = SnappedPoint(target, bboxes_x, bboxes_y, bbox, sx.getDistributionDistance(), sy.getDistributionDistance(), p.getSourceType(), p.getSourceNum(), SNAPTARGET_DISTRIBUTION_XY, offset, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + isr.points.push_back(si); + return; + } + + if (snap_x) { + isr.points.push_back(sx); + } + + if (snap_y) { + isr.points.push_back(sy); + } +} + +void Inkscape::DistributionSnapper::_correctSelectionBBox(Geom::Point &target, + Geom::Point const &p, + Geom::Rect const &bbox_to_snap) const +{ + if (_snapmanager->_desktop->selection->size() > 1) { + auto correction = bbox_to_snap.midpoint() - p; + target -= correction; + } +} + +void Inkscape::DistributionSnapper::freeSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector const *it, + std::vector *unselected_nodes) const +{ + if (bbox_to_snap.empty()) + return; + + if (!(p.getSourceType() & SNAPSOURCE_BBOX_CATEGORY)) { + return; + } + + // toggle checks + if (!_snap_enabled || !_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_DISTRIBUTION_CATEGORY)) + return; + + _snapEquidistantPoints(isr, p, bbox_to_snap, unselected_nodes); +} + +void Inkscape::DistributionSnapper::constrainedSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + SnapConstraint const &c, + std::vector const *it, + std::vector *unselected_nodes) const +{ + if (bbox_to_snap.empty()) + return; + + // project the mouse pointer onto the constraint. Only the projected point will be considered for snapping + Geom::Point pp = c.projection(p.getPoint()); + + // toggle checks + if (!_snap_enabled || !_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_DISTRIBUTION_CATEGORY)) + return; + + _snapEquidistantPoints(isr, p, bbox_to_snap, unselected_nodes, c, pp); +} + +bool Inkscape::DistributionSnapper::ThisSnapperMightSnap() const +{ + return true; +} + +bool Inkscape::DistributionSnapper::getSnapperAlwaysSnap() const +{ + // TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp + return _snapmanager->snapprefs.getDistributionTolerance() == 10000; +} + +Geom::Coord Inkscape::DistributionSnapper::getSnapperTolerance() const +{ + SPDesktop const *dt = _snapmanager->getDesktop(); + double const zoom = dt ? dt->current_zoom() : 1; + return _snapmanager->snapprefs.getDistributionTolerance() / zoom; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/distribution-snapper.h b/src/distribution-snapper.h new file mode 100644 index 0000000000000000000000000000000000000000..9c4153c649f7cd7b77324169e63c7325fcc345df --- /dev/null +++ b/src/distribution-snapper.h @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Authors: + * Parth Pant + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DISTRIBUTION_SNAPPER_H +#define SEEN_DISTRIBUTION_SNAPPER_H + +#include <2geom/affine.h> +#include + +#include "snap-enums.h" +#include "snapper.h" +#include "snap-candidate.h" + +class SPDesktop; +class SPNamedView; +class SPItem; +class SPObject; +class SPPath; +class SPDesktop; + +namespace Inkscape +{ + +/** + * Snapping equidistant objects + */ +class DistributionSnapper : public Snapper +{ + +public: + DistributionSnapper(SnapManager *sm, Geom::Coord const d); + ~DistributionSnapper() override; + + /** + * @return true if this Snapper will snap at least one kind of point. + */ + bool ThisSnapperMightSnap() const override; + + /** + * @return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels. + */ + Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom) + + bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance + + void freeSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector const *it, + std::vector *unselected_nodes) const override; + + void constrainedSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + SnapConstraint const &c, + std::vector const *it, + std::vector *unselected_nodes) const override; + +private: + std::unique_ptr> _bboxes_left; + std::unique_ptr> _bboxes_right; + std::unique_ptr> _bboxes_down; + std::unique_ptr> _bboxes_up; + + /** Collects and caches bounding boxes to the left, right, up, and down of the + * selected object. + * @param bounding box of the selected object + * @param is the point first point in the selection? + */ + void _collectBBoxes(Geom::OptRect const &bbox_to_snap, bool const &first_point) const; + + /** Finds and snaps to points that is equidistant from surrounding bboxes + * @param interm snap results + * @param source point to snap + * @param bounding box of the selecton to snap + * @param unselected nodes in case editing nodes (never used here, remove?) + * @param active snap constraint + * @param projection of the source point on the constraint (never used, remove?) + */ + void _snapEquidistantPoints(IntermSnapResults &isr, + SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector *unselected_nodes, + SnapConstraint const &c = SnapConstraint(), + Geom::Point const &p_proj_on_constraint = Geom::Point()) const; + + /** When the selection has more than one objects in it, the bounding box of + * the object that the selection is grabbed from (closest to the pointer) is + * snapped to the center of the overall bounding box of the selection. This + * function corrects the target point to be a point where the bounding box of + * that particular object must be snapped to. + * @param snap target point that need to be snapped to + * @param source point to snap (this bbox midpoint of the object closest to the mouse pointer) + * @param bounding box of the active selection to snap + */ + void _correctSelectionBBox(Geom::Point &target, Geom::Point const &p, Geom::Rect const &bbox_to_snap) const; + + /** Finds and stores the bounding boxes that are at equal distance from each other + * @param the distance between the object that needs to be snapped and the first + * object in the sideways vectors. + * @param first iterator of the sideways vector + * @param end of the sideways vector + * @param vector where the snapped bboxes will be stored + * @param equal distance between consecutive vectors + * @param snapped tolerance + * @param a function pointer to the distance function + * @param level of recursion - do not pass this while calling the function + */ + bool _findSidewaysSnaps(Geom::Rect const &source_bbox, + std::vector::iterator it, + std::vector::iterator end, + std::vector &vec, + Geom::Coord &dist, + Geom::Coord tol, + std::function const & distance_func, + int level = 0) const; + + enum class Direction { + RIGHT, + LEFT, + UP, + DOWN + }; + + /** This functions adds overlapping bounding boxes to the list of bounding boxes. + * The new bounding boxes are added such that the final list is still sorted. + * This extra step is needed so that the overall union of any overlapping bounding + * boxes is also considered during distribution snapping. + */ + void _addBBoxForIntersectingBoxes(std::vector *vec, Direction dir) const; + + // distance functions for different orientations + static Geom::Coord distRight(Geom::Rect const &a, Geom::Rect const &b); + static Geom::Coord distLeft(Geom::Rect const &a, Geom::Rect const &b); + static Geom::Coord distUp(Geom::Rect const &a, Geom::Rect const &b); + static Geom::Coord distDown(Geom::Rect const &a, Geom::Rect const &b); +}; // end of AlignmentSnapper class + +} // end of namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object-snapper.cpp b/src/object-snapper.cpp index ca9bd997decd7e6fb933ec4bbc4a1dc4c6ec9e0e..67083e1e14fe0ac15504e03c28d4345d06c11cf1 100644 --- a/src/object-snapper.cpp +++ b/src/object-snapper.cpp @@ -17,6 +17,7 @@ #include <2geom/line.h> #include <2geom/path-intersection.h> #include <2geom/path-sink.h> +#include #include "desktop.h" #include "display/curve.h" @@ -36,6 +37,7 @@ #include "object/sp-use.h" #include "path/path-util.h" // curve_for_item #include "preferences.h" +#include "snap-enums.h" #include "style.h" #include "svg/svg.h" #include "text-editing.h" @@ -43,21 +45,14 @@ Inkscape::ObjectSnapper::ObjectSnapper(SnapManager *sm, Geom::Coord const d) : Snapper(sm, d) { - _candidates = new std::vector; - _points_to_snap_to = new std::vector; - _paths_to_snap_to = new std::vector; + _points_to_snap_to = std::make_unique>(); + _paths_to_snap_to = std::make_unique>(); } Inkscape::ObjectSnapper::~ObjectSnapper() { - _candidates->clear(); - delete _candidates; - _points_to_snap_to->clear(); - delete _points_to_snap_to; - _clear_paths(); - delete _paths_to_snap_to; } Geom::Coord Inkscape::ObjectSnapper::getSnapperTolerance() const @@ -72,139 +67,6 @@ bool Inkscape::ObjectSnapper::getSnapperAlwaysSnap() const return _snapmanager->snapprefs.getObjectTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp } -void Inkscape::ObjectSnapper::_findCandidates(SPObject* parent, - std::vector const *it, - bool const &first_point, - Geom::Rect const &bbox_to_snap, - bool const clip_or_mask, - Geom::Affine const additional_affine) const // transformation of the item being clipped / masked -{ - SPDesktop const *dt = _snapmanager->getDesktop(); - if (dt == nullptr) { - g_warning("desktop == NULL, so we cannot snap; please inform the developers of this bug"); - // Apparently the setup() method from the SnapManager class hasn't been called before trying to snap. - } - - if (first_point) { - _candidates->clear(); - } - - Geom::Rect bbox_to_snap_incl = bbox_to_snap; // _incl means: will include the snapper tolerance - bbox_to_snap_incl.expandBy(getSnapperTolerance()); // see? - - for (auto& o: parent->children) { - g_assert(dt != nullptr); - SPItem *item = dynamic_cast(&o); - if (item && !(dt->itemIsHidden(item) && !clip_or_mask)) { - // Fix LPE boolops selfsnaping - bool stop = false; - if (item->style) { - SPFilter *filt = item->style->getFilter(); - if (filt && filt->getId() && strcmp(filt->getId(), "selectable_hidder_filter") == 0) { - stop = true; - } - SPLPEItem *lpeitem = dynamic_cast(item); - if (lpeitem && lpeitem->hasPathEffectOfType(Inkscape::LivePathEffect::EffectType::BOOL_OP)) { - stop = true; - } - } - if (stop) { - stop = false; - for (auto skipitem : *it) { - if (skipitem && skipitem->style) { - SPItem *toskip = const_cast(skipitem); - if (toskip) { - SPFilter *filt = toskip->style->getFilter(); - if (filt && filt->getId() && strcmp(filt->getId(), "selectable_hidder_filter") == 0) { - stop = true; - break; - } - - SPLPEItem *lpeitem = dynamic_cast(toskip); - if (!stop && lpeitem && - lpeitem->hasPathEffectOfType(Inkscape::LivePathEffect::EffectType::BOOL_OP)) { - stop = true; - break; - } - } - } - } - if (stop) { - continue; - } - } - // Snapping to items in a locked layer is allowed - // Don't snap to hidden objects, unless they're a clipped path or a mask - /* See if this item is on the ignore list */ - std::vector::const_iterator i; - if (it != nullptr) { - i = it->begin(); - while (i != it->end() && *i != &o) { - ++i; - } - } - - if (it == nullptr || i == it->end()) { - if (item) { - if (!clip_or_mask) { // cannot clip or mask more than once - // The current item is not a clipping path or a mask, but might - // still be the subject of clipping or masking itself ; if so, then - // we should also consider that path or mask for snapping to - SPObject *obj = item->getClipObject(); - if (obj && _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH_CLIP)) { - _findCandidates(obj, it, false, bbox_to_snap, true, item->i2doc_affine()); - } - obj = item->getMaskObject(); - if (obj && _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH_MASK)) { - _findCandidates(obj, it, false, bbox_to_snap, true, item->i2doc_affine()); - } - } - - if (dynamic_cast(item)) { - _findCandidates(&o, it, false, bbox_to_snap, clip_or_mask, additional_affine); - } else { - Geom::OptRect bbox_of_item; - Preferences *prefs = Preferences::get(); - int prefs_bbox = prefs->getBool("/tools/bounding_box", false); - // We'll only need to obtain the visual bounding box if the user preferences tell - // us to, AND if we are snapping to the bounding box itself. If we're snapping to - // paths only, then we can just as well use the geometric bounding box (which is faster) - SPItem::BBoxType bbox_type = (!prefs_bbox && _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CATEGORY)) ? - SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; - if (clip_or_mask) { - // Oh oh, this will get ugly. We cannot use sp_item_i2d_affine directly because we need to - // insert an additional transformation in document coordinates (code copied from sp_item_i2d_affine) - bbox_of_item = item->bounds(bbox_type, item->i2doc_affine() * additional_affine * dt->doc2dt()); - } else { - bbox_of_item = item->desktopBounds(bbox_type); - } - if (bbox_of_item) { - // See if the item is within range - if (bbox_to_snap_incl.intersects(*bbox_of_item) - || (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_ROTATION_CENTER) && bbox_to_snap_incl.contains(item->getCenter()))) { // rotation center might be outside of the bounding box - // This item is within snapping range, so record it as a candidate - _candidates->push_back(SnapCandidateItem(item, clip_or_mask, additional_affine)); - // For debugging: print the id of the candidate to the console - // SPObject *obj = (SPObject*)item; - // std::cout << "Snap candidate added: " << obj->getId() << std::endl; - if (_candidates->size() > 200) { // This makes Inkscape crawl already - static Glib::Timer timer; - if (timer.elapsed() > 1.0) { - timer.reset(); - std::cout << "Warning: limit of 200 snap target paths reached, some will be ignored" << std::endl; - } - break; - } - } - } - } - } - } - } - } -} - - void Inkscape::ObjectSnapper::_collectNodes(SnapSourceType const &t, bool const &first_point) const { @@ -235,10 +97,10 @@ void Inkscape::ObjectSnapper::_collectNodes(SnapSourceType const &t, // Consider the page border for snapping to if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PAGE_CORNER)) { - _getBorderNodes(_points_to_snap_to); + _getBorderNodes(_points_to_snap_to.get()); } - for (const auto & _candidate : *_candidates) { + for (const auto & _candidate : *_snapmanager->obj_snapper_candidates) { //Geom::Affine i2doc(Geom::identity()); SPItem *root_item = _candidate.item; @@ -304,7 +166,7 @@ void Inkscape::ObjectSnapper::_collectNodes(SnapSourceType const &t, // of the item AND the bbox of the clipping path at the same time if (!_candidate.clip_or_mask) { Geom::OptRect b = root_item->desktopBounds(bbox_type); - getBBoxPoints(b, _points_to_snap_to, true, + getBBoxPoints(b, _points_to_snap_to.get(), true, _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CORNER), _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_EDGE_MIDPOINT), _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_MIDPOINT)); @@ -423,7 +285,7 @@ void Inkscape::ObjectSnapper::_collectPaths(Geom::Point /*p*/, } } - for (const auto & _candidate : *_candidates) { + for (const auto & _candidate : *_snapmanager->obj_snapper_candidates) { /* Transform the requested snap point to this item's coordinates */ Geom::Affine i2doc(Geom::identity()); @@ -707,12 +569,6 @@ void Inkscape::ObjectSnapper::freeSnap(IntermSnapResults &isr, return; } - /* Get a list of all the SPItems that we will try to snap to */ - if (p.getSourceNum() <= 0) { - Geom::Rect const local_bbox_to_snap = bbox_to_snap ? *bbox_to_snap : Geom::Rect(p.getPoint(), p.getPoint()); - _findCandidates(_snapmanager->getDocument()->getRoot(), it, p.getSourceNum() <= 0, local_bbox_to_snap, false, Geom::identity()); - } - _snapNodes(isr, p, unselected_nodes); if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION, SNAPTARGET_BBOX_EDGE, SNAPTARGET_PAGE_BORDER, SNAPTARGET_TEXT_BASELINE)) { @@ -752,12 +608,6 @@ void Inkscape::ObjectSnapper::constrainedSnap( IntermSnapResults &isr, // project the mouse pointer onto the constraint. Only the projected point will be considered for snapping Geom::Point pp = c.projection(p.getPoint()); - /* Get a list of all the SPItems that we will try to snap to */ - if (p.getSourceNum() <= 0) { - Geom::Rect const local_bbox_to_snap = bbox_to_snap ? *bbox_to_snap : Geom::Rect(pp, pp); - _findCandidates(_snapmanager->getDocument()->getRoot(), it, p.getSourceNum() <= 0, local_bbox_to_snap, false, Geom::identity()); - } - // A constrained snap, is a snap in only one degree of freedom (specified by the constraint line). // This is useful for example when scaling an object while maintaining a fixed aspect ratio. It's // nodes are only allowed to move in one direction (i.e. in one degree of freedom). @@ -814,21 +664,34 @@ void Inkscape::getBBoxPoints(Geom::OptRect const bbox, bool const /*isTarget*/, bool const includeCorners, bool const includeLineMidpoints, - bool const includeObjectMidpoints) + bool const includeObjectMidpoints, + bool const isAlignment) { if (bbox) { // collect the corners of the bounding box for ( unsigned k = 0 ; k < 4 ; k++ ) { if (includeCorners) { - points->push_back(SnapCandidatePoint(bbox->corner(k), SNAPSOURCE_BBOX_CORNER, -1, SNAPTARGET_BBOX_CORNER, *bbox)); + points->push_back(SnapCandidatePoint(bbox->corner(k), + isAlignment ? SNAPSOURCE_ALIGNMENT_BBOX_CORNER : SNAPSOURCE_BBOX_CORNER, + -1, + isAlignment ? SNAPTARGET_ALIGNMENT_BBOX_CORNER : SNAPTARGET_BBOX_CORNER, + *bbox)); } // optionally, collect the midpoints of the bounding box's edges too if (includeLineMidpoints) { - points->push_back(SnapCandidatePoint((bbox->corner(k) + bbox->corner((k+1) % 4))/2, SNAPSOURCE_BBOX_EDGE_MIDPOINT, -1, SNAPTARGET_BBOX_EDGE_MIDPOINT, *bbox)); + points->push_back(SnapCandidatePoint((bbox->corner(k) + bbox->corner((k+1) % 4))/2, + isAlignment ? SNAPSOURCE_ALIGNMENT_BBOX_EDGE_MIDPOINT : SNAPSOURCE_BBOX_EDGE_MIDPOINT, + -1, + isAlignment ? SNAPTARGET_ALIGNMENT_BBOX_EDGE_MIDPOINT : SNAPTARGET_BBOX_EDGE_MIDPOINT, + *bbox)); } } if (includeObjectMidpoints) { - points->push_back(SnapCandidatePoint(bbox->midpoint(), SNAPSOURCE_BBOX_MIDPOINT, -1, SNAPTARGET_BBOX_MIDPOINT, *bbox)); + points->push_back(SnapCandidatePoint(bbox->midpoint(), + isAlignment ? SNAPSOURCE_ALIGNMENT_BBOX_MIDPOINT : SNAPSOURCE_BBOX_MIDPOINT, + -1, + isAlignment ? SNAPTARGET_ALIGNMENT_BBOX_MIDPOINT : SNAPTARGET_BBOX_MIDPOINT, + *bbox)); } } } diff --git a/src/object-snapper.h b/src/object-snapper.h index 1709137db30fe29c573992b15581c65a7b06f0a3..b0b55ede55d31e6439b2ceaedde5e33d863b2d3a 100644 --- a/src/object-snapper.h +++ b/src/object-snapper.h @@ -11,6 +11,7 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ +#include #include "snapper.h" #include "snap-candidate.h" @@ -60,24 +61,8 @@ public: std::vector *unselected_nodes) const override; private: - //store some lists of candidates, points and paths, so we don't have to rebuild them for each point we want to snap - std::vector *_candidates; - std::vector *_points_to_snap_to; - std::vector *_paths_to_snap_to; - - /** - * Find all items within snapping range. - * @param parent Pointer to the document's root, or to a clipped path or mask object. - * @param it List of items to ignore. - * @param bbox_to_snap Bounding box hulling the whole bunch of points, all from the same selection and having the same transformation. - * @param clip_or_mask The parent object being passed is either a clip or mask. - */ - void _findCandidates(SPObject* parent, - std::vector const *it, - bool const &first_point, - Geom::Rect const &bbox_to_snap, - bool const _clip_or_mask, - Geom::Affine const additional_affine) const; + std::unique_ptr> _points_to_snap_to; + std::unique_ptr> _paths_to_snap_to; void _snapNodes(IntermSnapResults &isr, Inkscape::SnapCandidatePoint const &p, // in desktop coordinates @@ -126,7 +111,7 @@ private: }; // end of ObjectSnapper class -void getBBoxPoints(Geom::OptRect const bbox, std::vector *points, bool const isTarget, bool const includeCorners, bool const includeLineMidpoints, bool const includeObjectMidpoints); +void getBBoxPoints(Geom::OptRect const bbox, std::vector *points, bool const isTarget, bool const includeCorners, bool const includeLineMidpoints, bool const includeObjectMidpoints, bool const isAlignment = false); } // end of namespace Inkscape diff --git a/src/object/sp-namedview.cpp b/src/object/sp-namedview.cpp index 8d902e8fe956f876f18a3d2a12319025f4f12e1c..68ee49a3107136690113c6c653a6915ef2d31990 100644 --- a/src/object/sp-namedview.cpp +++ b/src/object/sp-namedview.cpp @@ -200,6 +200,8 @@ void SPNamedView::build(SPDocument *document, Inkscape::XML::Node *repr) { this->readAttr(SPAttr::GRIDTOLERANCE); this->readAttr(SPAttr::GUIDETOLERANCE); this->readAttr(SPAttr::OBJECTTOLERANCE); + this->readAttr(SPAttr::ALIGNMENTTOLERANCE); + this->readAttr(SPAttr::DISTRIBUTIONTOLERANCE); this->readAttr(SPAttr::GUIDECOLOR); this->readAttr(SPAttr::GUIDEOPACITY); this->readAttr(SPAttr::GUIDEHICOLOR); @@ -246,6 +248,9 @@ void SPNamedView::build(SPDocument *document, Inkscape::XML::Node *repr) { this->readAttr(SPAttr::INKSCAPE_SNAP_BBOX_EDGE); this->readAttr(SPAttr::INKSCAPE_SNAP_BBOX_CORNER); this->readAttr(SPAttr::INKSCAPE_SNAP_PAGE_BORDER); + this->readAttr(SPAttr::INKSCAPE_SNAP_ALIGNMENT); + this->readAttr(SPAttr::INKSCAPE_SNAP_ALIGNMENT_SELF); + this->readAttr(SPAttr::INKSCAPE_SNAP_DISTRIBUTION); this->readAttr(SPAttr::INKSCAPE_CURRENT_LAYER); this->readAttr(SPAttr::INKSCAPE_CONNECTOR_SPACING); this->readAttr(SPAttr::INKSCAPE_LOCKGUIDES); @@ -313,6 +318,14 @@ void SPNamedView::set(SPAttr key, const gchar* value) { this->snap_manager.snapprefs.setObjectTolerance(value ? g_ascii_strtod(value, nullptr) : 20); this->requestModified(SP_OBJECT_MODIFIED_FLAG); break; + case SPAttr::ALIGNMENTTOLERANCE: + this->snap_manager.snapprefs.setAlignementTolerance(value ? g_ascii_strtod(value, nullptr) : 5); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::DISTRIBUTIONTOLERANCE: + this->snap_manager.snapprefs.setDistributionTolerance(value ? g_ascii_strtod(value, nullptr) : 5); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; case SPAttr::GUIDECOLOR: this->guidecolor = (this->guidecolor & 0xff) | (DEFAULTGUIDECOLOR & 0xffffff00); @@ -532,6 +545,18 @@ void SPNamedView::set(SPAttr key, const gchar* value) { this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PAGE_BORDER, value ? sp_str_to_bool(value) : FALSE); this->requestModified(SP_OBJECT_MODIFIED_FLAG); break; + case SPAttr::INKSCAPE_SNAP_ALIGNMENT: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_ALIGNMENT_CATEGORY, value ? sp_str_to_bool(value) : TRUE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::INKSCAPE_SNAP_ALIGNMENT_SELF: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_ALIGNMENT_HANDLE, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::INKSCAPE_SNAP_DISTRIBUTION: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_DISTRIBUTION_CATEGORY, value ? sp_str_to_bool(value) : TRUE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; case SPAttr::INKSCAPE_CURRENT_LAYER: this->default_layer_id = value ? g_quark_from_string(value) : 0; this->requestModified(SP_OBJECT_MODIFIED_FLAG); diff --git a/src/seltrans.cpp b/src/seltrans.cpp index f4d908961b0e34c8a62c69bb13a59ada6db36e50..922b5a80057c1a8b9f7393d7fe5246c3f221a676 100644 --- a/src/seltrans.cpp +++ b/src/seltrans.cpp @@ -300,9 +300,9 @@ void Inkscape::SelTrans::grab(Geom::Point const &p, gdouble x, gdouble y, bool s _bbox_points.clear(); // Collect the bounding box's corners and midpoints for each selected item - if (m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CATEGORY)) { - bool c = m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CORNER); - bool mp = m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_MIDPOINT); + if (m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CATEGORY, SNAPTARGET_ALIGNMENT_CATEGORY, SNAPTARGET_DISTRIBUTION_CATEGORY)) { + bool c = m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CORNER, SNAPTARGET_ALIGNMENT_CATEGORY, SNAPTARGET_DISTRIBUTION_CATEGORY); + bool mp = m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_MIDPOINT, SNAPTARGET_ALIGNMENT_CATEGORY, SNAPTARGET_DISTRIBUTION_CATEGORY); bool emp = m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_EDGE_MIDPOINT); // Preferably we'd use the bbox of each selected item, but for example 50 items will produce at least 200 bbox points, // which might make Inkscape crawl(see the comment a few lines above). In that case we will use the bbox of the selection as a whole @@ -490,6 +490,8 @@ void Inkscape::SelTrans::ungrab() _items_centers.clear(); _updateHandles(); } + + _desktop->snapindicator->remove_snaptarget(); } /* fixme: This is really bad, as we compare positions for each stamp (Lauris) */ @@ -1702,7 +1704,7 @@ void Inkscape::SelTrans::_keepClosestPointOnly(Geom::Point const &p) } // If we're not going to snap bounding boxes, then we might just as well get rid of their snappoints right away - if (!m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CATEGORY)) { + if (!m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CATEGORY) && !m.snapprefs.isTargetSnappable(SNAPTARGET_ALIGNMENT_CATEGORY)) { _bbox_points.clear(); } diff --git a/src/snap-candidate.h b/src/snap-candidate.h index 4854fbabec2f47f405c148ac0998decdedac521b..a3a1387b7d39c3fffb93c53c9ad86d71e459106a 100644 --- a/src/snap-candidate.h +++ b/src/snap-candidate.h @@ -38,7 +38,8 @@ public: _target_type(target), _source_num(source_num), _target_bbox(std::move(bbox)), - _dist() + _dist(), + _alignment(false) { }; @@ -47,7 +48,8 @@ public: _source_type(source), _target_type(target), _target_bbox(Geom::OptRect()), - _dist() + _dist(), + _alignment(false) { _source_num = -1; } @@ -58,7 +60,8 @@ public: _target_type(Inkscape::SNAPTARGET_UNDEFINED), _source_num(-1), _target_bbox(Geom::OptRect()), - _dist() + _dist(), + _alignment(true) { }; @@ -80,6 +83,7 @@ public: bool operator <(const SnapCandidatePoint &other) const { return _dist < other._dist; } // Needed for sorting the SnapCandidatePoints inline Geom::OptRect const getTargetBBox() const {return _target_bbox;} + inline bool considerForAlignment() const {return _alignment;} private: // Coordinates of the point Geom::Point _point; @@ -108,6 +112,9 @@ private: // For finding the snap candidate closest to the mouse pointer Geom::Coord _dist; + + // Consider this point for alignment snapping + bool _alignment; }; class SnapCandidateItem diff --git a/src/snap-enums.h b/src/snap-enums.h index 321963b08722544f82857523437254255e3a4dc0..199fc49186057c4ef354812892cbc8161520071a 100644 --- a/src/snap-enums.h +++ b/src/snap-enums.h @@ -50,6 +50,16 @@ enum SnapSourceType { // When adding source types here, then also update Inkscap SNAPSOURCE_TEXT_ANCHOR, SNAPSOURCE_OTHER_HANDLE, // eg. the handle of a gradient or of a connector (ie not being tied to a stroke) SNAPSOURCE_GRID_PITCH, // eg. when pasting or alt-dragging in the selector tool; not really a snap source + + //------------------------------------------------------------------- + // Alignment snapping + SNAPSOURCE_ALIGNMENT_CATEGORY = 256, + SNAPSOURCE_ALIGNMENT_BBOX_CORNER, + SNAPSOURCE_ALIGNMENT_BBOX_MIDPOINT, + SNAPSOURCE_ALIGNMENT_BBOX_EDGE_MIDPOINT, + SNAPSOURCE_ALIGNMENT_PAGE_CENTER, + SNAPSOURCE_ALIGNMENT_PAGE_CORNER, + SNAPSOURCE_ALIGNMENT_HANDLE }; enum SnapTargetType { @@ -97,6 +107,29 @@ enum SnapTargetType { SNAPTARGET_TEXT_BASELINE, SNAPTARGET_CONSTRAINED_ANGLE, SNAPTARGET_CONSTRAINT, + + //------------------------------------------------------------------- + // Alignment snapping + SNAPTARGET_ALIGNMENT_CATEGORY = 256, // will be used as a flag and must therefore be a power of two + SNAPTARGET_ALIGNMENT_BBOX_CORNER, + SNAPTARGET_ALIGNMENT_BBOX_MIDPOINT, + SNAPTARGET_ALIGNMENT_BBOX_EDGE_MIDPOINT, + SNAPTARGET_ALIGNMENT_PAGE_CENTER, + SNAPTARGET_ALIGNMENT_PAGE_CORNER, + SNAPTARGET_ALIGNMENT_HANDLE, + SNAPTARGET_ALIGNMENT_INTERSECTION, + + //------------------------------------------------------------------- + // Distribution snapping + SNAPTARGET_DISTRIBUTION_CATEGORY = 512, // will be used as a flag and must therefore be a power of two + SNAPTARGET_DISTRIBUTION_X, + SNAPTARGET_DISTRIBUTION_Y, + SNAPTARGET_DISTRIBUTION_RIGHT, + SNAPTARGET_DISTRIBUTION_LEFT, + SNAPTARGET_DISTRIBUTION_UP, + SNAPTARGET_DISTRIBUTION_DOWN, + SNAPTARGET_DISTRIBUTION_XY, + //------------------------------------------------------------------- SNAPTARGET_MAX_ENUM_VALUE }; diff --git a/src/snap-preferences.cpp b/src/snap-preferences.cpp index 876293aca5cf117816ce751fcdc43fb1e41644b3..5074182d69a66235b40034a41118f351a61c6acf 100644 --- a/src/snap-preferences.cpp +++ b/src/snap-preferences.cpp @@ -11,6 +11,7 @@ */ #include "inkscape.h" +#include "snap-enums.h" Inkscape::SnapPreferences::SnapPreferences() : _snap_enabled_globally(true), @@ -21,6 +22,8 @@ Inkscape::SnapPreferences::SnapPreferences() : { // Check for powers of two; see the comments in snap-enums.h g_assert((SNAPTARGET_BBOX_CATEGORY != 0) && !(SNAPTARGET_BBOX_CATEGORY & (SNAPTARGET_BBOX_CATEGORY - 1))); + g_assert((SNAPTARGET_DISTRIBUTION_CATEGORY != 0) && !(SNAPTARGET_DISTRIBUTION_CATEGORY & (SNAPTARGET_DISTRIBUTION_CATEGORY - 1))); + g_assert((SNAPTARGET_ALIGNMENT_CATEGORY != 0) && !(SNAPTARGET_ALIGNMENT_CATEGORY & (SNAPTARGET_ALIGNMENT_CATEGORY - 1))); g_assert((SNAPTARGET_NODE_CATEGORY != 0) && !(SNAPTARGET_NODE_CATEGORY & (SNAPTARGET_NODE_CATEGORY - 1))); g_assert((SNAPTARGET_DATUMS_CATEGORY != 0) && !(SNAPTARGET_DATUMS_CATEGORY & (SNAPTARGET_DATUMS_CATEGORY - 1))); g_assert((SNAPTARGET_OTHERS_CATEGORY != 0) && !(SNAPTARGET_OTHERS_CATEGORY & (SNAPTARGET_OTHERS_CATEGORY - 1))); @@ -45,7 +48,9 @@ void Inkscape::SnapPreferences::_mapTargetToArrayIndex(Inkscape::SnapTargetType if (target == SNAPTARGET_BBOX_CATEGORY || target == SNAPTARGET_NODE_CATEGORY || target == SNAPTARGET_OTHERS_CATEGORY || - target == SNAPTARGET_DATUMS_CATEGORY) { + target == SNAPTARGET_DATUMS_CATEGORY || + target == SNAPTARGET_ALIGNMENT_CATEGORY || + target == SNAPTARGET_DISTRIBUTION_CATEGORY) { // These main targets should be handled separately, because otherwise we might call isTargetSnappable() // for them (to check whether the corresponding group is on) which would lead to an infinite recursive loop always_on = (target == SNAPTARGET_DATUMS_CATEGORY); @@ -117,6 +122,16 @@ void Inkscape::SnapPreferences::_mapTargetToArrayIndex(Inkscape::SnapTargetType return; } + if (target & SNAPTARGET_ALIGNMENT_CATEGORY) { + group_on = isTargetSnappable(SNAPTARGET_ALIGNMENT_CATEGORY); + return; + } + + if (target & SNAPTARGET_DISTRIBUTION_CATEGORY) { + group_on = isTargetSnappable(SNAPTARGET_DISTRIBUTION_CATEGORY); + return; + } + if (target & SNAPTARGET_OTHERS_CATEGORY) { // Only if the group with "other" snap sources/targets has been enabled, then we might snap to any of those targets // ... but this doesn't hold for the page border, grids, and guides @@ -295,6 +310,23 @@ Inkscape::SnapTargetType Inkscape::SnapPreferences::source2target(Inkscape::Snap return SNAPTARGET_NODE_CATEGORY; case SNAPSOURCE_GRID_PITCH: return SNAPTARGET_GRID; + + + case SNAPSOURCE_ALIGNMENT_CATEGORY: + return SNAPTARGET_ALIGNMENT_CATEGORY; + case SNAPSOURCE_ALIGNMENT_BBOX_CORNER: + return SNAPTARGET_ALIGNMENT_BBOX_CORNER; + case SNAPSOURCE_ALIGNMENT_BBOX_MIDPOINT: + return SNAPTARGET_ALIGNMENT_BBOX_EDGE_MIDPOINT; + case SNAPSOURCE_ALIGNMENT_BBOX_EDGE_MIDPOINT: + return SNAPTARGET_ALIGNMENT_BBOX_EDGE_MIDPOINT; + case SNAPSOURCE_ALIGNMENT_PAGE_CENTER: + return SNAPTARGET_ALIGNMENT_PAGE_CENTER; + case SNAPSOURCE_ALIGNMENT_PAGE_CORNER: + return SNAPTARGET_ALIGNMENT_PAGE_CORNER; + case Inkscape::SNAPSOURCE_ALIGNMENT_HANDLE: + return SNAPTARGET_ALIGNMENT_HANDLE; + default: g_warning("Mapping of snap source to snap target undefined"); return SNAPTARGET_UNDEFINED; diff --git a/src/snap-preferences.h b/src/snap-preferences.h index 4c0bc02ae08583ef620485f902beb485ae896c05..34709c942d18358d74c3c9c5ba2e4b48d199f003 100644 --- a/src/snap-preferences.h +++ b/src/snap-preferences.h @@ -53,10 +53,14 @@ public: double getGridTolerance() const {return _grid_tolerance;} double getGuideTolerance() const {return _guide_tolerance;} double getObjectTolerance() const {return _object_tolerance;} + double getAlignmentTolerance() const {return _alignment_tolerance;} + double getDistributionTolerance() const {return _distribution_tolerance;} void setGridTolerance(double val) {_grid_tolerance = val;} void setGuideTolerance(double val) {_guide_tolerance = val;} void setObjectTolerance(double val) {_object_tolerance = val;} + void setAlignementTolerance(double val) {_alignment_tolerance = val;} + void setDistributionTolerance(double val) {_distribution_tolerance = val;} private: @@ -95,6 +99,8 @@ private: double _grid_tolerance; double _guide_tolerance; double _object_tolerance; + double _alignment_tolerance; + double _distribution_tolerance; }; } diff --git a/src/snap.cpp b/src/snap.cpp index c56358540b8fa5c378f92bd20ed781329806a4b6..a6b8b67196994afb26d48d7e4846643975df1b70 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -16,12 +16,23 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ +#include #include #include #include <2geom/transforms.h> #include "snap.h" +#include "snap-enums.h" +#include "preferences.h" +#include "object/sp-use.h" +#include "object/sp-mask.h" +#include "live_effects/effect-enum.h" +#include "object/sp-filter.h" +#include "object/sp-object.h" +#include "object/sp-clippath.h" +#include "object/sp-root.h" +#include "style.h" #include "desktop.h" #include "inkscape.h" @@ -43,6 +54,8 @@ using Inkscape::Util::round_to_lower_multiple_plus; SnapManager::SnapManager(SPNamedView const *v) : guide(this, 0), object(this, 0), + alignment(this, 0), + distribution(this, 0), snapprefs(), _named_view(v), _rotation_center_source_items(std::vector()), @@ -51,6 +64,14 @@ SnapManager::SnapManager(SPNamedView const *v) : _snapindicator(true), _unselected_nodes(nullptr) { + obj_snapper_candidates = std::make_unique>(); + align_snapper_candidates = std::make_unique>(); +} + +SnapManager::~SnapManager() +{ + obj_snapper_candidates->clear(); + align_snapper_candidates->clear(); } SnapManager::SnapperList SnapManager::getSnappers() const @@ -58,6 +79,8 @@ SnapManager::SnapperList SnapManager::getSnappers() const SnapManager::SnapperList s; s.push_back(&guide); s.push_back(&object); + s.push_back(&alignment); + s.push_back(&distribution); SnapManager::SnapperList gs = getGridSnappers(); s.splice(s.begin(), gs); @@ -132,6 +155,11 @@ Inkscape::SnappedPoint SnapManager::freeSnap(Inkscape::SnapCandidatePoint const IntermSnapResults isr; SnapperList const snappers = getSnappers(); + if (p.getSourceNum() <= 0){ + Geom::Rect const local_bbox_to_snap = bbox_to_snap ? *bbox_to_snap : Geom::Rect(p.getPoint(), p.getPoint()); + _findCandidates(getDocument()->getRoot(), &_items_to_ignore, p.getSourceNum() <= 0, local_bbox_to_snap, false, Geom::identity()); + } + for (auto snapper : snappers) { snapper->freeSnap(isr, p, bbox_to_snap, &_items_to_ignore, _unselected_nodes); } @@ -789,6 +817,154 @@ void SnapManager::displaySnapsource(Inkscape::SnapCandidatePoint const &p) const } } } + +void SnapManager::_findCandidates(SPObject* parent, + std::vector const *it, + bool const &first_point, + Geom::Rect const &bbox_to_snap, + bool const clip_or_mask, + Geom::Affine const additional_affine) const +{ + SPDesktop const *dt = getDesktop(); + if (dt == nullptr) { + g_error("desktop == NULL, so we cannot snap; please inform the developers of this bug"); + return; + // Apparently the setup() method from the SnapManager class hasn't been called before trying to snap. + } + + if (first_point) { + obj_snapper_candidates->clear(); + align_snapper_candidates->clear(); + } + + Geom::Rect bbox_to_snap_incl = bbox_to_snap; // _incl means: will include the snapper tolerance + bbox_to_snap_incl.expandBy(object.getSnapperTolerance()); // see? + + for (auto& o: parent->children) { + SPItem *item = dynamic_cast(&o); + if (item && !(dt->itemIsHidden(item) && !clip_or_mask)) { + // Fix LPE boolops selfsnaping + bool stop = false; + if (item->style) { + SPFilter *filt = item->style->getFilter(); + if (filt && filt->getId() && strcmp(filt->getId(), "selectable_hidder_filter") == 0) { + stop = true; + } + SPLPEItem *lpeitem = dynamic_cast(item); + if (lpeitem && lpeitem->hasPathEffectOfType(Inkscape::LivePathEffect::EffectType::BOOL_OP)) { + stop = true; + } + } + if (stop) { + stop = false; + for (auto skipitem : *it) { + if (skipitem && skipitem->style) { + SPItem *toskip = const_cast(skipitem); + if (toskip) { + SPFilter *filt = toskip->style->getFilter(); + if (filt && filt->getId() && strcmp(filt->getId(), "selectable_hidder_filter") == 0) { + stop = true; + break; + } + + SPLPEItem *lpeitem = dynamic_cast(toskip); + if (!stop && lpeitem && + lpeitem->hasPathEffectOfType(Inkscape::LivePathEffect::EffectType::BOOL_OP)) { + stop = true; + break; + } + } + } + } + if (stop) { + continue; + } + } + // Snapping to items in a locked layer is allowed + // Don't snap to hidden objects, unless they're a clipped path or a mask + /* See if this item is on the ignore list */ + std::vector::const_iterator i; + if (it != nullptr) { + i = it->begin(); + while (i != it->end() && *i != &o) { + ++i; + } + } + + if (it == nullptr || i == it->end()) { + if (item) { + if (!clip_or_mask) { // cannot clip or mask more than once + // The current item is not a clipping path or a mask, but might + // still be the subject of clipping or masking itself ; if so, then + // we should also consider that path or mask for snapping to + SPObject *obj = item->getClipObject(); + if (obj && snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_PATH_CLIP)) { + _findCandidates(obj, it, false, bbox_to_snap, true, item->i2doc_affine()); + } + obj = item->getMaskObject(); + if (obj && snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_PATH_MASK)) { + _findCandidates(obj, it, false, bbox_to_snap, true, item->i2doc_affine()); + } + } + + if (dynamic_cast(item)) { + _findCandidates(&o, it, false, bbox_to_snap, clip_or_mask, additional_affine); + } else { + Geom::OptRect bbox_of_item; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box", false); + // We'll only need to obtain the visual bounding box if the user preferences tell + // us to, AND if we are snapping to the bounding box itself. If we're snapping to + // paths only, then we can just as well use the geometric bounding box (which is faster) + SPItem::BBoxType bbox_type = (!prefs_bbox && snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_BBOX_CATEGORY)) ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + if (clip_or_mask) { + // Oh oh, this will get ugly. We cannot use sp_item_i2d_affine directly because we need to + // insert an additional transformation in document coordinates (code copied from sp_item_i2d_affine) + bbox_of_item = item->bounds(bbox_type, item->i2doc_affine() * additional_affine * dt->doc2dt()); + } else { + bbox_of_item = item->desktopBounds(bbox_type); + } + if (bbox_of_item) { + bool overflow = false; + // See if the item is within range + auto display_area = getDesktop()->get_display_area(); + if (display_area.contains(bbox_of_item->min()) || display_area.contains(bbox_of_item->max())) { + // Finally add the object to _candidates. + align_snapper_candidates->push_back(Inkscape::SnapCandidateItem(item, clip_or_mask, additional_affine)); + // For debugging: print the id of the candidate to the console + // SPObject *obj = (SPObject*)item; + // std::cout << "Snap candidate added: " << obj->getId() << std::endl; + + if (bbox_to_snap_incl.intersects(*bbox_of_item) + || (snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_ROTATION_CENTER) && bbox_to_snap_incl.contains(item->getCenter()))) { // rotation center might be outside of the bounding box + // This item is within snapping range, so record it as a candidate + obj_snapper_candidates->push_back(Inkscape::SnapCandidateItem(item, clip_or_mask, additional_affine)); + // For debugging: print the id of the candidate to the console + // SPObject *obj = (SPObject*)item; + // std::cout << "Snap candidate added: " << obj->getId() << std::endl; + } + + if (align_snapper_candidates->size() > 200) { // This makes Inkscape crawl already + overflow = true; + } + } + + if (overflow) { + static Glib::Timer timer; + if (timer.elapsed() > 1.0) { + timer.reset(); + std::cout << "Warning: limit of 200 snap target paths reached, some will be ignored" << std::endl; + } + break; + } + } + } + } + } + } + } +} /* Local Variables: mode:c++ diff --git a/src/snap.h b/src/snap.h index ba0cb15cec13b8be1ba400490fcabb92d52d66a5..ad34b44361f67e6841f113c77f04c64d6f9fd813 100644 --- a/src/snap.h +++ b/src/snap.h @@ -18,11 +18,14 @@ #ifndef SEEN_SNAP_H #define SEEN_SNAP_H +#include #include #include "guide-snapper.h" #include "object-snapper.h" +#include "alignment-snapper.h" #include "snap-preferences.h" +#include "distribution-snapper.h" // Guides @@ -82,13 +85,13 @@ public: SKEW, ROTATE }; - /** * Construct a SnapManager for a SPNamedView. * * @param v 'Owning' SPNamedView. */ SnapManager(SPNamedView const *v); + ~SnapManager(); typedef std::list SnapperList; @@ -337,6 +340,8 @@ public: Inkscape::GuideSnapper guide; ///< guide snapper Inkscape::ObjectSnapper object; ///< snapper to other objects + Inkscape::AlignmentSnapper alignment; ///< snapper to align with other objects + Inkscape::DistributionSnapper distribution; Inkscape::SnapPreferences snapprefs; /** @@ -428,6 +433,26 @@ private: bool _snapindicator; ///< When true, an indicator will be drawn at the position that was being snapped to std::vector *_unselected_nodes; ///< Nodes of the path that is currently being edited and which have not been selected and which will therefore be stationary. Only these nodes will be considered for snapping to. Of each unselected node both the position (Geom::Point) and the type (Inkscape::SnapTargetType) will be stored + /** + * Find all items within snapping range. + * @param parent Pointer to the document's root, or to a clipped path or mask object. + * @param it List of items to ignore. + * @param bbox_to_snap Bounding box hulling the whole bunch of points, all from the same selection and having the same transformation. + * @param clip_or_mask The parent object being passed is either a clip or mask. + */ + void _findCandidates(SPObject* parent, + std::vector const *it, + bool const &first_point, + Geom::Rect const &bbox_to_snap, + bool const _clip_or_mask, + Geom::Affine const additional_affine) const; + + std::unique_ptr> obj_snapper_candidates; + std::unique_ptr> align_snapper_candidates; + + friend class Inkscape::ObjectSnapper; + friend class Inkscape::AlignmentSnapper; + friend class Inkscape::DistributionSnapper; }; #endif // !SEEN_SNAP_H diff --git a/src/snapped-point.cpp b/src/snapped-point.cpp index 6ef67881ab0e75a7ba20852e41f3bacbc9ce93d0..040df81ce7780ed515af696671243825eaee824d 100644 --- a/src/snapped-point.cpp +++ b/src/snapped-point.cpp @@ -17,6 +17,9 @@ // overloaded constructor Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained, Geom::OptRect target_bbox) : _point(p), + //_alignment_target(Geom::Point(0,0)), + //_alignment_target2(Geom::Point(0,0)), + _equal_distance(Geom::infinity()), _tangent(Geom::Point(0,0)), _source(source), _source_num(source_num), @@ -35,8 +38,105 @@ Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p, SnapSourceType const { } +Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p, Geom::Point const &ap, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained, Geom::OptRect target_bbox) : + _point(p), + _alignment_target(ap), + //_alignment_target2(Geom::Point(0,0)), + _alignment_target_type(target), + _equal_distance(Geom::infinity()), + _tangent(Geom::Point(0,0)), + _source(source), + _source_num(source_num), + _target(target), + _at_intersection (false), + _constrained_snap (constrained_snap), + _fully_constrained (fully_constrained), + _distance(d), + _tolerance(std::max(t,1.0)),// tolerance should never be smaller than 1 px, as it is used for normalization in isOtherSnapBetter. We don't want a division by zero. + _always_snap(a), + _second_distance (Geom::infinity()), + _second_tolerance (1), + _second_always_snap (false), + _target_bbox(std::move(target_bbox)), + _pointer_distance (Geom::infinity()) +{ +} + +Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p, Geom::Point const &ap, Geom::Point const &ap2, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained, Geom::OptRect target_bbox) : + _point(p), + _alignment_target(ap), + _alignment_target2(ap2), + _alignment_target_type(target), + _equal_distance(Geom::infinity()), + _tangent(Geom::Point(0,0)), + _source(source), + _source_num(source_num), + _target(target), + _at_intersection (false), + _constrained_snap (constrained_snap), + _fully_constrained (fully_constrained), + _distance(d), + _tolerance(std::max(t,1.0)),// tolerance should never be smaller than 1 px, as it is used for normalization in isOtherSnapBetter. We don't want a division by zero. + _always_snap(a), + _second_distance (Geom::infinity()), + _second_tolerance (1), + _second_always_snap (false), + _target_bbox(std::move(target_bbox)), + _pointer_distance (Geom::infinity()) +{ +} + +Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p, std::vector const &bboxes, Geom::Rect const &source_bbox, Geom::Coord equal_dist, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained) : + _point(p), + _equal_distance(equal_dist), + _distribution_bboxes(std::move(bboxes)), + _source_bbox(source_bbox), + _tangent(Geom::Point(0,0)), + _source(source), + _source_num(source_num), + _target(target), + _at_intersection (false), + _constrained_snap (constrained_snap), + _fully_constrained (fully_constrained), + _distance(d), + _tolerance(std::max(t,1.0)),// tolerance should never be smaller than 1 px, as it is used for normalization in isOtherSnapBetter. We don't want a division by zero. + _always_snap(a), + _second_distance (Geom::infinity()), + _second_tolerance (1), + _second_always_snap (false), + //_target_bbox(std::move(target_bbox)), + _pointer_distance (Geom::infinity()) +{ +} + +Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p, std::vector const &bboxes, std::vector const &bboxes2, Geom::Rect const &source_bbox, Geom::Coord equal_dist, Geom::Coord equal_dist2, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained) : + _point(p), + _equal_distance(equal_dist), + _equal_distance2(equal_dist2), + _distribution_bboxes(std::move(bboxes)), + _distribution_bboxes2(std::move(bboxes2)), + _source_bbox(source_bbox), + _tangent(Geom::Point(0,0)), + _source(source), + _source_num(source_num), + _target(target), + _at_intersection (false), + _constrained_snap (constrained_snap), + _fully_constrained (fully_constrained), + _distance(d), + _tolerance(std::max(t,1.0)),// tolerance should never be smaller than 1 px, as it is used for normalization in isOtherSnapBetter. We don't want a division by zero. + _always_snap(a), + _second_distance (Geom::infinity()), + _second_tolerance (1), + _second_always_snap (false), + //_target_bbox(std::move(target_bbox)), + _pointer_distance (Geom::infinity()) +{ +} + Inkscape::SnappedPoint::SnappedPoint(Inkscape::SnapCandidatePoint const &p, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained) : _point (p.getPoint()), + _equal_distance(Geom::infinity()), _tangent (Geom::Point(0,0)), _source (p.getSourceType()), _source_num (p.getSourceNum()), @@ -57,6 +157,7 @@ Inkscape::SnappedPoint::SnappedPoint(Inkscape::SnapCandidatePoint const &p, Snap Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &at_intersection, bool const &constrained_snap, bool const &fully_constrained, Geom::Coord const &d2, Geom::Coord const &t2, bool const &a2) : _point(p), + _equal_distance(Geom::infinity()), _tangent (Geom::Point(0,0)), _source(source), _source_num(source_num), @@ -79,6 +180,7 @@ Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p, SnapSourceType const Inkscape::SnappedPoint::SnappedPoint(): _point (Geom::Point(0,0)), + _equal_distance(Geom::infinity()), _tangent (Geom::Point(0,0)), _source (SNAPSOURCE_UNDEFINED), _source_num (-1), @@ -99,6 +201,7 @@ Inkscape::SnappedPoint::SnappedPoint(): Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p): _point (p), + _equal_distance(Geom::infinity()), _tangent (Geom::Point(0,0)), _source (SNAPSOURCE_UNDEFINED), _source_num (-1), @@ -130,18 +233,77 @@ void Inkscape::SnappedPoint::getPointIfSnapped(Geom::Point &p) const } // search for the closest snapped point +// This function give preference to the snapped points that are not in SNAPTARGET_ALIGNMENT_CATEGORY +// ie. for example, a longer Corner to Corner snap will be given prefrence over +// a Corner to alignment snap with lesser snapDistance. +// +// Incase there is an alignment snap along with a distribution snap possible, this +// function returns a new SnappedPoint that is are mix of the both bool getClosestSP(std::list const &list, Inkscape::SnappedPoint &result) { bool success = false; + bool aligned_success = false; + + Inkscape::SnappedPoint aligned; for (std::list::const_iterator i = list.begin(); i != list.end(); ++i) { - if ((i == list.begin()) || (*i).getSnapDistance() < result.getSnapDistance()) { + bool alignment = (*i).getTarget() & Inkscape::SNAPTARGET_ALIGNMENT_CATEGORY; + if (i == list.begin()) { + result = *i; + success = !alignment; + aligned = *i; + aligned_success = alignment; + } else if (alignment) { + if (!aligned_success || (*i).getSnapDistance() <= aligned.getSnapDistance()) { + if ((*i).getSnapDistance() == aligned.getSnapDistance()) { + if ((*i).getDistanceToAlignTarget() < aligned.getDistanceToAlignTarget()) { + aligned = *i; + aligned_success = true; + } + } else { + aligned = *i; + aligned_success = true; + } + } + } else if (!success || (*i).getSnapDistance() < result.getSnapDistance()){ result = *i; success = true; } + + } + + if (!success && aligned_success) + result = aligned; + + // the following code merges an alignment snap and a distribution snap + if (success && aligned_success) { + auto type = result.getTarget(); + if (type & Inkscape::SNAPTARGET_DISTRIBUTION_CATEGORY) { + switch (type) { + case Inkscape::SNAPTARGET_DISTRIBUTION_X: + case Inkscape::SNAPTARGET_DISTRIBUTION_RIGHT: + case Inkscape::SNAPTARGET_DISTRIBUTION_LEFT: + result.setPoint({result.getPoint().x(), aligned.getPoint().y()}); + break; + case Inkscape::SNAPTARGET_DISTRIBUTION_Y: + case Inkscape::SNAPTARGET_DISTRIBUTION_UP: + case Inkscape::SNAPTARGET_DISTRIBUTION_DOWN: + result.setPoint({aligned.getPoint().x() ,result.getPoint().y()}); + break; + case Inkscape::SNAPTARGET_DISTRIBUTION_XY: + break; + default: + g_warning("getClosestSP(): unknown distribution snap target %i", result.getTarget()); + break; + } + result.setAlignmentTargetType(aligned.getAlignmentTargetType()); + result.setAlignmentTarget(aligned.getAlignmentTarget()); + result.setAlignmentTarget2(aligned.getAlignmentTarget2()); + return true; + } } - return success; + return success ? success : aligned_success; } bool Inkscape::SnappedPoint::isOtherSnapBetter(Inkscape::SnappedPoint const &other_one, bool weighted) const diff --git a/src/snapped-point.h b/src/snapped-point.h index 75b5472ccc9bdb7aeb8c0cc27810e84d68ac328d..aa094971fa76de70789f10258da00ec6f08c9e4b 100644 --- a/src/snapped-point.h +++ b/src/snapped-point.h @@ -15,7 +15,9 @@ #include <2geom/geom.h> #include +#include #include +#include #include "snap-candidate.h" @@ -31,6 +33,10 @@ public: SnappedPoint(Geom::Point const &p); SnappedPoint(Geom::Point const &p, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &at_intersection, bool const &constrained_snap, bool const &fully_constrained, Geom::Coord const &d2, Geom::Coord const &t2, bool const &a2); SnappedPoint(Geom::Point const &p, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained, Geom::OptRect target_bbox = Geom::OptRect()); + SnappedPoint(Geom::Point const &p, Geom::Point const &ap, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained, Geom::OptRect target_bbox); + SnappedPoint(Geom::Point const &p, Geom::Point const &ap, Geom::Point const &ap2, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained, Geom::OptRect target_bbox); + SnappedPoint(Geom::Point const &p, std::vector const &bboxes, Geom::Rect const &source_bbox, Geom::Coord equal_dist, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained); + SnappedPoint(Geom::Point const &p, std::vector const &bboxes,std::vector const &bboxes2, Geom::Rect const &source_bbox, Geom::Coord equal_dist, Geom::Coord equal_dist2, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained); SnappedPoint(SnapCandidatePoint const &p, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained); ~SnappedPoint(); @@ -45,6 +51,9 @@ public: Geom::Coord getPointerDistance() const {return _pointer_distance;} void setPointerDistance(Geom::Coord const d) {_pointer_distance = d;} + std::vector const &getBBoxes() const {return _distribution_bboxes;} + std::vector const &getBBoxes2() const {return _distribution_bboxes2;} + /* This is the preferred method to find out which point we have snapped * to, because it only returns a point if snapping has actually occurred * (by overwriting p) @@ -61,16 +70,52 @@ public: */ Geom::Point getPoint() const {return _point;} void setPoint(Geom::Point const &p) {_point = p;} + void setAlignmentTarget(std::optional const &p) { + if (p.has_value()) + _alignment_target = p; + } + void setAlignmentTarget2(std::optional const &p) { + if (p.has_value()) + _alignment_target2 = p; + } Geom::Point getTangent() const {return _tangent;} + std::optional getAlignmentTarget() const + { + if (_alignment_target.has_value()) + return *_alignment_target; + else + return {}; + } + + std::optional getAlignmentTarget2() const + { + if (_alignment_target2.has_value()) + return *_alignment_target2; + else + return {}; + } + + Geom::Coord getDistanceToAlignTarget() const + { + return _alignment_target.has_value() ? Geom::L2(_point - *_alignment_target) : Geom::infinity(); + } bool getAtIntersection() const {return _at_intersection;} bool getFullyConstrained() const {return _fully_constrained;} bool getConstrainedSnap() const {return _constrained_snap;} bool getSnapped() const {return _distance < Geom::infinity();} void setTarget(SnapTargetType const target) {_target = target;} + void setAlignmentTargetType(SnapTargetType const target) {_alignment_target_type = target;} SnapTargetType getTarget() const {return _target;} + SnapTargetType getAlignmentTargetType() const + { + return _alignment_target_type.has_value() ? *_alignment_target_type : SNAPTARGET_UNDEFINED; + } void setTargetBBox(Geom::OptRect const target) {_target_bbox = target;} Geom::OptRect const getTargetBBox() const {return _target_bbox;} + Geom::OptRect const getSourceBBox() const {return _source_bbox;} + Geom::Coord getDistributionDistance() const {return _equal_distance;} + Geom::Coord getDistributionDistance2() const {return _equal_distance2;} void setSource(SnapSourceType const source) {_source = source;} SnapSourceType getSource() const {return _source;} long getSourceNum() const {return _source_num;} @@ -97,9 +142,14 @@ public: protected: Geom::Point _point; // Location of the snapped point Geom::Point _tangent; // Tangent of the curve we snapped to, at the snapped point + std::optional _alignment_target; // Target point for alignment snapping + std::optional _alignment_target2; // Target point when alignment guides intersect + std::vector _distribution_bboxes; // A list of bounding boxes in case of distribution snapping + std::vector _distribution_bboxes2; // Target point when there is a bidirectional distribution snap SnapSourceType _source; // Describes what snapped long _source_num; // Sequence number of the source point that snapped, if that point is part of a set of points. (starting at zero if we might have a set of points; -1 if we only have a single point) SnapTargetType _target; // Describes to what we've snapped to + std::optional _alignment_target_type; // TODO: this is only used in case of both alignment and distribution snap bool _at_intersection; // If true, the snapped point is at an intersection bool _constrained_snap; // If true, then the snapped point was found when looking for a constrained snap bool _fully_constrained; // When snapping for example to a node, then the snap will be "fully constrained". @@ -119,10 +169,16 @@ protected: Geom::Coord _second_distance; /* The snapping tolerance in screen pixels (depends on zoom)*/ Geom::Coord _second_tolerance; + /* The equal distance between objects in screen pixels (depends on zoom) in case of distribution snapping*/ + Geom::Coord _equal_distance; + /* The equal distance between objects in screen pixels (depends on zoom) in case of bidirectional distribution snapping*/ + Geom::Coord _equal_distance2; /* If true then "Always snap" is on */ bool _second_always_snap; /* The bounding box we've snapped to (when applicable); will be used by the snapindicator */ Geom::OptRect _target_bbox; + /* The bounding box of the object we've snapped */ + Geom::OptRect _source_bbox; /* Distance from the un-transformed point to the mouse pointer, measured at the point in time when dragging started */ Geom::Coord _pointer_distance; }; diff --git a/src/ui/dialog/document-properties.cpp b/src/ui/dialog/document-properties.cpp index 8ddc09e234d8b89a076ab7f310fccbba9b993a29..16d59f9d863f8ea64c5054ac110de8e8c046d7e8 100644 --- a/src/ui/dialog/document-properties.cpp +++ b/src/ui/dialog/document-properties.cpp @@ -108,7 +108,7 @@ DocumentProperties::DocumentProperties() , _rum_deflt(_("Display _units:"), "inkscape:document-units", _wr) , _page_sizer(_wr) //--------------------------------------------------------------- - //General snap options + //General guide options , _rcb_sgui(_("Show _guides"), _("Show or hide guides"), "showguides", _wr) , _rcb_lgui(_("Lock all guides"), _("Toggle lock of all guides in the document"), "inkscape:lockguides", _wr) , _rcp_gui(_("Guide co_lor:"), _("Guideline color"), _("Color of guidelines"), "guidecolor", "guideopacity", _wr) @@ -130,6 +130,16 @@ DocumentProperties::DocumentProperties() _("Snapping distance, in screen pixels, for snapping to guides"), _("Always snap to guides, regardless of the distance"), _("If set, objects only snap to a guide when it's within the range specified below"), "guidetolerance", _wr) + //Options for alignement snapping + , _rsu_assn(_("Snap dista_nce"), _("Snap only when cl_oser than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for alignment snapping"), _("Always snap to alignment guides, regardless of the distance"), + _("If set, objects only snap to as alignment guide when it's within the range specified below"), + "alignmenttolerance", _wr) + //Options for distribution snapping + , _rsu_dssn(_("Snap distanc_e"), _("Snap only _when closer than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for distribution snapping"), _("Always snap objects at equal distance, regardless of the distance"), + _("If set, objects only snap to at equal distances when it's within the range specified below"), + "distributiontolerance", _wr) //--------------------------------------------------------------- , _rcb_snclp(_("Snap to clip paths"), _("When snapping to paths, then also try snapping to clip paths"), "inkscape:snap-path-clip", _wr) , _rcb_snmsk(_("Snap to mask paths"), _("When snapping to paths, then also try snapping to mask paths"), "inkscape:snap-path-mask", _wr) @@ -361,6 +371,10 @@ void DocumentProperties::build_snap() label_gr->set_markup (_("Snap to grids")); Gtk::Label *label_gu = Gtk::manage (new Gtk::Label); label_gu->set_markup (_("Snap to guides")); + Gtk::Label *label_as = Gtk::manage (new Gtk::Label); + label_as->set_markup (_("Alignment Snapping")); + Gtk::Label *label_ds = Gtk::manage (new Gtk::Label); + label_ds->set_markup (_("Distance Snapping")); Gtk::Label *label_m = Gtk::manage (new Gtk::Label); label_m->set_markup (_("Miscellaneous")); @@ -379,6 +393,12 @@ void DocumentProperties::build_snap() label_gu, nullptr, nullptr, _rsu_gusn._vbox, nullptr, nullptr, + label_as, nullptr, + nullptr, _rsu_assn._vbox, + nullptr, nullptr, + label_ds, nullptr, + nullptr, _rsu_dssn._vbox, + nullptr, nullptr, label_m, nullptr, nullptr, &_rcb_perp, nullptr, &_rcb_tang @@ -1404,6 +1424,8 @@ void DocumentProperties::update_widgets() _rsu_sno.setValue (nv->snap_manager.snapprefs.getObjectTolerance()); _rsu_sn.setValue (nv->snap_manager.snapprefs.getGridTolerance()); _rsu_gusn.setValue (nv->snap_manager.snapprefs.getGuideTolerance()); + _rsu_assn.setValue (nv->snap_manager.snapprefs.getAlignmentTolerance()); + _rsu_dssn.setValue (nv->snap_manager.snapprefs.getDistributionTolerance()); _rcb_snclp.setActive (nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_CLIP)); _rcb_snmsk.setActive (nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_MASK)); _rcb_perp.setActive (nv->snap_manager.snapprefs.getSnapPerp()); diff --git a/src/ui/dialog/document-properties.h b/src/ui/dialog/document-properties.h index 9a8a6bbd961b3cb458e3a6b7070bfeacb570b967..79d2b43fcab48a6ecc57c387a982a981d4d3f747 100644 --- a/src/ui/dialog/document-properties.h +++ b/src/ui/dialog/document-properties.h @@ -141,6 +141,8 @@ protected: UI::Widget::ToleranceSlider _rsu_sno; UI::Widget::ToleranceSlider _rsu_sn; UI::Widget::ToleranceSlider _rsu_gusn; + UI::Widget::ToleranceSlider _rsu_assn; + UI::Widget::ToleranceSlider _rsu_dssn; UI::Widget::RegisteredCheckButton _rcb_snclp; UI::Widget::RegisteredCheckButton _rcb_snmsk; UI::Widget::RegisteredCheckButton _rcb_perp; diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 2c48f93dcced2988fe7c45bca652b83df3f04110..cb59d885b8f614c941c605cc0265d1a478f8419a 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -2368,6 +2368,10 @@ void InkscapePreferences::initPageBehavior() _page_snapping.add_line( true, _("Snap indicator persistence (in seconds):"), _snap_persistence, "", _("Controls how long the snap indicator message will be shown, before it disappears"), true); + _snap_indicator_distance.init( _("Show snap distance in case of alignment or distribution snap"), "/options/snapindicatordistance/value", false); + _page_snapping.add_line( true, "", _snap_indicator_distance, "", + _("Show snap distance in case of alignment or distribution snap")); + _page_snapping.add_group_header( _("What should snap")); _snap_closest_only.init( _("Only snap the node closest to the pointer"), "/options/snapclosestonly/value", false); diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index a00dcae0a3ab42f60cca1c866d048e09f56cd8ff..e19208a68a4136c95829c4f1a3fa61f8852d7bb2 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -216,6 +216,7 @@ protected: UI::Widget::PrefCheckButton _snap_indicator; UI::Widget::PrefCheckButton _snap_closest_only; UI::Widget::PrefCheckButton _snap_mouse_pointer; + UI::Widget::PrefCheckButton _snap_indicator_distance; UI::Widget::PrefCombo _steps_rot_snap; UI::Widget::PrefCheckButton _steps_rot_relative; diff --git a/src/ui/tool/control-point-selection.cpp b/src/ui/tool/control-point-selection.cpp index 39245f177107c836f14567f5a1b6b27a178db146..2ee5d617d99ad2ff5bdb6d2396c26f40f4e30f94 100644 --- a/src/ui/tool/control-point-selection.cpp +++ b/src/ui/tool/control-point-selection.cpp @@ -18,6 +18,7 @@ #include "ui/tool/event-utils.h" #include "ui/tool/transform-handle-set.h" #include "ui/tool/node.h" +#include "display/control/snap-indicator.h" @@ -442,6 +443,7 @@ void ControlPointSelection::_pointDragged(Geom::Point &new_pos, GdkEventMotion * void ControlPointSelection::_pointUngrabbed() { + _desktop->snapindicator->remove_snaptarget(); _original_positions.clear(); _last_trans.clear(); _dragging = false; diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp index b011ce5106764009fa1df87454e4ef82acc94788..d55f42060c570b25ed909567e205716bdc7608d3 100644 --- a/src/ui/tools/tool-base.cpp +++ b/src/ui/tools/tool-base.cpp @@ -33,6 +33,7 @@ #include "display/control/canvas-item-catchall.h" // Grab/Ungrab #include "display/control/canvas-item-rotate.h" +#include "display/control/snap-indicator.h" #include "include/gtkmm_version.h" #include "include/macros.h" @@ -1046,6 +1047,7 @@ void ToolBase::grabCanvasEvents(Gdk::EventMask mask) */ void ToolBase::ungrabCanvasEvents() { + desktop->snapindicator->remove_snaptarget(); desktop->getCanvasCatchall()->ungrab(); } diff --git a/testfiles/src/attributes-test.cpp b/testfiles/src/attributes-test.cpp index f3e44aa3d74a1dba3aed22ce4ca7388c061f2349..dc0ad1d9e895a313e127f5b48fe6e3c9b8ef7c55 100644 --- a/testfiles/src/attributes-test.cpp +++ b/testfiles/src/attributes-test.cpp @@ -432,6 +432,9 @@ std::vector getKnownAttrs() AttributeInfo("inkscape:radius", true), AttributeInfo("inkscape:randomized", true), AttributeInfo("inkscape:rounded", true), + AttributeInfo("inkscape:snap-alignment", true), + AttributeInfo("inkscape:snap-alignment-self", true), + AttributeInfo("inkscape:snap-distribution", true), AttributeInfo("inkscape:snap-bbox", true), AttributeInfo("inkscape:snap-bbox-edge-midpoints", true), AttributeInfo("inkscape:snap-bbox-midpoints", true), @@ -511,6 +514,8 @@ std::vector getKnownAttrs() AttributeInfo("gridtolerance", true), AttributeInfo("guidetolerance", true), AttributeInfo("objecttolerance", true), + AttributeInfo("alignmenttolerance", true), + AttributeInfo("distributiontolerance", true), /* AttributeInfo("gridoriginx", true), AttributeInfo("gridoriginy", true), AttributeInfo("gridspacingx", true),