From cca66b9ec4e494c1d919bff0f71a820d8afab1fa Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 20:24:48 +0200 Subject: Adding upstream version 1.2.2. Signed-off-by: Daniel Baumann --- src/ui/tool/path-manipulator.cpp | 1804 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1804 insertions(+) create mode 100644 src/ui/tool/path-manipulator.cpp (limited to 'src/ui/tool/path-manipulator.cpp') diff --git a/src/ui/tool/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp new file mode 100644 index 0000000..7f30633 --- /dev/null +++ b/src/ui/tool/path-manipulator.cpp @@ -0,0 +1,1804 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Path manipulator - implementation. + */ +/* Authors: + * Krzysztof KosiƄski + * 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 + +#include "display/curve.h" +#include "display/control/canvas-item-bpath.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/path-manipulator.h" +#include "ui/tools/node-tool.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 HANDLE_CUBIC_GAP = 0.001; +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) + , _spcurve(new SPCurve()) + , _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)) +{ + LivePathEffectObject *lpeobj = dynamic_cast(_path); + SPPath *pathshadow = dynamic_cast(_path); + if (!lpeobj) { + _i2d_transform = pathshadow->i2dt_affine(); + } else { + _i2d_transform = Geom::identity(); + } + _d2i_transform = _i2d_transform.inverse(); + _dragpoint->setVisible(false); + + _getGeometry(); + + _outline = new 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))); + + _createControlPointsFromGeometry(); + //Define if the path is BSpline on construction + _recalculateIsBSpline(); +} + +PathManipulator::~PathManipulator() +{ + delete _dragpoint; + delete _observer; + delete _outline; + 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 > &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 > 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 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 || dynamic_cast(_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(bool 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; + } +} + +/** + * 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, bool keep_shape) +{ + 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; + + // set surrounding node types to cusp if: + // 1. keep_shape is on, or + // 2. we are deleting at the end or beginning of an open path + if ((keep_shape || !end) && start.prev()) start.prev()->setType(NODE_CUSP, false); + if ((keep_shape || !start.prev()) && end) end->setType(NODE_CUSP, false); + + if (keep_shape && start.prev() && end) { + unsigned num_samples = (del_len + 1) * samples_per_segment + 1; + Geom::Point *bezier_data = new Geom::Point[num_samples]; + Geom::Point result[4]; + unsigned seg = 0; + + for (NodeList::iterator cur = start.prev(); cur != end; cur = cur.next()) { + Geom::CubicBezier bc(*cur, *cur->front(), *cur.next(), *cur.next()->back()); + for (unsigned s = 0; s < samples_per_segment; ++s) { + bezier_data[seg * samples_per_segment + s] = bc.pointAt(t_step * s); + } + ++seg; + } + // Fill last point + bezier_data[num_samples - 1] = end->position(); + // Compute replacement bezier curve + // TODO the fitting algorithm sucks - rewrite it to be awesome + bezier_fit_cubic(result, bezier_data, num_samples, 0.5); + delete[] bezier_data; + + 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(_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 div = temp.subdivide(t); + std::vector 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; + std::unique_ptr line_inside_nodes(new SPCurve()); + 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); + next = Geom::Point(next[Geom::X] + HANDLE_CUBIC_GAP,next[Geom::Y] + HANDLE_CUBIC_GAP); + 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); + previous = Geom::Point(previous[Geom::X] + HANDLE_CUBIC_GAP,previous[Geom::Y] + HANDLE_CUBIC_GAP); + 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 should be specialized so that it takes only 1 bit per value + std::vector 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: { + SPPath *path = dynamic_cast(_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; + } +} + +/** 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 = 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->set_pathvector(pathv); + + pathv *= (_edit_transform * _i2d_transform); + + // 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(&*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; + + SPLPEItem * path = dynamic_cast(_path); + if (path){ + if(path->hasPathEffect()){ + Inkscape::LivePathEffect::Effect const *this_effect = + path->getFirstPathEffectOfType(Inkscape::LivePathEffect::BSPLINE); + if(this_effect){ + lpe_bsp = dynamic_cast(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(){ + SPPath *path = dynamic_cast(_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){ + std::unique_ptr line_inside_nodes(new SPCurve()); + line_inside_nodes->moveto(n->position()); + line_inside_nodes->lineto(next_node->position()); + if(!are_near(h->position(), n->position())){ + pos = Geom::nearest_time(Geom::Point(h->position()[X] - HANDLE_CUBIC_GAP, h->position()[Y] - HANDLE_CUBIC_GAP), *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; + std::unique_ptr line_inside_nodes(new SPCurve()); + 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); + ret = Geom::Point(ret[X] + HANDLE_CUBIC_GAP, ret[Y] + HANDLE_CUBIC_GAP); + }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::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() * (_edit_transform * _i2d_transform).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->set_pathvector(pathv); + if (alert_LPE) { + /// \todo note that _path can be an Inkscape::LivePathEffect::Effect* too, kind of confusing, rework member naming? + SPPath *path = dynamic_cast(_path); + if (path && path->hasPathEffect()) { + Inkscape::LivePathEffect::Effect *this_effect = + path->getFirstPathEffectOfType(Inkscape::LivePathEffect::POWERSTROKE); + if(this_effect){ + LivePathEffect::LPEPowerStroke *lpe_pwr = dynamic_cast(this_effect->getLPEObj()->get_lpe()); + if (lpe_pwr) { + lpe_pwr->adjustForNewPath(pathv); + } + } + } + } + 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; + } + + Geom::PathVector pv = _spcurve->get_pathvector(); + pv *= (_edit_transform * _i2d_transform); + // This SPCurve thing has to be killed with extreme prejudice + auto _hc = std::make_unique(); + 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(arrow_end); + arrows.push_back(arrow); + } + } + pv.insert(pv.end(), arrows.begin(), arrows.end()); + } + _hc->set_pathvector(pv); + _outline->set_bpath(_hc.get()); + _outline->show(); +} + +/** Retrieve the geometry of the edited object from the object tree */ +void PathManipulator::_getGeometry() +{ + using namespace Inkscape::LivePathEffect; + LivePathEffectObject *lpeobj = dynamic_cast(_path); + SPPath *path = dynamic_cast(_path); + if (lpeobj) { + Effect *lpe = lpeobj->get_lpe(); + if (lpe) { + PathParam *pathparam = dynamic_cast(lpe->getParameter(_lpe_key.data())); + _spcurve.reset(new SPCurve(pathparam->get_pathvector())); + } + } else if (path) { + _spcurve = SPCurve::copy(path->curveForEdit()); + // never allow NULL to sneak in here! + if (_spcurve == nullptr) { + _spcurve.reset(new 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; + LivePathEffectObject *lpeobj = dynamic_cast(_path); + SPPath *path = dynamic_cast(_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(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.get()); + if (!path->hasPathEffectOfTypeRecursive(Inkscape::LivePathEffect::SLICE)) { + sp_lpe_item_update_patheffect(path, true, false); + } else { + path->setCurve(_spcurve.get()); + } + } else { + path->setCurve(_spcurve.get()); + } + } +} + +/** Figure out in what attribute to store the nodetype string. */ +Glib::ustring PathManipulator::_nodetypesKey() +{ + LivePathEffectObject *lpeobj = dynamic_cast(_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. + LivePathEffectObject *lpeobj = dynamic_cast(_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(), true); + } + + 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((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 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(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 = _edit_transform * _i2d_transform; + Geom::PathVector pv = _spcurve->get_pathvector(); + std::optional 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::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 + * (_edit_transform * _i2d_transform).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 : -- cgit v1.2.3