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