summaryrefslogtreecommitdiffstats
path: root/src/ui/tool/multi-path-manipulator.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/tool/multi-path-manipulator.cpp')
-rw-r--r--src/ui/tool/multi-path-manipulator.cpp568
1 files changed, 568 insertions, 0 deletions
diff --git a/src/ui/tool/multi-path-manipulator.cpp b/src/ui/tool/multi-path-manipulator.cpp
new file mode 100644
index 000000000..6b245702a
--- /dev/null
+++ b/src/ui/tool/multi-path-manipulator.cpp
@@ -0,0 +1,568 @@
+/** @file
+ * Path manipulator - implementation
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL, read the file 'COPYING' for more information
+ */
+
+#include <tr1/unordered_set>
+#include <boost/shared_ptr.hpp>
+#include <glib.h>
+#include <glibmm/i18n.h>
+#include "desktop.h"
+#include "desktop-handles.h"
+#include "document.h"
+#include "message-stack.h"
+#include "sp-path.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/node.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/path-manipulator.h"
+
+namespace std { using namespace tr1; }
+
+namespace Inkscape {
+namespace UI {
+
+namespace {
+typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair;
+typedef std::vector<IterPair> IterPairList;
+typedef std::unordered_set<NodeList::iterator> IterSet;
+typedef std::multimap<double, IterPair> DistanceMap;
+typedef std::pair<double, IterPair> DistanceMapItem;
+
+/** Find two selected endnodes.
+ * @returns -1 if not enough endnodes selected, 1 if too many, 0 if OK */
+void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs)
+{
+ IterSet join_iters;
+ DistanceMap dists;
+
+ // find all endnodes in selection
+ for (ControlPointSelection::iterator i = sel.begin(); i != sel.end(); ++i) {
+ Node *node = dynamic_cast<Node*>(i->first);
+ if (!node) continue;
+ NodeList::iterator iter = NodeList::get_iterator(node);
+ if (!iter.next() || !iter.prev()) join_iters.insert(iter);
+ }
+
+ if (join_iters.size() < 2) return;
+
+ // Below we find the closest pairs. The algorithm is O(N^3).
+ // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs
+ // with their distances in a multimap (not worth it IMO).
+ while (join_iters.size() >= 2) {
+ double closest = DBL_MAX;
+ IterPair closest_pair;
+ for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) {
+ for (IterSet::iterator j = join_iters.begin(); j != i; ++j) {
+ double dist = Geom::distance(**i, **j);
+ if (dist < closest) {
+ closest = dist;
+ closest_pair = std::make_pair(*i, *j);
+ }
+ }
+ }
+ pairs.push_back(closest_pair);
+ join_iters.erase(closest_pair.first);
+ join_iters.erase(closest_pair.second);
+ }
+}
+
+/** After this function, first should be at the end of path and second at the beginnning.
+ * @returns True if the nodes are in the same subpath */
+bool prepare_join(IterPair &join_iters)
+{
+ if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) {
+ if (join_iters.first.next()) // if first is begin, swap the iterators
+ std::swap(join_iters.first, join_iters.second);
+ return true;
+ }
+
+ NodeList &sp_first = NodeList::get(join_iters.first);
+ NodeList &sp_second = NodeList::get(join_iters.second);
+ if (join_iters.first.next()) { // first is begin
+ if (join_iters.second.next()) { // second is begin
+ sp_first.reverse();
+ } else { // second is end
+ std::swap(join_iters.first, join_iters.second);
+ }
+ } else { // first is end
+ if (join_iters.second.next()) { // second is begin
+ // do nothing
+ } else { // second is end
+ sp_second.reverse();
+ }
+ }
+ return false;
+}
+} // anonymous namespace
+
+
+MultiPathManipulator::MultiPathManipulator(PathSharedData const &data, sigc::connection &chg)
+ : PointManipulator(data.node_data.desktop, *data.node_data.selection)
+ , _path_data(data)
+ , _changed(chg)
+{
+ //
+ _selection.signal_commit.connect(
+ sigc::mem_fun(*this, &MultiPathManipulator::_commit));
+ _selection.signal_point_changed.connect(
+ sigc::hide( sigc::hide(
+ signal_coords_changed.make_slot())));
+}
+
+MultiPathManipulator::~MultiPathManipulator()
+{
+ _mmap.clear();
+}
+
+/** Remove empty manipulators. */
+void MultiPathManipulator::cleanup()
+{
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
+ if (i->second->empty()) _mmap.erase(i++);
+ else ++i;
+ }
+}
+
+void MultiPathManipulator::setItems(std::map<SPPath*,
+ std::pair<Geom::Matrix, guint32> > const &items)
+{
+ typedef std::map<SPPath*, std::pair<Geom::Matrix, guint32> > TransMap;
+ typedef std::set<SPPath*> ItemSet;
+ ItemSet to_remove, to_add, current, new_items;
+
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ current.insert(i->first);
+ }
+ for (TransMap::const_iterator i = items.begin(); i != items.end(); ++i) {
+ new_items.insert(i->first);
+ }
+
+ std::set_difference(current.begin(), current.end(), new_items.begin(), new_items.end(),
+ std::inserter(to_remove, to_remove.end()));
+ std::set_difference(new_items.begin(), new_items.end(), current.begin(), current.end(),
+ std::inserter(to_add, to_add.end()));
+
+ for (ItemSet::iterator i = to_remove.begin(); i != to_remove.end(); ++i) {
+ _mmap.erase(*i);
+ }
+ for (ItemSet::iterator i = to_add.begin(); i != to_add.end(); ++i) {
+ boost::shared_ptr<PathManipulator> pm;
+ TransMap::const_iterator f = items.find(*i);
+ pm.reset(new PathManipulator(_path_data, *i, f->second.first, f->second.second));
+ pm->showHandles(_show_handles);
+ pm->showOutline(_show_outline);
+ pm->showPathDirection(_show_path_direction);
+ _mmap.insert(std::make_pair(*i, pm));
+ }
+}
+
+void MultiPathManipulator::selectSubpaths()
+{
+ if (_selection.empty()) {
+ invokeForAll(&PathManipulator::selectAll);
+ } else {
+ invokeForAll(&PathManipulator::selectSubpaths);
+ }
+}
+void MultiPathManipulator::selectAll()
+{
+ invokeForAll(&PathManipulator::selectAll);
+}
+
+void MultiPathManipulator::selectArea(Geom::Rect const &area, bool take)
+{
+ if (take) _selection.clear();
+ invokeForAll(&PathManipulator::selectArea, area);
+}
+
+void MultiPathManipulator::shiftSelection(int dir)
+{
+ invokeForAll(&PathManipulator::shiftSelection, dir);
+}
+void MultiPathManipulator::invertSelection()
+{
+ invokeForAll(&PathManipulator::invertSelection);
+}
+void MultiPathManipulator::invertSelectionInSubpaths()
+{
+ invokeForAll(&PathManipulator::invertSelectionInSubpaths);
+}
+void MultiPathManipulator::deselect()
+{
+ _selection.clear();
+}
+
+void MultiPathManipulator::setNodeType(NodeType type)
+{
+ if (_selection.empty()) return;
+ for (ControlPointSelection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
+ Node *node = dynamic_cast<Node*>(i->first);
+ if (node) node->setType(type);
+ }
+ _done(_("Change node type"));
+}
+
+void MultiPathManipulator::setSegmentType(SegmentType type)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::setSegmentType, type);
+ if (type == SEGMENT_STRAIGHT) {
+ _done(_("Straighten segments"));
+ } else {
+ _done(_("Make segments curves"));
+ }
+}
+
+void MultiPathManipulator::insertNodes()
+{
+ invokeForAll(&PathManipulator::insertNodes);
+ _done(_("Add nodes"));
+}
+
+void MultiPathManipulator::joinNodes()
+{
+ // Node join has two parts. In the first one we join two subpaths by fusing endpoints
+ // into one. In the second we fuse nodes in each subpath.
+ IterPairList joins;
+ NodeList::iterator preserve_pos;
+ Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point);
+ if (mouseover_node) {
+ preserve_pos = NodeList::get_iterator(mouseover_node);
+ }
+ find_join_iterators(_selection, joins);
+
+ for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
+ bool same_path = prepare_join(*i);
+ bool mouseover = true;
+ NodeList &sp_first = NodeList::get(i->first);
+ NodeList &sp_second = NodeList::get(i->second);
+ i->first->setType(NODE_CUSP, false);
+
+ Geom::Point joined_pos, pos_front, pos_back;
+ pos_front = *i->second->front();
+ pos_back = *i->first->back();
+ if (i->first == preserve_pos) {
+ joined_pos = *i->first;
+ } else if (i->second == preserve_pos) {
+ joined_pos = *i->second;
+ } else {
+ joined_pos = Geom::middle_point(pos_back, pos_front);
+ mouseover = false;
+ }
+
+ // if the handles aren't degenerate, don't move them
+ i->first->move(joined_pos);
+ Node *joined_node = i->first.ptr();
+ if (!i->second->front()->isDegenerate()) {
+ joined_node->front()->setPosition(pos_front);
+ }
+ if (!i->first->back()->isDegenerate()) {
+ joined_node->back()->setPosition(pos_back);
+ }
+ if (mouseover) {
+ // Second node could be mouseovered, but it will be deleted, so we must change
+ // the preserve_pos iterator to the first node.
+ preserve_pos = i->first;
+ }
+ sp_second.erase(i->second);
+
+ if (same_path) {
+ sp_first.setClosed(true);
+ } else {
+ sp_first.splice(sp_first.end(), sp_second);
+ sp_second.kill();
+ }
+ _selection.insert(i->first.ptr());
+ }
+ // Second part replaces contiguous selections of nodes with single nodes
+ invokeForAll(&PathManipulator::weldNodes, preserve_pos);
+ _doneWithCleanup(_("Join nodes"));
+}
+
+void MultiPathManipulator::breakNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::breakNodes);
+ _done(_("Break nodes"));
+}
+
+void MultiPathManipulator::deleteNodes(bool keep_shape)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::deleteNodes, keep_shape);
+ _doneWithCleanup(_("Delete nodes"));
+}
+
+/** Join selected endpoints to create segments. */
+void MultiPathManipulator::joinSegment()
+{
+ IterPairList joins;
+ find_join_iterators(_selection, joins);
+ if (joins.empty()) {
+ _desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE,
+ _("There must be at least 2 endnodes in selection"));
+ return;
+ }
+
+ for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
+ bool same_path = prepare_join(*i);
+ NodeList &sp_first = NodeList::get(i->first);
+ NodeList &sp_second = NodeList::get(i->second);
+ i->first->setType(NODE_CUSP, false);
+ i->second->setType(NODE_CUSP, false);
+ if (same_path) {
+ sp_first.setClosed(true);
+ } else {
+ sp_first.splice(sp_first.end(), sp_second);
+ sp_second.kill();
+ }
+ }
+
+ _doneWithCleanup("Join segment");
+}
+
+void MultiPathManipulator::deleteSegments()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::deleteSegments);
+ _doneWithCleanup("Delete segments");
+}
+
+void MultiPathManipulator::alignNodes(Geom::Dim2 d)
+{
+ _selection.align(d);
+ if (d == Geom::X) {
+ _done("Align nodes to a horizontal line");
+ } else {
+ _done("Align nodes to a vertical line");
+ }
+}
+
+void MultiPathManipulator::distributeNodes(Geom::Dim2 d)
+{
+ _selection.distribute(d);
+ if (d == Geom::X) {
+ _done("Distrubute nodes horizontally");
+ } else {
+ _done("Distribute nodes vertically");
+ }
+}
+
+void MultiPathManipulator::reverseSubpaths()
+{
+ invokeForAll(&PathManipulator::reverseSubpaths);
+ _done("Reverse selected subpaths");
+}
+
+void MultiPathManipulator::move(Geom::Point const &delta)
+{
+ _selection.transform(Geom::Translate(delta));
+ _done("Move nodes");
+}
+
+void MultiPathManipulator::showOutline(bool show)
+{
+ invokeForAll(&PathManipulator::showOutline, show);
+ _show_outline = show;
+}
+
+void MultiPathManipulator::showHandles(bool show)
+{
+ invokeForAll(&PathManipulator::showHandles, show);
+ _show_handles = show;
+}
+
+void MultiPathManipulator::showPathDirection(bool show)
+{
+ invokeForAll(&PathManipulator::showPathDirection, show);
+ _show_path_direction = show;
+}
+
+bool MultiPathManipulator::event(GdkEvent *event)
+{
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ switch (shortcut_key(event->key)) {
+ case GDK_Insert:
+ case GDK_KP_Insert:
+ insertNodes();
+ return true;
+ case GDK_i:
+ case GDK_I:
+ if (held_only_shift(event->key)) {
+ insertNodes();
+ return true;
+ }
+ break;
+ case GDK_j:
+ case GDK_J:
+ if (held_only_shift(event->key)) {
+ joinNodes();
+ return true;
+ }
+ if (held_only_alt(event->key)) {
+ joinSegment();
+ return true;
+ }
+ break;
+ case GDK_b:
+ case GDK_B:
+ if (held_only_shift(event->key)) {
+ breakNodes();
+ return true;
+ }
+ break;
+ case GDK_Delete:
+ case GDK_KP_Delete:
+ case GDK_BackSpace:
+ if (held_shift(event->key)) break;
+ if (held_alt(event->key)) {
+ deleteSegments();
+ } else {
+ deleteNodes(!held_control(event->key));
+ }
+ return true;
+ case GDK_c:
+ case GDK_C:
+ if (held_only_shift(event->key)) {
+ setNodeType(NODE_CUSP);
+ return true;
+ }
+ break;
+ case GDK_s:
+ case GDK_S:
+ if (held_only_shift(event->key)) {
+ setNodeType(NODE_SMOOTH);
+ return true;
+ }
+ break;
+ case GDK_a:
+ case GDK_A:
+ if (held_only_shift(event->key)) {
+ setNodeType(NODE_AUTO);
+ return true;
+ }
+ break;
+ case GDK_y:
+ case GDK_Y:
+ if (held_only_shift(event->key)) {
+ setNodeType(NODE_SYMMETRIC);
+ return true;
+ }
+ break;
+ case GDK_r:
+ case GDK_R:
+ if (held_only_shift(event->key)) {
+ reverseSubpaths();
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ default: break;
+ }
+
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ if (i->second->event(event)) return true;
+ }
+ return false;
+}
+
+void MultiPathManipulator::_commit(CommitEvent cps)
+{
+ gchar const *reason = NULL;
+ gchar const *key = NULL;
+ switch(cps) {
+ case COMMIT_MOUSE_MOVE:
+ reason = _("Move nodes");
+ break;
+ case COMMIT_KEYBOARD_MOVE_X:
+ reason = _("Move nodes horizontally");
+ key = "node:move:x";
+ break;
+ case COMMIT_KEYBOARD_MOVE_Y:
+ reason = _("Move nodes vertically");
+ key = "node:move:y";
+ break;
+ case COMMIT_MOUSE_ROTATE:
+ reason = _("Rotate nodes");
+ break;
+ case COMMIT_KEYBOARD_ROTATE:
+ reason = _("Rotate nodes");
+ key = "node:rotate";
+ break;
+ case COMMIT_MOUSE_SCALE_UNIFORM:
+ reason = _("Scale nodes uniformly");
+ break;
+ case COMMIT_MOUSE_SCALE:
+ reason = _("Scale nodes");
+ break;
+ case COMMIT_KEYBOARD_SCALE_UNIFORM:
+ reason = _("Scale nodes uniformly");
+ key = "node:scale:uniform";
+ break;
+ case COMMIT_KEYBOARD_SCALE_X:
+ reason = _("Scale nodes horizontally");
+ key = "node:scale:x";
+ break;
+ case COMMIT_KEYBOARD_SCALE_Y:
+ reason = _("Scale nodes vertically");
+ key = "node:scale:y";
+ break;
+ case COMMIT_FLIP_X:
+ reason = _("Flip nodes horizontally");
+ break;
+ case COMMIT_FLIP_Y:
+ reason = _("Flip nodes vertically");
+ break;
+ default: return;
+ }
+
+ _selection.signal_update.emit();
+ invokeForAll(&PathManipulator::writeXML);
+ if (key) {
+ sp_document_maybe_done(sp_desktop_document(_desktop), key, SP_VERB_CONTEXT_NODE, reason);
+ } else {
+ sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
+ }
+ signal_coords_changed.emit();
+}
+
+/** Commits changes to XML and adds undo stack entry. */
+void MultiPathManipulator::_done(gchar const *reason) {
+ invokeForAll(&PathManipulator::update);
+ invokeForAll(&PathManipulator::writeXML);
+ sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
+ signal_coords_changed.emit();
+}
+
+/** Commits changes to XML, adds undo stack entry and removes empty manipulators. */
+void MultiPathManipulator::_doneWithCleanup(gchar const *reason) {
+ _changed.block();
+ _done(reason);
+ cleanup();
+ _changed.unblock();
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :