Code

Implement selection linear grow
authorKrzysztof Kosiński <tweenk.pl@gmail.com>
Sat, 26 Dec 2009 23:59:01 +0000 (00:59 +0100)
committerKrzysztof Kosiński <tweenk.pl@gmail.com>
Sat, 26 Dec 2009 23:59:01 +0000 (00:59 +0100)
src/ui/tool/node-tool.cpp
src/ui/tool/node.cpp
src/ui/tool/node.h
src/ui/tool/path-manipulator.cpp
src/ui/tool/path-manipulator.h
src/ui/tool/selectable-control-point.h

index e310731bbf07162de3fda3fa9f1cecb8db612998..735ddf87ef61c97a8f2f4a3ca7d227ea7e59a59c 100644 (file)
 #include "pixmaps/cursor-node.xpm"
 #include "pixmaps/cursor-node-d.xpm"
 
+/** @struct InkNodeTool
+ *
+ * Node tool event context.
+ *
+ * @par Architectural overview of the tool
+ * @par
+ * Here's a breakdown of what each object does.
+ * - Handle: shows a handle and keeps the node type constraint (smooth / symmetric) by updating
+ *   the other handle's position when dragged. Its move() method cannot violate the constraints.
+ * - Node: keeps node type constraints for auto nodes and smooth nodes at ends of linear segments.
+ *   Its move() method cannot violate constraints. Handles linear grow and dispatches spatial grow
+ *   to MultiPathManipulator. Keeps a reference to its NodeList.
+ * - NodeList: exposes an iterator-based interface to nodes. It is possible to obtain an iterator
+ *   to a node from the node. Keeps a reference to its SubpathList.
+ * - SubpathList: list of NodeLists that represents an editable pathvector. Keeps a reference
+ *   to its PathManipulator.
+ * - PathManipulator: performs most of the single-path actions like reverse subpaths,
+ *   delete segment, shift selection, etc. Keeps a reference to MultiPathManipulator.
+ * - MultiPathManipulator: performs additional operations for actions that are not per-path,
+ *   for example node joins and segment joins. Tracks the control transforms for PMs that edit
+ *   clipping paths and masks. It is more or less equivalent to ShapeEditor and in the future
+ *   it might handle all shapes. Handles XML commit of actions that affect all paths or
+ *   the node selection and removes PathManipulators that have no nodes left after e.g. node
+ *   deletes.
+ * - ControlPointSelection: keeps track of node selection. Performs actions that require no
+ *   knowledge about the path, only about the nodes, like dragging and transforms. It is not
+ *   specific to nodes and can accomodate any control point derived from SelectableControlPoint.
+ *   Transforms nodes in response to transform handle events.
+ * - TransformHandleSet: displays nodeset transform handles and emits transform events. The aim
+ *   is to eventually use a common class for object and control point transforms.
+ * 
+ * @par Plans for the future
+ * @par
+ * - MultiPathManipulator should become a generic shape editor that manages all active manipulator,
+ *   more or less like the old ShapeEditor.
+ * - Knotholder should be rewritten into one manipulator class per shape, using the control point
+ *   classes. Interesting features like dragging rectangle sides could be added along the way.
+ * - Better handling of clip and mask editing, particularly in response to undo.
+ * - High level refactoring of the event context hierarchy. All aspects of tools, like toolbox
+ *   controls, icons, event handling should be collected in one class, though each aspect
+ *   of a tool might be in an separate class for better modularity. The long term goal is to allow
+ *   tools to be defined in extensions or shared library plugins.
+ */
+
 namespace {
 SPCanvasGroup *create_control_group(SPDesktop *d);
 void ink_node_tool_class_init(InkNodeToolClass *klass);
index 4f6d0d5d7d994eeb0db82c13eb8e110c0306d5d5..22d4ddc47f1ef2b6ed47cb8ba90eb0e0b2743f0b 100644 (file)
 #include <boost/utility.hpp>
 #include <glib.h>
 #include <glib/gi18n.h>
+#include <2geom/bezier-utils.h>
 #include <2geom/transforms.h>
-#include "ui/tool/event-utils.h"
-#include "ui/tool/multi-path-manipulator.h"
-#include "ui/tool/node.h"
-#include "ui/tool/path-manipulator.h"
+
 #include "display/sp-ctrlline.h"
 #include "display/sp-canvas.h"
 #include "display/sp-canvas-util.h"
 #include "preferences.h"
 #include "sp-metrics.h"
 #include "sp-namedview.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/node.h"
+#include "ui/tool/path-manipulator.h"
 
 namespace Inkscape {
 namespace UI {   
@@ -621,6 +624,7 @@ NodeType Node::parse_nodetype(char x)
     }
 }
 
+/** Customized event handler to catch scroll events needed for selection grow/shrink. */
 bool Node::_eventHandler(GdkEvent *event)
 {
     static NodeList::iterator origin;
@@ -639,7 +643,7 @@ bool Node::_eventHandler(GdkEvent *event)
         if (held_control(event->scroll)) {
             list()->_list._path_manipulator._multi_path_manipulator.spatialGrow(origin, dir);
         } else {
-            list()->_list._path_manipulator.linearGrow(origin, dir);
+            _linearGrow(dir);
         }
         return true;
     default:
@@ -648,6 +652,124 @@ bool Node::_eventHandler(GdkEvent *event)
     return ControlPoint::_eventHandler(event);
 }
 
+// TODO Move this to 2Geom
+static double bezier_length (Geom::Point a0, Geom::Point a1, Geom::Point a2, Geom::Point a3)
+{
+    double lower = Geom::distance(a0, a3);
+    double upper = Geom::distance(a0, a1) + Geom::distance(a1, a2) + Geom::distance(a2, a3);
+
+    // TODO maybe EPSILON is this is too big in this case?
+    if (upper - lower < Geom::EPSILON) return (lower + upper)/2;
+
+    Geom::Point // Casteljau subdivision
+        b0 = a0,
+        c0 = a3,
+        b1 = 0.5*(a0 + a1),
+        t0 = 0.5*(a1 + a2),
+        c1 = 0.5*(a2 + a3),
+        b2 = 0.5*(b1 + t0),
+        c2 = 0.5*(t0 + c1),
+        b3 = 0.5*(b2 + c2); // == c3
+    return bezier_length(b0, b1, b2, b3) + bezier_length(b3, c2, c1, c0);
+}
+
+/** Select or deselect a node in this node's subpath based on its path distance from this node.
+ * @param dir If negative, shrink selection by one node; if positive, grow by one node */
+void Node::_linearGrow(int dir)
+{
+    // Interestingly, we do not need any help from PathManipulator when doing linear grow.
+    // First handle the trivial case of growing over an unselected node.
+    if (!selected() && dir > 0) {
+        _selection.insert(this);
+        return;
+    }
+
+    NodeList::iterator this_iter = NodeList::get_iterator(this);
+    NodeList::iterator fwd = this_iter, rev = this_iter;
+    double distance_back = 0, distance_front = 0;
+
+    // Linear grow is simple. We find the first unselected nodes in each direction
+    // and compare the linear distances to them.
+    if (dir > 0) {
+        if (!selected()) {
+            _selection.insert(this);
+            return;
+        }
+
+        // find first unselected nodes on both sides
+        while (fwd && fwd->selected()) {
+            NodeList::iterator n = fwd.next();
+            distance_front += bezier_length(*fwd, fwd->_front, n->_back, *n);
+            fwd = n;
+            if (fwd == this_iter)
+                // there is no unselected node in this cyclic subpath
+                return;
+        }
+        // do the same for the second direction. Do not check for equality with
+        // this node, because there is at least one unselected node in the subpath,
+        // so we are guaranteed to stop.
+        while (rev && rev->selected()) {
+            NodeList::iterator p = rev.prev();
+            distance_back += bezier_length(*rev, rev->_back, p->_front, *p);
+            rev = p;
+        }
+
+        NodeList::iterator t; // node to select
+        if (fwd && rev) {
+            if (distance_front <= distance_back) t = fwd;
+            else t = rev;
+        } else {
+            if (fwd) t = fwd;
+            if (rev) t = rev;
+        }
+        if (t) _selection.insert(t.ptr());
+
+    // Linear shrink is more complicated. We need to find the farthest selected node.
+    // This means we have to check the entire subpath. We go in the direction in which
+    // the distance we traveled is lower. We do this until we run out of nodes (ends of path)
+    // or the two iterators meet. On the way, we store the last selected node and its distance
+    // in each direction (if any). At the end, we choose the one that is farther and deselect it.
+    } else {
+        // both iterators that store last selected nodes are initially empty
+        NodeList::iterator last_fwd, last_rev;
+        double last_distance_back, last_distance_front;
+
+        while (rev || fwd) {
+            if (fwd && (!rev || distance_front <= distance_back)) {
+                if (fwd->selected()) {
+                    last_fwd = fwd;
+                    last_distance_front = distance_front;
+                }
+                NodeList::iterator n = fwd.next();
+                distance_front += bezier_length(*fwd, fwd->_front, n->_back, *n);
+                fwd = n;
+            } else if (rev && (!fwd || distance_front > distance_back)) {
+                if (rev->selected()) {
+                    last_rev = rev;
+                    last_distance_back = distance_back;
+                }
+                NodeList::iterator p = rev.prev();
+                distance_back += bezier_length(*rev, rev->_back, p->_front, *p);
+                rev = p;
+            }
+            // Check whether we walked the entire cyclic subpath.
+            // This is initially true because both iterators start from this node,
+            // so this check cannot go in the while condition.
+            if (fwd == rev) break;
+        }
+
+        NodeList::iterator t;
+        if (last_fwd && last_rev) {
+            if (last_distance_front >= last_distance_back) t = last_fwd;
+            else t = last_rev;
+        } else {
+            if (last_fwd) t = last_fwd;
+            if (last_rev) t = last_rev;
+        }
+        if (t) _selection.erase(t.ptr());
+    }
+}
+
 void Node::_setState(State state)
 {
     // change node size to match type and selection state
@@ -667,14 +789,14 @@ void Node::_setState(State state)
 
 bool Node::_grabbedHandler(GdkEventMotion *event)
 {
-    // dragging out handles
+    // Dragging out handles with Shift + drag on a node.
     if (!held_shift(*event)) return false;
 
     Handle *h;
     Geom::Point evp = event_point(*event);
     Geom::Point rel_evp = evp - _last_click_event_point();
 
-    // this should work even if dragtolerance is zero and evp coincides with node position
+    // This should work even if dragtolerance is zero and evp coincides with node position.
     double angle_next = HUGE_VAL;
     double angle_prev = HUGE_VAL;
     bool has_degenerate = false;
@@ -727,7 +849,7 @@ void Node::_draggedHandler(Geom::Point &new_pos, GdkEventMotion *event)
             new_pos[d] = origin[d];
         }
     } else {
-        // snapping?
+        // TODO snapping?
     }
 }
 
@@ -813,8 +935,6 @@ SPCtrlShapeType Node::_node_type_to_shape(NodeType type)
  * It can optionally be cyclic to represent a closed path.
  * The list has iterators that act like plain node iterators, but can also be used
  * to obtain shared pointers to nodes.
- *
- * @todo Manage geometric representation to improve speed
  */
 
 NodeList::NodeList(SubpathList &splist)
@@ -959,7 +1079,7 @@ NodeList::iterator NodeList::erase(iterator i)
     return i;
 }
 
-// TODO this method is nasty and ugly!
+// TODO this method is very ugly!
 // converting SubpathList to an intrusive list might allow us to get rid of it
 void NodeList::kill()
 {
index 68ad63ba9b831d065737f0d3405cfa2ea2f7a28a..167cf90b871e63c20816a050c481ec70f1004ba7 100644 (file)
@@ -148,6 +148,7 @@ private:
     void _draggedHandler(Geom::Point &, GdkEventMotion *);
     void _fixNeighbors(Geom::Point const &old_pos, Geom::Point const &new_pos);
     void _updateAutoHandles();
+    void _linearGrow(int dir);
     Node *_next();
     Node *_prev();
     static SPCtrlShapeType _node_type_to_shape(NodeType type);
index 42db45321e82f13e9e3353d9dace1b96a2be4927..2755d6fb35c94cb4bd8d0bde7258d862b706e6e1 100644 (file)
@@ -298,11 +298,6 @@ void PathManipulator::shiftSelection(int dir)
     }
 }
 
-void PathManipulator::linearGrow(NodeList::iterator center, int dir)
-{
-    g_message("linearGrow unimplemented");
-}
-
 /** Invert selection in the entire path. */
 void PathManipulator::invertSelection()
 {
index 82ce7fd5d3e560689936299f7634f49af49fe779..38f66dee0a0f62d5a0e4c381c087c64facb90ab6 100644 (file)
@@ -67,7 +67,6 @@ public:
     void selectAll();
     void selectArea(Geom::Rect const &);
     void shiftSelection(int dir);
-    void linearGrow(NodeList::iterator center, int dir);
     void invertSelection();
     void invertSelectionInSubpaths();
 
index a432b68db652cd4ba594b6e1b75be9062b931c79..87e4152581f39aa67bf986fdc65082ae0db17737 100644 (file)
@@ -44,14 +44,14 @@ protected:
         ControlPointSelection &sel, ColorSet *cset = 0, SPCanvasGroup *group = 0);
 
     virtual void _setState(State state);
+
+    ControlPointSelection &_selection;
 private:
     void _connectHandlers();
     void _takeSelection();
     
     bool _clickedHandler(GdkEventButton *);
     void _grabbedHandler(GdkEventMotion *);
-    
-    ControlPointSelection &_selection;
 };
 
 } // namespace UI