diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
commit | c853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch) | |
tree | 7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/ui/tool | |
parent | Initial commit. (diff) | |
download | inkscape-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')
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 : |