From 03ed24edc88ac56afbd596deb205c078a07426bd Mon Sep 17 00:00:00 2001 From: Rafael Siejakowski Date: Sun, 27 Oct 2024 20:50:31 +0100 Subject: [PATCH 01/13] Refactoring: pathvector cleaning to a function Remove duplicated code whose task was to sanitize a PathVector for use in the Node Tool. --- src/ui/tool/path-manipulator.cpp | 39 ++++++++++++++------------------ 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/ui/tool/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp index 4ce04aa5ae..a8114b1400 100644 --- a/src/ui/tool/path-manipulator.cpp +++ b/src/ui/tool/path-manipulator.cpp @@ -53,6 +53,18 @@ enum PathChange { PATH_CHANGE_TRANSFORM }; +/// Remove empty paths from a path vector. +void sanitize_path_vector(Geom::PathVector &pathvector) +{ + for (auto it = pathvector.begin(); it != pathvector.end();) { + if (it->empty()) { + it = pathvector.erase(it); + } else { + ++it; + } + } +} + } // anonymous namespace static constexpr double BSPLINE_TOL = 0.001; static constexpr double NO_POWER = 0.0; @@ -1265,24 +1277,16 @@ 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; - } - } + + // sanitize pathvector and store it in SPCurve, + // so that _updateDragPoint doesn't crash on paths with naked movetos + sanitize_path_vector(pathv); if (pathv.empty()) { return; } @@ -1486,16 +1490,7 @@ void PathManipulator::_createGeometryFromControlPoints(bool alert_LPE) } 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; - } - } + sanitize_path_vector(pathv); if (pathv.empty()) { return; } -- GitLab From 532446c2a2e6f55a66b0165034580112a0828132 Mon Sep 17 00:00:00 2001 From: Rafael Siejakowski Date: Sun, 27 Oct 2024 19:48:30 +0100 Subject: [PATCH 02/13] Minimally usable version of elliptical arc handles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the Node Tool, an elliptical arc inside a pre-existing path can now be manipulated without being converted into a Bézier curve. Supported operations: - Dragging a point on the arc to sculpt it - Double-clicking on the arc subdivides it - Dragging an endpoint adjusts the arc by rotation and uniform scaling (preserves the aspect ratio). Limitations and unsupported operations: - Arc controller point to be implemented - Ellipse outline is not shown - Changing node type does not work for endpoints of elliptical arcs - A button to be added to convert nodes to arc controllers - A button for changing segment type to elliptical arc to be added - No support for keyboard modifiers. These limitations will be addressed in future work as discussed with Adam Belis. This is the first step to implementing https://gitlab.com/inkscape/ux/-/issues/206 Co-authored-by: Adam Belis --- po/POTFILES.src.in | 2 + share/themes | 2 +- src/ui/CMakeLists.txt | 9 ++ src/ui/tool/bezier-curve-handler.cpp | 152 ++++++++++++++++++++ src/ui/tool/bezier-curve-handler.h | 50 +++++++ src/ui/tool/control-point.cpp | 2 +- src/ui/tool/control-point.h | 8 +- src/ui/tool/curve-drag-point.cpp | 144 +++++++------------ src/ui/tool/curve-drag-point.h | 9 +- src/ui/tool/curve-event-handler.h | 78 +++++++++++ src/ui/tool/elliptical-arc-end-node.cpp | 95 +++++++++++++ src/ui/tool/elliptical-arc-end-node.h | 76 ++++++++++ src/ui/tool/elliptical-arc-handler.cpp | 175 ++++++++++++++++++++++++ src/ui/tool/elliptical-arc-handler.h | 53 +++++++ src/ui/tool/elliptical-manipulator.cpp | 121 ++++++++++++++++ src/ui/tool/elliptical-manipulator.h | 81 +++++++++++ src/ui/tool/multi-path-manipulator.cpp | 2 +- src/ui/tool/node.cpp | 44 ++++-- src/ui/tool/node.h | 23 +++- src/ui/tool/path-manipulator.cpp | 90 ++++++------ 20 files changed, 1056 insertions(+), 160 deletions(-) create mode 100644 src/ui/tool/bezier-curve-handler.cpp create mode 100644 src/ui/tool/bezier-curve-handler.h create mode 100644 src/ui/tool/curve-event-handler.h create mode 100644 src/ui/tool/elliptical-arc-end-node.cpp create mode 100644 src/ui/tool/elliptical-arc-end-node.h create mode 100644 src/ui/tool/elliptical-arc-handler.cpp create mode 100644 src/ui/tool/elliptical-arc-handler.h create mode 100644 src/ui/tool/elliptical-manipulator.cpp create mode 100644 src/ui/tool/elliptical-manipulator.h diff --git a/po/POTFILES.src.in b/po/POTFILES.src.in index 50e24abeea..7d480f4d01 100644 --- a/po/POTFILES.src.in +++ b/po/POTFILES.src.in @@ -338,7 +338,9 @@ ${_build_dir}/share/templates/templates.h ../src/ui/modifiers.cpp ../src/ui/shape-editor-knotholders.cpp ../src/ui/shortcuts.cpp +../src/ui/tool/bezier-curve-handler.cpp ../src/ui/tool/curve-drag-point.cpp +../src/ui/tool/elliptical-arc-handler.cpp ../src/ui/tool/multi-path-manipulator.cpp ../src/ui/tool/node.cpp ../src/ui/tool/path-manipulator.cpp diff --git a/share/themes b/share/themes index 0a8234085a..2fc6ece138 160000 --- a/share/themes +++ b/share/themes @@ -1 +1 @@ -Subproject commit 0a8234085a2f55aabd02a0afb60c035823ef46a5 +Subproject commit 2fc6ece138323f905c9b475c3bcdef0d007eb233 diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index bb6c07e290..3dbd01746d 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -36,9 +36,13 @@ set(ui_SRC knot/knot-holder-entity.cpp knot/knot-ptr.cpp + tool/bezier-curve-handler.cpp tool/control-point-selection.cpp tool/control-point.cpp tool/curve-drag-point.cpp + tool/elliptical-arc-end-node.cpp + tool/elliptical-arc-handler.cpp + tool/elliptical-manipulator.cpp tool/modifier-tracker.cpp tool/multi-path-manipulator.cpp tool/node.cpp @@ -399,10 +403,15 @@ set(ui_SRC knot/knot-holder-entity.h knot/knot-ptr.h + tool/bezier-curve-handler.h tool/commit-events.h tool/control-point-selection.h tool/control-point.h tool/curve-drag-point.h + tool/curve-event-handler.h + tool/elliptical-arc-end-node.h + tool/elliptical-arc-handler.h + tool/elliptical-manipulator.h tool/manipulator.h tool/modifier-tracker.h tool/multi-path-manipulator.h diff --git a/src/ui/tool/bezier-curve-handler.cpp b/src/ui/tool/bezier-curve-handler.cpp new file mode 100644 index 0000000000..5cb647f712 --- /dev/null +++ b/src/ui/tool/bezier-curve-handler.cpp @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof Kosiński + * Jon A. Cruz + * Rafał M. Siejakowski + * + * Copyright (C) 2009-2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tool/bezier-curve-handler.h" + +#include <2geom/math-utils.h> +#include + +#include "ui/tool/control-point.h" +#include "ui/tool/node.h" +#include "ui/widget/events/canvas-event.h" + +namespace { + +/** Compute the "weight" describing how the influence of the drag should be distributed + * between the handles; 0 = front handle only, 1 = back handle only. + */ +double compute_bezier_drag_weight_for_time(double t) +{ + if (t <= 1.0 / 6.0) { + return 0.0; + } + if (t <= 0.5) { + return Geom::cube((6 * t - 1) / 2.0) / 2; + } + if (t <= 5.0 / 6.0) { + return (1 - Geom::cube((6 * (1 - t) - 1) / 2.0)) / 2 + 0.5; + } + return 1.0; +} +} // namespace + +namespace Inkscape::UI { + +bool BezierCurveHandler::pointGrabbed(NodeList::iterator curve_start, NodeList::iterator curve_end) +{ + // move the handles to 1/3 the length of the segment for line segments + auto *initial_handle = curve_start->front(); + auto *final_handle = curve_end->back(); + + if (initial_handle->isDegenerate() && final_handle->isDegenerate()) { + _segment_degenerate_at_drag_start = true; + + // delta is a vector equal 1/3 of the way between endpoint nodes + Geom::Point const delta = (curve_end->position() - curve_start->position()) / 3.0; + + if (!_is_bspline) { + initial_handle->move(initial_handle->position() + delta); + final_handle->move(final_handle->position() - delta); + return true; + } + return false; + } + + _segment_degenerate_at_drag_start = false; + return false; +} + +bool BezierCurveHandler::pointDragged(NodeList::iterator curve_start, NodeList::iterator curve_end, double curve_time, + Geom::Point const &drag_origin, Geom::Point const &drag_destination, + MotionEvent const &event) +{ + // special cancel handling - retract handles when if the segment was degenerate + if (ControlPoint::is_drag_cancelled(event) && _segment_degenerate_at_drag_start) { + curve_start->front()->retract(); + curve_end->back()->retract(); + return true; + } + + double const &t = curve_time; + double const weight = compute_bezier_drag_weight_for_time(t); + auto const delta = drag_destination - drag_origin; + + auto *initial_handle = curve_start->front(); + auto *final_handle = curve_end->back(); + + // Magic Bezier Drag Equations follow! + if (!_is_bspline) { + auto const offset0 = ((1 - weight) / (3 * t * (1 - t) * (1 - t))) * delta; + auto const offset1 = (weight / (3 * t * t * (1 - t))) * delta; + + initial_handle->move(initial_handle->position() + offset0); + final_handle->move(final_handle->position() + offset1); + return true; + } + + if (weight >= 0.8) { + if (held_shift(event)) { + final_handle->move(drag_destination); + } else { + curve_end->move(curve_end->position() + delta); + } + } else if (weight <= 0.2) { + if (held_shift(event)) { + initial_handle->move(drag_destination); + } else { + curve_start->move(curve_start->position() + delta); + } + } else { + curve_start->move(curve_start->position() + delta); + curve_end->move(curve_end->position() + delta); + } + return true; +} + +Glib::ustring BezierCurveHandler::getTooltip(unsigned event_state, NodeList::iterator curve_start) +{ + if (state_held_shift(event_state) && _is_bspline) { + return C_("Path segment tip", "Shift: drag to open or move BSpline handles"); + } + if (state_held_shift(event_state)) { + return C_("Path segment tip", "Shift: click to toggle segment selection"); + } + if (state_held_ctrl(event_state) && state_held_alt(event_state)) { + return C_("Path segment tip", "Ctrl+Alt: click to insert a node"); + } + if (state_held_alt(event_state)) { + return C_("Path segment tip", "Alt: double click to change line type"); + } + if (_is_bspline) { + return C_("Path segment tip", "BSpline segment: drag to shape the segment, doubleclick to insert node, " + "click to select (more: Alt, Shift, Ctrl+Alt)"); + } + + bool const linear = curve_start->front()->isDegenerate() && curve_start.next()->back()->isDegenerate(); + if (linear) { + return C_("Path segment tip", "Linear segment: drag to convert to a Bezier segment, " + "doubleclick to insert node, click to select (more: Alt, Shift, Ctrl+Alt)"); + } + + return C_("Path segment tip", "Bezier segment: drag to shape the segment, doubleclick to insert node, " + "click to select (more: Alt, Shift, Ctrl+Alt)"); +} +} // namespace Inkscape::UI + +/* + 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/bezier-curve-handler.h b/src/ui/tool/bezier-curve-handler.h new file mode 100644 index 0000000000..1b8ebe58f5 --- /dev/null +++ b/src/ui/tool/bezier-curve-handler.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_TOOL_BEZIER_CURVE_HANDLER_H +#define INKSCAPE_UI_TOOL_BEZIER_CURVE_HANDLER_H + +#include "ui/tool/curve-event-handler.h" + +namespace Inkscape::UI { + +class BezierCurveHandler : public CurveHandler +{ +public: + explicit BezierCurveHandler(bool is_bspline) + : _is_bspline{is_bspline} + {} + + ~BezierCurveHandler() override = default; + + bool pointGrabbed(NodeList::iterator curve_start, NodeList::iterator curve_end) override; + + bool pointDragged(NodeList::iterator curve_start, NodeList::iterator curve_end, double curve_time, + Geom::Point const &drag_origin, Geom::Point const &drag_destination, + MotionEvent const &event) override; + + Glib::ustring getTooltip(unsigned event_state, NodeList::iterator curve_start) override; + +private: + bool _is_bspline = false; + bool _segment_degenerate_at_drag_start = false; +}; +} // namespace Inkscape::UI + +#endif // INKSCAPE_UI_TOOL_BEZIER_CURVE_HANDLER_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/control-point.cpp b/src/ui/tool/control-point.cpp index cfcb252891..5c876ea2e8 100644 --- a/src/ui/tool/control-point.cpp +++ b/src/ui/tool/control-point.cpp @@ -440,7 +440,7 @@ void ControlPoint::_handleControlStyling() _canvas_item_ctrl->set_size_default(); } -bool ControlPoint::_is_drag_cancelled(MotionEvent const &event) +bool ControlPoint::is_drag_cancelled(MotionEvent const &event) { return event.control_point_synthesized; } diff --git a/src/ui/tool/control-point.h b/src/ui/tool/control-point.h index 43ef623c1a..540c80c57a 100644 --- a/src/ui/tool/control-point.h +++ b/src/ui/tool/control-point.h @@ -190,6 +190,10 @@ public: // make handle look like "selected" one without participating in selection void set_selected_appearance(bool selected); + static bool is_drag_cancelled(MotionEvent const &event); + + static bool is_drag_initiated() { return _drag_initiated; } + protected: /** * Create a regular control point. @@ -278,9 +282,6 @@ protected: 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(MotionEvent const &event); - - static bool _drag_initiated; private: static void _setMouseover(ControlPoint *, unsigned state); @@ -302,6 +303,7 @@ private: /** Stores the desktop point from which the last drag was initiated. */ static Geom::Point _drag_origin; static bool _event_grab; + static bool _drag_initiated; bool _double_clicked = false; bool _selected_appearance = false; diff --git a/src/ui/tool/curve-drag-point.cpp b/src/ui/tool/curve-drag-point.cpp index dcf544e7f0..fd430e5746 100644 --- a/src/ui/tool/curve-drag-point.cpp +++ b/src/ui/tool/curve-drag-point.cpp @@ -8,15 +8,17 @@ */ #include "ui/tool/curve-drag-point.h" + #include + #include "desktop.h" +#include "object/sp-namedview.h" +#include "ui/modifiers.h" #include "ui/tool/control-point-selection.h" #include "ui/tool/multi-path-manipulator.h" #include "ui/tool/path-manipulator.h" #include "ui/widget/events/canvas-event.h" -#include "object/sp-namedview.h" - namespace Inkscape { namespace UI { @@ -44,86 +46,69 @@ bool CurveDragPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, return ControlPoint::_eventHandler(event_context, event); } +void CurveDragPoint::setIterator(NodeList::iterator iterator) +{ + if (iterator == NodeList::iterator()) { + _curve_event_handler.reset(); + return; + } + if (iterator == first) { + return; + } + first = iterator; + _curve_event_handler.reset(); + if (!first) { + return; + } + + if (auto end_node = first.next()) { + _curve_event_handler = end_node->createEventHandlerForPrecedingCurve(); + } +} + bool CurveDragPoint::grabbed(MotionEvent const &/*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; + if (_curve_event_handler) { + _curve_event_handler->pointGrabbed(first, second); } return false; } +void CurveDragPoint::_adjustPointToSnappedPosition(Geom::Point &point_to_adjust, CanvasEvent const &event, + SPItem const &item_to_ignore) const +{ + if (!is_drag_initiated()) { + return; + } + if (auto const *snap_inhibitor = Modifiers::Modifier::get(Modifiers::Type::MOVE_SNAPPING); + snap_inhibitor && snap_inhibitor->active(event.modifiers)) { + return; + } + + auto &snap_manager = _desktop->getNamedView()->snap_manager; + snap_manager.setup(_desktop, true, &item_to_ignore); + const SnapCandidatePoint candidate_point{point_to_adjust, Inkscape::SNAPSOURCE_OTHER_HANDLE}; + point_to_adjust = snap_manager.freeSnap(candidate_point, {}, false).getPoint(); + snap_manager.unSetup(); +} + void CurveDragPoint::dragged(Geom::Point &new_pos, MotionEvent const &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(); + const auto *path_item = cast(_pm._path); + if (!_curve_event_handler || !path_item) { return; } - if (_drag_initiated && !(event.modifiers & GDK_SHIFT_MASK)) { - auto &m = _desktop->getNamedView()->snap_manager; - SPItem *path = static_cast(_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); + _adjustPointToSnappedPosition(new_pos, event, *path_item); + if (_curve_event_handler->pointDragged(first, second, _t, position(), new_pos, event)) { + _pm.update(); } - _pm.update(); } void CurveDragPoint::ungrabbed(ButtonReleaseEvent const *) @@ -196,35 +181,10 @@ void CurveDragPoint::_insertNode(bool 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", - "Shift: drag to open or move BSpline handles"); - } - if (state_held_shift(state)) { - return C_("Path segment tip", - "Shift: click to toggle segment selection"); - } - if (state_held_ctrl(state) && state_held_alt(state)) { - return C_("Path segment tip", - "Ctrl+Alt: click to insert a node"); - } - if (state_held_alt(state)) { - return C_("Path segment tip", "Alt: double click to change line type"); - } - if (_pm._isBSpline()) { - return C_("Path segment tip", "BSpline segment: drag to shape the segment, doubleclick to insert node, " - "click to select (more: Alt, Shift, Ctrl+Alt)"); - } - if (linear) { - return C_("Path segment tip", "Linear segment: drag to convert to a Bezier segment, " - "doubleclick to insert node, click to select (more: Alt, Shift, Ctrl+Alt)"); - } else { - return C_("Path segment tip", "Bezier segment: drag to shape the segment, doubleclick to insert node, " - "click to select (more: Alt, Shift, Ctrl+Alt)"); + if (_pm.empty() || !first || !first.next() || !_curve_event_handler) { + return {}; } + return _curve_event_handler->getTooltip(state, first); } } // namespace UI diff --git a/src/ui/tool/curve-drag-point.h b/src/ui/tool/curve-drag-point.h index ada9fd2b7b..b835e4ecf9 100644 --- a/src/ui/tool/curve-drag-point.h +++ b/src/ui/tool/curve-drag-point.h @@ -11,6 +11,7 @@ #define INKSCAPE_UI_TOOL_CURVE_DRAG_POINT_H #include "ui/tool/control-point.h" +#include "ui/tool/curve-event-handler.h" #include "ui/tool/node.h" class SPDesktop; @@ -35,7 +36,9 @@ public: void setSize(double sz) { _setSize(sz); } void setTimeValue(double t) { _t = t; } double getTimeValue() { return _t; } - void setIterator(NodeList::iterator i) { first = i; } + + /// Set iterator to the start node of the curve on which the dragpoint is located. + void setIterator(NodeList::iterator iterator); NodeList::iterator getIterator() { return first; } bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, CanvasEvent const &event) override; @@ -48,9 +51,13 @@ protected: bool doubleclicked(ButtonReleaseEvent const &) override; private: + void _adjustPointToSnappedPosition(Geom::Point &point_to_adjust, CanvasEvent const &event, + SPItem const &item_to_ignore) const; + double _t; PathManipulator &_pm; NodeList::iterator first; + std::unique_ptr _curve_event_handler; static bool _drags_stroke; static bool _segment_was_degenerate; diff --git a/src/ui/tool/curve-event-handler.h b/src/ui/tool/curve-event-handler.h new file mode 100644 index 0000000000..b22e80c144 --- /dev/null +++ b/src/ui/tool/curve-event-handler.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_TOOL_CURVE_EVENT_HANDLER_H +#define INKSCAPE_UI_TOOL_CURVE_EVENT_HANDLER_H + +#include + +#include "ui/tool/node.h" + +namespace Geom { +class Point; +} + +namespace Inkscape { +class MotionEvent; + +namespace UI { + +/** + * An interface handling the UI actions performed on a point on the curve ("dragpoint") + * in the Node Tool. Dragging or clicking a point on the curve (between the nodes) may + * have a different result than performing these operations on a node. Furthermore, the + * result of these actions may depend on the type of curve segment. + */ +struct CurveHandler +{ + /** + * Handle the initial grab of a point on the curve. + * + * @param curve_start Start node of the grabbed segment. + * @param curve_end End node of the grabbed segment. + * + * @return Whether the geometry needs to be updated. + */ + virtual bool pointGrabbed(NodeList::iterator curve_start, NodeList::iterator curve_end) = 0; + + /** + * Handle an ongoing drag of a point on a curve segment. + * + * @param curve_start Initial node of the curve segment. + * @param curve_end Final node of the curve segment. + * @param curve_time Time parameter of the dragged point, in the interval [0, 1]. + * @param drag_origin The point from which the drag started. + * @param drag_destination The point to which the curve is being dragged. + * @param event The details of the motion event. + * + * @return Whether the curve geometry should be updated. + */ + virtual bool pointDragged(NodeList::iterator curve_start, NodeList::iterator curve_end, double curve_time, + Geom::Point const &drag_origin, Geom::Point const &drag_destination, + MotionEvent const &event) = 0; + + /// Get the tooltip string with Pango markup, to be displayed on the status bar. + virtual Glib::ustring getTooltip(unsigned event_state, NodeList::iterator curve_start) = 0; + + virtual ~CurveHandler() = default; +}; +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_TOOL_CURVE_DRAG_HANDLER_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/elliptical-arc-end-node.cpp b/src/ui/tool/elliptical-arc-end-node.cpp new file mode 100644 index 0000000000..1a9d6c24b7 --- /dev/null +++ b/src/ui/tool/elliptical-arc-end-node.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Editable node at the end of an elliptical arc. + */ +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "elliptical-arc-end-node.h" + +#include <2geom/elliptical-arc.h> + +#include "elliptical-arc-handler.h" +#include "inkscape.h" + +namespace Inkscape::UI { + +EllipticalArcEndNode::EllipticalArcEndNode(Geom::EllipticalArc const &preceding_arc, NodeSharedData const &data) + : Node{data, preceding_arc.finalPoint()} + , _manipulator{_desktop ? *_desktop : *SP_ACTIVE_DESKTOP, preceding_arc, data} +{} + +std::unique_ptr EllipticalArcEndNode::createEventHandlerForPrecedingCurve() +{ + return std::make_unique(_manipulator); +} + +void EllipticalArcEndNode::move(Geom::Point const &p) +{ + Node::move(p); + _manipulator.setArcFinalPoint(p); +} + +void EllipticalArcEndNode::notifyPrecedingNodeUpdate(Node &previous_node) +{ + _manipulator.setArcInitialPoint(previous_node.position()); + previous_node.front()->retract(); +} + +void EllipticalArcEndNode::transform(Geom::Affine const &m) +{ + setPosition(position() * m); + back()->retract(); + _manipulator.setArcFinalPoint(position()); +} + +void EllipticalArcEndNode::fixNeighbors() +{ + back()->retract(); +} + +void EllipticalArcEndNode::showHandles(bool v) +{ + _extra_ui_visible = v; + _manipulator.setVisible(v); + + if (auto next_node = NodeList::get_iterator(this).next(); + next_node && next_node->handlesAllowedOnPrecedingSegment()) { + front()->setVisible(v && !front()->isDegenerate()); + } +} + +void EllipticalArcEndNode::setType(NodeType, bool) +{ + /// TODO: replace setType with polymorphism + updateState(); + _manipulator.updateDisplay(); +} + +std::unique_ptr EllipticalArcEndNode::subdivideArc(double curve_time) +{ + auto result = _manipulator.subdivideArc(curve_time); + result->setType(NODE_CUSP, false); + return result; +} + +void EllipticalArcEndNode::writeSegment(Geom::PathSink &output, const Node &) const +{ + _manipulator.writeSegment(output); +} +} // namespace Inkscape::UI + +/* + 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/elliptical-arc-end-node.h b/src/ui/tool/elliptical-arc-end-node.h new file mode 100644 index 0000000000..c023a69d6e --- /dev/null +++ b/src/ui/tool/elliptical-arc-end-node.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Editable node at the end of an elliptical arc. + */ +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_TOOL_ELLIPTICAL_ARC_END_NODE_H +#define INKSCAPE_UI_TOOL_ELLIPTICAL_ARC_END_NODE_H + +#include "elliptical-manipulator.h" +#include "ui/tool/node.h" + +namespace Geom { +class Affine; +class EllipticalArc; +class Point; +} // namespace Geom + +namespace Inkscape::UI { +class NodeSharedData; +class CurveHandler; + +class EllipticalArcEndNode : public Node { +public: + EllipticalArcEndNode(Geom::EllipticalArc const &preceding_arc, NodeSharedData const &data); + + ~EllipticalArcEndNode() override = default; + + void move(Geom::Point const &p) final; + + void transform(Geom::Affine const &m) final; + + void fixNeighbors() final; + + void showHandles(bool v) final; + + bool isPrecedingSegmentStraight() const final { return false; } + + bool handlesAllowedOnPrecedingSegment() const final { return false; } + + void notifyPrecedingNodeUpdate(Node &previous_node) final; + + void setType(NodeType, bool) final; + + /// Subdivide the arc preceding this node and return a new node at the prescribed curve time parameter. + std::unique_ptr subdivideArc(double curve_time); + + void writeSegment(Geom::PathSink &output, Node const &) const final; + + std::unique_ptr createEventHandlerForPrecedingCurve() final; + + bool areHandlesVisible() const final { return _extra_ui_visible; } + +private: + EllipticalManipulator _manipulator; + bool _extra_ui_visible = false; +}; +} + +#endif // INKSCAPE_UI_TOOL_ELLIPTICAL_ARC_END_NODE_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/elliptical-arc-handler.cpp b/src/ui/tool/elliptical-arc-handler.cpp new file mode 100644 index 0000000000..83dcf284d7 --- /dev/null +++ b/src/ui/tool/elliptical-arc-handler.cpp @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tool/elliptical-arc-handler.h" + +#include <2geom/ellipse.h> +#include <2geom/elliptical-arc.h> +#include <2geom/line.h> +#include <2geom/transforms.h> +#include +#include +#include + +#include "ui/modifiers.h" +#include "ui/tool/elliptical-arc-end-node.h" +#include "ui/tool/elliptical-manipulator.h" +#include "ui/tool/node.h" +#include "ui/widget/events/canvas-event.h" + +namespace { + +/** Given three collinear points, construct the smallest degenerate arc with the given initial and final points + * containing the specified third point. + * The last argunment arc_center_hint is used as a loose suggestion for where the arc's enter should be placed. + */ +Geom::EllipticalArc compute_degenerate_arc(Geom::Point const &initial_point, Geom::Point const &final_point, + Geom::Point const &point_on_arc, Geom::Point const &arc_center_hint) +{ + Geom::Line const line{initial_point, final_point}; + auto const arc_center = line.isDegenerate() ? initial_point : line.pointAt(line.nearestTime(arc_center_hint)); + + double const ray = + std::max(Geom::distance(arc_center, point_on_arc), + std::max(Geom::distance(arc_center, initial_point), Geom::distance(arc_center, final_point))); + + Geom::Ellipse degenerate_ellipse{arc_center, {ray, 0.0}, line.angle()}; // TODO const-correct Geom::Ellipse::arc() + + // This is needed only because lib2geom insists on heap allocation + std::unique_ptr result{degenerate_ellipse.arc(initial_point, point_on_arc, final_point)}; + return *result; +} + +Geom::Line perpendicular_bisector(Geom::Point const &a, Geom::Point const &b) +{ + return Geom::Line::from_origin_and_vector(Geom::middle_point(a, b), (a - b).cw()); +} + +Geom::Point triple_intersection(Geom::Line const &a, Geom::Line const &b, Geom::Line const &c) +{ + auto intersections = a.intersect(b); + auto const ac = c.intersect(a); + auto const bc = b.intersect(c); + + intersections.insert(intersections.end(), ac.begin(), ac.end()); + intersections.insert(intersections.end(), bc.begin(), bc.end()); + if (intersections.empty()) { + return {}; + } + + return std::accumulate( + intersections.begin(), intersections.end(), Geom::Point{}, + [coefficient = 1.0 / static_cast(intersections.size())](const auto &accumulated, const auto &incoming) { + return accumulated + coefficient * incoming.point(); + }); +} + +/** + * @brief Find an ellipse with the specified aspect ratio and rotation angle passing through three given points. + * + * @pre Points a, b, c do not lie on the same straight line. + * + * @param aspect_ratio A point whose ratio of coordinates expresses the desired ratio of ellipse's rays. + * @pre Both of the coordinates of aspect_ratio are strictly positive. + * + * @param rotation The angle of the rotation taking the X axis to the axis of aspect_ratio.x(). + * @return An ellipse passing through a, b, c whose rotation angle and aspect ratio match the input. + */ +Geom::Ellipse fit_ellipse_to_three_points(Geom::Point const &a, Geom::Point const &b, Geom::Point const &c, + Geom::Point const &aspect_ratio, Geom::Angle rotation) +{ + auto const level = Geom::Rotate(-rotation); + auto const circularize = Geom::Scale(aspect_ratio.y(), aspect_ratio.x()); + auto const transform = level * circularize; + + auto const p = a * transform; + auto const q = b * transform; + auto const r = c * transform; + + auto const center_transformed = + triple_intersection(perpendicular_bisector(p, q), perpendicular_bisector(q, r), perpendicular_bisector(r, p)); + auto const radius_transformed = Geom::distance(center_transformed, p) / 3.0 + + Geom::distance(center_transformed, q) / 3.0 + + Geom::distance(center_transformed, r) / 3.0; + auto const rays = Geom::Point{radius_transformed, radius_transformed} * circularize.inverse(); + + return {center_transformed * transform.inverse(), rays, rotation}; +} + +bool is_modifier_active(Inkscape::Modifiers::Type type, Inkscape::CanvasEvent const &event) +{ + auto const *modifier = Inkscape::Modifiers::Modifier::get(type); + return modifier && modifier->active(event.modifiers); +} +} // namespace + +namespace Inkscape::UI { + +EllipticalArcHandler::EllipticalArcHandler(EllipticalManipulator &manipulator) + : _manipulator{&manipulator} +{} + +bool EllipticalArcHandler::pointGrabbed(NodeList::iterator curve_start, NodeList::iterator curve_end) +{ + _rays_at_drag_start = _manipulator->arc().rays(); + return false; +} + +bool EllipticalArcHandler::pointDragged(NodeList::iterator curve_start, NodeList::iterator curve_end, double curve_time, + Geom::Point const &drag_origin, Geom::Point const &drag_destination, + MotionEvent const &event) +{ + auto const &arc = _manipulator->arc(); + auto const initial_point = arc.initialPoint(); + auto const final_point = arc.finalPoint(); + + if (Geom::are_collinear(initial_point, drag_destination, final_point)) { + // The endpoints of the arc lie on the same straight line as the drag point; the arc will be degenerate. + _manipulator->setArcGeometry( + compute_degenerate_arc(initial_point, final_point, drag_destination, arc.center())); + return true; + } + + // The points are not on the same line, so we need a real arc. However, the original arc might have become + // degenerate in case the user is in the midst of dragging the point from one side of the chord to the other. + // So we make an arc of an ellipse with the same aspect ratio as the original ellipse at the start of the drag, + // unless that ellipse itself was degenerate, in which case we create a circle. + bool const was_degenerate = _rays_at_drag_start.x() < Geom::EPSILON || _rays_at_drag_start.y() < Geom::EPSILON; + auto const &reference_rays = was_degenerate ? Geom::Point{1.0, 1.0} : _rays_at_drag_start; + + Geom::Angle arc_rotation = arc.rotationAngle(); + if (!is_modifier_active(Modifiers::Type::MOVE_CONFINE, event)) { + auto const center = arc.center(); + arc_rotation += Geom::angle_between(drag_origin - center, drag_destination - center); + // TODO: the expression (drag_destination - center) does not take into account the changed center + } + + auto fitting_ellipse = + fit_ellipse_to_three_points(initial_point, drag_destination, final_point, reference_rays, arc_rotation); + std::unique_ptr new_arc{fitting_ellipse.arc(initial_point, drag_destination, final_point)}; + + _manipulator->setArcGeometry(*new_arc); + return true; +} + +Glib::ustring EllipticalArcHandler::getTooltip(unsigned event_state, NodeList::iterator /*curve_start*/) +{ + return C_("Path segment tip", "Elliptical arc: drag to shape the arc, doubleclick to insert node"); +} +} // namespace Inkscape::UI + +/* + 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/elliptical-arc-handler.h b/src/ui/tool/elliptical-arc-handler.h new file mode 100644 index 0000000000..32985c7029 --- /dev/null +++ b/src/ui/tool/elliptical-arc-handler.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_TOOL_ELLIPTICAL_ARC_HANDLER_H +#define INKSCAPE_UI_TOOL_ELLIPTICAL_ARC_HANDLER_H + +#include <2geom/point.h> + +#include "ui/tool/curve-event-handler.h" + +class SPItem; + +namespace Inkscape::UI { +class EllipticalManipulator; + +class EllipticalArcHandler : public CurveHandler +{ +public: + explicit EllipticalArcHandler(EllipticalManipulator &manipulator); + + ~EllipticalArcHandler() override = default; + + bool pointGrabbed(NodeList::iterator curve_start, NodeList::iterator curve_end) override; + + bool pointDragged(NodeList::iterator curve_start, NodeList::iterator curve_end, double curve_time, + Geom::Point const &drag_origin, Geom::Point const &drag_destination, + MotionEvent const &event) override; + + Glib::ustring getTooltip(unsigned event_state, NodeList::iterator /*curve_start*/) override; + +private: + EllipticalManipulator *_manipulator = nullptr; + Geom::Point _rays_at_drag_start; +}; +} // namespace Inkscape::UI + +#endif // INKSCAPE_UI_TOOL_ELLIPTICAL_ARC_HANDLER_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/elliptical-manipulator.cpp b/src/ui/tool/elliptical-manipulator.cpp new file mode 100644 index 0000000000..3e33fb9bf6 --- /dev/null +++ b/src/ui/tool/elliptical-manipulator.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Handles for the manipulation of elliptical arcs. + */ +/* Authors: + * Rafał M. Siejakowski - Implementation + * Adam Belis - Design and definition + * + * Copyright (C) 2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tool/elliptical-manipulator.h" + +#include <2geom/elliptical-arc.h> +#include <2geom/line.h> +#include <2geom/path-sink.h> +#include <2geom/point.h> + +#include "ui/tool/elliptical-arc-end-node.h" +#include "ui/tool/node.h" + +namespace Inkscape::UI { + +EllipticalManipulator::EllipticalManipulator(SPDesktop &desktop, Geom::EllipticalArc const &arc, + NodeSharedData const &data) + : _arc{arc} + , _node_shared_data{&data} +{} + +void EllipticalManipulator::updateDisplay() +{ + // TODO: implement the center node +} + +void EllipticalManipulator::writeSegment(Geom::PathSink &output) const +{ + auto const [ray_x, ray_y] = _arc.rays(); + output.arcTo(ray_x, ray_y, _arc.rotationAngle(), _arc.largeArc(), _arc.sweep(), _arc.finalPoint()); +} + +void EllipticalManipulator::setVisible(bool visible) +{ + // TODO: implement the center node +} + +void EllipticalManipulator::setArcGeometry(Geom::EllipticalArc const &new_arc) +{ + auto const old_initial_point = _arc.initialPoint(); + auto const old_final_point = _arc.finalPoint(); + + _arc = new_arc; + _arc.setInitial(old_initial_point); + _arc.setFinal(old_final_point); + + updateDisplay(); +} + +void EllipticalManipulator::setArcFinalPoint(Geom::Point const &new_point) +{ + auto const old_initial_point = _arc.initialPoint(); + auto const old_chord_line = Geom::Line(_arc.chord()); + auto const new_chord_line = Geom::Line(old_initial_point, new_point); + + _arc.transform(old_chord_line.transformTo(new_chord_line)); + _arc.setInitial(old_initial_point); + _arc.setFinal(new_point); + + updateDisplay(); +} + +void EllipticalManipulator::setArcInitialPoint(Geom::Point const &new_point) +{ + auto const old_final_point = _arc.finalPoint(); + auto const old_chord_line = Geom::Line(_arc.chord()); + auto const new_chord_line = Geom::Line(new_point, old_final_point); + + _arc.transform(old_chord_line.transformTo(new_chord_line)); + _arc.setInitial(new_point); + _arc.setFinal(old_final_point); + + updateDisplay(); +} + +std::unique_ptr EllipticalManipulator::subdivideArc(double subdivision_time) +{ + double const t = std::clamp(subdivision_time, 0.0, 1.0); + auto const subdivision_point = _arc.pointAt(t); + + std::unique_ptr first_curve{_arc.portion(0.0, t)}; + std::unique_ptr second_curve{_arc.portion(t, 1.0)}; + + // TODO: fix portion() so that dynamic casts are unnecessary + auto *first_arc = dynamic_cast(first_curve.get()); + auto *second_arc = dynamic_cast(second_curve.get()); + + assert(first_arc && second_arc); + + first_arc->setInitial(_arc.initialPoint()); + first_arc->setFinal(subdivision_point); + + second_arc->setInitial(subdivision_point); + second_arc->setFinal(_arc.finalPoint()); + + _arc = *second_arc; + updateDisplay(); + + return std::make_unique(*first_arc, *_node_shared_data); +} +} // namespace Inkscape::UI + +/* + 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/elliptical-manipulator.h b/src/ui/tool/elliptical-manipulator.h new file mode 100644 index 0000000000..869468f280 --- /dev/null +++ b/src/ui/tool/elliptical-manipulator.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Handles for the manipulation of elliptical arcs in the Node tool. + */ +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_TOOL_ELLIPTICAL_MANIPULATOR_H +#define INKSCAPE_UI_TOOL_ELLIPTICAL_MANIPULATOR_H + +#include <2geom/elliptical-arc.h> +#include <2geom/point.h> +#include +#include + +#include "ui/tool/control-point.h" + +class SPDesktop; + +namespace Geom { +class Ellipse; +class EllipticalArc; +class PathSink; +} // namespace Geom + +namespace Inkscape::UI { +class Node; +class NodeSharedData; + +class EllipticalManipulator +{ +public: + EllipticalManipulator(SPDesktop &desktop, Geom::EllipticalArc const &arc, NodeSharedData const &data); + + /// Read-only access to the geometric arc. + Geom::EllipticalArc const &arc() const { return _arc; } + + /** + * Shorten the controlled arc to only the part after the subdivision point, + * returning a new subdivision node controlling the part before the subdivision point. + * + * @param subdivision_time Curve time in the interval [0, 1]. + */ + std::unique_ptr subdivideArc(double subdivision_time); + + void setVisible(bool visible); + + void setArcInitialPoint(Geom::Point const &new_point); + + void setArcFinalPoint(Geom::Point const &new_point); + + /// Replace the manipulated arc with a new one. + void setArcGeometry(Geom::EllipticalArc const &new_arc); + + void updateDisplay(); + + /// Feed the manipulated elliptical arc into a path sink. + void writeSegment(Geom::PathSink &output) const; + +private: + Geom::EllipticalArc _arc; + NodeSharedData const *_node_shared_data = nullptr; +}; +} // namespace Inkscape::UI + +#endif // INKSCAPE_UI_TOOL_ELLIPTICAL_MANIPULATOR_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 index dca599d0c2..1cd64dd205 100644 --- a/src/ui/tool/multi-path-manipulator.cpp +++ b/src/ui/tool/multi-path-manipulator.cpp @@ -296,7 +296,7 @@ void MultiPathManipulator::setNodeType(NodeType type) Node *node = dynamic_cast(i); if (node) { retract_handles &= (node->type() == NODE_CUSP); - node->setType(type); + node->setType(type, true); } } diff --git a/src/ui/tool/node.cpp b/src/ui/tool/node.cpp index 360fcf7948..4d39627c6f 100644 --- a/src/ui/tool/node.cpp +++ b/src/ui/tool/node.cpp @@ -21,7 +21,9 @@ #include "object/sp-namedview.h" #include "snap.h" #include "ui/modifiers.h" +#include "ui/tool/bezier-curve-handler.h" #include "ui/tool/control-point-selection.h" +#include "ui/tool/elliptical-manipulator.h" #include "ui/tool/path-manipulator.h" #include "ui/tools/node-tool.h" #include "ui/widget/events/canvas-event.h" @@ -295,16 +297,8 @@ void Handle::setPosition(Geom::Point const &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); - } + _degenerate = Geom::are_near(position(), _parent->position()); + setVisible(!_degenerate && _parent->areHandlesVisible()); } void Handle::setLength(double len) @@ -829,6 +823,9 @@ void Node::move(Geom::Point const &new_pos) nextNode->back()->setPosition(_pm()._bsplineHandleReposition(nextNode->back(), nextNodeWeight)); } } + if (nextNode) { + nextNode->notifyPrecedingNodeUpdate(*this); + } Inkscape::UI::Tools::sp_update_helperpath(_desktop); } @@ -856,6 +853,10 @@ void Node::transform(Geom::Affine const &m) _front.setPosition(_front.position() * m); _back.setPosition(_back.position() * m); + if (nextNode) { + nextNode->notifyPrecedingNodeUpdate(*this); + } + // move the involved handles. First the node ones, later the adjoining ones. if (_pm()._isBSpline()) { _front.setPosition(_pm()._bsplineHandleReposition(this->front(), nodeWeight)); @@ -971,6 +972,12 @@ void Node::showHandles(bool v) } } +bool Node::isPrecedingSegmentStraight() const +{ + auto const *previous_node = _prev(); + return previous_node && _back.isDegenerate() && previous_node->_front.isDegenerate(); +} + void Node::updateHandles() { _handleControlStyling(); @@ -979,6 +986,23 @@ void Node::updateHandles() _back._handleControlStyling(); } +void Node::writeSegment(Geom::PathSink &output, Node const &previous) const +{ + if (_back.isDegenerate() && previous._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. + output.lineTo(position()); + } else { + // The preceding segment is a Bezier curve + output.curveTo(previous._front.position(), _back.position(), position()); + } +} + +std::unique_ptr Node::createEventHandlerForPrecedingCurve() +{ + return std::make_unique(_pm()._isBSpline()); +} + void Node::setType(NodeType type, bool update_handles) { if (type == NODE_PICK_BEST) { diff --git a/src/ui/tool/node.h b/src/ui/tool/node.h index 792d5991bd..ac1e2115b1 100644 --- a/src/ui/tool/node.h +++ b/src/ui/tool/node.h @@ -31,6 +31,7 @@ class CanvasItemCurve; namespace UI { +class CurveHandler; class PathManipulator; class MultiPathManipulator; @@ -147,12 +148,23 @@ public: * 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); + virtual void setType(NodeType type, bool update_handles); - void showHandles(bool v); + virtual void showHandles(bool v); + /// Create a CurveHandler object which can process events such as drag on the curve segment before the node. + virtual std::unique_ptr createEventHandlerForPrecedingCurve(); + + /// \todo: Create a BezierNode class deriving from Node and move all functionality related to handles there. void updateHandles(); + virtual bool handlesAllowedOnPrecedingSegment() const { return true; } + + virtual bool areHandlesVisible() const { return visible() && _handles_shown; } + + /// Notify this node that the previous node in the path underwent an update. + virtual void notifyPrecedingNodeUpdate(Node & /* previous_node */) {} + /** * 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. @@ -160,6 +172,8 @@ public: void pickBestType(); // automatically determine the type from handle positions bool isDegenerate() const { return _front.isDegenerate() && _back.isDegenerate(); } + virtual bool isPrecedingSegmentStraight() const; + bool isEndNode() const; Handle *front() { return &_front; } Handle *back() { return &_back; } @@ -188,7 +202,10 @@ public: */ Node *nodeAwayFrom(Handle *h); - NodeList &nodeList() { return *(static_cast(this)->ln_list); } + /// Append the preceding curve segment to a path sink. + virtual void writeSegment(Geom::PathSink &output, Node const &previous) const; + + NodeList &nodeList() { return *(static_cast(this)->ln_list); } NodeList &nodeList() const { return *(static_cast(this)->ln_list); } /** diff --git a/src/ui/tool/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp index a8114b1400..d5ee35e5db 100644 --- a/src/ui/tool/path-manipulator.cpp +++ b/src/ui/tool/path-manipulator.cpp @@ -11,36 +11,33 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ +#include "ui/tool/path-manipulator.h" + #include <2geom/bezier-utils.h> +#include <2geom/forward.h> #include <2geom/path-sink.h> #include <2geom/point.h> - #include #include -#include "display/curve.h" #include "display/control/canvas-item-bpath.h" - -#include <2geom/forward.h> +#include "display/curve.h" #include "helper/geom.h" - -#include "live_effects/lpeobject.h" -#include "live_effects/lpe-powerstroke.h" #include "live_effects/lpe-bspline.h" +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpeobject.h" #include "live_effects/parameter/path.h" - #include "object/sp-path.h" +#include "path/splinefit/bezier-fit.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/elliptical-arc-end-node.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 "ui/widget/events/canvas-event.h" -#include "path/splinefit/bezier-fit.h" #include "xml/node-observer.h" namespace Inkscape::UI { @@ -65,10 +62,11 @@ void sanitize_path_vector(Geom::PathVector &pathvector) } } +constexpr double BSPLINE_TOL = 0.001; +constexpr double NO_POWER = 0.0; +constexpr double DEFAULT_START_POWER = 1.0 / 3.0; + } // anonymous namespace -static constexpr double BSPLINE_TOL = 0.001; -static constexpr double NO_POWER = 0.0; -static constexpr double DEFAULT_START_POWER = 1.0/3.0; /** * Notifies the path manipulator when something changes the path being edited @@ -116,7 +114,6 @@ private: 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) @@ -428,7 +425,7 @@ void PathManipulator::copySelectedPath(Geom::PathBuilder *builder) if (!builder->inPath() || !prev) { builder->moveTo(node.position()); } else { - build_segment(*builder, prev, &node); + node.writeSegment(*builder, *prev); } prev = &node; is_last_node = true; @@ -440,7 +437,7 @@ void PathManipulator::copySelectedPath(Geom::PathBuilder *builder) // 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()); + subpath->front().writeSegment(*builder, *prev); } // if that segment is linear, we just call closePath(). builder->closePath(); @@ -808,11 +805,11 @@ unsigned PathManipulator::_deleteStretch(NodeList::iterator start, NodeList::ite } else if (mode == NodeDeleteMode::line_segment) { // Handle line straigtening if (start.prev()) { - start.prev()->setType(NodeType::NODE_CUSP); + start.prev()->setType(NodeType::NODE_CUSP, true); start.prev()->front()->move(start.prev()->position()); } if (end) { - end->setType(NodeType::NODE_CUSP); + end->setType(NodeType::NODE_CUSP, true); end->back()->move(end->position()); } } @@ -941,6 +938,7 @@ void PathManipulator::setSegmentType(SegmentType type) j->front()->move(j->position() + (k->position() - j->position()) / 3); k->back()->move(k->position() + (j->position() - k->position()) / 3); break; + // TODO: add support for elliptical arc creation } } } @@ -949,7 +947,7 @@ void PathManipulator::setSegmentType(SegmentType type) void PathManipulator::scaleHandle(Node *n, int which, int dir, bool pixel) { if (n->type() == NODE_SYMMETRIC || n->type() == NODE_AUTO) { - n->setType(NODE_SMOOTH); + n->setType(NODE_SMOOTH, true); } Handle *h = _chooseHandle(n, which); double length_change; @@ -982,7 +980,7 @@ void PathManipulator::scaleHandle(Node *n, int which, int dir, bool pixel) void PathManipulator::rotateHandle(Node *n, int which, int dir, bool pixel) { if (n->type() != NODE_CUSP) { - n->setType(NODE_CUSP); + n->setType(NODE_CUSP, true); } Handle *h = _chooseHandle(n, which); if (h->isDegenerate()) return; @@ -1127,7 +1125,10 @@ NodeList::iterator PathManipulator::subdivideSegment(NodeList::iterator first, d ++insert_at; NodeList::iterator inserted; - if (first->front()->isDegenerate() && second->back()->isDegenerate()) { + // TODO: refactor so that a dynamic cast is not needed. + if (auto *arc_endpoint = dynamic_cast(second.ptr())) { + inserted = list.insert(insert_at, arc_endpoint->subdivideArc(t).release()); + } else 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())); @@ -1281,7 +1282,7 @@ void PathManipulator::_createControlPointsFromGeometry() if (_is_bspline) { pathv = pathv_to_cubicbezier(_spcurve.get_pathvector(), false); } else { - pathv = pathv_to_linear_and_cubic_beziers(_spcurve.get_pathvector()); + pathv = _spcurve.get_pathvector(); } // sanitize pathvector and store it in SPCurve, @@ -1303,7 +1304,7 @@ void PathManipulator::_createControlPointsFromGeometry() Node *previous_node = new Node(_multi_path_manipulator._path_data.node_data, pit.initialPoint()); subpath->push_back(previous_node); - bool closed = pit.closed(); + bool const closed = pit.closed(); for (Geom::Path::iterator cit = pit.begin(); cit != pit.end(); ++cit) { Geom::Point pos = cit->finalPoint(); @@ -1312,6 +1313,9 @@ void PathManipulator::_createControlPointsFromGeometry() // the handle of the first node instead of creating a new one if (closed && cit == --(pit.end())) { current_node = subpath->begin().get_pointer(); + } else if (auto const *arc = dynamic_cast(&*cit)) { + current_node = new EllipticalArcEndNode(*arc, _multi_path_manipulator._path_data.node_data); + subpath->push_back(current_node); } 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 @@ -1332,7 +1336,15 @@ void PathManipulator::_createControlPointsFromGeometry() previous_node = current_node; } // If the path is closed, make the list cyclic - if (pit.closed()) subpath->setClosed(true); + if (closed) { + if (pit.size_open() && pit.closingSegment().isDegenerate()) { + if (auto const *arc = dynamic_cast(&pit.back_open())) { + subpath->pop_front(); + subpath->push_front(new EllipticalArcEndNode(*arc, _multi_path_manipulator._path_data.node_data)); + } + } + subpath->setClosed(true); + } } // we need to set the nodetypes after all the handles are in place, @@ -1474,14 +1486,14 @@ void PathManipulator::_createGeometryFromControlPoints(bool alert_LPE) 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()); + i->writeSegment(builder, *prev); 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 the last segment is not straight, we add it. + if (!subpath->front().isPrecedingSegmentStraight()) { + subpath->front().writeSegment(builder, *prev); } // if that segment is linear, we just call closePath(). builder.closePath(); @@ -1519,24 +1531,6 @@ void PathManipulator::_createGeometryFromControlPoints(bool alert_LPE) } } -/** 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() { @@ -1693,7 +1687,7 @@ bool PathManipulator::_nodeClicked(Node *n, ButtonReleaseEvent const &event) } else if (held_ctrl(event)) { // Ctrl+click: cycle between node types if (!n->isEndNode()) { - n->setType(static_cast((n->type() + 1) % NODE_LAST_REAL_TYPE)); + n->setType(static_cast((n->type() + 1) % NODE_LAST_REAL_TYPE), true); update(); _commit(_("Cycle node type")); } -- GitLab From b16ab7ad6a60692f0ce7dc18b2637c1822b94160 Mon Sep 17 00:00:00 2001 From: Rafael Siejakowski Date: Sat, 16 Nov 2024 21:44:05 +0100 Subject: [PATCH 03/13] Add the ellipse outline to Node Tool The Node Tool will now display the outline of the ellipse of which the elliptical arc is being manipulated. Design: https://gitlab.com/inkscape/ux/-/issues/206 --- src/ui/tool/elliptical-arc-end-node.cpp | 7 ++- src/ui/tool/elliptical-arc-end-node.h | 9 +++- src/ui/tool/elliptical-manipulator.cpp | 57 ++++++++++++++++++++++--- src/ui/tool/elliptical-manipulator.h | 13 +++++- src/ui/tool/multi-path-manipulator.cpp | 5 +-- src/ui/tool/multi-path-manipulator.h | 2 +- src/ui/tool/path-manipulator.cpp | 6 ++- 7 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/ui/tool/elliptical-arc-end-node.cpp b/src/ui/tool/elliptical-arc-end-node.cpp index 1a9d6c24b7..8dea2409b6 100644 --- a/src/ui/tool/elliptical-arc-end-node.cpp +++ b/src/ui/tool/elliptical-arc-end-node.cpp @@ -15,12 +15,15 @@ #include "elliptical-arc-handler.h" #include "inkscape.h" +#include "object/sp-item.h" +#include "util/cast.h" namespace Inkscape::UI { -EllipticalArcEndNode::EllipticalArcEndNode(Geom::EllipticalArc const &preceding_arc, NodeSharedData const &data) +EllipticalArcEndNode::EllipticalArcEndNode(Geom::EllipticalArc const &preceding_arc, NodeSharedData const &data, + SPObject const *path, PathManipulator &parent) : Node{data, preceding_arc.finalPoint()} - , _manipulator{_desktop ? *_desktop : *SP_ACTIVE_DESKTOP, preceding_arc, data} + , _manipulator{_desktop ? *_desktop : *SP_ACTIVE_DESKTOP, preceding_arc, data, cast(path), parent} {} std::unique_ptr EllipticalArcEndNode::createEventHandlerForPrecedingCurve() diff --git a/src/ui/tool/elliptical-arc-end-node.h b/src/ui/tool/elliptical-arc-end-node.h index c023a69d6e..c32d8f09d9 100644 --- a/src/ui/tool/elliptical-arc-end-node.h +++ b/src/ui/tool/elliptical-arc-end-node.h @@ -15,6 +15,8 @@ #include "elliptical-manipulator.h" #include "ui/tool/node.h" +class SPObject; + namespace Geom { class Affine; class EllipticalArc; @@ -24,10 +26,13 @@ class Point; namespace Inkscape::UI { class NodeSharedData; class CurveHandler; +class PathManipulator; -class EllipticalArcEndNode : public Node { +class EllipticalArcEndNode : public Node +{ public: - EllipticalArcEndNode(Geom::EllipticalArc const &preceding_arc, NodeSharedData const &data); + EllipticalArcEndNode(Geom::EllipticalArc const &preceding_arc, NodeSharedData const &data, SPObject const *path, + PathManipulator &parent); ~EllipticalArcEndNode() override = default; diff --git a/src/ui/tool/elliptical-manipulator.cpp b/src/ui/tool/elliptical-manipulator.cpp index 3e33fb9bf6..f27fb639c8 100644 --- a/src/ui/tool/elliptical-manipulator.cpp +++ b/src/ui/tool/elliptical-manipulator.cpp @@ -15,22 +15,68 @@ #include <2geom/elliptical-arc.h> #include <2geom/line.h> #include <2geom/path-sink.h> +#include <2geom/pathvector.h> #include <2geom/point.h> +#include "display/control/canvas-item-bpath.h" +#include "display/control/canvas-item-enums.h" +#include "display/control/canvas-item-ptr.h" +#include "object/sp-item.h" #include "ui/tool/elliptical-arc-end-node.h" +#include "ui/tool/multi-path-manipulator.h" #include "ui/tool/node.h" +#include "ui/tool/path-manipulator.h" + +namespace { + +/// Given the arc of an ellipse, return the other arc making up the ellipse. +Geom::PathVector arc_complement(Geom::EllipticalArc const &arc) +{ + Geom::PathVector result; + Geom::PathBuilder builder{result}; + + auto const [rx, ry] = arc.rays(); + builder.moveTo(arc.finalPoint()); + builder.arcTo(rx, ry, arc.rotationAngle(), !arc.largeArc(), arc.sweep(), arc.initialPoint()); + builder.flush(); + return result; +} + +double constexpr CONTOUR_WIDTH = 2.0; +double constexpr DASH_LENGTH = 2.0; +} // namespace namespace Inkscape::UI { EllipticalManipulator::EllipticalManipulator(SPDesktop &desktop, Geom::EllipticalArc const &arc, - NodeSharedData const &data) + NodeSharedData const &data, SPItem const *path, PathManipulator &parent) : _arc{arc} , _node_shared_data{&data} -{} + , _contour{make_canvasitem(data.handle_line_group)} + , _path{path} + , _parent{&parent} +{ + _contour->set_bpath(arc_complement(_arc)); + _contour->set_name("CanvasItemBPath:EllipseContour"); + _contour->set_stroke(CANVAS_ITEM_PRIMARY); + _contour->lower_to_bottom(); + _contour->set_pickable(false); + _contour->CanvasItem::set_fill(0U); + _contour->set_stroke_width(CONTOUR_WIDTH); + _contour->set_dashes({DASH_LENGTH, DASH_LENGTH}); +} void EllipticalManipulator::updateDisplay() { - // TODO: implement the center node + // TODO: implement the arc controller + // _center_node.setPosition(_arc.center()); + _contour->set_bpath(arc_complement(_arc)); + _parent->update(); +} + +void EllipticalManipulator::commitUndoEvent(CommitEvent event_type) const +{ + _parent->mpm().commit(event_type); } void EllipticalManipulator::writeSegment(Geom::PathSink &output) const @@ -41,7 +87,8 @@ void EllipticalManipulator::writeSegment(Geom::PathSink &output) const void EllipticalManipulator::setVisible(bool visible) { - // TODO: implement the center node + // TODO: implement the halo node + _contour->set_visible(visible); } void EllipticalManipulator::setArcGeometry(Geom::EllipticalArc const &new_arc) @@ -105,7 +152,7 @@ std::unique_ptr EllipticalManipulator::subdivideArc(double subdivision_tim _arc = *second_arc; updateDisplay(); - return std::make_unique(*first_arc, *_node_shared_data); + return std::make_unique(*first_arc, *_node_shared_data, _path, *_parent); } } // namespace Inkscape::UI diff --git a/src/ui/tool/elliptical-manipulator.h b/src/ui/tool/elliptical-manipulator.h index 869468f280..b11a40f204 100644 --- a/src/ui/tool/elliptical-manipulator.h +++ b/src/ui/tool/elliptical-manipulator.h @@ -17,9 +17,13 @@ #include #include +#include "display/control/canvas-item-bpath.h" +#include "display/control/canvas-item-ptr.h" +#include "ui/tool/commit-events.h" #include "ui/tool/control-point.h" class SPDesktop; +class SPItem; namespace Geom { class Ellipse; @@ -30,11 +34,13 @@ class PathSink; namespace Inkscape::UI { class Node; class NodeSharedData; +class PathManipulator; class EllipticalManipulator { public: - EllipticalManipulator(SPDesktop &desktop, Geom::EllipticalArc const &arc, NodeSharedData const &data); + EllipticalManipulator(SPDesktop &desktop, Geom::EllipticalArc const &arc, NodeSharedData const &data, + SPItem const *path, PathManipulator &parent); /// Read-only access to the geometric arc. Geom::EllipticalArc const &arc() const { return _arc; } @@ -56,6 +62,8 @@ public: /// Replace the manipulated arc with a new one. void setArcGeometry(Geom::EllipticalArc const &new_arc); + void commitUndoEvent(CommitEvent event_type) const; + void updateDisplay(); /// Feed the manipulated elliptical arc into a path sink. @@ -64,6 +72,9 @@ public: private: Geom::EllipticalArc _arc; NodeSharedData const *_node_shared_data = nullptr; + CanvasItemPtr _contour; + SPItem const *_path = nullptr; + PathManipulator *_parent = nullptr; }; } // namespace Inkscape::UI diff --git a/src/ui/tool/multi-path-manipulator.cpp b/src/ui/tool/multi-path-manipulator.cpp index 1cd64dd205..75d9193095 100644 --- a/src/ui/tool/multi-path-manipulator.cpp +++ b/src/ui/tool/multi-path-manipulator.cpp @@ -118,8 +118,7 @@ MultiPathManipulator::MultiPathManipulator(PathSharedData &data) : PointManipulator(data.node_data.desktop, *data.node_data.selection) , _path_data(data) { - _selection.signal_commit.connect( - sigc::mem_fun(*this, &MultiPathManipulator::_commit)); + _selection.signal_commit.connect(sigc::mem_fun(*this, &MultiPathManipulator::commit)); _selection.signal_selection_changed.connect( sigc::hide( sigc::hide( signal_coords_changed.make_slot()))); @@ -799,7 +798,7 @@ bool MultiPathManipulator::event(Inkscape::UI::Tools::ToolBase *tool, CanvasEven /** 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) +void MultiPathManipulator::commit(CommitEvent cps) { gchar const *reason = nullptr; gchar const *key = nullptr; diff --git a/src/ui/tool/multi-path-manipulator.h b/src/ui/tool/multi-path-manipulator.h index 2c3ac8aa61..6a870179dd 100644 --- a/src/ui/tool/multi-path-manipulator.h +++ b/src/ui/tool/multi-path-manipulator.h @@ -70,6 +70,7 @@ public: void reverseSubpaths(); void move(Geom::Point const &delta); void scale(Geom::Point const ¢er, Geom::Point const &scale); + void commit(CommitEvent cps); void showOutline(bool show); void showHandles(bool show); @@ -123,7 +124,6 @@ private: } } - void _commit(CommitEvent cps); void _done(gchar const *reason, bool alert_LPE = true); void _doneWithCleanup(gchar const *reason, bool alert_LPE = false); Colors::Color _getOutlineColor(ShapeRole role, SPObject *object); diff --git a/src/ui/tool/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp index d5ee35e5db..62c52075d3 100644 --- a/src/ui/tool/path-manipulator.cpp +++ b/src/ui/tool/path-manipulator.cpp @@ -1314,7 +1314,8 @@ void PathManipulator::_createControlPointsFromGeometry() if (closed && cit == --(pit.end())) { current_node = subpath->begin().get_pointer(); } else if (auto const *arc = dynamic_cast(&*cit)) { - current_node = new EllipticalArcEndNode(*arc, _multi_path_manipulator._path_data.node_data); + current_node = + new EllipticalArcEndNode(*arc, _multi_path_manipulator._path_data.node_data, _path, *this); subpath->push_back(current_node); } else { /* regardless of segment type, create a new node at the end @@ -1340,7 +1341,8 @@ void PathManipulator::_createControlPointsFromGeometry() if (pit.size_open() && pit.closingSegment().isDegenerate()) { if (auto const *arc = dynamic_cast(&pit.back_open())) { subpath->pop_front(); - subpath->push_front(new EllipticalArcEndNode(*arc, _multi_path_manipulator._path_data.node_data)); + subpath->push_front( + new EllipticalArcEndNode(*arc, _multi_path_manipulator._path_data.node_data, _path, *this)); } } subpath->setClosed(true); -- GitLab From 6fce56e48519b773212edde2b6213f3cc4135649 Mon Sep 17 00:00:00 2001 From: Rafael Siejakowski Date: Sat, 18 Jan 2025 17:59:36 +0100 Subject: [PATCH 04/13] Refactor the handling of sodipodi:nodetypes The code of the Node Tool related to reading and writing the XML attribute sodipodi:nodetypes is refactored and enhanced with support for the new character 'e'. This character triggers the creation of elliptical arc controls. In other words, if the node type sequence, instead of, "sc" is "sec", then the curve segment between the 's'-node and the 'c'-node will have elliptical arc controls whenever it is actually an arc. The creation of nodes of different types is moved to a dedicated factory class. --- src/ui/CMakeLists.txt | 19 ++-- src/ui/tool/node-factory.cpp | 174 +++++++++++++++++++++++++++++++ src/ui/tool/node-factory.h | 111 ++++++++++++++++++++ src/ui/tool/node-types.h | 51 +++++++++ src/ui/tool/node.cpp | 46 ++------ src/ui/tool/node.h | 13 +-- src/ui/tool/path-manipulator.cpp | 122 ++++++++++------------ src/ui/tool/path-manipulator.h | 22 ++-- 8 files changed, 425 insertions(+), 133 deletions(-) create mode 100644 src/ui/tool/node-factory.cpp create mode 100644 src/ui/tool/node-factory.h diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 3dbd01746d..eb60059b5f 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -45,20 +45,23 @@ set(ui_SRC tool/elliptical-manipulator.cpp tool/modifier-tracker.cpp tool/multi-path-manipulator.cpp + tool/node-factory.cpp tool/node.cpp tool/path-manipulator.cpp tool/selectable-control-point.cpp tool/transform-handle-set.cpp toolbar/arc-toolbar.cpp + toolbar/booleans-toolbar.cpp toolbar/box3d-toolbar.cpp toolbar/calligraphy-toolbar.cpp + toolbar/command-toolbar.cpp toolbar/connector-toolbar.cpp toolbar/dropper-toolbar.cpp - toolbar/marker-toolbar.cpp toolbar/eraser-toolbar.cpp toolbar/gradient-toolbar.cpp toolbar/lpe-toolbar.cpp + toolbar/marker-toolbar.cpp toolbar/measure-toolbar.cpp toolbar/mesh-toolbar.cpp toolbar/node-toolbar.cpp @@ -68,20 +71,17 @@ set(ui_SRC toolbar/pencil-toolbar.cpp toolbar/rect-toolbar.cpp toolbar/select-toolbar.cpp - toolbar/booleans-toolbar.cpp + toolbar/snap-toolbar.cpp toolbar/spiral-toolbar.cpp toolbar/spray-toolbar.cpp toolbar/star-toolbar.cpp toolbar/text-toolbar.cpp + toolbar/tool-toolbar.cpp toolbar/toolbar.cpp + toolbar/toolbars.cpp toolbar/tweak-toolbar.cpp toolbar/zoom-toolbar.cpp - toolbar/command-toolbar.cpp - toolbar/tool-toolbar.cpp - toolbar/snap-toolbar.cpp - toolbar/toolbars.cpp - tools/arc-tool.cpp tools/box3d-tool.cpp tools/calligraphic-tool.cpp @@ -146,7 +146,7 @@ set(ui_SRC dialog/font-collections-manager.cpp dialog/font-substitution.cpp dialog/global-palettes.cpp - dialog/glyphs.cpp + dialog/glyphs.cpp dialog/grid-arrange-tab.cpp dialog/guides.cpp dialog/icon-preview.cpp @@ -316,7 +316,7 @@ set(ui_SRC pack.h popup-menu.h shape-editor.h - shape-editor-knotholders.h + shape-editor-knotholders.h simple-pref-pusher.h shortcuts.h svg-renderer.h @@ -415,6 +415,7 @@ set(ui_SRC tool/manipulator.h tool/modifier-tracker.h tool/multi-path-manipulator.h + tool/node-factory.h tool/node-types.h tool/node.h tool/path-manipulator.h diff --git a/src/ui/tool/node-factory.cpp b/src/ui/tool/node-factory.cpp new file mode 100644 index 0000000000..fa57399903 --- /dev/null +++ b/src/ui/tool/node-factory.cpp @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Factory for creating Node objects for the Node Tool + */ +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "node-factory.h" + +#include <2geom/bezier-curve.h> +#include <2geom/curve.h> +#include <2geom/elliptical-arc.h> +#include <2geom/exception.h> +#include +#include +#include +#include + +#include "elliptical-arc-end-node.h" +#include "node.h" +#include "ui/tool/path-manipulator.h" + +namespace Inkscape::UI { + +namespace { + +Geom::EllipticalArc make_semicircle(Geom::Point const &from, Geom::Point const &to) +{ + auto const r = 0.5 * Geom::distance(from, to); + return {from, r, r, 0.0, true, true, to}; +} + +std::unique_ptr fit_arc_to_cubic_bezier(Geom::CubicBezier const &bezier) +{ + assert(!bezier.isLineSegment()); + + auto const initial_point = bezier.initialPoint(); + auto const mid_point = bezier.pointAt(0.5); + auto const final_point = bezier.finalPoint(); + + Geom::Ellipse ellipse; + try { + const std::vector points{initial_point, bezier.pointAt(0.25), mid_point, bezier.pointAt(0.75), final_point}; + ellipse.fit(points); + } catch (Geom::RangeError const &) { + return {}; + } + + return std::unique_ptr{ellipse.arc(initial_point, mid_point, final_point)}; +} +} // namespace + +std::vector read_node_type_requests(char const *xml_node_type_string) +{ + std::string_view const node_type_str = xml_node_type_string ? xml_node_type_string : ""; + + std::vector result; + result.reserve(node_type_str.size()); + + auto const parse_symbol = [&node_type_str](auto &it) -> NodeTypeRequest { + if (it == node_type_str.end()) { + return {}; + } + + NodeTypeRequest result; + + while (it != node_type_str.end() && *it == static_cast(XmlNodeType::ELLIPSE_MODIFIER)) { + result.elliptical_arc_requested = true; + ++it; + } + if (it == node_type_str.end()) { + return result; + } + + result.requested_type = static_cast(*it++); + return result; + }; + + auto iterator = node_type_str.begin(); + while (iterator != node_type_str.end()) { + result.push_back(parse_symbol(iterator)); + } + return result; +} + +void set_node_types(SubpathList &subpath_list, std::span requests) +{ + auto const get_next = [&requests](auto &it) { return it == requests.end() ? NodeTypeRequest{} : *it++; }; + + auto iterator = requests.begin(); + for (auto &subpath : subpath_list) { + for (auto &node : *subpath) { + const auto [node_type, ellipse_modifier] = get_next(iterator); + node.setType(decode_node_type(node_type), false); + } + if (subpath->closed()) { + // STUPIDITY ALERT: it seems we need to use the duplicate type symbol instead of + // the first one to remain backward compatible. + const auto [node_type, ellipse_modifier] = get_next(iterator); + if (node_type != XmlNodeType::BOGUS) { + subpath->begin()->setType(decode_node_type(node_type), false); + } + } + } +} + +NodeFactory::NodeFactory(std::span request_sequence, PathManipulator *manipulator, + SPObject *path) + : _requests{request_sequence} + , _manipulator{manipulator} + , _always_create_elliptical_arcs{request_sequence.empty()} +{ + assert(_manipulator); + _shared_data = _manipulator->getNodeSharedData(); +} + +std::unique_ptr NodeFactory::createInitialNode(Geom::Path const &path) +{ + std::ignore = _getNextRequest(); + + // Initial node is always of base type; we can change it upon reaching the end of a closed path + return std::make_unique(_shared_data, path.initialPoint()); +} + +std::unique_ptr NodeFactory::createNextNode(Geom::Curve const &preceding_curve) +{ + auto const type_request = _getNextRequest(); + if (type_request.elliptical_arc_requested || _always_create_elliptical_arcs) { + if (auto const *arc = dynamic_cast(&preceding_curve)) { + return std::make_unique(*arc, _shared_data, _path, *_manipulator); + } + } + return std::make_unique(_shared_data, preceding_curve.finalPoint()); +} + +std::unique_ptr NodeFactory::createArcEndpointNode(Geom::Curve const &curve) +{ + Geom::EllipticalArc arc = make_semicircle(curve.initialPoint(), curve.finalPoint()); + if (!curve.isLineSegment()) { + if (const auto *cubic_bezier = dynamic_cast(&curve)) { + if (const auto fitted = fit_arc_to_cubic_bezier(*cubic_bezier)) { + arc = *fitted; + } + } else if (const auto *already_arc = dynamic_cast(&curve)) { + arc = *already_arc; + } + } + return std::make_unique(arc, _shared_data, _path, *_manipulator); +} + +NodeTypeRequest NodeFactory::_getNextRequest() +{ + if (_pos < _requests.size()) { + return _requests[_pos++]; + } + return {}; +} + +} // namespace Inkscape::UI + +/* + 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 : \ No newline at end of file diff --git a/src/ui/tool/node-factory.h b/src/ui/tool/node-factory.h new file mode 100644 index 0000000000..3315ed4fbc --- /dev/null +++ b/src/ui/tool/node-factory.h @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Factory for creating Node objects for the Node Tool + */ +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_TOOL_NODE_FACTORY_H +#define INKSCAPE_UI_TOOL_NODE_FACTORY_H + +#include +#include +#include + +#include "node-types.h" + +class SPObject; +class SPDesktop; + +namespace Geom { +class Curve; +class Path; +} // namespace Geom + +namespace Inkscape { + +class CanvasItemGroup; + +namespace UI { + +class Node; +class PathManipulator; +class SubpathList; +class ControlPointSelection; + +struct NodeSharedData +{ + SPDesktop *desktop; + ControlPointSelection *selection; + CanvasItemGroup *node_group; + CanvasItemGroup *handle_group; + CanvasItemGroup *handle_line_group; +}; + +struct NodeTypeRequest +{ + XmlNodeType requested_type = XmlNodeType::BOGUS; + bool elliptical_arc_requested = false; +}; + +/// Convert the content of the node type attribute in XML to a list of node request objects. +std::vector read_node_type_requests(char const *xml_node_type_string); + +/// Set the node types in the passed list of subpaths according to the passed requests. +void set_node_types(SubpathList &subpath_list, std::span requests); + +class NodeFactory +{ +public: + NodeFactory(std::span request_sequence, PathManipulator *manipulator, SPObject *path); + + NodeFactory(NodeFactory const &) = delete; + NodeFactory &operator=(NodeFactory const &) = delete; + + /** + * Create the initial node at the beginning of a path + */ + std::unique_ptr createInitialNode(Geom::Path const &path); + + /** + * Create a new node at the endpoint of the passed curve, + * consuming one element of the node type request sequence. + */ + std::unique_ptr createNextNode(Geom::Curve const &preceding_curve); + + /** + * Create a node controlling an elliptical arc. The node will be placed at the endpoint + * of the passed curve, and the elliptical arc will attempt to approximate the curve, + * or will be a semicircle if the curve is a straight line. + */ + std::unique_ptr createArcEndpointNode(Geom::Curve const &curve); + +private: + NodeTypeRequest _getNextRequest(); + + PathManipulator *_manipulator = nullptr; + SPObject *_path = nullptr; + NodeSharedData _shared_data; + std::span _requests; + unsigned _pos = 0; + bool _always_create_elliptical_arcs = 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 : \ No newline at end of file diff --git a/src/ui/tool/node-types.h b/src/ui/tool/node-types.h index bad6a5c52c..82e27cf971 100644 --- a/src/ui/tool/node-types.h +++ b/src/ui/tool/node-types.h @@ -40,6 +40,57 @@ enum class AlignTargetNode { MAX_NODE }; +/// Characters used in the sodipodi:nodetype and LPE-related nodetype XML attributes +enum class XmlNodeType : char +{ + AUTO = 'a', + BOGUS = 'b', + CUSP = 'c', + ELLIPSE_MODIFIER = 'e', + SMOOTH = 's', + SYMMETRIC = 'z', +}; + +inline constexpr XmlNodeType encode_node_type(NodeType type) +{ + switch (type) { + case NODE_CUSP: + return XmlNodeType::CUSP; + + case NODE_SMOOTH: + return XmlNodeType::SMOOTH; + + case NODE_AUTO: + return XmlNodeType::AUTO; + + case NODE_SYMMETRIC: + return XmlNodeType::SYMMETRIC; + + default: + return XmlNodeType::BOGUS; + } +} + +inline constexpr NodeType decode_node_type(XmlNodeType xml_type) +{ + switch (xml_type) { + case XmlNodeType::AUTO: + return NODE_AUTO; + + case XmlNodeType::CUSP: + return NODE_CUSP; + + case XmlNodeType::SMOOTH: + return NODE_SMOOTH; + + case XmlNodeType::SYMMETRIC: + return NODE_SYMMETRIC; + + default: + return NODE_PICK_BEST; + } +} + } // namespace UI } // namespace Inkscape diff --git a/src/ui/tool/node.cpp b/src/ui/tool/node.cpp index 4d39627c6f..3a0b88e20d 100644 --- a/src/ui/tool/node.cpp +++ b/src/ui/tool/node.cpp @@ -131,6 +131,11 @@ bool are_collinear_within_serializing_error(const Geom::Point &A, const Geom::Po return Geom::are_near(C_reflect_scaled, A, tolerance_C_reflect_scaled + tolerance_A); } +/// Compute a unit vector in the direction from first to second control point +Geom::Point direction(Geom::Point const &first, Geom::Point const &second) +{ + return Geom::unit_vector(second - first); +} } // namespace namespace Inkscape { @@ -142,30 +147,7 @@ const double DEFAULT_START_POWER = 1.0 / 3.0; 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); + return out << static_cast(encode_node_type(type)); } Geom::Point Handle::_saved_other_pos(0, 0); @@ -1176,22 +1158,6 @@ 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(Tools::ToolBase *event_context, CanvasEvent const &event) { int dir = 0; diff --git a/src/ui/tool/node.h b/src/ui/tool/node.h index ac1e2115b1..1509104bf6 100644 --- a/src/ui/tool/node.h +++ b/src/ui/tool/node.h @@ -50,14 +50,7 @@ struct ListNode NodeList *ln_list; }; -struct NodeSharedData -{ - SPDesktop *desktop; - ControlPointSelection *selection; - Inkscape::CanvasItemGroup *node_group; - Inkscape::CanvasItemGroup *handle_group; - Inkscape::CanvasItemGroup *handle_line_group; -}; +struct NodeSharedData; class Handle : public ControlPoint { @@ -140,6 +133,9 @@ public: NodeType type() const { return _type; } + /// Write a textual representation of the node type to an output stream + virtual void writeType(std::ostream &output_stream) const { output_stream << _type; } + /** * Sets the node type and optionally restores the invariants associated with the given type. * @param type The type to set. @@ -214,7 +210,6 @@ public: */ void sink(); - static NodeType parse_nodetype(char x); static char const *node_type_to_localized_string(NodeType type); // temporarily public diff --git a/src/ui/tool/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp index 62c52075d3..50a1d7fca6 100644 --- a/src/ui/tool/path-manipulator.cpp +++ b/src/ui/tool/path-manipulator.cpp @@ -17,6 +17,7 @@ #include <2geom/forward.h> #include <2geom/path-sink.h> #include <2geom/point.h> +#include #include #include @@ -35,6 +36,7 @@ #include "ui/tool/curve-drag-point.h" #include "ui/tool/elliptical-arc-end-node.h" #include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/node-factory.h" #include "ui/tool/node-types.h" #include "ui/tools/node-tool.h" #include "ui/widget/events/canvas-event.h" @@ -266,6 +268,11 @@ void PathManipulator::invertSelectionInSubpaths() } } +NodeFactory PathManipulator::createNodeFactory(std::span request_sequence) +{ + return {request_sequence, this, _path}; +} + /** Insert a new node in the middle of each selected segment. */ void PathManipulator::insertNodes() { @@ -1105,6 +1112,11 @@ void PathManipulator::hideDragPoint() _dragpoint->setIterator(NodeList::iterator()); } +NodeSharedData const &PathManipulator::getNodeSharedData() const +{ + return _multi_path_manipulator._path_data.node_data; +} + /** 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) @@ -1292,43 +1304,38 @@ void PathManipulator::_createControlPointsFromGeometry() return; } _spcurve = SPCurve(pathv); - pathv *= _getTransform(); + // XML Tree being used here directly while it shouldn't be. + auto const requests = + read_node_type_requests(_path ? _path->getRepr()->attribute(_nodetypesKey().data()) : nullptr); + auto factory = createNodeFactory(requests); + // in this loop, we know that there are no zero-segment subpaths - for (auto & pit : pathv) { - // prepare new subpath - SubpathPtr subpath(new NodeList(_subpaths)); + for (auto const &geometric_path : pathv) { + auto subpath = std::make_shared(_subpaths); _subpaths.push_back(subpath); - Node *previous_node = new Node(_multi_path_manipulator._path_data.node_data, pit.initialPoint()); - subpath->push_back(previous_node); + auto first_node_on_path = factory.createInitialNode(geometric_path); + Node *previous_node = first_node_on_path.get(); + subpath->push_back(first_node_on_path.release()); - bool const closed = pit.closed(); + bool const closed = geometric_path.closed(); - for (Geom::Path::iterator cit = pit.begin(); cit != pit.end(); ++cit) { - Geom::Point pos = cit->finalPoint(); - Node *current_node; + for (auto curve_it = geometric_path.begin(); curve_it != geometric_path.end(); ++curve_it) { + Node *current_node = nullptr; // 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())) { + if (closed && curve_it == std::prev(geometric_path.end())) { current_node = subpath->begin().get_pointer(); - } else if (auto const *arc = dynamic_cast(&*cit)) { - current_node = - new EllipticalArcEndNode(*arc, _multi_path_manipulator._path_data.node_data, _path, *this); - subpath->push_back(current_node); } 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); + auto new_node = factory.createNextNode(*curve_it); + current_node = new_node.get(); + subpath->push_back(new_node.release()); } // 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); + // TODO: this probably isn't the right place for this code + Geom::BezierCurve const *bezier = dynamic_cast(&*curve_it); if (bezier && bezier->order() == 3) { previous_node->front()->setPosition((*bezier)[1]); @@ -1338,43 +1345,19 @@ void PathManipulator::_createControlPointsFromGeometry() } // If the path is closed, make the list cyclic if (closed) { - if (pit.size_open() && pit.closingSegment().isDegenerate()) { - if (auto const *arc = dynamic_cast(&pit.back_open())) { - subpath->pop_front(); - subpath->push_front( - new EllipticalArcEndNode(*arc, _multi_path_manipulator._path_data.node_data, _path, *this)); - } + // If the closing segment is degenerate, the first node of the subpath is at the end + // of the last "real" segment of the path (excluding the closing segment) and may therefore + // be affected by the geometry of that segment + if (geometric_path.size_open() && geometric_path.closingSegment().isDegenerate()) { + auto replacement_node = factory.createNextNode(geometric_path.back_open()); + subpath->pop_front(); + subpath->push_front(replacement_node.release()); } 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); - } - } + set_node_types(_subpaths, requests); } //determines if the trace has a bspline effect and the number of steps that it takes @@ -1533,19 +1516,24 @@ void PathManipulator::_createGeometryFromControlPoints(bool alert_LPE) } } -/** Construct a node type string to store in the sodipodi:nodetypes attribute. */ +/** Construct a node type string to store in the sodipodi:nodetypes attribute. + * @pre no single-node subpaths + */ std::string PathManipulator::_createTypeString() { - // precondition: no single-node subpaths - std::stringstream tstr; - for (auto & _subpath : _subpaths) { - for (auto & j : *_subpath) { - tstr << j.type(); + std::stringstream output; + + for (auto const &subpath : _subpaths) { + for (auto const &node : *subpath) { + node.writeType(output); } // nodestring format peculiarity: first node is counted twice for closed paths - if (_subpath->closed()) tstr << _subpath->begin()->type(); + if (subpath->closed()) { + subpath->begin()->writeType(output); + } } - return tstr.str(); + + return output.str(); } /** Update the path outline. */ @@ -1641,12 +1629,10 @@ void PathManipulator::_setGeometry() /** Figure out in what attribute to store the nodetype string. */ Glib::ustring PathManipulator::_nodetypesKey() { - auto lpeobj = cast(_path); - if (!lpeobj) { - return "sodipodi:nodetypes"; - } else { + if (is(_path)) { return _lpe_key + "-nodetypes"; } + return "sodipodi:nodetypes"; } /** Return the XML node we are editing. diff --git a/src/ui/tool/path-manipulator.h b/src/ui/tool/path-manipulator.h index 3818af0802..4a31083cee 100644 --- a/src/ui/tool/path-manipulator.h +++ b/src/ui/tool/path-manipulator.h @@ -12,14 +12,17 @@ #ifndef INKSCAPE_UI_TOOL_PATH_MANIPULATOR_H #define INKSCAPE_UI_TOOL_PATH_MANIPULATOR_H -#include -#include -#include <2geom/pathvector.h> -#include <2geom/path-sink.h> #include <2geom/affine.h> -#include "ui/tool/node.h" -#include "ui/tool/manipulator.h" +#include <2geom/path-sink.h> +#include <2geom/pathvector.h> +#include +#include +#include + #include "display/curve.h" +#include "ui/tool/manipulator.h" +#include "ui/tool/node-factory.h" +#include "ui/tool/node.h" class SPCurve; class SPPath; @@ -84,6 +87,9 @@ public: void selectSubpaths(); void invertSelectionInSubpaths(); + /// Create a new node factory able to produce nodes for this manipulator. + NodeFactory createNodeFactory(std::span request_sequence = {}); + void insertNodeAtExtremum(ExtremumType extremum); void insertNodes(); void insertNode(Geom::Point); @@ -111,6 +117,8 @@ public: void updatePath(); void setControlsTransform(Geom::Affine const &); void hideDragPoint(); + NodeSharedData const &getNodeSharedData() const; + MultiPathManipulator &mpm() { return _multi_path_manipulator; } NodeList::iterator subdivideSegment(NodeList::iterator after, double t); @@ -164,7 +172,7 @@ private: SubpathList _subpaths; MultiPathManipulator &_multi_path_manipulator; - SPObject *_path; ///< can be an SPPath or an Inkscape::LivePathEffect::Effect !!! + SPObject *_path; ///< can be an SPPath or a LivePathEffectObject SPCurve _spcurve; // in item coordinates CanvasItemPtr _outline; CurveDragPoint *_dragpoint; // an invisible control point hovering over curve -- GitLab From f0065ec30170996afdad8562c3b7cb80ae18588f Mon Sep 17 00:00:00 2001 From: Rafael Siejakowski Date: Sat, 18 Jan 2025 18:33:01 +0100 Subject: [PATCH 05/13] Provide a node replacement mechanism Create a way for a node instance to replace its own occurrence in a list of nodes with another instance. --- src/ui/tool/node.cpp | 25 +++++++++++++++++++++++++ src/ui/tool/node.h | 10 ++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/ui/tool/node.cpp b/src/ui/tool/node.cpp index 3a0b88e20d..607f5982b5 100644 --- a/src/ui/tool/node.cpp +++ b/src/ui/tool/node.cpp @@ -1597,6 +1597,11 @@ Node *Node::nodeAwayFrom(Handle *h) return nullptr; } +void Node::_replace(Node *replacement) && +{ + ln_list->replace(NodeList::get_iterator(this), replacement); +} + Glib::ustring Node::_getTip(unsigned state) const { bool isBSpline = _pm()._isBSpline(); @@ -1783,6 +1788,26 @@ NodeList::iterator NodeList::insert(iterator pos, Node *x) return iterator(x); } +void NodeList::replace(iterator pos, Node *replacement) +{ + assert(replacement); + Node *to_replace = static_cast(pos._node); + const bool was_selected = to_replace->selected(); + + ListNode *prev = to_replace->ln_prev; + ListNode *next = to_replace->ln_next; + + to_replace->_pm().hideDragPoint(); + delete to_replace; + + prev->ln_next = replacement; + next->ln_prev = replacement; + replacement->ln_prev = prev; + replacement->ln_next = next; + replacement->ln_list = this; + replacement->select(was_selected); +} + void NodeList::splice(iterator pos, NodeList &list) { splice(pos, list, list.begin(), list.end()); diff --git a/src/ui/tool/node.h b/src/ui/tool/node.h index 1509104bf6..ca0e8f882e 100644 --- a/src/ui/tool/node.h +++ b/src/ui/tool/node.h @@ -227,6 +227,11 @@ protected: Glib::ustring _getTip(unsigned state) const override; Glib::ustring _getDragTip(MotionEvent const &event) const override; bool _hasDragTips() const override { return true; } + inline PathManipulator &_pm(); + inline PathManipulator &_pm() const; + + /// Destroy this node and replace it with another one. + void _replace(Node *replacement) &&; private: void _updateAutoHandles(); @@ -243,8 +248,6 @@ private: 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); @@ -400,6 +403,9 @@ public: /** insert a node before pos. */ iterator insert(iterator pos, Node *x); + /// Replace the node at a given position with a new one + void replace(iterator pos, Node *replacement); + template void insert(iterator pos, InputIterator first, InputIterator last) { for (; first != last; ++first) insert(pos, *first); -- GitLab From d38af01bd2e755ee52ed15ebb3ca5419cadfddc9 Mon Sep 17 00:00:00 2001 From: Rafael Siejakowski Date: Sun, 19 Jan 2025 22:32:22 +0100 Subject: [PATCH 06/13] Create Elliptical Arc controllers only on demand Ensure that elliptical arc controllers are created by the Node Tool only when the character 'e' is found in the XML attribute sodipodi: nodetypes or when the attribute is absent. Ensure that converting an Ellipse to path manually sets this attribute to a string not containing 'e's in order to preserve the historical behaviour of the Node Tool on such paths. --- src/path-chemistry.cpp | 24 +++++---- src/ui/CMakeLists.txt | 2 + src/ui/tool/elliptical-arc-end-node.cpp | 16 +++++- src/ui/tool/elliptical-arc-end-node.h | 4 +- src/ui/tool/path-manipulator.cpp | 50 ++++++++++++++---- src/ui/tool/remove-elliptical-arcs.cpp | 69 +++++++++++++++++++++++++ src/ui/tool/remove-elliptical-arcs.h | 45 ++++++++++++++++ 7 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 src/ui/tool/remove-elliptical-arcs.cpp create mode 100644 src/ui/tool/remove-elliptical-arcs.h diff --git a/src/path-chemistry.cpp b/src/path-chemistry.cpp index cbd8158cd8..652a2841d3 100644 --- a/src/path-chemistry.cpp +++ b/src/path-chemistry.cpp @@ -15,32 +15,29 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#include -#include +#include "path-chemistry.h" + #include +#include #include +#include #include "desktop.h" +#include "display/curve.h" #include "document-undo.h" #include "document.h" #include "message-stack.h" -#include "path-chemistry.h" -#include "text-editing.h" - -#include "display/curve.h" - #include "object/box3d.h" #include "object/object-set.h" +#include "object/sp-ellipse.h" #include "object/sp-flowtext.h" #include "object/sp-path.h" #include "object/sp-root.h" #include "object/sp-text.h" #include "style.h" - -#include "ui/icon-names.h" - #include "svg/svg.h" - +#include "text-editing.h" +#include "ui/icon-names.h" #include "xml/repr.h" using Inkscape::DocumentUndo; @@ -632,6 +629,11 @@ sp_selected_item_to_curved_repr(SPItem *item, guint32 /*text_grouping_policy*/) /* Transformation */ repr->setAttribute("transform", item->getRepr()->attribute("transform")); + /* Manually specify symmetric nodes on an ellipse to preserve the historical look and feel in Node Tool */ + if (is(item)) { + repr->setAttribute("sodipodi:nodetypes", "sssss"); + } + /* Style */ Glib::ustring style_str = item->style->writeIfDiff(item->parent ? item->parent->style : nullptr); // TODO investigate possibility diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index eb60059b5f..4e42f17f40 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -48,6 +48,7 @@ set(ui_SRC tool/node-factory.cpp tool/node.cpp tool/path-manipulator.cpp + tool/remove-elliptical-arcs.cpp tool/selectable-control-point.cpp tool/transform-handle-set.cpp @@ -419,6 +420,7 @@ set(ui_SRC tool/node-types.h tool/node.h tool/path-manipulator.h + tool/remove-elliptical-arcs.h tool/selectable-control-point.h tool/shape-record.h tool/transform-handle-set.h diff --git a/src/ui/tool/elliptical-arc-end-node.cpp b/src/ui/tool/elliptical-arc-end-node.cpp index 8dea2409b6..aee0ea3221 100644 --- a/src/ui/tool/elliptical-arc-end-node.cpp +++ b/src/ui/tool/elliptical-arc-end-node.cpp @@ -16,6 +16,7 @@ #include "elliptical-arc-handler.h" #include "inkscape.h" #include "object/sp-item.h" +#include "ui/tool/node-types.h" #include "util/cast.h" namespace Inkscape::UI { @@ -66,13 +67,24 @@ void EllipticalArcEndNode::showHandles(bool v) } } -void EllipticalArcEndNode::setType(NodeType, bool) +void EllipticalArcEndNode::setType(NodeType type, bool) { - /// TODO: replace setType with polymorphism + if (type == NODE_PICK_BEST) { + type = NODE_CUSP; + } + Node::setType(type, false); + /// TODO: update handles manually + updateState(); _manipulator.updateDisplay(); } +void EllipticalArcEndNode::writeType(std::ostream &output_stream) const +{ + output_stream << static_cast(XmlNodeType::ELLIPSE_MODIFIER); + Node::writeType(output_stream); +} + std::unique_ptr EllipticalArcEndNode::subdivideArc(double curve_time) { auto result = _manipulator.subdivideArc(curve_time); diff --git a/src/ui/tool/elliptical-arc-end-node.h b/src/ui/tool/elliptical-arc-end-node.h index c32d8f09d9..89486ff59f 100644 --- a/src/ui/tool/elliptical-arc-end-node.h +++ b/src/ui/tool/elliptical-arc-end-node.h @@ -50,7 +50,9 @@ public: void notifyPrecedingNodeUpdate(Node &previous_node) final; - void setType(NodeType, bool) final; + void setType(NodeType type, bool update_handles) final; + + void writeType(std::ostream &output_stream) const final; /// Subdivide the arc preceding this node and return a new node at the prescribed curve time parameter. std::unique_ptr subdivideArc(double curve_time); diff --git a/src/ui/tool/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp index 50a1d7fca6..01fcb634d2 100644 --- a/src/ui/tool/path-manipulator.cpp +++ b/src/ui/tool/path-manipulator.cpp @@ -17,6 +17,7 @@ #include <2geom/forward.h> #include <2geom/path-sink.h> #include <2geom/point.h> +#include #include #include #include @@ -30,6 +31,7 @@ #include "live_effects/parameter/path.h" #include "object/sp-path.h" #include "path/splinefit/bezier-fit.h" +#include "remove-elliptical-arcs.h" #include "style.h" #include "ui/icon-names.h" #include "ui/tool/control-point-selection.h" @@ -64,6 +66,27 @@ void sanitize_path_vector(Geom::PathVector &pathvector) } } +enum class BezierHandleType +{ + INITIAL, + FINAL +}; + +std::optional compute_handle_position(Geom::BezierCurve const *bezier, BezierHandleType type) +{ + if (!bezier || bezier->order() != 3) { + return {}; + } + return bezier->controlPoint(type == BezierHandleType::INITIAL ? 1 : 2); +} + +void set_handle_position(Handle *handle, std::optional const &position) +{ + if (handle && position) { + handle->setPosition(*position); + } +} + constexpr double BSPLINE_TOL = 0.001; constexpr double NO_POWER = 0.0; constexpr double DEFAULT_START_POWER = 1.0 / 3.0; @@ -1290,11 +1313,15 @@ void PathManipulator::_createControlPointsFromGeometry() { clear(); + // XML Tree being used here directly while it shouldn't be. + auto const requests = + read_node_type_requests(_path ? _path->getRepr()->attribute(_nodetypesKey().data()) : nullptr); + Geom::PathVector pathv; if (_is_bspline) { pathv = pathv_to_cubicbezier(_spcurve.get_pathvector(), false); } else { - pathv = _spcurve.get_pathvector(); + pathv = remove_elliptical_arcs_if_not_requested(_spcurve.get_pathvector(), requests); } // sanitize pathvector and store it in SPCurve, @@ -1306,9 +1333,6 @@ void PathManipulator::_createControlPointsFromGeometry() _spcurve = SPCurve(pathv); pathv *= _getTransform(); - // XML Tree being used here directly while it shouldn't be. - auto const requests = - read_node_type_requests(_path ? _path->getRepr()->attribute(_nodetypesKey().data()) : nullptr); auto factory = createNodeFactory(requests); // in this loop, we know that there are no zero-segment subpaths @@ -1336,11 +1360,9 @@ void PathManipulator::_createControlPointsFromGeometry() // if this is a bezier segment, move handles appropriately // TODO: this probably isn't the right place for this code Geom::BezierCurve const *bezier = dynamic_cast(&*curve_it); - if (bezier && bezier->order() == 3) - { - previous_node->front()->setPosition((*bezier)[1]); - current_node ->back() ->setPosition((*bezier)[2]); - } + set_handle_position(previous_node->front(), compute_handle_position(bezier, BezierHandleType::INITIAL)); + set_handle_position(current_node->back(), compute_handle_position(bezier, BezierHandleType::FINAL)); + previous_node = current_node; } // If the path is closed, make the list cyclic @@ -1352,6 +1374,16 @@ void PathManipulator::_createControlPointsFromGeometry() auto replacement_node = factory.createNextNode(geometric_path.back_open()); subpath->pop_front(); subpath->push_front(replacement_node.release()); + + // Since the node was recreated, set up its Bézier handles again + set_handle_position( + subpath->front().front(), + compute_handle_position(dynamic_cast(&geometric_path.front()), + BezierHandleType::INITIAL)); + set_handle_position( + subpath->front().back(), + compute_handle_position(dynamic_cast(&geometric_path.back_open()), + BezierHandleType::FINAL)); } subpath->setClosed(true); } diff --git a/src/ui/tool/remove-elliptical-arcs.cpp b/src/ui/tool/remove-elliptical-arcs.cpp new file mode 100644 index 0000000000..edc8f7bda0 --- /dev/null +++ b/src/ui/tool/remove-elliptical-arcs.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Convert elliptical arcs to Béziers if elliptical arcs are unwelcome + */ +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "remove-elliptical-arcs.h" + +#include <2geom/bezier-curve.h> +#include <2geom/curve.h> +#include <2geom/elliptical-arc.h> +#include <2geom/pathvector.h> +#include <2geom/sbasis-to-bezier.h> +#include +#include + +namespace Inkscape::UI { + +Geom::PathVector remove_elliptical_arcs_if_not_requested(Geom::PathVector pathvector_to_convert, + std::span requested_node_types) +{ + if (requested_node_types.empty()) { + return pathvector_to_convert; // By default, we do not kill arcs + } + + auto const is_ellipse_requested = [&requested_node_types](auto &it) { + return it == requested_node_types.end() || (it++)->elliptical_arc_requested; + }; + + Geom::PathVector result; + auto iterator = requested_node_types.begin(); + + for (auto const &path : pathvector_to_convert) { + result.push_back(Geom::Path(path.initialPoint())); + auto ¤t_path = result.back(); + current_path.setStitching(true); + + std::ignore = is_ellipse_requested(iterator); // Ignore the first request on a path + for (auto curve_it = path.begin(); curve_it != path.end_open(); ++curve_it) { + if (!is_ellipse_requested(iterator) && dynamic_cast(&*curve_it)) { + // convert this arc to a Bézier path + auto bezier_path = Geom::cubicbezierpath_from_sbasis(curve_it->toSBasis(), 0.1); + bezier_path.close(false); + current_path.append(bezier_path); + } else { + current_path.append(*curve_it); + } + } + current_path.close(path.closed()); + } + return result; +} +} // namespace Inkscape::UI + +/* + 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 : \ No newline at end of file diff --git a/src/ui/tool/remove-elliptical-arcs.h b/src/ui/tool/remove-elliptical-arcs.h new file mode 100644 index 0000000000..83a31601df --- /dev/null +++ b/src/ui/tool/remove-elliptical-arcs.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Convert elliptical arcs to Béziers if elliptical arcs are unwelcome + */ +/* Authors: + * Rafał M. Siejakowski + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_TOOL_CURVE_CONVERTER_H +#define INKSCAPE_UI_TOOL_CURVE_CONVERTER_H + +#include <2geom/pathvector.h> +#include + +#include "node-factory.h" + +namespace Inkscape::UI { + +/** + * Convert elliptical arcs in a PathVector to Bézier curves if the corresponding node type request for the + * Node Tool does not call for the creation of elliptical arc controls. + * + * This preserves the historical behaviour of the Node Tool on old SVG documents (which do not have the character + * 'e' in the sodipodi:nodetypes attribute) and after an ellipse is converted to a path manually. + */ +Geom::PathVector remove_elliptical_arcs_if_not_requested(Geom::PathVector pathvector_to_convert, + std::span requested_node_types); + +} // namespace Inkscape::UI + +#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 : \ No newline at end of file -- GitLab From 89959e21ce9d1cbdacf5efdb310e87eee443cf7d Mon Sep 17 00:00:00 2001 From: Rafael Siejakowski Date: Sun, 26 Jan 2025 19:37:13 +0100 Subject: [PATCH 07/13] Refactoring: reduce code repetition in NodeToolbar In the constructor of the Node Toolbar, replace repeated function calls with a loop. --- src/ui/toolbar/node-toolbar.cpp | 68 ++++++++++++--------------------- 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/src/ui/toolbar/node-toolbar.cpp b/src/ui/toolbar/node-toolbar.cpp index 586c7d9847..5b0b0bf233 100644 --- a/src/ui/toolbar/node-toolbar.cpp +++ b/src/ui/toolbar/node-toolbar.cpp @@ -89,50 +89,30 @@ NodeToolbar::NodeToolbar(Glib::RefPtr const &builder) get_widget(builder, "unit_menu_box").append(*unit_menu); // Attach the signals. - - get_widget(builder, "insert_node_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_add)); - - setup_insert_node_menu(); - - get_widget(builder, "delete_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_delete)); - - get_widget(builder, "join_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_join)); - get_widget(builder, "break_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_break)); - - get_widget(builder, "join_segment_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_join_segment)); - get_widget(builder, "delete_segment_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_delete_segment)); - - get_widget(builder, "cusp_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_cusp)); - get_widget(builder, "smooth_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_smooth)); - get_widget(builder, "symmetric_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_symmetrical)); - get_widget(builder, "auto_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_auto)); - - get_widget(builder, "line_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_toline)); - get_widget(builder, "curve_btn") - .signal_clicked() - .connect(sigc::mem_fun(*this, &NodeToolbar::edit_tocurve)); + static constexpr struct + { + const char *button_id; + void (NodeToolbar::*callback)(); + } button_mapping[] = { + {"insert_node_btn", &NodeToolbar::edit_add}, + {"delete_btn", &NodeToolbar::edit_delete}, + {"join_btn", &NodeToolbar::edit_join}, + {"break_btn", &NodeToolbar::edit_break}, + {"join_segment_btn", &NodeToolbar::edit_join_segment}, + {"delete_segment_btn", &NodeToolbar::edit_delete_segment}, + {"cusp_btn", &NodeToolbar::edit_cusp}, + {"smooth_btn", &NodeToolbar::edit_smooth}, + {"symmetric_btn", &NodeToolbar::edit_symmetrical}, + {"auto_btn", &NodeToolbar::edit_auto}, + {"line_btn", &NodeToolbar::edit_toline}, + {"curve_btn", &NodeToolbar::edit_tocurve}, + }; + + for (auto const &button_info : button_mapping) { + get_widget(builder, button_info.button_id) + .signal_clicked() + .connect(sigc::mem_fun(*this, button_info.callback)); + } _pusher_show_outline = std::make_unique(_show_helper_path_btn, "/tools/nodes/show_outline"); _show_helper_path_btn->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), -- GitLab From 0d41c26760afdf3ea5a883bac8d0780fa1258f2e Mon Sep 17 00:00:00 2001 From: Rafael Siejakowski Date: Sun, 9 Feb 2025 19:48:44 +0100 Subject: [PATCH 08/13] Add a Node toolbar button for creating arcs Pressing the new button turns selected segments into elliptical arcs. --- .../actions/node-segment-arc-symbolic.svg | 103 ++++++++++++++++++ .../actions/node-segment-arc-symbolic.svg | 103 ++++++++++++++++++ .../actions/node-segment-arc-symbolic.svg | 103 ++++++++++++++++++ share/ui/toolbar-node.ui | 16 +++ src/ui/tool/elliptical-arc-end-node.cpp | 63 +++++++++++ src/ui/tool/elliptical-arc-end-node.h | 2 + src/ui/tool/elliptical-manipulator.h | 1 + src/ui/tool/multi-path-manipulator.cpp | 27 ++++- src/ui/tool/node-types.h | 8 +- src/ui/tool/node.cpp | 28 +++++ src/ui/tool/node.h | 2 + src/ui/tool/path-manipulator.cpp | 31 ++---- src/ui/toolbar/node-toolbar.cpp | 8 ++ src/ui/toolbar/node-toolbar.h | 1 + 14 files changed, 466 insertions(+), 30 deletions(-) create mode 100644 share/icons/Dash/symbolic/actions/node-segment-arc-symbolic.svg create mode 100644 share/icons/hicolor/symbolic/actions/node-segment-arc-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/node-segment-arc-symbolic.svg diff --git a/share/icons/Dash/symbolic/actions/node-segment-arc-symbolic.svg b/share/icons/Dash/symbolic/actions/node-segment-arc-symbolic.svg new file mode 100644 index 0000000000..f09fed134c --- /dev/null +++ b/share/icons/Dash/symbolic/actions/node-segment-arc-symbolic.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/node-segment-arc-symbolic.svg b/share/icons/hicolor/symbolic/actions/node-segment-arc-symbolic.svg new file mode 100644 index 0000000000..f09fed134c --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/node-segment-arc-symbolic.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + diff --git a/share/icons/multicolor/symbolic/actions/node-segment-arc-symbolic.svg b/share/icons/multicolor/symbolic/actions/node-segment-arc-symbolic.svg new file mode 100644 index 0000000000..f09fed134c --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/node-segment-arc-symbolic.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + diff --git a/share/ui/toolbar-node.ui b/share/ui/toolbar-node.ui index f48308e340..178d0d81be 100644 --- a/share/ui/toolbar-node.ui +++ b/share/ui/toolbar-node.ui @@ -271,6 +271,22 @@ + + + center + True + Create arcs + False + + + + node-segment-arc + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/node-type-elliptical-symbolic.svg b/share/icons/hicolor/symbolic/actions/node-type-elliptical-symbolic.svg new file mode 100644 index 0000000000..93abaad1e2 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/node-type-elliptical-symbolic.svg @@ -0,0 +1,94 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/share/icons/multicolor/symbolic/actions/node-type-elliptical-symbolic.svg b/share/icons/multicolor/symbolic/actions/node-type-elliptical-symbolic.svg new file mode 100644 index 0000000000..93abaad1e2 --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/node-type-elliptical-symbolic.svg @@ -0,0 +1,94 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/share/ui/toolbar-node.ui b/share/ui/toolbar-node.ui index 178d0d81be..1503054cc6 100644 --- a/share/ui/toolbar-node.ui +++ b/share/ui/toolbar-node.ui @@ -232,6 +232,22 @@ + + + center + True + Make selected nodes arc handles + False + + + + node-type-elliptical + + + +