Code

Node tool: snap while scaling a selection of nodes. Consider this as experimental...
[inkscape.git] / src / ui / tool / control-point-selection.cpp
index 245feae1d2bed10e132a6c0090936822b74ec8e2..517e90da22d5e6a6e5bcaca6c6237e7011c1f227 100644 (file)
@@ -8,6 +8,7 @@
  * Released under GNU GPL, read the file 'COPYING' for more information
  */
 
+#include <boost/none.hpp>
 #include <2geom/transforms.h>
 #include "desktop.h"
 #include "preferences.h"
@@ -53,15 +54,13 @@ ControlPointSelection::ControlPointSelection(SPDesktop *d, SPCanvasGroup *th_gro
     signal_update.connect( sigc::bind(
         sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
         true));
-    signal_point_changed.connect(
-        sigc::hide( sigc::hide(
-            sigc::bind(
-                sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
-                false))));
+    ControlPoint::signal_mouseover_change.connect(
+        sigc::hide(
+            sigc::mem_fun(*this, &ControlPointSelection::_mouseoverChanged)));
     _handles->signal_transform.connect(
         sigc::mem_fun(*this, &ControlPointSelection::transform));
     _handles->signal_commit.connect(
-        sigc::mem_fun(*this, &ControlPointSelection::_commitTransform));
+        sigc::mem_fun(*this, &ControlPointSelection::_commitHandlesTransform));
 }
 
 ControlPointSelection::~ControlPointSelection()
@@ -78,35 +77,10 @@ std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(c
         return std::pair<iterator, bool>(found, false);
     }
 
-    boost::shared_ptr<connlist_type> clist(new connlist_type());
-
-    // hide event param and always return false
-    /*clist->push_back(
-        x->signal_grabbed.connect(
-            sigc::bind_return(
-                sigc::bind<0>(
-                    sigc::mem_fun(*this, &ControlPointSelection::_selectionGrabbed),
-                    x),
-                false)));
-    clist->push_back(
-        x->signal_dragged.connect(
-                sigc::mem_fun(*this, &ControlPointSelection::_selectionDragged)));
-    clist->push_back(
-        x->signal_ungrabbed.connect(
-            sigc::hide(
-                sigc::mem_fun(*this, &ControlPointSelection::_selectionUngrabbed))));
-    clist->push_back(
-        x->signal_clicked.connect(
-            sigc::hide(
-                sigc::bind<0>(
-                    sigc::mem_fun(*this, &ControlPointSelection::_selectionClicked),
-                    x))));*/
-
-    found = _points.insert(std::make_pair(x, clist)).first;
+    found = _points.insert(x).first;
 
     x->updateState();
-    _rot_radius.reset();
-    signal_point_changed.emit(x, true);
+    _pointChanged(x, true);
 
     return std::pair<iterator, bool>(found, true);
 }
@@ -114,15 +88,10 @@ std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(c
 /** Remove a point from the selection. */
 void ControlPointSelection::erase(iterator pos)
 {
-    SelectableControlPoint *erased = pos->first;
-    boost::shared_ptr<connlist_type> clist = pos->second;
-    for (connlist_type::iterator i = clist->begin(); i != clist->end(); ++i) {
-        i->disconnect();
-    }
+    SelectableControlPoint *erased = *pos;
     _points.erase(pos);
     erased->updateState();
-    _rot_radius.reset();
-    signal_point_changed.emit(erased, false);
+    _pointChanged(erased, false);
 }
 ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k)
 {
@@ -200,11 +169,13 @@ void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir)
 void ControlPointSelection::transform(Geom::Matrix const &m)
 {
     for (iterator i = _points.begin(); i != _points.end(); ++i) {
-        SelectableControlPoint *cur = i->first;
+        SelectableControlPoint *cur = *i;
         cur->transform(m);
     }
+    _updateBounds();
     // TODO preserving the rotation radius needs some rethinking...
     if (_rot_radius) (*_rot_radius) *= m.descrim();
+    if (_mouseover_rot_radius) (*_mouseover_rot_radius) *= m.descrim();
     signal_update.emit();
 }
 
@@ -216,14 +187,14 @@ void ControlPointSelection::align(Geom::Dim2 axis)
 
     Geom::OptInterval bound;
     for (iterator i = _points.begin(); i != _points.end(); ++i) {
-        bound.unionWith(Geom::OptInterval(i->first->position()[d]));
+        bound.unionWith(Geom::OptInterval((*i)->position()[d]));
     }
 
     double new_coord = bound->middle();
     for (iterator i = _points.begin(); i != _points.end(); ++i) {
-        Geom::Point pos = i->first->position();
+        Geom::Point pos = (*i)->position();
         pos[d] = new_coord;
-        i->first->move(pos);
+        (*i)->move(pos);
     }
 }
 
@@ -240,8 +211,8 @@ void ControlPointSelection::distribute(Geom::Dim2 d)
     // first we insert all points into a multimap keyed by the aligned coord to sort them
     // simultaneously we compute the extent of selection
     for (iterator i = _points.begin(); i != _points.end(); ++i) {
-        Geom::Point pos = i->first->position();
-        sm.insert(std::make_pair(pos[d], i->first));
+        Geom::Point pos = (*i)->position();
+        sm.insert(std::make_pair(pos[d], (*i)));
         bound.unionWith(Geom::OptInterval(pos[d]));
     }
 
@@ -261,28 +232,12 @@ void ControlPointSelection::distribute(Geom::Dim2 d)
  *         or nothing if the selection is empty */
 Geom::OptRect ControlPointSelection::pointwiseBounds()
 {
-    Geom::OptRect bound;
-    for (iterator i = _points.begin(); i != _points.end(); ++i) {
-        SelectableControlPoint *cur = i->first;
-        Geom::Point p = cur->position();
-        if (!bound) {
-            bound = Geom::Rect(p, p);
-        } else {
-            bound->expandTo(p);
-        }
-    }
-    return bound;
+    return _bounds;
 }
 
 Geom::OptRect ControlPointSelection::bounds()
 {
-    Geom::OptRect bound;
-    for (iterator i = _points.begin(); i != _points.end(); ++i) {
-        SelectableControlPoint *cur = i->first;
-        Geom::OptRect r = cur->bounds();
-        bound.unionWith(r);
-    }
-    return bound;
+    return size() == 1 ? (*_points.begin())->bounds() : _bounds;
 }
 
 void ControlPointSelection::showTransformHandles(bool v, bool one_node)
@@ -311,28 +266,96 @@ void ControlPointSelection::toggleTransformHandlesMode()
     }
 }
 
-void ControlPointSelection::_pointGrabbed()
+void ControlPointSelection::_pointGrabbed(SelectableControlPoint *point)
 {
     hideTransformHandles();
     _dragging = true;
+    _grabbed_point = point;
+    _farthest_point = point;
+    double maxdist = 0;
+    Geom::Matrix m;
+    m.setIdentity();
+    for (iterator i = _points.begin(); i != _points.end(); ++i) {
+        _original_positions.insert(std::make_pair(*i, (*i)->position()));
+        _last_trans.insert(std::make_pair(*i, m));
+        double dist = Geom::distance(*_grabbed_point, **i);
+        if (dist > maxdist) {
+            maxdist = dist;
+            _farthest_point = *i;
+        }
+    }
 }
 
-void ControlPointSelection::_pointDragged(Geom::Point const &old_pos, Geom::Point &new_pos,
-    GdkEventMotion *event)
+void ControlPointSelection::_pointDragged(Geom::Point &new_pos, GdkEventMotion *event)
 {
-    Geom::Point delta = new_pos - old_pos;
-    for (iterator i = _points.begin(); i != _points.end(); ++i) {
-        SelectableControlPoint *cur = i->first;
-        cur->move(cur->position() + delta);
+    Geom::Point abs_delta = new_pos - _original_positions[_grabbed_point];
+    double fdist = Geom::distance(_original_positions[_grabbed_point], _original_positions[_farthest_point]);
+    if (held_only_alt(*event) && fdist > 0) {
+        // Sculpting
+        for (iterator i = _points.begin(); i != _points.end(); ++i) {
+            SelectableControlPoint *cur = (*i);
+            Geom::Matrix trans;
+            trans.setIdentity();
+            double dist = Geom::distance(_original_positions[cur], _original_positions[_grabbed_point]);
+            double deltafrac = 0.5 + 0.5 * cos(M_PI * dist/fdist);
+            if (dist != 0.0) {
+                // The sculpting transformation is not affine, but it can be
+                // locally approximated by one. Here we compute the local
+                // affine approximation of the sculpting transformation near
+                // the currently transformed point. We then transform the point
+                // by this approximation. This gives us sensible behavior for node handles.
+                // NOTE: probably it would be better to transform the node handles,
+                // but ControlPointSelection is supposed to work for any
+                // SelectableControlPoints, not only Nodes. We could create a specialized
+                // NodeSelection class that inherits from this one and move sculpting there.
+                Geom::Point origdx(Geom::EPSILON, 0);
+                Geom::Point origdy(0, Geom::EPSILON);
+                Geom::Point origp = _original_positions[cur];
+                Geom::Point origpx = _original_positions[cur] + origdx;
+                Geom::Point origpy = _original_positions[cur] + origdy;
+                double distdx = Geom::distance(origpx, _original_positions[_grabbed_point]);
+                double distdy = Geom::distance(origpy, _original_positions[_grabbed_point]);
+                double deltafracdx = 0.5 + 0.5 * cos(M_PI * distdx/fdist);
+                double deltafracdy = 0.5 + 0.5 * cos(M_PI * distdy/fdist);
+                Geom::Point newp = origp + abs_delta * deltafrac;
+                Geom::Point newpx = origpx + abs_delta * deltafracdx;
+                Geom::Point newpy = origpy + abs_delta * deltafracdy;
+                Geom::Point newdx = (newpx - newp) / Geom::EPSILON;
+                Geom::Point newdy = (newpy - newp) / Geom::EPSILON;
+
+                Geom::Matrix itrans(newdx[Geom::X], newdx[Geom::Y], newdy[Geom::X], newdy[Geom::Y], 0, 0);
+                if (itrans.isSingular())
+                    itrans.setIdentity();
+
+                trans *= Geom::Translate(-cur->position());
+                trans *= _last_trans[cur].inverse();
+                trans *= itrans;
+                trans *= Geom::Translate(_original_positions[cur] + abs_delta * deltafrac);
+                _last_trans[cur] = itrans;
+            } else {
+                trans *= Geom::Translate(-cur->position() + _original_positions[cur] + abs_delta * deltafrac);
+            }
+            cur->transform(trans);
+            //cur->move(_original_positions[cur] + abs_delta * deltafrac);
+        }
+    } else {
+        Geom::Point delta = new_pos - _grabbed_point->position();
+        for (iterator i = _points.begin(); i != _points.end(); ++i) {
+            SelectableControlPoint *cur = (*i);
+            cur->move(_original_positions[cur] + abs_delta);
+        }
+        _handles->rotationCenter().move(_handles->rotationCenter().position() + delta);
     }
-    _handles->rotationCenter().move(_handles->rotationCenter().position() + delta);
     signal_update.emit();
 }
 
 void ControlPointSelection::_pointUngrabbed()
 {
+    _original_positions.clear();
+    _last_trans.clear();
     _dragging = false;
-    _grabbed_point = NULL;
+    _grabbed_point = _farthest_point = NULL;
+    _updateBounds();
     restoreTransformHandles();
     signal_commit.emit(COMMIT_MOUSE_MOVE);
 }
@@ -341,24 +364,52 @@ bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventBut
 {
     // clicking a selected node should toggle the transform handles between rotate and scale mode,
     // if they are visible
-    if (held_shift(*event)) return false;
-    if (_handles_visible && p->selected()) {
+    if (held_no_modifiers(*event) && _handles_visible && p->selected()) {
         toggleTransformHandlesMode();
         return true;
     }
     return false;
 }
 
+void ControlPointSelection::_pointChanged(SelectableControlPoint *p, bool selected)
+{
+    _updateBounds();
+    _updateTransformHandles(false);
+    if (_bounds)
+        _handles->rotationCenter().move(_bounds->midpoint());
+
+    signal_point_changed.emit(p, selected);
+}
+
+void ControlPointSelection::_mouseoverChanged()
+{
+    _mouseover_rot_radius = boost::none;
+}
+
+void ControlPointSelection::_updateBounds()
+{
+    _rot_radius = boost::none;
+    _bounds = Geom::OptRect();
+    for (iterator i = _points.begin(); i != _points.end(); ++i) {
+        SelectableControlPoint *cur = (*i);
+        Geom::Point p = cur->position();
+        if (!_bounds) {
+            _bounds = Geom::Rect(p, p);
+        } else {
+            _bounds->expandTo(p);
+        }
+    }
+}
+
 void ControlPointSelection::_updateTransformHandles(bool preserve_center)
 {
     if (_dragging) return;
 
     if (_handles_visible && size() > 1) {
-        Geom::OptRect b = pointwiseBounds();
-        _handles->setBounds(*b, preserve_center);
+        _handles->setBounds(*bounds(), preserve_center);
         _handles->setVisible(true);
     } else if (_one_node_handles && size() == 1) { // only one control point in selection
-        SelectableControlPoint *p = begin()->first;
+        SelectableControlPoint *p = *begin();
         _handles->setBounds(p->bounds());
         _handles->rotationCenter().move(p->position());
         _handles->rotationCenter().setVisible(false);
@@ -373,7 +424,7 @@ void ControlPointSelection::_updateTransformHandles(bool preserve_center)
 bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir)
 {
     if (held_control(event)) return false;
-    unsigned num = 1 + consume_same_key_events(shortcut_key(event), 0);
+    unsigned num = 1 + combine_key_events(shortcut_key(event), 0);
 
     Geom::Point delta = dir * num; 
     if (held_shift(event)) delta *= 10;
@@ -394,6 +445,20 @@ bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point
     return true;
 }
 
+/** @brief Computes the distance to the farthest corner of the bounding box.
+ * Used to determine what it means to "rotate by one pixel". */
+double ControlPointSelection::_rotationRadius(Geom::Point const &rc)
+{
+    if (empty()) return 1.0; // some safe value
+    Geom::Rect b = *bounds();
+    double maxlen = 0;
+    for (unsigned i = 0; i < 4; ++i) {
+        double len = Geom::distance(b.corner(i), rc);
+        if (len > maxlen) maxlen = len;
+    }
+    return maxlen;
+}
+
 /** Rotates the selected points in the given direction according to the modifier state
  * from the supplied event.
  * @param event Key event to take modifier state from
@@ -403,15 +468,25 @@ bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
 {
     if (empty()) return false;
 
-    Geom::Point rc = _handles->rotationCenter();
-    if (!_rot_radius) {
-        Geom::Rect b = *(size() == 1 ? bounds() : pointwiseBounds());
-        double maxlen = 0;
-        for (unsigned i = 0; i < 4; ++i) {
-            double len = (b.corner(i) - rc).length();
-            if (len > maxlen) maxlen = len;
+    Geom::Point rc;
+
+    // rotate around the mouseovered point, or the selection's rotation center
+    // if nothing is mouseovered
+    double radius;
+    SelectableControlPoint *scp =
+        dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
+    if (scp) {
+        rc = scp->position();
+        if (!_mouseover_rot_radius) {
+            _mouseover_rot_radius = _rotationRadius(rc);
         }
-        _rot_radius = maxlen;
+        radius = *_mouseover_rot_radius;
+    } else {
+        rc = _handles->rotationCenter();
+        if (!_rot_radius) {
+            _rot_radius = _rotationRadius(rc);
+        }
+        radius = *_rot_radius;
     }
 
     double angle;
@@ -419,7 +494,7 @@ bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
         // Rotate by "one pixel". We interpret this as rotating by an angle that causes
         // the topmost point of a circle circumscribed about the selection's bounding box
         // to move on an arc 1 screen pixel long.
-        angle = atan2(1.0 / _desktop->current_zoom(), *_rot_radius) * dir;
+        angle = atan2(1.0 / _desktop->current_zoom(), radius) * dir;
     } else {
         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
         int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
@@ -439,11 +514,17 @@ bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir)
 {
     if (empty()) return false;
 
-    // TODO should the saved rotation center or the current center be used?
-    Geom::Rect bound = (size() == 1 ? *bounds() : *pointwiseBounds());
-    double maxext = bound.maxExtent();
+    double maxext = bounds()->maxExtent();
     if (Geom::are_near(maxext, 0)) return false;
-    Geom::Point center = _handles->rotationCenter().position();
+
+    Geom::Point center;
+    SelectableControlPoint *scp =
+        dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
+    if (scp) {
+        center = scp->position();
+    } else {
+        center = _handles->rotationCenter().position();
+    }
 
     double length_change;
     if (held_alt(event)) {
@@ -484,8 +565,9 @@ bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d)
     return true;
 }
 
-void ControlPointSelection::_commitTransform(CommitEvent ce)
+void ControlPointSelection::_commitHandlesTransform(CommitEvent ce)
 {
+    _updateBounds();
     _updateTransformHandles(true);
     signal_commit.emit(ce);
 }
@@ -560,6 +642,24 @@ bool ControlPointSelection::event(GdkEvent *event)
     return false;
 }
 
+std::vector<Inkscape::SnapCandidatePoint> ControlPointSelection::getOriginalPoints()
+{
+    std::vector<Inkscape::SnapCandidatePoint> points;
+    for (iterator i = _points.begin(); i != _points.end(); ++i) {
+        points.push_back(Inkscape::SnapCandidatePoint(_original_positions[*i], SNAPSOURCE_NODE_HANDLE));
+    }
+    return points;
+}
+
+void ControlPointSelection::setOriginalPoints()
+{
+    _original_positions.clear();
+    for (iterator i = _points.begin(); i != _points.end(); ++i) {
+        _original_positions.insert(std::make_pair(*i, (*i)->position()));
+    }
+}
+
+
 } // namespace UI
 } // namespace Inkscape
 
@@ -572,4 +672,4 @@ bool ControlPointSelection::event(GdkEvent *event)
   fill-column:99
   End:
 */
-// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :