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 0000000000000000000000000000000000000000..7d53a2d020cd79868afef62e8caaed72bc747b9e --- /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 0000000000000000000000000000000000000000..8f8f9ae65e9a25abcce825ecd260629ee514a382 --- /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 0000000000000000000000000000000000000000..7d53a2d020cd79868afef62e8caaed72bc747b9e --- /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 0000000000000000000000000000000000000000..8f8f9ae65e9a25abcce825ecd260629ee514a382 --- /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 0000000000000000000000000000000000000000..7d53a2d020cd79868afef62e8caaed72bc747b9e --- /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 0000000000000000000000000000000000000000..8f8f9ae65e9a25abcce825ecd260629ee514a382 --- /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 26872d93305133c491ef69a024a89974655011e5..7e80da5b4ceed1537c61e938094b7100bdd16693 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 985cd07ad3bfc33b21f69ec303c28c6e1921d3e0..6c05a7eca30f9435552bbed631682dd606a71f70 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 1d235aef9ba643f6df94e3bc48cce3cae402a2ec..6cc497788bc991ce4fc00f27b16cc60ac0e9eede 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 c32a18b4eec55135eeb032a628f07c7ae5dfadf9..dcd0aa2a9e97fc2c0dc9e1c86401d07854ba4124 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 951f04444f97ab513c083b577394a63b3c03e5b8..6022ab4163115fd1640284185314e7782b076982 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 7bd512689158f1df83f20a601e414708636ea59f..6dee04cc0bf0c3657f448a68412816e8681b8700 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);