1 /** @file
2 * Editable node - 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 <iostream>
12 #include <stdexcept>
13 #include <boost/utility.hpp>
14 #include <glib.h>
15 #include <glib/gi18n.h>
16 #include <2geom/transforms.h>
17 #include "ui/tool/event-utils.h"
18 #include "ui/tool/node.h"
19 #include "display/sp-ctrlline.h"
20 #include "display/sp-canvas.h"
21 #include "display/sp-canvas-util.h"
22 #include "desktop.h"
23 #include "desktop-handles.h"
24 #include "preferences.h"
25 #include "sp-metrics.h"
26 #include "sp-namedview.h"
28 namespace Inkscape {
29 namespace UI {
31 static SelectableControlPoint::ColorSet node_colors = {
32 {
33 {0xbfbfbf00, 0x000000ff}, // normal fill, stroke
34 {0xff000000, 0x000000ff}, // mouseover fill, stroke
35 {0xff000000, 0x000000ff} // clicked fill, stroke
36 },
37 {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected
38 {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected
39 {0xff000000, 0x000000ff} // clicked fill, stroke when selected
40 };
42 static ControlPoint::ColorSet handle_colors = {
43 {0xffffffff, 0x000000ff}, // normal fill, stroke
44 {0xff000000, 0x000000ff}, // mouseover fill, stroke
45 {0xff000000, 0x000000ff} // clicked fill, stroke
46 };
48 std::ostream &operator<<(std::ostream &out, NodeType type)
49 {
50 switch(type) {
51 case NODE_CUSP: out << 'c'; break;
52 case NODE_SMOOTH: out << 's'; break;
53 case NODE_AUTO: out << 'a'; break;
54 case NODE_SYMMETRIC: out << 'z'; break;
55 default: out << 'b'; break;
56 }
57 return out;
58 }
60 /** Computes an unit vector of the direction from first to second control point */
61 static Geom::Point direction(Geom::Point const &first, Geom::Point const &second) {
62 return Geom::unit_vector(second - first);
63 }
65 /**
66 * @class Handle
67 * Represents a control point of a cubic Bezier curve in a path.
68 */
70 double Handle::_saved_length = 0.0;
71 bool Handle::_drag_out = false;
73 Handle::Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent)
74 : ControlPoint(data.desktop, initial_pos, Gtk::ANCHOR_CENTER, SP_CTRL_SHAPE_CIRCLE, 7.0,
75 &handle_colors, data.handle_group)
76 , _parent(parent)
77 , _degenerate(true)
78 {
79 _cset = &handle_colors;
80 _handle_line = sp_canvas_item_new(data.handle_line_group, SP_TYPE_CTRLLINE, NULL);
81 setVisible(false);
82 signal_grabbed.connect(
83 sigc::bind_return(
84 sigc::hide(
85 sigc::mem_fun(*this, &Handle::_grabbedHandler)),
86 false));
87 signal_dragged.connect(
88 sigc::hide<0>(
89 sigc::mem_fun(*this, &Handle::_draggedHandler)));
90 signal_ungrabbed.connect(
91 sigc::hide(sigc::mem_fun(*this, &Handle::_ungrabbedHandler)));
92 }
93 Handle::~Handle()
94 {
95 sp_canvas_item_hide(_handle_line);
96 gtk_object_destroy(GTK_OBJECT(_handle_line));
97 }
99 void Handle::setVisible(bool v)
100 {
101 ControlPoint::setVisible(v);
102 if (v) sp_canvas_item_show(_handle_line);
103 else sp_canvas_item_hide(_handle_line);
104 }
106 void Handle::move(Geom::Point const &new_pos)
107 {
108 Handle *other, *towards, *towards_second;
109 Node *node_towards; // node in direction of this handle
110 Node *node_away; // node in the opposite direction
111 if (this == &_parent->_front) {
112 other = &_parent->_back;
113 node_towards = _parent->_next();
114 node_away = _parent->_prev();
115 towards = node_towards ? &node_towards->_back : 0;
116 towards_second = node_towards ? &node_towards->_front : 0;
117 } else {
118 other = &_parent->_front;
119 node_towards = _parent->_prev();
120 node_away = _parent->_next();
121 towards = node_towards ? &node_towards->_front : 0;
122 towards_second = node_towards ? &node_towards->_back : 0;
123 }
125 if (Geom::are_near(new_pos, _parent->position())) {
126 // The handle becomes degenerate. If the segment between it and the node
127 // in its direction becomes linear and there are smooth nodes
128 // at its ends, make their handles colinear with the segment
129 if (towards && towards->isDegenerate()) {
130 if (node_towards->type() == NODE_SMOOTH) {
131 towards_second->setDirection(*_parent, *node_towards);
132 }
133 if (_parent->type() == NODE_SMOOTH) {
134 other->setDirection(*node_towards, *_parent);
135 }
136 }
137 setPosition(new_pos);
138 return;
139 }
141 if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) {
142 // restrict movement to the line joining the nodes
143 Geom::Point direction = _parent->position() - node_away->position();
144 Geom::Point delta = new_pos - _parent->position();
145 // project the relative position on the direction line
146 Geom::Point new_delta = (Geom::dot(delta, direction)
147 / Geom::L2sq(direction)) * direction;
148 setRelativePos(new_delta);
149 return;
150 }
152 switch (_parent->type()) {
153 case NODE_AUTO:
154 _parent->setType(NODE_SMOOTH, false);
155 // fall through - auto nodes degrade into smooth nodes
156 case NODE_SMOOTH: {
157 /* for smooth nodes, we need to rotate the other handle so that it's colinear
158 * with the dragged one while conserving length. */
159 other->setDirection(new_pos, *_parent);
160 } break;
161 case NODE_SYMMETRIC:
162 // for symmetric nodes, place the other handle on the opposite side
163 other->setRelativePos(-(new_pos - _parent->position()));
164 break;
165 default: break;
166 }
168 setPosition(new_pos);
169 }
171 void Handle::setPosition(Geom::Point const &p)
172 {
173 ControlPoint::setPosition(p);
174 sp_ctrlline_set_coords(SP_CTRLLINE(_handle_line), _parent->position(), position());
176 // update degeneration info and visibility
177 if (Geom::are_near(position(), _parent->position()))
178 _degenerate = true;
179 else _degenerate = false;
180 if (_parent->_handles_shown && _parent->visible() && !_degenerate) {
181 setVisible(true);
182 } else {
183 setVisible(false);
184 }
185 // If both handles become degenerate, convert to parent cusp node
186 if (_parent->isDegenerate()) {
187 _parent->setType(NODE_CUSP, false);
188 }
189 }
191 void Handle::setLength(double len)
192 {
193 if (isDegenerate()) return;
194 Geom::Point dir = Geom::unit_vector(relativePos());
195 setRelativePos(dir * len);
196 }
198 void Handle::retract()
199 {
200 setPosition(_parent->position());
201 }
203 void Handle::setDirection(Geom::Point const &from, Geom::Point const &to)
204 {
205 setDirection(to - from);
206 }
208 void Handle::setDirection(Geom::Point const &dir)
209 {
210 Geom::Point unitdir = Geom::unit_vector(dir);
211 setRelativePos(unitdir * length());
212 }
214 char const *Handle::handle_type_to_localized_string(NodeType type)
215 {
216 switch(type) {
217 case NODE_CUSP: return _("Cusp node handle");
218 case NODE_SMOOTH: return _("Smooth node handle");
219 case NODE_SYMMETRIC: return _("Symmetric node handle");
220 case NODE_AUTO: return _("Auto-smooth node handle");
221 default: return "";
222 }
223 }
225 void Handle::_grabbedHandler()
226 {
227 _saved_length = _drag_out ? 0 : length();
228 }
230 void Handle::_draggedHandler(Geom::Point &new_pos, GdkEventMotion *event)
231 {
232 Geom::Point parent_pos = _parent->position();
233 // with Alt, preserve length
234 if (held_alt(*event)) {
235 new_pos = parent_pos + Geom::unit_vector(new_pos - parent_pos) * _saved_length;
236 }
237 // with Ctrl, constrain to M_PI/rotationsnapsperpi increments.
238 if (held_control(*event)) {
239 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
240 int snaps = 2 * prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
241 Geom::Point origin = _last_drag_origin();
242 Geom::Point rel_origin = origin - parent_pos;
243 new_pos = parent_pos + Geom::constrain_angle(Geom::Point(0,0), new_pos - parent_pos, snaps,
244 _drag_out ? Geom::Point(1,0) : Geom::unit_vector(rel_origin));
245 }
246 signal_update.emit();
247 }
249 void Handle::_ungrabbedHandler()
250 {
251 // hide the handle if it's less than dragtolerance away from the node
252 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
253 int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
255 Geom::Point dist = _desktop->d2w(_parent->position()) - _desktop->d2w(position());
256 if (dist.length() <= drag_tolerance) {
257 move(_parent->position());
258 }
259 _drag_out = false;
260 }
262 static double snap_increment_degrees() {
263 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
264 int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
265 return 180.0 / snaps;
266 }
268 Glib::ustring Handle::_getTip(unsigned state)
269 {
270 if (state_held_alt(state)) {
271 if (state_held_control(state)) {
272 return format_tip(C_("Path handle tip",
273 "<b>Ctrl+Alt</b>: preserve length and snap rotation angle to %f° increments"),
274 snap_increment_degrees());
275 } else {
276 return C_("Path handle tip",
277 "<b>Alt:</b> preserve handle length while dragging");
278 }
279 } else {
280 if (state_held_control(state)) {
281 return format_tip(C_("Path handle tip",
282 "<b>Ctrl:</b> snap rotation angle to %f° increments, click to retract"),
283 snap_increment_degrees());
284 }
285 }
286 switch (_parent->type()) {
287 case NODE_AUTO:
288 return C_("Path handle tip",
289 "<b>Auto node handle:</b> drag to convert to smooth node");
290 default:
291 return format_tip(C_("Path handle tip", "<b>%s:</b> drag to shape the curve"),
292 handle_type_to_localized_string(_parent->type()));
293 }
294 }
296 Glib::ustring Handle::_getDragTip(GdkEventMotion *event)
297 {
298 Geom::Point dist = position() - _last_drag_origin();
299 // report angle in mathematical convention
300 double angle = Geom::angle_between(Geom::Point(-1,0), position() - _parent->position());
301 angle += M_PI; // angle is (-M_PI...M_PI] - offset by +pi and scale to 0...360
302 angle *= 360.0 / (2 * M_PI);
303 GString *x = SP_PX_TO_METRIC_STRING(dist[Geom::X], _desktop->namedview->getDefaultMetric());
304 GString *y = SP_PX_TO_METRIC_STRING(dist[Geom::Y], _desktop->namedview->getDefaultMetric());
305 GString *len = SP_PX_TO_METRIC_STRING(length(), _desktop->namedview->getDefaultMetric());
306 Glib::ustring ret = format_tip(C_("Path handle tip",
307 "Move by %s, %s; angle %.2f°, length %s"), x->str, y->str, angle, len->str);
308 g_string_free(x, TRUE);
309 g_string_free(y, TRUE);
310 g_string_free(len, TRUE);
311 return ret;
312 }
314 /**
315 * @class Node
316 * Represents a curve endpoint in an editable path.
317 */
319 Node::Node(NodeSharedData const &data, Geom::Point const &initial_pos)
320 : SelectableControlPoint(data.desktop, initial_pos, Gtk::ANCHOR_CENTER,
321 SP_CTRL_SHAPE_DIAMOND, 9.0, *data.selection, &node_colors, data.node_group)
322 , _front(data, initial_pos, this)
323 , _back(data, initial_pos, this)
324 , _type(NODE_CUSP)
325 , _handles_shown(false)
326 {
327 // NOTE we do not set type here, because the handles are still degenerate
328 // connect to own grabbed signal - dragging out handles
329 signal_grabbed.connect(
330 sigc::mem_fun(*this, &Node::_grabbedHandler));
331 signal_dragged.connect( sigc::hide<0>(
332 sigc::mem_fun(*this, &Node::_draggedHandler)));
333 }
335 // NOTE: not using iterators won't make this much quicker because iterators can be 100% inlined.
336 Node *Node::_next()
337 {
338 NodeList::iterator n = NodeList::get_iterator(this).next();
339 if (n) return n.ptr();
340 return NULL;
341 }
342 Node *Node::_prev()
343 {
344 NodeList::iterator p = NodeList::get_iterator(this).prev();
345 if (p) return p.ptr();
346 return NULL;
347 }
349 void Node::move(Geom::Point const &new_pos)
350 {
351 // move handles when the node moves.
352 Geom::Point old_pos = position();
353 Geom::Point delta = new_pos - position();
354 setPosition(new_pos);
355 _front.setPosition(_front.position() + delta);
356 _back.setPosition(_back.position() + delta);
358 // if the node has a smooth handle after a line segment, it should be kept colinear
359 // with the segment
360 _fixNeighbors(old_pos, new_pos);
361 }
363 void Node::transform(Geom::Matrix const &m)
364 {
365 Geom::Point old_pos = position();
366 setPosition(position() * m);
367 _front.setPosition(_front.position() * m);
368 _back.setPosition(_back.position() * m);
370 /* Affine transforms keep handle invariants for smooth and symmetric nodes,
371 * but smooth nodes at ends of linear segments and auto nodes need special treatment */
372 _fixNeighbors(old_pos, position());
373 }
375 Geom::Rect Node::bounds()
376 {
377 Geom::Rect b(position(), position());
378 b.expandTo(_front.position());
379 b.expandTo(_back.position());
380 return b;
381 }
383 void Node::_fixNeighbors(Geom::Point const &old_pos, Geom::Point const &new_pos)
384 {
385 /* This method restores handle invariants for neighboring nodes,
386 * and invariants that are based on positions of those nodes for this one. */
388 /* Fix auto handles */
389 if (_type == NODE_AUTO) _updateAutoHandles();
390 if (old_pos != new_pos) {
391 if (_next() && _next()->_type == NODE_AUTO) _next()->_updateAutoHandles();
392 if (_prev() && _prev()->_type == NODE_AUTO) _prev()->_updateAutoHandles();
393 }
395 /* Fix smooth handles at the ends of linear segments.
396 * Rotate the appropriate handle to be colinear with the segment.
397 * If there is a smooth node at the other end of the segment, rotate it too. */
398 Handle *handle, *other_handle;
399 Node *other;
400 if (_is_line_segment(this, _next())) {
401 handle = &_back;
402 other = _next();
403 other_handle = &_next()->_front;
404 } else if (_is_line_segment(_prev(), this)) {
405 handle = &_front;
406 other = _prev();
407 other_handle = &_prev()->_back;
408 } else return;
410 if (_type == NODE_SMOOTH && !handle->isDegenerate()) {
411 handle->setDirection(other->position(), new_pos);
412 /*Geom::Point handle_delta = handle->position() - position();
413 Geom::Point new_delta = Geom::unit_vector(new_direction) * handle_delta.length();
414 handle->setPosition(position() + new_delta);*/
415 }
416 // also update the handle on the other end of the segment
417 if (other->_type == NODE_SMOOTH && !other_handle->isDegenerate()) {
418 other_handle->setDirection(new_pos, other->position());
419 /*
420 Geom::Point handle_delta2 = other_handle->position() - other->position();
421 Geom::Point new_delta2 = Geom::unit_vector(new_direction) * handle_delta2.length();
422 other_handle->setPosition(other->position() + new_delta2);*/
423 }
424 }
426 void Node::_updateAutoHandles()
427 {
428 // Recompute the position of automatic handles.
429 if (!_prev() || !_next()) {
430 _front.retract();
431 _back.retract();
432 return;
433 }
434 // TODO describe in detail what the code below does
435 Geom::Point vec_next = _next()->position() - position();
436 Geom::Point vec_prev = _prev()->position() - position();
437 double len_next = vec_next.length(), len_prev = vec_prev.length();
438 if (len_next > 0 && len_prev > 0) {
439 Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev);
440 _back.setRelativePos(-dir * (len_prev / 3));
441 _front.setRelativePos(dir * (len_next / 3));
442 } else {
443 _front.retract();
444 _back.retract();
445 }
446 }
448 void Node::showHandles(bool v)
449 {
450 _handles_shown = v;
451 if (!_front.isDegenerate()) _front.setVisible(v);
452 if (!_back.isDegenerate()) _back.setVisible(v);
453 }
455 /** Sets the node type and optionally restores the invariants associated with the given type.
456 * @param type The type to set
457 * @param update_handles Whether to restore invariants associated with the given type.
458 * Passing false is useful e.g. wen initially creating the path,
459 * and when making cusp nodes during some node algorithms.
460 * Pass true when used in response to an UI node type button.
461 */
462 void Node::setType(NodeType type, bool update_handles)
463 {
464 if (type == NODE_PICK_BEST) {
465 pickBestType();
466 updateState(); // The size of the control might have changed
467 return;
468 }
470 // if update_handles is true, adjust handle positions to match the node type
471 // handle degenerate handles appropriately
472 if (update_handles) {
473 switch (type) {
474 case NODE_CUSP:
475 // if the existing type is also NODE_CUSP, retract handles
476 if (_type == NODE_CUSP) {
477 _front.retract();
478 _back.retract();
479 }
480 break;
481 case NODE_AUTO:
482 // auto handles make no sense for endnodes
483 if (isEndNode()) return;
484 _updateAutoHandles();
485 break;
486 case NODE_SMOOTH: {
487 // rotate handles to be colinear
488 // for degenerate nodes set positions like auto handles
489 bool prev_line = _is_line_segment(_prev(), this);
490 bool next_line = _is_line_segment(this, _next());
491 if (isDegenerate()) {
492 _updateAutoHandles();
493 } else if (_front.isDegenerate()) {
494 // if the front handle is degenerate and this...next is a line segment,
495 // make back colinear; otherwise pull out the other handle
496 // to 1/3 of distance to prev
497 if (next_line) {
498 _back.setDirection(*_next(), *this);
499 } else if (_prev()) {
500 Geom::Point dir = direction(_back, *this);
501 _front.setRelativePos((_prev()->position() - position()).length() / 3 * dir);
502 }
503 } else if (_back.isDegenerate()) {
504 if (prev_line) {
505 _front.setDirection(*_prev(), *this);
506 } else if (_next()) {
507 Geom::Point dir = direction(_front, *this);
508 _back.setRelativePos((_next()->position() - position()).length() / 3 * dir);
509 }
510 } else {
511 // both handles are extended. make colinear while keeping length
512 // first make back colinear with the vector front ---> back,
513 // then make front colinear with back ---> node
514 // (not back ---> front because back's position was changed in the first call)
515 _back.setDirection(_front, _back);
516 _front.setDirection(_back, *this);
517 }
518 } break;
519 case NODE_SYMMETRIC:
520 if (isEndNode()) return; // symmetric handles make no sense for endnodes
521 if (isDegenerate()) {
522 // similar to auto handles but set the same length for both
523 Geom::Point vec_next = _next()->position() - position();
524 Geom::Point vec_prev = _prev()->position() - position();
525 double len_next = vec_next.length(), len_prev = vec_prev.length();
526 double len = (len_next + len_prev) / 6; // take 1/3 of average
527 if (len == 0) return;
529 Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev);
530 _back.setRelativePos(-dir * len);
531 _front.setRelativePos(dir * len);
532 } else {
533 // Both handles are extended. Compute average length, use direction from
534 // back handle to front handle. This also works correctly for degenerates
535 double len = (_front.length() + _back.length()) / 2;
536 Geom::Point dir = direction(_back, _front);
537 _front.setRelativePos(dir * len);
538 _back.setRelativePos(-dir * len);
539 }
540 break;
541 default: break;
542 }
543 }
544 _type = type;
545 _setShape(_node_type_to_shape(type));
546 updateState();
547 }
549 void Node::pickBestType()
550 {
551 _type = NODE_CUSP;
552 bool front_degen = _front.isDegenerate();
553 bool back_degen = _back.isDegenerate();
554 bool both_degen = front_degen && back_degen;
555 bool neither_degen = !front_degen && !back_degen;
556 do {
557 // if both handles are degenerate, do nothing
558 if (both_degen) break;
559 // if neither are degenerate, check their respective positions
560 if (neither_degen) {
561 Geom::Point front_delta = _front.position() - position();
562 Geom::Point back_delta = _back.position() - position();
563 // for now do not automatically make nodes symmetric, it can be annoying
564 /*if (Geom::are_near(front_delta, -back_delta)) {
565 _type = NODE_SYMMETRIC;
566 break;
567 }*/
568 if (Geom::are_near(Geom::unit_vector(front_delta),
569 Geom::unit_vector(-back_delta)))
570 {
571 _type = NODE_SMOOTH;
572 break;
573 }
574 }
575 // check whether the handle aligns with the previous line segment.
576 // we know that if front is degenerate, back isn't, because
577 // both_degen was false
578 if (front_degen && _next() && _next()->_back.isDegenerate()) {
579 Geom::Point segment_delta = Geom::unit_vector(_next()->position() - position());
580 Geom::Point handle_delta = Geom::unit_vector(_back.position() - position());
581 if (Geom::are_near(segment_delta, -handle_delta)) {
582 _type = NODE_SMOOTH;
583 break;
584 }
585 } else if (back_degen && _prev() && _prev()->_front.isDegenerate()) {
586 Geom::Point segment_delta = Geom::unit_vector(_prev()->position() - position());
587 Geom::Point handle_delta = Geom::unit_vector(_front.position() - position());
588 if (Geom::are_near(segment_delta, -handle_delta)) {
589 _type = NODE_SMOOTH;
590 break;
591 }
592 }
593 } while (false);
594 _setShape(_node_type_to_shape(_type));
595 updateState();
596 }
598 bool Node::isEndNode()
599 {
600 return !_prev() || !_next();
601 }
603 /** Move the node to the bottom of its canvas group. Useful for node break, to ensure that
604 * the selected nodes are above the unselected ones. */
605 void Node::sink()
606 {
607 sp_canvas_item_move_to_z(_canvas_item, 0);
608 }
610 NodeType Node::parse_nodetype(char x)
611 {
612 switch (x) {
613 case 'a': return NODE_AUTO;
614 case 'c': return NODE_CUSP;
615 case 's': return NODE_SMOOTH;
616 case 'z': return NODE_SYMMETRIC;
617 default: return NODE_PICK_BEST;
618 }
619 }
621 void Node::_setState(State state)
622 {
623 // change node size to match type and selection state
624 switch (_type) {
625 case NODE_AUTO:
626 case NODE_CUSP:
627 if (selected()) _setSize(11);
628 else _setSize(9);
629 break;
630 default:
631 if(selected()) _setSize(9);
632 else _setSize(7);
633 break;
634 }
635 SelectableControlPoint::_setState(state);
636 }
638 bool Node::_grabbedHandler(GdkEventMotion *event)
639 {
640 // dragging out handles
641 if (!held_shift(*event)) return false;
643 Handle *h;
644 Geom::Point evp = event_point(*event);
645 Geom::Point rel_evp = evp - _last_click_event_point();
647 // this should work even if dragtolerance is zero and evp coincides with node position
648 double angle_next = HUGE_VAL;
649 double angle_prev = HUGE_VAL;
650 bool has_degenerate = false;
651 // determine which handle to drag out based on degeneration and the direction of drag
652 if (_front.isDegenerate() && _next()) {
653 Geom::Point next_relpos = _desktop->d2w(_next()->position())
654 - _desktop->d2w(position());
655 angle_next = fabs(Geom::angle_between(rel_evp, next_relpos));
656 has_degenerate = true;
657 }
658 if (_back.isDegenerate() && _prev()) {
659 Geom::Point prev_relpos = _desktop->d2w(_prev()->position())
660 - _desktop->d2w(position());
661 angle_prev = fabs(Geom::angle_between(rel_evp, prev_relpos));
662 has_degenerate = true;
663 }
664 if (!has_degenerate) return false;
665 h = angle_next < angle_prev ? &_front : &_back;
667 h->setPosition(_desktop->w2d(evp));
668 h->setVisible(true);
669 h->transferGrab(this, event);
670 Handle::_drag_out = true;
671 return true;
672 }
674 void Node::_draggedHandler(Geom::Point &new_pos, GdkEventMotion *event)
675 {
676 if (!held_control(*event)) return;
677 if (held_alt(*event)) {
678 // with Ctrl+Alt, constrain to handle lines
679 // project the new position onto a handle line that is closer
680 Geom::Point origin = _last_drag_origin();
681 Geom::Line line_front(origin, origin + _front.relativePos());
682 Geom::Line line_back(origin, origin + _back.relativePos());
683 double dist_front, dist_back;
684 dist_front = Geom::distance(new_pos, line_front);
685 dist_back = Geom::distance(new_pos, line_back);
686 if (dist_front < dist_back) {
687 new_pos = Geom::projection(new_pos, line_front);
688 } else {
689 new_pos = Geom::projection(new_pos, line_back);
690 }
691 } else {
692 // with Ctrl, constrain to axes
693 // TODO this probably has to be separated into an AxisConstrainablePoint class
694 // TODO maybe add diagonals when the distance from origin is large enough?
695 Geom::Point origin = _last_drag_origin();
696 Geom::Point delta = new_pos - origin;
697 Geom::Dim2 d = (fabs(delta[Geom::X]) < fabs(delta[Geom::Y])) ? Geom::X : Geom::Y;
698 new_pos[d] = origin[d];
699 }
700 }
702 Glib::ustring Node::_getTip(unsigned state)
703 {
704 if (state_held_shift(state)) {
705 if ((_next() && _front.isDegenerate()) || (_prev() && _back.isDegenerate())) {
706 if (state_held_control(state)) {
707 return format_tip(C_("Path node tip",
708 "<b>Shift+Ctrl:</b> drag out a handle and snap its angle "
709 "to %f° increments"), snap_increment_degrees());
710 }
711 return C_("Path node tip",
712 "<b>Shift:</b> drag out a handle, click to toggle selection");
713 }
714 return C_("Path node statusbar tip", "<b>Shift:</b> click to toggle selection");
715 }
717 if (state_held_control(state)) {
718 if (state_held_alt(state)) {
719 return C_("Path node tip", "<b>Ctrl+Alt:</b> move along handle lines");
720 }
721 return C_("Path node tip",
722 "<b>Ctrl:</b> move along axes, click to change node type");
723 }
725 // assemble tip from node name
726 char const *nodetype = node_type_to_localized_string(_type);
727 return format_tip(C_("Path node tip",
728 "<b>%s:</b> drag to shape the path, click to select this node"), nodetype);
729 }
731 Glib::ustring Node::_getDragTip(GdkEventMotion *event)
732 {
733 Geom::Point dist = position() - _last_drag_origin();
734 GString *x = SP_PX_TO_METRIC_STRING(dist[Geom::X], _desktop->namedview->getDefaultMetric());
735 GString *y = SP_PX_TO_METRIC_STRING(dist[Geom::Y], _desktop->namedview->getDefaultMetric());
736 Glib::ustring ret = format_tip(C_("Path node statusbar tip", "Move by %s, %s"),
737 x->str, y->str);
738 g_string_free(x, TRUE);
739 g_string_free(y, TRUE);
740 return ret;
741 }
743 char const *Node::node_type_to_localized_string(NodeType type)
744 {
745 switch (type) {
746 case NODE_CUSP: return _("Cusp node");
747 case NODE_SMOOTH: return _("Smooth node");
748 case NODE_SYMMETRIC: return _("Symmetric node");
749 case NODE_AUTO: return _("Auto-smooth node");
750 default: return "";
751 }
752 }
754 /** Determine whether two nodes are joined by a linear segment. */
755 bool Node::_is_line_segment(Node *first, Node *second)
756 {
757 if (!first || !second) return false;
758 if (first->_next() == second)
759 return first->_front.isDegenerate() && second->_back.isDegenerate();
760 if (second->_next() == first)
761 return second->_front.isDegenerate() && first->_back.isDegenerate();
762 return false;
763 }
765 SPCtrlShapeType Node::_node_type_to_shape(NodeType type)
766 {
767 switch(type) {
768 case NODE_CUSP: return SP_CTRL_SHAPE_DIAMOND;
769 case NODE_SMOOTH: return SP_CTRL_SHAPE_SQUARE;
770 case NODE_AUTO: return SP_CTRL_SHAPE_CIRCLE;
771 case NODE_SYMMETRIC: return SP_CTRL_SHAPE_SQUARE;
772 default: return SP_CTRL_SHAPE_DIAMOND;
773 }
774 }
777 /**
778 * @class NodeList
779 * @brief An editable list of nodes representing a subpath.
780 *
781 * It can optionally be cyclic to represent a closed path.
782 * The list has iterators that act like plain node iterators, but can also be used
783 * to obtain shared pointers to nodes.
784 *
785 * @todo Manage geometric representation to improve speed
786 */
788 NodeList::NodeList(SubpathList &splist)
789 : _list(splist)
790 , _closed(false)
791 {
792 this->list = this;
793 this->next = this;
794 this->prev = this;
795 }
797 NodeList::~NodeList()
798 {
799 clear();
800 }
802 bool NodeList::empty()
803 {
804 return next == this;
805 }
807 NodeList::size_type NodeList::size()
808 {
809 size_type sz = 0;
810 for (ListNode *ln = next; ln != this; ln = ln->next) ++sz;
811 return sz;
812 }
814 bool NodeList::closed()
815 {
816 return _closed;
817 }
819 /** A subpath is degenerate if it has no segments - either one node in an open path
820 * or no nodes in a closed path */
821 bool NodeList::degenerate()
822 {
823 return closed() ? empty() : ++begin() == end();
824 }
826 NodeList::iterator NodeList::before(double t, double *fracpart)
827 {
828 double intpart;
829 *fracpart = std::modf(t, &intpart);
830 int index = intpart;
832 iterator ret = begin();
833 std::advance(ret, index);
834 return ret;
835 }
837 // insert a node before i
838 NodeList::iterator NodeList::insert(iterator i, Node *x)
839 {
840 ListNode *ins = i._node;
841 x->next = ins;
842 x->prev = ins->prev;
843 ins->prev->next = x;
844 ins->prev = x;
845 x->ListNode::list = this;
846 _list.signal_insert_node.emit(x);
847 return iterator(x);
848 }
850 void NodeList::splice(iterator pos, NodeList &list)
851 {
852 splice(pos, list, list.begin(), list.end());
853 }
855 void NodeList::splice(iterator pos, NodeList &list, iterator i)
856 {
857 NodeList::iterator j = i;
858 ++j;
859 splice(pos, list, i, j);
860 }
862 void NodeList::splice(iterator pos, NodeList &list, iterator first, iterator last)
863 {
864 ListNode *ins_beg = first._node, *ins_end = last._node, *at = pos._node;
865 for (ListNode *ln = ins_beg; ln != ins_end; ln = ln->next) {
866 list._list.signal_remove_node.emit(static_cast<Node*>(ln));
867 ln->list = this;
868 _list.signal_insert_node.emit(static_cast<Node*>(ln));
869 }
870 ins_beg->prev->next = ins_end;
871 ins_end->prev->next = at;
872 at->prev->next = ins_beg;
874 ListNode *atprev = at->prev;
875 at->prev = ins_end->prev;
876 ins_end->prev = ins_beg->prev;
877 ins_beg->prev = atprev;
878 }
880 void NodeList::shift(int n)
881 {
882 // 1. make the list perfectly cyclic
883 next->prev = prev;
884 prev->next = next;
885 // 2. find new begin
886 ListNode *new_begin = next;
887 if (n > 0) {
888 for (; n > 0; --n) new_begin = new_begin->next;
889 } else {
890 for (; n < 0; ++n) new_begin = new_begin->prev;
891 }
892 // 3. relink begin to list
893 next = new_begin;
894 prev = new_begin->prev;
895 new_begin->prev->next = this;
896 new_begin->prev = this;
897 }
899 void NodeList::reverse()
900 {
901 for (ListNode *ln = next; ln != this; ln = ln->prev) {
902 std::swap(ln->next, ln->prev);
903 Node *node = static_cast<Node*>(ln);
904 Geom::Point save_pos = node->front()->position();
905 node->front()->setPosition(node->back()->position());
906 node->back()->setPosition(save_pos);
907 }
908 std::swap(next, prev);
909 }
911 void NodeList::clear()
912 {
913 for (iterator i = begin(); i != end();) erase (i++);
914 }
916 NodeList::iterator NodeList::erase(iterator i)
917 {
918 // some acrobatics are required to ensure that the node is valid when deleted;
919 // otherwise the code that updates handle visibility will break
920 Node *rm = static_cast<Node*>(i._node);
921 ListNode *rmnext = rm->next, *rmprev = rm->prev;
922 ++i;
923 _list.signal_remove_node.emit(rm);
924 delete rm;
925 rmprev->next = rmnext;
926 rmnext->prev = rmprev;
927 return i;
928 }
930 void NodeList::kill()
931 {
932 for (SubpathList::iterator i = _list.begin(); i != _list.end(); ++i) {
933 if (i->get() == this) {
934 _list.erase(i);
935 return;
936 }
937 }
938 }
940 NodeList &NodeList::get(Node *n) {
941 return *(n->list());
942 }
943 NodeList &NodeList::get(iterator const &i) {
944 return *(i._node->list);
945 }
948 /**
949 * @class SubpathList
950 * @brief Editable path composed of one or more subpaths
951 */
953 } // namespace UI
954 } // namespace Inkscape
956 /*
957 Local Variables:
958 mode:c++
959 c-file-style:"stroustrup"
960 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
961 indent-tabs-mode:nil
962 fill-column:99
963 End:
964 */
965 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :