615587eebb7aaa62bc38277c7978bae2b936ca48
1 /** @file
2 * Node selection - 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 <boost/none.hpp>
12 #include <2geom/transforms.h>
13 #include "desktop.h"
14 #include "preferences.h"
15 #include "ui/tool/control-point-selection.h"
16 #include "ui/tool/event-utils.h"
17 #include "ui/tool/selectable-control-point.h"
18 #include "ui/tool/transform-handle-set.h"
20 namespace Inkscape {
21 namespace UI {
23 /**
24 * @class ControlPointSelection
25 * @brief Group of selected control points.
26 *
27 * Some operations can be performed on all selected points regardless of their type, therefore
28 * this class is also a Manipulator. It handles the transformations of points using
29 * the keyboard.
30 *
31 * The exposed interface is similar to that of an STL set. Internally, a hash map is used.
32 * @todo Correct iterators (that don't expose the connection list)
33 */
35 /** @var ControlPointSelection::signal_update
36 * Fires when the display needs to be updated to reflect changes.
37 */
38 /** @var ControlPointSelection::signal_point_changed
39 * Fires when a control point is added to or removed from the selection.
40 * The first param contains a pointer to the control point that changed sel. state.
41 * The second says whether the point is currently selected.
42 */
43 /** @var ControlPointSelection::signal_commit
44 * Fires when a change that needs to be committed to XML happens.
45 */
47 ControlPointSelection::ControlPointSelection(SPDesktop *d, SPCanvasGroup *th_group)
48 : Manipulator(d)
49 , _handles(new TransformHandleSet(d, th_group))
50 , _dragging(false)
51 , _handles_visible(true)
52 , _one_node_handles(false)
53 {
54 signal_update.connect( sigc::bind(
55 sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
56 true));
57 ControlPoint::signal_mouseover_change.connect(
58 sigc::hide(
59 sigc::mem_fun(*this, &ControlPointSelection::_mouseoverChanged)));
60 _handles->signal_transform.connect(
61 sigc::mem_fun(*this, &ControlPointSelection::transform));
62 _handles->signal_commit.connect(
63 sigc::mem_fun(*this, &ControlPointSelection::_commitHandlesTransform));
64 }
66 ControlPointSelection::~ControlPointSelection()
67 {
68 clear();
69 delete _handles;
70 }
72 /** Add a control point to the selection. */
73 std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(const value_type &x)
74 {
75 iterator found = _points.find(x);
76 if (found != _points.end()) {
77 return std::pair<iterator, bool>(found, false);
78 }
80 found = _points.insert(x).first;
82 x->updateState();
83 _pointChanged(x, true);
85 return std::pair<iterator, bool>(found, true);
86 }
88 /** Remove a point from the selection. */
89 void ControlPointSelection::erase(iterator pos)
90 {
91 SelectableControlPoint *erased = *pos;
92 _points.erase(pos);
93 erased->updateState();
94 _pointChanged(erased, false);
95 }
96 ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k)
97 {
98 iterator pos = _points.find(k);
99 if (pos == _points.end()) return 0;
100 erase(pos);
101 return 1;
102 }
103 void ControlPointSelection::erase(iterator first, iterator last)
104 {
105 while (first != last) erase(first++);
106 }
108 /** Remove all points from the selection, making it empty. */
109 void ControlPointSelection::clear()
110 {
111 for (iterator i = begin(); i != end(); )
112 erase(i++);
113 }
115 /** Select all points that this selection can contain. */
116 void ControlPointSelection::selectAll()
117 {
118 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
119 insert(*i);
120 }
121 }
122 /** Select all points inside the given rectangle (in desktop coordinates). */
123 void ControlPointSelection::selectArea(Geom::Rect const &r)
124 {
125 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
126 if (r.contains(**i))
127 insert(*i);
128 }
129 }
130 /** Unselect all selected points and select all unselected points. */
131 void ControlPointSelection::invertSelection()
132 {
133 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
134 if ((*i)->selected()) erase(*i);
135 else insert(*i);
136 }
137 }
138 void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir)
139 {
140 bool grow = (dir > 0);
141 Geom::Point p = origin->position();
142 double best_dist = grow ? HUGE_VAL : 0;
143 SelectableControlPoint *match = NULL;
144 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
145 bool selected = (*i)->selected();
146 if (grow && !selected) {
147 double dist = Geom::distance((*i)->position(), p);
148 if (dist < best_dist) {
149 best_dist = dist;
150 match = *i;
151 }
152 }
153 if (!grow && selected) {
154 double dist = Geom::distance((*i)->position(), p);
155 // use >= to also deselect the origin node when it's the last one selected
156 if (dist >= best_dist) {
157 best_dist = dist;
158 match = *i;
159 }
160 }
161 }
162 if (match) {
163 if (grow) insert(match);
164 else erase(match);
165 }
166 }
168 /** Transform all selected control points by the given affine transformation. */
169 void ControlPointSelection::transform(Geom::Matrix const &m)
170 {
171 for (iterator i = _points.begin(); i != _points.end(); ++i) {
172 SelectableControlPoint *cur = *i;
173 cur->transform(m);
174 }
175 _updateBounds();
176 // TODO preserving the rotation radius needs some rethinking...
177 if (_rot_radius) (*_rot_radius) *= m.descrim();
178 if (_mouseover_rot_radius) (*_mouseover_rot_radius) *= m.descrim();
179 signal_update.emit();
180 }
182 /** Align control points on the specified axis. */
183 void ControlPointSelection::align(Geom::Dim2 axis)
184 {
185 if (empty()) return;
186 Geom::Dim2 d = static_cast<Geom::Dim2>((axis + 1) % 2);
188 Geom::OptInterval bound;
189 for (iterator i = _points.begin(); i != _points.end(); ++i) {
190 bound.unionWith(Geom::OptInterval((*i)->position()[d]));
191 }
193 double new_coord = bound->middle();
194 for (iterator i = _points.begin(); i != _points.end(); ++i) {
195 Geom::Point pos = (*i)->position();
196 pos[d] = new_coord;
197 (*i)->move(pos);
198 }
199 }
201 /** Equdistantly distribute control points by moving them in the specified dimension. */
202 void ControlPointSelection::distribute(Geom::Dim2 d)
203 {
204 if (empty()) return;
206 // this needs to be a multimap, otherwise it will fail when some points have the same coord
207 typedef std::multimap<double, SelectableControlPoint*> SortMap;
209 SortMap sm;
210 Geom::OptInterval bound;
211 // first we insert all points into a multimap keyed by the aligned coord to sort them
212 // simultaneously we compute the extent of selection
213 for (iterator i = _points.begin(); i != _points.end(); ++i) {
214 Geom::Point pos = (*i)->position();
215 sm.insert(std::make_pair(pos[d], (*i)));
216 bound.unionWith(Geom::OptInterval(pos[d]));
217 }
219 // now we iterate over the multimap and set aligned positions.
220 double step = size() == 1 ? 0 : bound->extent() / (size() - 1);
221 double start = bound->min();
222 unsigned num = 0;
223 for (SortMap::iterator i = sm.begin(); i != sm.end(); ++i, ++num) {
224 Geom::Point pos = i->second->position();
225 pos[d] = start + num * step;
226 i->second->move(pos);
227 }
228 }
230 /** Get the bounds of the selection.
231 * @return Smallest rectangle containing the positions of all selected points,
232 * or nothing if the selection is empty */
233 Geom::OptRect ControlPointSelection::pointwiseBounds()
234 {
235 return _bounds;
236 }
238 Geom::OptRect ControlPointSelection::bounds()
239 {
240 return size() == 1 ? (*_points.begin())->bounds() : _bounds;
241 }
243 void ControlPointSelection::showTransformHandles(bool v, bool one_node)
244 {
245 _one_node_handles = one_node;
246 _handles_visible = v;
247 _updateTransformHandles(false);
248 }
250 void ControlPointSelection::hideTransformHandles()
251 {
252 _handles->setVisible(false);
253 }
254 void ControlPointSelection::restoreTransformHandles()
255 {
256 _updateTransformHandles(true);
257 }
259 void ControlPointSelection::toggleTransformHandlesMode()
260 {
261 if (_handles->mode() == TransformHandleSet::MODE_SCALE) {
262 _handles->setMode(TransformHandleSet::MODE_ROTATE_SKEW);
263 if (size() == 1) _handles->rotationCenter().setVisible(false);
264 } else {
265 _handles->setMode(TransformHandleSet::MODE_SCALE);
266 }
267 }
269 void ControlPointSelection::_pointGrabbed(SelectableControlPoint *point)
270 {
271 hideTransformHandles();
272 _dragging = true;
273 _grabbed_point = point;
274 _farthest_point = point;
275 double maxdist = 0;
276 Geom::Matrix m;
277 m.setIdentity();
278 for (iterator i = _points.begin(); i != _points.end(); ++i) {
279 _original_positions.insert(std::make_pair(*i, (*i)->position()));
280 _last_trans.insert(std::make_pair(*i, m));
281 double dist = Geom::distance(*_grabbed_point, **i);
282 if (dist > maxdist) {
283 maxdist = dist;
284 _farthest_point = *i;
285 }
286 }
287 }
289 void ControlPointSelection::_pointDragged(Geom::Point &new_pos, GdkEventMotion *event)
290 {
291 Geom::Point abs_delta = new_pos - _original_positions[_grabbed_point];
292 double fdist = Geom::distance(_original_positions[_grabbed_point], _original_positions[_farthest_point]);
293 if (held_alt(*event) && fdist > 0) {
294 // Sculpting
295 for (iterator i = _points.begin(); i != _points.end(); ++i) {
296 SelectableControlPoint *cur = (*i);
297 Geom::Matrix trans;
298 trans.setIdentity();
299 double dist = Geom::distance(_original_positions[cur], _original_positions[_grabbed_point]);
300 double deltafrac = 0.5 + 0.5 * cos(M_PI * dist/fdist);
301 if (dist != 0.0) {
302 // The sculpting transformation is not affine, but it can be
303 // locally approximated by one. Here we compute the local
304 // affine approximation of the sculpting transformation near
305 // the currently transformed point. We then transform the point
306 // by this approximation. This gives us sensible behavior for node handles.
307 // NOTE: probably it would be better to transform the node handles,
308 // but ControlPointSelection is supposed to work for any
309 // SelectableControlPoints, not only Nodes. We could create a specialized
310 // NodeSelection class that inherits from this one and move sculpting there.
311 Geom::Point origdx(Geom::EPSILON, 0);
312 Geom::Point origdy(0, Geom::EPSILON);
313 Geom::Point origp = _original_positions[cur];
314 Geom::Point origpx = _original_positions[cur] + origdx;
315 Geom::Point origpy = _original_positions[cur] + origdy;
316 double distdx = Geom::distance(origpx, _original_positions[_grabbed_point]);
317 double distdy = Geom::distance(origpy, _original_positions[_grabbed_point]);
318 double deltafracdx = 0.5 + 0.5 * cos(M_PI * distdx/fdist);
319 double deltafracdy = 0.5 + 0.5 * cos(M_PI * distdy/fdist);
320 Geom::Point newp = origp + abs_delta * deltafrac;
321 Geom::Point newpx = origpx + abs_delta * deltafracdx;
322 Geom::Point newpy = origpy + abs_delta * deltafracdy;
323 Geom::Point newdx = (newpx - newp) / Geom::EPSILON;
324 Geom::Point newdy = (newpy - newp) / Geom::EPSILON;
326 Geom::Matrix itrans(newdx[Geom::X], newdx[Geom::Y], newdy[Geom::X], newdy[Geom::Y], 0, 0);
327 if (itrans.isSingular())
328 itrans.setIdentity();
330 trans *= Geom::Translate(-cur->position());
331 trans *= _last_trans[cur].inverse();
332 trans *= itrans;
333 trans *= Geom::Translate(_original_positions[cur] + abs_delta * deltafrac);
334 _last_trans[cur] = itrans;
335 } else {
336 trans *= Geom::Translate(-cur->position() + _original_positions[cur] + abs_delta * deltafrac);
337 }
338 cur->transform(trans);
339 //cur->move(_original_positions[cur] + abs_delta * deltafrac);
340 }
341 } else {
342 Geom::Point delta = new_pos - _grabbed_point->position();
343 for (iterator i = _points.begin(); i != _points.end(); ++i) {
344 SelectableControlPoint *cur = (*i);
345 cur->move(_original_positions[cur] + abs_delta);
346 }
347 _handles->rotationCenter().move(_handles->rotationCenter().position() + delta);
348 }
349 signal_update.emit();
350 }
352 void ControlPointSelection::_pointUngrabbed()
353 {
354 _original_positions.clear();
355 _last_trans.clear();
356 _dragging = false;
357 _grabbed_point = _farthest_point = NULL;
358 _updateBounds();
359 restoreTransformHandles();
360 signal_commit.emit(COMMIT_MOUSE_MOVE);
361 }
363 bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventButton *event)
364 {
365 // clicking a selected node should toggle the transform handles between rotate and scale mode,
366 // if they are visible
367 if (held_no_modifiers(*event) && _handles_visible && p->selected()) {
368 toggleTransformHandlesMode();
369 return true;
370 }
371 return false;
372 }
374 void ControlPointSelection::_pointChanged(SelectableControlPoint *p, bool selected)
375 {
376 _updateBounds();
377 _updateTransformHandles(false);
378 if (_bounds)
379 _handles->rotationCenter().move(_bounds->midpoint());
381 signal_point_changed.emit(p, selected);
382 }
384 void ControlPointSelection::_mouseoverChanged()
385 {
386 _mouseover_rot_radius = boost::none;
387 }
389 void ControlPointSelection::_updateBounds()
390 {
391 _rot_radius = boost::none;
392 _bounds = Geom::OptRect();
393 for (iterator i = _points.begin(); i != _points.end(); ++i) {
394 SelectableControlPoint *cur = (*i);
395 Geom::Point p = cur->position();
396 if (!_bounds) {
397 _bounds = Geom::Rect(p, p);
398 } else {
399 _bounds->expandTo(p);
400 }
401 }
402 }
404 void ControlPointSelection::_updateTransformHandles(bool preserve_center)
405 {
406 if (_dragging) return;
408 if (_handles_visible && size() > 1) {
409 _handles->setBounds(*bounds(), preserve_center);
410 _handles->setVisible(true);
411 } else if (_one_node_handles && size() == 1) { // only one control point in selection
412 SelectableControlPoint *p = *begin();
413 _handles->setBounds(p->bounds());
414 _handles->rotationCenter().move(p->position());
415 _handles->rotationCenter().setVisible(false);
416 _handles->setVisible(true);
417 } else {
418 _handles->setVisible(false);
419 }
420 }
422 /** Moves the selected points along the supplied unit vector according to
423 * the modifier state of the supplied event. */
424 bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir)
425 {
426 if (held_control(event)) return false;
427 unsigned num = 1 + combine_key_events(shortcut_key(event), 0);
429 Geom::Point delta = dir * num;
430 if (held_shift(event)) delta *= 10;
431 if (held_alt(event)) {
432 delta /= _desktop->current_zoom();
433 } else {
434 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
435 double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000);
436 delta *= nudge;
437 }
439 transform(Geom::Translate(delta));
440 if (fabs(dir[Geom::X]) > 0) {
441 signal_commit.emit(COMMIT_KEYBOARD_MOVE_X);
442 } else {
443 signal_commit.emit(COMMIT_KEYBOARD_MOVE_Y);
444 }
445 return true;
446 }
448 /** @brief Computes the distance to the farthest corner of the bounding box.
449 * Used to determine what it means to "rotate by one pixel". */
450 double ControlPointSelection::_rotationRadius(Geom::Point const &rc)
451 {
452 if (empty()) return 1.0; // some safe value
453 Geom::Rect b = *bounds();
454 double maxlen = 0;
455 for (unsigned i = 0; i < 4; ++i) {
456 double len = Geom::distance(b.corner(i), rc);
457 if (len > maxlen) maxlen = len;
458 }
459 return maxlen;
460 }
462 /** Rotates the selected points in the given direction according to the modifier state
463 * from the supplied event.
464 * @param event Key event to take modifier state from
465 * @param dir Direction of rotation (math convention: 1 = counterclockwise, -1 = clockwise)
466 */
467 bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
468 {
469 if (empty()) return false;
471 Geom::Point rc;
473 // rotate around the mouseovered point, or the selection's rotation center
474 // if nothing is mouseovered
475 double radius;
476 SelectableControlPoint *scp =
477 dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
478 if (scp) {
479 rc = scp->position();
480 if (!_mouseover_rot_radius) {
481 _mouseover_rot_radius = _rotationRadius(rc);
482 }
483 radius = *_mouseover_rot_radius;
484 } else {
485 rc = _handles->rotationCenter();
486 if (!_rot_radius) {
487 _rot_radius = _rotationRadius(rc);
488 }
489 radius = *_rot_radius;
490 }
492 double angle;
493 if (held_alt(event)) {
494 // Rotate by "one pixel". We interpret this as rotating by an angle that causes
495 // the topmost point of a circle circumscribed about the selection's bounding box
496 // to move on an arc 1 screen pixel long.
497 angle = atan2(1.0 / _desktop->current_zoom(), radius) * dir;
498 } else {
499 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
500 int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
501 angle = M_PI * dir / snaps;
502 }
504 // translate to origin, rotate, translate back to original position
505 Geom::Matrix m = Geom::Translate(-rc)
506 * Geom::Rotate(angle) * Geom::Translate(rc);
507 transform(m);
508 signal_commit.emit(COMMIT_KEYBOARD_ROTATE);
509 return true;
510 }
513 bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir)
514 {
515 if (empty()) return false;
517 double maxext = bounds()->maxExtent();
518 if (Geom::are_near(maxext, 0)) return false;
520 Geom::Point center;
521 SelectableControlPoint *scp =
522 dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
523 if (scp) {
524 center = scp->position();
525 } else {
526 center = _handles->rotationCenter().position();
527 }
529 double length_change;
530 if (held_alt(event)) {
531 // Scale by "one pixel". It means shrink/grow 1px for the larger dimension
532 // of the bounding box.
533 length_change = 1.0 / _desktop->current_zoom() * dir;
534 } else {
535 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
536 length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000);
537 length_change *= dir;
538 }
539 double scale = (maxext + length_change) / maxext;
541 Geom::Matrix m = Geom::Translate(-center) * Geom::Scale(scale) * Geom::Translate(center);
542 transform(m);
543 signal_commit.emit(COMMIT_KEYBOARD_SCALE_UNIFORM);
544 return true;
545 }
547 bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d)
548 {
549 if (empty()) return false;
551 Geom::Scale scale_transform(1, 1);
552 if (d == Geom::X) {
553 scale_transform = Geom::Scale(-1, 1);
554 } else {
555 scale_transform = Geom::Scale(1, -1);
556 }
558 SelectableControlPoint *scp =
559 dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
560 Geom::Point center = scp ? scp->position() : _handles->rotationCenter().position();
562 Geom::Matrix m = Geom::Translate(-center) * scale_transform * Geom::Translate(center);
563 transform(m);
564 signal_commit.emit(d == Geom::X ? COMMIT_FLIP_X : COMMIT_FLIP_Y);
565 return true;
566 }
568 void ControlPointSelection::_commitHandlesTransform(CommitEvent ce)
569 {
570 _updateBounds();
571 _updateTransformHandles(true);
572 signal_commit.emit(ce);
573 }
575 bool ControlPointSelection::event(GdkEvent *event)
576 {
577 // implement generic event handling that should apply for all control point selections here;
578 // for example, keyboard moves and transformations. This way this functionality doesn't need
579 // to be duplicated in many places
580 // Later split out so that it can be reused in object selection
582 switch (event->type) {
583 case GDK_KEY_PRESS:
584 // do not handle key events if the selection is empty
585 if (empty()) break;
587 switch(shortcut_key(event->key)) {
588 // moves
589 case GDK_Up:
590 case GDK_KP_Up:
591 case GDK_KP_8:
592 return _keyboardMove(event->key, Geom::Point(0, 1));
593 case GDK_Down:
594 case GDK_KP_Down:
595 case GDK_KP_2:
596 return _keyboardMove(event->key, Geom::Point(0, -1));
597 case GDK_Right:
598 case GDK_KP_Right:
599 case GDK_KP_6:
600 return _keyboardMove(event->key, Geom::Point(1, 0));
601 case GDK_Left:
602 case GDK_KP_Left:
603 case GDK_KP_4:
604 return _keyboardMove(event->key, Geom::Point(-1, 0));
606 // rotates
607 case GDK_bracketleft:
608 return _keyboardRotate(event->key, 1);
609 case GDK_bracketright:
610 return _keyboardRotate(event->key, -1);
612 // scaling
613 case GDK_less:
614 case GDK_comma:
615 return _keyboardScale(event->key, -1);
616 case GDK_greater:
617 case GDK_period:
618 return _keyboardScale(event->key, 1);
620 // TODO: skewing
622 // flipping
623 // NOTE: H is horizontal flip, while Shift+H switches transform handle mode!
624 case GDK_h:
625 case GDK_H:
626 if (held_shift(event->key)) {
627 toggleTransformHandlesMode();
628 return true;
629 }
630 // any modifiers except shift should cause no action
631 if (held_any_modifiers(event->key)) break;
632 return _keyboardFlip(Geom::X);
633 case GDK_v:
634 case GDK_V:
635 if (held_any_modifiers(event->key)) break;
636 return _keyboardFlip(Geom::Y);
637 default: break;
638 }
639 break;
640 default: break;
641 }
642 return false;
643 }
645 } // namespace UI
646 } // namespace Inkscape
648 /*
649 Local Variables:
650 mode:c++
651 c-file-style:"stroustrup"
652 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
653 indent-tabs-mode:nil
654 fill-column:99
655 End:
656 */
657 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :