1 /** @file
2 * Path manipulator - implementation
3 */
4 /* Authors:
5 * Krzysztof KosiĆski <tweenk.pl@gmail.com>
6 *
7 * Copyright (C) 2009 Authors
8 * Released under GNU GPL, read the file 'COPYING' for more information
9 */
11 #include <tr1/unordered_set>
12 #include <boost/shared_ptr.hpp>
13 #include <glib.h>
14 #include <glibmm/i18n.h>
15 #include "desktop.h"
16 #include "desktop-handles.h"
17 #include "document.h"
18 #include "live_effects/lpeobject.h"
19 #include "message-stack.h"
20 #include "preferences.h"
21 #include "sp-path.h"
22 #include "ui/tool/control-point-selection.h"
23 #include "ui/tool/event-utils.h"
24 #include "ui/tool/node.h"
25 #include "ui/tool/multi-path-manipulator.h"
26 #include "ui/tool/path-manipulator.h"
28 namespace std { using namespace tr1; }
30 namespace Inkscape {
31 namespace UI {
33 namespace {
34 typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair;
35 typedef std::vector<IterPair> IterPairList;
36 typedef std::unordered_set<NodeList::iterator> IterSet;
37 typedef std::multimap<double, IterPair> DistanceMap;
38 typedef std::pair<double, IterPair> DistanceMapItem;
40 /** Find pairs of selected endnodes suitable for joining. */
41 void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs)
42 {
43 IterSet join_iters;
44 DistanceMap dists;
46 // find all endnodes in selection
47 for (ControlPointSelection::iterator i = sel.begin(); i != sel.end(); ++i) {
48 Node *node = dynamic_cast<Node*>(i->first);
49 if (!node) continue;
50 NodeList::iterator iter = NodeList::get_iterator(node);
51 if (!iter.next() || !iter.prev()) join_iters.insert(iter);
52 }
54 if (join_iters.size() < 2) return;
56 // Below we find the closest pairs. The algorithm is O(N^3).
57 // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs
58 // with their distances in a multimap (not worth it IMO).
59 while (join_iters.size() >= 2) {
60 double closest = DBL_MAX;
61 IterPair closest_pair;
62 for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) {
63 for (IterSet::iterator j = join_iters.begin(); j != i; ++j) {
64 double dist = Geom::distance(**i, **j);
65 if (dist < closest) {
66 closest = dist;
67 closest_pair = std::make_pair(*i, *j);
68 }
69 }
70 }
71 pairs.push_back(closest_pair);
72 join_iters.erase(closest_pair.first);
73 join_iters.erase(closest_pair.second);
74 }
75 }
77 /** After this function, first should be at the end of path and second at the beginnning.
78 * @returns True if the nodes are in the same subpath */
79 bool prepare_join(IterPair &join_iters)
80 {
81 if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) {
82 if (join_iters.first.next()) // if first is begin, swap the iterators
83 std::swap(join_iters.first, join_iters.second);
84 return true;
85 }
87 NodeList &sp_first = NodeList::get(join_iters.first);
88 NodeList &sp_second = NodeList::get(join_iters.second);
89 if (join_iters.first.next()) { // first is begin
90 if (join_iters.second.next()) { // second is begin
91 sp_first.reverse();
92 } else { // second is end
93 std::swap(join_iters.first, join_iters.second);
94 }
95 } else { // first is end
96 if (join_iters.second.next()) { // second is begin
97 // do nothing
98 } else { // second is end
99 sp_second.reverse();
100 }
101 }
102 return false;
103 }
104 } // anonymous namespace
107 MultiPathManipulator::MultiPathManipulator(PathSharedData &data, sigc::connection &chg)
108 : PointManipulator(data.node_data.desktop, *data.node_data.selection)
109 , _path_data(data)
110 , _changed(chg)
111 {
112 _selection.signal_commit.connect(
113 sigc::mem_fun(*this, &MultiPathManipulator::_commit));
114 _selection.signal_point_changed.connect(
115 sigc::hide( sigc::hide(
116 signal_coords_changed.make_slot())));
117 }
119 MultiPathManipulator::~MultiPathManipulator()
120 {
121 _mmap.clear();
122 }
124 /** Remove empty manipulators. */
125 void MultiPathManipulator::cleanup()
126 {
127 for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
128 if (i->second->empty()) _mmap.erase(i++);
129 else ++i;
130 }
131 }
133 /** @brief Change the set of items to edit.
134 *
135 * This method attempts to preserve as much of the state as possible. */
136 void MultiPathManipulator::setItems(std::set<ShapeRecord> const &s)
137 {
138 std::set<ShapeRecord> shapes(s);
140 // iterate over currently edited items, modifying / removing them as necessary
141 for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) {
142 std::set<ShapeRecord>::iterator si = shapes.find(i->first);
143 if (si == shapes.end()) {
144 // This item is no longer supposed to be edited - remove its manipulator
145 _mmap.erase(i++);
146 } else {
147 ShapeRecord const &sr = i->first;
148 ShapeRecord const &sr_new = *si;
149 // if the shape record differs, replace the key only and modify other values
150 if (sr.edit_transform != sr_new.edit_transform ||
151 sr.role != sr_new.role)
152 {
153 boost::shared_ptr<PathManipulator> hold(i->second);
154 if (sr.edit_transform != sr_new.edit_transform)
155 hold->setControlsTransform(sr_new.edit_transform);
156 if (sr.role != sr_new.role) {
157 //hold->setOutlineColor(_getOutlineColor(sr_new.role));
158 }
159 _mmap.erase(sr);
160 _mmap.insert(std::make_pair(sr_new, hold));
161 }
162 shapes.erase(si); // remove the processed record
163 ++i;
164 }
165 }
167 // add newly selected items
168 for (std::set<ShapeRecord>::iterator i = shapes.begin(); i != shapes.end(); ++i) {
169 ShapeRecord const &r = *i;
170 if (!SP_IS_PATH(r.item) && !IS_LIVEPATHEFFECT(r.item)) continue;
171 boost::shared_ptr<PathManipulator> newpm(new PathManipulator(*this, (SPPath*) r.item,
172 r.edit_transform, _getOutlineColor(r.role), r.lpe_key));
173 newpm->showHandles(_show_handles);
174 // always show outlines for clips and masks
175 newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL);
176 newpm->showPathDirection(_show_path_direction);
177 _mmap.insert(std::make_pair(r, newpm));
178 }
179 }
181 void MultiPathManipulator::selectSubpaths()
182 {
183 if (_selection.empty()) {
184 invokeForAll(&PathManipulator::selectAll);
185 } else {
186 invokeForAll(&PathManipulator::selectSubpaths);
187 }
188 }
189 void MultiPathManipulator::selectAll()
190 {
191 invokeForAll(&PathManipulator::selectAll);
192 }
194 void MultiPathManipulator::selectArea(Geom::Rect const &area, bool take)
195 {
196 if (take) _selection.clear();
197 invokeForAll(&PathManipulator::selectArea, area);
198 }
200 void MultiPathManipulator::shiftSelection(int dir)
201 {
202 invokeForAll(&PathManipulator::shiftSelection, dir);
203 }
204 void MultiPathManipulator::spatialGrow(NodeList::iterator origin, int dir)
205 {
206 double extr_dist = dir > 0 ? HUGE_VAL : -HUGE_VAL;
207 NodeList::iterator target;
209 do { // this substitutes for goto
210 if ((dir > 0 && !origin->selected())) {
211 target = origin;
212 break;
213 }
215 bool closest = dir > 0; // when growing, find closest node
216 bool selected = dir < 0; // when growing, consider only unselected nodes
218 for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
219 NodeList::iterator t = i->second->extremeNode(origin, selected, !selected, closest);
220 if (!t) continue;
221 double dist = Geom::distance(*t, *origin);
222 bool cond = closest ? (dist < extr_dist) : (dist > extr_dist);
223 if (cond) {
224 extr_dist = dist;
225 target = t;
226 }
227 }
228 } while (0);
230 if (!target) return;
231 if (dir > 0) {
232 _selection.insert(target.ptr());
233 } else {
234 _selection.erase(target.ptr());
235 }
236 }
237 void MultiPathManipulator::invertSelection()
238 {
239 invokeForAll(&PathManipulator::invertSelection);
240 }
241 void MultiPathManipulator::invertSelectionInSubpaths()
242 {
243 invokeForAll(&PathManipulator::invertSelectionInSubpaths);
244 }
245 void MultiPathManipulator::deselect()
246 {
247 _selection.clear();
248 }
250 void MultiPathManipulator::setNodeType(NodeType type)
251 {
252 if (_selection.empty()) return;
253 for (ControlPointSelection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
254 Node *node = dynamic_cast<Node*>(i->first);
255 if (node) node->setType(type);
256 }
257 _done(_("Change node type"));
258 }
260 void MultiPathManipulator::setSegmentType(SegmentType type)
261 {
262 if (_selection.empty()) return;
263 invokeForAll(&PathManipulator::setSegmentType, type);
264 if (type == SEGMENT_STRAIGHT) {
265 _done(_("Straighten segments"));
266 } else {
267 _done(_("Make segments curves"));
268 }
269 }
271 void MultiPathManipulator::insertNodes()
272 {
273 invokeForAll(&PathManipulator::insertNodes);
274 _done(_("Add nodes"));
275 }
277 void MultiPathManipulator::joinNodes()
278 {
279 invokeForAll(&PathManipulator::hideDragPoint);
280 // Node join has two parts. In the first one we join two subpaths by fusing endpoints
281 // into one. In the second we fuse nodes in each subpath.
282 IterPairList joins;
283 NodeList::iterator preserve_pos;
284 Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point);
285 if (mouseover_node) {
286 preserve_pos = NodeList::get_iterator(mouseover_node);
287 }
288 find_join_iterators(_selection, joins);
290 for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
291 bool same_path = prepare_join(*i);
292 bool mouseover = true;
293 NodeList &sp_first = NodeList::get(i->first);
294 NodeList &sp_second = NodeList::get(i->second);
295 i->first->setType(NODE_CUSP, false);
297 Geom::Point joined_pos, pos_front, pos_back;
298 pos_front = *i->second->front();
299 pos_back = *i->first->back();
300 if (i->first == preserve_pos) {
301 joined_pos = *i->first;
302 } else if (i->second == preserve_pos) {
303 joined_pos = *i->second;
304 } else {
305 joined_pos = Geom::middle_point(pos_back, pos_front);
306 mouseover = false;
307 }
309 // if the handles aren't degenerate, don't move them
310 i->first->move(joined_pos);
311 Node *joined_node = i->first.ptr();
312 if (!i->second->front()->isDegenerate()) {
313 joined_node->front()->setPosition(pos_front);
314 }
315 if (!i->first->back()->isDegenerate()) {
316 joined_node->back()->setPosition(pos_back);
317 }
318 if (mouseover) {
319 // Second node could be mouseovered, but it will be deleted, so we must change
320 // the preserve_pos iterator to the first node.
321 preserve_pos = i->first;
322 }
323 sp_second.erase(i->second);
325 if (same_path) {
326 sp_first.setClosed(true);
327 } else {
328 sp_first.splice(sp_first.end(), sp_second);
329 sp_second.kill();
330 }
331 _selection.insert(i->first.ptr());
332 }
333 // Second part replaces contiguous selections of nodes with single nodes
334 invokeForAll(&PathManipulator::weldNodes, preserve_pos);
335 _doneWithCleanup(_("Join nodes"));
336 }
338 void MultiPathManipulator::breakNodes()
339 {
340 if (_selection.empty()) return;
341 invokeForAll(&PathManipulator::breakNodes);
342 _done(_("Break nodes"));
343 }
345 void MultiPathManipulator::deleteNodes(bool keep_shape)
346 {
347 if (_selection.empty()) return;
348 invokeForAll(&PathManipulator::deleteNodes, keep_shape);
349 _doneWithCleanup(_("Delete nodes"));
350 }
352 /** Join selected endpoints to create segments. */
353 void MultiPathManipulator::joinSegment()
354 {
355 IterPairList joins;
356 find_join_iterators(_selection, joins);
357 if (joins.empty()) {
358 _desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE,
359 _("There must be at least 2 endnodes in selection"));
360 return;
361 }
363 for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
364 bool same_path = prepare_join(*i);
365 NodeList &sp_first = NodeList::get(i->first);
366 NodeList &sp_second = NodeList::get(i->second);
367 i->first->setType(NODE_CUSP, false);
368 i->second->setType(NODE_CUSP, false);
369 if (same_path) {
370 sp_first.setClosed(true);
371 } else {
372 sp_first.splice(sp_first.end(), sp_second);
373 sp_second.kill();
374 }
375 }
377 _doneWithCleanup("Join segments");
378 }
380 void MultiPathManipulator::deleteSegments()
381 {
382 if (_selection.empty()) return;
383 invokeForAll(&PathManipulator::deleteSegments);
384 _doneWithCleanup("Delete segments");
385 }
387 void MultiPathManipulator::alignNodes(Geom::Dim2 d)
388 {
389 _selection.align(d);
390 if (d == Geom::X) {
391 _done("Align nodes to a horizontal line");
392 } else {
393 _done("Align nodes to a vertical line");
394 }
395 }
397 void MultiPathManipulator::distributeNodes(Geom::Dim2 d)
398 {
399 _selection.distribute(d);
400 if (d == Geom::X) {
401 _done("Distrubute nodes horizontally");
402 } else {
403 _done("Distribute nodes vertically");
404 }
405 }
407 void MultiPathManipulator::reverseSubpaths()
408 {
409 invokeForAll(&PathManipulator::reverseSubpaths);
410 _done("Reverse selected subpaths");
411 }
413 void MultiPathManipulator::move(Geom::Point const &delta)
414 {
415 _selection.transform(Geom::Translate(delta));
416 _done("Move nodes");
417 }
419 void MultiPathManipulator::showOutline(bool show)
420 {
421 for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
422 // always show outlines for clipping paths and masks
423 i->second->showOutline(show || i->first.role != SHAPE_ROLE_NORMAL);
424 }
425 _show_outline = show;
426 }
428 void MultiPathManipulator::showHandles(bool show)
429 {
430 invokeForAll(&PathManipulator::showHandles, show);
431 _show_handles = show;
432 }
434 void MultiPathManipulator::showPathDirection(bool show)
435 {
436 invokeForAll(&PathManipulator::showPathDirection, show);
437 _show_path_direction = show;
438 }
440 void MultiPathManipulator::updateOutlineColors()
441 {
442 //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
443 // i->second->setOutlineColor(_getOutlineColor(i->first.role));
444 //}
445 }
447 bool MultiPathManipulator::event(GdkEvent *event)
448 {
449 switch (event->type) {
450 case GDK_KEY_PRESS:
451 switch (shortcut_key(event->key)) {
452 case GDK_Insert:
453 case GDK_KP_Insert:
454 insertNodes();
455 return true;
456 case GDK_i:
457 case GDK_I:
458 if (held_only_shift(event->key)) {
459 insertNodes();
460 return true;
461 }
462 break;
463 case GDK_j:
464 case GDK_J:
465 if (held_only_shift(event->key)) {
466 joinNodes();
467 return true;
468 }
469 if (held_only_alt(event->key)) {
470 joinSegment();
471 return true;
472 }
473 break;
474 case GDK_b:
475 case GDK_B:
476 if (held_only_shift(event->key)) {
477 breakNodes();
478 return true;
479 }
480 break;
481 case GDK_Delete:
482 case GDK_KP_Delete:
483 case GDK_BackSpace:
484 if (held_shift(event->key)) break;
485 if (held_alt(event->key)) {
486 deleteSegments();
487 } else {
488 deleteNodes(!held_control(event->key));
489 }
490 return true;
491 case GDK_c:
492 case GDK_C:
493 if (held_only_shift(event->key)) {
494 setNodeType(NODE_CUSP);
495 return true;
496 }
497 break;
498 case GDK_s:
499 case GDK_S:
500 if (held_only_shift(event->key)) {
501 setNodeType(NODE_SMOOTH);
502 return true;
503 }
504 break;
505 case GDK_a:
506 case GDK_A:
507 if (held_only_shift(event->key)) {
508 setNodeType(NODE_AUTO);
509 return true;
510 }
511 break;
512 case GDK_y:
513 case GDK_Y:
514 if (held_only_shift(event->key)) {
515 setNodeType(NODE_SYMMETRIC);
516 return true;
517 }
518 break;
519 case GDK_r:
520 case GDK_R:
521 if (held_only_shift(event->key)) {
522 reverseSubpaths();
523 break;
524 }
525 break;
526 default:
527 break;
528 }
529 break;
530 default: break;
531 }
533 for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
534 if (i->second->event(event)) return true;
535 }
536 return false;
537 }
539 /** Commit changes to XML and add undo stack entry based on the action that was done. Invoked
540 * by sub-manipulators, for example TransformHandleSet and ControlPointSelection. */
541 void MultiPathManipulator::_commit(CommitEvent cps)
542 {
543 gchar const *reason = NULL;
544 gchar const *key = NULL;
545 switch(cps) {
546 case COMMIT_MOUSE_MOVE:
547 reason = _("Move nodes");
548 break;
549 case COMMIT_KEYBOARD_MOVE_X:
550 reason = _("Move nodes horizontally");
551 key = "node:move:x";
552 break;
553 case COMMIT_KEYBOARD_MOVE_Y:
554 reason = _("Move nodes vertically");
555 key = "node:move:y";
556 break;
557 case COMMIT_MOUSE_ROTATE:
558 reason = _("Rotate nodes");
559 break;
560 case COMMIT_KEYBOARD_ROTATE:
561 reason = _("Rotate nodes");
562 key = "node:rotate";
563 break;
564 case COMMIT_MOUSE_SCALE_UNIFORM:
565 reason = _("Scale nodes uniformly");
566 break;
567 case COMMIT_MOUSE_SCALE:
568 reason = _("Scale nodes");
569 break;
570 case COMMIT_KEYBOARD_SCALE_UNIFORM:
571 reason = _("Scale nodes uniformly");
572 key = "node:scale:uniform";
573 break;
574 case COMMIT_KEYBOARD_SCALE_X:
575 reason = _("Scale nodes horizontally");
576 key = "node:scale:x";
577 break;
578 case COMMIT_KEYBOARD_SCALE_Y:
579 reason = _("Scale nodes vertically");
580 key = "node:scale:y";
581 break;
582 case COMMIT_FLIP_X:
583 reason = _("Flip nodes horizontally");
584 break;
585 case COMMIT_FLIP_Y:
586 reason = _("Flip nodes vertically");
587 break;
588 default: return;
589 }
591 _selection.signal_update.emit();
592 invokeForAll(&PathManipulator::writeXML);
593 if (key) {
594 sp_document_maybe_done(sp_desktop_document(_desktop), key, SP_VERB_CONTEXT_NODE, reason);
595 } else {
596 sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
597 }
598 signal_coords_changed.emit();
599 }
601 /** Commits changes to XML and adds undo stack entry. */
602 void MultiPathManipulator::_done(gchar const *reason) {
603 invokeForAll(&PathManipulator::update);
604 invokeForAll(&PathManipulator::writeXML);
605 sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
606 signal_coords_changed.emit();
607 }
609 /** Commits changes to XML, adds undo stack entry and removes empty manipulators. */
610 void MultiPathManipulator::_doneWithCleanup(gchar const *reason) {
611 _changed.block();
612 _done(reason);
613 cleanup();
614 _changed.unblock();
615 }
617 /** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */
618 guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role)
619 {
620 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
621 switch(role) {
622 case SHAPE_ROLE_CLIPPING_PATH:
623 return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff);
624 case SHAPE_ROLE_MASK:
625 return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff);
626 case SHAPE_ROLE_LPE_PARAM:
627 return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff);
628 case SHAPE_ROLE_NORMAL:
629 default:
630 return prefs->getColor("/tools/nodes/outline_color", 0xff0000ff);
631 }
632 }
634 } // namespace UI
635 } // namespace Inkscape
637 /*
638 Local Variables:
639 mode:c++
640 c-file-style:"stroustrup"
641 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
642 indent-tabs-mode:nil
643 fill-column:99
644 End:
645 */
646 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :