diff --git a/src/ui/tool/node.cpp b/src/ui/tool/node.cpp
index 5f792cfc74bb5fc8880ad5ef6d66f91c3bb1d343..303c0fb75b6bf6f2a40e286eb85246761c13de71 100644 (file)
--- a/src/ui/tool/node.cpp
+++ b/src/ui/tool/node.cpp
#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/node.h"
+
#include "display/sp-ctrlline.h"
#include "display/sp-canvas.h"
#include "display/sp-canvas-util.h"
#include "desktop.h"
#include "desktop-handles.h"
#include "preferences.h"
+#include "snap.h"
+#include "snap-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 {
/**
* @class Handle
- * Represents a control point of a cubic Bezier curve in a path.
+ * @brief Control point of a cubic Bezier curve in a path.
+ *
+ * Handle keeps the node type invariant only for the opposite handle of the same node.
+ * Keeping the invariant on node moves is left to the %Node class.
*/
double Handle::_saved_length = 0.0;
/**
* @class Node
- * Represents a curve endpoint in an editable path.
+ * @brief Curve endpoint in an editable path.
+ *
+ * The method move() keeps node type invariants during translations.
*/
Node::Node(NodeSharedData const &data, Geom::Point const &initial_pos)
@@ -409,37 +421,38 @@ void Node::_fixNeighbors(Geom::Point const &old_pos, Geom::Point const &new_pos)
if (_type == NODE_SMOOTH && !handle->isDegenerate()) {
handle->setDirection(other->position(), new_pos);
- /*Geom::Point handle_delta = handle->position() - position();
- Geom::Point new_delta = Geom::unit_vector(new_direction) * handle_delta.length();
- handle->setPosition(position() + new_delta);*/
}
// also update the handle on the other end of the segment
if (other->_type == NODE_SMOOTH && !other_handle->isDegenerate()) {
other_handle->setDirection(new_pos, other->position());
- /*
- Geom::Point handle_delta2 = other_handle->position() - other->position();
- Geom::Point new_delta2 = Geom::unit_vector(new_direction) * handle_delta2.length();
- other_handle->setPosition(other->position() + new_delta2);*/
}
}
void Node::_updateAutoHandles()
{
// Recompute the position of automatic handles.
- if (!_prev() || !_next()) {
+ // For endnodes, retract both handles. (It's only possible to create an end auto node
+ // through the XML editor.)
+ if (isEndNode()) {
_front.retract();
_back.retract();
return;
}
- // TODO describe in detail what the code below does
+
+ // Auto nodes automaticaly adjust their handles to give an appearance of smoothness,
+ // no matter what their surroundings are.
Geom::Point vec_next = _next()->position() - position();
Geom::Point vec_prev = _prev()->position() - position();
double len_next = vec_next.length(), len_prev = vec_prev.length();
if (len_next > 0 && len_prev > 0) {
+ // "dir" is an unit vector perpendicular to the bisector of the angle created
+ // by the previous node, this auto node and the next node.
Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev);
+ // Handle lengths are equal to 1/3 of the distance from the adjacent node.
_back.setRelativePos(-dir * (len_prev / 3));
_front.setRelativePos(dir * (len_next / 3));
} else {
+ // If any of the adjacent nodes coincides, retract both handles.
_front.retract();
_back.retract();
}
updateState();
}
+/** Pick the best type for this node, based on the position of its handles.
+ * This is what assigns types to nodes created using the pen tool. */
void Node::pickBestType()
{
_type = NODE_CUSP;
}
}
+/** Customized event handler to catch scroll events needed for selection grow/shrink. */
+bool Node::_eventHandler(GdkEvent *event)
+{
+ static NodeList::iterator origin;
+ static int dir;
+
+ switch (event->type)
+ {
+ case GDK_SCROLL:
+ if (event->scroll.direction == GDK_SCROLL_UP) {
+ dir = 1;
+ } else if (event->scroll.direction == GDK_SCROLL_DOWN) {
+ dir = -1;
+ } else break;
+ if (held_control(event->scroll)) {
+ _selection.spatialGrow(this, dir);
+ } else {
+ _linearGrow(dir);
+ }
+ return true;
+ default:
+ break;
+ }
+ 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);
+
+ 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 = 0, last_distance_front = 0;
+
+ 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();
+ if (n) 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();
+ if (p) 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.
+ // When this happens, we need to check the last node, pointed to by the iterators.
+ if (fwd && fwd == rev) {
+ if (!fwd->selected()) break;
+ NodeList::iterator fwdp = fwd.prev(), revn = rev.next();
+ double df = distance_front + bezier_length(*fwdp, fwdp->_front, fwd->_back, *fwd);
+ double db = distance_back + bezier_length(*revn, revn->_back, rev->_front, *rev);
+ if (df > db) {
+ last_fwd = fwd;
+ last_distance_front = df;
+ } else {
+ last_rev = rev;
+ last_distance_back = db;
+ }
+ 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
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;
void Node::_draggedHandler(Geom::Point &new_pos, GdkEventMotion *event)
{
- if (!held_control(*event)) return;
- if (held_alt(*event)) {
- // with Ctrl+Alt, constrain to handle lines
- // project the new position onto a handle line that is closer
+ // For a note on how snapping is implemented in Inkscape, see snap.h.
+ SnapManager &sm = _desktop->namedview->snap_manager;
+ Inkscape::SnapPreferences::PointType t = Inkscape::SnapPreferences::SNAPPOINT_NODE;
+ bool snap = sm.someSnapperMightSnap();
+ std::vector<Inkscape::SnapCandidatePoint> unselected;
+ if (snap) {
+ /* setup
+ * TODO We are doing this every time a snap happens. It should once be done only once
+ * per drag - maybe in the grabbed handler?
+ * TODO Unselected nodes vector must be valid during the snap run, because it is not
+ * copied. Fix this in snap.h and snap.cpp, then the above.
+ * TODO Snapping to unselected segments of selected paths doesn't work yet. */
+
+ // Build the list of unselected nodes.
+ typedef ControlPointSelection::Set Set;
+ Set nodes = _selection.allPoints();
+ for (Set::iterator i = nodes.begin(); i != nodes.end(); ++i) {
+ if (!(*i)->selected()) {
+ Node *n = static_cast<Node*>(*i);
+ Inkscape::SnapCandidatePoint p(n->position(), n->_snapSourceType(), n->_snapTargetType());
+ unselected.push_back(p);
+ }
+ }
+ sm.setupIgnoreSelection(_desktop, true, &unselected);
+ }
+
+ if (held_control(*event)) {
Geom::Point origin = _last_drag_origin();
- Geom::Line line_front(origin, origin + _front.relativePos());
- Geom::Line line_back(origin, origin + _back.relativePos());
- double dist_front, dist_back;
- dist_front = Geom::distance(new_pos, line_front);
- dist_back = Geom::distance(new_pos, line_back);
- if (dist_front < dist_back) {
- new_pos = Geom::projection(new_pos, line_front);
+ if (held_alt(*event)) {
+ // with Ctrl+Alt, constrain to handle lines
+ // project the new position onto a handle line that is closer
+ Inkscape::Snapper::ConstraintLine line_front(origin, _front.relativePos());
+ Inkscape::Snapper::ConstraintLine line_back(origin, _back.relativePos());
+
+ // TODO: combine these two branches by modifying snap.h / snap.cpp
+ if (snap) {
+ Inkscape::SnappedPoint fp, bp;
+ fp = sm.constrainedSnap(t, Inkscape::SnapCandidatePoint(position(), _snapSourceType()), line_front);
+ bp = sm.constrainedSnap(t, Inkscape::SnapCandidatePoint(position(), _snapSourceType()), line_back);
+
+ if (fp.isOtherSnapBetter(bp, false)) {
+ bp.getPoint(new_pos);
+ } else {
+ fp.getPoint(new_pos);
+ }
+ } else {
+ Geom::Point p_front = line_front.projection(new_pos);
+ Geom::Point p_back = line_back.projection(new_pos);
+ if (Geom::distance(new_pos, p_front) < Geom::distance(new_pos, p_back)) {
+ new_pos = p_front;
+ } else {
+ new_pos = p_back;
+ }
+ }
} else {
- new_pos = Geom::projection(new_pos, line_back);
+ // with Ctrl, constrain to axes
+ // TODO combine the two branches
+ if (snap) {
+ Inkscape::SnappedPoint fp, bp;
+ Inkscape::Snapper::ConstraintLine line_x(origin, Geom::Point(1, 0));
+ Inkscape::Snapper::ConstraintLine line_y(origin, Geom::Point(0, 1));
+ fp = sm.constrainedSnap(t, Inkscape::SnapCandidatePoint(position(), _snapSourceType()), line_x);
+ bp = sm.constrainedSnap(t, Inkscape::SnapCandidatePoint(position(), _snapSourceType()), line_y);
+
+ if (fp.isOtherSnapBetter(bp, false)) {
+ fp = bp;
+ }
+ fp.getPoint(new_pos);
+ } else {
+ Geom::Point origin = _last_drag_origin();
+ Geom::Point delta = new_pos - origin;
+ Geom::Dim2 d = (fabs(delta[Geom::X]) < fabs(delta[Geom::Y])) ? Geom::X : Geom::Y;
+ new_pos[d] = origin[d];
+ }
}
- } else {
- // with Ctrl, constrain to axes
- // TODO this probably has to be separated into an AxisConstrainablePoint class
- // TODO maybe add diagonals when the distance from origin is large enough?
- Geom::Point origin = _last_drag_origin();
- Geom::Point delta = new_pos - origin;
- Geom::Dim2 d = (fabs(delta[Geom::X]) < fabs(delta[Geom::Y])) ? Geom::X : Geom::Y;
- new_pos[d] = origin[d];
+ } else if (snap) {
+ sm.freeSnapReturnByRef(Inkscape::SnapPreferences::SNAPPOINT_NODE, new_pos, _snapSourceType());
}
}
+Inkscape::SnapSourceType Node::_snapSourceType()
+{
+ if (_type == NODE_SMOOTH || _type == NODE_AUTO)
+ return SNAPSOURCE_NODE_SMOOTH;
+ return SNAPSOURCE_NODE_CUSP;
+}
+Inkscape::SnapTargetType Node::_snapTargetType()
+{
+ if (_type == NODE_SMOOTH || _type == NODE_AUTO)
+ return SNAPTARGET_NODE_SMOOTH;
+ return SNAPTARGET_NODE_CUSP;
+}
+
Glib::ustring Node::_getTip(unsigned state)
{
if (state_held_shift(state)) {
return C_("Path node tip",
"<b>Shift:</b> drag out a handle, click to toggle selection");
}
- return C_("Path node statusbar tip", "<b>Shift:</b> click to toggle selection");
+ return C_("Path node tip", "<b>Shift:</b> click to toggle selection");
}
if (state_held_control(state)) {
Geom::Point dist = position() - _last_drag_origin();
GString *x = SP_PX_TO_METRIC_STRING(dist[Geom::X], _desktop->namedview->getDefaultMetric());
GString *y = SP_PX_TO_METRIC_STRING(dist[Geom::Y], _desktop->namedview->getDefaultMetric());
- Glib::ustring ret = format_tip(C_("Path node statusbar tip", "Move by %s, %s"),
+ Glib::ustring ret = format_tip(C_("Path node tip", "Move by %s, %s"),
x->str, y->str);
g_string_free(x, TRUE);
g_string_free(y, TRUE);
* 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)
NodeList::iterator NodeList::erase(iterator i)
{
- // some acrobatics are required to ensure that the node is valid when deleted;
+ // some gymnastics are required to ensure that the node is valid when deleted;
// otherwise the code that updates handle visibility will break
Node *rm = static_cast<Node*>(i._node);
ListNode *rmnext = rm->next, *rmprev = rm->prev;
return i;
}
+// TODO this method is very ugly!
+// converting SubpathList to an intrusive list might allow us to get rid of it
void NodeList::kill()
{
for (SubpathList::iterator i = _list.begin(); i != _list.end(); ++i) {