From: Krzysztof Kosiński Date: Thu, 14 Jan 2010 08:13:09 +0000 (+0100) Subject: * Merge from trunk X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=9eb3f5487e349bceb5be7b5c41b20710ee798038;p=inkscape.git * Merge from trunk * Update to new snapping API * Modify the join action slightly --- 9eb3f5487e349bceb5be7b5c41b20710ee798038 diff --cc po/POTFILES.in index df09acc22,191186560..f5e172a63 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@@ -352,8 -197,7 +352,12 @@@ src/sp-text.cp src/sp-tref.cpp src/sp-tspan.cpp src/sp-use.cpp ++<<<<<<< TREE +src/spiral-context.cpp +src/splivarot.cpp ++======= + src/spray-context.cpp ++>>>>>>> MERGE-SOURCE src/star-context.cpp src/text-chemistry.cpp src/text-context.cpp @@@ -386,8 -230,8 +390,9 @@@ src/ui/dialog/memory.cp src/ui/dialog/messages.cpp src/ui/dialog/ocaldialogs.cpp src/ui/dialog/print.cpp + src/ui/dialog/print-colors-preview-dialog.cpp src/ui/dialog/scriptdialog.cpp +src/ui/dialog/session-player.cpp src/ui/dialog/svg-fonts-dialog.cpp src/ui/dialog/swatches.cpp src/ui/dialog/tile.cpp diff --cc src/event-context.cpp index 13e7e9410,918e3d419..363f9fe71 --- a/src/event-context.cpp +++ b/src/event-context.cpp @@@ -1239,59 -1239,53 +1239,60 @@@ void sp_event_context_snap_delay_handle gboolean sp_event_context_snap_watchdog_callback(gpointer data) { - if (!data) return FALSE; - // Snap NOW! For this the "postponed" flag will be reset and the last motion event will be repeated - DelayedSnapEvent *dse = reinterpret_cast(data); - - if (dse == NULL) { - // This might occur when this method is called directly, i.e. not through the timer - // E.g. on GDK_BUTTON_RELEASE in sp_event_context_root_handler() - return FALSE; - } - - SPEventContext *ec = dse->getEventContext(); - if (ec == NULL || ec->desktop == NULL) { - return false; - } - - SPDesktop *dt = ec->desktop; - dt->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); - - switch (dse->getOrigin()) { - case DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER: - sp_event_context_virtual_root_handler(ec, dse->getEvent()); - break; - case DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER: - { - SPItem* item = NULL; - item = dse->getItem(); - if (item && SP_IS_ITEM(item)) { - sp_event_context_virtual_item_handler(ec, item, dse->getEvent()); - } - } - break; - case DelayedSnapEvent::KNOT_HANDLER: - { - SPKnot* knot = dse->getKnot(); - if (knot && SP_IS_KNOT(knot)) { - sp_knot_handler_request_position(dse->getEvent(), knot); - } - } - break; - case DelayedSnapEvent::CONTROL_POINT_HANDLER: { + // Snap NOW! For this the "postponed" flag will be reset and the last motion event will be repeated + DelayedSnapEvent *dse = reinterpret_cast(data); + + if (dse == NULL) { + // This might occur when this method is called directly, i.e. not through the timer + // E.g. on GDK_BUTTON_RELEASE in sp_event_context_root_handler() + return FALSE; + } + + SPEventContext *ec = dse->getEventContext(); + if (ec == NULL || ec->desktop == NULL) { + return false; + } + + SPDesktop *dt = ec->desktop; + dt->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); + + switch (dse->getOrigin()) { + case DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER: + sp_event_context_virtual_root_handler(ec, dse->getEvent()); + break; + case DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER: + { + SPItem* item = NULL; + item = dse->getItem(); + if (item && SP_IS_ITEM(item)) { + sp_event_context_virtual_item_handler(ec, item, dse->getEvent()); + } + } + break; + case DelayedSnapEvent::KNOT_HANDLER: + { + SPKnot* knot = dse->getKnot(); + if (knot && SP_IS_KNOT(knot)) { + sp_knot_handler_request_position(dse->getEvent(), knot); + } + } + break; ++ case DelayedSnapEvent::CONTROL_POINT_HANDLER: ++ { + using Inkscape::UI::ControlPoint; + ControlPoint *point = reinterpret_cast(dse->getKnot()); + point->_eventHandler(dse->getEvent()); - } break; - default: - g_warning("Origin of snap-delay event has not been defined!;"); - break; - } ++ } ++ break; + default: + g_warning("Origin of snap-delay event has not been defined!;"); + break; + } - ec->_delayed_snap_event = NULL; - delete dse; + ec->_delayed_snap_event = NULL; + delete dse; - return FALSE; //Kills the timer and stops it from executing this callback over and over again. + return FALSE; //Kills the timer and stops it from executing this callback over and over again. } void sp_event_context_discard_delayed_snap_event(SPEventContext *ec) diff --cc src/libnrtype/FontFactory.cpp index db7cd9747,1f85ee5ca..a63f70d75 --- a/src/libnrtype/FontFactory.cpp +++ b/src/libnrtype/FontFactory.cpp @@@ -26,6 -26,10 +26,10 @@@ /* Freetype2 */ # include -#include ++#include + + -typedef __gnu_cxx::hash_map FaceMapType; ++typedef std::tr1::unordered_map FaceMapType; // need to avoid using the size field size_t font_descr_hash::operator()( PangoFontDescription *const &x) const { diff --cc src/libnrtype/FontInstance.cpp index d9a0c43e6,f34a230c1..6b0725b34 --- a/src/libnrtype/FontInstance.cpp +++ b/src/libnrtype/FontInstance.cpp @@@ -29,6 -29,22 +29,22 @@@ # include FT_TRUETYPE_TABLES_H # include -#include ++#include + + + // the various raster_font in use at a given time are held in a hash_map whose indices are the + // styles, hence the 2 following 'classes' + struct font_style_hash : public std::unary_function { + size_t operator()(font_style const &x) const; + }; + + struct font_style_equal : public std::binary_function { - bool operator()(font_style const &a, font_style const &b); ++ bool operator()(font_style const &a, font_style const &b) const; + }; + + -typedef __gnu_cxx::hash_map StyleMap; ++typedef std::tr1::unordered_map StyleMap; + size_t font_style_hash::operator()(const font_style &x) const { diff --cc src/preferences-skeleton.h index 075d2b031,4e5291baf..90fc85757 --- a/src/preferences-skeleton.h +++ b/src/preferences-skeleton.h @@@ -111,9 -111,11 +111,11 @@@ static char const preferences_skeleton[ " \n" " \n" -" \n" +" \n" " \n" + " \n" " \n" " \n" " \n" diff --cc src/snap.cpp index 558f61814,970f29ece..c39cd9e04 --- a/src/snap.cpp +++ b/src/snap.cpp @@@ -208,17 -204,31 +205,17 @@@ Inkscape::SnappedPoint SnapManager::fre Geom::OptRect const &bbox_to_snap) const { if (!someSnapperMightSnap()) { - return Inkscape::SnappedPoint(p, source_type, Inkscape::SNAPTARGET_UNDEFINED, NR_HUGE, 0, false, false); + return Inkscape::SnappedPoint(p, Inkscape::SNAPTARGET_UNDEFINED, NR_HUGE, 0, false, false); } - std::vector *items_to_ignore; - if (_item_to_ignore) { // If we have only a single item to ignore - // then build a list containing this single item; - // This single-item list will prevail over any other _items_to_ignore list, should that exist - items_to_ignore = new std::vector; - items_to_ignore->push_back(_item_to_ignore); - } else { - items_to_ignore = _items_to_ignore; - } - SnappedConstraints sc; SnapperList const snappers = getSnappers(); for (SnapperList::const_iterator i = snappers.begin(); i != snappers.end(); i++) { - (*i)->freeSnap(sc, point_type, p, source_type, first_point, bbox_to_snap, &_items_to_ignore, _unselected_nodes); - (*i)->freeSnap(sc, point_type, p, bbox_to_snap, items_to_ignore, _unselected_nodes); - } - - if (_item_to_ignore) { - delete items_to_ignore; ++ (*i)->freeSnap(sc, point_type, p, bbox_to_snap, &_items_to_ignore, _unselected_nodes); } - return findBestSnap(p, source_type, sc, false); + return findBestSnap(p, sc, false); } /** @@@ -351,21 -360,36 +347,21 @@@ Inkscape::SnappedPoint SnapManager::con Geom::OptRect const &bbox_to_snap) const { if (!someSnapperMightSnap()) { - return Inkscape::SnappedPoint(p, source_type, Inkscape::SNAPTARGET_UNDEFINED, NR_HUGE, 0, false, false); + return Inkscape::SnappedPoint(p, Inkscape::SNAPTARGET_UNDEFINED, NR_HUGE, 0, false, false); } - std::vector *items_to_ignore; - if (_item_to_ignore) { // If we have only a single item to ignore - // then build a list containing this single item; - // This single-item list will prevail over any other _items_to_ignore list, should that exist - items_to_ignore = new std::vector; - items_to_ignore->push_back(_item_to_ignore); - } else { - items_to_ignore = _items_to_ignore; - } - - // First project the mouse pointer onto the constraint - Geom::Point pp = constraint.projection(p); + Geom::Point pp = constraint.projection(p.getPoint()); // Then try to snap the projected point + Inkscape::SnapCandidatePoint candidate(pp, p.getSourceType(), p.getSourceNum(), Inkscape::SNAPTARGET_UNDEFINED, Geom::Rect()); SnappedConstraints sc; SnapperList const snappers = getSnappers(); for (SnapperList::const_iterator i = snappers.begin(); i != snappers.end(); i++) { - (*i)->constrainedSnap(sc, point_type, pp, source_type, first_point, bbox_to_snap, constraint, &_items_to_ignore); - (*i)->constrainedSnap(sc, point_type, candidate, bbox_to_snap, constraint, items_to_ignore); - } - - if (_item_to_ignore) { - delete items_to_ignore; ++ (*i)->constrainedSnap(sc, point_type, candidate, bbox_to_snap, constraint, &_items_to_ignore); } - return findBestSnap(pp, source_type, sc, true); + return findBestSnap(candidate, sc, true); } /** @@@ -1051,25 -1098,6 +1054,25 @@@ void SnapManager::setup(SPDesktop cons _guide_to_ignore = guide_to_ignore; } +/// Setup, taking the list of items to ignore from the desktop's selection. +void SnapManager::setupIgnoreSelection(SPDesktop const *desktop, + bool snapindicator, - std::vector > *unselected_nodes, ++ std::vector *unselected_nodes, + SPGuide *guide_to_ignore) +{ + _desktop = desktop; + _snapindicator = snapindicator; + _unselected_nodes = unselected_nodes; + _guide_to_ignore = guide_to_ignore; + _items_to_ignore.clear(); + + Inkscape::Selection *sel = _desktop->selection; + GSList const *items = sel->itemList(); + for (GSList *i = const_cast(items); i; i = i->next) { + _items_to_ignore.push_back(static_cast(i->data)); + } +} + SPDocument *SnapManager::getDocument() const { return _named_view->document; diff --cc src/snap.h index 5696dcd53,40bf0996e..ae136a355 --- a/src/snap.h +++ b/src/snap.h @@@ -81,20 -67,16 +81,20 @@@ public bool gridSnapperMightSnap() const; void setup(SPDesktop const *desktop, - bool snapindicator = true, - SPItem const *item_to_ignore = NULL, - std::vector > *unselected_nodes = NULL, - SPGuide *guide_to_ignore = NULL); + bool snapindicator = true, + SPItem const *item_to_ignore = NULL, + std::vector *unselected_nodes = NULL, + SPGuide *guide_to_ignore = NULL); void setup(SPDesktop const *desktop, - bool snapindicator, - std::vector const &items_to_ignore, - std::vector > *unselected_nodes = NULL, - SPGuide *guide_to_ignore = NULL); - bool snapindicator, - std::vector &items_to_ignore, - std::vector *unselected_nodes = NULL, - SPGuide *guide_to_ignore = NULL); ++ bool snapindicator, ++ std::vector &items_to_ignore, ++ std::vector *unselected_nodes = NULL, ++ SPGuide *guide_to_ignore = NULL); + void setupIgnoreSelection(SPDesktop const *desktop, + bool snapindicator = true, - std::vector > *unselected_nodes = NULL, ++ std::vector *unselected_nodes = NULL, + SPGuide *guide_to_ignore = NULL); // freeSnapReturnByRef() is preferred over freeSnap(), because it only returns a // point if snapping has occurred (by overwriting p); otherwise p is untouched diff --cc src/tools-switch.cpp index 380267408,6c53ce61c..5f33453f0 --- a/src/tools-switch.cpp +++ b/src/tools-switch.cpp @@@ -26,8 -26,9 +26,9 @@@ #include #include "select-context.h" -#include "node-context.h" +#include "ui/tool/node-tool.h" #include "tweak-context.h" + #include "spray-context.h" #include "sp-path.h" #include "rect-context.h" #include "sp-rect.h" diff --cc src/ui/tool/multi-path-manipulator.cpp index 33d96c706,000000000..2cc9bc97b mode 100644,000000..100644 --- a/src/ui/tool/multi-path-manipulator.cpp +++ b/src/ui/tool/multi-path-manipulator.cpp @@@ -1,596 -1,0 +1,600 @@@ +/** @file + * Path manipulator - implementation + */ +/* Authors: + * Krzysztof Kosiński + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include +#include +#include +#include +#include "desktop.h" +#include "desktop-handles.h" +#include "document.h" +#include "live_effects/lpeobject.h" +#include "message-stack.h" +#include "preferences.h" +#include "sp-path.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/node.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" + +namespace std { using namespace tr1; } + +namespace Inkscape { +namespace UI { + +namespace { +typedef std::pair IterPair; +typedef std::vector IterPairList; +typedef std::unordered_set IterSet; +typedef std::multimap DistanceMap; +typedef std::pair DistanceMapItem; + +/** Find pairs of selected endnodes suitable for joining. */ +void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs) +{ + IterSet join_iters; + DistanceMap dists; + + // find all endnodes in selection + for (ControlPointSelection::iterator i = sel.begin(); i != sel.end(); ++i) { + Node *node = dynamic_cast(i->first); + if (!node) continue; + NodeList::iterator iter = NodeList::get_iterator(node); + if (!iter.next() || !iter.prev()) join_iters.insert(iter); + } + + if (join_iters.size() < 2) return; + + // Below we find the closest pairs. The algorithm is O(N^3). + // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs + // with their distances in a multimap (not worth it IMO). + while (join_iters.size() >= 2) { + double closest = DBL_MAX; + IterPair closest_pair; + for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) { + for (IterSet::iterator j = join_iters.begin(); j != i; ++j) { + double dist = Geom::distance(**i, **j); + if (dist < closest) { + closest = dist; + closest_pair = std::make_pair(*i, *j); + } + } + } + pairs.push_back(closest_pair); + join_iters.erase(closest_pair.first); + join_iters.erase(closest_pair.second); + } +} + +/** After this function, first should be at the end of path and second at the beginnning. + * @returns True if the nodes are in the same subpath */ +bool prepare_join(IterPair &join_iters) +{ + if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) { + if (join_iters.first.next()) // if first is begin, swap the iterators + std::swap(join_iters.first, join_iters.second); + return true; + } + + NodeList &sp_first = NodeList::get(join_iters.first); + NodeList &sp_second = NodeList::get(join_iters.second); + if (join_iters.first.next()) { // first is begin + if (join_iters.second.next()) { // second is begin + sp_first.reverse(); + } else { // second is end + std::swap(join_iters.first, join_iters.second); + } + } else { // first is end + if (join_iters.second.next()) { // second is begin + // do nothing + } else { // second is end + sp_second.reverse(); + } + } + return false; +} +} // anonymous namespace + + +MultiPathManipulator::MultiPathManipulator(PathSharedData &data, sigc::connection &chg) + : PointManipulator(data.node_data.desktop, *data.node_data.selection) + , _path_data(data) + , _changed(chg) +{ + _selection.signal_commit.connect( + sigc::mem_fun(*this, &MultiPathManipulator::_commit)); + _selection.signal_point_changed.connect( + sigc::hide( sigc::hide( + signal_coords_changed.make_slot()))); +} + +MultiPathManipulator::~MultiPathManipulator() +{ + _mmap.clear(); +} + +/** Remove empty manipulators. */ +void MultiPathManipulator::cleanup() +{ + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) { + if (i->second->empty()) _mmap.erase(i++); + else ++i; + } +} + +/** @brief Change the set of items to edit. + * + * This method attempts to preserve as much of the state as possible. */ +void MultiPathManipulator::setItems(std::set const &s) +{ + std::set shapes(s); + + // iterate over currently edited items, modifying / removing them as necessary + for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) { + std::set::iterator si = shapes.find(i->first); + if (si == shapes.end()) { + // This item is no longer supposed to be edited - remove its manipulator + _mmap.erase(i++); + } else { + ShapeRecord const &sr = i->first; + ShapeRecord const &sr_new = *si; + // if the shape record differs, replace the key only and modify other values + if (sr.edit_transform != sr_new.edit_transform || + sr.role != sr_new.role) + { + boost::shared_ptr hold(i->second); + if (sr.edit_transform != sr_new.edit_transform) + hold->setControlsTransform(sr_new.edit_transform); + if (sr.role != sr_new.role) { + //hold->setOutlineColor(_getOutlineColor(sr_new.role)); + } + _mmap.erase(sr); + _mmap.insert(std::make_pair(sr_new, hold)); + } + shapes.erase(si); // remove the processed record + ++i; + } + } + + // add newly selected items + for (std::set::iterator i = shapes.begin(); i != shapes.end(); ++i) { + ShapeRecord const &r = *i; + if (!SP_IS_PATH(r.item) && !IS_LIVEPATHEFFECT(r.item)) continue; + boost::shared_ptr newpm(new PathManipulator(*this, (SPPath*) r.item, + r.edit_transform, _getOutlineColor(r.role), r.lpe_key)); + newpm->showHandles(_show_handles); + // always show outlines for clips and masks + newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL); + newpm->showPathDirection(_show_path_direction); + _mmap.insert(std::make_pair(r, newpm)); + } +} + +void MultiPathManipulator::selectSubpaths() +{ + if (_selection.empty()) { + _selection.selectAll(); + } else { + invokeForAll(&PathManipulator::selectSubpaths); + } +} + +void MultiPathManipulator::shiftSelection(int dir) +{ + invokeForAll(&PathManipulator::shiftSelection, dir); +} + +void MultiPathManipulator::invertSelectionInSubpaths() +{ + invokeForAll(&PathManipulator::invertSelectionInSubpaths); +} + +void MultiPathManipulator::setNodeType(NodeType type) +{ + if (_selection.empty()) return; + for (ControlPointSelection::iterator i = _selection.begin(); i != _selection.end(); ++i) { + Node *node = dynamic_cast(i->first); + if (node) node->setType(type); + } + _done(_("Change node type")); +} + +void MultiPathManipulator::setSegmentType(SegmentType type) +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::setSegmentType, type); + if (type == SEGMENT_STRAIGHT) { + _done(_("Straighten segments")); + } else { + _done(_("Make segments curves")); + } +} + +void MultiPathManipulator::insertNodes() +{ + invokeForAll(&PathManipulator::insertNodes); + _done(_("Add nodes")); +} + +void MultiPathManipulator::joinNodes() +{ + invokeForAll(&PathManipulator::hideDragPoint); + // Node join has two parts. In the first one we join two subpaths by fusing endpoints + // into one. In the second we fuse nodes in each subpath. + IterPairList joins; + NodeList::iterator preserve_pos; + Node *mouseover_node = dynamic_cast(ControlPoint::mouseovered_point); + if (mouseover_node) { + preserve_pos = NodeList::get_iterator(mouseover_node); + } + find_join_iterators(_selection, joins); + + for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) { + bool same_path = prepare_join(*i); + bool mouseover = true; + NodeList &sp_first = NodeList::get(i->first); + NodeList &sp_second = NodeList::get(i->second); + i->first->setType(NODE_CUSP, false); + + Geom::Point joined_pos, pos_front, pos_back; + pos_front = *i->second->front(); + pos_back = *i->first->back(); + if (i->first == preserve_pos) { + joined_pos = *i->first; + } else if (i->second == preserve_pos) { + joined_pos = *i->second; + } else { + joined_pos = Geom::middle_point(pos_back, pos_front); + mouseover = false; + } + + // if the handles aren't degenerate, don't move them + i->first->move(joined_pos); + Node *joined_node = i->first.ptr(); + if (!i->second->front()->isDegenerate()) { + joined_node->front()->setPosition(pos_front); + } + if (!i->first->back()->isDegenerate()) { + joined_node->back()->setPosition(pos_back); + } + if (mouseover) { + // Second node could be mouseovered, but it will be deleted, so we must change + // the preserve_pos iterator to the first node. + preserve_pos = i->first; + } + sp_second.erase(i->second); + + if (same_path) { + sp_first.setClosed(true); + } else { + sp_first.splice(sp_first.end(), sp_second); + sp_second.kill(); + } + _selection.insert(i->first.ptr()); + } - // Second part replaces contiguous selections of nodes with single nodes - invokeForAll(&PathManipulator::weldNodes, preserve_pos); ++ ++ if (joins.empty()) { ++ // Second part replaces contiguous selections of nodes with single nodes ++ invokeForAll(&PathManipulator::weldNodes, preserve_pos); ++ } ++ + _doneWithCleanup(_("Join nodes")); +} + +void MultiPathManipulator::breakNodes() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::breakNodes); + _done(_("Break nodes")); +} + +void MultiPathManipulator::deleteNodes(bool keep_shape) +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::deleteNodes, keep_shape); + _doneWithCleanup(_("Delete nodes")); +} + +/** Join selected endpoints to create segments. */ +void MultiPathManipulator::joinSegment() +{ + IterPairList joins; + find_join_iterators(_selection, joins); + if (joins.empty()) { + _desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, + _("There must be at least 2 endnodes in selection")); + return; + } + + for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) { + bool same_path = prepare_join(*i); + NodeList &sp_first = NodeList::get(i->first); + NodeList &sp_second = NodeList::get(i->second); + i->first->setType(NODE_CUSP, false); + i->second->setType(NODE_CUSP, false); + if (same_path) { + sp_first.setClosed(true); + } else { + sp_first.splice(sp_first.end(), sp_second); + sp_second.kill(); + } + } + + _doneWithCleanup("Join segments"); +} + +void MultiPathManipulator::deleteSegments() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::deleteSegments); + _doneWithCleanup("Delete segments"); +} + +void MultiPathManipulator::alignNodes(Geom::Dim2 d) +{ + _selection.align(d); + if (d == Geom::X) { + _done("Align nodes to a horizontal line"); + } else { + _done("Align nodes to a vertical line"); + } +} + +void MultiPathManipulator::distributeNodes(Geom::Dim2 d) +{ + _selection.distribute(d); + if (d == Geom::X) { + _done("Distrubute nodes horizontally"); + } else { + _done("Distribute nodes vertically"); + } +} + +void MultiPathManipulator::reverseSubpaths() +{ + invokeForAll(&PathManipulator::reverseSubpaths); + _done("Reverse selected subpaths"); +} + +void MultiPathManipulator::move(Geom::Point const &delta) +{ + _selection.transform(Geom::Translate(delta)); + _done("Move nodes"); +} + +void MultiPathManipulator::showOutline(bool show) +{ + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + // always show outlines for clipping paths and masks + i->second->showOutline(show || i->first.role != SHAPE_ROLE_NORMAL); + } + _show_outline = show; +} + +void MultiPathManipulator::showHandles(bool show) +{ + invokeForAll(&PathManipulator::showHandles, show); + _show_handles = show; +} + +void MultiPathManipulator::showPathDirection(bool show) +{ + invokeForAll(&PathManipulator::showPathDirection, show); + _show_path_direction = show; +} + +void MultiPathManipulator::updateOutlineColors() +{ + //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + // i->second->setOutlineColor(_getOutlineColor(i->first.role)); + //} +} + +bool MultiPathManipulator::event(GdkEvent *event) +{ + switch (event->type) { + case GDK_KEY_PRESS: + switch (shortcut_key(event->key)) { + case GDK_Insert: + case GDK_KP_Insert: + insertNodes(); + return true; + case GDK_i: + case GDK_I: + if (held_only_shift(event->key)) { + insertNodes(); + return true; + } + break; + case GDK_j: + case GDK_J: + if (held_only_shift(event->key)) { + joinNodes(); + return true; + } + if (held_only_alt(event->key)) { + joinSegment(); + return true; + } + break; + case GDK_b: + case GDK_B: + if (held_only_shift(event->key)) { + breakNodes(); + return true; + } + break; + case GDK_Delete: + case GDK_KP_Delete: + case GDK_BackSpace: + if (held_shift(event->key)) break; + if (held_alt(event->key)) { + deleteSegments(); + } else { + deleteNodes(!held_control(event->key)); + } + return true; + case GDK_c: + case GDK_C: + if (held_only_shift(event->key)) { + setNodeType(NODE_CUSP); + return true; + } + break; + case GDK_s: + case GDK_S: + if (held_only_shift(event->key)) { + setNodeType(NODE_SMOOTH); + return true; + } + break; + case GDK_a: + case GDK_A: + if (held_only_shift(event->key)) { + setNodeType(NODE_AUTO); + return true; + } + break; + case GDK_y: + case GDK_Y: + if (held_only_shift(event->key)) { + setNodeType(NODE_SYMMETRIC); + return true; + } + break; + case GDK_r: + case GDK_R: + if (held_only_shift(event->key)) { + reverseSubpaths(); + break; + } + break; + default: + break; + } + break; + default: break; + } + + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + if (i->second->event(event)) return true; + } + return false; +} + +/** Commit changes to XML and add undo stack entry based on the action that was done. Invoked + * by sub-manipulators, for example TransformHandleSet and ControlPointSelection. */ +void MultiPathManipulator::_commit(CommitEvent cps) +{ + gchar const *reason = NULL; + gchar const *key = NULL; + switch(cps) { + case COMMIT_MOUSE_MOVE: + reason = _("Move nodes"); + break; + case COMMIT_KEYBOARD_MOVE_X: + reason = _("Move nodes horizontally"); + key = "node:move:x"; + break; + case COMMIT_KEYBOARD_MOVE_Y: + reason = _("Move nodes vertically"); + key = "node:move:y"; + break; + case COMMIT_MOUSE_ROTATE: + reason = _("Rotate nodes"); + break; + case COMMIT_KEYBOARD_ROTATE: + reason = _("Rotate nodes"); + key = "node:rotate"; + break; + case COMMIT_MOUSE_SCALE_UNIFORM: + reason = _("Scale nodes uniformly"); + break; + case COMMIT_MOUSE_SCALE: + reason = _("Scale nodes"); + break; + case COMMIT_KEYBOARD_SCALE_UNIFORM: + reason = _("Scale nodes uniformly"); + key = "node:scale:uniform"; + break; + case COMMIT_KEYBOARD_SCALE_X: + reason = _("Scale nodes horizontally"); + key = "node:scale:x"; + break; + case COMMIT_KEYBOARD_SCALE_Y: + reason = _("Scale nodes vertically"); + key = "node:scale:y"; + break; + case COMMIT_FLIP_X: + reason = _("Flip nodes horizontally"); + break; + case COMMIT_FLIP_Y: + reason = _("Flip nodes vertically"); + break; + default: return; + } + + _selection.signal_update.emit(); + invokeForAll(&PathManipulator::writeXML); + if (key) { + sp_document_maybe_done(sp_desktop_document(_desktop), key, SP_VERB_CONTEXT_NODE, reason); + } else { + sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason); + } + signal_coords_changed.emit(); +} + +/** Commits changes to XML and adds undo stack entry. */ +void MultiPathManipulator::_done(gchar const *reason) { + invokeForAll(&PathManipulator::update); + invokeForAll(&PathManipulator::writeXML); + sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason); + signal_coords_changed.emit(); +} + +/** Commits changes to XML, adds undo stack entry and removes empty manipulators. */ +void MultiPathManipulator::_doneWithCleanup(gchar const *reason) { + _changed.block(); + _done(reason); + cleanup(); + _changed.unblock(); +} + +/** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */ +guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch(role) { + case SHAPE_ROLE_CLIPPING_PATH: + return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff); + case SHAPE_ROLE_MASK: + return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff); + case SHAPE_ROLE_LPE_PARAM: + return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff); + case SHAPE_ROLE_NORMAL: + default: + return prefs->getColor("/tools/nodes/outline_color", 0xff0000ff); + } +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --cc src/ui/tool/node.cpp index 889f4a793,000000000..303c0fb75 mode 100644,000000..100644 --- a/src/ui/tool/node.cpp +++ b/src/ui/tool/node.cpp @@@ -1,1203 -1,0 +1,1204 @@@ +/** @file + * Editable node - implementation + */ +/* Authors: + * Krzysztof Kosiński + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include +#include +#include +#include +#include +#include <2geom/bezier-utils.h> +#include <2geom/transforms.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 { + +static SelectableControlPoint::ColorSet node_colors = { + { + {0xbfbfbf00, 0x000000ff}, // normal fill, stroke + {0xff000000, 0x000000ff}, // mouseover fill, stroke + {0xff000000, 0x000000ff} // clicked fill, stroke + }, + {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected + {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected + {0xff000000, 0x000000ff} // clicked fill, stroke when selected +}; + +static ControlPoint::ColorSet handle_colors = { + {0xffffffff, 0x000000ff}, // normal fill, stroke + {0xff000000, 0x000000ff}, // mouseover fill, stroke + {0xff000000, 0x000000ff} // clicked fill, stroke +}; + +std::ostream &operator<<(std::ostream &out, NodeType type) +{ + switch(type) { + case NODE_CUSP: out << 'c'; break; + case NODE_SMOOTH: out << 's'; break; + case NODE_AUTO: out << 'a'; break; + case NODE_SYMMETRIC: out << 'z'; break; + default: out << 'b'; break; + } + return out; +} + +/** Computes an unit vector of the direction from first to second control point */ +static Geom::Point direction(Geom::Point const &first, Geom::Point const &second) { + return Geom::unit_vector(second - first); +} + +/** + * @class Handle + * @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; +bool Handle::_drag_out = false; + +Handle::Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent) + : ControlPoint(data.desktop, initial_pos, Gtk::ANCHOR_CENTER, SP_CTRL_SHAPE_CIRCLE, 7.0, + &handle_colors, data.handle_group) + , _parent(parent) + , _degenerate(true) +{ + _cset = &handle_colors; + _handle_line = sp_canvas_item_new(data.handle_line_group, SP_TYPE_CTRLLINE, NULL); + setVisible(false); + signal_grabbed.connect( + sigc::bind_return( + sigc::hide( + sigc::mem_fun(*this, &Handle::_grabbedHandler)), + false)); + signal_dragged.connect( + sigc::hide<0>( + sigc::mem_fun(*this, &Handle::_draggedHandler))); + signal_ungrabbed.connect( + sigc::hide(sigc::mem_fun(*this, &Handle::_ungrabbedHandler))); +} +Handle::~Handle() +{ + sp_canvas_item_hide(_handle_line); + gtk_object_destroy(GTK_OBJECT(_handle_line)); +} + +void Handle::setVisible(bool v) +{ + ControlPoint::setVisible(v); + if (v) sp_canvas_item_show(_handle_line); + else sp_canvas_item_hide(_handle_line); +} + +void Handle::move(Geom::Point const &new_pos) +{ + Handle *other, *towards, *towards_second; + Node *node_towards; // node in direction of this handle + Node *node_away; // node in the opposite direction + if (this == &_parent->_front) { + other = &_parent->_back; + node_towards = _parent->_next(); + node_away = _parent->_prev(); + towards = node_towards ? &node_towards->_back : 0; + towards_second = node_towards ? &node_towards->_front : 0; + } else { + other = &_parent->_front; + node_towards = _parent->_prev(); + node_away = _parent->_next(); + towards = node_towards ? &node_towards->_front : 0; + towards_second = node_towards ? &node_towards->_back : 0; + } + + if (Geom::are_near(new_pos, _parent->position())) { + // The handle becomes degenerate. If the segment between it and the node + // in its direction becomes linear and there are smooth nodes + // at its ends, make their handles colinear with the segment + if (towards && towards->isDegenerate()) { + if (node_towards->type() == NODE_SMOOTH) { + towards_second->setDirection(*_parent, *node_towards); + } + if (_parent->type() == NODE_SMOOTH) { + other->setDirection(*node_towards, *_parent); + } + } + setPosition(new_pos); + return; + } + + if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) { + // restrict movement to the line joining the nodes + Geom::Point direction = _parent->position() - node_away->position(); + Geom::Point delta = new_pos - _parent->position(); + // project the relative position on the direction line + Geom::Point new_delta = (Geom::dot(delta, direction) + / Geom::L2sq(direction)) * direction; + setRelativePos(new_delta); + return; + } + + switch (_parent->type()) { + case NODE_AUTO: + _parent->setType(NODE_SMOOTH, false); + // fall through - auto nodes degrade into smooth nodes + case NODE_SMOOTH: { + /* for smooth nodes, we need to rotate the other handle so that it's colinear + * with the dragged one while conserving length. */ + other->setDirection(new_pos, *_parent); + } break; + case NODE_SYMMETRIC: + // for symmetric nodes, place the other handle on the opposite side + other->setRelativePos(-(new_pos - _parent->position())); + break; + default: break; + } + + setPosition(new_pos); +} + +void Handle::setPosition(Geom::Point const &p) +{ + ControlPoint::setPosition(p); + sp_ctrlline_set_coords(SP_CTRLLINE(_handle_line), _parent->position(), position()); + + // update degeneration info and visibility + if (Geom::are_near(position(), _parent->position())) + _degenerate = true; + else _degenerate = false; + if (_parent->_handles_shown && _parent->visible() && !_degenerate) { + setVisible(true); + } else { + setVisible(false); + } + // If both handles become degenerate, convert to parent cusp node + if (_parent->isDegenerate()) { + _parent->setType(NODE_CUSP, false); + } +} + +void Handle::setLength(double len) +{ + if (isDegenerate()) return; + Geom::Point dir = Geom::unit_vector(relativePos()); + setRelativePos(dir * len); +} + +void Handle::retract() +{ + setPosition(_parent->position()); +} + +void Handle::setDirection(Geom::Point const &from, Geom::Point const &to) +{ + setDirection(to - from); +} + +void Handle::setDirection(Geom::Point const &dir) +{ + Geom::Point unitdir = Geom::unit_vector(dir); + setRelativePos(unitdir * length()); +} + +char const *Handle::handle_type_to_localized_string(NodeType type) +{ + switch(type) { + case NODE_CUSP: return _("Cusp node handle"); + case NODE_SMOOTH: return _("Smooth node handle"); + case NODE_SYMMETRIC: return _("Symmetric node handle"); + case NODE_AUTO: return _("Auto-smooth node handle"); + default: return ""; + } +} + +void Handle::_grabbedHandler() +{ + _saved_length = _drag_out ? 0 : length(); +} + +void Handle::_draggedHandler(Geom::Point &new_pos, GdkEventMotion *event) +{ + Geom::Point parent_pos = _parent->position(); + // with Alt, preserve length + if (held_alt(*event)) { + new_pos = parent_pos + Geom::unit_vector(new_pos - parent_pos) * _saved_length; + } + // with Ctrl, constrain to M_PI/rotationsnapsperpi increments. + if (held_control(*event)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = 2 * prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + Geom::Point origin = _last_drag_origin(); + Geom::Point rel_origin = origin - parent_pos; + new_pos = parent_pos + Geom::constrain_angle(Geom::Point(0,0), new_pos - parent_pos, snaps, + _drag_out ? Geom::Point(1,0) : Geom::unit_vector(rel_origin)); + } + signal_update.emit(); +} + +void Handle::_ungrabbedHandler() +{ + // hide the handle if it's less than dragtolerance away from the node + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + Geom::Point dist = _desktop->d2w(_parent->position()) - _desktop->d2w(position()); + if (dist.length() <= drag_tolerance) { + move(_parent->position()); + } + _drag_out = false; +} + +static double snap_increment_degrees() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + return 180.0 / snaps; +} + +Glib::ustring Handle::_getTip(unsigned state) +{ + if (state_held_alt(state)) { + if (state_held_control(state)) { + return format_tip(C_("Path handle tip", + "Ctrl+Alt: preserve length and snap rotation angle to %f° increments"), + snap_increment_degrees()); + } else { + return C_("Path handle tip", + "Alt: preserve handle length while dragging"); + } + } else { + if (state_held_control(state)) { + return format_tip(C_("Path handle tip", + "Ctrl: snap rotation angle to %f° increments, click to retract"), + snap_increment_degrees()); + } + } + switch (_parent->type()) { + case NODE_AUTO: + return C_("Path handle tip", + "Auto node handle: drag to convert to smooth node"); + default: + return format_tip(C_("Path handle tip", "%s: drag to shape the curve"), + handle_type_to_localized_string(_parent->type())); + } +} + +Glib::ustring Handle::_getDragTip(GdkEventMotion *event) +{ + Geom::Point dist = position() - _last_drag_origin(); + // report angle in mathematical convention + double angle = Geom::angle_between(Geom::Point(-1,0), position() - _parent->position()); + angle += M_PI; // angle is (-M_PI...M_PI] - offset by +pi and scale to 0...360 + angle *= 360.0 / (2 * M_PI); + 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()); + GString *len = SP_PX_TO_METRIC_STRING(length(), _desktop->namedview->getDefaultMetric()); + Glib::ustring ret = format_tip(C_("Path handle tip", + "Move by %s, %s; angle %.2f°, length %s"), x->str, y->str, angle, len->str); + g_string_free(x, TRUE); + g_string_free(y, TRUE); + g_string_free(len, TRUE); + return ret; +} + +/** + * @class Node + * @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) + : SelectableControlPoint(data.desktop, initial_pos, Gtk::ANCHOR_CENTER, + SP_CTRL_SHAPE_DIAMOND, 9.0, *data.selection, &node_colors, data.node_group) + , _front(data, initial_pos, this) + , _back(data, initial_pos, this) + , _type(NODE_CUSP) + , _handles_shown(false) +{ + // NOTE we do not set type here, because the handles are still degenerate + // connect to own grabbed signal - dragging out handles + signal_grabbed.connect( + sigc::mem_fun(*this, &Node::_grabbedHandler)); + signal_dragged.connect( sigc::hide<0>( + sigc::mem_fun(*this, &Node::_draggedHandler))); +} + +// NOTE: not using iterators won't make this much quicker because iterators can be 100% inlined. +Node *Node::_next() +{ + NodeList::iterator n = NodeList::get_iterator(this).next(); + if (n) return n.ptr(); + return NULL; +} +Node *Node::_prev() +{ + NodeList::iterator p = NodeList::get_iterator(this).prev(); + if (p) return p.ptr(); + return NULL; +} + +void Node::move(Geom::Point const &new_pos) +{ + // move handles when the node moves. + Geom::Point old_pos = position(); + Geom::Point delta = new_pos - position(); + setPosition(new_pos); + _front.setPosition(_front.position() + delta); + _back.setPosition(_back.position() + delta); + + // if the node has a smooth handle after a line segment, it should be kept colinear + // with the segment + _fixNeighbors(old_pos, new_pos); +} + +void Node::transform(Geom::Matrix const &m) +{ + Geom::Point old_pos = position(); + setPosition(position() * m); + _front.setPosition(_front.position() * m); + _back.setPosition(_back.position() * m); + + /* Affine transforms keep handle invariants for smooth and symmetric nodes, + * but smooth nodes at ends of linear segments and auto nodes need special treatment */ + _fixNeighbors(old_pos, position()); +} + +Geom::Rect Node::bounds() +{ + Geom::Rect b(position(), position()); + b.expandTo(_front.position()); + b.expandTo(_back.position()); + return b; +} + +void Node::_fixNeighbors(Geom::Point const &old_pos, Geom::Point const &new_pos) +{ + /* This method restores handle invariants for neighboring nodes, + * and invariants that are based on positions of those nodes for this one. */ + + /* Fix auto handles */ + if (_type == NODE_AUTO) _updateAutoHandles(); + if (old_pos != new_pos) { + if (_next() && _next()->_type == NODE_AUTO) _next()->_updateAutoHandles(); + if (_prev() && _prev()->_type == NODE_AUTO) _prev()->_updateAutoHandles(); + } + + /* Fix smooth handles at the ends of linear segments. + * Rotate the appropriate handle to be colinear with the segment. + * If there is a smooth node at the other end of the segment, rotate it too. */ + Handle *handle, *other_handle; + Node *other; + if (_is_line_segment(this, _next())) { + handle = &_back; + other = _next(); + other_handle = &_next()->_front; + } else if (_is_line_segment(_prev(), this)) { + handle = &_front; + other = _prev(); + other_handle = &_prev()->_back; + } else return; + + if (_type == NODE_SMOOTH && !handle->isDegenerate()) { + handle->setDirection(other->position(), new_pos); + } + // 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()); + } +} + +void Node::_updateAutoHandles() +{ + // Recompute the position of automatic handles. + // 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; + } + + // 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(); + } +} + +void Node::showHandles(bool v) +{ + _handles_shown = v; + if (!_front.isDegenerate()) _front.setVisible(v); + if (!_back.isDegenerate()) _back.setVisible(v); +} + +/** Sets the node type and optionally restores the invariants associated with the given type. + * @param type The type to set + * @param update_handles Whether to restore invariants associated with the given type. + * Passing false is useful e.g. wen initially creating the path, + * and when making cusp nodes during some node algorithms. + * Pass true when used in response to an UI node type button. + */ +void Node::setType(NodeType type, bool update_handles) +{ + if (type == NODE_PICK_BEST) { + pickBestType(); + updateState(); // The size of the control might have changed + return; + } + + // if update_handles is true, adjust handle positions to match the node type + // handle degenerate handles appropriately + if (update_handles) { + switch (type) { + case NODE_CUSP: + // if the existing type is also NODE_CUSP, retract handles + if (_type == NODE_CUSP) { + _front.retract(); + _back.retract(); + } + break; + case NODE_AUTO: + // auto handles make no sense for endnodes + if (isEndNode()) return; + _updateAutoHandles(); + break; + case NODE_SMOOTH: { + // rotate handles to be colinear + // for degenerate nodes set positions like auto handles + bool prev_line = _is_line_segment(_prev(), this); + bool next_line = _is_line_segment(this, _next()); + if (isDegenerate()) { + _updateAutoHandles(); + } else if (_front.isDegenerate()) { + // if the front handle is degenerate and this...next is a line segment, + // make back colinear; otherwise pull out the other handle + // to 1/3 of distance to prev + if (next_line) { + _back.setDirection(*_next(), *this); + } else if (_prev()) { + Geom::Point dir = direction(_back, *this); + _front.setRelativePos((_prev()->position() - position()).length() / 3 * dir); + } + } else if (_back.isDegenerate()) { + if (prev_line) { + _front.setDirection(*_prev(), *this); + } else if (_next()) { + Geom::Point dir = direction(_front, *this); + _back.setRelativePos((_next()->position() - position()).length() / 3 * dir); + } + } else { + // both handles are extended. make colinear while keeping length + // first make back colinear with the vector front ---> back, + // then make front colinear with back ---> node + // (not back ---> front because back's position was changed in the first call) + _back.setDirection(_front, _back); + _front.setDirection(_back, *this); + } + } break; + case NODE_SYMMETRIC: + if (isEndNode()) return; // symmetric handles make no sense for endnodes + if (isDegenerate()) { + // similar to auto handles but set the same length for both + 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(); + double len = (len_next + len_prev) / 6; // take 1/3 of average + if (len == 0) return; + + Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev); + _back.setRelativePos(-dir * len); + _front.setRelativePos(dir * len); + } else { + // Both handles are extended. Compute average length, use direction from + // back handle to front handle. This also works correctly for degenerates + double len = (_front.length() + _back.length()) / 2; + Geom::Point dir = direction(_back, _front); + _front.setRelativePos(dir * len); + _back.setRelativePos(-dir * len); + } + break; + default: break; + } + } + _type = type; + _setShape(_node_type_to_shape(type)); + 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; + bool front_degen = _front.isDegenerate(); + bool back_degen = _back.isDegenerate(); + bool both_degen = front_degen && back_degen; + bool neither_degen = !front_degen && !back_degen; + do { + // if both handles are degenerate, do nothing + if (both_degen) break; + // if neither are degenerate, check their respective positions + if (neither_degen) { + Geom::Point front_delta = _front.position() - position(); + Geom::Point back_delta = _back.position() - position(); + // for now do not automatically make nodes symmetric, it can be annoying + /*if (Geom::are_near(front_delta, -back_delta)) { + _type = NODE_SYMMETRIC; + break; + }*/ + if (Geom::are_near(Geom::unit_vector(front_delta), + Geom::unit_vector(-back_delta))) + { + _type = NODE_SMOOTH; + break; + } + } + // check whether the handle aligns with the previous line segment. + // we know that if front is degenerate, back isn't, because + // both_degen was false + if (front_degen && _next() && _next()->_back.isDegenerate()) { + Geom::Point segment_delta = Geom::unit_vector(_next()->position() - position()); + Geom::Point handle_delta = Geom::unit_vector(_back.position() - position()); + if (Geom::are_near(segment_delta, -handle_delta)) { + _type = NODE_SMOOTH; + break; + } + } else if (back_degen && _prev() && _prev()->_front.isDegenerate()) { + Geom::Point segment_delta = Geom::unit_vector(_prev()->position() - position()); + Geom::Point handle_delta = Geom::unit_vector(_front.position() - position()); + if (Geom::are_near(segment_delta, -handle_delta)) { + _type = NODE_SMOOTH; + break; + } + } + } while (false); + _setShape(_node_type_to_shape(_type)); + updateState(); +} + +bool Node::isEndNode() +{ + return !_prev() || !_next(); +} + +/** Move the node to the bottom of its canvas group. Useful for node break, to ensure that + * the selected nodes are above the unselected ones. */ +void Node::sink() +{ + sp_canvas_item_move_to_z(_canvas_item, 0); +} + +NodeType Node::parse_nodetype(char x) +{ + switch (x) { + case 'a': return NODE_AUTO; + case 'c': return NODE_CUSP; + case 's': return NODE_SMOOTH; + case 'z': return NODE_SYMMETRIC; + default: return NODE_PICK_BEST; + } +} + +/** 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 + switch (_type) { + case NODE_AUTO: + case NODE_CUSP: + if (selected()) _setSize(11); + else _setSize(9); + break; + default: + if(selected()) _setSize(9); + else _setSize(7); + break; + } + SelectableControlPoint::_setState(state); +} + +bool Node::_grabbedHandler(GdkEventMotion *event) +{ + // 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. + double angle_next = HUGE_VAL; + double angle_prev = HUGE_VAL; + bool has_degenerate = false; + // determine which handle to drag out based on degeneration and the direction of drag + if (_front.isDegenerate() && _next()) { + Geom::Point next_relpos = _desktop->d2w(_next()->position()) + - _desktop->d2w(position()); + angle_next = fabs(Geom::angle_between(rel_evp, next_relpos)); + has_degenerate = true; + } + if (_back.isDegenerate() && _prev()) { + Geom::Point prev_relpos = _desktop->d2w(_prev()->position()) + - _desktop->d2w(position()); + angle_prev = fabs(Geom::angle_between(rel_evp, prev_relpos)); + has_degenerate = true; + } + if (!has_degenerate) return false; + h = angle_next < angle_prev ? &_front : &_back; + + h->setPosition(_desktop->w2d(evp)); + h->setVisible(true); + h->transferGrab(this, event); + Handle::_drag_out = true; + return true; +} + +void Node::_draggedHandler(Geom::Point &new_pos, GdkEventMotion *event) +{ + // 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< std::pair > unselected; ++ std::vector 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. */ ++ * 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(*i); - unselected.push_back(std::make_pair((*i)->position(), (int) n->_snapTargetType())); ++ 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(); + 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, position(), _snapSourceType(), line_front); - bp = sm.constrainedSnap(t, position(), _snapSourceType(), line_back); ++ 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 { + // 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, position(), _snapSourceType(), line_x); - bp = sm.constrainedSnap(t, position(), _snapSourceType(), line_y); ++ 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 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)) { + if ((_next() && _front.isDegenerate()) || (_prev() && _back.isDegenerate())) { + if (state_held_control(state)) { + return format_tip(C_("Path node tip", + "Shift+Ctrl: drag out a handle and snap its angle " + "to %f° increments"), snap_increment_degrees()); + } + return C_("Path node tip", + "Shift: drag out a handle, click to toggle selection"); + } + return C_("Path node tip", "Shift: click to toggle selection"); + } + + if (state_held_control(state)) { + if (state_held_alt(state)) { + return C_("Path node tip", "Ctrl+Alt: move along handle lines"); + } + return C_("Path node tip", + "Ctrl: move along axes, click to change node type"); + } + + // assemble tip from node name + char const *nodetype = node_type_to_localized_string(_type); + return format_tip(C_("Path node tip", + "%s: drag to shape the path, click to select this node"), nodetype); +} + +Glib::ustring Node::_getDragTip(GdkEventMotion *event) +{ + 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 tip", "Move by %s, %s"), + x->str, y->str); + g_string_free(x, TRUE); + g_string_free(y, TRUE); + return ret; +} + +char const *Node::node_type_to_localized_string(NodeType type) +{ + switch (type) { + case NODE_CUSP: return _("Cusp node"); + case NODE_SMOOTH: return _("Smooth node"); + case NODE_SYMMETRIC: return _("Symmetric node"); + case NODE_AUTO: return _("Auto-smooth node"); + default: return ""; + } +} + +/** Determine whether two nodes are joined by a linear segment. */ +bool Node::_is_line_segment(Node *first, Node *second) +{ + if (!first || !second) return false; + if (first->_next() == second) + return first->_front.isDegenerate() && second->_back.isDegenerate(); + if (second->_next() == first) + return second->_front.isDegenerate() && first->_back.isDegenerate(); + return false; +} + +SPCtrlShapeType Node::_node_type_to_shape(NodeType type) +{ + switch(type) { + case NODE_CUSP: return SP_CTRL_SHAPE_DIAMOND; + case NODE_SMOOTH: return SP_CTRL_SHAPE_SQUARE; + case NODE_AUTO: return SP_CTRL_SHAPE_CIRCLE; + case NODE_SYMMETRIC: return SP_CTRL_SHAPE_SQUARE; + default: return SP_CTRL_SHAPE_DIAMOND; + } +} + + +/** + * @class NodeList + * @brief An editable list of nodes representing a subpath. + * + * 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. + */ + +NodeList::NodeList(SubpathList &splist) + : _list(splist) + , _closed(false) +{ + this->list = this; + this->next = this; + this->prev = this; +} + +NodeList::~NodeList() +{ + clear(); +} + +bool NodeList::empty() +{ + return next == this; +} + +NodeList::size_type NodeList::size() +{ + size_type sz = 0; + for (ListNode *ln = next; ln != this; ln = ln->next) ++sz; + return sz; +} + +bool NodeList::closed() +{ + return _closed; +} + +/** A subpath is degenerate if it has no segments - either one node in an open path + * or no nodes in a closed path */ +bool NodeList::degenerate() +{ + return closed() ? empty() : ++begin() == end(); +} + +NodeList::iterator NodeList::before(double t, double *fracpart) +{ + double intpart; + *fracpart = std::modf(t, &intpart); + int index = intpart; + + iterator ret = begin(); + std::advance(ret, index); + return ret; +} + +// insert a node before i +NodeList::iterator NodeList::insert(iterator i, Node *x) +{ + ListNode *ins = i._node; + x->next = ins; + x->prev = ins->prev; + ins->prev->next = x; + ins->prev = x; + x->ListNode::list = this; + _list.signal_insert_node.emit(x); + return iterator(x); +} + +void NodeList::splice(iterator pos, NodeList &list) +{ + splice(pos, list, list.begin(), list.end()); +} + +void NodeList::splice(iterator pos, NodeList &list, iterator i) +{ + NodeList::iterator j = i; + ++j; + splice(pos, list, i, j); +} + +void NodeList::splice(iterator pos, NodeList &list, iterator first, iterator last) +{ + ListNode *ins_beg = first._node, *ins_end = last._node, *at = pos._node; + for (ListNode *ln = ins_beg; ln != ins_end; ln = ln->next) { + list._list.signal_remove_node.emit(static_cast(ln)); + ln->list = this; + _list.signal_insert_node.emit(static_cast(ln)); + } + ins_beg->prev->next = ins_end; + ins_end->prev->next = at; + at->prev->next = ins_beg; + + ListNode *atprev = at->prev; + at->prev = ins_end->prev; + ins_end->prev = ins_beg->prev; + ins_beg->prev = atprev; +} + +void NodeList::shift(int n) +{ + // 1. make the list perfectly cyclic + next->prev = prev; + prev->next = next; + // 2. find new begin + ListNode *new_begin = next; + if (n > 0) { + for (; n > 0; --n) new_begin = new_begin->next; + } else { + for (; n < 0; ++n) new_begin = new_begin->prev; + } + // 3. relink begin to list + next = new_begin; + prev = new_begin->prev; + new_begin->prev->next = this; + new_begin->prev = this; +} + +void NodeList::reverse() +{ + for (ListNode *ln = next; ln != this; ln = ln->prev) { + std::swap(ln->next, ln->prev); + Node *node = static_cast(ln); + Geom::Point save_pos = node->front()->position(); + node->front()->setPosition(node->back()->position()); + node->back()->setPosition(save_pos); + } + std::swap(next, prev); +} + +void NodeList::clear() +{ + for (iterator i = begin(); i != end();) erase (i++); +} + +NodeList::iterator NodeList::erase(iterator i) +{ + // 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(i._node); + ListNode *rmnext = rm->next, *rmprev = rm->prev; + ++i; + _list.signal_remove_node.emit(rm); + delete rm; + rmprev->next = rmnext; + rmnext->prev = rmprev; + 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) { + if (i->get() == this) { + _list.erase(i); + return; + } + } +} + +NodeList &NodeList::get(Node *n) { + return *(n->list()); +} +NodeList &NodeList::get(iterator const &i) { + return *(i._node->list); +} + + +/** + * @class SubpathList + * @brief Editable path composed of one or more subpaths + */ + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --cc src/widgets/toolbox.cpp index ee45665f3,6724da1bb..bb13bfdad --- a/src/widgets/toolbox.cpp +++ b/src/widgets/toolbox.cpp @@@ -85,11 -86,9 +85,12 @@@ #include "../svg/css-ostringstream.h" #include "../tools-switch.h" #include "../tweak-context.h" + #include "../spray-context.h" #include "../ui/dialog/calligraphic-profile-rename.h" #include "../ui/icon-names.h" +#include "../ui/tool/control-point-selection.h" +#include "../ui/tool/node-tool.h" +#include "../ui/tool/multi-path-manipulator.h" #include "../ui/widget/style-swatch.h" #include "../verbs.h" #include "../widgets/button.h" @@@ -146,8 -171,9 +173,9 @@@ static struct sp_verb_t doubleclick_verb; } const tools[] = { { "SPSelectContext", "select_tool", SP_VERB_CONTEXT_SELECT, SP_VERB_CONTEXT_SELECT_PREFS}, - { "SPNodeContext", "node_tool", SP_VERB_CONTEXT_NODE, SP_VERB_CONTEXT_NODE_PREFS }, + { "InkNodeTool", "node_tool", SP_VERB_CONTEXT_NODE, SP_VERB_CONTEXT_NODE_PREFS }, { "SPTweakContext", "tweak_tool", SP_VERB_CONTEXT_TWEAK, SP_VERB_CONTEXT_TWEAK_PREFS }, + { "SPSprayContext", "spray_tool", SP_VERB_CONTEXT_SPRAY, SP_VERB_CONTEXT_SPRAY_PREFS }, { "SPZoomContext", "zoom_tool", SP_VERB_CONTEXT_ZOOM, SP_VERB_CONTEXT_ZOOM_PREFS }, { "SPRectContext", "rect_tool", SP_VERB_CONTEXT_RECT, SP_VERB_CONTEXT_RECT_PREFS }, { "Box3DContext", "3dbox_tool", SP_VERB_CONTEXT_3DBOX, SP_VERB_CONTEXT_3DBOX_PREFS }, @@@ -1256,8 -1336,8 +1284,8 @@@ static void sp_node_toolbox_prep(SPDesk { InkAction* inky = ink_action_new( "NodeJoinAction", -- _("Join endnodes"), -- _("Join selected endnodes"), ++ _("Join nodes"), ++ _("Join selected nodes"), INKSCAPE_ICON_NODE_JOIN, secondarySize ); g_object_set( inky, "short_label", _("Join"), NULL );