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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
-
-
-
-
+
+
+ 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);