1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * A class for handling shape interaction with libavoid.
4 *
5 * Authors:
6 * Michael Wybrow <mjwybrow@users.sourceforge.net>
7 * Abhishek Sharma
8 *
9 * Copyright (C) 2005 Michael Wybrow
10 *
11 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
12 */
13
14
15 #include <cstring>
16 #include <string>
17 #include <iostream>
18
19 #include "2geom/convex-hull.h"
20 #include "2geom/line.h"
21
22 #include "conn-avoid-ref.h"
23 #include "desktop.h"
24 #include "document-undo.h"
25 #include "document.h"
26 #include "inkscape.h"
27 #include "verbs.h"
28
29 #include "display/curve.h"
30
31 #include "3rdparty/adaptagrams/libavoid/router.h"
32 #include "3rdparty/adaptagrams/libavoid/shape.h"
33
34 #include "object/sp-namedview.h"
35 #include "object/sp-shape.h"
36
37 #include "svg/stringstream.h"
38
39 #include "xml/node.h"
40
41 using Inkscape::DocumentUndo;
42
43 using Avoid::Router;
44
45 static Avoid::Polygon avoid_item_poly(SPItem const *item);
46
47
SPAvoidRef(SPItem * spitem)48 SPAvoidRef::SPAvoidRef(SPItem *spitem)
49 : shapeRef(nullptr)
50 , item(spitem)
51 , setting(false)
52 , new_setting(false)
53 , _transformed_connection()
54 {
55 }
56
57
~SPAvoidRef()58 SPAvoidRef::~SPAvoidRef()
59 {
60 _transformed_connection.disconnect();
61
62 // If the document is being destroyed then the router instance
63 // and the ShapeRefs will have been destroyed with it.
64 Router *router = item->document->getRouter();
65
66 if (shapeRef && router) {
67 router->deleteShape(shapeRef);
68 }
69 shapeRef = nullptr;
70 }
71
72
setAvoid(char const * value)73 void SPAvoidRef::setAvoid(char const *value)
74 {
75 // Don't keep avoidance information for cloned objects.
76 if ( !item->cloned ) {
77 new_setting = false;
78 if (value && (strcmp(value, "true") == 0)) {
79 new_setting = true;
80 }
81 }
82 }
83
handleSettingChange()84 void SPAvoidRef::handleSettingChange()
85 {
86 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
87 if (desktop == nullptr) {
88 return;
89 }
90 if (desktop->getDocument() != item->document) {
91 // We don't want to go any further if the active desktop's document
92 // isn't the same as the document that this item is part of. This
93 // case can happen if a new document is loaded from the file chooser
94 // or via the recent file menu. In this case, we can end up here
95 // as a result of a ensureUpToDate performed on a
96 // document not yet attached to the active desktop.
97 return;
98 }
99
100 if (new_setting == setting) {
101 // Don't need to make any changes
102 return;
103 }
104 setting = new_setting;
105
106 Router *router = item->document->getRouter();
107
108 _transformed_connection.disconnect();
109 if (new_setting) {
110 Avoid::Polygon poly = avoid_item_poly(item);
111 if (poly.size() > 0) {
112 _transformed_connection = item->connectTransformed(
113 sigc::ptr_fun(&avoid_item_move));
114
115 char const *id = item->getAttribute("id");
116 g_assert(id != nullptr);
117
118 // Get a unique ID for the item.
119 GQuark itemID = g_quark_from_string(id);
120
121 shapeRef = new Avoid::ShapeRef(router, poly, itemID);
122 }
123 }
124 else if (shapeRef)
125 {
126 router->deleteShape(shapeRef);
127 shapeRef = nullptr;
128 }
129 }
130
131
getAttachedShapes(const unsigned int type)132 std::vector<SPItem *> SPAvoidRef::getAttachedShapes(const unsigned int type)
133 {
134 std::vector<SPItem *> list;
135
136 Avoid::IntList shapes;
137 GQuark shapeId = g_quark_from_string(item->getId());
138 item->document->getRouter()->attachedShapes(shapes, shapeId, type);
139
140 Avoid::IntList::iterator finish = shapes.end();
141 for (Avoid::IntList::iterator i = shapes.begin(); i != finish; ++i) {
142 const gchar *connId = g_quark_to_string(*i);
143 SPObject *obj = item->document->getObjectById(connId);
144 if (obj == nullptr) {
145 g_warning("getAttachedShapes: Object with id=\"%s\" is not "
146 "found. Skipping.", connId);
147 continue;
148 }
149 SPItem *shapeItem = SP_ITEM(obj);
150 list.push_back(shapeItem);
151 }
152 return list;
153 }
154
155
getAttachedConnectors(const unsigned int type)156 std::vector<SPItem *> SPAvoidRef::getAttachedConnectors(const unsigned int type)
157 {
158 std::vector<SPItem *> list;
159
160 Avoid::IntList conns;
161 GQuark shapeId = g_quark_from_string(item->getId());
162 item->document->getRouter()->attachedConns(conns, shapeId, type);
163
164 Avoid::IntList::iterator finish = conns.end();
165 for (Avoid::IntList::iterator i = conns.begin(); i != finish; ++i) {
166 const gchar *connId = g_quark_to_string(*i);
167 SPObject *obj = item->document->getObjectById(connId);
168 if (obj == nullptr) {
169 g_warning("getAttachedConnectors: Object with id=\"%s\" is not "
170 "found. Skipping.", connId);
171 continue;
172 }
173 SPItem *connItem = SP_ITEM(obj);
174 list.push_back(connItem);
175 }
176 return list;
177 }
178
getConnectionPointPos()179 Geom::Point SPAvoidRef::getConnectionPointPos()
180 {
181 g_assert(item);
182 // the center is all we are interested in now; we used to care
183 // about non-center points, but that's moot.
184 Geom::OptRect bbox = item->documentVisualBounds();
185 return (bbox) ? bbox->midpoint() : Geom::Point(0, 0);
186 }
187
approxCurveWithPoints(SPCurve * curve)188 static std::vector<Geom::Point> approxCurveWithPoints(SPCurve *curve)
189 {
190 // The number of segments to use for not straight curves approximation
191 const unsigned NUM_SEGS = 4;
192
193 const Geom::PathVector& curve_pv = curve->get_pathvector();
194
195 // The structure to hold the output
196 std::vector<Geom::Point> poly_points;
197
198 // Iterate over all curves, adding the endpoints for linear curves and
199 // sampling the other curves
200 double seg_size = 1.0 / NUM_SEGS;
201 double at;
202 at = 0;
203 Geom::PathVector::const_iterator pit = curve_pv.begin();
204 while (pit != curve_pv.end())
205 {
206 Geom::Path::const_iterator cit = pit->begin();
207 while (cit != pit->end())
208 {
209 if (cit == pit->begin())
210 {
211 poly_points.push_back(cit->initialPoint());
212 }
213
214 if (dynamic_cast<Geom::CubicBezier const*>(&*cit))
215 {
216 at += seg_size;
217 if (at <= 1.0 )
218 poly_points.push_back(cit->pointAt(at));
219 else
220 {
221 at = 0.0;
222 ++cit;
223 }
224 }
225 else
226 {
227 poly_points.push_back(cit->finalPoint());
228 ++cit;
229 }
230 }
231 ++pit;
232 }
233 return poly_points;
234 }
235
approxItemWithPoints(SPItem const * item,const Geom::Affine & item_transform)236 static std::vector<Geom::Point> approxItemWithPoints(SPItem const *item, const Geom::Affine& item_transform)
237 {
238 // The structure to hold the output
239 std::vector<Geom::Point> poly_points;
240 std::unique_ptr<SPCurve> item_curve;
241
242 if (SP_IS_GROUP(item))
243 {
244 SPGroup* group = SP_GROUP(item);
245 // consider all first-order children
246 std::vector<SPItem*> itemlist = sp_item_group_item_list(group);
247 for (auto child_item : itemlist) {
248 std::vector<Geom::Point> child_points = approxItemWithPoints(child_item, item_transform * child_item->transform);
249 poly_points.insert(poly_points.end(), child_points.begin(), child_points.end());
250 }
251 }
252 else if (SP_IS_SHAPE(item))
253 {
254 auto shape = static_cast<SPShape const *>(item);
255 SP_SHAPE(item)->set_shape();
256 item_curve = SPCurve::copy(shape->curve());
257 // make sure it has an associated curve
258 if (item_curve)
259 {
260 // apply transformations (up to common ancestor)
261 item_curve->transform(item_transform);
262 }
263 } else {
264 auto bbox = item->documentPreferredBounds();
265 if (bbox) {
266 item_curve = SPCurve::new_from_rect(*bbox);
267 }
268 }
269
270 if (item_curve) {
271 std::vector<Geom::Point> curve_points = approxCurveWithPoints(item_curve.get());
272 poly_points.insert(poly_points.end(), curve_points.begin(), curve_points.end());
273 }
274
275 return poly_points;
276 }
avoid_item_poly(SPItem const * item)277 static Avoid::Polygon avoid_item_poly(SPItem const *item)
278 {
279 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
280 g_assert(desktop != nullptr);
281 double spacing = desktop->namedview->connector_spacing;
282
283 Geom::Affine itd_mat = item->i2doc_affine();
284 std::vector<Geom::Point> hull_points;
285 hull_points = approxItemWithPoints(item, itd_mat);
286
287 // create convex hull from all sampled points
288 Geom::ConvexHull hull(hull_points);
289
290 // enlarge path by "desktop->namedview->connector_spacing"
291 // store expanded convex hull in Avoid::Polygn
292 Avoid::Polygon poly;
293 if (hull.empty()) {
294 return poly;
295 }
296
297 Geom::Line hull_edge(hull.back(), hull.front());
298 Geom::Line prev_parallel_hull_edge;
299 prev_parallel_hull_edge.setOrigin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
300 prev_parallel_hull_edge.setVector(hull_edge.versor());
301 int hull_size = hull.size();
302 for (int i = 0; i < hull_size; ++i)
303 {
304 if (i + 1 == hull_size) {
305 hull_edge.setPoints(hull.back(), hull.front());
306 } else {
307 hull_edge.setPoints(hull[i], hull[i + 1]);
308 }
309 Geom::Line parallel_hull_edge;
310 parallel_hull_edge.setOrigin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
311 parallel_hull_edge.setVector(hull_edge.versor());
312
313 // determine the intersection point
314 try {
315 Geom::OptCrossing int_pt = Geom::intersection(parallel_hull_edge, prev_parallel_hull_edge);
316 if (int_pt)
317 {
318 Avoid::Point avoid_pt((parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
319 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);
320 poly.ps.push_back(avoid_pt);
321 }
322 else
323 {
324 // something went wrong...
325 std::cout<<"conn-avoid-ref.cpp: avoid_item_poly: Geom:intersection failed."<<std::endl;
326 }
327 }
328 catch (Geom::InfiniteSolutions const &e) {
329 // the parallel_hull_edge and prev_parallel_hull_edge lie on top of each other, hence infinite crossings
330 g_message("conn-avoid-ref.cpp: trying to get crossings of identical lines");
331 }
332 prev_parallel_hull_edge = parallel_hull_edge;
333 }
334
335 return poly;
336 }
337
338
get_avoided_items(std::vector<SPItem * > & list,SPObject * from,SPDesktop * desktop,bool initialised)339 std::vector<SPItem *> get_avoided_items(std::vector<SPItem *> &list, SPObject *from, SPDesktop *desktop,
340 bool initialised)
341 {
342 for (auto& child: from->children) {
343 if (SP_IS_ITEM(&child) &&
344 !desktop->isLayer(SP_ITEM(&child)) &&
345 !SP_ITEM(&child)->isLocked() &&
346 !desktop->itemIsHidden(SP_ITEM(&child)) &&
347 (!initialised || SP_ITEM(&child)->getAvoidRef().shapeRef)
348 )
349 {
350 list.push_back(SP_ITEM(&child));
351 }
352
353 if (SP_IS_ITEM(&child) && desktop->isLayer(SP_ITEM(&child))) {
354 list = get_avoided_items(list, &child, desktop, initialised);
355 }
356 }
357
358 return list;
359 }
360
361
avoid_item_move(Geom::Affine const *,SPItem * moved_item)362 void avoid_item_move(Geom::Affine const */*mp*/, SPItem *moved_item)
363 {
364 Avoid::ShapeRef *shapeRef = moved_item->getAvoidRef().shapeRef;
365 g_assert(shapeRef);
366
367 Router *router = moved_item->document->getRouter();
368 Avoid::Polygon poly = avoid_item_poly(moved_item);
369 if (!poly.empty()) {
370 router->moveShape(shapeRef, poly);
371 }
372 }
373
374
init_avoided_shape_geometry(SPDesktop * desktop)375 void init_avoided_shape_geometry(SPDesktop *desktop)
376 {
377 // Don't count this as changes to the document,
378 // it is basically just late initialisation.
379 SPDocument *document = desktop->getDocument();
380 DocumentUndo::ScopedInsensitive _no_undo(document);
381
382 bool initialised = false;
383 std::vector<SPItem *> tmp;
384 std::vector<SPItem *> items = get_avoided_items(tmp, desktop->currentRoot(), desktop,
385 initialised);
386
387 for (auto item : items) {
388 item->getAvoidRef().handleSettingChange();
389 }
390 }
391
392
393 /*
394 Local Variables:
395 mode:c++
396 c-file-style:"stroustrup"
397 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
398 indent-tabs-mode:nil
399 fill-column:99
400 End:
401 */
402 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
403