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