summaryrefslogtreecommitdiffstats
path: root/src/ui/tool
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/ui/tool
parentInitial commit. (diff)
downloadinkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz
inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/ui/tool')
-rw-r--r--src/ui/tool/README29
-rw-r--r--src/ui/tool/commit-events.h52
-rw-r--r--src/ui/tool/control-point-selection.cpp784
-rw-r--r--src/ui/tool/control-point-selection.h179
-rw-r--r--src/ui/tool/control-point.cpp587
-rw-r--r--src/ui/tool/control-point.h413
-rw-r--r--src/ui/tool/curve-drag-point.cpp247
-rw-r--r--src/ui/tool/curve-drag-point.h77
-rw-r--r--src/ui/tool/event-utils.cpp93
-rw-r--r--src/ui/tool/event-utils.h129
-rw-r--r--src/ui/tool/manipulator.h174
-rw-r--r--src/ui/tool/modifier-tracker.cpp94
-rw-r--r--src/ui/tool/modifier-tracker.h55
-rw-r--r--src/ui/tool/multi-path-manipulator.cpp907
-rw-r--r--src/ui/tool/multi-path-manipulator.h159
-rw-r--r--src/ui/tool/node-types.h57
-rw-r--r--src/ui/tool/node.cpp1915
-rw-r--r--src/ui/tool/node.h513
-rw-r--r--src/ui/tool/path-manipulator.cpp1847
-rw-r--r--src/ui/tool/path-manipulator.h195
-rw-r--r--src/ui/tool/selectable-control-point.cpp150
-rw-r--r--src/ui/tool/selectable-control-point.h80
-rw-r--r--src/ui/tool/shape-record.h65
-rw-r--r--src/ui/tool/transform-handle-set.cpp827
-rw-r--r--src/ui/tool/transform-handle-set.h147
25 files changed, 9775 insertions, 0 deletions
diff --git a/src/ui/tool/README b/src/ui/tool/README
new file mode 100644
index 0000000..8a1c41a
--- /dev/null
+++ b/src/ui/tool/README
@@ -0,0 +1,29 @@
+
+
+This directory contains code related to on-screen editing (nodes, handles, etc.).
+
+Note that there are classes with similar functionality based on the SPKnot class in src/ui/knot.
+
+Classes here:
+
+ * ControlPoint
+ ** CurveDragPoint
+ ** Handle
+ ** RotationHandle
+ ** SelectableContrlPoint
+ *** Node
+ ** SelectorPoint,
+ ** TransformHandle
+ *** RotateHandle
+ *** ScaleHandle
+ **** ScaleCornerHandle
+ **** ScaleSideHandle
+ *** SkewHandle
+
+ * Manipulator
+ ** PointManipulator
+ *** MultiManipulator
+ *** PathManipulator
+ *** MultiPathManipulator
+ ** Selector
+ ** TransformHandleSet
diff --git a/src/ui/tool/commit-events.h b/src/ui/tool/commit-events.h
new file mode 100644
index 0000000..37fb861
--- /dev/null
+++ b/src/ui/tool/commit-events.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Commit events.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_COMMIT_EVENTS_H
+#define SEEN_UI_TOOL_COMMIT_EVENTS_H
+
+namespace Inkscape {
+namespace UI {
+
+/// This is used to provide sensible messages on the undo stack.
+enum CommitEvent {
+ COMMIT_MOUSE_MOVE,
+ COMMIT_KEYBOARD_MOVE_X,
+ COMMIT_KEYBOARD_MOVE_Y,
+ COMMIT_MOUSE_SCALE,
+ COMMIT_MOUSE_SCALE_UNIFORM,
+ COMMIT_KEYBOARD_SCALE_UNIFORM,
+ COMMIT_KEYBOARD_SCALE_X,
+ COMMIT_KEYBOARD_SCALE_Y,
+ COMMIT_MOUSE_ROTATE,
+ COMMIT_KEYBOARD_ROTATE,
+ COMMIT_MOUSE_SKEW_X,
+ COMMIT_MOUSE_SKEW_Y,
+ COMMIT_KEYBOARD_SKEW_X,
+ COMMIT_KEYBOARD_SKEW_Y,
+ COMMIT_FLIP_X,
+ COMMIT_FLIP_Y
+};
+
+} // namespace UI
+} // 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/ui/tool/control-point-selection.cpp b/src/ui/tool/control-point-selection.cpp
new file mode 100644
index 0000000..b94129c
--- /dev/null
+++ b/src/ui/tool/control-point-selection.cpp
@@ -0,0 +1,784 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Node selection - implementation.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <boost/none.hpp>
+#include "ui/tool/selectable-control-point.h"
+#include <2geom/transforms.h>
+#include "desktop.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/transform-handle-set.h"
+#include "ui/tool/node.h"
+#include "display/control/snap-indicator.h"
+#include "ui/widget/canvas.h"
+
+
+
+#include <gdk/gdkkeysyms.h>
+
+namespace Inkscape {
+namespace UI {
+
+/**
+ * @class ControlPointSelection
+ * Group of selected control points.
+ *
+ * Some operations can be performed on all selected points regardless of their type, therefore
+ * this class is also a Manipulator. It handles the transformations of points using
+ * the keyboard.
+ *
+ * The exposed interface is similar to that of an STL set. Internally, a hash map is used.
+ * @todo Correct iterators (that don't expose the connection list)
+ */
+
+/** @var ControlPointSelection::signal_update
+ * Fires when the display needs to be updated to reflect changes.
+ */
+/** @var ControlPointSelection::signal_point_changed
+ * Fires when a control point is added to or removed from the selection.
+ * The first param contains a pointer to the control point that changed sel. state.
+ * The second says whether the point is currently selected.
+ */
+/** @var ControlPointSelection::signal_commit
+ * Fires when a change that needs to be committed to XML happens.
+ */
+
+ControlPointSelection::ControlPointSelection(SPDesktop *d, Inkscape::CanvasItemGroup *th_group)
+ : Manipulator(d)
+ , _handles(new TransformHandleSet(d, th_group))
+ , _dragging(false)
+ , _handles_visible(true)
+ , _one_node_handles(false)
+{
+ signal_update.connect( sigc::bind(
+ sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
+ true));
+ ControlPoint::signal_mouseover_change.connect(
+ sigc::hide(
+ sigc::mem_fun(*this, &ControlPointSelection::_mouseoverChanged)));
+ _handles->signal_transform.connect(
+ sigc::mem_fun(*this, &ControlPointSelection::transform));
+ _handles->signal_commit.connect(
+ sigc::mem_fun(*this, &ControlPointSelection::_commitHandlesTransform));
+}
+
+ControlPointSelection::~ControlPointSelection()
+{
+ clear();
+ delete _handles;
+}
+
+/** Add a control point to the selection. */
+std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(const value_type &x, bool notify, bool to_update)
+{
+ iterator found = _points.find(x);
+ if (found != _points.end()) {
+ return std::pair<iterator, bool>(found, false);
+ }
+
+ found = _points.insert(x).first;
+ _points_list.push_back(x);
+
+ x->updateState();
+
+ if (to_update) {
+ _update();
+ }
+ if (notify) {
+ signal_selection_changed.emit(std::vector<key_type>(1, x), true);
+ }
+
+ return std::pair<iterator, bool>(found, true);
+}
+
+/** Remove a point from the selection. */
+void ControlPointSelection::erase(iterator pos, bool to_update)
+{
+ SelectableControlPoint *erased = *pos;
+ _points_list.remove(*pos);
+ _points.erase(pos);
+ erased->updateState();
+ if (to_update) {
+ _update();
+ }
+}
+ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k, bool notify)
+{
+ iterator pos = _points.find(k);
+ if (pos == _points.end()) return 0;
+ erase(pos);
+
+ if (notify) {
+ signal_selection_changed.emit(std::vector<key_type>(1, k), false);
+ }
+ return 1;
+}
+void ControlPointSelection::erase(iterator first, iterator last)
+{
+ std::vector<SelectableControlPoint *> out(first, last);
+ while (first != last) {
+ erase(first++, false);
+ }
+ _update();
+ signal_selection_changed.emit(out, false);
+}
+
+/** Remove all points from the selection, making it empty. */
+void ControlPointSelection::clear()
+{
+ if (empty()) {
+ return;
+ }
+
+ std::vector<SelectableControlPoint *> out(begin(), end()); // begin() takes from _points
+ _points.clear();
+ _points_list.clear();
+ for (auto erased : out) {
+ erased->updateState();
+ }
+
+ _update();
+ signal_selection_changed.emit(out, false);
+}
+
+/** Select all points that this selection can contain. */
+void ControlPointSelection::selectAll()
+{
+ for (auto _all_point : _all_points) {
+ insert(_all_point, false, false);
+ }
+ std::vector<SelectableControlPoint *> out(_all_points.begin(), _all_points.end());
+ if (!out.empty()) {
+ _update();
+ signal_selection_changed.emit(out, true);
+ }
+}
+/** Select all points inside the given rectangle (in desktop coordinates). */
+void ControlPointSelection::selectArea(Geom::Path const &path, bool invert)
+{
+ std::vector<SelectableControlPoint *> out;
+ for (auto _all_point : _all_points) {
+ if (path.winding(*_all_point) % 2 != 0) {
+ if (invert) {
+ erase(_all_point);
+ } else {
+ insert(_all_point, false, false);
+ }
+ out.push_back(_all_point);
+ }
+ }
+ if (!out.empty()) {
+ _update();
+ signal_selection_changed.emit(out, true);
+ }
+}
+/** Unselect all selected points and select all unselected points. */
+void ControlPointSelection::invertSelection()
+{
+ std::vector<SelectableControlPoint *> in, out;
+ for (auto _all_point : _all_points) {
+ if (_all_point->selected()) {
+ in.push_back(_all_point);
+ erase(_all_point);
+ }
+ else {
+ out.push_back(_all_point);
+ insert(_all_point, false, false);
+ }
+ }
+ _update();
+ if (!in.empty())
+ signal_selection_changed.emit(in, false);
+ if (!out.empty())
+ signal_selection_changed.emit(out, true);
+}
+void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir)
+{
+ bool grow = (dir > 0);
+ Geom::Point p = origin->position();
+ double best_dist = grow ? HUGE_VAL : 0;
+ SelectableControlPoint *match = nullptr;
+ for (auto _all_point : _all_points) {
+ bool selected = _all_point->selected();
+ if (grow && !selected) {
+ double dist = Geom::distance(_all_point->position(), p);
+ if (dist < best_dist) {
+ best_dist = dist;
+ match = _all_point;
+ }
+ }
+ if (!grow && selected) {
+ double dist = Geom::distance(_all_point->position(), p);
+ // use >= to also deselect the origin node when it's the last one selected
+ if (dist >= best_dist) {
+ best_dist = dist;
+ match = _all_point;
+ }
+ }
+ }
+ if (match) {
+ if (grow) insert(match);
+ else erase(match);
+ signal_selection_changed.emit(std::vector<value_type>(1, match), grow);
+ }
+}
+
+/** Transform all selected control points by the given affine transformation. */
+void ControlPointSelection::transform(Geom::Affine const &m)
+{
+ for (auto cur : _points) {
+ cur->transform(m);
+ }
+ for (auto cur : _points) {
+ cur->fixNeighbors();
+ }
+
+ _updateBounds();
+ // TODO preserving the rotation radius needs some rethinking...
+ if (_rot_radius) (*_rot_radius) *= m.descrim();
+ if (_mouseover_rot_radius) (*_mouseover_rot_radius) *= m.descrim();
+ signal_update.emit();
+}
+
+/** Align control points on the specified axis. */
+void ControlPointSelection::align(Geom::Dim2 axis, AlignTargetNode target)
+{
+ if (empty()) return;
+ Geom::Dim2 d = static_cast<Geom::Dim2>((axis + 1) % 2);
+
+ Geom::OptInterval bound;
+ for (auto _point : _points) {
+ bound.unionWith(Geom::OptInterval(_point->position()[d]));
+ }
+
+ if (!bound) { return; }
+
+ double new_coord;
+ switch (target) {
+ case AlignTargetNode::FIRST_NODE:
+ new_coord=(_points_list.front())->position()[d];
+ break;
+ case AlignTargetNode::LAST_NODE:
+ new_coord=(_points_list.back())->position()[d];
+ break;
+ case AlignTargetNode::MID_NODE:
+ new_coord=bound->middle();
+ break;
+ case AlignTargetNode::MIN_NODE:
+ new_coord=bound->min();
+ break;
+ case AlignTargetNode::MAX_NODE:
+ new_coord=bound->max();
+ break;
+ default:
+ return;
+ }
+
+ for (auto _point : _points) {
+ Geom::Point pos = _point->position();
+ pos[d] = new_coord;
+ _point->move(pos);
+ }
+}
+
+/** Equdistantly distribute control points by moving them in the specified dimension. */
+void ControlPointSelection::distribute(Geom::Dim2 d)
+{
+ if (empty()) return;
+
+ // this needs to be a multimap, otherwise it will fail when some points have the same coord
+ typedef std::multimap<double, SelectableControlPoint*> SortMap;
+
+ SortMap sm;
+ Geom::OptInterval bound;
+ // first we insert all points into a multimap keyed by the aligned coord to sort them
+ // simultaneously we compute the extent of selection
+ for (auto _point : _points) {
+ Geom::Point pos = _point->position();
+ sm.insert(std::make_pair(pos[d], _point));
+ bound.unionWith(Geom::OptInterval(pos[d]));
+ }
+
+ if (!bound) { return; }
+
+ // now we iterate over the multimap and set aligned positions.
+ double step = size() == 1 ? 0 : bound->extent() / (size() - 1);
+ double start = bound->min();
+ unsigned num = 0;
+ for (SortMap::iterator i = sm.begin(); i != sm.end(); ++i, ++num) {
+ Geom::Point pos = i->second->position();
+ pos[d] = start + num * step;
+ i->second->move(pos);
+ }
+}
+
+/** Get the bounds of the selection.
+ * @return Smallest rectangle containing the positions of all selected points,
+ * or nothing if the selection is empty */
+Geom::OptRect ControlPointSelection::pointwiseBounds()
+{
+ return _bounds;
+}
+
+Geom::OptRect ControlPointSelection::bounds()
+{
+ return size() == 1 ? (*_points.begin())->bounds() : _bounds;
+}
+
+void ControlPointSelection::showTransformHandles(bool v, bool one_node)
+{
+ _one_node_handles = one_node;
+ _handles_visible = v;
+ _updateTransformHandles(false);
+}
+
+void ControlPointSelection::hideTransformHandles()
+{
+ _handles->setVisible(false);
+}
+void ControlPointSelection::restoreTransformHandles()
+{
+ _updateTransformHandles(true);
+}
+
+void ControlPointSelection::toggleTransformHandlesMode()
+{
+ if (_handles->mode() == TransformHandleSet::MODE_SCALE) {
+ _handles->setMode(TransformHandleSet::MODE_ROTATE_SKEW);
+ if (size() == 1) {
+ _handles->rotationCenter().setVisible(false);
+ }
+ } else {
+ _handles->setMode(TransformHandleSet::MODE_SCALE);
+ }
+}
+
+void ControlPointSelection::_pointGrabbed(SelectableControlPoint *point)
+{
+ hideTransformHandles();
+ _dragging = true;
+ _grabbed_point = point;
+ _farthest_point = point;
+ double maxdist = 0;
+ Geom::Affine m;
+ m.setIdentity();
+ for (auto _point : _points) {
+ _original_positions.insert(std::make_pair(_point, _point->position()));
+ _last_trans.insert(std::make_pair(_point, m));
+ double dist = Geom::distance(*_grabbed_point, *_point);
+ if (dist > maxdist) {
+ maxdist = dist;
+ _farthest_point = _point;
+ }
+ }
+}
+
+void ControlPointSelection::_pointDragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ Geom::Point abs_delta = new_pos - _original_positions[_grabbed_point];
+ double fdist = Geom::distance(_original_positions[_grabbed_point], _original_positions[_farthest_point]);
+ if (held_only_alt(*event) && fdist > 0) {
+ // Sculpting
+ for (auto cur : _points) {
+ Geom::Affine trans;
+ trans.setIdentity();
+ double dist = Geom::distance(_original_positions[cur], _original_positions[_grabbed_point]);
+ double deltafrac = 0.5 + 0.5 * cos(M_PI * dist/fdist);
+ if (dist != 0.0) {
+ // The sculpting transformation is not affine, but it can be
+ // locally approximated by one. Here we compute the local
+ // affine approximation of the sculpting transformation near
+ // the currently transformed point. We then transform the point
+ // by this approximation. This gives us sensible behavior for node handles.
+ // NOTE: probably it would be better to transform the node handles,
+ // but ControlPointSelection is supposed to work for any
+ // SelectableControlPoints, not only Nodes. We could create a specialized
+ // NodeSelection class that inherits from this one and move sculpting there.
+ Geom::Point origdx(Geom::EPSILON, 0);
+ Geom::Point origdy(0, Geom::EPSILON);
+ Geom::Point origp = _original_positions[cur];
+ Geom::Point origpx = _original_positions[cur] + origdx;
+ Geom::Point origpy = _original_positions[cur] + origdy;
+ double distdx = Geom::distance(origpx, _original_positions[_grabbed_point]);
+ double distdy = Geom::distance(origpy, _original_positions[_grabbed_point]);
+ double deltafracdx = 0.5 + 0.5 * cos(M_PI * distdx/fdist);
+ double deltafracdy = 0.5 + 0.5 * cos(M_PI * distdy/fdist);
+ Geom::Point newp = origp + abs_delta * deltafrac;
+ Geom::Point newpx = origpx + abs_delta * deltafracdx;
+ Geom::Point newpy = origpy + abs_delta * deltafracdy;
+ Geom::Point newdx = (newpx - newp) / Geom::EPSILON;
+ Geom::Point newdy = (newpy - newp) / Geom::EPSILON;
+
+ Geom::Affine itrans(newdx[Geom::X], newdx[Geom::Y], newdy[Geom::X], newdy[Geom::Y], 0, 0);
+ if (itrans.isSingular())
+ itrans.setIdentity();
+
+ trans *= Geom::Translate(-cur->position());
+ trans *= _last_trans[cur].inverse();
+ trans *= itrans;
+ trans *= Geom::Translate(_original_positions[cur] + abs_delta * deltafrac);
+ _last_trans[cur] = itrans;
+ } else {
+ trans *= Geom::Translate(-cur->position() + _original_positions[cur] + abs_delta * deltafrac);
+ }
+ cur->transform(trans);
+ //cur->move(_original_positions[cur] + abs_delta * deltafrac);
+ }
+ } else {
+ Geom::Point delta = new_pos - _grabbed_point->position();
+ for (auto cur : _points) {
+ cur->move(_original_positions[cur] + abs_delta);
+ }
+ _handles->rotationCenter().move(_handles->rotationCenter().position() + delta);
+ }
+ for (auto cur : _points) {
+ cur->fixNeighbors();
+ }
+ signal_update.emit();
+}
+
+void ControlPointSelection::_pointUngrabbed()
+{
+ _desktop->snapindicator->remove_snaptarget();
+ _original_positions.clear();
+ _last_trans.clear();
+ _dragging = false;
+ _grabbed_point = _farthest_point = nullptr;
+ _updateBounds();
+ restoreTransformHandles();
+ signal_commit.emit(COMMIT_MOUSE_MOVE);
+}
+
+bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventButton *event)
+{
+ // clicking a selected node should toggle the transform handles between rotate and scale mode,
+ // if they are visible
+ if (held_no_modifiers(*event) && _handles_visible && p->selected()) {
+ toggleTransformHandlesMode();
+ return true;
+ }
+ return false;
+}
+
+void ControlPointSelection::_mouseoverChanged()
+{
+ _mouseover_rot_radius = std::nullopt;
+}
+
+void ControlPointSelection::_update()
+{
+ _updateBounds();
+ _updateTransformHandles(false);
+ if (_bounds) {
+ _handles->rotationCenter().move(_bounds->midpoint());
+ }
+}
+
+void ControlPointSelection::_updateBounds()
+{
+ _rot_radius = std::nullopt;
+ _bounds = Geom::OptRect();
+ for (auto cur : _points) {
+ Geom::Point p = cur->position();
+ if (!_bounds) {
+ _bounds = Geom::Rect(p, p);
+ } else {
+ _bounds->expandTo(p);
+ }
+ }
+}
+
+void ControlPointSelection::_updateTransformHandles(bool preserve_center)
+{
+ if (_dragging) return;
+
+ if (_handles_visible && size() > 1) {
+ _handles->setBounds(*bounds(), preserve_center);
+ _handles->setVisible(true);
+ } else if (_one_node_handles && size() == 1) { // only one control point in selection
+ SelectableControlPoint *p = *begin();
+ _handles->setBounds(p->bounds());
+ _handles->rotationCenter().move(p->position());
+ _handles->rotationCenter().setVisible(false);
+ _handles->setVisible(true);
+ } else {
+ _handles->setVisible(false);
+ }
+}
+
+/** Moves the selected points along the supplied unit vector according to
+ * the modifier state of the supplied event. */
+bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir)
+{
+ if (held_control(event)) return false;
+ unsigned num = 1 + Tools::gobble_key_events(shortcut_key(event), 0);
+
+ Geom::Point delta = dir * num;
+ if (held_shift(event)) delta *= 10;
+ if (held_alt(event)) {
+ delta /= _desktop->current_zoom();
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px");
+ delta *= nudge;
+ }
+
+ transform(Geom::Translate(delta));
+ if (fabs(dir[Geom::X]) > 0) {
+ signal_commit.emit(COMMIT_KEYBOARD_MOVE_X);
+ } else {
+ signal_commit.emit(COMMIT_KEYBOARD_MOVE_Y);
+ }
+ return true;
+}
+
+/**
+ * Computes the distance to the farthest corner of the bounding box.
+ * Used to determine what it means to "rotate by one pixel".
+ */
+double ControlPointSelection::_rotationRadius(Geom::Point const &rc)
+{
+ if (empty()) return 1.0; // some safe value
+ Geom::Rect b = *bounds();
+ double maxlen = 0;
+ for (unsigned i = 0; i < 4; ++i) {
+ double len = Geom::distance(b.corner(i), rc);
+ if (len > maxlen) maxlen = len;
+ }
+ return maxlen;
+}
+
+/**
+ * Rotates the selected points in the given direction according to the modifier state
+ * from the supplied event.
+ * @param event Key event to take modifier state from
+ * @param dir Direction of rotation (math convention: 1 = counterclockwise, -1 = clockwise)
+ */
+bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
+{
+ if (empty()) return false;
+
+ Geom::Point rc;
+
+ // rotate around the mouseovered point, or the selection's rotation center
+ // if nothing is mouseovered
+ double radius;
+ SelectableControlPoint *scp =
+ dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
+ if (scp) {
+ rc = scp->position();
+ if (!_mouseover_rot_radius) {
+ _mouseover_rot_radius = _rotationRadius(rc);
+ }
+ radius = *_mouseover_rot_radius;
+ } else {
+ rc = _handles->rotationCenter();
+ if (!_rot_radius) {
+ _rot_radius = _rotationRadius(rc);
+ }
+ radius = *_rot_radius;
+ }
+
+ double angle;
+ if (held_alt(event)) {
+ // Rotate by "one pixel". We interpret this as rotating by an angle that causes
+ // the topmost point of a circle circumscribed about the selection's bounding box
+ // to move on an arc 1 screen pixel long.
+ angle = atan2(1.0 / _desktop->current_zoom(), radius) * dir;
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ angle = M_PI * dir / snaps;
+ }
+
+ // translate to origin, rotate, translate back to original position
+ Geom::Affine m = Geom::Translate(-rc)
+ * Geom::Rotate(angle) * Geom::Translate(rc);
+ transform(m);
+ signal_commit.emit(COMMIT_KEYBOARD_ROTATE);
+ return true;
+}
+
+
+bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir)
+{
+ if (empty()) return false;
+
+ double maxext = bounds()->maxExtent();
+ if (Geom::are_near(maxext, 0)) return false;
+
+ Geom::Point center;
+ SelectableControlPoint *scp =
+ dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
+ if (scp) {
+ center = scp->position();
+ } else {
+ center = _handles->rotationCenter().position();
+ }
+
+ double length_change;
+ if (held_alt(event)) {
+ // Scale by "one pixel". It means shrink/grow 1px for the larger dimension
+ // of the bounding box.
+ length_change = 1.0 / _desktop->current_zoom() * dir;
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000, "px");
+ length_change *= dir;
+ }
+ double scale = (maxext + length_change) / maxext;
+
+ Geom::Affine m = Geom::Translate(-center) * Geom::Scale(scale) * Geom::Translate(center);
+ transform(m);
+ signal_commit.emit(COMMIT_KEYBOARD_SCALE_UNIFORM);
+ return true;
+}
+
+bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d)
+{
+ if (empty()) return false;
+
+ Geom::Scale scale_transform(1, 1);
+ if (d == Geom::X) {
+ scale_transform = Geom::Scale(-1, 1);
+ } else {
+ scale_transform = Geom::Scale(1, -1);
+ }
+
+ SelectableControlPoint *scp =
+ dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
+ Geom::Point center = scp ? scp->position() : _handles->rotationCenter().position();
+
+ Geom::Affine m = Geom::Translate(-center) * scale_transform * Geom::Translate(center);
+ transform(m);
+ signal_commit.emit(d == Geom::X ? COMMIT_FLIP_X : COMMIT_FLIP_Y);
+ return true;
+}
+
+void ControlPointSelection::_commitHandlesTransform(CommitEvent ce)
+{
+ _updateBounds();
+ _updateTransformHandles(true);
+ signal_commit.emit(ce);
+}
+
+bool ControlPointSelection::event(Inkscape::UI::Tools::ToolBase * /*event_context*/, GdkEvent *event)
+{
+ // implement generic event handling that should apply for all control point selections here;
+ // for example, keyboard moves and transformations. This way this functionality doesn't need
+ // to be duplicated in many places
+ // Later split out so that it can be reused in object selection
+
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ // do not handle key events if the selection is empty
+ if (empty()) break;
+
+ switch(shortcut_key(event->key)) {
+ // moves
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_8:
+ return _keyboardMove(event->key, Geom::Point(0, -_desktop->yaxisdir()));
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ case GDK_KEY_KP_2:
+ return _keyboardMove(event->key, Geom::Point(0, _desktop->yaxisdir()));
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ case GDK_KEY_KP_6:
+ return _keyboardMove(event->key, Geom::Point(1, 0));
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ case GDK_KEY_KP_4:
+ return _keyboardMove(event->key, Geom::Point(-1, 0));
+
+ // rotates
+ case GDK_KEY_bracketleft:
+ return _keyboardRotate(event->key, -_desktop->yaxisdir());
+ case GDK_KEY_bracketright:
+ return _keyboardRotate(event->key, _desktop->yaxisdir());
+
+ // scaling
+ case GDK_KEY_less:
+ case GDK_KEY_comma:
+ return _keyboardScale(event->key, -1);
+ case GDK_KEY_greater:
+ case GDK_KEY_period:
+ return _keyboardScale(event->key, 1);
+
+ // TODO: skewing
+
+ // flipping
+ // NOTE: H is horizontal flip, while Shift+H switches transform handle mode!
+ case GDK_KEY_h:
+ case GDK_KEY_H:
+ if (held_shift(event->key)) {
+ toggleTransformHandlesMode();
+ return true;
+ }
+ // any modifiers except shift should cause no action
+ if (held_any_modifiers(event->key)) break;
+ return _keyboardFlip(Geom::X);
+ case GDK_KEY_v:
+ case GDK_KEY_V:
+ if (held_any_modifiers(event->key)) break;
+ return _keyboardFlip(Geom::Y);
+ default: break;
+ }
+ break;
+ default: break;
+ }
+ return false;
+}
+
+void ControlPointSelection::getOriginalPoints(std::vector<Inkscape::SnapCandidatePoint> &pts)
+{
+ pts.clear();
+ for (auto _point : _points) {
+ pts.emplace_back(_original_positions[_point], SNAPSOURCE_NODE_HANDLE);
+ }
+}
+
+void ControlPointSelection::getUnselectedPoints(std::vector<Inkscape::SnapCandidatePoint> &pts)
+{
+ pts.clear();
+ ControlPointSelection::Set &nodes = this->allPoints();
+ for (auto node : nodes) {
+ if (!node->selected()) {
+ Node *n = static_cast<Node*>(node);
+ pts.push_back(n->snapCandidatePoint());
+ }
+ }
+}
+
+void ControlPointSelection::setOriginalPoints()
+{
+ _original_positions.clear();
+ for (auto _point : _points) {
+ _original_positions.insert(std::make_pair(_point, _point->position()));
+ }
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ 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/ui/tool/control-point-selection.h b/src/ui/tool/control-point-selection.h
new file mode 100644
index 0000000..36260f8
--- /dev/null
+++ b/src/ui/tool/control-point-selection.h
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Control point selection - stores a set of control points and applies transformations
+ * to them
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_CONTROL_POINT_SELECTION_H
+#define SEEN_UI_TOOL_CONTROL_POINT_SELECTION_H
+
+#include <list>
+#include <memory>
+#include <unordered_map>
+#include <unordered_set>
+#include <optional>
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include <2geom/forward.h>
+#include <2geom/point.h>
+#include <2geom/rect.h>
+#include "ui/tool/commit-events.h"
+#include "ui/tool/manipulator.h"
+#include "ui/tool/node-types.h"
+#include "snap-candidate.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+class CanvasItemGroup;
+namespace UI {
+class TransformHandleSet;
+class SelectableControlPoint;
+}
+}
+
+namespace Inkscape {
+namespace UI {
+
+class ControlPointSelection : public Manipulator, public sigc::trackable {
+public:
+ ControlPointSelection(SPDesktop *d, Inkscape::CanvasItemGroup *th_group);
+ ~ControlPointSelection() override;
+ typedef std::unordered_set<SelectableControlPoint *> set_type;
+ typedef set_type Set; // convenience alias
+
+ typedef set_type::iterator iterator;
+ typedef set_type::const_iterator const_iterator;
+ typedef set_type::size_type size_type;
+ typedef SelectableControlPoint *value_type;
+ typedef SelectableControlPoint *key_type;
+
+ // size
+ bool empty() { return _points.empty(); }
+ size_type size() { return _points.size(); }
+
+ // iterators
+ iterator begin() { return _points.begin(); }
+ const_iterator begin() const { return _points.begin(); }
+ iterator end() { return _points.end(); }
+ const_iterator end() const { return _points.end(); }
+
+ // insert
+ std::pair<iterator, bool> insert(const value_type& x, bool notify = true, bool to_update = true);
+ template <class InputIterator>
+ void insert(InputIterator first, InputIterator last) {
+ for (; first != last; ++first) {
+ insert(*first, false, false);
+ }
+ _update();
+ signal_selection_changed.emit(std::vector<key_type>(first, last), true);
+ }
+
+ // erase
+ void clear();
+ void erase(iterator pos, bool to_update = true);
+ size_type erase(const key_type& k, bool notify = true);
+ void erase(iterator first, iterator last);
+
+ // find
+ iterator find(const key_type &k) {
+ return _points.find(k);
+ }
+
+ // Sometimes it is very useful to keep a list of all selectable points.
+ set_type const &allPoints() const { return _all_points; }
+ set_type &allPoints() { return _all_points; }
+ // ...for example in these methods. Another useful case is snapping.
+ void selectAll();
+ void selectArea(Geom::Path const &, bool invert = false);
+ void invertSelection();
+ void spatialGrow(SelectableControlPoint *origin, int dir);
+
+ bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override;
+
+ void transform(Geom::Affine const &m);
+ void align(Geom::Dim2 d, AlignTargetNode target = AlignTargetNode::MID_NODE);
+ void distribute(Geom::Dim2 d);
+
+ Geom::OptRect pointwiseBounds();
+ Geom::OptRect bounds();
+
+ bool transformHandlesEnabled() { return _handles_visible; }
+ void showTransformHandles(bool v, bool one_node);
+ // the two methods below do not modify the state; they are for use in manipulators
+ // that need to temporarily hide the handles, for example when moving a node
+ void hideTransformHandles();
+ void restoreTransformHandles();
+ void toggleTransformHandlesMode();
+
+ sigc::signal<void ()> signal_update;
+ // It turns out that emitting a signal after every point is selected or deselected is not too efficient,
+ // so this can be done in a massive group once the selection is finally changed.
+ sigc::signal<void (std::vector<SelectableControlPoint *>, bool)> signal_selection_changed;
+ sigc::signal<void (CommitEvent)> signal_commit;
+
+ void getOriginalPoints(std::vector<Inkscape::SnapCandidatePoint> &pts);
+ void getUnselectedPoints(std::vector<Inkscape::SnapCandidatePoint> &pts);
+ void setOriginalPoints();
+ //the purpose of this list is to keep track of first and last selected
+ std::list<SelectableControlPoint *> _points_list;
+
+private:
+ // The functions below are invoked from SelectableControlPoint.
+ // Previously they were connected to handlers when selecting, but this
+ // creates problems when dragging a point that was not selected.
+ void _pointGrabbed(SelectableControlPoint *);
+ void _pointDragged(Geom::Point &, GdkEventMotion *);
+ void _pointUngrabbed();
+ bool _pointClicked(SelectableControlPoint *, GdkEventButton *);
+ void _mouseoverChanged();
+
+ void _update();
+ void _updateTransformHandles(bool preserve_center);
+ void _updateBounds();
+ bool _keyboardMove(GdkEventKey const &, Geom::Point const &);
+ bool _keyboardRotate(GdkEventKey const &, int);
+ bool _keyboardScale(GdkEventKey const &, int);
+ bool _keyboardFlip(Geom::Dim2);
+ void _keyboardTransform(Geom::Affine const &);
+ void _commitHandlesTransform(CommitEvent ce);
+ double _rotationRadius(Geom::Point const &);
+
+ set_type _points;
+
+ set_type _all_points;
+ std::unordered_map<SelectableControlPoint *, Geom::Point> _original_positions;
+ std::unordered_map<SelectableControlPoint *, Geom::Affine> _last_trans;
+ std::optional<double> _rot_radius;
+ std::optional<double> _mouseover_rot_radius;
+ Geom::OptRect _bounds;
+ TransformHandleSet *_handles;
+ SelectableControlPoint *_grabbed_point, *_farthest_point;
+ unsigned _dragging : 1;
+ unsigned _handles_visible : 1;
+ unsigned _one_node_handles : 1;
+
+ friend class SelectableControlPoint;
+};
+
+} // namespace UI
+} // 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 :
diff --git a/src/ui/tool/control-point.cpp b/src/ui/tool/control-point.cpp
new file mode 100644
index 0000000..1c40dfb
--- /dev/null
+++ b/src/ui/tool/control-point.cpp
@@ -0,0 +1,587 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iostream>
+
+#include <gdk/gdkkeysyms.h>
+#include <gdkmm.h>
+
+#include <2geom/point.h>
+
+#include "desktop.h"
+#include "message-context.h"
+
+#include "display/control/canvas-item-enums.h"
+#include "display/control/snap-indicator.h"
+
+#include "object/sp-namedview.h"
+
+#include "ui/tools/tool-base.h"
+#include "ui/tool/control-point.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/transform-handle-set.h"
+#include "ui/widget/canvas.h" // autoscroll
+
+namespace Inkscape {
+namespace UI {
+
+
+// Default colors for control points
+ControlPoint::ColorSet ControlPoint::_default_color_set = {
+ {0xffffff00, 0x01000000}, // normal fill, stroke
+ {0xff0000ff, 0x01000000}, // mouseover fill, stroke
+ {0x0000ffff, 0x01000000}, // clicked fill, stroke
+ //
+ {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected
+ {0xff000000, 0x000000ff} // clicked fill, stroke when selected
+};
+
+ControlPoint *ControlPoint::mouseovered_point = nullptr;
+
+sigc::signal<void (ControlPoint*)> ControlPoint::signal_mouseover_change;
+
+Geom::Point ControlPoint::_drag_event_origin(Geom::infinity(), Geom::infinity());
+
+Geom::Point ControlPoint::_drag_origin(Geom::infinity(), Geom::infinity());
+
+Gdk::EventMask const ControlPoint::_grab_event_mask = (Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK |
+ Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::SCROLL_MASK |
+ Gdk::SMOOTH_SCROLL_MASK );
+
+bool ControlPoint::_drag_initiated = false;
+bool ControlPoint::_event_grab = false;
+
+ControlPoint::ColorSet ControlPoint::invisible_cset = {
+ {0x00000000, 0x00000000},
+ {0x00000000, 0x00000000},
+ {0x00000000, 0x00000000},
+ {0x00000000, 0x00000000},
+ {0x00000000, 0x00000000},
+ {0x00000000, 0x00000000}
+};
+
+ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ ColorSet const &cset,
+ Inkscape::CanvasItemGroup *group)
+ : _desktop(d)
+ , _cset(cset)
+ , _position(initial_pos)
+{
+ _canvas_item_ctrl = make_canvasitem<Inkscape::CanvasItemCtrl>(group ? group : _desktop->getCanvasControls(),
+ Inkscape::CANVAS_ITEM_CTRL_SHAPE_BITMAP);
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:ControlPoint");
+ _canvas_item_ctrl->set_pixbuf(std::move(pixbuf));
+ _canvas_item_ctrl->set_fill( _cset.normal.fill);
+ _canvas_item_ctrl->set_stroke(_cset.normal.stroke);
+ _canvas_item_ctrl->set_anchor(anchor);
+
+ _commonInit();
+}
+
+ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Inkscape::CanvasItemCtrlType type,
+ ColorSet const &cset,
+ Inkscape::CanvasItemGroup *group)
+ : _desktop(d)
+ , _cset(cset)
+ , _position(initial_pos)
+{
+ _canvas_item_ctrl = make_canvasitem<Inkscape::CanvasItemCtrl>(group ? group : _desktop->getCanvasControls(), type);
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:ControlPoint");
+ _canvas_item_ctrl->set_fill( _cset.normal.fill);
+ _canvas_item_ctrl->set_stroke(_cset.normal.stroke);
+ _canvas_item_ctrl->set_anchor(anchor);
+
+ _commonInit();
+}
+
+ControlPoint::~ControlPoint()
+{
+ // avoid storing invalid points in mouseovered_point
+ if (this == mouseovered_point) {
+ _clearMouseover();
+ }
+
+ //g_signal_handler_disconnect(G_OBJECT(_canvas_item_ctrl), _event_handler_connection);
+ _event_handler_connection.disconnect();
+ _canvas_item_ctrl->hide();
+}
+
+void ControlPoint::_commonInit()
+{
+ _canvas_item_ctrl->set_position(_position);
+ _event_handler_connection =
+ _canvas_item_ctrl->connect_event(sigc::bind(sigc::ptr_fun(_event_handler), this));
+ // _event_handler_connection = g_signal_connect(G_OBJECT(_canvas_item_ctrl), "event",
+ // G_CALLBACK(_event_handler), this);
+}
+
+void ControlPoint::setPosition(Geom::Point const &pos)
+{
+ _position = pos;
+ _canvas_item_ctrl->set_position(_position);
+}
+
+void ControlPoint::move(Geom::Point const &pos)
+{
+ setPosition(pos);
+}
+
+void ControlPoint::transform(Geom::Affine const &m) {
+ move(position() * m);
+}
+
+bool ControlPoint::visible() const
+{
+ return _canvas_item_ctrl->is_visible();
+}
+
+void ControlPoint::setVisible(bool v)
+{
+ if (v) {
+ _canvas_item_ctrl->show();
+ } else {
+ _canvas_item_ctrl->hide();
+ }
+}
+
+Glib::ustring ControlPoint::format_tip(char const *format, ...)
+{
+ va_list args;
+ va_start(args, format);
+ char *dyntip = g_strdup_vprintf(format, args);
+ va_end(args);
+ Glib::ustring ret = dyntip;
+ g_free(dyntip);
+ return ret;
+}
+
+
+// ===== Setters =====
+
+void ControlPoint::_setSize(unsigned int size)
+{
+ _canvas_item_ctrl->set_size(size);
+}
+
+void ControlPoint::_setControlType(Inkscape::CanvasItemCtrlType type)
+{
+ _canvas_item_ctrl->set_type(type);
+}
+
+void ControlPoint::_setAnchor(SPAnchorType anchor)
+{
+// g_object_set(_canvas_item_ctrl, "anchor", anchor, nullptr);
+}
+
+// re-routes events into the virtual function TODO: Refactor this nonsense.
+bool ControlPoint::_event_handler(GdkEvent *event, ControlPoint *point)
+{
+ if ((point == nullptr) || (point->_desktop == nullptr)) {
+ return false;
+ }
+ return point->_eventHandler(point->_desktop->event_context, event);
+}
+
+// main event callback, which emits all other callbacks.
+bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ // NOTE the static variables below are shared for all points!
+ // TODO handle clicks and drags from other buttons too
+
+ if (event == nullptr)
+ {
+ return false;
+ }
+
+ if (event_context == nullptr)
+ {
+ return false;
+ }
+ if (_desktop == nullptr)
+ {
+ return false;
+ }
+ if(event_context->getDesktop() !=_desktop)
+ {
+ g_warning ("ControlPoint: desktop pointers not equal!");
+ //return false;
+ }
+ // offset from the pointer hotspot to the center of the grabbed knot in desktop coords
+ static Geom::Point pointer_offset;
+ // number of last doubleclicked button
+ static unsigned next_release_doubleclick = 0;
+ _double_clicked = false;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ switch(event->type)
+ {
+ case GDK_BUTTON_PRESS:
+ next_release_doubleclick = 0;
+ if (event->button.button == 1 && !event_context->is_space_panning()) {
+ // 1st mouse button click. internally, start dragging, but do not emit signals
+ // or change position until drag tolerance is exceeded.
+ _drag_event_origin[Geom::X] = event->button.x;
+ _drag_event_origin[Geom::Y] = event->button.y;
+ pointer_offset = _position - _desktop->w2d(_drag_event_origin);
+ _drag_initiated = false;
+ // route all events to this handler
+ _canvas_item_ctrl->grab(_grab_event_mask); // cursor is null
+ _event_grab = true;
+ _setState(STATE_CLICKED);
+ return true;
+ }
+ return _event_grab;
+
+ case GDK_2BUTTON_PRESS:
+ // store the button number for next release
+ next_release_doubleclick = event->button.button;
+ return true;
+
+ case GDK_MOTION_NOTIFY:
+ if (_event_grab && ! event_context->is_space_panning()) {
+ _desktop->snapindicator->remove_snaptarget();
+ bool transferred = false;
+ if (!_drag_initiated) {
+ bool t = fabs(event->motion.x - _drag_event_origin[Geom::X]) <= drag_tolerance &&
+ fabs(event->motion.y - _drag_event_origin[Geom::Y]) <= drag_tolerance;
+ if (t){
+ return true;
+ }
+
+ // if we are here, it means the tolerance was just exceeded.
+ _drag_origin = _position;
+ transferred = grabbed(&event->motion);
+ // _drag_initiated might change during the above virtual call
+ _drag_initiated = true;
+ }
+
+ if (!transferred) {
+ // dragging in progress
+ Geom::Point new_pos = _desktop->w2d(event_point(event->motion)) + pointer_offset;
+ // the new position is passed by reference and can be changed in the handlers.
+ dragged(new_pos, &event->motion);
+ move(new_pos);
+ _updateDragTip(&event->motion); // update dragging tip after moving to new position
+
+ _desktop->getCanvas()->enable_autoscroll();
+ _desktop->set_coordinate_status(_position);
+ event_context->snap_delay_handler(nullptr, this, &event->motion,
+ Inkscape::UI::Tools::DelayedSnapEvent::CONTROL_POINT_HANDLER);
+ }
+ return true;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (_event_grab && event->button.button == 1) {
+ // If we have any pending snap event, then invoke it now!
+ // (This is needed because we might not have snapped on the latest GDK_MOTION_NOTIFY event
+ // if the mouse speed was too high. This is inherent to the snap-delay mechanism.
+ // We must snap at some point in time though, and this is our last chance)
+ // PS: For other contexts this is handled already in start_item_handler or start_root_handler
+ // if (_desktop && _desktop->event_context && _desktop->event_context->_delayed_snap_event) {
+ event_context->process_delayed_snap_event();
+
+ _canvas_item_ctrl->ungrab();
+ _setMouseover(this, event->button.state);
+ _event_grab = false;
+
+ if (_drag_initiated) {
+ // it is the end of a drag
+ _drag_initiated = false;
+ ungrabbed(&event->button);
+ return true;
+ } else {
+ // it is the end of a click
+ if (next_release_doubleclick) {
+ _double_clicked = true;
+ return doubleclicked(&event->button);
+ } else {
+ return clicked(&event->button);
+ }
+ }
+ }
+ break;
+
+ case GDK_ENTER_NOTIFY:
+ _setMouseover(this, event->crossing.state);
+ return true;
+ case GDK_LEAVE_NOTIFY:
+ _clearMouseover();
+ return true;
+
+ case GDK_GRAB_BROKEN:
+ if (_event_grab && !event->grab_broken.keyboard) {
+ {
+ ungrabbed(nullptr);
+ }
+ _setState(STATE_NORMAL);
+ _event_grab = false;
+ _drag_initiated = false;
+ return true;
+ }
+ break;
+
+ // update tips on modifier state change
+ // TODO add ESC keybinding as drag cancel
+ case GDK_KEY_PRESS:
+ switch (Inkscape::UI::Tools::get_latin_keyval(&event->key))
+ {
+ case GDK_KEY_Escape: {
+ // ignore Escape if this is not a drag
+ if (!_drag_initiated) break;
+
+ // temporarily disable snapping - we might snap to a different place than we were initially
+ event_context->discard_delayed_snap_event();
+ SnapPreferences &snapprefs = _desktop->namedview->snap_manager.snapprefs;
+ bool snap_save = snapprefs.getSnapEnabledGlobally();
+ snapprefs.setSnapEnabledGlobally(false);
+
+ Geom::Point new_pos = _drag_origin;
+
+ // make a fake event for dragging
+ // ASSUMPTION: dragging a point without modifiers will never prevent us from moving it
+ // to its original position
+ GdkEventMotion fake;
+ fake.type = GDK_MOTION_NOTIFY;
+ fake.window = event->key.window;
+ fake.send_event = event->key.send_event;
+ fake.time = event->key.time;
+ fake.x = _drag_event_origin[Geom::X]; // these two are normally not used in handlers
+ fake.y = _drag_event_origin[Geom::Y]; // (and shouldn't be)
+ fake.axes = nullptr;
+ fake.state = 0; // unconstrained drag
+ fake.is_hint = FALSE;
+ fake.device = nullptr;
+ fake.x_root = -1; // not used in handlers (and shouldn't be)
+ fake.y_root = -1; // can be used as a flag to check for cancelled drag
+
+ dragged(new_pos, &fake);
+
+ _canvas_item_ctrl->ungrab();
+ _clearMouseover(); // this will also reset state to normal
+ _event_grab = false;
+ _drag_initiated = false;
+
+ ungrabbed(nullptr); // ungrabbed handlers can handle a NULL event
+ snapprefs.setSnapEnabledGlobally(snap_save);
+ }
+ return true;
+ case GDK_KEY_Tab:
+ {// Downcast from ControlPoint to TransformHandle, if possible
+ // This is an ugly hack; we should have the transform handle intercept the keystrokes itself
+ TransformHandle *th = dynamic_cast<TransformHandle*>(this);
+ if (th) {
+ th->getNextClosestPoint(false);
+ return true;
+ }
+ break;
+ }
+ case GDK_KEY_ISO_Left_Tab:
+ {// Downcast from ControlPoint to TransformHandle, if possible
+ // This is an ugly hack; we should have the transform handle intercept the keystrokes itself
+ TransformHandle *th = dynamic_cast<TransformHandle*>(this);
+ if (th) {
+ th->getNextClosestPoint(true);
+ return true;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ // Do not break here, to allow for updating tooltips and such
+ case GDK_KEY_RELEASE:
+ if (mouseovered_point != this){
+ return false;
+ }
+ if (_drag_initiated) {
+ return true; // this prevents the tool from overwriting the drag tip
+ } else {
+ unsigned state = state_after_event(event);
+ if (state != event->key.state) {
+ // we need to return true if there was a tip available, otherwise the tool's
+ // handler will process this event and set the tool's message, overwriting
+ // the point's message
+ return _updateTip(state);
+ }
+ }
+ break;
+
+ default: break;
+ }
+
+ // do not propagate events during grab - it might cause problems
+ return _event_grab;
+}
+
+void ControlPoint::_setMouseover(ControlPoint *p, unsigned state)
+{
+ bool visible = p->visible();
+ if (visible) { // invisible points shouldn't get mouseovered
+ p->_setState(STATE_MOUSEOVER);
+ }
+ p->_updateTip(state);
+
+ if (visible && mouseovered_point != p) {
+ mouseovered_point = p;
+ signal_mouseover_change.emit(mouseovered_point);
+ }
+}
+
+bool ControlPoint::_updateTip(unsigned state)
+{
+ Glib::ustring tip = _getTip(state);
+ if (!tip.empty()) {
+ _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
+ tip.data());
+ return true;
+ } else {
+ _desktop->event_context->defaultMessageContext()->clear();
+ return false;
+ }
+}
+
+bool ControlPoint::_updateDragTip(GdkEventMotion *event)
+{
+ if (!_hasDragTips()) {
+ return false;
+ }
+ Glib::ustring tip = _getDragTip(event);
+ if (!tip.empty()) {
+ _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
+ tip.data());
+ return true;
+ } else {
+ _desktop->event_context->defaultMessageContext()->clear();
+ return false;
+ }
+}
+
+void ControlPoint::_clearMouseover()
+{
+ if (mouseovered_point) {
+ mouseovered_point->_desktop->event_context->defaultMessageContext()->clear();
+ mouseovered_point->_setState(STATE_NORMAL);
+ mouseovered_point = nullptr;
+ signal_mouseover_change.emit(mouseovered_point);
+ }
+}
+
+void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event)
+{
+ if (!_event_grab) return;
+
+ grabbed(event);
+ prev_point->_canvas_item_ctrl->ungrab();
+ _canvas_item_ctrl->grab(_grab_event_mask); // cursor is null
+
+ _drag_initiated = true;
+
+ prev_point->_setState(STATE_NORMAL);
+ _setMouseover(this, event->state);
+}
+
+void ControlPoint::_setState(State state)
+{
+ ColorEntry current = {0, 0};
+ ColorSet const &activeCset = (_isLurking()) ? invisible_cset : _cset;
+ switch(state) {
+ case STATE_NORMAL:
+ current = activeCset.normal;
+ break;
+ case STATE_MOUSEOVER:
+ current = activeCset.mouseover;
+ break;
+ case STATE_CLICKED:
+ current = activeCset.clicked;
+ break;
+ };
+ _setColors(current);
+ _state = state;
+}
+
+// TODO: RENAME
+void ControlPoint::_handleControlStyling()
+{
+ _canvas_item_ctrl->set_size_default();
+}
+
+void ControlPoint::_setColors(ColorEntry colors)
+{
+ _canvas_item_ctrl->set_fill(colors.fill);
+ _canvas_item_ctrl->set_stroke(colors.stroke);
+}
+
+bool ControlPoint::_isLurking()
+{
+ return _lurking;
+}
+
+void ControlPoint::_setLurking(bool lurking)
+{
+ if (lurking != _lurking) {
+ _lurking = lurking;
+ _setState(_state); // TODO refactor out common part
+ }
+}
+
+
+bool ControlPoint::_is_drag_cancelled(GdkEventMotion *event)
+{
+ return !event || event->x_root == -1;
+}
+
+// dummy implementations for handlers
+
+bool ControlPoint::grabbed(GdkEventMotion * /*event*/)
+{
+ return false;
+}
+
+void ControlPoint::dragged(Geom::Point &/*new_pos*/, GdkEventMotion * /*event*/)
+{
+}
+
+void ControlPoint::ungrabbed(GdkEventButton * /*event*/)
+{
+}
+
+bool ControlPoint::clicked(GdkEventButton * /*event*/)
+{
+ return false;
+}
+
+bool ControlPoint::doubleclicked(GdkEventButton * /*event*/)
+{
+ return false;
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ 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/ui/tool/control-point.h b/src/ui/tool/control-point.h
new file mode 100644
index 0000000..345f918
--- /dev/null
+++ b/src/ui/tool/control-point.h
@@ -0,0 +1,413 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2012 Authors
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_CONTROL_POINT_H
+#define SEEN_UI_TOOL_CONTROL_POINT_H
+
+#include <gdkmm/pixbuf.h>
+#include <boost/utility.hpp>
+#include <cstddef>
+#include <sigc++/signal.h>
+#include <sigc++/trackable.h>
+#include <2geom/point.h>
+
+// #include "ui/control-types.h"
+#include "display/control/canvas-item-ctrl.h"
+#include "display/control/canvas-item-enums.h"
+#include "display/control/canvas-item-ptr.h"
+
+#include "enums.h" // TEMP TEMP
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class ToolBase;
+
+}
+}
+}
+
+namespace Inkscape {
+namespace UI {
+
+/**
+ * Draggable point, the workhorse of on-canvas editing.
+ *
+ * Control points (formerly known as knots) are graphical representations of some significant
+ * point in the drawing. The drawing can be changed by dragging the point and the things that are
+ * attached to it with the mouse. Example things that could be edited with draggable points
+ * are gradient stops, the place where text is attached to a path, text kerns, nodes and handles
+ * in a path, and many more.
+ *
+ * @par Control point event handlers
+ * @par
+ * The control point has several virtual methods which allow you to react to things that
+ * happen to it. The most important ones are the grabbed, dragged, ungrabbed and moved functions.
+ * When a drag happens, the order of calls is as follows:
+ * - <tt>grabbed()</tt>
+ * - <tt>dragged()</tt>
+ * - <tt>dragged()</tt>
+ * - <tt>dragged()</tt>
+ * - ...
+ * - <tt>dragged()</tt>
+ * - <tt>ungrabbed()</tt>
+ *
+ * The control point can also respond to clicks and double clicks. On a double click,
+ * clicked() is called, followed by doubleclicked(). When deriving from SelectableControlPoint,
+ * you need to manually call the superclass version at the appropriate point in your handler.
+ *
+ * @par Which method to override?
+ * @par
+ * You might wonder which hook to use when you want to do things when the point is relocated.
+ * Here are some tips:
+ * - If the point is used to edit an object, override the move() method.
+ * - If the point can usually be dragged wherever you like but can optionally be constrained
+ * to axes or the like, add a handler for <tt>signal_dragged</tt> that modifies its new
+ * position argument.
+ * - If the point has additional canvas items tied to it (like handle lines), override
+ * the setPosition() method.
+ */
+class ControlPoint : boost::noncopyable, public sigc::trackable {
+public:
+
+ /**
+ * Enumeration representing the possible states of the control point, used to determine
+ * its appearance.
+ *
+ * @todo resolve this to be in sync with the five standard GTK states.
+ */
+ enum State {
+ /** Normal state. */
+ STATE_NORMAL,
+
+ /** Mouse is hovering over the control point. */
+ STATE_MOUSEOVER,
+
+ /** First mouse button pressed over the control point. */
+ STATE_CLICKED
+ };
+
+ /**
+ * Destructor
+ */
+ virtual ~ControlPoint();
+
+ /// @name Adjust the position of the control point
+ /// @{
+ /** Current position of the control point. */
+ Geom::Point const &position() const { return _position; }
+
+ operator Geom::Point const &() { return _position; }
+
+ /**
+ * Move the control point to new position with side effects.
+ * This is called after each drag. Override this method if only some positions make sense
+ * for a control point (like a point that must always be on a path and can't modify it),
+ * or when moving a control point changes the positions of other points.
+ */
+ virtual void move(Geom::Point const &pos);
+
+ /**
+ * Relocate the control point without side effects.
+ * Overload this method only if there is an additional graphical representation
+ * that must be updated (like the lines that connect handles to nodes). If you override it,
+ * you must also call the superclass implementation of the method.
+ * @todo Investigate whether this method should be protected
+ */
+ virtual void setPosition(Geom::Point const &pos);
+
+ /**
+ * Apply an arbitrary affine transformation to a control point. This is used
+ * by ControlPointSelection, and is important for things like nodes with handles.
+ * The default implementation simply moves the point according to the transform.
+ */
+ virtual void transform(Geom::Affine const &m);
+
+ /**
+ * Apply any node repairs, by default no fixing is applied but Nodes will update
+ * smooth nodes to make sure nodes are kept consistent.
+ */
+ virtual void fixNeighbors() {};
+
+ /// @}
+
+ /// @name Toggle the point's visibility
+ /// @{
+ bool visible() const;
+
+ /**
+ * Set the visibility of the control point. An invisible point is not drawn on the canvas
+ * and cannot receive any events. If you want to have an invisible point that can respond
+ * to events, use <tt>invisible_cset</tt> as its color set.
+ */
+ virtual void setVisible(bool v);
+ /// @}
+
+ /// @name Transfer grab from another event handler
+ /// @{
+ /**
+ * Transfer the grab to another point. This method allows one to create a draggable point
+ * that should be dragged instead of the one that received the grabbed signal.
+ * This is used to implement dragging out handles in the new node tool, for example.
+ *
+ * This method will NOT emit the ungrab signal of @c prev_point, because this would complicate
+ * using it with selectable control points. If you use this method while dragging, you must emit
+ * the ungrab signal yourself.
+ *
+ * Note that this will break horribly if you try to transfer grab between points in different
+ * desktops, which doesn't make much sense anyway.
+ */
+ void transferGrab(ControlPoint *from, GdkEventMotion *event);
+ /// @}
+
+ /// @name Inspect the state of the control point
+ /// @{
+ State state() const { return _state; }
+
+ bool mouseovered() const { return this == mouseovered_point; }
+ /// @}
+
+ /** Holds the currently mouseovered control point. */
+ static ControlPoint *mouseovered_point;
+
+ /**
+ * Emitted when the mouseovered point changes. The parameter is the new mouseovered point.
+ * When a point ceases to be mouseovered, the parameter will be NULL.
+ */
+ static sigc::signal<void (ControlPoint*)> signal_mouseover_change;
+
+ static Glib::ustring format_tip(char const *format, ...) G_GNUC_PRINTF(1,2);
+
+ // temporarily public, until snap delay is refactored a little
+ virtual bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event);
+ SPDesktop *const _desktop; ///< The desktop this control point resides on.
+
+ bool doubleClicked() {return _double_clicked;}
+
+protected:
+
+ struct ColorEntry {
+ guint32 fill;
+ guint32 stroke;
+ };
+
+ /**
+ * Color entries for each possible state.
+ * @todo resolve this to be in sync with the five standard GTK states.
+ */
+ struct ColorSet {
+ ColorEntry normal;
+ ColorEntry mouseover;
+ ColorEntry clicked;
+ ColorEntry selected_normal;
+ ColorEntry selected_mouseover;
+ ColorEntry selected_clicked;
+ };
+
+ /**
+ * A color set which you can use to create an invisible control that can still receive events.
+ */
+ static ColorSet invisible_cset;
+
+ /**
+ * Create a regular control point.
+ * Derive to have constructors with a reasonable number of parameters.
+ *
+ * @param d Desktop for this control
+ * @param initial_pos Initial position of the control point in desktop coordinates
+ * @param anchor Where is the control point rendered relative to its desktop coordinates
+ * @param type Logical type of the control point.
+ * @param cset Colors of the point
+ * @param group The canvas group the point's canvas item should be created in
+ */
+ ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Inkscape::CanvasItemCtrlType type,
+ ColorSet const &cset = _default_color_set,
+ Inkscape::CanvasItemGroup *group = nullptr);
+
+ /**
+ * Create a control point with a pixbuf-based visual representation.
+ *
+ * @param d Desktop for this control
+ * @param initial_pos Initial position of the control point in desktop coordinates
+ * @param anchor Where is the control point rendered relative to its desktop coordinates
+ * @param pixbuf Pixbuf to be used as the visual representation
+ * @param cset Colors of the point
+ * @param group The canvas group the point's canvas item should be created in
+ */
+ ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ ColorSet const &cset = _default_color_set,
+ Inkscape::CanvasItemGroup *group = nullptr);
+
+ /// @name Handle control point events in subclasses
+ /// @{
+ /**
+ * Called when the user moves the point beyond the drag tolerance with the first button held
+ * down.
+ *
+ * @param event Motion event when drag tolerance was exceeded.
+ * @return true if you called transferGrab() during this method.
+ */
+ virtual bool grabbed(GdkEventMotion *event);
+
+ /**
+ * Called while dragging, but before moving the knot to new position.
+ *
+ * @param pos Old position, always equal to position()
+ * @param new_pos New position (after drag). This is passed as a non-const reference,
+ * so you can change it from the handler - that's how constrained dragging is implemented.
+ * @param event Motion event.
+ */
+ virtual void dragged(Geom::Point &new_pos, GdkEventMotion *event);
+
+ /**
+ * Called when the control point finishes a drag.
+ *
+ * @param event Button release event
+ */
+ virtual void ungrabbed(GdkEventButton *event);
+
+ /**
+ * Called when the control point is clicked, at mouse button release.
+ * Improperly implementing this method can cause the default context menu not to appear when a control
+ * point is right-clicked.
+ *
+ * @param event Button release event
+ * @return true if the click had some effect, false if it did nothing.
+ */
+ virtual bool clicked(GdkEventButton *event);
+
+ /**
+ * Called when the control point is doubleclicked, at mouse button release.
+ *
+ * @param event Button release event
+ */
+ virtual bool doubleclicked(GdkEventButton *event);
+ /// @}
+
+ /// @name Manipulate the control point's appearance in subclasses
+ /// @{
+
+ /**
+ * Change the state of the knot.
+ * Alters the appearance of the knot to match one of the states: normal, mouseover
+ * or clicked.
+ */
+ virtual void _setState(State state);
+
+ void _handleControlStyling();
+
+ void _setColors(ColorEntry c);
+
+ void _setSize(unsigned int size);
+
+ void _setControlType(Inkscape::CanvasItemCtrlType type);
+
+ void _setAnchor(SPAnchorType anchor);
+
+ /**
+ * Determines if the control point is not visible yet still reacting to events.
+ *
+ * @return true if non-visible, false otherwise.
+ */
+ bool _isLurking();
+
+ /**
+ * Sets the control point to be non-visible yet still reacting to events.
+ *
+ * @param lurking true to make non-visible, false otherwise.
+ */
+ void _setLurking(bool lurking);
+
+ /// @}
+
+ virtual Glib::ustring _getTip(unsigned /*state*/) const { return ""; }
+
+ virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) const { return ""; }
+
+ virtual bool _hasDragTips() const { return false; }
+
+
+ CanvasItemPtr<Inkscape::CanvasItemCtrl> _canvas_item_ctrl; ///< Visual representation of the control point.
+
+ ColorSet const &_cset; ///< Colors used to represent the point
+
+ State _state = STATE_NORMAL;
+
+ static Geom::Point const &_last_click_event_point() { return _drag_event_origin; }
+
+ static Geom::Point const &_last_drag_origin() { return _drag_origin; }
+
+ static bool _is_drag_cancelled(GdkEventMotion *event);
+
+ /** Events which should be captured when a handle is being dragged. */
+ static Gdk::EventMask const _grab_event_mask;
+
+ static bool _drag_initiated;
+
+private:
+
+ ControlPoint(ControlPoint const &other);
+
+ void operator=(ControlPoint const &other);
+
+ static bool _event_handler(GdkEvent *event, ControlPoint *point);
+
+ static void _setMouseover(ControlPoint *, unsigned state);
+
+ static void _clearMouseover();
+
+ bool _updateTip(unsigned state);
+
+ bool _updateDragTip(GdkEventMotion *event);
+
+ void _setDefaultColors();
+
+ void _commonInit();
+
+ Geom::Point _position; ///< Current position in desktop coordinates
+
+ sigc::connection _event_handler_connection;
+
+ bool _lurking = false;
+
+ static ColorSet _default_color_set;
+
+ /** Stores the window point over which the cursor was during the last mouse button press. */
+ static Geom::Point _drag_event_origin;
+
+ /** Stores the desktop point from which the last drag was initiated. */
+ static Geom::Point _drag_origin;
+
+ static bool _event_grab;
+
+ bool _double_clicked = false;
+};
+
+
+} // namespace UI
+} // 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/ui/tool/curve-drag-point.cpp b/src/ui/tool/curve-drag-point.cpp
new file mode 100644
index 0000000..acf1299
--- /dev/null
+++ b/src/ui/tool/curve-drag-point.cpp
@@ -0,0 +1,247 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tool/curve-drag-point.h"
+#include <glib/gi18n.h>
+#include "desktop.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/path-manipulator.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-path.h"
+
+namespace Inkscape {
+namespace UI {
+
+
+bool CurveDragPoint::_drags_stroke = false;
+bool CurveDragPoint::_segment_was_degenerate = false;
+
+CurveDragPoint::CurveDragPoint(PathManipulator &pm) :
+ ControlPoint(pm._multi_path_manipulator._path_data.node_data.desktop, Geom::Point(), SP_ANCHOR_CENTER,
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_INVISIPOINT,
+ invisible_cset, pm._multi_path_manipulator._path_data.dragpoint_group),
+ _pm(pm)
+{
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:CurveDragPoint");
+ setVisible(false);
+}
+
+bool CurveDragPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ // do not process any events when the manipulator is empty
+ if (_pm.empty()) {
+ setVisible(false);
+ return false;
+ }
+ return ControlPoint::_eventHandler(event_context, event);
+}
+
+bool CurveDragPoint::grabbed(GdkEventMotion */*event*/)
+{
+ _pm._selection.hideTransformHandles();
+ NodeList::iterator second = first.next();
+
+ // move the handles to 1/3 the length of the segment for line segments
+ if (first->front()->isDegenerate() && second->back()->isDegenerate()) {
+ _segment_was_degenerate = true;
+
+ // delta is a vector equal 1/3 of distance from first to second
+ Geom::Point delta = (second->position() - first->position()) / 3.0;
+ // only update the nodes if the mode is bspline
+ if(!_pm._isBSpline()){
+ first->front()->move(first->front()->position() + delta);
+ second->back()->move(second->back()->position() - delta);
+ }
+ _pm.update();
+ } else {
+ _segment_was_degenerate = false;
+ }
+ return false;
+}
+
+void CurveDragPoint::dragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ if (!first || !first.next()) return;
+ NodeList::iterator second = first.next();
+
+ // special cancel handling - retract handles when if the segment was degenerate
+ if (_is_drag_cancelled(event) && _segment_was_degenerate) {
+ first->front()->retract();
+ second->back()->retract();
+ _pm.update();
+ return;
+ }
+
+ if (_drag_initiated && !(event->state & GDK_SHIFT_MASK)) {
+ SnapManager &m = _desktop->namedview->snap_manager;
+ SPItem *path = static_cast<SPItem *>(_pm._path);
+ m.setup(_desktop, true, path); // We will not try to snap to "path" itself
+ Inkscape::SnapCandidatePoint scp(new_pos, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ Inkscape::SnappedPoint sp = m.freeSnap(scp, Geom::OptRect(), false);
+ new_pos = sp.getPoint();
+ m.unSetup();
+ }
+
+ // Magic Bezier Drag Equations follow!
+ // "weight" describes how the influence of the drag should be distributed
+ // among the handles; 0 = front handle only, 1 = back handle only.
+ double weight, t = _t;
+ if (t <= 1.0 / 6.0) weight = 0;
+ else if (t <= 0.5) weight = (pow((6 * t - 1) / 2.0, 3)) / 2;
+ else if (t <= 5.0 / 6.0) weight = (1 - pow((6 * (1-t) - 1) / 2.0, 3)) / 2 + 0.5;
+ else weight = 1;
+
+ Geom::Point delta = new_pos - position();
+ Geom::Point offset0 = ((1-weight)/(3*t*(1-t)*(1-t))) * delta;
+ Geom::Point offset1 = (weight/(3*t*t*(1-t))) * delta;
+
+ //modified so that, if the trace is bspline, it only acts if the SHIFT key is pressed
+ if(!_pm._isBSpline()){
+ first->front()->move(first->front()->position() + offset0);
+ second->back()->move(second->back()->position() + offset1);
+ }else if(weight>=0.8){
+ if(held_shift(*event)){
+ second->back()->move(new_pos);
+ } else {
+ second->move(second->position() + delta);
+ }
+ }else if(weight<=0.2){
+ if(held_shift(*event)){
+ first->back()->move(new_pos);
+ } else {
+ first->move(first->position() + delta);
+ }
+ }else{
+ first->move(first->position() + delta);
+ second->move(second->position() + delta);
+ }
+ _pm.update();
+}
+
+void CurveDragPoint::ungrabbed(GdkEventButton *)
+{
+ _pm._updateDragPoint(_desktop->d2w(position()));
+ _pm._commit(_("Drag curve"));
+ _pm._selection.restoreTransformHandles();
+}
+
+bool CurveDragPoint::clicked(GdkEventButton *event)
+{
+ // This check is probably redundant
+ if (!first || event->button != 1) return false;
+ // the next iterator can be invalid if we click very near the end of path
+ NodeList::iterator second = first.next();
+ if (!second) return false;
+
+ // insert nodes on Ctrl+Alt+click
+ if (held_control(*event) && held_alt(*event)) {
+ _insertNode(false);
+ return true;
+ }
+
+ if (held_shift(*event)) {
+ // if both nodes of the segment are selected, deselect;
+ // otherwise add to selection
+ if (first->selected() && second->selected()) {
+ _pm._selection.erase(first.ptr());
+ _pm._selection.erase(second.ptr());
+ } else {
+ _pm._selection.insert(first.ptr());
+ _pm._selection.insert(second.ptr());
+ }
+ } else {
+ // without Shift, take selection
+ _pm._selection.clear();
+ _pm._selection.insert(first.ptr());
+ _pm._selection.insert(second.ptr());
+ if (held_control(*event)) {
+ _pm.setSegmentType(Inkscape::UI::SEGMENT_STRAIGHT);
+ _pm.update(true);
+ _pm._commit(_("Straighten segments"));
+ }
+ }
+ return true;
+}
+
+bool CurveDragPoint::doubleclicked(GdkEventButton *event)
+{
+ if (event->button != 1 || !first || !first.next()) return false;
+ if (held_control(*event)) {
+ _pm.deleteSegments();
+ _pm.update(true);
+ _pm._commit(_("Remove segment"));
+ } else {
+ _insertNode(true);
+ }
+ return true;
+}
+
+void CurveDragPoint::_insertNode(bool take_selection)
+{
+ // The purpose of this call is to make way for the just created node.
+ // Otherwise clicks on the new node would only work after the user moves the mouse a bit.
+ // PathManipulator will restore visibility when necessary.
+ setVisible(false);
+
+ _pm.insertNode(first, _t, take_selection);
+}
+
+Glib::ustring CurveDragPoint::_getTip(unsigned state) const
+{
+ if (_pm.empty()) return "";
+ if (!first || !first.next()) return "";
+ bool linear = first->front()->isDegenerate() && first.next()->back()->isDegenerate();
+ if(state_held_shift(state) && _pm._isBSpline()){
+ return C_("Path segment tip",
+ "<b>Shift</b>: drag to open or move BSpline handles");
+ }
+ if (state_held_shift(state)) {
+ return C_("Path segment tip",
+ "<b>Shift</b>: click to toggle segment selection");
+ }
+ if (state_held_control(state) && state_held_alt(state)) {
+ return C_("Path segment tip",
+ "<b>Ctrl+Alt</b>: click to insert a node");
+ }
+ if (state_held_control(state)) {
+ return C_("Path segment tip",
+ "<b>Ctrl</b>: click to change line type");
+ }
+ if(_pm._isBSpline()){
+ return C_("Path segment tip",
+ "<b>BSpline segment</b>: drag to shape the segment, doubleclick to insert node, "
+ "click to select (more: Shift, Ctrl+Alt)");
+ }
+ if (linear) {
+ return C_("Path segment tip",
+ "<b>Linear segment</b>: drag to convert to a Bezier segment, "
+ "doubleclick to insert node, click to select (more: Shift, Ctrl+Alt)");
+ } else {
+ return C_("Path segment tip",
+ "<b>Bezier segment</b>: drag to shape the segment, doubleclick to insert node, "
+ "click to select (more: Shift, Ctrl+Alt)");
+ }
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ 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/ui/tool/curve-drag-point.h b/src/ui/tool/curve-drag-point.h
new file mode 100644
index 0000000..bfe0ad7
--- /dev/null
+++ b/src/ui/tool/curve-drag-point.h
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_CURVE_DRAG_POINT_H
+#define SEEN_UI_TOOL_CURVE_DRAG_POINT_H
+
+#include "ui/tool/control-point.h"
+#include "ui/tool/node.h"
+
+class SPDesktop;
+namespace Inkscape {
+namespace UI {
+
+class PathManipulator;
+struct PathSharedData;
+
+// This point should be invisible to the user - use the invisible_cset from control-point.h
+// TODO make some methods from path-manipulator.cpp public so that this point doesn't have
+// to be declared as a friend
+/**
+ * An invisible point used to drag curves. This point is used by PathManipulator to allow editing
+ * of path segments by dragging them. It is defined in a separate file so that the node tool
+ * can check if the mouseovered control point is a curve drag point and update the cursor
+ * accordingly, without the need to drag in the full PathManipulator header.
+ */
+class CurveDragPoint : public ControlPoint {
+public:
+
+ CurveDragPoint(PathManipulator &pm);
+ void setSize(double sz) { _setSize(sz); }
+ void setTimeValue(double t) { _t = t; }
+ double getTimeValue() { return _t; }
+ void setIterator(NodeList::iterator i) { first = i; }
+ NodeList::iterator getIterator() { return first; }
+ bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override;
+
+protected:
+
+ Glib::ustring _getTip(unsigned state) const override;
+ void dragged(Geom::Point &, GdkEventMotion *) override;
+ bool grabbed(GdkEventMotion *) override;
+ void ungrabbed(GdkEventButton *) override;
+ bool clicked(GdkEventButton *) override;
+ bool doubleclicked(GdkEventButton *) override;
+
+private:
+ double _t;
+ PathManipulator &_pm;
+ NodeList::iterator first;
+
+ static bool _drags_stroke;
+ static bool _segment_was_degenerate;
+ static Geom::Point _stroke_drag_origin;
+ void _insertNode(bool take_selection);
+};
+
+} // namespace UI
+} // 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/ui/tool/event-utils.cpp b/src/ui/tool/event-utils.cpp
new file mode 100644
index 0000000..f131d4f
--- /dev/null
+++ b/src/ui/tool/event-utils.cpp
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Collection of shorthands to deal with GDK events.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <gdk/gdk.h>
+#include <gdk/gdkkeysyms.h>
+#include <gdkmm/display.h>
+#include "ui/tool/event-utils.h"
+
+namespace Inkscape {
+namespace UI {
+
+
+guint shortcut_key(GdkEventKey const &event)
+{
+ guint shortcut_key = 0;
+ gdk_keymap_translate_keyboard_state(
+ Gdk::Display::get_default()->get_keymap(),
+ event.hardware_keycode,
+ (GdkModifierType) event.state,
+ 0 /*event->key.group*/,
+ &shortcut_key, nullptr, nullptr, nullptr);
+ return shortcut_key;
+}
+
+/** Returns the modifier state valid after this event. Use this when you process events
+ * that change the modifier state. Currently handles only Shift, Ctrl, Alt. */
+unsigned state_after_event(GdkEvent *event)
+{
+ unsigned state = 0;
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ state = event->key.state;
+ switch(shortcut_key(event->key)) {
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ state |= GDK_SHIFT_MASK;
+ break;
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ state |= GDK_CONTROL_MASK;
+ break;
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ state |= GDK_MOD1_MASK;
+ break;
+ default: break;
+ }
+ break;
+ case GDK_KEY_RELEASE:
+ state = event->key.state;
+ switch(shortcut_key(event->key)) {
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ state &= ~GDK_SHIFT_MASK;
+ break;
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ state &= ~GDK_CONTROL_MASK;
+ break;
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ state &= ~GDK_MOD1_MASK;
+ break;
+ default: break;
+ }
+ break;
+ default: break;
+ }
+ return state;
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ 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/ui/tool/event-utils.h b/src/ui/tool/event-utils.h
new file mode 100644
index 0000000..37961e3
--- /dev/null
+++ b/src/ui/tool/event-utils.h
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Collection of shorthands to deal with GDK events.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_EVENT_UTILS_H
+#define SEEN_UI_TOOL_EVENT_UTILS_H
+
+#include <gdk/gdk.h>
+#include <2geom/point.h>
+
+namespace Inkscape {
+namespace UI {
+
+inline bool state_held_shift(unsigned state) {
+ return state & GDK_SHIFT_MASK;
+}
+inline bool state_held_control(unsigned state) {
+ return state & GDK_CONTROL_MASK;
+}
+inline bool state_held_alt(unsigned state) {
+ return state & GDK_MOD1_MASK;
+}
+inline bool state_held_only_shift(unsigned state) {
+ return (state & GDK_SHIFT_MASK) && !(state & (GDK_CONTROL_MASK | GDK_MOD1_MASK));
+}
+inline bool state_held_only_control(unsigned state) {
+ return (state & GDK_CONTROL_MASK) && !(state & (GDK_SHIFT_MASK | GDK_MOD1_MASK));
+}
+inline bool state_held_only_alt(unsigned state) {
+ return (state & GDK_MOD1_MASK) && !(state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK));
+}
+inline bool state_held_any_modifiers(unsigned state) {
+ return state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK);
+}
+inline bool state_held_no_modifiers(unsigned state) {
+ return !state_held_any_modifiers(state);
+}
+template <unsigned button>
+inline bool state_held_button(unsigned state) {
+ return (button == 0 || button > 5) ? false : state & (GDK_BUTTON1_MASK << (button-1));
+}
+
+
+/** Checks whether Shift was held when the event was generated. */
+template <typename E>
+inline bool held_shift(E const &event) {
+ return state_held_shift(event.state);
+}
+
+/** Checks whether Control was held when the event was generated. */
+template <typename E>
+inline bool held_control(E const &event) {
+ return state_held_control(event.state);
+}
+
+/** Checks whether Alt was held when the event was generated. */
+template <typename E>
+inline bool held_alt(E const &event) {
+ return state_held_alt(event.state);
+}
+
+/** True if from the set of Ctrl, Shift and Alt only Ctrl was held when the event
+ * was generated. */
+template <typename E>
+inline bool held_only_control(E const &event) {
+ return state_held_only_control(event.state);
+}
+
+/** True if from the set of Ctrl, Shift and Alt only Shift was held when the event
+ * was generated. */
+template <typename E>
+inline bool held_only_shift(E const &event) {
+ return state_held_only_shift(event.state);
+}
+
+/** True if from the set of Ctrl, Shift and Alt only Alt was held when the event
+ * was generated. */
+template <typename E>
+inline bool held_only_alt(E const &event) {
+ return state_held_only_alt(event.state);
+}
+
+template <typename E>
+inline bool held_no_modifiers(E const &event) {
+ return state_held_no_modifiers(event.state);
+}
+
+template <typename E>
+inline bool held_any_modifiers(E const &event) {
+ return state_held_any_modifiers(event.state);
+}
+
+template <typename E>
+inline Geom::Point event_point(E const &event) {
+ return Geom::Point(event.x, event.y);
+}
+
+/** Use like this:
+ * @code if (held_button<2>(event->motion)) { ... @endcode */
+template <unsigned button, typename E>
+inline bool held_button(E const &event) {
+ return state_held_button<button>(event.state);
+}
+
+guint shortcut_key(GdkEventKey const &event);
+unsigned state_after_event(GdkEvent *event);
+
+} // namespace UI
+} // 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/ui/tool/manipulator.h b/src/ui/tool/manipulator.h
new file mode 100644
index 0000000..308ad1c
--- /dev/null
+++ b/src/ui/tool/manipulator.h
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Manipulator - edits something on-canvas
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_MANIPULATOR_H
+#define SEEN_UI_TOOL_MANIPULATOR_H
+
+#include <set>
+#include <map>
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include <glib.h>
+#include <gdk/gdk.h>
+#include "ui/tools/tool-base.h"
+
+class SPDesktop;
+namespace Inkscape {
+namespace UI {
+
+class ManipulatorGroup;
+class ControlPointSelection;
+
+/**
+ * @brief Tool component that processes events and does something in response to them.
+ * Note: this class is probably redundant.
+ */
+class Manipulator {
+friend class ManipulatorGroup;
+public:
+ Manipulator(SPDesktop *d)
+ : _desktop(d)
+ {}
+ virtual ~Manipulator() = default;
+
+ /// Handle input event. Returns true if handled.
+ virtual bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *)=0;
+ SPDesktop *const _desktop;
+};
+
+/**
+ * @brief Tool component that edits something on the canvas using selectable control points.
+ * Note: this class is probably redundant.
+ */
+class PointManipulator : public Manipulator, public sigc::trackable {
+public:
+ PointManipulator(SPDesktop *d, ControlPointSelection &sel)
+ : Manipulator(d)
+ , _selection(sel)
+ {}
+
+ /// Type of extremum points to add in PathManipulator::insertNodeAtExtremum
+ enum ExtremumType {
+ EXTR_MIN_X = 0,
+ EXTR_MAX_X,
+ EXTR_MIN_Y,
+ EXTR_MAX_Y
+ };
+protected:
+ ControlPointSelection &_selection;
+};
+
+/** Manipulator that aggregates several manipulators of the same type.
+ * The order of invoking events on the member manipulators is undefined.
+ * To make this class more useful, derive from it and add actions that can be performed
+ * on all manipulators in the set.
+ *
+ * This is not used at the moment and is probably useless. */
+template <typename T>
+class MultiManipulator : public PointManipulator {
+public:
+ //typedef typename T::ItemType ItemType;
+ typedef typename std::pair<void*, std::shared_ptr<T> > MapPair;
+ typedef typename std::map<void*, std::shared_ptr<T> > MapType;
+
+ MultiManipulator(SPDesktop *d, ControlPointSelection &sel)
+ : PointManipulator(d, sel)
+ {}
+ void addItem(void *item) {
+ std::shared_ptr<T> m(_createManipulator(item));
+ _mmap.insert(MapPair(item, m));
+ }
+ void removeItem(void *item) {
+ _mmap.erase(item);
+ }
+ void clear() {
+ _mmap.clear();
+ }
+ bool contains(void *item) {
+ return _mmap.find(item) != _mmap.end();
+ }
+ bool empty() {
+ return _mmap.empty();
+ }
+
+ void setItems(std::vector<gpointer> list) { // this function is not called anywhere ... delete ?
+ std::set<void*> to_remove;
+ for (typename MapType::iterator mi = _mmap.begin(); mi != _mmap.end(); ++mi) {
+ to_remove.insert(mi->first);
+ }
+ for (auto i:list) {
+ if (_isItemType(i)) {
+ // erase returns the number of items removed
+ // if nothing was removed, it means this item did not have a manipulator - add it
+ if (!to_remove.erase(i)) addItem(i);
+ }
+ }
+ for (auto ri : to_remove) {
+ removeItem(ri);
+ }
+ }
+
+ /** Invoke a method on all managed manipulators.
+ * Example:
+ * @code m.invokeForAll(&SomeManipulator::someMethod); @endcode
+ */
+ template <typename R>
+ void invokeForAll(R (T::*method)()) {
+ for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ ((i->second.get())->*method)();
+ }
+ }
+ template <typename R, typename A>
+ void invokeForAll(R (T::*method)(A), A a) {
+ for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ ((i->second.get())->*method)(a);
+ }
+ }
+ template <typename R, typename A>
+ void invokeForAll(R (T::*method)(A const &), A const &a) {
+ for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ ((i->second.get())->*method)(a);
+ }
+ }
+ template <typename R, typename A, typename B>
+ void invokeForAll(R (T::*method)(A,B), A a, B b) {
+ for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ ((i->second.get())->*method)(a, b);
+ }
+ }
+
+ bool event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override {
+ for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ if ((*i).second->event(event_context, event)) return true;
+ }
+ return false;
+ }
+protected:
+ virtual T *_createManipulator(void *item) = 0;
+ virtual bool _isItemType(void *item) = 0;
+ MapType _mmap;
+};
+
+} // namespace UI
+} // 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/ui/tool/modifier-tracker.cpp b/src/ui/tool/modifier-tracker.cpp
new file mode 100644
index 0000000..70c85a6
--- /dev/null
+++ b/src/ui/tool/modifier-tracker.cpp
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Fine-grained modifier tracker for event handling.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdk.h>
+#include <gdk/gdkkeysyms.h>
+#include "ui/tool/event-utils.h"
+#include "ui/tool/modifier-tracker.h"
+
+namespace Inkscape {
+namespace UI {
+
+ModifierTracker::ModifierTracker()
+ : _left_shift(false)
+ , _right_shift(false)
+ , _left_ctrl(false)
+ , _right_ctrl(false)
+ , _left_alt(false)
+ , _right_alt(false)
+{}
+
+bool ModifierTracker::event(GdkEvent *event)
+{
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ switch (shortcut_key(event->key)) {
+ case GDK_KEY_Shift_L:
+ _left_shift = true;
+ break;
+ case GDK_KEY_Shift_R:
+ _right_shift = true;
+ break;
+ case GDK_KEY_Control_L:
+ _left_ctrl = true;
+ break;
+ case GDK_KEY_Control_R:
+ _right_ctrl = true;
+ break;
+ case GDK_KEY_Alt_L:
+ _left_alt = true;
+ break;
+ case GDK_KEY_Alt_R:
+ _right_alt = true;
+ break;
+ }
+ break;
+ case GDK_KEY_RELEASE:
+ switch (shortcut_key(event->key)) {
+ case GDK_KEY_Shift_L:
+ _left_shift = false;
+ break;
+ case GDK_KEY_Shift_R:
+ _right_shift = false;
+ break;
+ case GDK_KEY_Control_L:
+ _left_ctrl = false;
+ break;
+ case GDK_KEY_Control_R:
+ _right_ctrl = false;
+ break;
+ case GDK_KEY_Alt_L:
+ _left_alt = false;
+ break;
+ case GDK_KEY_Alt_R:
+ _right_alt = false;
+ break;
+ }
+ break;
+ default: break;
+ }
+
+ return false;
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ 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/ui/tool/modifier-tracker.h b/src/ui/tool/modifier-tracker.h
new file mode 100644
index 0000000..c5762e5
--- /dev/null
+++ b/src/ui/tool/modifier-tracker.h
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Fine-grained modifier tracker for event handling.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_MODIFIER_TRACKER_H
+#define SEEN_UI_TOOL_MODIFIER_TRACKER_H
+
+#include <gdk/gdk.h>
+
+namespace Inkscape {
+namespace UI {
+
+class ModifierTracker {
+public:
+ ModifierTracker();
+ bool event(GdkEvent *);
+
+ bool leftShift() const { return _left_shift; }
+ bool rightShift() const { return _right_shift; }
+ bool leftControl() const { return _left_ctrl; }
+ bool rightControl() const { return _right_ctrl; }
+ bool leftAlt() const { return _left_alt; }
+ bool rightAlt() const { return _right_alt; }
+
+private:
+ bool _left_shift;
+ bool _right_shift;
+ bool _left_ctrl;
+ bool _right_ctrl;
+ bool _left_alt;
+ bool _right_alt;
+};
+
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_UI_TOOL_MODIFIER_TRACKER_H
+
+/*
+ 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/ui/tool/multi-path-manipulator.cpp b/src/ui/tool/multi-path-manipulator.cpp
new file mode 100644
index 0000000..a4f34de
--- /dev/null
+++ b/src/ui/tool/multi-path-manipulator.cpp
@@ -0,0 +1,907 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Multi path manipulator - implementation.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <unordered_set>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document.h"
+#include "document-undo.h"
+#include "message-stack.h"
+#include "node.h"
+
+#include "live_effects/lpeobject.h"
+
+#include "object/sp-path.h"
+
+#include "ui/icon-names.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/path-manipulator.h"
+
+namespace Inkscape {
+namespace UI {
+
+namespace {
+
+struct hash_nodelist_iterator
+{
+ std::size_t operator()(NodeList::iterator i) const {
+ return std::hash<NodeList::iterator::pointer>()(&*i);
+ }
+};
+
+typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair;
+typedef std::vector<IterPair> IterPairList;
+typedef std::unordered_set<NodeList::iterator, hash_nodelist_iterator> IterSet;
+typedef std::multimap<double, IterPair> DistanceMap;
+typedef std::pair<double, IterPair> DistanceMapItem;
+
+/** Find pairs of selected endnodes suitable for joining. */
+void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs)
+{
+ IterSet join_iters;
+
+ // find all endnodes in selection
+ for (auto i : sel) {
+ Node *node = dynamic_cast<Node*>(i);
+ if (!node) continue;
+ NodeList::iterator iter = NodeList::get_iterator(node);
+ if (!iter.next() || !iter.prev()) join_iters.insert(iter);
+ }
+
+ if (join_iters.size() < 2) return;
+
+ // Below we find the closest pairs. The algorithm is O(N^3).
+ // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs
+ // with their distances in a multimap (not worth it IMO).
+ while (join_iters.size() >= 2) {
+ double closest = DBL_MAX;
+ IterPair closest_pair;
+ for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) {
+ for (IterSet::iterator j = join_iters.begin(); j != i; ++j) {
+ double dist = Geom::distance(**i, **j);
+ if (dist < closest) {
+ closest = dist;
+ closest_pair = std::make_pair(*i, *j);
+ }
+ }
+ }
+ pairs.push_back(closest_pair);
+ join_iters.erase(closest_pair.first);
+ join_iters.erase(closest_pair.second);
+ }
+}
+
+/** After this function, first should be at the end of path and second at the beginning.
+ * @returns True if the nodes are in the same subpath */
+bool prepare_join(IterPair &join_iters)
+{
+ if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) {
+ if (join_iters.first.next()) // if first is begin, swap the iterators
+ std::swap(join_iters.first, join_iters.second);
+ return true;
+ }
+
+ NodeList &sp_first = NodeList::get(join_iters.first);
+ NodeList &sp_second = NodeList::get(join_iters.second);
+ if (join_iters.first.next()) { // first is begin
+ if (join_iters.second.next()) { // second is begin
+ sp_first.reverse();
+ } else { // second is end
+ std::swap(join_iters.first, join_iters.second);
+ }
+ } else { // first is end
+ if (join_iters.second.next()) { // second is begin
+ // do nothing
+ } else { // second is end
+ sp_second.reverse();
+ }
+ }
+ return false;
+}
+} // anonymous namespace
+
+
+MultiPathManipulator::MultiPathManipulator(PathSharedData &data, sigc::connection &chg)
+ : PointManipulator(data.node_data.desktop, *data.node_data.selection)
+ , _path_data(data)
+ , _changed(chg)
+{
+ _selection.signal_commit.connect(
+ sigc::mem_fun(*this, &MultiPathManipulator::_commit));
+ _selection.signal_selection_changed.connect(
+ sigc::hide( sigc::hide(
+ signal_coords_changed.make_slot())));
+}
+
+MultiPathManipulator::~MultiPathManipulator()
+{
+ _mmap.clear();
+}
+
+/** Remove empty manipulators. */
+void MultiPathManipulator::cleanup()
+{
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
+ if (i->second->empty()) i = _mmap.erase(i);
+ else ++i;
+ }
+}
+
+/**
+ * Change the set of items to edit.
+ *
+ * This method attempts to preserve as much of the state as possible.
+ */
+void MultiPathManipulator::setItems(std::set<ShapeRecord> const &s)
+{
+ std::set<ShapeRecord> shapes(s);
+
+ // iterate over currently edited items, modifying / removing them as necessary
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) {
+ std::set<ShapeRecord>::iterator si = shapes.find(i->first);
+ if (si == shapes.end()) {
+ // This item is no longer supposed to be edited - remove its manipulator
+ i = _mmap.erase(i);
+ } else {
+ ShapeRecord const &sr = i->first;
+ ShapeRecord const &sr_new = *si;
+ // if the shape record differs, replace the key only and modify other values
+ if (sr.edit_transform != sr_new.edit_transform ||
+ sr.role != sr_new.role)
+ {
+ std::shared_ptr<PathManipulator> hold(i->second);
+ if (sr.edit_transform != sr_new.edit_transform)
+ hold->setControlsTransform(sr_new.edit_transform);
+ if (sr.role != sr_new.role) {
+ //hold->setOutlineColor(_getOutlineColor(sr_new.role));
+ }
+ i = _mmap.erase(i);
+ _mmap.insert(std::make_pair(sr_new, hold));
+ } else {
+ ++i;
+ }
+ shapes.erase(si); // remove the processed record
+ }
+ }
+
+ // add newly selected items
+ for (const auto & r : shapes) {
+ auto lpobj = cast<LivePathEffectObject>(r.object);
+ if (!is<SPPath>(r.object) && !lpobj) continue;
+ std::shared_ptr<PathManipulator> newpm(new PathManipulator(*this, (SPPath*) r.object,
+ r.edit_transform, _getOutlineColor(r.role, r.object), r.lpe_key));
+ newpm->showHandles(_show_handles);
+ // always show outlines for clips and masks
+ newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL);
+ newpm->showPathDirection(_show_path_direction);
+ newpm->setLiveOutline(_live_outline);
+ newpm->setLiveObjects(_live_objects);
+ _mmap.insert(std::make_pair(r, newpm));
+ }
+}
+
+void MultiPathManipulator::selectSubpaths()
+{
+ if (_selection.empty()) {
+ _selection.selectAll();
+ } else {
+ invokeForAll(&PathManipulator::selectSubpaths);
+ }
+}
+
+void MultiPathManipulator::shiftSelection(int dir)
+{
+ if (empty()) return;
+
+ // 1. find last selected node
+ // 2. select the next node; if the last node or nothing is selected,
+ // select first node
+ MapType::iterator last_i;
+ SubpathList::iterator last_j;
+ NodeList::iterator last_k;
+ bool anything_found = false;
+ bool anynode_found = false;
+
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ SubpathList &sp = i->second->subpathList();
+ for (SubpathList::iterator j = sp.begin(); j != sp.end(); ++j) {
+ anynode_found = true;
+ for (NodeList::iterator k = (*j)->begin(); k != (*j)->end(); ++k) {
+ if (k->selected()) {
+ last_i = i;
+ last_j = j;
+ last_k = k;
+ anything_found = true;
+ // when tabbing backwards, we want the first node
+ if (dir == -1) goto exit_loop;
+ }
+ }
+ }
+ }
+ exit_loop:
+
+ // NOTE: we should not assume the _selection contains only nodes
+ // in future it might also contain handles and other types of control points
+ // this is why we use a flag instead in the loop above, instead of calling
+ // selection.empty()
+ if (!anything_found) {
+ // select first / last node
+ // this should never fail because there must be at least 1 non-empty manipulator
+ if (anynode_found) {
+ if (dir == 1) {
+ _selection.insert((*_mmap.begin()->second->subpathList().begin())->begin().ptr());
+ } else {
+ _selection.insert((--(*--(--_mmap.end())->second->subpathList().end())->end()).ptr());
+ }
+ }
+ return;
+ }
+
+ // three levels deep - w00t!
+ if (dir == 1) {
+ if (++last_k == (*last_j)->end()) {
+ // here, last_k points to the node to be selected
+ ++last_j;
+ if (last_j == last_i->second->subpathList().end()) {
+ ++last_i;
+ if (last_i == _mmap.end()) {
+ last_i = _mmap.begin();
+ }
+ last_j = last_i->second->subpathList().begin();
+ }
+ last_k = (*last_j)->begin();
+ }
+ } else {
+ if (!last_k || last_k == (*last_j)->begin()) {
+ if (last_j == last_i->second->subpathList().begin()) {
+ if (last_i == _mmap.begin()) {
+ last_i = _mmap.end();
+ }
+ --last_i;
+ last_j = last_i->second->subpathList().end();
+ }
+ --last_j;
+ last_k = (*last_j)->end();
+ }
+ --last_k;
+ }
+ _selection.clear();
+ _selection.insert(last_k.ptr());
+}
+
+void MultiPathManipulator::invertSelectionInSubpaths()
+{
+ invokeForAll(&PathManipulator::invertSelectionInSubpaths);
+}
+
+void MultiPathManipulator::setNodeType(NodeType type)
+{
+ if (_selection.empty()) return;
+
+ // When all selected nodes are already cusp, retract their handles
+ bool retract_handles = (type == NODE_CUSP);
+
+ for (auto i : _selection) {
+ Node *node = dynamic_cast<Node*>(i);
+ if (node) {
+ retract_handles &= (node->type() == NODE_CUSP);
+ node->setType(type);
+ }
+ }
+
+ if (retract_handles) {
+ for (auto i : _selection) {
+ Node *node = dynamic_cast<Node*>(i);
+ if (node) {
+ node->front()->retract();
+ node->back()->retract();
+ }
+ }
+ }
+
+ _done(retract_handles ? _("Retract handles") : _("Change node type"));
+}
+
+void MultiPathManipulator::setSegmentType(SegmentType type)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::setSegmentType, type);
+ if (type == SEGMENT_STRAIGHT) {
+ _done(_("Straighten segments"));
+ } else {
+ _done(_("Make segments curves"));
+ }
+}
+
+void MultiPathManipulator::insertNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::insertNodes);
+ _done(_("Add nodes"));
+}
+void MultiPathManipulator::insertNodesAtExtrema(ExtremumType extremum)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::insertNodeAtExtremum, extremum);
+ _done(_("Add extremum nodes"));
+}
+
+void MultiPathManipulator::insertNode(Geom::Point pt)
+{
+ // When double clicking to insert nodes, we might not have a selection of nodes (and we don't need one)
+ // so don't check for "_selection.empty()" here, contrary to the other methods above and below this one
+ invokeForAll(&PathManipulator::insertNode, pt);
+ _done(_("Add nodes"));
+}
+
+void MultiPathManipulator::duplicateNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::duplicateNodes);
+ _done(_("Duplicate nodes"));
+}
+
+void MultiPathManipulator::copySelectedPath(Geom::PathBuilder *builder)
+{
+ if (_selection.empty())
+ return;
+ invokeForAll(&PathManipulator::copySelectedPath, builder);
+ _done(_("Copy nodes"));
+}
+
+void MultiPathManipulator::joinNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::hideDragPoint);
+ // Node join has two parts. In the first one we join two subpaths by fusing endpoints
+ // into one. In the second we fuse nodes in each subpath.
+ IterPairList joins;
+ NodeList::iterator preserve_pos;
+ Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point);
+ if (mouseover_node) {
+ preserve_pos = NodeList::get_iterator(mouseover_node);
+ }
+ find_join_iterators(_selection, joins);
+
+ for (auto & join : joins) {
+ bool same_path = prepare_join(join);
+ NodeList &sp_first = NodeList::get(join.first);
+ NodeList &sp_second = NodeList::get(join.second);
+ join.first->setType(NODE_CUSP, false);
+
+ Geom::Point joined_pos, pos_handle_front, pos_handle_back;
+ pos_handle_front = *join.second->front();
+ pos_handle_back = *join.first->back();
+
+ // When we encounter the mouseover node, we unset the iterator - it will be invalidated
+ if (join.first == preserve_pos) {
+ joined_pos = *join.first;
+ preserve_pos = NodeList::iterator();
+ } else if (join.second == preserve_pos) {
+ joined_pos = *join.second;
+ preserve_pos = NodeList::iterator();
+ } else {
+ joined_pos = Geom::middle_point(*join.first, *join.second);
+ }
+
+ // if the handles aren't degenerate, don't move them
+ join.first->move(joined_pos);
+ Node *joined_node = join.first.ptr();
+ if (!join.second->front()->isDegenerate()) {
+ joined_node->front()->setPosition(pos_handle_front);
+ }
+ if (!join.first->back()->isDegenerate()) {
+ joined_node->back()->setPosition(pos_handle_back);
+ }
+ sp_second.erase(join.second);
+
+ if (same_path) {
+ sp_first.setClosed(true);
+ } else {
+ sp_first.splice(sp_first.end(), sp_second);
+ sp_second.kill();
+ }
+ _selection.insert(join.first.ptr());
+ }
+
+ if (joins.empty()) {
+ // Second part replaces contiguous selections of nodes with single nodes
+ invokeForAll(&PathManipulator::weldNodes, preserve_pos);
+ }
+
+ _doneWithCleanup(_("Join nodes"), true);
+}
+
+void MultiPathManipulator::breakNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::breakNodes);
+ _done(_("Break nodes"), true);
+}
+
+void MultiPathManipulator::deleteNodes(bool keep_shape) {
+ deleteNodes(keep_shape ? NodeDeleteMode::curve_fit : NodeDeleteMode::line_segment);
+}
+
+void MultiPathManipulator::deleteNodes(NodeDeleteMode mode)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::deleteNodes, mode);
+ _doneWithCleanup(_("Delete nodes"), true);
+}
+
+/** Join selected endpoints to create segments. */
+void MultiPathManipulator::joinSegments()
+{
+ if (_selection.empty()) return;
+ IterPairList joins;
+ find_join_iterators(_selection, joins);
+
+ for (auto & join : joins) {
+ bool same_path = prepare_join(join);
+ NodeList &sp_first = NodeList::get(join.first);
+ NodeList &sp_second = NodeList::get(join.second);
+ join.first->setType(NODE_CUSP, false);
+ join.second->setType(NODE_CUSP, false);
+ if (same_path) {
+ sp_first.setClosed(true);
+ } else {
+ sp_first.splice(sp_first.end(), sp_second);
+ sp_second.kill();
+ }
+ }
+
+ if (joins.empty()) {
+ invokeForAll(&PathManipulator::weldSegments);
+ }
+ _doneWithCleanup("Join segments", true);
+}
+
+void MultiPathManipulator::deleteSegments()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::deleteSegments);
+ _doneWithCleanup("Delete segments", true);
+}
+
+void MultiPathManipulator::alignNodes(Geom::Dim2 d, AlignTargetNode target)
+{
+ if (_selection.empty()) return;
+ _selection.align(d, target);
+ if (d == Geom::X) {
+ _done("Align nodes to a horizontal line");
+ } else {
+ _done("Align nodes to a vertical line");
+ }
+}
+
+void MultiPathManipulator::distributeNodes(Geom::Dim2 d)
+{
+ if (_selection.empty()) return;
+ _selection.distribute(d);
+ if (d == Geom::X) {
+ _done("Distribute nodes horizontally");
+ } else {
+ _done("Distribute nodes vertically");
+ }
+}
+
+void MultiPathManipulator::reverseSubpaths()
+{
+ if (_selection.empty()) {
+ invokeForAll(&PathManipulator::reverseSubpaths, false);
+ _done("Reverse subpaths");
+ } else {
+ invokeForAll(&PathManipulator::reverseSubpaths, true);
+ _done("Reverse selected subpaths");
+ }
+}
+
+void MultiPathManipulator::move(Geom::Point const &delta)
+{
+ if (_selection.empty()) return;
+ _selection.transform(Geom::Translate(delta));
+ _done("Move nodes");
+}
+
+void MultiPathManipulator::showOutline(bool show)
+{
+ for (auto & i : _mmap) {
+ // always show outlines for clipping paths and masks
+ i.second->showOutline(show || i.first.role != SHAPE_ROLE_NORMAL);
+ }
+ _show_outline = show;
+}
+
+void MultiPathManipulator::showHandles(bool show)
+{
+ invokeForAll(&PathManipulator::showHandles, show);
+ _show_handles = show;
+}
+
+void MultiPathManipulator::showPathDirection(bool show)
+{
+ invokeForAll(&PathManipulator::showPathDirection, show);
+ _show_path_direction = show;
+}
+
+/**
+ * Set live outline update status.
+ * When set to true, outline will be updated continuously when dragging
+ * or transforming nodes. Otherwise it will only update when changes are committed
+ * to XML.
+ */
+void MultiPathManipulator::setLiveOutline(bool set)
+{
+ invokeForAll(&PathManipulator::setLiveOutline, set);
+ _live_outline = set;
+}
+
+/**
+ * Set live object update status.
+ * When set to true, objects will be updated continuously when dragging
+ * or transforming nodes. Otherwise they will only update when changes are committed
+ * to XML.
+ */
+void MultiPathManipulator::setLiveObjects(bool set)
+{
+ invokeForAll(&PathManipulator::setLiveObjects, set);
+ _live_objects = set;
+}
+
+void MultiPathManipulator::updateOutlineColors()
+{
+ //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ // i->second->setOutlineColor(_getOutlineColor(i->first.role));
+ //}
+}
+
+void MultiPathManipulator::updateHandles()
+{
+ invokeForAll(&PathManipulator::updateHandles);
+}
+
+void MultiPathManipulator::updatePaths()
+{
+ invokeForAll(&PathManipulator::updatePath);
+}
+
+bool MultiPathManipulator::event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ _tracker.event(event);
+ guint key = 0;
+ if (event->type == GDK_KEY_PRESS) {
+ key = shortcut_key(event->key);
+ }
+
+ // Single handle adjustments go here.
+ if (_selection.size() == 1 && event->type == GDK_KEY_PRESS) {
+ do {
+ Node *n = dynamic_cast<Node *>(*_selection.begin());
+ if (!n) break;
+
+ PathManipulator &pm = n->nodeList().subpathList().pm();
+
+ int which = 0;
+ if (_tracker.rightAlt() || _tracker.rightControl()) {
+ which = 1;
+ }
+ if (_tracker.leftAlt() || _tracker.leftControl()) {
+ if (which != 0) break; // ambiguous
+ which = -1;
+ }
+ if (which == 0) break; // no handle chosen
+ bool one_pixel = _tracker.leftAlt() || _tracker.rightAlt();
+ bool handled = true;
+
+ switch (key) {
+ // single handle functions
+ // rotation
+ case GDK_KEY_bracketleft:
+ case GDK_KEY_braceleft:
+ pm.rotateHandle(n, which, -_desktop->yaxisdir(), one_pixel);
+ break;
+ case GDK_KEY_bracketright:
+ case GDK_KEY_braceright:
+ pm.rotateHandle(n, which, _desktop->yaxisdir(), one_pixel);
+ break;
+ // adjust length
+ case GDK_KEY_period:
+ case GDK_KEY_greater:
+ pm.scaleHandle(n, which, 1, one_pixel);
+ break;
+ case GDK_KEY_comma:
+ case GDK_KEY_less:
+ pm.scaleHandle(n, which, -1, one_pixel);
+ break;
+ default:
+ handled = false;
+ break;
+ }
+
+ if (handled) return true;
+ } while(false);
+ }
+
+
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ switch (key) {
+ case GDK_KEY_Insert:
+ case GDK_KEY_KP_Insert:
+ // Insert - insert nodes in the middle of selected segments
+ insertNodes();
+ return true;
+ case GDK_KEY_i:
+ case GDK_KEY_I:
+ if (held_only_shift(event->key)) {
+ // Shift+I - insert nodes (alternate keybinding for Mac keyboards
+ // that don't have the Insert key)
+ insertNodes();
+ return true;
+ }
+ break;
+ case GDK_KEY_d:
+ case GDK_KEY_D:
+ if (held_only_shift(event->key)) {
+ duplicateNodes();
+ return true;
+ }
+ case GDK_KEY_j:
+ case GDK_KEY_J:
+ if (held_only_shift(event->key)) {
+ // Shift+J - join nodes
+ joinNodes();
+ return true;
+ }
+ if (held_only_alt(event->key)) {
+ // Alt+J - join segments
+ joinSegments();
+ return true;
+ }
+ break;
+ case GDK_KEY_b:
+ case GDK_KEY_B:
+ if (held_only_shift(event->key)) {
+ // Shift+B - break nodes
+ breakNodes();
+ return true;
+ }
+ break;
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ if (held_shift(event->key)) break;
+ if (held_alt(event->key)) {
+ // Alt+Delete - delete segments
+ deleteSegments();
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool del_preserves_shape = prefs->getBool("/tools/nodes/delete_preserves_shape", true);
+ //MK: how can multi-path-manipulator know it is dealing with a bspline if it's checking tool mode???
+ /*
+ // pass keep_shape = true when:
+ // a) del preserves shape, and control is not pressed
+ // b) ctrl+del preserves shape (del_preserves_shape is false), and control is pressed
+ // Hence xor
+ guint mode = prefs->getInt("/tools/freehand/pen/freehand-mode", 0);
+ //if the trace is bspline ( mode 2)
+ if(mode==2){
+ // is this correct ?
+ if(del_preserves_shape ^ held_control(event->key)){
+ deleteNodes(false);
+ } else {
+ deleteNodes(true);
+ }
+ } else {
+ */
+ auto mode =
+ held_control(event->key) ?
+ (del_preserves_shape ? NodeDeleteMode::inverse_auto : NodeDeleteMode::curve_fit) :
+ (del_preserves_shape ? NodeDeleteMode::automatic : NodeDeleteMode::line_segment);
+ deleteNodes(mode);
+
+ // Delete any selected gradient nodes as well
+ event_context->deleteSelectedDrag(held_control(event->key));
+ }
+ return true;
+ case GDK_KEY_c:
+ case GDK_KEY_C:
+ if (held_only_shift(event->key)) {
+ // Shift+C - make nodes cusp
+ setNodeType(NODE_CUSP);
+ return true;
+ }
+ break;
+ case GDK_KEY_s:
+ case GDK_KEY_S:
+ if (held_only_shift(event->key)) {
+ // Shift+S - make nodes smooth
+ setNodeType(NODE_SMOOTH);
+ return true;
+ }
+ break;
+ case GDK_KEY_a:
+ case GDK_KEY_A:
+ if (held_only_shift(event->key)) {
+ // Shift+A - make nodes auto-smooth
+ setNodeType(NODE_AUTO);
+ return true;
+ }
+ break;
+ case GDK_KEY_y:
+ case GDK_KEY_Y:
+ if (held_only_shift(event->key)) {
+ // Shift+Y - make nodes symmetric
+ setNodeType(NODE_SYMMETRIC);
+ return true;
+ }
+ break;
+ case GDK_KEY_r:
+ case GDK_KEY_R:
+ if (held_only_shift(event->key)) {
+ // Shift+R - reverse subpaths
+ reverseSubpaths();
+ return true;
+ }
+ break;
+ case GDK_KEY_l:
+ case GDK_KEY_L:
+ if (held_only_shift(event->key)) {
+ // Shift+L - make segments linear
+ setSegmentType(SEGMENT_STRAIGHT);
+ return true;
+ }
+ case GDK_KEY_u:
+ case GDK_KEY_U:
+ if (held_only_shift(event->key)) {
+ // Shift+U - make segments curves
+ setSegmentType(SEGMENT_CUBIC_BEZIER);
+ return true;
+ }
+ default:
+ break;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ for (auto & i : _mmap) {
+ if (i.second->event(event_context, event)) return true;
+ }
+ break;
+ default: break;
+ }
+
+ return false;
+}
+
+/** Commit changes to XML and add undo stack entry based on the action that was done. Invoked
+ * by sub-manipulators, for example TransformHandleSet and ControlPointSelection. */
+void MultiPathManipulator::_commit(CommitEvent cps)
+{
+ gchar const *reason = nullptr;
+ gchar const *key = nullptr;
+ switch(cps) {
+ case COMMIT_MOUSE_MOVE:
+ reason = _("Move nodes");
+ break;
+ case COMMIT_KEYBOARD_MOVE_X:
+ reason = _("Move nodes horizontally");
+ key = "node:move:x";
+ break;
+ case COMMIT_KEYBOARD_MOVE_Y:
+ reason = _("Move nodes vertically");
+ key = "node:move:y";
+ break;
+ case COMMIT_MOUSE_ROTATE:
+ reason = _("Rotate nodes");
+ break;
+ case COMMIT_KEYBOARD_ROTATE:
+ reason = _("Rotate nodes");
+ key = "node:rotate";
+ break;
+ case COMMIT_MOUSE_SCALE_UNIFORM:
+ reason = _("Scale nodes uniformly");
+ break;
+ case COMMIT_MOUSE_SCALE:
+ reason = _("Scale nodes");
+ break;
+ case COMMIT_KEYBOARD_SCALE_UNIFORM:
+ reason = _("Scale nodes uniformly");
+ key = "node:scale:uniform";
+ break;
+ case COMMIT_KEYBOARD_SCALE_X:
+ reason = _("Scale nodes horizontally");
+ key = "node:scale:x";
+ break;
+ case COMMIT_KEYBOARD_SCALE_Y:
+ reason = _("Scale nodes vertically");
+ key = "node:scale:y";
+ break;
+ case COMMIT_MOUSE_SKEW_X:
+ reason = _("Skew nodes horizontally");
+ key = "node:skew:x";
+ break;
+ case COMMIT_MOUSE_SKEW_Y:
+ reason = _("Skew nodes vertically");
+ key = "node:skew:y";
+ break;
+ case COMMIT_FLIP_X:
+ reason = _("Flip nodes horizontally");
+ break;
+ case COMMIT_FLIP_Y:
+ reason = _("Flip nodes vertically");
+ break;
+ default: return;
+ }
+
+ _selection.signal_update.emit();
+ invokeForAll(&PathManipulator::writeXML);
+ if (key) {
+ DocumentUndo::maybeDone(_desktop->getDocument(), key, reason, INKSCAPE_ICON("tool-node-editor"));
+ } else {
+ DocumentUndo::done(_desktop->getDocument(), reason, INKSCAPE_ICON("tool-node-editor"));
+ }
+ signal_coords_changed.emit();
+}
+
+/** Commits changes to XML and adds undo stack entry. */
+void MultiPathManipulator::_done(gchar const *reason, bool alert_LPE) {
+ invokeForAll(&PathManipulator::update, alert_LPE);
+ invokeForAll(&PathManipulator::writeXML);
+ DocumentUndo::done(_desktop->getDocument(), reason, INKSCAPE_ICON("tool-node-editor"));
+ signal_coords_changed.emit();
+}
+
+/** Commits changes to XML, adds undo stack entry and removes empty manipulators. */
+void MultiPathManipulator::_doneWithCleanup(gchar const *reason, bool alert_LPE) {
+ _changed.block();
+ _done(reason, alert_LPE);
+ cleanup();
+ _changed.unblock();
+}
+
+/** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */
+guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role, SPObject *object)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ switch(role) {
+ case SHAPE_ROLE_CLIPPING_PATH:
+ return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff);
+ case SHAPE_ROLE_MASK:
+ return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff);
+ case SHAPE_ROLE_LPE_PARAM:
+ return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff);
+ case SHAPE_ROLE_NORMAL:
+ default:
+ return cast<SPItem>(object)->highlight_color();
+ }
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ 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/ui/tool/multi-path-manipulator.h b/src/ui/tool/multi-path-manipulator.h
new file mode 100644
index 0000000..7ae6b9e
--- /dev/null
+++ b/src/ui/tool/multi-path-manipulator.h
@@ -0,0 +1,159 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Multi path manipulator - a tool component that edits multiple paths at once
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_MULTI_PATH_MANIPULATOR_H
+#define SEEN_UI_TOOL_MULTI_PATH_MANIPULATOR_H
+
+#include <cstddef>
+#include <sigc++/connection.h>
+#include <2geom/path-sink.h>
+#include "node.h"
+#include "commit-events.h"
+#include "manipulator.h"
+#include "modifier-tracker.h"
+#include "node-types.h"
+#include "shape-record.h"
+#include "ui/tool/path-manipulator.h"
+
+struct SPCanvasGroup;
+
+namespace Inkscape {
+namespace UI {
+
+class PathManipulator;
+class MultiPathManipulator;
+struct PathSharedData;
+
+/**
+ * Manipulator that manages multiple path manipulators active at the same time.
+ */
+class MultiPathManipulator : public PointManipulator {
+public:
+ MultiPathManipulator(PathSharedData &data, sigc::connection &chg);
+ ~MultiPathManipulator() override;
+ bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *event) override;
+
+ bool empty() { return _mmap.empty(); }
+ unsigned size() { return _mmap.size(); }
+ void setItems(std::set<ShapeRecord> const &);
+ void clear() { _mmap.clear(); }
+ void cleanup();
+
+ void selectSubpaths();
+ void shiftSelection(int dir);
+ void invertSelectionInSubpaths();
+
+ void setNodeType(NodeType t);
+ void setSegmentType(SegmentType t);
+
+ void insertNodesAtExtrema(ExtremumType extremum);
+ void insertNodes();
+ void insertNode(Geom::Point pt);
+ void alertLPE();
+ void duplicateNodes();
+ void copySelectedPath(Geom::PathBuilder *builder);
+ void joinNodes();
+ void breakNodes();
+ void deleteNodes(NodeDeleteMode mode);
+ void deleteNodes(bool keep_shape);
+ void joinSegments();
+ void deleteSegments();
+ void alignNodes(Geom::Dim2 d, AlignTargetNode target = AlignTargetNode::MID_NODE);
+ void distributeNodes(Geom::Dim2 d);
+ void reverseSubpaths();
+ void move(Geom::Point const &delta);
+
+ void showOutline(bool show);
+ void showHandles(bool show);
+ void showPathDirection(bool show);
+ void setLiveOutline(bool set);
+ void setLiveObjects(bool set);
+ void updateOutlineColors();
+ void updateHandles();
+ void updatePaths();
+
+ sigc::signal<void ()> signal_coords_changed; /// Emitted whenever the coordinates
+ /// shown in the status bar need updating
+private:
+ typedef std::pair<ShapeRecord, std::shared_ptr<PathManipulator> > MapPair;
+ typedef std::map<ShapeRecord, std::shared_ptr<PathManipulator> > MapType;
+
+ template <typename R>
+ void invokeForAll(R (PathManipulator::*method)()) {
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
+ // Sometimes the PathManipulator got freed at loop end, thus
+ // invalidating the iterator so make sure that next_i will
+ // be a valid iterator and then assign i to it.
+ MapType::iterator next_i = i;
+ ++next_i;
+ // i->second is a std::shared_ptr so try to hold on to it so
+ // it won't get freed prematurely by the WriteXML() method or
+ // whatever. See https://bugs.launchpad.net/inkscape/+bug/1617615
+ // Applicable to empty paths.
+ std::shared_ptr<PathManipulator> hold(i->second);
+ ((hold.get())->*method)();
+ i = next_i;
+ }
+ }
+ template <typename R, typename A>
+ void invokeForAll(R (PathManipulator::*method)(A), A a) {
+ for (auto & i : _mmap) {
+ ((i.second.get())->*method)(a);
+ }
+ }
+ template <typename R, typename A>
+ void invokeForAll(R (PathManipulator::*method)(A const &), A const &a) {
+ for (auto & i : _mmap) {
+ ((i.second.get())->*method)(a);
+ }
+ }
+ template <typename R, typename A, typename B>
+ void invokeForAll(R (PathManipulator::*method)(A,B), A a, B b) {
+ for (auto & i : _mmap) {
+ ((i.second.get())->*method)(a, b);
+ }
+ }
+
+ void _commit(CommitEvent cps);
+ void _done(gchar const *reason, bool alert_LPE = true);
+ void _doneWithCleanup(gchar const *reason, bool alert_LPE = false);
+ guint32 _getOutlineColor(ShapeRole role, SPObject *object);
+
+ MapType _mmap;
+public:
+ PathSharedData const &_path_data;
+private:
+ sigc::connection &_changed;
+ ModifierTracker _tracker;
+ bool _show_handles;
+ bool _show_outline;
+ bool _show_path_direction;
+ bool _live_outline;
+ bool _live_objects;
+
+ friend class PathManipulator;
+};
+
+} // namespace UI
+} // 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/ui/tool/node-types.h b/src/ui/tool/node-types.h
new file mode 100644
index 0000000..bad6a5c
--- /dev/null
+++ b/src/ui/tool/node-types.h
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Node types and other small enums.
+ * This file exists to reduce the number of includes pulled in by toolbox.cpp.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_NODE_TYPES_H
+#define SEEN_UI_TOOL_NODE_TYPES_H
+
+namespace Inkscape {
+namespace UI {
+
+/** Types of nodes supported in the node tool. */
+enum NodeType {
+ NODE_CUSP, ///< Cusp node - no handle constraints
+ NODE_SMOOTH, ///< Smooth node - handles must be colinear
+ NODE_AUTO, ///< Auto node - handles adjusted automatically based on neighboring nodes
+ NODE_SYMMETRIC, ///< Symmetric node - handles must be colinear and of equal length
+ NODE_LAST_REAL_TYPE, ///< Last real type of node - used for ctrl+click on a node
+ NODE_PICK_BEST = 100 ///< Select type based on handle positions
+};
+
+/** Types of segments supported in the node tool. */
+enum SegmentType {
+ SEGMENT_STRAIGHT, ///< Straight linear segment
+ SEGMENT_CUBIC_BEZIER ///< Bezier curve with two control points
+};
+
+enum class AlignTargetNode {
+ LAST_NODE,
+ FIRST_NODE,
+ MID_NODE,
+ MIN_NODE,
+ MAX_NODE
+};
+
+} // namespace UI
+} // 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/ui/tool/node.cpp b/src/ui/tool/node.cpp
new file mode 100644
index 0000000..b1cd452
--- /dev/null
+++ b/src/ui/tool/node.cpp
@@ -0,0 +1,1915 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <atomic>
+#include <iostream>
+#include <stdexcept>
+#include <boost/utility.hpp>
+
+#include <glib/gi18n.h>
+#include <gdk/gdkkeysyms.h>
+
+#include <2geom/bezier-utils.h>
+
+#include "desktop.h"
+#include "multi-path-manipulator.h"
+#include "snap.h"
+
+#include "display/control/canvas-item-group.h"
+#include "display/control/canvas-item-curve.h"
+
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/path-manipulator.h"
+#include "ui/tools/node-tool.h"
+#include "ui/widget/canvas.h"
+#include "ui/modifiers.h"
+
+namespace {
+
+Inkscape::CanvasItemCtrlType nodeTypeToCtrlType(Inkscape::UI::NodeType type)
+{
+ Inkscape::CanvasItemCtrlType result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_CUSP;
+ switch(type) {
+ case Inkscape::UI::NODE_SMOOTH:
+ result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH;
+ break;
+ case Inkscape::UI::NODE_AUTO:
+ result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_AUTO;
+ break;
+ case Inkscape::UI::NODE_SYMMETRIC:
+ result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL;
+ break;
+ case Inkscape::UI::NODE_CUSP:
+ default:
+ result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_CUSP;
+ break;
+ }
+ return result;
+}
+
+/**
+ * @brief provides means to estimate float point rounding error due to serialization to svg
+ *
+ * Keeps cached value up to date with preferences option `/options/svgoutput/numericprecision`
+ * to avoid costly direct reads
+ * */
+class SvgOutputPrecisionWatcher : public Inkscape::Preferences::Observer {
+public:
+ /// Returns absolute \a value`s rounding serialization error based on current preferences settings
+ static double error_of(double value) {
+ return value * instance().rel_error;
+ }
+
+ void notify(const Inkscape::Preferences::Entry &new_val) override {
+ int digits = new_val.getIntLimited(6, 1, 16);
+ set_numeric_precision(digits);
+ }
+
+private:
+ SvgOutputPrecisionWatcher() : Observer("/options/svgoutput/numericprecision"), rel_error(1) {
+ Inkscape::Preferences::get()->addObserver(*this);
+ int digits = Inkscape::Preferences::get()->getIntLimited("/options/svgoutput/numericprecision", 6, 1, 16);
+ set_numeric_precision(digits);
+ }
+
+ ~SvgOutputPrecisionWatcher() override {
+ Inkscape::Preferences::get()->removeObserver(*this);
+ }
+ /// Update cached value of relative error with number of significant digits
+ void set_numeric_precision(int digits) {
+ double relative_error = 0.5; // the error is half of last digit
+ while (digits > 0) {
+ relative_error /= 10;
+ digits--;
+ }
+ rel_error = relative_error;
+ }
+
+ static SvgOutputPrecisionWatcher &instance() {
+ static SvgOutputPrecisionWatcher _instance;
+ return _instance;
+ }
+
+ std::atomic<double> rel_error; /// Cached relative error
+};
+
+/// Returns absolute error of \a point as if serialized to svg with current preferences
+double serializing_error_of(const Geom::Point &point) {
+ return SvgOutputPrecisionWatcher::error_of(point.length());
+}
+
+/**
+ * @brief Returns true if three points are collinear within current serializing precision
+ *
+ * The algorithm of collinearity check is explicitly used to calculate the check error.
+ *
+ * This function can be sufficiently reduced or even removed completely if `Geom::are_collinear`
+ * would declare it's check algorithm as part of the public API.
+ *
+ * */
+bool are_collinear_within_serializing_error(const Geom::Point &A, const Geom::Point &B, const Geom::Point &C) {
+ const double tolerance_factor = 10; // to account other factors which increase uncertainty
+ const double tolerance_A = serializing_error_of(A) * tolerance_factor;
+ const double tolerance_B = serializing_error_of(B) * tolerance_factor;
+ const double tolerance_C = serializing_error_of(C) * tolerance_factor;
+ const double CB_length = (B - C).length();
+ const double AB_length = (B - A).length();
+ Geom::Point C_reflect_scaled = B + (B - C) / CB_length * AB_length;
+ double tolerance_C_reflect_scaled = tolerance_B
+ + (tolerance_B + tolerance_C)
+ * (1 + (tolerance_A + tolerance_B) / AB_length)
+ * (1 + (tolerance_C + tolerance_B) / CB_length);
+ return Geom::are_near(C_reflect_scaled, A, tolerance_C_reflect_scaled + tolerance_A);
+}
+
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+
+const double NO_POWER = 0.0;
+const double DEFAULT_START_POWER = 1.0/3.0;
+
+ControlPoint::ColorSet Node::node_colors = {
+ {0xbfbfbf00, 0x000000ff}, // normal fill, stroke
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke
+ {0xff000000, 0x000000ff}, // clicked fill, stroke
+ //
+ {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected
+ {0xff000000, 0x000000ff} // clicked fill, stroke when selected
+};
+
+ControlPoint::ColorSet Handle::_handle_colors = {
+ {0xffffffff, 0x000000ff}, // normal fill, stroke
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke
+ {0xff000000, 0x000000ff}, // clicked fill, stroke
+ //
+ {0xffffffff, 0x000000ff}, // normal fill, stroke
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke
+ {0xff000000, 0x000000ff} // clicked fill, stroke
+};
+
+std::ostream &operator<<(std::ostream &out, NodeType type)
+{
+ switch(type) {
+ case NODE_CUSP: out << 'c'; break;
+ case NODE_SMOOTH: out << 's'; break;
+ case NODE_AUTO: out << 'a'; break;
+ case NODE_SYMMETRIC: out << 'z'; break;
+ default: out << 'b'; break;
+ }
+ return out;
+}
+
+/** Computes an unit vector of the direction from first to second control point */
+static Geom::Point direction(Geom::Point const &first, Geom::Point const &second) {
+ return Geom::unit_vector(second - first);
+}
+
+Geom::Point Handle::_saved_other_pos(0, 0);
+
+double Handle::_saved_length = 0.0;
+
+bool Handle::_drag_out = false;
+
+Handle::Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent)
+ : ControlPoint(data.desktop, initial_pos, SP_ANCHOR_CENTER,
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE,
+ _handle_colors, data.handle_group)
+ , _handle_line(make_canvasitem<CanvasItemCurve>(data.handle_line_group))
+ , _parent(parent)
+ , _degenerate(true)
+{
+ setVisible(false);
+}
+
+Handle::~Handle() = default;
+
+void Handle::setVisible(bool v)
+{
+ ControlPoint::setVisible(v);
+ _handle_line->set_visible(v);
+}
+
+void Handle::_update_bspline_handles() {
+ // move the handle and its opposite the same proportion
+ if (_pm()._isBSpline()){
+ setPosition(_pm()._bsplineHandleReposition(this, false));
+ double bspline_weight = _pm()._bsplineHandlePosition(this, false);
+ other()->setPosition(_pm()._bsplineHandleReposition(other(), bspline_weight));
+ _pm().update();
+ }
+}
+
+void Handle::move(Geom::Point const &new_pos)
+{
+ Handle *other = this->other();
+ Node *node_towards = _parent->nodeToward(this); // node in direction of this handle
+ Node *node_away = _parent->nodeAwayFrom(this); // node in the opposite direction
+ Handle *towards = node_towards ? node_towards->handleAwayFrom(_parent) : nullptr;
+ Handle *towards_second = node_towards ? node_towards->handleToward(_parent) : nullptr;
+ if (Geom::are_near(new_pos, _parent->position())) {
+ // The handle becomes degenerate.
+ // Adjust node type as necessary.
+ if (other->isDegenerate()) {
+ // If both handles become degenerate, convert to parent cusp node
+ _parent->setType(NODE_CUSP, false);
+ } else {
+ // Only 1 handle becomes degenerate
+ switch (_parent->type()) {
+ case NODE_AUTO:
+ case NODE_SYMMETRIC:
+ _parent->setType(NODE_SMOOTH, false);
+ break;
+ default:
+ // do nothing for other node types
+ break;
+ }
+ }
+ // If the segment between the handle and the node in its direction becomes linear,
+ // and there are smooth nodes at its ends, make their handles collinear with the segment.
+ if (towards && towards_second->isDegenerate()) {
+ if (node_towards->type() == NODE_SMOOTH) {
+ towards->setDirection(*_parent, *node_towards);
+ }
+ if (_parent->type() == NODE_SMOOTH) {
+ other->setDirection(*node_towards, *_parent);
+ }
+ }
+ setPosition(new_pos);
+
+ // move the handle and its opposite the same proportion
+ _update_bspline_handles();
+ return;
+ }
+
+ if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) {
+ // restrict movement to the line joining the nodes
+ Geom::Point direction = _parent->position() - node_away->position();
+ Geom::Point delta = new_pos - _parent->position();
+ // project the relative position on the direction line
+ Geom::Coord direction_length = Geom::L2sq(direction);
+ Geom::Point new_delta;
+ if (direction_length == 0) {
+ // joining line has zero length - any direction is okay, prevent division by zero
+ new_delta = delta;
+ } else {
+ new_delta = (Geom::dot(delta, direction) / direction_length) * direction;
+ }
+ setRelativePos(new_delta);
+
+ // move the handle and its opposite the same proportion
+ _update_bspline_handles();
+
+ return;
+ }
+
+ switch (_parent->type()) {
+ case NODE_AUTO:
+ _parent->setType(NODE_SMOOTH, false);
+ // fall through - auto nodes degrade into smooth nodes
+ case NODE_SMOOTH: {
+ // for smooth nodes, we need to rotate the opposite handle
+ // so that it's collinear with the dragged one, while conserving length.
+ other->setDirection(new_pos, *_parent);
+ } break;
+ case NODE_SYMMETRIC:
+ // for symmetric nodes, place the other handle on the opposite side
+ other->setRelativePos(-(new_pos - _parent->position()));
+ break;
+ default: break;
+ }
+ setPosition(new_pos);
+
+ // move the handle and its opposite the same proportion
+ _update_bspline_handles();
+ Inkscape::UI::Tools::sp_update_helperpath(_desktop);
+}
+
+void Handle::setPosition(Geom::Point const &p)
+{
+ ControlPoint::setPosition(p);
+ _handle_line->set_coords(_parent->position(), position());
+
+ // update degeneration info and visibility
+ if (Geom::are_near(position(), _parent->position()))
+ _degenerate = true;
+ else _degenerate = false;
+
+ if (_parent->_handles_shown && _parent->visible() && !_degenerate) {
+ setVisible(true);
+ } else {
+ setVisible(false);
+ }
+}
+
+void Handle::setLength(double len)
+{
+ if (isDegenerate()) return;
+ Geom::Point dir = Geom::unit_vector(relativePos());
+ setRelativePos(dir * len);
+}
+
+void Handle::retract()
+{
+ move(_parent->position());
+}
+
+void Handle::setDirection(Geom::Point const &from, Geom::Point const &to)
+{
+ setDirection(to - from);
+}
+
+void Handle::setDirection(Geom::Point const &dir)
+{
+ Geom::Point unitdir = Geom::unit_vector(dir);
+ setRelativePos(unitdir * length());
+}
+
+/**
+ * See also: Node::node_type_to_localized_string(NodeType type)
+ */
+char const *Handle::handle_type_to_localized_string(NodeType type)
+{
+ switch(type) {
+ case NODE_CUSP:
+ return _("Corner node handle");
+ case NODE_SMOOTH:
+ return _("Smooth node handle");
+ case NODE_SYMMETRIC:
+ return _("Symmetric node handle");
+ case NODE_AUTO:
+ return _("Auto-smooth node handle");
+ default:
+ return "";
+ }
+}
+
+bool Handle::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ switch (event->type)
+ {
+ case GDK_KEY_PRESS:
+
+ switch (shortcut_key(event->key))
+ {
+ case GDK_KEY_s:
+ case GDK_KEY_S:
+
+ /* if Shift+S is pressed while hovering over a cusp node handle,
+ hold the handle in place; otherwise, process normally.
+ this handle is guaranteed not to be degenerate. */
+
+ if (held_only_shift(event->key) && _parent->_type == NODE_CUSP) {
+
+ // make opposite handle collinear,
+ // but preserve length, unless degenerate
+ if (other()->isDegenerate())
+ other()->setRelativePos(-relativePos());
+ else
+ other()->setDirection(-relativePos());
+ _parent->setType(NODE_SMOOTH, false);
+
+ // update display
+ _parent->_pm().update();
+
+ // update undo history
+ _parent->_pm()._commit(_("Change node type"));
+
+ return true;
+ }
+ break;
+
+ case GDK_KEY_y:
+ case GDK_KEY_Y:
+
+ /* if Shift+Y is pressed while hovering over a cusp, smooth, or auto node handle,
+ hold the handle in place; otherwise, process normally.
+ this handle is guaranteed not to be degenerate. */
+
+ if (held_only_shift(event->key) && (_parent->_type == NODE_CUSP ||
+ _parent->_type == NODE_SMOOTH ||
+ _parent->_type == NODE_AUTO)) {
+
+ // make opposite handle collinear, and of equal length
+ other()->setRelativePos(-relativePos());
+ _parent->setType(NODE_SYMMETRIC, false);
+
+ // update display
+ _parent->_pm().update();
+
+ // update undo history
+ _parent->_pm()._commit(_("Change node type"));
+
+ return true;
+ }
+ break;
+ }
+ break;
+
+ case GDK_2BUTTON_PRESS:
+
+ // double-click event to set the handles of a node
+ // to the position specified by DEFAULT_START_POWER
+ handle_2button_press();
+ break;
+ }
+
+ return ControlPoint::_eventHandler(event_context, event);
+}
+
+// this function moves the handle and its opposite to the position specified by DEFAULT_START_POWER
+void Handle::handle_2button_press(){
+ if(_pm()._isBSpline()){
+ setPosition(_pm()._bsplineHandleReposition(this, DEFAULT_START_POWER));
+ this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), DEFAULT_START_POWER));
+ _pm().update();
+ }
+}
+
+bool Handle::grabbed(GdkEventMotion *)
+{
+ _saved_other_pos = other()->position();
+ _saved_length = _drag_out ? 0 : length();
+ _pm()._handleGrabbed();
+ return false;
+}
+
+void Handle::dragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ Geom::Point parent_pos = _parent->position();
+ Geom::Point origin = _last_drag_origin();
+ SnapManager &sm = _desktop->namedview->snap_manager;
+ bool snap = held_shift(*event) ? false : sm.someSnapperMightSnap();
+ std::optional<Inkscape::Snapper::SnapConstraint> ctrl_constraint;
+
+ // with Alt, preserve length of the handle
+ if (held_alt(*event)) {
+ new_pos = parent_pos + Geom::unit_vector(new_pos - parent_pos) * _saved_length;
+ snap = false;
+ }
+ // with Ctrl, constrain to M_PI/rotationsnapsperpi increments from vertical
+ // and the original position.
+ if (held_control(*event)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = 2 * prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+
+ // note: if snapping to the original position is only desired in the original
+ // direction of the handle, use Geom::Ray instead of Geom::Line
+ Geom::Line original_line(parent_pos, origin);
+ Geom::Line perp_line(parent_pos, parent_pos + Geom::rot90(origin - parent_pos));
+ Geom::Point snap_pos = parent_pos + Geom::constrain_angle(
+ Geom::Point(0,0), new_pos - parent_pos, snaps, Geom::Point(1,0));
+ Geom::Point orig_pos = original_line.pointAt(original_line.nearestTime(new_pos));
+ Geom::Point perp_pos = perp_line.pointAt(perp_line.nearestTime(new_pos));
+
+ Geom::Point result = snap_pos;
+ ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - snap_pos);
+ if (Geom::distance(orig_pos, new_pos) < Geom::distance(result, new_pos)) {
+ result = orig_pos;
+ ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - orig_pos);
+ }
+ if (Geom::distance(perp_pos, new_pos) < Geom::distance(result, new_pos)) {
+ result = perp_pos;
+ ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - perp_pos);
+ }
+ new_pos = result;
+ // move the handle and its opposite in X fixed positions depending on parameter "steps with control"
+ // by default in live BSpline
+ if(_pm()._isBSpline()){
+ setPosition(new_pos);
+ int steps = _pm()._bsplineGetSteps();
+ new_pos=_pm()._bsplineHandleReposition(this,ceilf(_pm()._bsplineHandlePosition(this, false)*steps)/steps);
+ }
+ }
+
+ std::vector<Inkscape::SnapCandidatePoint> unselected;
+ // If the snapping is active and we're not working with a B-spline
+ if (snap && !_pm()._isBSpline()) {
+ // We will only snap this handle to stationary path segments; some path segments may move as we move the
+ // handle; those path segments are connected to the parent node of this handle.
+ ControlPointSelection::Set &nodes = _parent->_selection.allPoints();
+ for (auto node : nodes) {
+ Node *n = static_cast<Node*>(node);
+ if (_parent != n) { // We're adding all nodes in the path, except the parent node of this handle
+ unselected.push_back(n->snapCandidatePoint());
+ }
+ }
+ sm.setupIgnoreSelection(_desktop, true, &unselected);
+
+ Node *node_away = _parent->nodeAwayFrom(this);
+ if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) {
+ Inkscape::Snapper::SnapConstraint cl(_parent->position(),
+ _parent->position() - node_away->position());
+ Inkscape::SnappedPoint p;
+ p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), cl);
+ new_pos = p.getPoint();
+ } else if (ctrl_constraint) {
+ // NOTE: this is subtly wrong.
+ // We should get all possible constraints and snap along them using
+ // multipleConstrainedSnaps, instead of first snapping to angle and then to objects
+ Inkscape::SnappedPoint p;
+ p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), *ctrl_constraint);
+ new_pos = p.getPoint();
+ } else {
+ sm.freeSnapReturnByRef(new_pos, SNAPSOURCE_NODE_HANDLE);
+ }
+ sm.unSetup();
+ }
+
+ // with Shift, if the node is cusp, rotate the other handle as well
+ if (_parent->type() == NODE_CUSP && !_drag_out) {
+ if (held_shift(*event)) {
+ Geom::Point other_relpos = _saved_other_pos - parent_pos;
+ other_relpos *= Geom::Rotate(Geom::angle_between(origin - parent_pos, new_pos - parent_pos));
+ other()->setRelativePos(other_relpos);
+ } else {
+ // restore the position
+ other()->setPosition(_saved_other_pos);
+ }
+ }
+ // if it is BSpline, but SHIFT or CONTROL are not pressed, fix it in the original position
+ if(_pm()._isBSpline() && !held_shift(*event) && !held_control(*event)){
+ new_pos=_last_drag_origin();
+ }
+ _pm().update();
+}
+
+void Handle::ungrabbed(GdkEventButton *event)
+{
+ // hide the handle if it's less than dragtolerance away from the node
+ // however, never do this for cancelled drag / broken grab
+ // TODO is this actually a good idea?
+ if (event) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ Geom::Point dist = _desktop->d2w(_parent->position()) - _desktop->d2w(position());
+ if (dist.length() <= drag_tolerance) {
+ move(_parent->position());
+ }
+ }
+
+ // HACK: If the handle was dragged out, call parent's ungrabbed handler,
+ // so that transform handles reappear
+ if (_drag_out) {
+ _parent->ungrabbed(event);
+ }
+ _drag_out = false;
+ Inkscape::UI::Tools::sp_update_helperpath(_desktop);
+ _pm()._handleUngrabbed();
+}
+
+bool Handle::clicked(GdkEventButton *event)
+{
+ _pm()._handleClicked(this, event);
+ return true;
+}
+
+Handle const *Handle::other() const
+{
+ return const_cast<Handle *>(this)->other();
+}
+
+Handle *Handle::other()
+{
+ if (this == &_parent->_front) {
+ return &_parent->_back;
+ } else {
+ return &_parent->_front;
+ }
+}
+
+static double snap_increment_degrees() {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ return 180.0 / snaps;
+}
+
+Glib::ustring Handle::_getTip(unsigned state) const
+{
+ /* a trick to mark as BSpline if the node has no strength;
+ we are going to use it later to show the appropriate messages.
+ we cannot do it in any different way because the function is constant. */
+ Handle *h = const_cast<Handle *>(this);
+ bool isBSpline = _pm()._isBSpline();
+ bool can_shift_rotate = _parent->type() == NODE_CUSP && !other()->isDegenerate();
+ Glib::ustring s = C_("Status line hint",
+ "node control handle"); // not expected
+
+ if (state_held_alt(state) && !isBSpline) {
+ if (state_held_control(state)) {
+ if (state_held_shift(state) && can_shift_rotate) {
+ s = format_tip(C_("Status line hint",
+ "<b>Shift+Ctrl+Alt</b>: "
+ "preserve length and snap rotation angle to %g° increments, "
+ "and rotate both handles"),
+ snap_increment_degrees());
+ }
+ else {
+ s = format_tip(C_("Status line hint",
+ "<b>Ctrl+Alt</b>: "
+ "preserve length and snap rotation angle to %g° increments"),
+ snap_increment_degrees());
+ }
+ }
+ else {
+ if (state_held_shift(state) && can_shift_rotate) {
+ s = C_("Path handle tip",
+ "<b>Shift+Alt</b>: preserve handle length and rotate both handles");
+ }
+ else {
+ s = C_("Path handle tip",
+ "<b>Alt</b>: preserve handle length while dragging");
+ }
+ }
+ }
+ else {
+ if (state_held_control(state)) {
+ if (state_held_shift(state) && can_shift_rotate && !isBSpline) {
+ s = format_tip(C_("Path handle tip",
+ "<b>Shift+Ctrl</b>: "
+ "snap rotation angle to %g° increments, and rotate both handles"),
+ snap_increment_degrees());
+ }
+ else if (isBSpline) {
+ s = C_("Path handle tip",
+ "<b>Ctrl</b>: "
+ "Snap handle to steps defined in BSpline Live Path Effect");
+ }
+ else {
+ s = format_tip(C_("Path handle tip",
+ "<b>Ctrl</b>: "
+ "snap rotation angle to %g° increments, click to retract"),
+ snap_increment_degrees());
+ }
+ }
+ else if (state_held_shift(state) && can_shift_rotate && !isBSpline) {
+ s = C_("Path handle tip",
+ "<b>Shift</b>: rotate both handles by the same angle");
+ }
+ else if (state_held_shift(state) && isBSpline) {
+ s = C_("Path handle tip",
+ "<b>Shift</b>: move handle");
+ }
+ else {
+ char const *handletype = handle_type_to_localized_string(_parent->_type);
+ char const *more;
+
+ if (can_shift_rotate && !isBSpline) {
+ more = C_("Status line hint",
+ "Shift, Ctrl, Alt");
+ }
+ else if (isBSpline) {
+ more = C_("Status line hint",
+ "Shift, Ctrl");
+ }
+ else {
+ more = C_("Status line hint",
+ "Ctrl, Alt");
+ }
+ if (isBSpline) {
+ double power = _pm()._bsplineHandlePosition(h);
+ s = format_tip(C_("Status line hint",
+ "<b>BSpline node handle</b> (%.3g power): "
+ "Shift-drag to move, "
+ "double-click to reset. "
+ "(more: %s)"),
+ power, more);
+ } else if (_parent->type() == NODE_CUSP) {
+ s = format_tip(C_("Status line hint",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "hover to lock" ", "
+ "Shift+S to make smooth" ", "
+ "Shift+Y to make symmetric" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (_parent->type() == NODE_SMOOTH) {
+ s = format_tip(C_("Status line hint",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "hover to lock" ", "
+ "Shift+Y to make symmetric" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (_parent->type() == NODE_AUTO) {
+ s = format_tip(C_("Status line hint",
+ "<b>%s</b>: "
+ "drag to make smooth, "
+ "hover to lock" ", "
+ "Shift+Y to make symmetric" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (_parent->type() == NODE_SYMMETRIC) {
+ s = format_tip(C_("Status line hint",
+ "<b>%s</b>: "
+ "drag to shape the path" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else {
+ s = C_("Status line hint",
+ "<b>unknown node handle</b>"); // not expected
+ }
+ }
+ }
+
+ return (s);
+}
+
+Glib::ustring Handle::_getDragTip(GdkEventMotion */*event*/) const
+{
+ Geom::Point dist = position() - _last_drag_origin();
+ // report angle in mathematical convention
+ double angle = Geom::angle_between(Geom::Point(-1,0), position() - _parent->position());
+ angle += M_PI; // angle is (-M_PI...M_PI] - offset by +pi and scale to 0...360
+ angle *= 360.0 / (2 * M_PI);
+
+ Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dist[Geom::X], "px");
+ Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dist[Geom::Y], "px");
+ Inkscape::Util::Quantity len_q = Inkscape::Util::Quantity(length(), "px");
+ Glib::ustring x = x_q.string(_desktop->namedview->display_units);
+ Glib::ustring y = y_q.string(_desktop->namedview->display_units);
+ Glib::ustring len = len_q.string(_desktop->namedview->display_units);
+ Glib::ustring ret = format_tip(C_("Status line hint",
+ "Move handle by %s, %s; angle %.2f°, length %s"), x.c_str(), y.c_str(), angle, len.c_str());
+ return ret;
+}
+
+Node::Node(NodeSharedData const &data, Geom::Point const &initial_pos) :
+ SelectableControlPoint(data.desktop, initial_pos, SP_ANCHOR_CENTER,
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_CUSP,
+ *data.selection,
+ node_colors, data.node_group),
+ _front(data, initial_pos, this),
+ _back(data, initial_pos, this),
+ _type(NODE_CUSP),
+ _handles_shown(false)
+{
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:Node");
+ // NOTE we do not set type here, because the handles are still degenerate
+}
+
+Node const *Node::_next() const
+{
+ return const_cast<Node*>(this)->_next();
+}
+
+// NOTE: not using iterators won't make this much quicker because iterators can be 100% inlined.
+Node *Node::_next()
+{
+ NodeList::iterator n = NodeList::get_iterator(this).next();
+ if (n) {
+ return n.ptr();
+ } else {
+ return nullptr;
+ }
+}
+
+Node const *Node::_prev() const
+{
+ return const_cast<Node *>(this)->_prev();
+}
+
+Node *Node::_prev()
+{
+ NodeList::iterator p = NodeList::get_iterator(this).prev();
+ if (p) {
+ return p.ptr();
+ } else {
+ return nullptr;
+ }
+}
+
+void Node::move(Geom::Point const &new_pos)
+{
+ // move handles when the node moves.
+ Geom::Point delta = new_pos - position();
+
+ // save the previous nodes strength to apply it again once the node is moved
+ double nodeWeight = NO_POWER;
+ double nextNodeWeight = NO_POWER;
+ double prevNodeWeight = NO_POWER;
+ Node *n = this;
+ Node * nextNode = n->nodeToward(n->front());
+ Node * prevNode = n->nodeToward(n->back());
+ nodeWeight = fmax(_pm()._bsplineHandlePosition(n->front(), false),_pm()._bsplineHandlePosition(n->back(), false));
+ if(prevNode){
+ prevNodeWeight = _pm()._bsplineHandlePosition(prevNode->front());
+ }
+ if(nextNode){
+ nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back());
+ }
+
+ // Save original position for post-processing
+ _unfixed_pos = std::optional<Geom::Point>(position());
+
+ setPosition(new_pos);
+ _front.setPosition(_front.position() + delta);
+ _back.setPosition(_back.position() + delta);
+
+ // move the affected handles. First the node ones, later the adjoining ones.
+ if(_pm()._isBSpline()){
+ _front.setPosition(_pm()._bsplineHandleReposition(this->front(),nodeWeight));
+ _back.setPosition(_pm()._bsplineHandleReposition(this->back(),nodeWeight));
+ if(prevNode){
+ prevNode->front()->setPosition(_pm()._bsplineHandleReposition(prevNode->front(), prevNodeWeight));
+ }
+ if(nextNode){
+ nextNode->back()->setPosition(_pm()._bsplineHandleReposition(nextNode->back(), nextNodeWeight));
+ }
+ }
+ Inkscape::UI::Tools::sp_update_helperpath(_desktop);
+}
+
+void Node::transform(Geom::Affine const &m)
+{
+ // save the previous nodes strength to apply it again once the node is moved
+ double nodeWeight = NO_POWER;
+ double nextNodeWeight = NO_POWER;
+ double prevNodeWeight = NO_POWER;
+ Node *n = this;
+ Node * nextNode = n->nodeToward(n->front());
+ Node * prevNode = n->nodeToward(n->back());
+ nodeWeight = _pm()._bsplineHandlePosition(n->front());
+ if(prevNode){
+ prevNodeWeight = _pm()._bsplineHandlePosition(prevNode->front());
+ }
+ if(nextNode){
+ nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back());
+ }
+
+ // Save original position for post-processing
+ _unfixed_pos = std::optional<Geom::Point>(position());
+
+ setPosition(position() * m);
+ _front.setPosition(_front.position() * m);
+ _back.setPosition(_back.position() * m);
+
+ // move the involved handles. First the node ones, later the adjoining ones.
+ if(_pm()._isBSpline()){
+ _front.setPosition(_pm()._bsplineHandleReposition(this->front(), nodeWeight));
+ _back.setPosition(_pm()._bsplineHandleReposition(this->back(), nodeWeight));
+ if(prevNode){
+ prevNode->front()->setPosition(_pm()._bsplineHandleReposition(prevNode->front(), prevNodeWeight));
+ }
+ if(nextNode){
+ nextNode->back()->setPosition(_pm()._bsplineHandleReposition(nextNode->back(), nextNodeWeight));
+ }
+ }
+}
+
+Geom::Rect Node::bounds() const
+{
+ Geom::Rect b(position(), position());
+ b.expandTo(_front.position());
+ b.expandTo(_back.position());
+ return b;
+}
+
+/**
+ * Affine transforms keep handle invariants for smooth and symmetric nodes,
+ * but smooth nodes at ends of linear segments and auto nodes need special treatment
+ *
+ * Call this function once you have finished called ::move or ::transform on ALL nodes
+ * that are being transformed in that one operation to avoid problematic bugs.
+ */
+void Node::fixNeighbors()
+{
+ if (!_unfixed_pos)
+ return;
+
+ Geom::Point const new_pos = position();
+
+ // This method restores handle invariants for neighboring nodes,
+ // and invariants that are based on positions of those nodes for this one.
+
+ // Fix auto handles
+ if (_type == NODE_AUTO) _updateAutoHandles();
+ if (*_unfixed_pos != new_pos) {
+ if (_next() && _next()->_type == NODE_AUTO) _next()->_updateAutoHandles();
+ if (_prev() && _prev()->_type == NODE_AUTO) _prev()->_updateAutoHandles();
+ }
+
+ /* Fix smooth handles at the ends of linear segments.
+ Rotate the appropriate handle to be collinear with the segment.
+ If there is a smooth node at the other end of the segment, rotate it too. */
+ Handle *handle, *other_handle;
+ Node *other;
+ if (_is_line_segment(this, _next())) {
+ handle = &_back;
+ other = _next();
+ other_handle = &_next()->_front;
+ } else if (_is_line_segment(_prev(), this)) {
+ handle = &_front;
+ other = _prev();
+ other_handle = &_prev()->_back;
+ } else return;
+
+ if (_type == NODE_SMOOTH && !handle->isDegenerate()) {
+ handle->setDirection(other->position(), new_pos);
+ }
+ // also update the handle on the other end of the segment
+ if (other->_type == NODE_SMOOTH && !other_handle->isDegenerate()) {
+ other_handle->setDirection(new_pos, other->position());
+ }
+
+ _unfixed_pos.reset();
+}
+
+void Node::_updateAutoHandles()
+{
+ // Recompute the position of automatic handles. For endnodes, retract both handles.
+ // (It's only possible to create an end auto node through the XML editor.)
+ if (isEndNode()) {
+ _front.retract();
+ _back.retract();
+ return;
+ }
+
+ // auto nodes automatically adjust their handles to give
+ // an appearance of smoothness, no matter what their surroundings are.
+ Geom::Point vec_next = _next()->position() - position();
+ Geom::Point vec_prev = _prev()->position() - position();
+ double len_next = vec_next.length(), len_prev = vec_prev.length();
+ if (len_next > 0 && len_prev > 0) {
+ // "dir" is an unit vector perpendicular to the bisector of the angle created
+ // by the previous node, this auto node and the next node.
+ Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev);
+ // Handle lengths are equal to 1/3 of the distance from the adjacent node.
+ _back.setRelativePos(-dir * (len_prev / 3));
+ _front.setRelativePos(dir * (len_next / 3));
+ } else {
+ // If any of the adjacent nodes coincides, retract both handles.
+ _front.retract();
+ _back.retract();
+ }
+}
+
+void Node::showHandles(bool v)
+{
+ _handles_shown = v;
+ if (!_front.isDegenerate()) {
+ _front.setVisible(v);
+ }
+ if (!_back.isDegenerate()) {
+ _back.setVisible(v);
+ }
+
+}
+
+void Node::updateHandles()
+{
+ _handleControlStyling();
+
+ _front._handleControlStyling();
+ _back._handleControlStyling();
+}
+
+
+void Node::setType(NodeType type, bool update_handles)
+{
+ if (type == NODE_PICK_BEST) {
+ pickBestType();
+ updateState(); // The size of the control might have changed
+ return;
+ }
+
+ // if update_handles is true, adjust handle positions to match the node type
+ // handle degenerate handles appropriately
+ if (update_handles) {
+ switch (type) {
+ case NODE_CUSP:
+ // nothing to do
+ break;
+ case NODE_AUTO:
+ // auto handles make no sense for endnodes
+ if (isEndNode()) return;
+ _updateAutoHandles();
+ break;
+ case NODE_SMOOTH: {
+ // ignore attempts to make smooth endnodes.
+ if (isEndNode()) return;
+ // rotate handles to be collinear
+ // for degenerate nodes set positions like auto handles
+ bool prev_line = _is_line_segment(_prev(), this);
+ bool next_line = _is_line_segment(this, _next());
+ if (_type == NODE_SMOOTH) {
+ // For a node that is already smooth and has a degenerate handle,
+ // drag out the second handle without changing the direction of the first one.
+ if (_front.isDegenerate()) {
+ double dist = Geom::distance(_next()->position(), position());
+ _front.setRelativePos(Geom::unit_vector(-_back.relativePos()) * dist / 3);
+ }
+ if (_back.isDegenerate()) {
+ double dist = Geom::distance(_prev()->position(), position());
+ _back.setRelativePos(Geom::unit_vector(-_front.relativePos()) * dist / 3);
+ }
+ } else if (isDegenerate()) {
+ _updateAutoHandles();
+ } else if (_front.isDegenerate()) {
+ // if the front handle is degenerate and next path segment is a line, make back collinear;
+ // otherwise, pull out the other handle to 1/3 of distance to prev.
+ if (next_line) {
+ _back.setDirection(*_next(), *this);
+ } else if (_prev()) {
+ Geom::Point dir = direction(_back, *this);
+ _front.setRelativePos(Geom::distance(_prev()->position(), position()) / 3 * dir);
+ }
+ } else if (_back.isDegenerate()) {
+ if (prev_line) {
+ _front.setDirection(*_prev(), *this);
+ } else if (_next()) {
+ Geom::Point dir = direction(_front, *this);
+ _back.setRelativePos(Geom::distance(_next()->position(), position()) / 3 * dir);
+ }
+ } else {
+ /* both handles are extended. make collinear while keeping length.
+ first make back collinear with the vector front ---> back,
+ then make front collinear with back ---> node.
+ (not back ---> front, because back's position was changed in the first call) */
+ _back.setDirection(_front, _back);
+ _front.setDirection(_back, *this);
+ }
+ } break;
+ case NODE_SYMMETRIC:
+ if (isEndNode()) return; // symmetric handles make no sense for endnodes
+ if (isDegenerate()) {
+ // similar to auto handles but set the same length for both
+ Geom::Point vec_next = _next()->position() - position();
+ Geom::Point vec_prev = _prev()->position() - position();
+ double len_next = vec_next.length(), len_prev = vec_prev.length();
+ double len = (len_next + len_prev) / 6; // take 1/3 of average
+ if (len == 0) return;
+
+ Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev);
+ _back.setRelativePos(-dir * len);
+ _front.setRelativePos(dir * len);
+ } else {
+ // Both handles are extended. Compute average length, use direction from
+ // back handle to front handle. This also works correctly for degenerates
+ double len = (_front.length() + _back.length()) / 2;
+ Geom::Point dir = direction(_back, _front);
+ _front.setRelativePos(dir * len);
+ _back.setRelativePos(-dir * len);
+ }
+ break;
+ default: break;
+ }
+ // in node type changes, for BSpline traces, we can either maintain them
+ // with NO_POWER power in border mode, or give them the default power in curve mode.
+ if(_pm()._isBSpline()){
+ double weight = NO_POWER;
+ if(_pm()._bsplineHandlePosition(this->front()) != NO_POWER ){
+ weight = DEFAULT_START_POWER;
+ }
+ _front.setPosition(_pm()._bsplineHandleReposition(this->front(), weight));
+ _back.setPosition(_pm()._bsplineHandleReposition(this->back(), weight));
+ }
+ }
+ _type = type;
+ _setControlType(nodeTypeToCtrlType(_type));
+ updateState();
+}
+
+void Node::pickBestType()
+{
+ _type = NODE_CUSP;
+ bool front_degen = _front.isDegenerate();
+ bool back_degen = _back.isDegenerate();
+ bool both_degen = front_degen && back_degen;
+ bool neither_degen = !front_degen && !back_degen;
+ do {
+ // if both handles are degenerate, do nothing
+ if (both_degen) break;
+ // if neither are degenerate, check their respective positions
+ if (neither_degen) {
+ // for now do not automatically make nodes symmetric, it can be annoying
+ /*if (Geom::are_near(front_delta, -back_delta)) {
+ _type = NODE_SYMMETRIC;
+ break;
+ }*/
+ if (are_collinear_within_serializing_error(_front.position(), position(), _back.position())) {
+ _type = NODE_SMOOTH;
+ break;
+ }
+ }
+ // check whether the handle aligns with the previous line segment.
+ // we know that if front is degenerate, back isn't, because
+ // both_degen was false
+ if (front_degen && _next() && _next()->_back.isDegenerate()) {
+ if (are_collinear_within_serializing_error(_next()->position(), position(), _back.position())) {
+ _type = NODE_SMOOTH;
+ break;
+ }
+ } else if (back_degen && _prev() && _prev()->_front.isDegenerate()) {
+ if (are_collinear_within_serializing_error(_prev()->position(), position(), _front.position())) {
+ _type = NODE_SMOOTH;
+ break;
+ }
+ }
+ } while (false);
+ _setControlType(nodeTypeToCtrlType(_type));
+ updateState();
+}
+
+bool Node::isEndNode() const
+{
+ return !_prev() || !_next();
+}
+
+void Node::sink()
+{
+ _canvas_item_ctrl->lower_to_bottom();
+}
+
+NodeType Node::parse_nodetype(char x)
+{
+ switch (x) {
+ case 'a': return NODE_AUTO;
+ case 'c': return NODE_CUSP;
+ case 's': return NODE_SMOOTH;
+ case 'z': return NODE_SYMMETRIC;
+ default: return NODE_PICK_BEST;
+ }
+}
+
+bool Node::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ int dir = 0;
+ int state = 0;
+
+ switch (event->type)
+ {
+ case GDK_SCROLL:
+ state = event->scroll.state;
+ if (event->scroll.direction == GDK_SCROLL_UP) {
+ dir = 1;
+ } else if (event->scroll.direction == GDK_SCROLL_DOWN) {
+ dir = -1;
+ } else if (event->scroll.direction == GDK_SCROLL_SMOOTH) {
+ dir = event->scroll.delta_y > 0 ? -1 : 1;
+ } else {
+ break;
+ }
+ break;
+ case GDK_KEY_PRESS:
+ state = event->key.state;
+ switch (shortcut_key(event->key))
+ {
+ case GDK_KEY_Page_Up:
+ dir = 1;
+ break;
+ case GDK_KEY_Page_Down:
+ dir = -1;
+ break;
+ default:
+ break;
+ }
+ default:
+ break;
+ }
+
+ using namespace Inkscape::Modifiers;
+ auto linear_grow = Modifier::get(Modifiers::Type::NODE_GROW_LINEAR)->active(state);
+ auto spatial_grow = Modifier::get(Modifiers::Type::NODE_GROW_SPATIAL)->active(state);
+
+ if (dir && (linear_grow || spatial_grow)) {
+ if (linear_grow)
+ _linearGrow(dir);
+ else if (spatial_grow)
+ _selection.spatialGrow(this, dir);
+ return true;
+ }
+
+ return ControlPoint::_eventHandler(event_context, event);
+}
+
+void Node::_linearGrow(int dir)
+{
+ // Interestingly, we do not need any help from PathManipulator when doing linear grow.
+ // First handle the trivial case of growing over an unselected node.
+ if (!selected() && dir > 0) {
+ _selection.insert(this);
+ return;
+ }
+
+ NodeList::iterator this_iter = NodeList::get_iterator(this);
+ NodeList::iterator fwd = this_iter, rev = this_iter;
+ double distance_back = 0, distance_front = 0;
+
+ // Linear grow is simple. We find the first unselected nodes in each direction
+ // and compare the linear distances to them.
+ if (dir > 0) {
+ if (!selected()) {
+ _selection.insert(this);
+ return;
+ }
+
+ // find first unselected nodes on both sides
+ while (fwd && fwd->selected()) {
+ NodeList::iterator n = fwd.next();
+ distance_front += Geom::bezier_length(*fwd, fwd->_front, n->_back, *n);
+ fwd = n;
+ if (fwd == this_iter)
+ // there is no unselected node in this cyclic subpath
+ return;
+ }
+ // do the same for the second direction. Do not check for equality with
+ // this node, because there is at least one unselected node in the subpath,
+ // so we are guaranteed to stop.
+ while (rev && rev->selected()) {
+ NodeList::iterator p = rev.prev();
+ distance_back += Geom::bezier_length(*rev, rev->_back, p->_front, *p);
+ rev = p;
+ }
+
+ NodeList::iterator t; // node to select
+ if (fwd && rev) {
+ if (distance_front <= distance_back) t = fwd;
+ else t = rev;
+ } else {
+ if (fwd) t = fwd;
+ if (rev) t = rev;
+ }
+ if (t) _selection.insert(t.ptr());
+
+ // Linear shrink is more complicated. We need to find the farthest selected node.
+ // This means we have to check the entire subpath. We go in the direction in which
+ // the distance we traveled is lower. We do this until we run out of nodes (ends of path)
+ // or the two iterators meet. On the way, we store the last selected node and its distance
+ // in each direction (if any). At the end, we choose the one that is farther and deselect it.
+ } else {
+ // both iterators that store last selected nodes are initially empty
+ NodeList::iterator last_fwd, last_rev;
+ double last_distance_back = 0, last_distance_front = 0;
+
+ while (rev || fwd) {
+ if (fwd && (!rev || distance_front <= distance_back)) {
+ if (fwd->selected()) {
+ last_fwd = fwd;
+ last_distance_front = distance_front;
+ }
+ NodeList::iterator n = fwd.next();
+ if (n) distance_front += Geom::bezier_length(*fwd, fwd->_front, n->_back, *n);
+ fwd = n;
+ } else if (rev && (!fwd || distance_front > distance_back)) {
+ if (rev->selected()) {
+ last_rev = rev;
+ last_distance_back = distance_back;
+ }
+ NodeList::iterator p = rev.prev();
+ if (p) distance_back += Geom::bezier_length(*rev, rev->_back, p->_front, *p);
+ rev = p;
+ }
+ // Check whether we walked the entire cyclic subpath.
+ // This is initially true because both iterators start from this node,
+ // so this check cannot go in the while condition.
+ // When this happens, we need to check the last node, pointed to by the iterators.
+ if (fwd && fwd == rev) {
+ if (!fwd->selected()) break;
+ NodeList::iterator fwdp = fwd.prev(), revn = rev.next();
+ double df = distance_front + Geom::bezier_length(*fwdp, fwdp->_front, fwd->_back, *fwd);
+ double db = distance_back + Geom::bezier_length(*revn, revn->_back, rev->_front, *rev);
+ if (df > db) {
+ last_fwd = fwd;
+ last_distance_front = df;
+ } else {
+ last_rev = rev;
+ last_distance_back = db;
+ }
+ break;
+ }
+ }
+
+ NodeList::iterator t;
+ if (last_fwd && last_rev) {
+ if (last_distance_front >= last_distance_back) t = last_fwd;
+ else t = last_rev;
+ } else {
+ if (last_fwd) t = last_fwd;
+ if (last_rev) t = last_rev;
+ }
+ if (t) _selection.erase(t.ptr());
+ }
+}
+
+void Node::_setState(State state)
+{
+ // change node size to match type and selection state
+ _canvas_item_ctrl->set_size_extra(selected() ? 2 : 0);
+ switch (state) {
+ // These were used to set "active" and "prelight" flags but the flags weren't being used.
+ case STATE_NORMAL:
+ case STATE_MOUSEOVER:
+ break;
+ case STATE_CLICKED:
+ // show the handles when selecting the nodes
+ if(_pm()._isBSpline()){
+ this->front()->setPosition(_pm()._bsplineHandleReposition(this->front()));
+ this->back()->setPosition(_pm()._bsplineHandleReposition(this->back()));
+ }
+ break;
+ }
+ SelectableControlPoint::_setState(state);
+}
+
+bool Node::grabbed(GdkEventMotion *event)
+{
+ if (SelectableControlPoint::grabbed(event)) {
+ return true;
+ }
+
+ // Dragging out handles with Shift + drag on a node.
+ if (!held_shift(*event)) {
+ return false;
+ }
+
+ Geom::Point evp = event_point(*event);
+ Geom::Point rel_evp = evp - _last_click_event_point();
+
+ // This should work even if dragtolerance is zero and evp coincides with node position.
+ double angle_next = HUGE_VAL;
+ double angle_prev = HUGE_VAL;
+ bool has_degenerate = false;
+ // determine which handle to drag out based on degeneration and the direction of drag
+ if (_front.isDegenerate() && _next()) {
+ Geom::Point next_relpos = _desktop->d2w(_next()->position())
+ - _desktop->d2w(position());
+ angle_next = fabs(Geom::angle_between(rel_evp, next_relpos));
+ has_degenerate = true;
+ }
+ if (_back.isDegenerate() && _prev()) {
+ Geom::Point prev_relpos = _desktop->d2w(_prev()->position())
+ - _desktop->d2w(position());
+ angle_prev = fabs(Geom::angle_between(rel_evp, prev_relpos));
+ has_degenerate = true;
+ }
+ if (!has_degenerate) {
+ return false;
+ }
+
+ Handle *h = angle_next < angle_prev ? &_front : &_back;
+
+ h->setPosition(_desktop->w2d(evp));
+ h->setVisible(true);
+ h->transferGrab(this, event);
+ Handle::_drag_out = true;
+ return true;
+}
+
+void Node::dragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ // For a note on how snapping is implemented in Inkscape, see snap.h.
+ SnapManager &sm = _desktop->namedview->snap_manager;
+ // even if we won't really snap, we might still call the one of the
+ // constrainedSnap() methods to enforce the constraints, so we need
+ // to setup the snapmanager anyway; this is also required for someSnapperMightSnap()
+ sm.setup(_desktop);
+
+ // do not snap when Shift is pressed
+ bool snap = !held_shift(*event) && sm.someSnapperMightSnap();
+
+ Inkscape::SnappedPoint sp;
+ std::vector<Inkscape::SnapCandidatePoint> unselected;
+ if (snap) {
+ /* setup
+ * TODO We are doing this every time a snap happens. It should once be done only once
+ * per drag - maybe in the grabbed handler?
+ * TODO Unselected nodes vector must be valid during the snap run, because it is not
+ * copied. Fix this in snap.h and snap.cpp, then the above.
+ * TODO Snapping to unselected segments of selected paths doesn't work yet. */
+
+ // Build the list of unselected nodes.
+ typedef ControlPointSelection::Set Set;
+ Set &nodes = _selection.allPoints();
+ for (auto node : nodes) {
+ if (!node->selected()) {
+ Node *n = static_cast<Node*>(node);
+ Inkscape::SnapCandidatePoint p(n->position(), n->_snapSourceType(), n->_snapTargetType());
+ unselected.push_back(p);
+ }
+ }
+ sm.unSetup();
+ sm.setupIgnoreSelection(_desktop, true, &unselected);
+ }
+
+ // Snap candidate point for free snapping; this will consider snapping tangentially
+ // and perpendicularly and therefore the origin or direction vector must be set
+ Inkscape::SnapCandidatePoint scp_free(new_pos, _snapSourceType());
+
+ std::optional<Geom::Point> front_direction, back_direction;
+ Geom::Point origin = _last_drag_origin();
+ Geom::Point dummy_cp;
+ if (_front.isDegenerate()) { // If there is no handle for the path segment towards the next node, then this segment may be straight
+ if (_is_line_segment(this, _next())) {
+ front_direction = _next()->position() - origin;
+ if (_next()->selected()) {
+ dummy_cp = _next()->position() - position();
+ scp_free.addVector(dummy_cp);
+ } else {
+ dummy_cp = _next()->position();
+ scp_free.addOrigin(dummy_cp);
+ }
+ }
+ } else { // .. this path segment is curved
+ front_direction = _front.relativePos();
+ scp_free.addVector(*front_direction);
+ }
+
+ if (_back.isDegenerate()) { // If there is no handle for the path segment towards the previous node, then this segment may be straight
+ if (_is_line_segment(_prev(), this)) {
+ back_direction = _prev()->position() - origin;
+ if (_prev()->selected()) {
+ dummy_cp = _prev()->position() - position();
+ scp_free.addVector(dummy_cp);
+ } else {
+ dummy_cp = _prev()->position();
+ scp_free.addOrigin(dummy_cp);
+ }
+ }
+ } else { // .. this path segment is curved
+ back_direction = _back.relativePos();
+ scp_free.addVector(*back_direction);
+ }
+
+ if (held_control(*event)) {
+ // We're about to consider a constrained snap, which is already limited to 1D
+ // Therefore tangential or perpendicular snapping will not be considered, and therefore
+ // all calls above to scp_free.addVector() and scp_free.addOrigin() can be neglected
+ std::vector<Inkscape::Snapper::SnapConstraint> constraints;
+ if (held_alt(*event)) { // with Ctrl+Alt, constrain to handle lines
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ double min_angle = M_PI / snaps;
+
+ if (front_direction) { // We only have a front_point if the front handle is extracted, or if it is not extracted but the path segment is straight (see above)
+ constraints.emplace_back(origin, *front_direction);
+ }
+
+ if (back_direction) {
+ constraints.emplace_back(origin, *back_direction);
+ }
+
+ // For smooth nodes, we will also snap to normals of handle lines. For cusp nodes this would be unintuitive and confusing
+ // Only snap to the normals when they are further than snap increment away from the second handle constraint
+ if (_type != NODE_CUSP) {
+ std::optional<Geom::Point> front_normal = Geom::rot90(*front_direction);
+ if (front_normal && (!back_direction ||
+ (fabs(Geom::angle_between(*front_normal, *back_direction)) > min_angle &&
+ fabs(Geom::angle_between(*front_normal, *back_direction)) < M_PI - min_angle)))
+ {
+ constraints.emplace_back(origin, *front_normal);
+ }
+
+ std::optional<Geom::Point> back_normal = Geom::rot90(*back_direction);
+ if (back_normal && (!front_direction ||
+ (fabs(Geom::angle_between(*back_normal, *front_direction)) > min_angle &&
+ fabs(Geom::angle_between(*back_normal, *front_direction)) < M_PI - min_angle)))
+ {
+ constraints.emplace_back(origin, *back_normal);
+ }
+ }
+
+ sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event));
+ } else {
+ // with Ctrl and no Alt: constrain to axes
+ constraints.emplace_back(origin, Geom::Point(1, 0));
+ constraints.emplace_back(origin, Geom::Point(0, 1));
+ sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event));
+ }
+ new_pos = sp.getPoint();
+ } else if (snap) {
+ Inkscape::SnappedPoint sp = sm.freeSnap(scp_free);
+ new_pos = sp.getPoint();
+ }
+
+ sm.unSetup();
+
+ SelectableControlPoint::dragged(new_pos, event);
+}
+
+bool Node::clicked(GdkEventButton *event)
+{
+ if(_pm()._nodeClicked(this, event))
+ return true;
+ return SelectableControlPoint::clicked(event);
+}
+
+Inkscape::SnapSourceType Node::_snapSourceType() const
+{
+ if (_type == NODE_SMOOTH || _type == NODE_AUTO)
+ return SNAPSOURCE_NODE_SMOOTH;
+ return SNAPSOURCE_NODE_CUSP;
+}
+Inkscape::SnapTargetType Node::_snapTargetType() const
+{
+ if (_type == NODE_SMOOTH || _type == NODE_AUTO)
+ return SNAPTARGET_NODE_SMOOTH;
+ return SNAPTARGET_NODE_CUSP;
+}
+
+Inkscape::SnapCandidatePoint Node::snapCandidatePoint()
+{
+ return SnapCandidatePoint(position(), _snapSourceType(), _snapTargetType());
+}
+
+Handle *Node::handleToward(Node *to)
+{
+ if (_next() == to) {
+ return front();
+ }
+ if (_prev() == to) {
+ return back();
+ }
+ g_error("Node::handleToward(): second node is not adjacent!");
+ return nullptr;
+}
+
+Node *Node::nodeToward(Handle *dir)
+{
+ if (front() == dir) {
+ return _next();
+ }
+ if (back() == dir) {
+ return _prev();
+ }
+ g_error("Node::nodeToward(): handle is not a child of this node!");
+ return nullptr;
+}
+
+Handle *Node::handleAwayFrom(Node *to)
+{
+ if (_next() == to) {
+ return back();
+ }
+ if (_prev() == to) {
+ return front();
+ }
+ g_error("Node::handleAwayFrom(): second node is not adjacent!");
+ return nullptr;
+}
+
+Node *Node::nodeAwayFrom(Handle *h)
+{
+ if (front() == h) {
+ return _prev();
+ }
+ if (back() == h) {
+ return _next();
+ }
+ g_error("Node::nodeAwayFrom(): handle is not a child of this node!");
+ return nullptr;
+}
+
+Glib::ustring Node::_getTip(unsigned state) const
+{
+ bool isBSpline = _pm()._isBSpline();
+ Handle *h = const_cast<Handle *>(&_front);
+ Glib::ustring s = C_("Path node tip",
+ "node handle"); // not expected
+
+ if (state_held_shift(state)) {
+ bool can_drag_out = (_next() && _front.isDegenerate()) ||
+ (_prev() && _back.isDegenerate());
+
+ if (can_drag_out) {
+ /*if (state_held_control(state)) {
+ s = format_tip(C_("Path node tip",
+ "<b>Shift+Ctrl:</b> drag out a handle and snap its angle "
+ "to %f° increments"), snap_increment_degrees());
+ }*/
+ s = C_("Path node tip",
+ "<b>Shift</b>: drag out a handle, click to toggle selection");
+ }
+ else {
+ s = C_("Path node tip",
+ "<b>Shift</b>: click to toggle selection");
+ }
+ }
+
+ else if (state_held_control(state)) {
+ if (state_held_alt(state)) {
+ s = C_("Path node tip",
+ "<b>Ctrl+Alt</b>: move along handle lines or line segment, click to delete node");
+ }
+ else {
+ s = C_("Path node tip",
+ "<b>Ctrl</b>: move along axes, click to change node type");
+ }
+ }
+
+ else if (state_held_alt(state)) {
+ s = C_("Path node tip",
+ "<b>Alt</b>: sculpt nodes");
+ }
+
+ else { // No modifiers: assemble tip from node type
+ char const *nodetype = node_type_to_localized_string(_type);
+ double power = _pm()._bsplineHandlePosition(h);
+
+ if (_selection.transformHandlesEnabled() && selected()) {
+ if (_selection.size() == 1) {
+ if (!isBSpline) {
+ s = format_tip(C_("Path node tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ nodetype);
+ }
+ else {
+ s = format_tip(C_("Path node tip",
+ "<b>BSpline node</b> (%.3g power): "
+ "drag to shape the path" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ power);
+ }
+ }
+ else {
+ s = format_tip(C_("Path node tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "click to toggle scale/rotation handles" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ nodetype);
+ }
+ }
+ else if (!isBSpline) {
+ s = format_tip(C_("Path node tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "click to select only this node" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ nodetype);
+ }
+ else {
+ s = format_tip(C_("Path node tip",
+ "<b>BSpline node</b> (%.3g power): "
+ "drag to shape the path" ", "
+ "click to select only this node" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ power);
+ }
+ }
+
+ return (s);
+}
+
+Glib::ustring Node::_getDragTip(GdkEventMotion */*event*/) const
+{
+ Geom::Point dist = position() - _last_drag_origin();
+
+ Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dist[Geom::X], "px");
+ Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dist[Geom::Y], "px");
+ Glib::ustring x = x_q.string(_desktop->namedview->display_units);
+ Glib::ustring y = y_q.string(_desktop->namedview->display_units);
+ Glib::ustring ret = format_tip(C_("Path node tip", "Move node by %s, %s"), x.c_str(), y.c_str());
+ return ret;
+}
+
+/**
+ * See also: Handle::handle_type_to_localized_string(NodeType type)
+ */
+char const *Node::node_type_to_localized_string(NodeType type)
+{
+ switch (type) {
+ case NODE_CUSP:
+ return _("Corner node");
+ case NODE_SMOOTH:
+ return _("Smooth node");
+ case NODE_SYMMETRIC:
+ return _("Symmetric node");
+ case NODE_AUTO:
+ return _("Auto-smooth node");
+ default:
+ return "";
+ }
+}
+
+bool Node::_is_line_segment(Node *first, Node *second)
+{
+ if (!first || !second) return false;
+ if (first->_next() == second)
+ return first->_front.isDegenerate() && second->_back.isDegenerate();
+ if (second->_next() == first)
+ return second->_front.isDegenerate() && first->_back.isDegenerate();
+ return false;
+}
+
+NodeList::NodeList(SubpathList &splist)
+ : _list(splist)
+ , _closed(false)
+{
+ this->ln_list = this;
+ this->ln_next = this;
+ this->ln_prev = this;
+}
+
+NodeList::~NodeList()
+{
+ clear();
+}
+
+bool NodeList::empty()
+{
+ return ln_next == this;
+}
+
+NodeList::size_type NodeList::size()
+{
+ size_type sz = 0;
+ for (ListNode *ln = ln_next; ln != this; ln = ln->ln_next) ++sz;
+ return sz;
+}
+
+bool NodeList::closed()
+{
+ return _closed;
+}
+
+bool NodeList::degenerate()
+{
+ return closed() ? empty() : ++begin() == end();
+}
+
+NodeList::iterator NodeList::before(double t, double *fracpart)
+{
+ double intpart;
+ *fracpart = std::modf(t, &intpart);
+ int index = intpart;
+
+ iterator ret = begin();
+ std::advance(ret, index);
+ return ret;
+}
+
+NodeList::iterator NodeList::before(Geom::PathTime const &pvp)
+{
+ iterator ret = begin();
+ std::advance(ret, pvp.curve_index);
+ return ret;
+}
+
+NodeList::iterator NodeList::insert(iterator pos, Node *x)
+{
+ ListNode *ins = pos._node;
+ x->ln_next = ins;
+ x->ln_prev = ins->ln_prev;
+ ins->ln_prev->ln_next = x;
+ ins->ln_prev = x;
+ x->ln_list = this;
+ return iterator(x);
+}
+
+void NodeList::splice(iterator pos, NodeList &list)
+{
+ splice(pos, list, list.begin(), list.end());
+}
+
+void NodeList::splice(iterator pos, NodeList &list, iterator i)
+{
+ NodeList::iterator j = i;
+ ++j;
+ splice(pos, list, i, j);
+}
+
+void NodeList::splice(iterator pos, NodeList &/*list*/, iterator first, iterator last)
+{
+ ListNode *ins_beg = first._node, *ins_end = last._node, *at = pos._node;
+ for (ListNode *ln = ins_beg; ln != ins_end; ln = ln->ln_next) {
+ ln->ln_list = this;
+ }
+ ins_beg->ln_prev->ln_next = ins_end;
+ ins_end->ln_prev->ln_next = at;
+ at->ln_prev->ln_next = ins_beg;
+
+ ListNode *atprev = at->ln_prev;
+ at->ln_prev = ins_end->ln_prev;
+ ins_end->ln_prev = ins_beg->ln_prev;
+ ins_beg->ln_prev = atprev;
+}
+
+void NodeList::shift(int n)
+{
+ // 1. make the list perfectly cyclic
+ ln_next->ln_prev = ln_prev;
+ ln_prev->ln_next = ln_next;
+ // 2. find new begin
+ ListNode *new_begin = ln_next;
+ if (n > 0) {
+ for (; n > 0; --n) new_begin = new_begin->ln_next;
+ } else {
+ for (; n < 0; ++n) new_begin = new_begin->ln_prev;
+ }
+ // 3. relink begin to list
+ ln_next = new_begin;
+ ln_prev = new_begin->ln_prev;
+ new_begin->ln_prev->ln_next = this;
+ new_begin->ln_prev = this;
+}
+
+void NodeList::reverse()
+{
+ for (ListNode *ln = ln_next; ln != this; ln = ln->ln_prev) {
+ std::swap(ln->ln_next, ln->ln_prev);
+ Node *node = static_cast<Node*>(ln);
+ Geom::Point save_pos = node->front()->position();
+ node->front()->setPosition(node->back()->position());
+ node->back()->setPosition(save_pos);
+ }
+ std::swap(ln_next, ln_prev);
+}
+
+void NodeList::clear()
+{
+ // ugly but more efficient clearing mechanism
+ std::vector<ControlPointSelection *> to_clear;
+ std::vector<std::pair<SelectableControlPoint *, long> > nodes;
+ long in = -1;
+ for (iterator i = begin(); i != end(); ++i) {
+ SelectableControlPoint *rm = static_cast<Node*>(i._node);
+ if (std::find(to_clear.begin(), to_clear.end(), &rm->_selection) == to_clear.end()) {
+ to_clear.push_back(&rm->_selection);
+ ++in;
+ }
+ nodes.emplace_back(rm, in);
+ }
+ for (auto const &node : nodes) {
+ to_clear[node.second]->erase(node.first, false);
+ }
+ std::vector<std::vector<SelectableControlPoint *> > emission;
+ for (long i = 0, e = to_clear.size(); i != e; ++i) {
+ emission.emplace_back();
+ for (auto const &node : nodes) {
+ if (node.second != i)
+ break;
+ emission[i].push_back(node.first);
+ }
+ }
+
+ for (size_t i = 0, e = emission.size(); i != e; ++i) {
+ to_clear[i]->signal_selection_changed.emit(emission[i], false);
+ }
+
+ for (iterator i = begin(); i != end();)
+ erase (i++);
+}
+
+NodeList::iterator NodeList::erase(iterator i)
+{
+ // some gymnastics are required to ensure that the node is valid when deleted;
+ // otherwise the code that updates handle visibility will break
+ Node *rm = static_cast<Node*>(i._node);
+ ListNode *rmnext = rm->ln_next, *rmprev = rm->ln_prev;
+ ++i;
+ delete rm;
+ rmprev->ln_next = rmnext;
+ rmnext->ln_prev = rmprev;
+ return i;
+}
+
+// TODO this method is very ugly!
+// converting SubpathList to an intrusive list might allow us to get rid of it
+void NodeList::kill()
+{
+ for (SubpathList::iterator i = _list.begin(); i != _list.end(); ++i) {
+ if (i->get() == this) {
+ _list.erase(i);
+ return;
+ }
+ }
+}
+
+NodeList &NodeList::get(Node *n) {
+ return n->nodeList();
+}
+NodeList &NodeList::get(iterator const &i) {
+ return *(i._node->ln_list);
+}
+
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ 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/ui/tool/node.h b/src/ui/tool/node.h
new file mode 100644
index 0000000..7fa3d2c
--- /dev/null
+++ b/src/ui/tool/node.h
@@ -0,0 +1,513 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Editable node and associated data structures.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_NODE_H
+#define SEEN_UI_TOOL_NODE_H
+
+#include <iterator>
+#include <iosfwd>
+#include <stdexcept>
+#include <cstddef>
+#include <functional>
+
+#include "ui/tool/selectable-control-point.h"
+#include "snapped-point.h"
+#include "ui/tool/node-types.h"
+
+namespace Inkscape {
+class CanvasItemGroup;
+class CanvasItemCurve;
+
+namespace UI {
+
+class PathManipulator;
+class MultiPathManipulator;
+
+class Node;
+class Handle;
+class NodeList;
+class SubpathList;
+template <typename> class NodeIterator;
+
+std::ostream &operator<<(std::ostream &, NodeType);
+
+struct ListNode {
+ ListNode *ln_next;
+ ListNode *ln_prev;
+ NodeList *ln_list;
+};
+
+struct NodeSharedData {
+ SPDesktop *desktop;
+ ControlPointSelection *selection;
+ Inkscape::CanvasItemGroup *node_group;
+ Inkscape::CanvasItemGroup *handle_group;
+ Inkscape::CanvasItemGroup *handle_line_group;
+};
+
+class Handle : public ControlPoint {
+public:
+
+ ~Handle() override;
+ inline Geom::Point relativePos() const;
+ inline double length() const;
+ bool isDegenerate() const { return _degenerate; } // True if the handle is retracted, i.e. has zero length.
+
+ void setVisible(bool) override;
+ void move(Geom::Point const &p) override;
+
+ void setPosition(Geom::Point const &p) override;
+ inline void setRelativePos(Geom::Point const &p);
+ void setLength(double len);
+ void retract();
+ void setDirection(Geom::Point const &from, Geom::Point const &to);
+ void setDirection(Geom::Point const &dir);
+ Node *parent() { return _parent; }
+ Handle *other();
+ Handle const *other() const;
+
+ static char const *handle_type_to_localized_string(NodeType type);
+
+protected:
+
+ Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent);
+ virtual void handle_2button_press();
+ bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override;
+ void dragged(Geom::Point &new_pos, GdkEventMotion *event) override;
+ bool grabbed(GdkEventMotion *event) override;
+ void ungrabbed(GdkEventButton *event) override;
+ bool clicked(GdkEventButton *event) override;
+
+ Glib::ustring _getTip(unsigned state) const override;
+ Glib::ustring _getDragTip(GdkEventMotion *event) const override;
+ bool _hasDragTips() const override { return true; }
+
+private:
+
+ inline PathManipulator &_pm();
+ inline PathManipulator &_pm() const;
+ void _update_bspline_handles();
+ Node *_parent; // the handle's lifetime does not extend beyond that of the parent node,
+ // so a naked pointer is OK and allows setting it during Node's construction
+ CanvasItemPtr<CanvasItemCurve> _handle_line;
+ bool _degenerate; // True if the handle is retracted, i.e. has zero length. This is used often internally so it makes sense to cache this
+
+ /**
+ * Control point of a cubic Bezier curve in a path.
+ *
+ * Handle keeps the node type invariant only for the opposite handle of the same node.
+ * Keeping the invariant on node moves is left to the %Node class.
+ */
+ static Geom::Point _saved_other_pos;
+
+ static double _saved_length;
+ static bool _drag_out;
+ static ColorSet _handle_colors;
+ friend class Node;
+};
+
+class Node : ListNode, public SelectableControlPoint {
+public:
+
+ /**
+ * Curve endpoint in an editable path.
+ *
+ * The method move() keeps node type invariants during translations.
+ */
+ Node(NodeSharedData const &data, Geom::Point const &pos);
+
+ Node(Node const &) = delete;
+
+ void move(Geom::Point const &p) override;
+ void transform(Geom::Affine const &m) override;
+ void fixNeighbors() override;
+ Geom::Rect bounds() const override;
+
+ NodeType type() const { return _type; }
+
+ /**
+ * Sets the node type and optionally restores the invariants associated with the given type.
+ * @param type The type to set.
+ * @param update_handles Whether to restore invariants associated with the given type.
+ * Passing false is useful e.g. when initially creating the path,
+ * and when making cusp nodes during some node algorithms.
+ * Pass true when used in response to an UI node type button.
+ */
+ void setType(NodeType type, bool update_handles = true);
+
+ void showHandles(bool v);
+
+ void updateHandles();
+
+
+ /**
+ * Pick the best type for this node, based on the position of its handles.
+ * This is what assigns types to nodes created using the pen tool.
+ */
+ void pickBestType(); // automatically determine the type from handle positions
+
+ bool isDegenerate() const { return _front.isDegenerate() && _back.isDegenerate(); }
+ bool isEndNode() const;
+ Handle *front() { return &_front; }
+ Handle *back() { return &_back; }
+
+ /**
+ * Gets the handle that faces the given adjacent node.
+ * Will abort with error if the given node is not adjacent.
+ */
+ Handle *handleToward(Node *to);
+
+ /**
+ * Gets the node in the direction of the given handle.
+ * Will abort with error if the handle doesn't belong to this node.
+ */
+ Node *nodeToward(Handle *h);
+
+ /**
+ * Gets the handle that goes in the direction opposite to the given adjacent node.
+ * Will abort with error if the given node is not adjacent.
+ */
+ Handle *handleAwayFrom(Node *to);
+
+ /**
+ * Gets the node in the direction opposite to the given handle.
+ * Will abort with error if the handle doesn't belong to this node.
+ */
+ Node *nodeAwayFrom(Handle *h);
+
+ NodeList &nodeList() { return *(static_cast<ListNode*>(this)->ln_list); }
+ NodeList &nodeList() const { return *(static_cast<ListNode const*>(this)->ln_list); }
+
+ /**
+ * Move the node to the bottom of its canvas group.
+ * Useful for node break, to ensure that the selected nodes are above the unselected ones.
+ */
+ void sink();
+
+ static NodeType parse_nodetype(char x);
+ static char const *node_type_to_localized_string(NodeType type);
+
+ // temporarily public
+ /** Customized event handler to catch scroll events needed for selection grow/shrink. */
+ bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override;
+
+ Inkscape::SnapCandidatePoint snapCandidatePoint();
+
+protected:
+
+ void dragged(Geom::Point &new_pos, GdkEventMotion *event) override;
+ bool grabbed(GdkEventMotion *event) override;
+ bool clicked(GdkEventButton *event) override;
+
+ void _setState(State state) override;
+ Glib::ustring _getTip(unsigned state) const override;
+ Glib::ustring _getDragTip(GdkEventMotion *event) const override;
+ bool _hasDragTips() const override { return true; }
+
+private:
+
+ void _updateAutoHandles();
+
+ /**
+ * Select or deselect a node in this node's subpath based on its path distance from this node.
+ * @param dir If negative, shrink selection by one node; if positive, grow by one node.
+ */
+ void _linearGrow(int dir);
+
+ Node *_next();
+ Node const *_next() const;
+ Node *_prev();
+ Node const *_prev() const;
+ Inkscape::SnapSourceType _snapSourceType() const;
+ Inkscape::SnapTargetType _snapTargetType() const;
+ inline PathManipulator &_pm();
+ inline PathManipulator &_pm() const;
+
+ /** Determine whether two nodes are joined by a linear segment. */
+ static bool _is_line_segment(Node *first, Node *second);
+
+ // Handles are always present, but are not visible if they coincide with the node
+ // (are degenerate). A segment that has both handles degenerate is always treated
+ // as a line segment
+ Handle _front; ///< Node handle in the backward direction of the path
+ Handle _back; ///< Node handle in the forward direction of the path
+ NodeType _type; ///< Type of node - cusp, smooth...
+ bool _handles_shown;
+ static ColorSet node_colors;
+
+ // This is used by fixNeighbors to repair smooth nodes after all move
+ // operations have been completed. If this is empty, no fixing is needed.
+ std::optional<Geom::Point> _unfixed_pos;
+
+ friend class Handle;
+ friend class NodeList;
+ friend class NodeIterator<Node>;
+ friend class NodeIterator<Node const>;
+};
+
+/// Iterator for editable nodes
+/** Use this class for all operations that require some knowledge about the node's
+ * neighbors. It is a bidirectional iterator.
+ *
+ * Because paths can be cyclic, node iterators have two different ways to
+ * increment and decrement them. When using ++/--, the end iterator will eventually
+ * be returned. When using advance()/retreat(), the end iterator will only be returned
+ * when the path is open. If it's closed, calling advance() will cycle indefinitely.
+ * This is particularly useful for cases where the adjacency of nodes is more important
+ * than their sequence order.
+ *
+ * When @a i is a node iterator, then:
+ * - <code>++i</code> moves the iterator to the next node in sequence order;
+ * - <code>--i</code> moves the iterator to the previous node in sequence order;
+ * - <code>i.next()</code> returns the next node with wrap-around;
+ * - <code>i.prev()</code> returns the previous node with wrap-around;
+ * - <code>i.advance()</code> moves the iterator to the next node with wrap-around;
+ * - <code>i.retreat()</code> moves the iterator to the previous node with wrap-around.
+ *
+ * next() and prev() do not change their iterator. They can return the end iterator
+ * if the path is open.
+ *
+ * Unlike most other iterators, you can check whether you've reached the end of the list
+ * without having access to the iterator's container.
+ * Simply use <code>if (i) { ...</code>
+ * */
+template <typename N>
+class NodeIterator
+ : public boost::bidirectional_iterator_helper<NodeIterator<N>, N, std::ptrdiff_t,
+ N *, N &>
+{
+public:
+ typedef NodeIterator self;
+ NodeIterator()
+ : _node(nullptr)
+ {}
+ // default copy, default assign
+
+ self &operator++() {
+ _node = (_node?_node->ln_next:nullptr);
+ return *this;
+ }
+ self &operator--() {
+ _node = (_node?_node->ln_prev:nullptr);
+ return *this;
+ }
+ bool operator==(self const &other) const { return _node == other._node; }
+ N &operator*() const { return *static_cast<N*>(_node); }
+ inline operator bool() const; // define after NodeList
+ /// Get a pointer to the underlying node. Equivalent to <code>&*i</code>.
+ N *get_pointer() const { return static_cast<N*>(_node); }
+ /// @see get_pointer()
+ N *ptr() const { return static_cast<N*>(_node); }
+
+ self next() const {
+ self r(*this);
+ r.advance();
+ return r;
+ }
+ self prev() const {
+ self r(*this);
+ r.retreat();
+ return r;
+ }
+ self &advance();
+ self &retreat();
+private:
+ NodeIterator(ListNode const *n)
+ : _node(const_cast<ListNode*>(n))
+ {}
+ ListNode *_node;
+ friend class NodeList;
+};
+
+class NodeList : ListNode, boost::noncopyable {
+public:
+ typedef std::size_t size_type;
+ typedef Node &reference;
+ typedef Node const &const_reference;
+ typedef Node *pointer;
+ typedef Node const *const_pointer;
+ typedef Node value_type;
+ typedef NodeIterator<value_type> iterator;
+ typedef NodeIterator<value_type const> const_iterator;
+
+ // TODO Lame. Make this private and make SubpathList a factory
+ /**
+ * An editable list of nodes representing a subpath.
+ *
+ * It can optionally be cyclic to represent a closed path.
+ * The list has iterators that act like plain node iterators, but can also be used
+ * to obtain shared pointers to nodes.
+ */
+ NodeList(SubpathList &_list);
+
+ ~NodeList();
+
+ // no copy or assign
+ NodeList(NodeList const &) = delete;
+ void operator=(NodeList const &) = delete;
+
+ // iterators
+ iterator begin() { return iterator(ln_next); }
+ iterator end() { return iterator(this); }
+ const_iterator begin() const { return const_iterator(ln_next); }
+ const_iterator end() const { return const_iterator(this); }
+
+ // size
+ bool empty();
+ size_type size();
+
+ // extra node-specific methods
+ bool closed();
+
+ /**
+ * A subpath is degenerate if it has no segments - either one node in an open path
+ * or no nodes in a closed path.
+ */
+ bool degenerate();
+
+ void setClosed(bool c) { _closed = c; }
+ iterator before(double t, double *fracpart = nullptr);
+ iterator before(Geom::PathTime const &pvp);
+ const_iterator before(double t, double *fracpart = nullptr) const {
+ return const_cast<NodeList *>(this)->before(t, fracpart)._node;
+ }
+ const_iterator before(Geom::PathTime const &pvp) const {
+ return const_cast<NodeList *>(this)->before(pvp)._node;
+ }
+
+ // list operations
+
+ /** insert a node before pos. */
+ iterator insert(iterator pos, Node *x);
+
+ template <class InputIterator>
+ void insert(iterator pos, InputIterator first, InputIterator last) {
+ for (; first != last; ++first) insert(pos, *first);
+ }
+ void splice(iterator pos, NodeList &list);
+ void splice(iterator pos, NodeList &list, iterator i);
+ void splice(iterator pos, NodeList &list, iterator first, iterator last);
+ void reverse();
+ void shift(int n);
+ void push_front(Node *x) { insert(begin(), x); }
+ void pop_front() { erase(begin()); }
+ void push_back(Node *x) { insert(end(), x); }
+ void pop_back() { erase(--end()); }
+ void clear();
+ iterator erase(iterator pos);
+ iterator erase(iterator first, iterator last) {
+ NodeList::iterator ret = first;
+ while (first != last) ret = erase(first++);
+ return ret;
+ }
+
+ // member access - undefined results when the list is empty
+ Node &front() { return *static_cast<Node*>(ln_next); }
+ Node &back() { return *static_cast<Node*>(ln_prev); }
+
+ // HACK remove this subpath from its path. This will be removed later.
+ void kill();
+ SubpathList &subpathList() { return _list; }
+
+ static iterator get_iterator(Node *n) { return iterator(n); }
+ static const_iterator get_iterator(Node const *n) { return const_iterator(n); }
+ static NodeList &get(Node *n);
+ static NodeList &get(iterator const &i);
+private:
+
+ SubpathList &_list;
+ bool _closed;
+
+ friend class Node;
+ friend class Handle; // required to access handle and handle line groups
+ friend class NodeIterator<Node>;
+ friend class NodeIterator<Node const>;
+};
+
+/**
+ * List of node lists. Represents an editable path.
+ * Editable path composed of one or more subpaths.
+ */
+class SubpathList : public std::list< std::shared_ptr<NodeList> > {
+public:
+ typedef std::list< std::shared_ptr<NodeList> > list_type;
+
+ SubpathList(PathManipulator &pm) : _path_manipulator(pm) {}
+ PathManipulator &pm() { return _path_manipulator; }
+
+private:
+ list_type _nodelists;
+ PathManipulator &_path_manipulator;
+ friend class NodeList;
+ friend class Node;
+ friend class Handle;
+};
+
+
+
+// define inline Handle funcs after definition of Node
+inline Geom::Point Handle::relativePos() const {
+ return position() - _parent->position();
+}
+inline void Handle::setRelativePos(Geom::Point const &p) {
+ setPosition(_parent->position() + p);
+}
+inline double Handle::length() const {
+ return relativePos().length();
+}
+inline PathManipulator &Handle::_pm() {
+ return _parent->_pm();
+}
+inline PathManipulator &Handle::_pm() const {
+ return _parent->_pm();
+}
+inline PathManipulator &Node::_pm() {
+ return nodeList().subpathList().pm();
+}
+
+inline PathManipulator &Node::_pm() const {
+ return nodeList().subpathList().pm();
+}
+
+// definitions for node iterator
+template <typename N>
+NodeIterator<N>::operator bool() const {
+ return _node && static_cast<ListNode*>(_node->ln_list) != _node;
+}
+template <typename N>
+NodeIterator<N> &NodeIterator<N>::advance() {
+ ++(*this);
+ if (G_UNLIKELY(!*this) && _node->ln_list->closed()) ++(*this);
+ return *this;
+}
+template <typename N>
+NodeIterator<N> &NodeIterator<N>::retreat() {
+ --(*this);
+ if (G_UNLIKELY(!*this) && _node->ln_list->closed()) --(*this);
+ return *this;
+}
+
+} // namespace UI
+} // 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/ui/tool/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp
new file mode 100644
index 0000000..d332c95
--- /dev/null
+++ b/src/ui/tool/path-manipulator.cpp
@@ -0,0 +1,1847 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Path manipulator - implementation.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/bezier-utils.h>
+#include <2geom/path-sink.h>
+#include <2geom/point.h>
+
+#include <utility>
+#include <vector>
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.h"
+
+#include <2geom/forward.h>
+#include "helper/geom.h"
+
+#include "live_effects/lpeobject.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/lpe-powerstroke.h"
+#include "live_effects/lpe-slice.h"
+#include "live_effects/lpe-bspline.h"
+#include "live_effects/parameter/path.h"
+
+#include "object/sp-path.h"
+#include "style.h"
+
+#include "ui/icon-names.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/curve-drag-point.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/node-types.h"
+#include "ui/tool/path-manipulator.h"
+#include "ui/tools/node-tool.h"
+#include "path/splinefit/bezier-fit.h"
+#include "xml/node-observer.h"
+
+namespace Inkscape {
+namespace UI {
+
+namespace {
+/// Types of path changes that we must react to.
+enum PathChange {
+ PATH_CHANGE_D,
+ PATH_CHANGE_TRANSFORM
+};
+
+} // anonymous namespace
+const double NO_POWER = 0.0;
+const double DEFAULT_START_POWER = 1.0/3.0;
+
+
+/**
+ * Notifies the path manipulator when something changes the path being edited
+ * (e.g. undo / redo)
+ */
+class PathManipulatorObserver : public Inkscape::XML::NodeObserver {
+public:
+ PathManipulatorObserver(PathManipulator *p, Inkscape::XML::Node *node)
+ : _pm(p)
+ , _node(node)
+ , _blocked(false)
+ {
+ Inkscape::GC::anchor(_node);
+ _node->addObserver(*this);
+ }
+
+ ~PathManipulatorObserver() override {
+ _node->removeObserver(*this);
+ Inkscape::GC::release(_node);
+ }
+
+ void notifyAttributeChanged(Inkscape::XML::Node &/*node*/, GQuark attr,
+ Util::ptr_shared, Util::ptr_shared) override
+ {
+ // do nothing if blocked
+ if (_blocked) return;
+
+ GQuark path_d = g_quark_from_static_string("d");
+ GQuark path_transform = g_quark_from_static_string("transform");
+ GQuark lpe_quark = _pm->_lpe_key.empty() ? 0 : g_quark_from_string(_pm->_lpe_key.data());
+
+ // only react to "d" (path data) and "transform" attribute changes
+ if (attr == lpe_quark || attr == path_d) {
+ _pm->_externalChange(PATH_CHANGE_D);
+ } else if (attr == path_transform) {
+ _pm->_externalChange(PATH_CHANGE_TRANSFORM);
+ }
+ }
+
+ void block() { _blocked = true; }
+ void unblock() { _blocked = false; }
+private:
+ PathManipulator *_pm;
+ Inkscape::XML::Node *_node;
+ bool _blocked;
+};
+
+void build_segment(Geom::PathBuilder &, Node *, Node *);
+PathManipulator::PathManipulator(MultiPathManipulator &mpm, SPObject *path,
+ Geom::Affine const &et, guint32 outline_color, Glib::ustring lpe_key)
+ : PointManipulator(mpm._path_data.node_data.desktop, *mpm._path_data.node_data.selection)
+ , _subpaths(*this)
+ , _multi_path_manipulator(mpm)
+ , _path(path)
+ , _dragpoint(new CurveDragPoint(*this))
+ , /* XML Tree being used here directly while it shouldn't be*/_observer(new PathManipulatorObserver(this, path->getRepr()))
+ , _edit_transform(et)
+ , _lpe_key(std::move(lpe_key))
+{
+ auto lpeobj = cast<LivePathEffectObject>(_path);
+ auto pathshadow = cast<SPPath>(_path);
+ if (!lpeobj) {
+ _i2d_transform = pathshadow->i2dt_affine();
+ } else {
+ _i2d_transform = Geom::identity();
+ }
+ _d2i_transform = _i2d_transform.inverse();
+ _dragpoint->setVisible(false);
+
+ _getGeometry();
+
+ _outline = make_canvasitem<Inkscape::CanvasItemBpath>(_multi_path_manipulator._path_data.outline_group);
+ _outline->hide();
+ _outline->set_stroke(outline_color);
+ _outline->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ _selection.signal_update.connect(
+ sigc::bind(sigc::mem_fun(*this, &PathManipulator::update), false));
+ _selection.signal_selection_changed.connect(
+ sigc::mem_fun(*this, &PathManipulator::_selectionChangedM));
+ _desktop->signal_zoom_changed.connect(
+ sigc::hide( sigc::mem_fun(*this, &PathManipulator::_updateOutlineOnZoomChange)));
+
+ //Define if the path is BSpline on construction
+ _recalculateIsBSpline();
+ _createControlPointsFromGeometry();
+}
+
+PathManipulator::~PathManipulator()
+{
+ delete _dragpoint;
+ delete _observer;
+ _outline.reset();
+ clear();
+}
+
+/** Handle motion events to update the position of the curve drag point. */
+bool PathManipulator::event(Inkscape::UI::Tools::ToolBase * /*event_context*/, GdkEvent *event)
+{
+ if (empty()) return false;
+
+ switch (event->type)
+ {
+ case GDK_MOTION_NOTIFY:
+ _updateDragPoint(event_point(event->motion));
+ break;
+ default:
+ break;
+ }
+ return false;
+}
+
+/** Check whether the manipulator has any nodes. */
+bool PathManipulator::empty() {
+ return !_path || _subpaths.empty();
+}
+
+/** Update the display and the outline of the path.
+ * \param alert_LPE if true, alerts an applied LPE to what the path is going to be changed to, so it can adjust its parameters for nicer user interfacing
+ */
+void PathManipulator::update(bool alert_LPE)
+{
+ _createGeometryFromControlPoints(alert_LPE);
+}
+
+/** Store the changes to the path in XML. */
+void PathManipulator::writeXML()
+{
+ if (!_live_outline)
+ _updateOutline();
+
+ _setGeometry();
+ if (!_path) {
+ return;
+ }
+
+ XML::Node *node = _getXMLNode();
+ if (!node) {
+ return;
+ }
+
+ _observer->block();
+ if (!empty()) {
+ _path->updateRepr();
+ node->setAttribute(_nodetypesKey(), _createTypeString());
+ } else {
+ // this manipulator will have to be destroyed right after this call
+ node->removeObserver(*_observer);
+ _path->deleteObject(true, true);
+ _path = nullptr;
+ }
+ _observer->unblock();
+}
+
+/** Remove all nodes from the path. */
+void PathManipulator::clear()
+{
+ // no longer necessary since nodes remove themselves from selection on destruction
+ //_removeNodesFromSelection();
+ _subpaths.clear();
+}
+
+/** Select all nodes in subpaths that have something selected. */
+void PathManipulator::selectSubpaths()
+{
+ for (auto & _subpath : _subpaths) {
+ NodeList::iterator sp_start = _subpath->begin(), sp_end = _subpath->end();
+ for (NodeList::iterator j = sp_start; j != sp_end; ++j) {
+ if (j->selected()) {
+ // if at least one of the nodes from this subpath is selected,
+ // select all nodes from this subpath
+ for (NodeList::iterator ins = sp_start; ins != sp_end; ++ins)
+ _selection.insert(ins.ptr());
+ continue;
+ }
+ }
+ }
+}
+
+/** Invert selection in the selected subpaths. */
+void PathManipulator::invertSelectionInSubpaths()
+{
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (j->selected()) {
+ // found selected node - invert selection in this subpath
+ for (NodeList::iterator k = _subpath->begin(); k != _subpath->end(); ++k) {
+ if (k->selected()) _selection.erase(k.ptr());
+ else _selection.insert(k.ptr());
+ }
+ // next subpath
+ break;
+ }
+ }
+ }
+}
+
+/** Insert a new node in the middle of each selected segment. */
+void PathManipulator::insertNodes()
+{
+ if (_selection.size() < 2) return;
+
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ NodeList::iterator k = j.next();
+ if (k && j->selected() && k->selected()) {
+ j = subdivideSegment(j, 0.5);
+ _selection.insert(j.ptr());
+ }
+ }
+ }
+}
+
+void PathManipulator::insertNode(Geom::Point pt)
+{
+ Geom::Coord dist = _updateDragPoint(pt);
+ if (dist < 1e-5) { // 1e-6 is too small, as observed occasionally when inserting a node at a snapped intersection of paths
+ insertNode(_dragpoint->getIterator(), _dragpoint->getTimeValue(), true);
+ }
+}
+
+void PathManipulator::insertNode(NodeList::iterator first, double t, bool take_selection)
+{
+ NodeList::iterator inserted = subdivideSegment(first, t);
+ if (take_selection) {
+ _selection.clear();
+ }
+ _selection.insert(inserted.ptr());
+
+ update(true);
+ _commit(_("Add node"));
+}
+
+
+static void
+add_or_replace_if_extremum(std::vector< std::pair<NodeList::iterator, double> > &vec,
+ double & extrvalue, double testvalue, NodeList::iterator const& node, double t)
+{
+ if (testvalue > extrvalue) {
+ // replace all extreme nodes with the new one
+ vec.clear();
+ vec.emplace_back( node, t );
+ extrvalue = testvalue;
+ } else if ( Geom::are_near(testvalue, extrvalue) ) {
+ // very rare but: extremum node at the same extreme value!!! so add it to the list
+ vec.emplace_back( node, t );
+ }
+}
+
+/** Insert a new node at the extremum of the selected segments. */
+void PathManipulator::insertNodeAtExtremum(ExtremumType extremum)
+{
+ if (_selection.size() < 2) return;
+
+ double sign = (extremum == EXTR_MIN_X || extremum == EXTR_MIN_Y) ? -1. : 1.;
+ Geom::Dim2 dim = (extremum == EXTR_MIN_X || extremum == EXTR_MAX_X) ? Geom::X : Geom::Y;
+
+ for (auto & _subpath : _subpaths) {
+ Geom::Coord extrvalue = - Geom::infinity();
+ std::vector< std::pair<NodeList::iterator, double> > extremum_vector;
+
+ for (NodeList::iterator first = _subpath->begin(); first != _subpath->end(); ++first) {
+ NodeList::iterator second = first.next();
+ if (second && first->selected() && second->selected()) {
+ add_or_replace_if_extremum(extremum_vector, extrvalue, sign * first->position()[dim], first, 0.);
+ add_or_replace_if_extremum(extremum_vector, extrvalue, sign * second->position()[dim], first, 1.);
+ if (first->front()->isDegenerate() && second->back()->isDegenerate()) {
+ // a line segment has is extrema at the start and end, no node should be added
+ continue;
+ } else {
+ // build 1D cubic bezier curve
+ Geom::Bezier temp1d(first->position()[dim], first->front()->position()[dim],
+ second->back()->position()[dim], second->position()[dim]);
+ // and determine extremum
+ Geom::Bezier deriv1d = derivative(temp1d);
+ std::vector<double> rs = deriv1d.roots();
+ for (double & r : rs) {
+ add_or_replace_if_extremum(extremum_vector, extrvalue, sign * temp1d.valueAt(r), first, r);
+ }
+ }
+ }
+ }
+
+ for (auto & i : extremum_vector) {
+ // don't insert node at the start or end of a segment, i.e. round values for extr_t
+ double t = i.second;
+ if ( !Geom::are_near(t - std::floor(t+0.5),0.) ) // std::floor(t+0.5) is another way of writing round(t)
+ {
+ _selection.insert( subdivideSegment(i.first, t).ptr() );
+ }
+ }
+ }
+}
+
+
+/** Insert new nodes exactly at the positions of selected nodes while preserving shape.
+ * This is equivalent to breaking, except that it doesn't split into subpaths. */
+void PathManipulator::duplicateNodes()
+{
+ if (_selection.empty()) return;
+
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (j->selected()) {
+ NodeList::iterator k = j.next();
+ Node *n = new Node(_multi_path_manipulator._path_data.node_data, *j);
+
+ if (k) {
+ // Move the new node to the bottom of the Z-order. This way you can drag all
+ // nodes that were selected before this operation without deselecting
+ // everything because there is a new node above.
+ n->sink();
+ }
+
+ n->front()->setPosition(*j->front());
+ j->front()->retract();
+ j->setType(NODE_CUSP, false);
+ _subpath->insert(k, n);
+
+ if (k) {
+ // We need to manually call the selection change callback to refresh
+ // the handle display correctly.
+ // This call changes num_selected, but we call this once for a selected node
+ // and once for an unselected node, so in the end the number stays correct.
+ _selectionChanged(j.ptr(), true);
+ _selectionChanged(n, false);
+ } else {
+ // select the new end node instead of the node just before it
+ _selection.erase(j.ptr());
+ _selection.insert(n);
+ break; // this was the end node, nothing more to do
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Copy the selected nodes using the PathBuilder
+ *
+ * @param builder[out] Selected nodes will be appended to this Path builder
+ * in pixel coordinates with all transforms applied.
+ */
+void PathManipulator::copySelectedPath(Geom::PathBuilder *builder)
+{
+ // Ignore LivePathEffect paths
+ if (!_path || cast<LivePathEffectObject>(_path))
+ return;
+ // Rebuild the selected parts of each subpath
+ for (auto &subpath : _subpaths) {
+ Node *prev = nullptr;
+ bool is_last_node = false;
+ for (auto &node : *subpath) {
+ if (node.selected()) {
+ // The node positions are already transformed
+ if (!builder->inPath() || !prev) {
+ builder->moveTo(node.position());
+ } else {
+ build_segment(*builder, prev, &node);
+ }
+ prev = &node;
+ is_last_node = true;
+ } else {
+ is_last_node = false;
+ }
+ }
+
+ // Complete the path, especially for closed sub paths where the last node is selected
+ if (subpath->closed() && is_last_node) {
+ if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()) {
+ build_segment(*builder, prev, subpath->begin().ptr());
+ }
+ // if that segment is linear, we just call closePath().
+ builder->closePath();
+ }
+ }
+ builder->flush();
+}
+
+/** Replace contiguous selections of nodes in each subpath with one node. */
+void PathManipulator::weldNodes(NodeList::iterator preserve_pos)
+{
+ if (_selection.size() < 2) return;
+ hideDragPoint();
+
+ bool pos_valid = preserve_pos;
+ for (auto sp : _subpaths) {
+ unsigned num_selected = 0, num_unselected = 0;
+ for (auto & j : *sp) {
+ if (j.selected()) ++num_selected;
+ else ++num_unselected;
+ }
+ if (num_selected < 2) continue;
+ if (num_unselected == 0) {
+ // if all nodes in a subpath are selected, the operation doesn't make much sense
+ continue;
+ }
+
+ // Start from unselected node in closed paths, so that we don't start in the middle
+ // of a selection
+ NodeList::iterator sel_beg = sp->begin(), sel_end;
+ if (sp->closed()) {
+ while (sel_beg->selected()) ++sel_beg;
+ }
+
+ // Work loop
+ while (num_selected > 0) {
+ // Find selected node
+ while (sel_beg && !sel_beg->selected()) sel_beg = sel_beg.next();
+ if (!sel_beg) throw std::logic_error("Join nodes: end of open path reached, "
+ "but there are still nodes to process!");
+
+ // note: this is initialized to zero, because the loop below counts sel_beg as well
+ // the loop conditions are simpler that way
+ unsigned num_points = 0;
+ bool use_pos = false;
+ Geom::Point back_pos, front_pos;
+ back_pos = *sel_beg->back();
+
+ for (sel_end = sel_beg; sel_end && sel_end->selected(); sel_end = sel_end.next()) {
+ ++num_points;
+ front_pos = *sel_end->front();
+ if (pos_valid && sel_end == preserve_pos) use_pos = true;
+ }
+ if (num_points > 1) {
+ Geom::Point joined_pos;
+ if (use_pos) {
+ joined_pos = preserve_pos->position();
+ pos_valid = false;
+ } else {
+ joined_pos = Geom::middle_point(back_pos, front_pos);
+ }
+ sel_beg->setType(NODE_CUSP, false);
+ sel_beg->move(joined_pos);
+ // do not move handles if they aren't degenerate
+ if (!sel_beg->back()->isDegenerate()) {
+ sel_beg->back()->setPosition(back_pos);
+ }
+ if (!sel_end.prev()->front()->isDegenerate()) {
+ sel_beg->front()->setPosition(front_pos);
+ }
+ sel_beg = sel_beg.next();
+ while (sel_beg != sel_end) {
+ NodeList::iterator next = sel_beg.next();
+ sp->erase(sel_beg);
+ sel_beg = next;
+ --num_selected;
+ }
+ }
+ --num_selected; // for the joined node or single selected node
+ }
+ }
+}
+
+/** Remove nodes in the middle of selected segments. */
+void PathManipulator::weldSegments()
+{
+ if (_selection.size() < 2) return;
+ hideDragPoint();
+
+ for (auto sp : _subpaths) {
+ unsigned num_selected = 0, num_unselected = 0;
+ for (auto & j : *sp) {
+ if (j.selected()) ++num_selected;
+ else ++num_unselected;
+ }
+
+ // if 2 or fewer nodes are selected, there can't be any middle points to remove.
+ if (num_selected <= 2) continue;
+
+ if (num_unselected == 0 && sp->closed()) {
+ // if all nodes in a closed subpath are selected, the operation doesn't make much sense
+ continue;
+ }
+
+ // Start from unselected node in closed paths, so that we don't start in the middle
+ // of a selection
+ NodeList::iterator sel_beg = sp->begin(), sel_end;
+ if (sp->closed()) {
+ while (sel_beg->selected()) ++sel_beg;
+ }
+
+ // Work loop
+ while (num_selected > 0) {
+ // Find selected node
+ while (sel_beg && !sel_beg->selected()) sel_beg = sel_beg.next();
+ if (!sel_beg) throw std::logic_error("Join nodes: end of open path reached, "
+ "but there are still nodes to process!");
+
+ // note: this is initialized to zero, because the loop below counts sel_beg as well
+ // the loop conditions are simpler that way
+ unsigned num_points = 0;
+
+ // find the end of selected segment
+ for (sel_end = sel_beg; sel_end && sel_end->selected(); sel_end = sel_end.next()) {
+ ++num_points;
+ }
+ if (num_points > 2) {
+ // remove nodes in the middle
+ // TODO: fit bezier to the former shape
+ sel_beg = sel_beg.next();
+ while (sel_beg != sel_end.prev()) {
+ NodeList::iterator next = sel_beg.next();
+ sp->erase(sel_beg);
+ sel_beg = next;
+ }
+ }
+ sel_beg = sel_end;
+ // decrease num_selected by the number of points processed
+ num_selected -= num_points;
+ }
+ }
+}
+
+/** Break the subpath at selected nodes. It also works for single node closed paths. */
+void PathManipulator::breakNodes()
+{
+ for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) {
+ SubpathPtr sp = *i;
+ NodeList::iterator cur = sp->begin(), end = sp->end();
+ if (!sp->closed()) {
+ // Each open path must have at least two nodes so no checks are required.
+ // For 2-node open paths, cur == end
+ ++cur;
+ --end;
+ }
+ for (; cur != end; ++cur) {
+ if (!cur->selected()) continue;
+ SubpathPtr ins;
+ bool becomes_open = false;
+
+ if (sp->closed()) {
+ // Move the node to break at to the beginning of path
+ if (cur != sp->begin())
+ sp->splice(sp->begin(), *sp, cur, sp->end());
+ sp->setClosed(false);
+ ins = sp;
+ becomes_open = true;
+ } else {
+ SubpathPtr new_sp(new NodeList(_subpaths));
+ new_sp->splice(new_sp->end(), *sp, sp->begin(), cur);
+ _subpaths.insert(i, new_sp);
+ ins = new_sp;
+ }
+
+ Node *n = new Node(_multi_path_manipulator._path_data.node_data, cur->position());
+ ins->insert(ins->end(), n);
+ cur->setType(NODE_CUSP, false);
+ n->back()->setRelativePos(cur->back()->relativePos());
+ cur->back()->retract();
+ n->sink();
+
+ if (becomes_open) {
+ cur = sp->begin(); // this will be increased to ++sp->begin()
+ end = --sp->end();
+ }
+ }
+ }
+}
+
+/** Delete selected nodes in the path, optionally substituting deleted segments with bezier curves
+ * in a way that attempts to preserve the original shape of the curve. */
+void PathManipulator::deleteNodes(NodeDeleteMode keep_shape)
+{
+ if (_selection.empty()) return;
+ hideDragPoint();
+
+ for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) {
+ SubpathPtr sp = *i;
+
+ // If there are less than 2 unselected nodes in an open subpath or no unselected nodes
+ // in a closed one, delete entire subpath.
+ unsigned num_unselected = 0, num_selected = 0;
+ for (auto & j : *sp) {
+ if (j.selected()) ++num_selected;
+ else ++num_unselected;
+ }
+ if (num_selected == 0) {
+ ++i;
+ continue;
+ }
+ if (sp->closed() ? (num_unselected < 1) : (num_unselected < 2)) {
+ _subpaths.erase(i++);
+ continue;
+ }
+
+ // In closed paths, start from an unselected node - otherwise we might start in the middle
+ // of a selected stretch and the resulting bezier fit would be suboptimal
+ NodeList::iterator sel_beg = sp->begin(), sel_end;
+ if (sp->closed()) {
+ while (sel_beg->selected()) ++sel_beg;
+ }
+ sel_end = sel_beg;
+
+ while (num_selected > 0) {
+ while (sel_beg && !sel_beg->selected()) {
+ sel_beg = sel_beg.next();
+ }
+ sel_end = sel_beg;
+
+ while (sel_end && sel_end->selected()) {
+ sel_end = sel_end.next();
+ }
+
+ num_selected -= _deleteStretch(sel_beg, sel_end, keep_shape);
+ sel_beg = sel_end;
+ }
+ ++i;
+ }
+}
+
+double get_angle(const Geom::Point& p0, const Geom::Point& p1, const Geom::Point& p2) {
+ auto d1 = p1 - p0;
+ auto d2 = p1 - p2;
+ if (d1.isZero() || d2.isZero()) return M_PI;
+
+ auto a1 = atan2(d1);
+ auto a2 = atan2(d2);
+ return a1 - a2;
+}
+
+/**
+ * Delete nodes between the two iterators.
+ * The given range can cross the beginning of the subpath in closed subpaths.
+ * @param start Beginning of the range to delete
+ * @param end End of the range
+ * @param keep_shape Whether to fit the handles at surrounding nodes to approximate
+ * the shape before deletion
+ * @return Number of deleted nodes
+ */
+unsigned PathManipulator::_deleteStretch(NodeList::iterator start, NodeList::iterator end, NodeDeleteMode mode)
+{
+ unsigned const samples_per_segment = 10;
+ double const t_step = 1.0 / samples_per_segment;
+
+ unsigned del_len = 0;
+ for (NodeList::iterator i = start; i != end; i = i.next()) {
+ ++del_len;
+ }
+ if (del_len == 0) return 0;
+
+ bool keep_shape = mode == NodeDeleteMode::automatic || mode == NodeDeleteMode::curve_fit;
+
+ if ((mode == NodeDeleteMode::automatic || mode == NodeDeleteMode::inverse_auto) && start.prev() && end) {
+ for (NodeList::iterator cur = start; cur != end; cur = cur.next()) {
+ auto back = cur->back() ->isDegenerate() ? cur.prev()->position() : cur->back() ->position();
+ auto front = cur->front()->isDegenerate() ? cur.next()->position() : cur->front()->position();
+ auto angle = get_angle(back, cur->position(), front);
+ auto a = fmod(fabs(angle), 2*M_PI);
+ auto diff = fabs(a - M_PI);
+ bool flat = diff < M_PI / 4; // flat if *somewhat* close to 180 degrees (+-45deg)
+ if (!flat && Geom::distance(back, front) > 1) {
+ // detected a cusp, so we'll try to remove nodes and insert line segment, rather than fitting a curve
+ // if in auto mode, or the opposite in inverse_auto
+ keep_shape = !keep_shape;
+ break;
+ }
+ }
+ }
+
+ // set surrounding node types to cusp if:
+ // 1. keep_shape is off, or
+ // 2. we are deleting at the end or beginning of an open path
+ if ((!keep_shape || !end) && start.prev()) {
+ auto p = start.prev();
+ p->setType(NODE_CUSP, false);
+ p->front()->retract();
+ }
+ if ((!keep_shape || !start.prev()) && end) {
+ end->setType(NODE_CUSP, false);
+ end->back()->retract();
+ }
+
+ if (keep_shape && start.prev() && end) {
+ std::vector<InputPoint> input;
+ Geom::Point result[4];
+ Geom::LineSegment s;
+ unsigned seg = 0;
+
+ for (NodeList::iterator cur = start.prev(); cur != end; cur = cur.next()) {
+ Geom::CubicBezier bc(*cur, *cur->front(), *cur.next()->back(), *cur.next());
+ for (unsigned s = 0; s < samples_per_segment; ++s) {
+ auto t = t_step * s;
+ input.emplace_back(InputPoint(bc.pointAt(t), t));
+ }
+ ++seg;
+ }
+ // Fill last point
+ // last point + its slope
+ input.emplace_back(InputPoint(end->position(), Geom::Point(), end->back()->position(), 1.0));
+
+ // get slope for the first point
+ input.front() = InputPoint(start.prev()->position(), start.prev()->front()->position(), Geom::Point(), 0.0);
+
+ // Compute replacement bezier curve
+ bezier_fit(result, input);
+
+ start.prev()->front()->setPosition(result[1]);
+ end->back()->setPosition(result[2]);
+ }
+
+ // We can't use nl->erase(start, end), because it would break when the stretch
+ // crosses the beginning of a closed subpath
+ NodeList &nl = start->nodeList();
+ while (start != end) {
+ NodeList::iterator next = start.next();
+ nl.erase(start);
+ start = next;
+ }
+ // if we are removing, we readjust the handlers
+ if (!keep_shape && _isBSpline()){
+ if(start.prev()){
+ double bspline_weight = _bsplineHandlePosition(start.prev()->back(), false);
+ start.prev()->front()->setPosition(_bsplineHandleReposition(start.prev()->front(), bspline_weight));
+ }
+ if(end){
+ double bspline_weight = _bsplineHandlePosition(end->front(), false);
+ end->back()->setPosition(_bsplineHandleReposition(end->back(),bspline_weight));
+ }
+ }
+
+ return del_len;
+}
+
+/** Removes selected segments */
+void PathManipulator::deleteSegments()
+{
+ if (_selection.empty()) return;
+ hideDragPoint();
+
+ for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) {
+ SubpathPtr sp = *i;
+ bool has_unselected = false;
+ unsigned num_selected = 0;
+ for (auto & j : *sp) {
+ if (j.selected()) {
+ ++num_selected;
+ } else {
+ has_unselected = true;
+ }
+ }
+ if (!has_unselected) {
+ _subpaths.erase(i++);
+ continue;
+ }
+
+ NodeList::iterator sel_beg = sp->begin();
+ if (sp->closed()) {
+ while (sel_beg && sel_beg->selected()) ++sel_beg;
+ }
+ while (num_selected > 0) {
+ if (!sel_beg->selected()) {
+ sel_beg = sel_beg.next();
+ continue;
+ }
+ NodeList::iterator sel_end = sel_beg;
+ unsigned num_points = 0;
+ while (sel_end && sel_end->selected()) {
+ sel_end = sel_end.next();
+ ++num_points;
+ }
+ if (num_points >= 2) {
+ // Retract end handles
+ sel_end.prev()->setType(NODE_CUSP, false);
+ sel_end.prev()->back()->retract();
+ sel_beg->setType(NODE_CUSP, false);
+ sel_beg->front()->retract();
+ if (sp->closed()) {
+ // In closed paths, relocate the beginning of the path to the last selected
+ // node and then unclose it. Remove the nodes from the first selected node
+ // to the new end of path.
+ if (sel_end.prev() != sp->begin())
+ sp->splice(sp->begin(), *sp, sel_end.prev(), sp->end());
+ sp->setClosed(false);
+ sp->erase(sel_beg.next(), sp->end());
+ } else {
+ // for open paths:
+ // 1. At end or beginning, delete including the node on the end or beginning
+ // 2. In the middle, delete only inner nodes
+ if (sel_beg == sp->begin()) {
+ sp->erase(sp->begin(), sel_end.prev());
+ } else if (sel_end == sp->end()) {
+ sp->erase(sel_beg.next(), sp->end());
+ } else {
+ SubpathPtr new_sp(new NodeList(_subpaths));
+ new_sp->splice(new_sp->end(), *sp, sp->begin(), sel_beg.next());
+ _subpaths.insert(i, new_sp);
+ if (sel_end.prev())
+ sp->erase(sp->begin(), sel_end.prev());
+ }
+ }
+ }
+ sel_beg = sel_end;
+ num_selected -= num_points;
+ }
+ ++i;
+ }
+}
+
+/** Reverse subpaths of the path.
+ * @param selected_only If true, only paths that have at least one selected node
+ * will be reversed. Otherwise all subpaths will be reversed. */
+void PathManipulator::reverseSubpaths(bool selected_only)
+{
+ for (auto & _subpath : _subpaths) {
+ if (selected_only) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (j->selected()) {
+ _subpath->reverse();
+ break; // continue with the next subpath
+ }
+ }
+ } else {
+ _subpath->reverse();
+ }
+ }
+}
+
+/** Make selected segments curves / lines. */
+void PathManipulator::setSegmentType(SegmentType type)
+{
+ if (_selection.empty()) return;
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ NodeList::iterator k = j.next();
+ if (!(k && j->selected() && k->selected())) continue;
+ switch (type) {
+ case SEGMENT_STRAIGHT:
+ if (j->front()->isDegenerate() && k->back()->isDegenerate())
+ break;
+ j->front()->move(*j);
+ k->back()->move(*k);
+ break;
+ case SEGMENT_CUBIC_BEZIER:
+ if (!j->front()->isDegenerate() || !k->back()->isDegenerate())
+ break;
+ // move both handles to 1/3 of the line
+ j->front()->move(j->position() + (k->position() - j->position()) / 3);
+ k->back()->move(k->position() + (j->position() - k->position()) / 3);
+ break;
+ }
+ }
+ }
+}
+
+void PathManipulator::scaleHandle(Node *n, int which, int dir, bool pixel)
+{
+ if (n->type() == NODE_SYMMETRIC || n->type() == NODE_AUTO) {
+ n->setType(NODE_SMOOTH);
+ }
+ Handle *h = _chooseHandle(n, which);
+ double length_change;
+
+ if (pixel) {
+ length_change = 1.0 / _desktop->current_zoom() * dir;
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000, "px");
+ length_change *= dir;
+ }
+
+ Geom::Point relpos;
+ if (h->isDegenerate()) {
+ if (dir < 0) return;
+ Node *nh = n->nodeToward(h);
+ if (!nh) return;
+ relpos = Geom::unit_vector(nh->position() - n->position()) * length_change;
+ } else {
+ relpos = h->relativePos();
+ double rellen = relpos.length();
+ relpos *= ((rellen + length_change) / rellen);
+ }
+ h->setRelativePos(relpos);
+ update();
+ gchar const *key = which < 0 ? "handle:scale:left" : "handle:scale:right";
+ _commit(_("Scale handle"), key);
+}
+
+void PathManipulator::rotateHandle(Node *n, int which, int dir, bool pixel)
+{
+ if (n->type() != NODE_CUSP) {
+ n->setType(NODE_CUSP);
+ }
+ Handle *h = _chooseHandle(n, which);
+ if (h->isDegenerate()) return;
+
+ double angle;
+ if (pixel) {
+ // Rotate by "one pixel"
+ angle = atan2(1.0 / _desktop->current_zoom(), h->length()) * dir;
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ angle = M_PI * dir / snaps;
+ }
+
+ h->setRelativePos(h->relativePos() * Geom::Rotate(angle));
+ update();
+ gchar const *key = which < 0 ? "handle:rotate:left" : "handle:rotate:right";
+ _commit(_("Rotate handle"), key);
+}
+
+Handle *PathManipulator::_chooseHandle(Node *n, int which)
+{
+ NodeList::iterator i = NodeList::get_iterator(n);
+ Node *prev = i.prev().ptr();
+ Node *next = i.next().ptr();
+
+ // on an endnode, the remaining handle automatically wins
+ if (!next) return n->back();
+ if (!prev) return n->front();
+
+ // compare X coord offline segments
+ Geom::Point npos = next->position();
+ Geom::Point ppos = prev->position();
+ if (which < 0) {
+ // pick left handle.
+ // we just swap the handles and pick the right handle below.
+ std::swap(npos, ppos);
+ }
+
+ if (npos[Geom::X] >= ppos[Geom::X]) {
+ return n->front();
+ } else {
+ return n->back();
+ }
+}
+
+/** Set the visibility of handles. */
+void PathManipulator::showHandles(bool show)
+{
+ if (show == _show_handles) return;
+ if (show) {
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (!j->selected()) continue;
+ j->showHandles(true);
+ if (j.prev()) j.prev()->showHandles(true);
+ if (j.next()) j.next()->showHandles(true);
+ }
+ }
+ } else {
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.showHandles(false);
+ }
+ }
+ }
+ _show_handles = show;
+}
+
+/** Set the visibility of outline. */
+void PathManipulator::showOutline(bool show)
+{
+ if (show == _show_outline) return;
+ _show_outline = show;
+ _updateOutline();
+}
+
+void PathManipulator::showPathDirection(bool show)
+{
+ if (show == _show_path_direction) return;
+ _show_path_direction = show;
+ _updateOutline();
+}
+
+void PathManipulator::setLiveOutline(bool set)
+{
+ _live_outline = set;
+}
+
+void PathManipulator::setLiveObjects(bool set)
+{
+ _live_objects = set;
+}
+
+void PathManipulator::updateHandles()
+{
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.updateHandles();
+ }
+ }
+}
+
+void PathManipulator::setControlsTransform(Geom::Affine const &tnew)
+{
+ Geom::Affine delta = _i2d_transform.inverse() * _edit_transform.inverse() * tnew * _i2d_transform;
+ _edit_transform = tnew;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.transform(delta);
+ }
+ }
+ _createGeometryFromControlPoints();
+}
+
+/** Hide the curve drag point until the next motion event.
+ * This should be called at the beginning of every method that can delete nodes.
+ * Otherwise the invalidated iterator in the dragpoint can cause crashes. */
+void PathManipulator::hideDragPoint()
+{
+ _dragpoint->setVisible(false);
+ _dragpoint->setIterator(NodeList::iterator());
+}
+
+/** Insert a node in the segment beginning with the supplied iterator,
+ * at the given time value */
+NodeList::iterator PathManipulator::subdivideSegment(NodeList::iterator first, double t)
+{
+ if (!first) throw std::invalid_argument("Subdivide after invalid iterator");
+ NodeList &list = NodeList::get(first);
+ NodeList::iterator second = first.next();
+ if (!second) throw std::invalid_argument("Subdivide after last node in open path");
+ if (first->type() == NODE_SYMMETRIC)
+ first->setType(NODE_SMOOTH, false);
+ if (second->type() == NODE_SYMMETRIC)
+ second->setType(NODE_SMOOTH, false);
+
+ // We need to insert the segment after 'first'. We can't simply use 'second'
+ // as the point of insertion, because when 'first' is the last node of closed path,
+ // the new node will be inserted as the first node instead.
+ NodeList::iterator insert_at = first;
+ ++insert_at;
+
+ NodeList::iterator inserted;
+ if (first->front()->isDegenerate() && second->back()->isDegenerate()) {
+ // for a line segment, insert a cusp node
+ Node *n = new Node(_multi_path_manipulator._path_data.node_data,
+ Geom::lerp(t, first->position(), second->position()));
+ n->setType(NODE_CUSP, false);
+ inserted = list.insert(insert_at, n);
+ } else {
+ // build bezier curve and subdivide
+ Geom::CubicBezier temp(first->position(), first->front()->position(),
+ second->back()->position(), second->position());
+ std::pair<Geom::CubicBezier, Geom::CubicBezier> div = temp.subdivide(t);
+ std::vector<Geom::Point> seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints();
+
+ // set new handle positions
+ Node *n = new Node(_multi_path_manipulator._path_data.node_data, seg2[0]);
+ if(!_isBSpline()){
+ n->back()->setPosition(seg1[2]);
+ n->front()->setPosition(seg2[1]);
+ n->setType(NODE_SMOOTH, false);
+ } else {
+ Geom::D2< Geom::SBasis > sbasis_inside_nodes;
+ SPCurve line_inside_nodes;
+ if(second->back()->isDegenerate()){
+ line_inside_nodes.moveto(n->position());
+ line_inside_nodes.lineto(second->position());
+ sbasis_inside_nodes = line_inside_nodes.first_segment()->toSBasis();
+ Geom::Point next = sbasis_inside_nodes.valueAt(DEFAULT_START_POWER);
+ line_inside_nodes.reset();
+ n->front()->setPosition(next);
+ }else{
+ n->front()->setPosition(seg2[1]);
+ }
+ if(first->front()->isDegenerate()){
+ line_inside_nodes.moveto(n->position());
+ line_inside_nodes.lineto(first->position());
+ sbasis_inside_nodes = line_inside_nodes.first_segment()->toSBasis();
+ Geom::Point previous = sbasis_inside_nodes.valueAt(DEFAULT_START_POWER);
+ n->back()->setPosition(previous);
+ }else{
+ n->back()->setPosition(seg1[2]);
+ }
+ n->setType(NODE_CUSP, false);
+ }
+ inserted = list.insert(insert_at, n);
+
+ first->front()->move(seg1[1]);
+ second->back()->move(seg2[2]);
+ }
+ return inserted;
+}
+
+/** Find the node that is closest/farthest from the origin
+ * @param origin Point of reference
+ * @param search_selected Consider selected nodes
+ * @param search_unselected Consider unselected nodes
+ * @param closest If true, return closest node, if false, return farthest
+ * @return The matching node, or an empty iterator if none found
+ */
+NodeList::iterator PathManipulator::extremeNode(NodeList::iterator origin, bool search_selected,
+ bool search_unselected, bool closest)
+{
+ NodeList::iterator match;
+ double extr_dist = closest ? HUGE_VAL : -HUGE_VAL;
+ if (_selection.empty() && !search_unselected) return match;
+
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if(j->selected()) {
+ if (!search_selected) continue;
+ } else {
+ if (!search_unselected) continue;
+ }
+ double dist = Geom::distance(*j, *origin);
+ bool cond = closest ? (dist < extr_dist) : (dist > extr_dist);
+ if (cond) {
+ match = j;
+ extr_dist = dist;
+ }
+ }
+ }
+ return match;
+}
+
+/* Called when a process updates the path in-situe */
+void PathManipulator::updatePath()
+{
+ _externalChange(PATH_CHANGE_D);
+}
+
+/** Called by the XML observer when something else than us modifies the path. */
+void PathManipulator::_externalChange(unsigned type)
+{
+ hideDragPoint();
+
+ switch (type) {
+ case PATH_CHANGE_D: {
+ _getGeometry();
+
+ // ugly: stored offsets of selected nodes in a vector
+ // vector<bool> should be specialized so that it takes only 1 bit per value
+ std::vector<bool> selpos;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ selpos.push_back(j.selected());
+ }
+ }
+ unsigned size = selpos.size(), curpos = 0;
+
+ _createControlPointsFromGeometry();
+
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (curpos >= size) goto end_restore;
+ if (selpos[curpos]) _selection.insert(j.ptr());
+ ++curpos;
+ }
+ }
+ end_restore:
+
+ _updateOutline();
+ } break;
+ case PATH_CHANGE_TRANSFORM: {
+ auto path = cast<SPPath>(_path);
+ if (path) {
+ Geom::Affine i2d_change = _d2i_transform;
+ _i2d_transform = path->i2dt_affine();
+ _d2i_transform = _i2d_transform.inverse();
+ i2d_change *= _i2d_transform;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.transform(i2d_change);
+ }
+ }
+ _updateOutline();
+ }
+ } break;
+ default: break;
+ }
+}
+
+Geom::Affine PathManipulator::_getTransform() const
+{
+ return _i2d_transform * _edit_transform;
+}
+
+/** Create nodes and handles based on the XML of the edited path. */
+void PathManipulator::_createControlPointsFromGeometry()
+{
+ clear();
+
+ // sanitize pathvector and store it in SPCurve,
+ // so that _updateDragPoint doesn't crash on paths with naked movetos
+ Geom::PathVector pathv;
+ if (_is_bspline) {
+ pathv = pathv_to_cubicbezier(_spcurve.get_pathvector(), false);
+ } else {
+ pathv = pathv_to_linear_and_cubic_beziers(_spcurve.get_pathvector());
+ }
+ for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) {
+ // NOTE: this utilizes the fact that Geom::PathVector is an std::vector.
+ // When we erase an element, the next one slides into position,
+ // so we do not increment the iterator even though it is theoretically invalidated.
+ if (i->empty()) {
+ i = pathv.erase(i);
+ } else {
+ ++i;
+ }
+ }
+ if (pathv.empty()) {
+ return;
+ }
+ _spcurve = SPCurve(pathv);
+
+ pathv *= _getTransform();
+
+ // in this loop, we know that there are no zero-segment subpaths
+ for (auto & pit : pathv) {
+ // prepare new subpath
+ SubpathPtr subpath(new NodeList(_subpaths));
+ _subpaths.push_back(subpath);
+
+ Node *previous_node = new Node(_multi_path_manipulator._path_data.node_data, pit.initialPoint());
+ subpath->push_back(previous_node);
+
+ bool closed = pit.closed();
+
+ for (Geom::Path::iterator cit = pit.begin(); cit != pit.end(); ++cit) {
+ Geom::Point pos = cit->finalPoint();
+ Node *current_node;
+ // if the closing segment is degenerate and the path is closed, we need to move
+ // the handle of the first node instead of creating a new one
+ if (closed && cit == --(pit.end())) {
+ current_node = subpath->begin().get_pointer();
+ } else {
+ /* regardless of segment type, create a new node at the end
+ * of this segment (unless this is the last segment of a closed path
+ * with a degenerate closing segment */
+ current_node = new Node(_multi_path_manipulator._path_data.node_data, pos);
+ subpath->push_back(current_node);
+ }
+ // if this is a bezier segment, move handles appropriately
+ // TODO: I don't know why the dynamic cast below doesn't want to work
+ // when I replace BezierCurve with CubicBezier. Might be a bug
+ // somewhere in pathv_to_linear_and_cubic_beziers
+ Geom::BezierCurve const *bezier = dynamic_cast<Geom::BezierCurve const*>(&*cit);
+ if (bezier && bezier->order() == 3)
+ {
+ previous_node->front()->setPosition((*bezier)[1]);
+ current_node ->back() ->setPosition((*bezier)[2]);
+ }
+ previous_node = current_node;
+ }
+ // If the path is closed, make the list cyclic
+ if (pit.closed()) subpath->setClosed(true);
+ }
+
+ // we need to set the nodetypes after all the handles are in place,
+ // so that pickBestType works correctly
+ // TODO maybe migrate to inkscape:node-types?
+ // TODO move this into SPPath - do not manipulate directly
+
+ //XML Tree being used here directly while it shouldn't be.
+ gchar const *nts_raw = _path ? _path->getRepr()->attribute(_nodetypesKey().data()) : nullptr;
+ /* Calculate the needed length of the nodetype string.
+ * For closed paths, the entry is duplicated for the starting node,
+ * so we can just use the count of segments including the closing one
+ * to include the extra end node. */
+ /* pad the string to required length with a bogus value.
+ * 'b' and any other letter not recognized by the parser causes the best fit to be set
+ * as the node type */
+ auto const *tsi = nts_raw ? nts_raw : "";
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ char nodetype = (*tsi) ? (*tsi++) : 'b';
+ j.setType(Node::parse_nodetype(nodetype), false);
+ }
+ if (_subpath->closed() && *tsi) {
+ // STUPIDITY ALERT: it seems we need to use the duplicate type symbol instead of
+ // the first one to remain backward compatible.
+ _subpath->begin()->setType(Node::parse_nodetype(*tsi++), false);
+ }
+ }
+}
+
+//determines if the trace has a bspline effect and the number of steps that it takes
+int PathManipulator::_bsplineGetSteps() const {
+
+ LivePathEffect::LPEBSpline const *lpe_bsp = nullptr;
+
+ auto path = cast<SPLPEItem>(_path);
+ if (path){
+ if(path->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect const *this_effect =
+ path->getFirstPathEffectOfType(Inkscape::LivePathEffect::BSPLINE);
+ if(this_effect){
+ lpe_bsp = dynamic_cast<LivePathEffect::LPEBSpline const*>(this_effect->getLPEObj()->get_lpe());
+ }
+ }
+ }
+ int steps = 0;
+ if(lpe_bsp){
+ steps = lpe_bsp->steps+1;
+ }
+ return steps;
+}
+
+// determines if the trace has bspline effect
+void PathManipulator::_recalculateIsBSpline(){
+ auto path = cast<SPPath>(_path);
+ if (path && path->hasPathEffect()) {
+ Inkscape::LivePathEffect::Effect const *this_effect =
+ path->getFirstPathEffectOfType(Inkscape::LivePathEffect::BSPLINE);
+ if(this_effect){
+ _is_bspline = true;
+ return;
+ }
+ }
+ _is_bspline = false;
+}
+
+bool PathManipulator::_isBSpline() const {
+ return _is_bspline;
+}
+
+// returns the corresponding strength to the position of the handlers
+double PathManipulator::_bsplineHandlePosition(Handle *h, bool check_other)
+{
+ using Geom::X;
+ using Geom::Y;
+ double pos = NO_POWER;
+ Node *n = h->parent();
+ Node * next_node = nullptr;
+ next_node = n->nodeToward(h);
+ if(next_node){
+ SPCurve line_inside_nodes;
+ line_inside_nodes.moveto(n->position());
+ line_inside_nodes.lineto(next_node->position());
+ if(!are_near(h->position(), n->position())){
+ pos = Geom::nearest_time(h->position(), *line_inside_nodes.first_segment());
+ }
+ }
+ if (pos == NO_POWER && check_other){
+ return _bsplineHandlePosition(h->other(), false);
+ }
+ return pos;
+}
+
+// give the location for the handler in the corresponding position
+Geom::Point PathManipulator::_bsplineHandleReposition(Handle *h, bool check_other)
+{
+ double pos = this->_bsplineHandlePosition(h, check_other);
+ return _bsplineHandleReposition(h,pos);
+}
+
+// give the location for the handler to the specified position
+Geom::Point PathManipulator::_bsplineHandleReposition(Handle *h,double pos){
+ using Geom::X;
+ using Geom::Y;
+ Geom::Point ret = h->position();
+ Node *n = h->parent();
+ Geom::D2< Geom::SBasis > sbasis_inside_nodes;
+ SPCurve line_inside_nodes;
+ Node * next_node = nullptr;
+ next_node = n->nodeToward(h);
+ if(next_node && pos != NO_POWER){
+ line_inside_nodes.moveto(n->position());
+ line_inside_nodes.lineto(next_node->position());
+ sbasis_inside_nodes = line_inside_nodes.first_segment()->toSBasis();
+ ret = sbasis_inside_nodes.valueAt(pos);
+ } else{
+ if(pos == NO_POWER){
+ ret = n->position();
+ }
+ }
+ return ret;
+}
+
+/** Construct the geometric representation of nodes and handles, update the outline
+ * and display
+ * \param alert_LPE if true, first the LPE is warned what the new path is going to be before updating it
+ */
+void PathManipulator::_createGeometryFromControlPoints(bool alert_LPE)
+{
+ Geom::PathBuilder builder;
+ //Refresh if is bspline some times -think on path change selection, this value get lost
+ _recalculateIsBSpline();
+ for (std::list<SubpathPtr>::iterator spi = _subpaths.begin(); spi != _subpaths.end(); ) {
+ SubpathPtr subpath = *spi;
+ if (subpath->empty()) {
+ _subpaths.erase(spi++);
+ continue;
+ }
+ NodeList::iterator prev = subpath->begin();
+ builder.moveTo(prev->position());
+ for (NodeList::iterator i = ++subpath->begin(); i != subpath->end(); ++i) {
+ build_segment(builder, prev.ptr(), i.ptr());
+ prev = i;
+ }
+ if (subpath->closed()) {
+ // Here we link the last and first node if the path is closed.
+ // If the last segment is Bezier, we add it.
+ if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()) {
+ build_segment(builder, prev.ptr(), subpath->begin().ptr());
+ }
+ // if that segment is linear, we just call closePath().
+ builder.closePath();
+ }
+ ++spi;
+ }
+ builder.flush();
+ Geom::PathVector pathv = builder.peek() * _getTransform().inverse();
+ for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) {
+ // NOTE: this utilizes the fact that Geom::PathVector is an std::vector.
+ // When we erase an element, the next one slides into position,
+ // so we do not increment the iterator even though it is theoretically invalidated.
+ if (i->empty()) {
+ i = pathv.erase(i);
+ } else {
+ ++i;
+ }
+ }
+ if (pathv.empty()) {
+ return;
+ }
+
+ if (_spcurve.get_pathvector() == pathv) {
+ return;
+ }
+ _spcurve = SPCurve(pathv);
+ if (alert_LPE) {
+ /// \todo note that _path can be an Inkscape::LivePathEffect::Effect* too, kind of confusing, rework member naming?
+ auto path = cast<SPPath>(_path);
+ if (path && path->hasPathEffect()) {
+ Inkscape::LivePathEffect::Effect *this_effect =
+ path->getFirstPathEffectOfType(Inkscape::LivePathEffect::POWERSTROKE);
+ LivePathEffect::LPEPowerStroke *lpe_pwr = dynamic_cast<LivePathEffect::LPEPowerStroke*>(this_effect);
+ if (lpe_pwr) {
+ lpe_pwr->adjustForNewPath();
+ }
+ }
+ }
+ if (_live_outline) {
+ _updateOutline();
+ }
+ if (_live_objects) {
+ _setGeometry();
+ }
+}
+
+/** Build one segment of the geometric representation.
+ * @relates PathManipulator */
+void build_segment(Geom::PathBuilder &builder, Node *prev_node, Node *cur_node)
+{
+ if (cur_node->back()->isDegenerate() && prev_node->front()->isDegenerate())
+ {
+ // NOTE: It seems like the renderer cannot correctly handle vline / hline segments,
+ // and trying to display a path using them results in funny artifacts.
+ builder.lineTo(cur_node->position());
+ } else {
+ // this is a bezier segment
+ builder.curveTo(
+ prev_node->front()->position(),
+ cur_node->back()->position(),
+ cur_node->position());
+ }
+}
+
+/** Construct a node type string to store in the sodipodi:nodetypes attribute. */
+std::string PathManipulator::_createTypeString()
+{
+ // precondition: no single-node subpaths
+ std::stringstream tstr;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ tstr << j.type();
+ }
+ // nodestring format peculiarity: first node is counted twice for closed paths
+ if (_subpath->closed()) tstr << _subpath->begin()->type();
+ }
+ return tstr.str();
+}
+
+/** Update the path outline. */
+void PathManipulator::_updateOutline()
+{
+ if (!_show_outline) {
+ _outline->hide();
+ return;
+ }
+
+ auto pv = _spcurve.get_pathvector() * _getTransform();
+ // This SPCurve thing has to be killed with extreme prejudice
+ if (_show_path_direction) {
+ // To show the direction, we append additional subpaths which consist of a single
+ // linear segment that starts at the time value of 0.5 and extends for 10 pixels
+ // at an angle 150 degrees from the unit tangent. This creates the appearance
+ // of little 'harpoons' that show the direction of the subpaths.
+ auto rot_scale_w2d = Geom::Rotate(210.0 / 180.0 * M_PI) * Geom::Scale(10.0) * _desktop->w2d();
+ Geom::PathVector arrows;
+ for (auto & path : pv) {
+ for (Geom::Path::iterator j = path.begin(); j != path.end_default(); ++j) {
+ Geom::Point at = j->pointAt(0.5);
+ Geom::Point ut = j->unitTangentAt(0.5);
+ Geom::Point arrow_end = at + (Geom::unit_vector(_desktop->d2w(ut)) * rot_scale_w2d);
+
+ Geom::Path arrow(at);
+ arrow.appendNew<Geom::LineSegment>(arrow_end);
+ arrows.push_back(arrow);
+ }
+ }
+ pv.insert(pv.end(), arrows.begin(), arrows.end());
+ }
+ auto tmp = SPCurve(std::move(pv));
+ _outline->set_bpath(&tmp);
+ _outline->show();
+}
+
+/** Retrieve the geometry of the edited object from the object tree */
+void PathManipulator::_getGeometry()
+{
+ using namespace Inkscape::LivePathEffect;
+ auto lpeobj = cast<LivePathEffectObject>(_path);
+ auto path = cast<SPPath>(_path);
+ if (lpeobj) {
+ Effect *lpe = lpeobj->get_lpe();
+ if (lpe) {
+ PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data()));
+ _spcurve = SPCurve(pathparam->get_pathvector());
+ }
+ } else if (path) {
+ if (path->curveForEdit()) {
+ _spcurve = *path->curveForEdit();
+ } else {
+ _spcurve = SPCurve();
+ }
+ }
+}
+
+/** Set the geometry of the edited object in the object tree, but do not commit to XML */
+void PathManipulator::_setGeometry()
+{
+ using namespace Inkscape::LivePathEffect;
+ auto lpeobj = cast<LivePathEffectObject>(_path);
+ auto path = cast<SPPath>(_path);
+ if (lpeobj) {
+ // copied from nodepath.cpp
+ // NOTE: if we are editing an LPE param, _path is not actually an SPPath, it is
+ // a LivePathEffectObject. (mad laughter)
+ Effect *lpe = lpeobj->get_lpe();
+ if (lpe) {
+ PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data()));
+ if (pathparam->get_pathvector() == _spcurve.get_pathvector()) {
+ return; //False we dont update LPE
+ }
+ pathparam->set_new_value(_spcurve.get_pathvector(), false);
+ lpeobj->requestModified(SP_OBJECT_MODIFIED_FLAG);
+ }
+ } else if (path) {
+ // return true to leave the decision on empty to the caller.
+ // Maybe the path become empty and we want to update to empty
+ if (empty()) return;
+ if (path->curveBeforeLPE()) {
+ path->setCurveBeforeLPE(&_spcurve);
+ if (path->hasPathEffectRecursive()) {
+ sp_lpe_item_update_patheffect(path, true, false);
+ }
+ } else {
+ path->setCurve(&_spcurve);
+ }
+ }
+}
+
+/** Figure out in what attribute to store the nodetype string. */
+Glib::ustring PathManipulator::_nodetypesKey()
+{
+ auto lpeobj = cast<LivePathEffectObject>(_path);
+ if (!lpeobj) {
+ return "sodipodi:nodetypes";
+ } else {
+ return _lpe_key + "-nodetypes";
+ }
+}
+
+/** Return the XML node we are editing.
+ * This method is wrong but necessary at the moment. */
+Inkscape::XML::Node *PathManipulator::_getXMLNode()
+{
+ //XML Tree being used here directly while it shouldn't be.
+ auto lpeobj = cast<LivePathEffectObject>(_path);
+ if (!lpeobj)
+ return _path->getRepr();
+ //XML Tree being used here directly while it shouldn't be.
+ return lpeobj->getRepr();
+}
+
+bool PathManipulator::_nodeClicked(Node *n, GdkEventButton *event)
+{
+ if (event->button != 1) return false;
+ if (held_alt(*event) && held_control(*event)) {
+ // Ctrl+Alt+click: delete nodes
+ hideDragPoint();
+ NodeList::iterator iter = NodeList::get_iterator(n);
+ NodeList &nl = iter->nodeList();
+
+ if (nl.size() <= 1 || (nl.size() <= 2 && !nl.closed())) {
+ // Removing last node of closed path - delete it
+ nl.kill();
+ } else {
+ // In other cases, delete the node under cursor
+ _deleteStretch(iter, iter.next(), NodeDeleteMode::curve_fit);
+ }
+
+ if (!empty()) {
+ update(true);
+ }
+
+ // We need to call MPM's method because it could have been our last node
+ _multi_path_manipulator._doneWithCleanup(_("Delete node"));
+
+ return true;
+ } else if (held_control(*event)) {
+ // Ctrl+click: cycle between node types
+ if (!n->isEndNode()) {
+ n->setType(static_cast<NodeType>((n->type() + 1) % NODE_LAST_REAL_TYPE));
+ update();
+ _commit(_("Cycle node type"));
+ }
+ return true;
+ }
+ return false;
+}
+
+void PathManipulator::_handleGrabbed()
+{
+ _selection.hideTransformHandles();
+}
+
+void PathManipulator::_handleUngrabbed()
+{
+ _selection.restoreTransformHandles();
+ _commit(_("Drag handle"));
+}
+
+bool PathManipulator::_handleClicked(Handle *h, GdkEventButton *event)
+{
+ // retracting by Ctrl+click
+ if (event->button == 1 && held_control(*event)) {
+ h->move(h->parent()->position());
+ update();
+ _commit(_("Retract handle"));
+ return true;
+ }
+ return false;
+}
+
+void PathManipulator::_selectionChangedM(std::vector<SelectableControlPoint *> pvec, bool selected) {
+ for (auto & n : pvec) {
+ _selectionChanged(n, selected);
+ }
+}
+
+void PathManipulator::_selectionChanged(SelectableControlPoint *p, bool selected)
+{
+ // don't do anything if we do not show handles
+ if (!_show_handles) return;
+
+ // only do something if a node changed selection state
+ Node *node = dynamic_cast<Node*>(p);
+ if (!node) return;
+
+ // update handle display
+ NodeList::iterator iters[5];
+ iters[2] = NodeList::get_iterator(node);
+ iters[1] = iters[2].prev();
+ iters[3] = iters[2].next();
+ if (selected) {
+ // selection - show handles on this node and adjacent ones
+ node->showHandles(true);
+ if (iters[1]) iters[1]->showHandles(true);
+ if (iters[3]) iters[3]->showHandles(true);
+ } else {
+ /* Deselection is more complex.
+ * The change might affect 3 nodes - this one and two adjacent.
+ * If the node and both its neighbors are deselected, hide handles.
+ * Otherwise, leave as is. */
+ if (iters[1]) iters[0] = iters[1].prev();
+ if (iters[3]) iters[4] = iters[3].next();
+ bool nodesel[5];
+ for (int i = 0; i < 5; ++i) {
+ nodesel[i] = iters[i] && iters[i]->selected();
+ }
+ for (int i = 1; i < 4; ++i) {
+ if (iters[i] && !nodesel[i-1] && !nodesel[i] && !nodesel[i+1]) {
+ iters[i]->showHandles(false);
+ }
+ }
+ }
+}
+
+/** Removes all nodes belonging to this manipulator from the control point selection */
+void PathManipulator::_removeNodesFromSelection()
+{
+ // remove this manipulator's nodes from selection
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ _selection.erase(j.get_pointer());
+ }
+ }
+}
+
+/** Update the XML representation and put the specified annotation on the undo stack */
+void PathManipulator::_commit(Glib::ustring const &annotation)
+{
+ writeXML();
+ if (_desktop) {
+ DocumentUndo::done(_desktop->getDocument(), annotation.data(), INKSCAPE_ICON("tool-node-editor"));
+ }
+}
+
+void PathManipulator::_commit(Glib::ustring const &annotation, gchar const *key)
+{
+ writeXML();
+ DocumentUndo::maybeDone(_desktop->getDocument(), key, annotation.data(), INKSCAPE_ICON("tool-node-editor"));
+}
+
+/** Update the position of the curve drag point such that it is over the nearest
+ * point of the path. */
+Geom::Coord PathManipulator::_updateDragPoint(Geom::Point const &evp)
+{
+ Geom::Coord dist = HUGE_VAL;
+
+ Geom::Affine to_desktop = _getTransform();
+ Geom::PathVector pv = _spcurve.get_pathvector();
+ std::optional<Geom::PathVectorTime> pvp =
+ pv.nearestTime(_desktop->w2d(evp) * to_desktop.inverse());
+ if (!pvp) return dist;
+ Geom::Point nearest_pt = _desktop->d2w(pv.pointAt(*pvp) * to_desktop);
+
+ double fracpart = pvp->t;
+ std::list<SubpathPtr>::iterator spi = _subpaths.begin();
+ for (unsigned i = 0; i < pvp->path_index; ++i, ++spi) {}
+ NodeList::iterator first = (*spi)->before(pvp->asPathTime());
+
+ dist = Geom::distance(evp, nearest_pt);
+
+ double stroke_tolerance = _getStrokeTolerance();
+ if (first && first.next() &&
+ fracpart != 0.0 &&
+ fracpart != 1.0 &&
+ dist < stroke_tolerance)
+ {
+ // stroke_tolerance is at least two.
+ int tolerance = std::max(2, (int)stroke_tolerance);
+ _dragpoint->setVisible(true);
+ _dragpoint->setPosition(_desktop->w2d(nearest_pt));
+ _dragpoint->setSize(2 * tolerance - 1);
+ _dragpoint->setTimeValue(fracpart);
+ _dragpoint->setIterator(first);
+ } else {
+ _dragpoint->setVisible(false);
+ }
+
+ return dist;
+}
+
+/// This is called on zoom change to update the direction arrows
+void PathManipulator::_updateOutlineOnZoomChange()
+{
+ if (_show_path_direction) _updateOutline();
+}
+
+/** Compute the radius from the edge of the path where clicks should initiate a curve drag
+ * or segment selection, in window coordinates. */
+double PathManipulator::_getStrokeTolerance()
+{
+ /* Stroke event tolerance is equal to half the stroke's width plus the global
+ * drag tolerance setting. */
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double ret = prefs->getIntLimited("/options/dragtolerance/value", 2, 0, 100);
+ if (_path && _path->style && !_path->style->stroke.isNone()) {
+ ret += _path->style->stroke_width.computed * 0.5
+ * _getTransform().descrim() // scale to desktop coords
+ * _desktop->current_zoom(); // == _d2w.descrim() - scale to window coords
+ }
+ return ret;
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ 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/ui/tool/path-manipulator.h b/src/ui/tool/path-manipulator.h
new file mode 100644
index 0000000..673424e
--- /dev/null
+++ b/src/ui/tool/path-manipulator.h
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Path manipulator - a component that edits a single path on-canvas
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_PATH_MANIPULATOR_H
+#define SEEN_UI_TOOL_PATH_MANIPULATOR_H
+
+#include <string>
+#include <memory>
+#include <2geom/pathvector.h>
+#include <2geom/path-sink.h>
+#include <2geom/affine.h>
+#include "ui/tool/node.h"
+#include "ui/tool/manipulator.h"
+#include "live_effects/lpe-bspline.h"
+#include "display/curve.h"
+
+struct SPCanvasItem;
+class SPCurve;
+class SPPath;
+
+namespace Inkscape {
+
+class CanvasItemBpath;
+
+namespace XML { class Node; }
+
+namespace UI {
+
+class PathManipulator;
+class ControlPointSelection;
+class PathManipulatorObserver;
+class CurveDragPoint;
+class PathCanvasGroups;
+class MultiPathManipulator;
+class Node;
+class Handle;
+
+struct PathSharedData {
+ NodeSharedData node_data;
+ Inkscape::CanvasItemGroup *outline_group;
+ Inkscape::CanvasItemGroup *dragpoint_group;
+};
+
+enum class NodeDeleteMode {
+ automatic, // try to preserve shape if deleted nodes do not form sharp corners
+ inverse_auto, // opposite of what automatic mode would do
+ curve_fit, // preserve shape
+ line_segment // do not preserve shape; delete nodes and connect subpaths with a line segment
+};
+
+/**
+ * Manipulator that edits a single path using nodes with handles.
+ * Currently only cubic bezier and linear segments are supported, but this might change
+ * some time in the future.
+ */
+class PathManipulator : public PointManipulator {
+public:
+ typedef SPPath *ItemType;
+
+ PathManipulator(MultiPathManipulator &mpm, SPObject *path, Geom::Affine const &edit_trans,
+ guint32 outline_color, Glib::ustring lpe_key);
+ ~PathManipulator() override;
+ bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override;
+
+ bool empty();
+ void writeXML();
+ void update(bool alert_LPE = false); // update display, but don't commit
+ void clear(); // remove all nodes from manipulator
+ SPObject *item() { return _path; }
+
+ void selectSubpaths();
+ void invertSelectionInSubpaths();
+
+ void insertNodeAtExtremum(ExtremumType extremum);
+ void insertNodes();
+ void insertNode(Geom::Point);
+ void insertNode(NodeList::iterator first, double t, bool take_selection);
+ void duplicateNodes();
+ void copySelectedPath(Geom::PathBuilder *builder);
+ void weldNodes(NodeList::iterator preserve_pos = NodeList::iterator());
+ void weldSegments();
+ void breakNodes();
+ void deleteNodes(NodeDeleteMode mode);
+ void deleteSegments();
+ void reverseSubpaths(bool selected_only);
+ void setSegmentType(SegmentType);
+
+ void scaleHandle(Node *n, int which, int dir, bool pixel);
+ void rotateHandle(Node *n, int which, int dir, bool pixel);
+
+ void showOutline(bool show);
+ void showHandles(bool show);
+ void showPathDirection(bool show);
+ void setLiveOutline(bool set);
+ void setLiveObjects(bool set);
+ void updateHandles();
+ void updatePath();
+ void setControlsTransform(Geom::Affine const &);
+ void hideDragPoint();
+ MultiPathManipulator &mpm() { return _multi_path_manipulator; }
+
+ NodeList::iterator subdivideSegment(NodeList::iterator after, double t);
+ NodeList::iterator extremeNode(NodeList::iterator origin, bool search_selected,
+ bool search_unselected, bool closest);
+
+ int _bsplineGetSteps() const;
+ // this is necessary for Tab-selection in MultiPathManipulator
+ SubpathList &subpathList() { return _subpaths; }
+
+ static bool is_item_type(void *item);
+private:
+ typedef NodeList Subpath;
+ typedef std::shared_ptr<NodeList> SubpathPtr;
+
+ void _createControlPointsFromGeometry();
+
+ void _recalculateIsBSpline();
+ bool _isBSpline() const;
+ double _bsplineHandlePosition(Handle *h, bool check_other = true);
+ Geom::Point _bsplineHandleReposition(Handle *h, bool check_other = true);
+ Geom::Point _bsplineHandleReposition(Handle *h, double pos);
+ void _createGeometryFromControlPoints(bool alert_LPE = false);
+ unsigned _deleteStretch(NodeList::iterator first, NodeList::iterator last, NodeDeleteMode mode);
+ std::string _createTypeString();
+ void _updateOutline();
+ //void _setOutline(Geom::PathVector const &);
+ void _getGeometry();
+ void _setGeometry();
+ Glib::ustring _nodetypesKey();
+ Inkscape::XML::Node *_getXMLNode();
+ Geom::Affine _getTransform() const;
+
+ void _selectionChangedM(std::vector<SelectableControlPoint *> pvec, bool selected);
+ void _selectionChanged(SelectableControlPoint * p, bool selected);
+ bool _nodeClicked(Node *, GdkEventButton *);
+ void _handleGrabbed();
+ bool _handleClicked(Handle *, GdkEventButton *);
+ void _handleUngrabbed();
+
+ void _externalChange(unsigned type);
+ void _removeNodesFromSelection();
+ void _commit(Glib::ustring const &annotation);
+ void _commit(Glib::ustring const &annotation, gchar const *key);
+ Geom::Coord _updateDragPoint(Geom::Point const &);
+ void _updateOutlineOnZoomChange();
+ double _getStrokeTolerance();
+ Handle *_chooseHandle(Node *n, int which);
+
+ SubpathList _subpaths;
+ MultiPathManipulator &_multi_path_manipulator;
+ SPObject *_path; ///< can be an SPPath or an Inkscape::LivePathEffect::Effect !!!
+ SPCurve _spcurve; // in item coordinates
+ CanvasItemPtr<Inkscape::CanvasItemBpath> _outline;
+ CurveDragPoint *_dragpoint; // an invisible control point hovering over curve
+ PathManipulatorObserver *_observer;
+ Geom::Affine _d2i_transform; ///< desktop-to-item transform
+ Geom::Affine _i2d_transform; ///< item-to-desktop transform, inverse of _d2i_transform
+ Geom::Affine _edit_transform; ///< additional transform to apply to editing controls
+ bool _show_handles = true;
+ bool _show_outline = false;
+ bool _show_path_direction = false;
+ bool _live_outline = true;
+ bool _live_objects = true;
+ bool _is_bspline = false;
+ Glib::ustring _lpe_key;
+
+ friend class PathManipulatorObserver;
+ friend class CurveDragPoint;
+ friend class Node;
+ friend class Handle;
+};
+
+} // namespace UI
+} // 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/ui/tool/selectable-control-point.cpp b/src/ui/tool/selectable-control-point.cpp
new file mode 100644
index 0000000..5e5d0b2
--- /dev/null
+++ b/src/ui/tool/selectable-control-point.cpp
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tool/selectable-control-point.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+
+namespace Inkscape {
+namespace UI {
+
+ControlPoint::ColorSet SelectableControlPoint::_default_scp_color_set = {
+ {0xffffff00, 0x01000000}, // normal fill, stroke
+ {0xff0000ff, 0x01000000}, // mouseover fill, stroke
+ {0x0000ffff, 0x01000000}, // clicked fill, stroke
+ //
+ {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected
+ {0xff000000, 0x000000ff} // clicked fill, stroke when selected
+};
+
+SelectableControlPoint::SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Inkscape::CanvasItemCtrlType type,
+ ControlPointSelection &sel,
+ ColorSet const &cset,
+ Inkscape::CanvasItemGroup *group)
+ : ControlPoint(d, initial_pos, anchor, type, cset, group)
+ , _selection(sel)
+{
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:SelectableControlPoint");
+ _selection.allPoints().insert(this);
+}
+
+SelectableControlPoint::SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ ControlPointSelection &sel,
+ ColorSet const &cset,
+ Inkscape::CanvasItemGroup *group)
+ : ControlPoint(d, initial_pos, anchor, pixbuf, cset, group)
+ , _selection (sel)
+{
+ _selection.allPoints().insert(this);
+}
+
+SelectableControlPoint::~SelectableControlPoint()
+{
+ _selection.erase(this);
+ _selection.allPoints().erase(this);
+}
+
+bool SelectableControlPoint::grabbed(GdkEventMotion *)
+{
+ // if a point is dragged while not selected, it should select itself
+ if (!selected()) {
+ _takeSelection();
+ }
+ _selection._pointGrabbed(this);
+ return false;
+}
+
+void SelectableControlPoint::dragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ _selection._pointDragged(new_pos, event);
+}
+
+void SelectableControlPoint::ungrabbed(GdkEventButton *)
+{
+ _selection._pointUngrabbed();
+}
+
+bool SelectableControlPoint::clicked(GdkEventButton *event)
+{
+ if (_selection._pointClicked(this, event))
+ return true;
+
+ if (event->button != 1) return false;
+ if (held_shift(*event)) {
+ if (selected()) {
+ _selection.erase(this);
+ } else {
+ _selection.insert(this);
+ }
+ } else {
+ _takeSelection();
+ }
+ return true;
+}
+
+void SelectableControlPoint::select(bool toselect)
+{
+ if (toselect) {
+ _selection.insert(this);
+ } else {
+ _selection.erase(this);
+ }
+}
+
+void SelectableControlPoint::_takeSelection()
+{
+ _selection.clear();
+ _selection.insert(this);
+}
+
+bool SelectableControlPoint::selected() const
+{
+ SelectableControlPoint *p = const_cast<SelectableControlPoint*>(this);
+ return _selection.find(p) != _selection.end();
+}
+
+void SelectableControlPoint::_setState(State state)
+{
+ if (!selected()) {
+ ControlPoint::_setState(state);
+ } else {
+ ColorEntry current = {0, 0};
+ ColorSet const &activeCset = (_isLurking()) ? invisible_cset : _cset;
+ switch (state) {
+ case STATE_NORMAL:
+ current = activeCset.selected_normal;
+ break;
+ case STATE_MOUSEOVER:
+ current = activeCset.selected_mouseover;
+ break;
+ case STATE_CLICKED:
+ current = activeCset.selected_clicked;
+ break;
+ }
+ _setColors(current);
+ _state = state;
+ }
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ 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/ui/tool/selectable-control-point.h b/src/ui/tool/selectable-control-point.h
new file mode 100644
index 0000000..d57dd50
--- /dev/null
+++ b/src/ui/tool/selectable-control-point.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_SELECTABLE_CONTROL_POINT_H
+#define SEEN_UI_TOOL_SELECTABLE_CONTROL_POINT_H
+
+#include "ui/tool/control-point.h"
+
+namespace Inkscape {
+namespace UI {
+
+class ControlPointSelection;
+
+/**
+ * Desktop-bound selectable control object.
+ */
+class SelectableControlPoint : public ControlPoint {
+public:
+
+ ~SelectableControlPoint() override;
+ bool selected() const;
+ void updateState() { _setState(_state); }
+ virtual Geom::Rect bounds() const {
+ return Geom::Rect(position(), position());
+ }
+ virtual void select(bool toselect);
+ friend class NodeList;
+
+
+protected:
+
+ SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Inkscape::CanvasItemCtrlType type,
+ ControlPointSelection &sel,
+ ColorSet const &cset = _default_scp_color_set,
+ Inkscape::CanvasItemGroup *group = nullptr);
+
+ SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ ControlPointSelection &sel,
+ ColorSet const &cset = _default_scp_color_set,
+ Inkscape::CanvasItemGroup *group = nullptr);
+
+ void _setState(State state) override;
+
+ void dragged(Geom::Point &new_pos, GdkEventMotion *event) override;
+ bool grabbed(GdkEventMotion *event) override;
+ void ungrabbed(GdkEventButton *event) override;
+ bool clicked(GdkEventButton *event) override;
+
+ ControlPointSelection &_selection;
+
+private:
+
+ void _takeSelection();
+
+ static ColorSet _default_scp_color_set;
+};
+
+} // namespace UI
+} // 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/ui/tool/shape-record.h b/src/ui/tool/shape-record.h
new file mode 100644
index 0000000..1f29453
--- /dev/null
+++ b/src/ui/tool/shape-record.h
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Structures that store data needed for shape editing which are not contained
+ * directly in the XML node
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_SHAPE_RECORD_H
+#define SEEN_UI_TOOL_SHAPE_RECORD_H
+
+#include <glibmm/ustring.h>
+#include <boost/operators.hpp>
+#include <2geom/affine.h>
+
+class SPItem;
+class SPObject;
+namespace Inkscape {
+namespace UI {
+
+/** Role of the shape in the drawing - affects outline display and color */
+enum ShapeRole {
+ SHAPE_ROLE_NORMAL,
+ SHAPE_ROLE_CLIPPING_PATH,
+ SHAPE_ROLE_MASK,
+ SHAPE_ROLE_LPE_PARAM // implies edit_original set to true in ShapeRecord
+};
+
+struct ShapeRecord :
+ public boost::totally_ordered<ShapeRecord>
+{
+ SPObject *object; // SP node for the edited shape could be a lpeoject invisible so we use a spobject
+ ShapeRole role;
+ Glib::ustring lpe_key; // name of LPE shape param being edited
+
+ Geom::Affine edit_transform; // how to transform controls - used for clipping paths, masks, and markers
+ double edit_rotation; // how to transform controls - used for markers
+
+ inline bool operator==(ShapeRecord const &o) const {
+ return object == o.object && lpe_key == o.lpe_key;
+ }
+ inline bool operator<(ShapeRecord const &o) const {
+ return object == o.object ? (lpe_key < o.lpe_key) : (object < o.object);
+ }
+};
+
+} // namespace UI
+} // 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/ui/tool/transform-handle-set.cpp b/src/ui/tool/transform-handle-set.cpp
new file mode 100644
index 0000000..875429a
--- /dev/null
+++ b/src/ui/tool/transform-handle-set.cpp
@@ -0,0 +1,827 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Affine transform handles component
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cmath>
+#include <algorithm>
+
+#include <glib/gi18n.h>
+
+#include <2geom/transforms.h>
+
+#include "control-point.h"
+#include "desktop.h"
+#include "pure-transform.h"
+#include "seltrans.h"
+#include "snap.h"
+
+#include "display/control/canvas-item-rect.h"
+
+#include "object/sp-namedview.h"
+
+#include "ui/tool/commit-events.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/node.h"
+#include "ui/tool/selectable-control-point.h"
+#include "ui/tool/transform-handle-set.h"
+#include "ui/tools/node-tool.h"
+
+
+GType sp_select_context_get_type();
+
+namespace Inkscape {
+namespace UI {
+
+namespace {
+
+SPAnchorType corner_to_anchor(unsigned c) {
+ switch (c % 4) {
+ case 0: return SP_ANCHOR_NE;
+ case 1: return SP_ANCHOR_NW;
+ case 2: return SP_ANCHOR_SW;
+ default: return SP_ANCHOR_SE;
+ }
+}
+
+SPAnchorType side_to_anchor(unsigned s) {
+ switch (s % 4) {
+ case 0: return SP_ANCHOR_N;
+ case 1: return SP_ANCHOR_W;
+ case 2: return SP_ANCHOR_S;
+ default: return SP_ANCHOR_E;
+ }
+}
+
+// TODO move those two functions into a common place
+double snap_angle(double a) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ double unit_angle = M_PI / snaps;
+ return CLAMP(unit_angle * round(a / unit_angle), -M_PI, M_PI);
+}
+
+double snap_increment_degrees() {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ return 180.0 / snaps;
+}
+
+} // anonymous namespace
+
+ControlPoint::ColorSet TransformHandle::thandle_cset = {
+ {0x000000ff, 0x000000ff}, // normal fill, stroke
+ {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke
+ {0x00ff66ff, 0x000000ff}, // clicked fill, stroke
+ //
+ {0x000000ff, 0x000000ff}, // normal fill, stroke when selected
+ {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke when selected
+ {0x00ff66ff, 0x000000ff} // clicked fill, stroke when selected
+};
+
+TransformHandle::TransformHandle(TransformHandleSet &th, SPAnchorType anchor, Inkscape::CanvasItemCtrlType type)
+ : ControlPoint(th._desktop, Geom::Point(), anchor, type, thandle_cset, th._transform_handle_group)
+ , _th(th)
+{
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:TransformHandle");
+ setVisible(false);
+}
+
+// TODO: This code is duplicated in seltrans.cpp; fix this!
+void TransformHandle::getNextClosestPoint(bool reverse)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/snapclosestonly/value", false)) {
+ if (!_all_snap_sources_sorted.empty()) {
+ if (reverse) { // Shift-tab will find a closer point
+ if (_all_snap_sources_iter == _all_snap_sources_sorted.begin()) {
+ _all_snap_sources_iter = _all_snap_sources_sorted.end();
+ }
+ --_all_snap_sources_iter;
+ } else { // Tab will find a point further away
+ ++_all_snap_sources_iter;
+ if (_all_snap_sources_iter == _all_snap_sources_sorted.end()) {
+ _all_snap_sources_iter = _all_snap_sources_sorted.begin();
+ }
+ }
+
+ _snap_points.clear();
+ _snap_points.push_back(*_all_snap_sources_iter);
+
+ // Show the updated snap source now; otherwise it won't be shown until the selection is being moved again
+ SnapManager &m = _desktop->namedview->snap_manager;
+ m.setup(_desktop);
+ m.displaySnapsource(*_all_snap_sources_iter);
+ m.unSetup();
+ }
+ }
+}
+
+bool TransformHandle::grabbed(GdkEventMotion *)
+{
+ _origin = position();
+ _last_transform.setIdentity();
+ startTransform();
+
+ _th._setActiveHandle(this);
+ _setLurking(true);
+ _setState(_state);
+
+ // Collect the snap-candidates, one for each selected node. These will be stored in the _snap_points vector.
+ Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(_th._desktop->event_context);
+ //ControlPointSelection *selection = nt->_selected_nodes.get();
+ ControlPointSelection* selection = nt->_selected_nodes;
+
+ selection->setOriginalPoints();
+ selection->getOriginalPoints(_snap_points);
+ selection->getUnselectedPoints(_unselected_points);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/snapclosestonly/value", false)) {
+ // Find the closest snap source candidate
+ _all_snap_sources_sorted = _snap_points;
+
+ // Calculate and store the distance to the reference point for each snap candidate point
+ for(auto & i : _all_snap_sources_sorted) {
+ i.setDistance(Geom::L2(i.getPoint() - _origin));
+ }
+
+ // Sort them ascending, using the distance calculated above as the single criteria
+ std::sort(_all_snap_sources_sorted.begin(), _all_snap_sources_sorted.end());
+
+ // Now get the closest snap source
+ _snap_points.clear();
+ if (!_all_snap_sources_sorted.empty()) {
+ _all_snap_sources_iter = _all_snap_sources_sorted.begin();
+ _snap_points.push_back(_all_snap_sources_sorted.front());
+ }
+ }
+
+ return false;
+}
+
+void TransformHandle::dragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ Geom::Affine t = computeTransform(new_pos, event);
+ // protect against degeneracies
+ if (t.isSingular()) return;
+ Geom::Affine incr = _last_transform.inverse() * t;
+ if (incr.isSingular()) return;
+ _th.signal_transform.emit(incr);
+ _last_transform = t;
+}
+
+void TransformHandle::ungrabbed(GdkEventButton *)
+{
+ _snap_points.clear();
+ _th._clearActiveHandle();
+ _setLurking(false);
+ _setState(_state);
+ endTransform();
+ _th.signal_commit.emit(getCommitEvent());
+
+ //updates the positions of the nodes
+ Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(_th._desktop->event_context);
+ ControlPointSelection* selection = nt->_selected_nodes;
+ selection->setOriginalPoints();
+}
+
+
+class ScaleHandle : public TransformHandle {
+public:
+ ScaleHandle(TransformHandleSet &th, SPAnchorType anchor, Inkscape::CanvasItemCtrlType type)
+ : TransformHandle(th, anchor, type)
+ {}
+protected:
+ Glib::ustring _getTip(unsigned state) const override {
+ if (state_held_control(state)) {
+ if (state_held_shift(state)) {
+ return C_("Transform handle tip",
+ "<b>Shift+Ctrl</b>: scale uniformly about the rotation center");
+ }
+ return C_("Transform handle tip", "<b>Ctrl:</b> scale uniformly");
+ }
+ if (state_held_shift(state)) {
+ if (state_held_alt(state)) {
+ return C_("Transform handle tip",
+ "<b>Shift+Alt</b>: scale using an integer ratio about the rotation center");
+ }
+ return C_("Transform handle tip", "<b>Shift</b>: scale from the rotation center");
+ }
+ if (state_held_alt(state)) {
+ return C_("Transform handle tip", "<b>Alt</b>: scale using an integer ratio");
+ }
+ return C_("Transform handle tip", "<b>Scale handle</b>: drag to scale the selection");
+ }
+
+ Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override {
+ return format_tip(C_("Transform handle tip",
+ "Scale by %.2f%% x %.2f%%"), _last_scale_x * 100, _last_scale_y * 100);
+ }
+
+ bool _hasDragTips() const override { return true; }
+
+ static double _last_scale_x, _last_scale_y;
+};
+double ScaleHandle::_last_scale_x = 1.0;
+double ScaleHandle::_last_scale_y = 1.0;
+
+/**
+ * Corner scaling handle for node transforms.
+ */
+class ScaleCornerHandle : public ScaleHandle {
+public:
+
+ ScaleCornerHandle(TransformHandleSet &th, unsigned corner, unsigned d_corner) :
+ ScaleHandle(th, corner_to_anchor(d_corner), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE),
+ _corner(corner)
+ {}
+
+protected:
+ void startTransform() override {
+ _sc_center = _th.rotationCenter();
+ _sc_opposite = _th.bounds().corner(_corner + 2);
+ _last_scale_x = _last_scale_y = 1.0;
+ }
+
+ Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override {
+ Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite;
+ Geom::Point vold = _origin - scc, vnew = new_pos - scc;
+ // avoid exploding the selection
+ if (Geom::are_near(vold[Geom::X], 0) || Geom::are_near(vold[Geom::Y], 0))
+ return Geom::identity();
+
+ Geom::Scale scale = Geom::Scale(vnew[Geom::X] / vold[Geom::X], vnew[Geom::Y] / vold[Geom::Y]);
+
+ if (held_alt(*event)) {
+ for (unsigned i = 0; i < 2; ++i) {
+ if (fabs(scale[i]) >= 1.0) {
+ scale[i] = round(scale[i]);
+ } else {
+ scale[i] = 1.0 / round(1.0 / MIN(scale[i],10));
+ }
+ }
+ } else {
+ SnapManager &m = _th._desktop->namedview->snap_manager;
+ m.setupIgnoreSelection(_th._desktop, true, &_unselected_points);
+
+ Inkscape::PureScale *ptr;
+ if (held_control(*event)) {
+ scale[0] = scale[1] = std::min(scale[0], scale[1]);
+ ptr = new Inkscape::PureScaleConstrained(Geom::Scale(scale[0], scale[1]), scc);
+ } else {
+ ptr = new Inkscape::PureScale(Geom::Scale(scale[0], scale[1]), scc, false);
+ }
+ m.snapTransformed(_snap_points, _origin, (*ptr));
+ m.unSetup();
+ if (ptr->best_snapped_point.getSnapped()) {
+ scale = ptr->getScaleSnapped();
+ }
+
+ delete ptr;
+ }
+
+ _last_scale_x = scale[0];
+ _last_scale_y = scale[1];
+ Geom::Affine t = Geom::Translate(-scc)
+ * Geom::Scale(scale[0], scale[1])
+ * Geom::Translate(scc);
+ return t;
+ }
+
+ CommitEvent getCommitEvent() override {
+ return _last_transform.isUniformScale()
+ ? COMMIT_MOUSE_SCALE_UNIFORM
+ : COMMIT_MOUSE_SCALE;
+ }
+
+private:
+
+ Geom::Point _sc_center;
+ Geom::Point _sc_opposite;
+ unsigned _corner;
+};
+
+/**
+ * Side scaling handle for node transforms.
+ */
+class ScaleSideHandle : public ScaleHandle {
+public:
+ ScaleSideHandle(TransformHandleSet &th, unsigned side, unsigned d_side)
+ : ScaleHandle(th, side_to_anchor(d_side), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE)
+ , _side(side)
+ {}
+protected:
+ void startTransform() override {
+ _sc_center = _th.rotationCenter();
+ Geom::Rect b = _th.bounds();
+ _sc_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3));
+ _last_scale_x = _last_scale_y = 1.0;
+ }
+ Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override {
+ Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite;
+ Geom::Point vs;
+ Geom::Dim2 d1 = static_cast<Geom::Dim2>((_side + 1) % 2);
+ Geom::Dim2 d2 = static_cast<Geom::Dim2>(_side % 2);
+
+ // avoid exploding the selection
+ if (Geom::are_near(scc[d1], _origin[d1]))
+ return Geom::identity();
+
+ vs[d1] = (new_pos - scc)[d1] / (_origin - scc)[d1];
+ if (held_alt(*event)) {
+ if (fabs(vs[d1]) >= 1.0) {
+ vs[d1] = round(vs[d1]);
+ } else {
+ vs[d1] = 1.0 / round(1.0 / MIN(vs[d1],10));
+ }
+ vs[d2] = 1.0;
+ } else {
+ SnapManager &m = _th._desktop->namedview->snap_manager;
+ m.setupIgnoreSelection(_th._desktop, true, &_unselected_points);
+
+ bool uniform = held_control(*event);
+ Inkscape::PureStretchConstrained psc = Inkscape::PureStretchConstrained(vs[d1], scc, d1, uniform);
+ m.snapTransformed(_snap_points, _origin, psc);
+ m.unSetup();
+
+ if (psc.best_snapped_point.getSnapped()) {
+ Geom::Point result = psc.getStretchSnapped().vector(); //best_snapped_point.getTransformation();
+ vs[d1] = result[d1];
+ vs[d2] = result[d2];
+ } else {
+ // on ctrl, apply uniform scaling instead of stretching
+ // Preserve aspect ratio, but never flip in the dimension not being edited (by using fabs())
+ vs[d2] = uniform ? fabs(vs[d1]) : 1.0;
+ }
+ }
+
+ _last_scale_x = vs[Geom::X];
+ _last_scale_y = vs[Geom::Y];
+ Geom::Affine t = Geom::Translate(-scc)
+ * Geom::Scale(vs)
+ * Geom::Translate(scc);
+ return t;
+ }
+ CommitEvent getCommitEvent() override {
+ return _last_transform.isUniformScale()
+ ? COMMIT_MOUSE_SCALE_UNIFORM
+ : COMMIT_MOUSE_SCALE;
+ }
+
+private:
+
+ Geom::Point _sc_center;
+ Geom::Point _sc_opposite;
+ unsigned _side;
+};
+
+/**
+ * Rotation handle for node transforms.
+ */
+class RotateHandle : public TransformHandle {
+public:
+ RotateHandle(TransformHandleSet &th, unsigned corner, unsigned d_corner)
+ : TransformHandle(th, corner_to_anchor(d_corner), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE)
+ , _corner(corner)
+ {}
+protected:
+
+ void startTransform() override {
+ _rot_center = _th.rotationCenter();
+ _rot_opposite = _th.bounds().corner(_corner + 2);
+ _last_angle = 0;
+ }
+
+ Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override
+ {
+ Geom::Point rotc = held_shift(*event) ? _rot_opposite : _rot_center;
+ double angle = Geom::angle_between(_origin - rotc, new_pos - rotc);
+ if (held_control(*event)) {
+ angle = snap_angle(angle);
+ } else {
+ SnapManager &m = _th._desktop->namedview->snap_manager;
+ m.setupIgnoreSelection(_th._desktop, true, &_unselected_points);
+ Inkscape::PureRotateConstrained prc = Inkscape::PureRotateConstrained(angle, rotc);
+ m.snapTransformed(_snap_points, _origin, prc);
+ m.unSetup();
+
+ if (prc.best_snapped_point.getSnapped()) {
+ angle = prc.getAngleSnapped(); //best_snapped_point.getTransformation()[0];
+ }
+ }
+
+ _last_angle = angle;
+ Geom::Affine t = Geom::Translate(-rotc)
+ * Geom::Rotate(angle)
+ * Geom::Translate(rotc);
+ return t;
+ }
+
+ CommitEvent getCommitEvent() override { return COMMIT_MOUSE_ROTATE; }
+
+ Glib::ustring _getTip(unsigned state) const override {
+ if (state_held_shift(state)) {
+ if (state_held_control(state)) {
+ return format_tip(C_("Transform handle tip",
+ "<b>Shift+Ctrl</b>: rotate around the opposite corner and snap "
+ "angle to %f° increments"), snap_increment_degrees());
+ }
+ return C_("Transform handle tip", "<b>Shift</b>: rotate around the opposite corner");
+ }
+ if (state_held_control(state)) {
+ return format_tip(C_("Transform handle tip",
+ "<b>Ctrl</b>: snap angle to %f° increments"), snap_increment_degrees());
+ }
+ return C_("Transform handle tip", "<b>Rotation handle</b>: drag to rotate "
+ "the selection around the rotation center");
+ }
+
+ Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override {
+ return format_tip(C_("Transform handle tip", "Rotate by %.2f°"),
+ _last_angle * 180.0 / M_PI);
+ }
+
+ bool _hasDragTips() const override { return true; }
+
+private:
+ Geom::Point _rot_center;
+ Geom::Point _rot_opposite;
+ unsigned _corner;
+ static double _last_angle;
+};
+double RotateHandle::_last_angle = 0;
+
+class SkewHandle : public TransformHandle {
+public:
+ SkewHandle(TransformHandleSet &th, unsigned side, unsigned d_side)
+ : TransformHandle(th, side_to_anchor(d_side), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW)
+ , _side(side)
+ {}
+
+protected:
+
+ void startTransform() override {
+ _skew_center = _th.rotationCenter();
+ Geom::Rect b = _th.bounds();
+ _skew_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3));
+ _last_angle = 0;
+ _last_horizontal = _side % 2;
+ }
+
+ Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override
+ {
+ Geom::Point scc = held_shift(*event) ? _skew_center : _skew_opposite;
+ Geom::Dim2 d1 = static_cast<Geom::Dim2>((_side + 1) % 2);
+ Geom::Dim2 d2 = static_cast<Geom::Dim2>(_side % 2);
+
+ Geom::Point const initial_delta = _origin - scc;
+
+ if (fabs(initial_delta[d1]) < 1e-15) {
+ return Geom::Affine();
+ }
+
+ // Calculate the scale factors, which can be either visual or geometric
+ // depending on which type of bbox is currently being used (see preferences -> selector tool)
+ Geom::Scale scale = calcScaleFactors(_origin, new_pos, scc, false);
+ Geom::Scale skew = calcScaleFactors(_origin, new_pos, scc, true);
+ scale[d2] = 1;
+ skew[d2] = 1;
+
+ // Skew handles allow scaling up to integer multiples of the original size
+ // in the second direction; prevent explosions
+
+ if (fabs(scale[d1]) < 1) {
+ // Prevent shrinking of the selected object, while allowing mirroring
+ scale[d1] = copysign(1.0, scale[d1]);
+ } else {
+ // Allow expanding of the selected object by integer multiples
+ scale[d1] = floor(scale[d1] + 0.5);
+ }
+
+ double angle = atan(skew[d1] / scale[d1]);
+
+ if (held_control(*event)) {
+ angle = snap_angle(angle);
+ skew[d1] = tan(angle) * scale[d1];
+ } else {
+ SnapManager &m = _th._desktop->namedview->snap_manager;
+ m.setupIgnoreSelection(_th._desktop, true, &_unselected_points);
+
+ Inkscape::PureSkewConstrained psc = Inkscape::PureSkewConstrained(skew[d1], scale[d1], scc, d2);
+ m.snapTransformed(_snap_points, _origin, psc);
+ m.unSetup();
+
+ if (psc.best_snapped_point.getSnapped()) {
+ skew[d1] = psc.getSkewSnapped(); //best_snapped_point.getTransformation()[0];
+ }
+ }
+
+ _last_angle = angle;
+
+ // Update the handle position
+ Geom::Point new_new_pos;
+ new_new_pos[d2] = initial_delta[d1] * skew[d1] + _origin[d2];
+ new_new_pos[d1] = initial_delta[d1] * scale[d1] + scc[d1];
+
+ // Calculate the relative affine
+ Geom::Affine relative_affine = Geom::identity();
+ relative_affine[2*d1 + d1] = (new_new_pos[d1] - scc[d1]) / initial_delta[d1];
+ relative_affine[2*d1 + (d2)] = (new_new_pos[d2] - _origin[d2]) / initial_delta[d1];
+ relative_affine[2*(d2) + (d1)] = 0;
+ relative_affine[2*(d2) + (d2)] = 1;
+
+ for (int i = 0; i < 2; i++) {
+ if (fabs(relative_affine[3*i]) < 1e-15) {
+ relative_affine[3*i] = 1e-15;
+ }
+ }
+
+ Geom::Affine t = Geom::Translate(-scc)
+ * relative_affine
+ * Geom::Translate(scc);
+
+ return t;
+ }
+
+ CommitEvent getCommitEvent() override {
+ return (_side % 2)
+ ? COMMIT_MOUSE_SKEW_Y
+ : COMMIT_MOUSE_SKEW_X;
+ }
+
+ Glib::ustring _getTip(unsigned state) const override {
+ if (state_held_shift(state)) {
+ if (state_held_control(state)) {
+ return format_tip(C_("Transform handle tip",
+ "<b>Shift+Ctrl</b>: skew about the rotation center with snapping "
+ "to %f° increments"), snap_increment_degrees());
+ }
+ return C_("Transform handle tip", "<b>Shift</b>: skew about the rotation center");
+ }
+ if (state_held_control(state)) {
+ return format_tip(C_("Transform handle tip",
+ "<b>Ctrl</b>: snap skew angle to %f° increments"), snap_increment_degrees());
+ }
+ return C_("Transform handle tip",
+ "<b>Skew handle</b>: drag to skew (shear) selection about "
+ "the opposite handle");
+ }
+
+ Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override {
+ if (_last_horizontal) {
+ return format_tip(C_("Transform handle tip", "Skew horizontally by %.2f°"),
+ _last_angle * 360.0);
+ } else {
+ return format_tip(C_("Transform handle tip", "Skew vertically by %.2f°"),
+ _last_angle * 360.0);
+ }
+ }
+
+ bool _hasDragTips() const override { return true; }
+
+private:
+
+ Geom::Point _skew_center;
+ Geom::Point _skew_opposite;
+ unsigned _side;
+ static bool _last_horizontal;
+ static double _last_angle;
+};
+bool SkewHandle::_last_horizontal = false;
+double SkewHandle::_last_angle = 0;
+
+class RotationCenter : public ControlPoint {
+
+public:
+ RotationCenter(TransformHandleSet &th) :
+ ControlPoint(th._desktop, Geom::Point(), SP_ANCHOR_CENTER,
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER,
+ _center_cset, th._transform_handle_group),
+ _th(th)
+ {
+ setVisible(false);
+ }
+
+protected:
+ void dragged(Geom::Point &new_pos, GdkEventMotion *event) override {
+ SnapManager &sm = _th._desktop->namedview->snap_manager;
+ sm.setup(_th._desktop);
+ bool snap = !held_shift(*event) && sm.someSnapperMightSnap();
+ if (held_control(*event)) {
+ // constrain to axes
+ Geom::Point origin = _last_drag_origin();
+ std::vector<Inkscape::Snapper::SnapConstraint> constraints;
+ constraints.emplace_back(origin, Geom::Point(1, 0));
+ constraints.emplace_back(origin, Geom::Point(0, 1));
+ new_pos = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos,
+ SNAPSOURCE_ROTATION_CENTER), constraints, held_shift(*event)).getPoint();
+ } else if (snap) {
+ sm.freeSnapReturnByRef(new_pos, SNAPSOURCE_ROTATION_CENTER);
+ }
+ sm.unSetup();
+ }
+ Glib::ustring _getTip(unsigned /*state*/) const override {
+ return C_("Transform handle tip",
+ "<b>Rotation center</b>: drag to change the origin of transforms");
+ }
+
+private:
+
+ static ColorSet _center_cset;
+ TransformHandleSet &_th;
+};
+
+ControlPoint::ColorSet RotationCenter::_center_cset = {
+ {0x000000ff, 0x000000ff}, // normal fill, stroke
+ {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke
+ {0x00ff66ff, 0x000000ff}, // clicked fill, stroke
+ //
+ {0x000000ff, 0x000000ff}, // normal fill, stroke when selected
+ {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke when selected
+ {0x00ff66ff, 0x000000ff} // clicked fill, stroke when selected
+};
+
+
+TransformHandleSet::TransformHandleSet(SPDesktop *d, Inkscape::CanvasItemGroup *th_group)
+ : Manipulator(d)
+ , _active(nullptr)
+ , _transform_handle_group(th_group)
+ , _mode(MODE_SCALE)
+ , _in_transform(false)
+ , _visible(true)
+{
+ _trans_outline = new Inkscape::CanvasItemRect(_desktop->getCanvasControls());
+ _trans_outline->set_name("CanvasItemRect:Transform");
+ _trans_outline->hide();
+ _trans_outline->set_dashed(true);
+
+ bool y_inverted = !d->is_yaxisdown();
+ for (unsigned i = 0; i < 4; ++i) {
+ unsigned d_c = y_inverted ? i : 3 - i;
+ unsigned d_s = y_inverted ? i : 6 - i;
+ _scale_corners[i] = new ScaleCornerHandle(*this, i, d_c);
+ _scale_sides[i] = new ScaleSideHandle(*this, i, d_s);
+ _rot_corners[i] = new RotateHandle(*this, i, d_c);
+ _skew_sides[i] = new SkewHandle(*this, i, d_s);
+ }
+ _center = new RotationCenter(*this);
+ // when transforming, update rotation center position
+ signal_transform.connect(sigc::mem_fun(*_center, &RotationCenter::transform));
+}
+
+TransformHandleSet::~TransformHandleSet()
+{
+ for (auto & _handle : _handles) {
+ delete _handle;
+ }
+}
+
+void TransformHandleSet::setMode(Mode m)
+{
+ _mode = m;
+ _updateVisibility(_visible);
+}
+
+Geom::Rect TransformHandleSet::bounds() const
+{
+ return Geom::Rect(*_scale_corners[0], *_scale_corners[2]);
+}
+
+ControlPoint const &TransformHandleSet::rotationCenter() const
+{
+ return *_center;
+}
+
+ControlPoint &TransformHandleSet::rotationCenter()
+{
+ return *_center;
+}
+
+void TransformHandleSet::setVisible(bool v)
+{
+ if (_visible != v) {
+ _visible = v;
+ _updateVisibility(_visible);
+ }
+}
+
+void TransformHandleSet::setBounds(Geom::Rect const &r, bool preserve_center)
+{
+ if (_in_transform) {
+ _trans_outline->set_rect(r);
+ } else {
+ for (unsigned i = 0; i < 4; ++i) {
+ _scale_corners[i]->move(r.corner(i));
+ _scale_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1)));
+ _rot_corners[i]->move(r.corner(i));
+ _skew_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1)));
+ }
+ if (!preserve_center) _center->move(r.midpoint());
+ if (_visible) _updateVisibility(true);
+ }
+}
+
+bool TransformHandleSet::event(Inkscape::UI::Tools::ToolBase *, GdkEvent*)
+{
+ return false;
+}
+
+void TransformHandleSet::_emitTransform(Geom::Affine const &t)
+{
+ signal_transform.emit(t);
+ _center->transform(t);
+}
+
+void TransformHandleSet::_setActiveHandle(ControlPoint *th)
+{
+ _active = th;
+ if (_in_transform)
+ throw std::logic_error("Transform initiated when another transform in progress");
+ _in_transform = true;
+ // hide all handles except the active one
+ _updateVisibility(false);
+ _trans_outline->show();
+}
+
+void TransformHandleSet::_clearActiveHandle()
+{
+ // This can only be called from handles, so they had to be visible before _setActiveHandle
+ _trans_outline->hide();
+ _active = nullptr;
+ _in_transform = false;
+ _updateVisibility(_visible);
+}
+
+void TransformHandleSet::_updateVisibility(bool v)
+{
+ if (v) {
+ Geom::Rect b = bounds();
+
+ // Roughly estimate handle size.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int handle_index = prefs->getIntLimited("/options/grabsize/value", 3, 1, 15);
+ int handle_size = handle_index * 2 + 1; // Handle pixmaps are actually larger but that's to allow space when handle is rotated.
+
+ Geom::Point bp = b.dimensions();
+
+ // do not scale when the bounding rectangle has zero width or height
+ bool show_scale = (_mode == MODE_SCALE) && !Geom::are_near(b.minExtent(), 0);
+ // do not rotate if the bounding rectangle is degenerate
+ bool show_rotate = (_mode == MODE_ROTATE_SKEW) && !Geom::are_near(b.maxExtent(), 0);
+ bool show_scale_side[2], show_skew[2];
+
+ // show sides if:
+ // a) there is enough space between corner handles, or
+ // b) corner handles are not shown, but side handles make sense
+ // this affects horizontal and vertical scale handles; skew handles never
+ // make sense if rotate handles are not shown
+ for (unsigned i = 0; i < 2; ++i) {
+ Geom::Dim2 d = static_cast<Geom::Dim2>(i);
+ Geom::Dim2 otherd = static_cast<Geom::Dim2>((i+1)%2);
+ show_scale_side[i] = (_mode == MODE_SCALE);
+ show_scale_side[i] &= (show_scale ? bp[d] >= handle_size
+ : !Geom::are_near(bp[otherd], 0));
+ show_skew[i] = (show_rotate && bp[d] >= handle_size
+ && !Geom::are_near(bp[otherd], 0));
+ }
+
+ for (unsigned i = 0; i < 4; ++i) {
+ _scale_corners[i]->setVisible(show_scale);
+ _rot_corners[i]->setVisible(show_rotate);
+ _scale_sides[i]->setVisible(show_scale_side[i%2]);
+ _skew_sides[i]->setVisible(show_skew[i%2]);
+ }
+
+ // show rotation center
+ _center->setVisible(show_rotate);
+ } else {
+ for (auto & _handle : _handles) {
+ if (_handle != _active)
+ _handle->setVisible(false);
+ }
+ }
+
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ 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/ui/tool/transform-handle-set.h b/src/ui/tool/transform-handle-set.h
new file mode 100644
index 0000000..8e0eede
--- /dev/null
+++ b/src/ui/tool/transform-handle-set.h
@@ -0,0 +1,147 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Affine transform handles component
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_TRANSFORM_HANDLE_SET_H
+#define SEEN_UI_TOOL_TRANSFORM_HANDLE_SET_H
+
+#include <memory>
+#include <gdk/gdk.h>
+#include <2geom/forward.h>
+#include "ui/tool/commit-events.h"
+#include "ui/tool/manipulator.h"
+#include "ui/tool/control-point.h"
+#include "enums.h"
+#include "snap-candidate.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+
+class CanvasItemGroup;
+class CanvasItemRect;
+
+namespace UI {
+
+class RotateHandle;
+class SkewHandle;
+class ScaleCornerHandle;
+class ScaleSideHandle;
+class RotationCenter;
+
+class TransformHandleSet : public Manipulator {
+public:
+
+ enum Mode {
+ MODE_SCALE,
+ MODE_ROTATE_SKEW
+ };
+
+ TransformHandleSet(SPDesktop *d, Inkscape::CanvasItemGroup *th_group);
+ ~TransformHandleSet() override;
+ bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override;
+
+ bool visible() const { return _visible; }
+ Mode mode() const { return _mode; }
+ Geom::Rect bounds() const;
+ void setVisible(bool v);
+
+ /** Sets the mode of transform handles (scale or rotate). */
+ void setMode(Mode m);
+
+ void setBounds(Geom::Rect const &, bool preserve_center = false);
+
+ bool transforming() { return _in_transform; }
+
+ ControlPoint const &rotationCenter() const;
+ ControlPoint &rotationCenter();
+
+ sigc::signal<void (Geom::Affine const &)> signal_transform;
+ sigc::signal<void (CommitEvent)> signal_commit;
+
+private:
+
+ void _emitTransform(Geom::Affine const &);
+ void _setActiveHandle(ControlPoint *h);
+ void _clearActiveHandle();
+
+ /** Update the visibility of transformation handles according to settings and the dimensions
+ * of the bounding box. It hides the handles that would have no effect or lead to
+ * discontinuities. Additionally, side handles for which there is no space are not shown.
+ */
+ void _updateVisibility(bool v);
+
+ // TODO unions must GO AWAY:
+ union {
+ ControlPoint *_handles[17];
+ struct {
+ ScaleCornerHandle *_scale_corners[4];
+ ScaleSideHandle *_scale_sides[4];
+ RotateHandle *_rot_corners[4];
+ SkewHandle *_skew_sides[4];
+ RotationCenter *_center;
+ };
+ };
+
+ ControlPoint *_active;
+ Inkscape::CanvasItemGroup *_transform_handle_group;
+ Inkscape::CanvasItemRect *_trans_outline;
+ Mode _mode;
+ bool _in_transform;
+ bool _visible;
+ friend class TransformHandle;
+ friend class RotationCenter;
+};
+
+/** Base class for node transform handles to simplify implementation. */
+class TransformHandle : public ControlPoint
+{
+public:
+ TransformHandle(TransformHandleSet &th, SPAnchorType anchor, Inkscape::CanvasItemCtrlType type);
+ void getNextClosestPoint(bool reverse);
+
+protected:
+ virtual void startTransform() {}
+ virtual void endTransform() {}
+ virtual Geom::Affine computeTransform(Geom::Point const &pos, GdkEventMotion *event) = 0;
+ virtual CommitEvent getCommitEvent() = 0;
+
+ Geom::Affine _last_transform;
+ Geom::Point _origin;
+ TransformHandleSet &_th;
+ std::vector<Inkscape::SnapCandidatePoint> _snap_points;
+ std::vector<Inkscape::SnapCandidatePoint> _unselected_points;
+ std::vector<Inkscape::SnapCandidatePoint> _all_snap_sources_sorted;
+ std::vector<Inkscape::SnapCandidatePoint>::iterator _all_snap_sources_iter;
+
+private:
+ bool grabbed(GdkEventMotion *) override;
+ void dragged(Geom::Point &new_pos, GdkEventMotion *event) override;
+ void ungrabbed(GdkEventButton *) override;
+
+ static ColorSet thandle_cset;
+};
+
+} // namespace UI
+} // 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 :