From 9842b04218e3ebfb3beba504a591935260270208 Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Fri, 3 May 2024 20:26:21 -0400 Subject: [PATCH] Add feature to symmetrically align selected nodes. By default this will act on all nodes in the selected objects if none are selected. Will pair up every node and move them towards each other's reflected position making the final shape symmetric along that axis. Unpaired nodes are considered centeral nodes and are moved to the axis. Additionally: 1. Fixed remove spaces widgets that were in the wrong parent 2. Readjusted widgets as requested by the UX team --- .../actions/symmetric-horizontal-node.svg | 29 ++ .../actions/symmetric-vertical-node.svg | 29 ++ .../symmetric-horizontal-node-symbolic.svg | 29 ++ .../symmetric-vertical-node-symbolic.svg | 29 ++ .../symmetric-horizontal-node-symbolic.svg | 29 ++ .../symmetric-vertical-node-symbolic.svg | 29 ++ share/ui/align-and-distribute.ui | 414 ++++++++++-------- src/actions/actions-node-align.cpp | 19 +- src/ui/tool/control-point-selection.cpp | 82 ++++ src/ui/tool/control-point-selection.h | 1 + src/ui/tool/multi-path-manipulator.cpp | 13 + src/ui/tool/multi-path-manipulator.h | 1 + 12 files changed, 527 insertions(+), 177 deletions(-) create mode 100644 share/icons/hicolor/scalable/actions/symmetric-horizontal-node.svg create mode 100644 share/icons/hicolor/scalable/actions/symmetric-vertical-node.svg create mode 100644 share/icons/hicolor/symbolic/actions/symmetric-horizontal-node-symbolic.svg create mode 100644 share/icons/hicolor/symbolic/actions/symmetric-vertical-node-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/symmetric-horizontal-node-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/symmetric-vertical-node-symbolic.svg diff --git a/share/icons/hicolor/scalable/actions/symmetric-horizontal-node.svg b/share/icons/hicolor/scalable/actions/symmetric-horizontal-node.svg new file mode 100644 index 0000000000..7d53a2d020 --- /dev/null +++ b/share/icons/hicolor/scalable/actions/symmetric-horizontal-node.svg @@ -0,0 +1,29 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/scalable/actions/symmetric-vertical-node.svg b/share/icons/hicolor/scalable/actions/symmetric-vertical-node.svg new file mode 100644 index 0000000000..8f8f9ae65e --- /dev/null +++ b/share/icons/hicolor/scalable/actions/symmetric-vertical-node.svg @@ -0,0 +1,29 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/symmetric-horizontal-node-symbolic.svg b/share/icons/hicolor/symbolic/actions/symmetric-horizontal-node-symbolic.svg new file mode 100644 index 0000000000..7d53a2d020 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/symmetric-horizontal-node-symbolic.svg @@ -0,0 +1,29 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/symmetric-vertical-node-symbolic.svg b/share/icons/hicolor/symbolic/actions/symmetric-vertical-node-symbolic.svg new file mode 100644 index 0000000000..8f8f9ae65e --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/symmetric-vertical-node-symbolic.svg @@ -0,0 +1,29 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/multicolor/symbolic/actions/symmetric-horizontal-node-symbolic.svg b/share/icons/multicolor/symbolic/actions/symmetric-horizontal-node-symbolic.svg new file mode 100644 index 0000000000..7d53a2d020 --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/symmetric-horizontal-node-symbolic.svg @@ -0,0 +1,29 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/share/icons/multicolor/symbolic/actions/symmetric-vertical-node-symbolic.svg b/share/icons/multicolor/symbolic/actions/symmetric-vertical-node-symbolic.svg new file mode 100644 index 0000000000..8f8f9ae65e --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/symmetric-vertical-node-symbolic.svg @@ -0,0 +1,29 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/share/ui/align-and-distribute.ui b/share/ui/align-and-distribute.ui index 26872d9330..7e80da5b4c 100644 --- a/share/ui/align-and-distribute.ui +++ b/share/ui/align-and-distribute.ui @@ -780,231 +780,293 @@ + - - - - - - - - 4 - 4 - 4 - 4 - 4 - + + + + + 4 + + + H: + + + + + True + Minimum horizontal gap (in pixel units) between bounding boxes + 0 + RemoveOverlap_HGap + True + + + + + 4 + 4 + V: + + + + + True + Minimum vertical gap (in pixel units) between bounding boxes + 0 + RemoveOverlap_VGap + True + + + + + True + True + Move objects as little as possible so that their bounding boxes do not overlap + 4 + 4 + False + + + distribute-remove-overlaps + True + + + + + + + - H: + 2 + 2 + Remove overlaps + + + + + + + + + + + align-and-distribute-node + vertical + 4 + + + 6 + 6 + 5 + 5 - + True - RemoveOverlap_HGap - True - Minimum horizontal gap (in pixel units) between bounding boxes + True + Align selected nodes to a common horizontal line + False + + + align-vertical-node + True + + + + 1 + 0 + - - 4 - V: + + True + True + Align selected nodes to a common vertical line + False + + + align-horizontal-node + True + + + + 2 + 0 + - + True - RemoveOverlap_VGap - True - Minimum vertical gap (in pixel units) between bounding boxes + True + Attempt to make the shape symmetrical in the vertical direction. + win.node-symmetry-vertical + False + + + symmetric-vertical-node + True + + + + 1 + 2 + - + True True - 4 + Attempt to make the shape symmetrical in the horizontal direction. + win.node-symmetry-horizontal False - Move objects as little as possible so that their bounding boxes do not overlap - - distribute-remove-overlaps + + symmetric-horizontal-node True + + 2 + 2 + - - - - - 2 - 2 - Remove overlaps - - - - - - - - - - align-and-distribute-node - vertical - 4 - - - 4 - - - - vertical + + + 6 + 2 + 2 + Align + 0 + + + + + 0 + 0 + + + + + + True + True + Distribute selected nodes horizontally + win.node-distribute-horizontal + False - - - - Relative to: - - - - - 4 - RelativeToNode - 0 - 1 - - - - 0 - - - - + + distribute-horizontal-node + True + + 1 + 1 + + + + + + True + True + Distribute selected nodes vertically + win.node-distribute-vertical + False - - - - True - True - False - Align selected nodes to a common horizontal line - - - align-vertical-node - True - - - - 0 - 0 - - - - - - True - True - False - Align selected nodes to a common vertical line - - - align-horizontal-node - True - - - - 1 - 0 - - - + + distribute-vertical-node + True + + 2 + 1 + - - + + + 6 2 2 - Align Nodes + Distribute + 0 + + 0 + 1 + - - - - - - - - vertical - - - - - True - True - win.node-distribute-horizontal - False - Distribute selected nodes horizontally - - - distribute-horizontal-node - True - - - - 0 - 0 - - - - - - True - True - win.node-distribute-vertical - False - Distribute selected nodes vertically - - - distribute-vertical-node - True - - - - 1 - 0 - - - - - - - - + + 6 2 2 - Distribute Nodes + Symmetry + 0 + + 0 + 2 + + + + to: + 1 + + 3 + 0 + + + + + + 4 + 4 + RelativeToNode + 0 + 1 + + + + 0 + + + + 4 + 0 + + + + + + + + + + + + + + + diff --git a/src/actions/actions-node-align.cpp b/src/actions/actions-node-align.cpp index 985cd07ad3..6c05a7eca3 100644 --- a/src/actions/actions-node-align.cpp +++ b/src/actions/actions-node-align.cpp @@ -86,13 +86,28 @@ node_distribute(InkscapeWindow* win, Geom::Dim2 direction) node_tool->_multipath->distributeNodes(direction); } +void +node_symmetry(InkscapeWindow* win, Geom::Dim2 direction) +{ + auto const tool = win->get_desktop()->getTool(); + auto node_tool = dynamic_cast(tool); + if (node_tool) { + node_tool->_multipath->symmetricNodes(direction); + return; + } + show_output("node_symmetry: tool is not Node tool!"); +} + + std::vector> raw_data_node_align = { // clang-format off {"win.node-align-horizontal", N_("Align nodes horizontally"), "Node", N_("Align selected nodes horizontally; usage [last|first|middle|min|max|pref]" )}, {"win.node-align-vertical", N_("Align nodes vertically"), "Node", N_("Align selected nodes vertically; usage [last|first|middle|min|max|pref]" )}, {"win.node-distribute-horizontal", N_("Distribute nodes horizontally"), "Node", N_("Distribute selected nodes horizontally" )}, - {"win.node-distribute-vertical", N_("Distribute nodes vertically"), "Node", N_("Distribute selected nodes vertically" )} + {"win.node-distribute-vertical", N_("Distribute nodes vertically"), "Node", N_("Distribute selected nodes vertically" )}, + {"win.node-symmetry-horizontal", N_("Make nodes horizontally symmetric"), "Node", N_("Make the shape symmetrical in the horizontal direction" )}, + {"win.node-symmetry-vertical", N_("Make nodes vertically symmetric"), "Node", N_("Make the shape symmetrical in the vertical direction" )} // clang-format on }; @@ -107,6 +122,8 @@ add_actions_node_align(InkscapeWindow* win) win->add_action_with_parameter( "node-align-vertical", String, sigc::bind(sigc::ptr_fun(&node_align), win, Geom::Y)); win->add_action( "node-distribute-horizontal", sigc::bind(sigc::ptr_fun(&node_distribute), win, Geom::X)); win->add_action( "node-distribute-vertical", sigc::bind(sigc::ptr_fun(&node_distribute), win, Geom::Y)); + win->add_action( "node-symmetry-horizontal", sigc::bind(sigc::ptr_fun(&node_symmetry), win, Geom::X)); + win->add_action( "node-symmetry-vertical", sigc::bind(sigc::ptr_fun(&node_symmetry), win, Geom::Y)); // clang-format on auto app = InkscapeApplication::instance(); diff --git a/src/ui/tool/control-point-selection.cpp b/src/ui/tool/control-point-selection.cpp index 1d235aef9b..6cc497788b 100644 --- a/src/ui/tool/control-point-selection.cpp +++ b/src/ui/tool/control-point-selection.cpp @@ -319,6 +319,88 @@ void ControlPointSelection::distribute(Geom::Dim2 d) } } +/** + * Pair nodes according the their symmetry and move them until they are the same. + */ +void ControlPointSelection::makeSymmetric(Geom::Dim2 dim) +{ + typedef std::pair NodePair; + + if (empty()) return; + + static auto move_towards = [](SelectableControlPoint *ctp, Geom::Point pt) { + ctp->move(Geom::middle_point(ctp->position(), pt)); + }; + + // Find the mirror axis for the requested dimention + auto mid = _bounds->midpoint()[dim]; + auto flip = Geom::Scale(dim ? 1 : -1, dim ? -1 : 1) + * Geom::Translate(dim ? 0 : mid * 2, dim ? mid * 2 : 0); + + std::vector> distances; + + std::vector left_bucket; + std::vector right_bucket; + for (auto point : _points) { + auto pos = point->position(); + if (pos[dim] > mid) { + right_bucket.push_back(point); + } else { + left_bucket.push_back(point); + } + // Add distance from the point to the axis + auto axis_pt = dim ? Geom::Point(pos[Geom::X], mid) : Geom::Point(mid, pos[Geom::Y]); + distances.emplace_back(Geom::distance(pos, axis_pt), std::make_pair(point, nullptr)); + } + + // Add the distance between each left point and each flipped right point + for (auto left : left_bucket) { + for (auto right : right_bucket) { + distances.emplace_back(Geom::distance(left->position(), right->position() * flip), std::make_pair(left, right)); + } + } + + // Sort the distances, closest points first + std::sort(distances.begin(), distances.end()); + + // Move the nodes according to their sorted distances into either pairs or center_points + for (auto &[dist, pair] : distances) { + auto left_iter = std::find(left_bucket.begin(), left_bucket.end(), pair.first); + if (left_iter == left_bucket.end()) + continue; // this point was already consumed previously + + if (pair.second) { // This point is not being compared to the axis + auto right_iter = std::find(right_bucket.begin(), right_bucket.end(), pair.second); + if (right_iter == right_bucket.end()) + continue; // this point was already consumed previously + + // Move towards each other's flipped position + pair.second->move(Geom::middle_point(pair.second->position(), pair.first->position() * flip)); + pair.first->move(Geom::middle_point(pair.first->position(), pair.second->position() * flip)); + + // Now remove from right side + right_bucket.erase(right_iter); + } else { + // Move left point to the center so we can exaust the left bucket quickly + // TODO: This aligns any non-paired points with the axis, but we could delete them if they are beyond a threshold. + auto pos = pair.first->position(); + pair.first->move(dim ? Geom::Point(pos[Geom::X], mid) : Geom::Point(mid, pos[Geom::Y])); + } + // Remove from left side, this only happens if there was an addition + left_bucket.erase(left_iter); + + if (left_bucket.empty()) + break; // No more points remaining on left side, return + } + + // Any remaining points in the right side are center points + for (auto point : right_bucket) { + // TODO: See above todo, non-paied points might need a threshold. + auto pos = point->position(); + point->move(dim ? Geom::Point(pos[Geom::X], mid) : Geom::Point(mid, pos[Geom::Y])); + } +} + /** Get the bounds of the selection. * @return Smallest rectangle containing the positions of all selected points, * or nothing if the selection is empty */ diff --git a/src/ui/tool/control-point-selection.h b/src/ui/tool/control-point-selection.h index c32a18b4ee..dcd0aa2a9e 100644 --- a/src/ui/tool/control-point-selection.h +++ b/src/ui/tool/control-point-selection.h @@ -101,6 +101,7 @@ public: void transform(Geom::Affine const &m); void align(Geom::Dim2 d, AlignTargetNode target = AlignTargetNode::MID_NODE); void distribute(Geom::Dim2 d); + void makeSymmetric(Geom::Dim2 d); Geom::OptRect pointwiseBounds(); Geom::OptRect bounds(); diff --git a/src/ui/tool/multi-path-manipulator.cpp b/src/ui/tool/multi-path-manipulator.cpp index 951f04444f..6022ab4163 100644 --- a/src/ui/tool/multi-path-manipulator.cpp +++ b/src/ui/tool/multi-path-manipulator.cpp @@ -497,6 +497,19 @@ void MultiPathManipulator::distributeNodes(Geom::Dim2 d) } } +void MultiPathManipulator::symmetricNodes(Geom::Dim2 d) +{ + if (_selection.empty()) { + _selection.selectAll(); + } + _selection.makeSymmetric(d); + if (d == Geom::X) { + _done("Make nodes horizontally symmetric"); + } else { + _done("Make nodes vertically symmetric"); + } +} + void MultiPathManipulator::reverseSubpaths() { if (_selection.empty()) { diff --git a/src/ui/tool/multi-path-manipulator.h b/src/ui/tool/multi-path-manipulator.h index 7bd5126891..6dee04cc0b 100644 --- a/src/ui/tool/multi-path-manipulator.h +++ b/src/ui/tool/multi-path-manipulator.h @@ -67,6 +67,7 @@ public: void deleteSegments(); void alignNodes(Geom::Dim2 d, AlignTargetNode target = AlignTargetNode::MID_NODE); void distributeNodes(Geom::Dim2 d); + void symmetricNodes(Geom::Dim2 d); void reverseSubpaths(); void move(Geom::Point const &delta); void scale(Geom::Point const ¢er, Geom::Point const &scale); -- GitLab