// SPDX-License-Identifier: GPL-2.0-or-later /* * A class for handling shape interaction with libavoid. * * Authors: * Michael Wybrow * Abhishek Sharma * * Copyright (C) 2005 Michael Wybrow * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include #include #include "2geom/convex-hull.h" #include "2geom/line.h" #include "conn-avoid-ref.h" #include "desktop.h" #include "document-undo.h" #include "document.h" #include "inkscape.h" #include "layer-manager.h" #include "display/curve.h" #include "3rdparty/adaptagrams/libavoid/router.h" #include "3rdparty/adaptagrams/libavoid/shape.h" #include "object/sp-namedview.h" #include "object/sp-shape.h" #include "svg/stringstream.h" #include "xml/node.h" using Inkscape::DocumentUndo; using Avoid::Router; static Avoid::Polygon avoid_item_poly(SPItem const *item); SPAvoidRef::SPAvoidRef(SPItem *spitem) : shapeRef(nullptr) , item(spitem) , setting(false) , new_setting(false) , _transformed_connection() { } SPAvoidRef::~SPAvoidRef() { _transformed_connection.disconnect(); // If the document is being destroyed then the router instance // and the ShapeRefs will have been destroyed with it. Router *router = item->document->getRouter(); if (shapeRef && router) { router->deleteShape(shapeRef); } shapeRef = nullptr; } void SPAvoidRef::setAvoid(char const *value) { // Don't keep avoidance information for cloned objects. if ( !item->cloned ) { new_setting = false; if (value && (strcmp(value, "true") == 0)) { new_setting = true; } } } void SPAvoidRef::handleSettingChange() { SPDesktop *desktop = SP_ACTIVE_DESKTOP; if (desktop == nullptr) { return; } if (desktop->getDocument() != item->document) { // We don't want to go any further if the active desktop's document // isn't the same as the document that this item is part of. This // case can happen if a new document is loaded from the file chooser // or via the recent file menu. In this case, we can end up here // as a result of a ensureUpToDate performed on a // document not yet attached to the active desktop. return; } if (new_setting == setting) { // Don't need to make any changes return; } setting = new_setting; Router *router = item->document->getRouter(); _transformed_connection.disconnect(); if (new_setting) { Avoid::Polygon poly = avoid_item_poly(item); if (poly.size() > 0) { _transformed_connection = item->connectTransformed( sigc::ptr_fun(&avoid_item_move)); char const *id = item->getAttribute("id"); g_assert(id != nullptr); // Get a unique ID for the item. GQuark itemID = g_quark_from_string(id); shapeRef = new Avoid::ShapeRef(router, poly, itemID); } } else if (shapeRef) { router->deleteShape(shapeRef); shapeRef = nullptr; } } std::vector SPAvoidRef::getAttachedShapes(const unsigned int type) { std::vector list; Avoid::IntList shapes; GQuark shapeId = g_quark_from_string(item->getId()); item->document->getRouter()->attachedShapes(shapes, shapeId, type); Avoid::IntList::iterator finish = shapes.end(); for (Avoid::IntList::iterator i = shapes.begin(); i != finish; ++i) { const gchar *connId = g_quark_to_string(*i); SPObject *obj = item->document->getObjectById(connId); if (obj == nullptr) { g_warning("getAttachedShapes: Object with id=\"%s\" is not " "found. Skipping.", connId); continue; } SPItem *shapeItem = SP_ITEM(obj); list.push_back(shapeItem); } return list; } std::vector SPAvoidRef::getAttachedConnectors(const unsigned int type) { std::vector list; Avoid::IntList conns; GQuark shapeId = g_quark_from_string(item->getId()); item->document->getRouter()->attachedConns(conns, shapeId, type); Avoid::IntList::iterator finish = conns.end(); for (Avoid::IntList::iterator i = conns.begin(); i != finish; ++i) { const gchar *connId = g_quark_to_string(*i); SPObject *obj = item->document->getObjectById(connId); if (obj == nullptr) { g_warning("getAttachedConnectors: Object with id=\"%s\" is not " "found. Skipping.", connId); continue; } SPItem *connItem = SP_ITEM(obj); list.push_back(connItem); } return list; } Geom::Point SPAvoidRef::getConnectionPointPos() { g_assert(item); // the center is all we are interested in now; we used to care // about non-center points, but that's moot. Geom::OptRect bbox = item->documentVisualBounds(); return (bbox) ? bbox->midpoint() : Geom::Point(0, 0); } static std::vector approxCurveWithPoints(SPCurve *curve) { // The number of segments to use for not straight curves approximation const unsigned NUM_SEGS = 4; const Geom::PathVector& curve_pv = curve->get_pathvector(); // The structure to hold the output std::vector poly_points; // Iterate over all curves, adding the endpoints for linear curves and // sampling the other curves double seg_size = 1.0 / NUM_SEGS; double at; at = 0; Geom::PathVector::const_iterator pit = curve_pv.begin(); while (pit != curve_pv.end()) { Geom::Path::const_iterator cit = pit->begin(); while (cit != pit->end()) { if (cit == pit->begin()) { poly_points.push_back(cit->initialPoint()); } if (dynamic_cast(&*cit)) { at += seg_size; if (at <= 1.0 ) poly_points.push_back(cit->pointAt(at)); else { at = 0.0; ++cit; } } else { poly_points.push_back(cit->finalPoint()); ++cit; } } ++pit; } return poly_points; } static std::vector approxItemWithPoints(SPItem const *item, const Geom::Affine& item_transform) { // The structure to hold the output std::vector poly_points; std::unique_ptr item_curve; auto item_mutable = const_cast(item); if (auto group = dynamic_cast(item_mutable)) { // consider all first-order children std::vector itemlist = sp_item_group_item_list(group); for (auto child_item : itemlist) { std::vector child_points = approxItemWithPoints(child_item, item_transform * child_item->transform); poly_points.insert(poly_points.end(), child_points.begin(), child_points.end()); } } else if (auto shape = dynamic_cast(item_mutable)) { shape->set_shape(); item_curve = SPCurve::copy(shape->curve()); // make sure it has an associated curve if (item_curve) { // apply transformations (up to common ancestor) item_curve->transform(item_transform); } } else { auto bbox = item->documentPreferredBounds(); if (bbox) { item_curve = SPCurve::new_from_rect(*bbox); } } if (item_curve) { std::vector curve_points = approxCurveWithPoints(item_curve.get()); poly_points.insert(poly_points.end(), curve_points.begin(), curve_points.end()); } return poly_points; } static Avoid::Polygon avoid_item_poly(SPItem const *item) { SPDesktop *desktop = SP_ACTIVE_DESKTOP; g_assert(desktop != nullptr); double spacing = desktop->namedview->connector_spacing; Geom::Affine itd_mat = item->i2doc_affine(); std::vector hull_points; hull_points = approxItemWithPoints(item, itd_mat); // create convex hull from all sampled points Geom::ConvexHull hull(hull_points); // enlarge path by "desktop->namedview->connector_spacing" // store expanded convex hull in Avoid::Polygn Avoid::Polygon poly; if (hull.empty()) { return poly; } Geom::Line hull_edge(hull.back(), hull.front()); Geom::Line prev_parallel_hull_edge; prev_parallel_hull_edge.setOrigin(hull_edge.origin()+hull_edge.versor().ccw()*spacing); prev_parallel_hull_edge.setVector(hull_edge.versor()); int hull_size = hull.size(); for (int i = 0; i < hull_size; ++i) { if (i + 1 == hull_size) { hull_edge.setPoints(hull.back(), hull.front()); } else { hull_edge.setPoints(hull[i], hull[i + 1]); } Geom::Line parallel_hull_edge; parallel_hull_edge.setOrigin(hull_edge.origin()+hull_edge.versor().ccw()*spacing); parallel_hull_edge.setVector(hull_edge.versor()); // determine the intersection point try { Geom::OptCrossing int_pt = Geom::intersection(parallel_hull_edge, prev_parallel_hull_edge); if (int_pt) { Avoid::Point avoid_pt((parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X], (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]); poly.ps.push_back(avoid_pt); } else { // something went wrong... std::cout<<"conn-avoid-ref.cpp: avoid_item_poly: Geom:intersection failed."< get_avoided_items(std::vector &list, SPObject *from, SPDesktop *desktop, bool initialised) { for (auto& child: from->children) { if (SP_IS_ITEM(&child) && !desktop->layerManager().isLayer(SP_ITEM(&child)) && !SP_ITEM(&child)->isLocked() && !desktop->itemIsHidden(SP_ITEM(&child)) && (!initialised || SP_ITEM(&child)->getAvoidRef().shapeRef) ) { list.push_back(SP_ITEM(&child)); } if (SP_IS_ITEM(&child) && desktop->layerManager().isLayer(SP_ITEM(&child))) { list = get_avoided_items(list, &child, desktop, initialised); } } return list; } void avoid_item_move(Geom::Affine const */*mp*/, SPItem *moved_item) { Avoid::ShapeRef *shapeRef = moved_item->getAvoidRef().shapeRef; g_assert(shapeRef); Router *router = moved_item->document->getRouter(); Avoid::Polygon poly = avoid_item_poly(moved_item); if (!poly.empty()) { router->moveShape(shapeRef, poly); } } void init_avoided_shape_geometry(SPDesktop *desktop) { // Don't count this as changes to the document, // it is basically just late initialisation. SPDocument *document = desktop->getDocument(); DocumentUndo::ScopedInsensitive _no_undo(document); bool initialised = false; std::vector tmp; std::vector items = get_avoided_items(tmp, desktop->layerManager().currentRoot(), desktop, initialised); for (auto item : items) { item->getAvoidRef().handleSettingChange(); } } /* 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 :