// 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-page.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" #include "page-manager.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)) { if (auto document = _snapmanager->getDocument()) { auto ignore_page = _snapmanager->getPageToIgnore(); for (auto page : document->getPageManager().getPages()) { if (ignore_page == page) continue; getBBoxPoints(page->getDesktopRect(), _points_to_snap_to.get(), true, SNAPSOURCE_ALIGNMENT_PAGE_CORNER, SNAPTARGET_ALIGNMENT_PAGE_CORNER, SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED, // No edges SNAPSOURCE_ALIGNMENT_PAGE_CENTER, SNAPTARGET_ALIGNMENT_PAGE_CENTER); } getBBoxPoints(document->preferredBounds(), _points_to_snap_to.get(), true, SNAPSOURCE_ALIGNMENT_PAGE_CORNER, SNAPTARGET_ALIGNMENT_PAGE_CORNER, SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED, // No edges 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, SNAPSOURCE_ALIGNMENT_BBOX_CORNER, SNAPTARGET_ALIGNMENT_BBOX_CORNER, SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED, // No edges SNAPSOURCE_ALIGNMENT_BBOX_MIDPOINT, SNAPTARGET_ALIGNMENT_BBOX_MIDPOINT); } } // 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) { if (_allowSourceToSnapToTarget(p.getSourceType(), k.getTargetType(), strict_snapping)) { 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, *sx.getAlignmentTarget(), *sy.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); } } } bool Inkscape::AlignmentSnapper::_allowSourceToSnapToTarget(SnapSourceType source, SnapTargetType target, bool strict_snapping) const { if (strict_snapping && (source == SNAPSOURCE_PAGE_CENTER || source == SNAPSOURCE_PAGE_CORNER)) { // Restrict page alignment snapping to just other pages (no objects please!) return target == SNAPTARGET_PAGE_CENTER || target == SNAPTARGET_PAGE_CORNER || target == SNAPTARGET_ALIGNMENT_PAGE_CENTER || target == SNAPTARGET_ALIGNMENT_PAGE_CORNER; } return true; } 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 { // toggle checks if (!_snap_enabled || !_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_ALIGNMENT_CATEGORY)) return; bool p_is_bbox = p.getSourceType() & SNAPSOURCE_BBOX_CATEGORY; bool p_is_node = p.getSourceType() & SNAPSOURCE_NODE_HANDLE; if (p.getSourceNum() <= 0) { Geom::Rect const local_bbox_to_snap = bbox_to_snap ? *bbox_to_snap : Geom::Rect(p.getPoint(), p.getPoint()); _snapmanager->_findCandidates(_snapmanager->getDocument()->getRoot(), it, local_bbox_to_snap, false, Geom::identity()); } 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 { // 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; bool p_is_bbox = p.getSourceType() & SNAPSOURCE_BBOX_CATEGORY; bool p_is_node = p.getSourceType() & SNAPSOURCE_NODE_HANDLE; if (p.getSourceNum() <= 0) { Geom::Rect const local_bbox_to_snap = bbox_to_snap ? *bbox_to_snap : Geom::Rect(p.getPoint(), p.getPoint()); _snapmanager->_findCandidates(_snapmanager->getDocument()->getRoot(), it, local_bbox_to_snap, false, Geom::identity()); } 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 :