1 /*
2 * A class for handling shape interaction with libavoid.
3 *
4 * Authors:
5 * Michael Wybrow <mjwybrow@users.sourceforge.net>
6 *
7 * Copyright (C) 2005 Michael Wybrow
8 *
9 * Released under GNU GPL, read the file 'COPYING' for more information
10 */
13 #include <cstring>
14 #include <string>
15 #include <iostream>
17 #include "sp-item.h"
18 #include "display/curve.h"
19 #include "2geom/line.h"
20 #include "2geom/crossing.h"
21 #include "2geom/convex-cover.h"
22 #include "helper/geom-curves.h"
23 #include "svg/stringstream.h"
24 #include "conn-avoid-ref.h"
25 #include "connection-points.h"
26 #include "sp-conn-end.h"
27 #include "sp-path.h"
28 #include "libavoid/router.h"
29 #include "libavoid/connector.h"
30 #include "libavoid/geomtypes.h"
31 #include "xml/node.h"
32 #include "document.h"
33 #include "desktop.h"
34 #include "desktop-handles.h"
35 #include "sp-namedview.h"
36 #include "sp-item-group.h"
37 #include "inkscape.h"
38 #include <glibmm/i18n.h>
42 using Avoid::Router;
44 static Avoid::Polygon avoid_item_poly(SPItem const *item);
47 SPAvoidRef::SPAvoidRef(SPItem *spitem)
48 : shapeRef(NULL)
49 , item(spitem)
50 , setting(false)
51 , new_setting(false)
52 , _transformed_connection()
53 {
54 }
57 SPAvoidRef::~SPAvoidRef()
58 {
59 _transformed_connection.disconnect();
61 // If the document is being destroyed then the router instance
62 // and the ShapeRefs will have been destroyed with it.
63 const bool routerInstanceExists = (item->document->router != NULL);
65 if (shapeRef && routerInstanceExists) {
66 Router *router = shapeRef->router();
67 router->removeShape(shapeRef);
68 delete shapeRef;
69 }
70 shapeRef = NULL;
71 }
74 void SPAvoidRef::setAvoid(char const *value)
75 {
76 if (SP_OBJECT_IS_CLONED(item)) {
77 // Don't keep avoidance information for cloned objects.
78 return;
79 }
80 new_setting = false;
81 if (value && (strcmp(value, "true") == 0)) {
82 new_setting = true;
83 }
84 }
86 void print_connection_points(std::map<int, ConnectionPoint>& cp)
87 {
88 std::map<int, ConnectionPoint>::iterator i;
89 for (i=cp.begin(); i!=cp.end(); ++i)
90 {
91 const ConnectionPoint& p = i->second;
92 std::cout<<p.id<<" "<<p.type<<" "<<p.pos[Geom::X]<<" "<<p.pos[Geom::Y]<<std::endl;
93 }
94 }
96 void SPAvoidRef::setConnectionPoints(gchar const *value)
97 {
98 std::set<int> updates;
99 std::set<int> deletes;
100 std::set<int> seen;
102 if (value)
103 {
104 /* Rebuild the connection points list.
105 Update the connectors for which
106 the endpoint has changed.
107 */
109 gchar ** strarray = g_strsplit(value, "|", 0);
110 gchar ** iter = strarray;
112 while (*iter != NULL) {
113 ConnectionPoint cp;
114 Inkscape::SVGIStringStream is(*iter);
115 is>>cp;
116 cp.type = ConnPointUserDefined;
118 /* Mark this connection point as seen, so we can delete
119 the other ones.
120 */
121 seen.insert(cp.id);
122 if ( connection_points.find(cp.id) != connection_points.end() )
123 {
124 /* An already existing connection point.
125 Check to see if changed, and, if it is
126 the case, trigger connector update for
127 the connector attached to this connection
128 point. This is done by adding the
129 connection point to a list of connection
130 points to be updated.
131 */
132 if ( connection_points[cp.id] != cp )
133 // The connection point got updated.
134 // Put it in the update list.
135 updates.insert(cp.id);
136 }
137 connection_points[cp.id] = cp;
138 ++iter;
139 }
140 /* Delete the connection points that didn't appear
141 in the new connection point list.
142 */
143 std::map<int, ConnectionPoint>::iterator it;
145 for (it=connection_points.begin(); it!=connection_points.end(); ++it)
146 if ( seen.find(it->first) == seen.end())
147 deletes.insert(it->first);
148 g_strfreev(strarray);
149 }
150 else
151 {
152 /* Delete all the user-defined connection points
153 Actually we do this by adding them to the list
154 of connection points to be deleted.
155 */
156 std::map<int, ConnectionPoint>::iterator it;
158 for (it=connection_points.begin(); it!=connection_points.end(); ++it)
159 deletes.insert(it->first);
160 }
161 /* Act upon updates and deletes.
162 */
163 if (deletes.empty() && updates.empty())
164 // Nothing to do, just return.
165 return;
166 // Get a list of attached connectors.
167 GSList* conns = getAttachedConnectors(Avoid::runningToAndFrom);
168 for (GSList *i = conns; i != NULL; i = i->next)
169 {
170 SPPath* path = SP_PATH(i->data);
171 SPConnEnd** connEnds = path->connEndPair.getConnEnds();
172 for (int ix=0; ix<2; ++ix) {
173 if (connEnds[ix]->type == ConnPointUserDefined) {
174 if (updates.find(connEnds[ix]->id) != updates.end()) {
175 if (path->connEndPair.isAutoRoutingConn()) {
176 path->connEndPair.tellLibavoidNewEndpoints();
177 } else {
178 }
179 }
180 else if (deletes.find(connEnds[ix]->id) != deletes.end()) {
181 sp_conn_end_detach(path, ix);
182 }
183 }
184 }
185 }
186 g_slist_free(conns);
187 // Remove all deleted connection points
188 if (deletes.size())
189 for (std::set<int>::iterator it = deletes.begin(); it != deletes.end(); ++it)
190 connection_points.erase(*it);
191 }
193 void SPAvoidRef::setConnectionPointsAttrUndoable(const gchar* value, const gchar* action)
194 {
195 SPDocument* doc = SP_OBJECT_DOCUMENT(item);
197 sp_object_setAttribute( SP_OBJECT(item), "inkscape:connection-points", value, 0 );
198 item->updateRepr();
199 sp_document_ensure_up_to_date(doc);
200 sp_document_done(doc, SP_VERB_CONTEXT_CONNECTOR, action);
201 }
203 void SPAvoidRef::addConnectionPoint(ConnectionPoint &cp)
204 {
205 Inkscape::SVGOStringStream ostr;
206 bool first = true;
207 int newId = 1;
208 if ( connection_points.size() )
209 {
210 for (IdConnectionPointMap::iterator it = connection_points.begin(); ; )
211 {
212 if ( first )
213 {
214 first = false;
215 ostr<<it->second;
216 }
217 else
218 ostr<<'|'<<it->second;
219 IdConnectionPointMap::iterator prev_it = it;
220 ++it;
221 if ( it == connection_points.end() || prev_it->first + 1 != it->first )
222 {
223 newId = prev_it->first + 1;
224 break;
225 }
226 }
227 }
228 cp.id = newId;
229 if ( first )
230 {
231 first = false;
232 ostr<<cp;
233 }
234 else
235 ostr<<'|'<<cp;
237 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Add a new connection point") );
238 }
240 void SPAvoidRef::updateConnectionPoint(ConnectionPoint &cp)
241 {
242 Inkscape::SVGOStringStream ostr;
243 IdConnectionPointMap::iterator cp_pos = connection_points.find( cp.id );
244 if ( cp_pos != connection_points.end() )
245 {
246 bool first = true;
247 for (IdConnectionPointMap::iterator it = connection_points.begin(); it != connection_points.end(); ++it)
248 {
249 ConnectionPoint* to_write;
250 if ( it != cp_pos )
251 to_write = &it->second;
252 else
253 to_write = &cp;
254 if ( first )
255 {
256 first = false;
257 ostr<<*to_write;
258 }
259 else
260 ostr<<'|'<<*to_write;
261 }
262 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Move a connection point") );
263 }
264 }
266 void SPAvoidRef::deleteConnectionPoint(ConnectionPoint &cp)
267 {
268 Inkscape::SVGOStringStream ostr;
269 IdConnectionPointMap::iterator cp_pos = connection_points.find( cp.id );
270 if ( cp_pos != connection_points.end() ) {
271 bool first = true;
272 for (IdConnectionPointMap::iterator it = connection_points.begin(); it != connection_points.end(); ++it) {
273 if ( it != cp_pos ) {
274 if ( first ) {
275 first = false;
276 ostr<<it->second;
277 } else {
278 ostr<<'|'<<it->second;
279 }
280 }
281 }
282 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Remove a connection point") );
283 }
284 }
286 void SPAvoidRef::handleSettingChange(void)
287 {
288 SPDesktop *desktop = inkscape_active_desktop();
289 if (desktop == NULL) {
290 return;
291 }
292 if (sp_desktop_document(desktop) != item->document) {
293 // We don't want to go any further if the active desktop's document
294 // isn't the same as the document that this item is part of. This
295 // case can happen if a new document is loaded from the file chooser
296 // or via the recent file menu. In this case, we can end up here
297 // as a rersult of a sp_document_ensure_up_to_date performed on a
298 // document not yet attached to the active desktop.
299 return;
300 }
302 if (new_setting == setting) {
303 // Don't need to make any changes
304 return;
305 }
306 setting = new_setting;
308 Router *router = item->document->router;
310 _transformed_connection.disconnect();
311 if (new_setting) {
312 Avoid::Polygon poly = avoid_item_poly(item);
313 if (poly.size() > 0) {
314 _transformed_connection = item->connectTransformed(
315 sigc::ptr_fun(&avoid_item_move));
317 const char *id = SP_OBJECT_REPR(item)->attribute("id");
318 g_assert(id != NULL);
320 // Get a unique ID for the item.
321 GQuark itemID = g_quark_from_string(id);
323 shapeRef = new Avoid::ShapeRef(router, poly, itemID);
325 router->addShape(shapeRef);
326 }
327 }
328 else
329 {
330 g_assert(shapeRef);
332 router->removeShape(shapeRef);
333 delete shapeRef;
334 shapeRef = NULL;
335 }
336 }
339 GSList *SPAvoidRef::getAttachedShapes(const unsigned int type)
340 {
341 GSList *list = NULL;
343 Avoid::IntList shapes;
344 GQuark shapeId = g_quark_from_string(item->getId());
345 item->document->router->attachedShapes(shapes, shapeId, type);
347 Avoid::IntList::iterator finish = shapes.end();
348 for (Avoid::IntList::iterator i = shapes.begin(); i != finish; ++i) {
349 const gchar *connId = g_quark_to_string(*i);
350 SPObject *obj = item->document->getObjectById(connId);
351 if (obj == NULL) {
352 g_warning("getAttachedShapes: Object with id=\"%s\" is not "
353 "found. Skipping.", connId);
354 continue;
355 }
356 SPItem *shapeItem = SP_ITEM(obj);
357 list = g_slist_prepend(list, shapeItem);
358 }
359 return list;
360 }
363 GSList *SPAvoidRef::getAttachedConnectors(const unsigned int type)
364 {
365 GSList *list = NULL;
367 Avoid::IntList conns;
368 GQuark shapeId = g_quark_from_string(item->getId());
369 item->document->router->attachedConns(conns, shapeId, type);
371 Avoid::IntList::iterator finish = conns.end();
372 for (Avoid::IntList::iterator i = conns.begin(); i != finish; ++i) {
373 const gchar *connId = g_quark_to_string(*i);
374 SPObject *obj = item->document->getObjectById(connId);
375 if (obj == NULL) {
376 g_warning("getAttachedConnectors: Object with id=\"%s\" is not "
377 "found. Skipping.", connId);
378 continue;
379 }
380 SPItem *connItem = SP_ITEM(obj);
381 list = g_slist_prepend(list, connItem);
382 }
383 return list;
384 }
386 Geom::Point SPAvoidRef::getConnectionPointPos(const int type, const int id)
387 {
388 g_assert(item);
389 Geom::Point pos;
390 const Geom::Matrix& transform = sp_item_i2doc_affine(item);
391 // TODO investigate why this was asking for the active desktop:
392 SPDesktop *desktop = inkscape_active_desktop();
394 if ( type == ConnPointDefault )
395 {
396 // For now, just default to the centre of the item
397 Geom::OptRect bbox = item->getBounds(sp_item_i2doc_affine(item));
398 pos = (bbox) ? bbox->midpoint() : Geom::Point(0, 0);
399 }
400 else
401 {
402 // Get coordinates from the list of connection points
403 // that are attached to the item
404 pos = connection_points[id].pos * transform;
405 }
407 return pos;
408 }
410 bool SPAvoidRef::isValidConnPointId( const int type, const int id )
411 {
412 if ( type < 0 || type > 1 )
413 return false;
414 else
415 {
416 if ( type == ConnPointDefault )
417 if ( id < 0 || id > 8 )
418 return false;
419 else
420 {
421 }
422 else
423 return connection_points.find( id ) != connection_points.end();
424 }
426 return true;
427 }
429 static std::vector<Geom::Point> approxCurveWithPoints(SPCurve *curve)
430 {
431 // The number of segments to use for not straight curves approximation
432 const unsigned NUM_SEGS = 4;
434 const Geom::PathVector& curve_pv = curve->get_pathvector();
436 // The structure to hold the output
437 std::vector<Geom::Point> poly_points;
439 // Iterate over all curves, adding the endpoints for linear curves and
440 // sampling the other curves
441 double seg_size = 1.0 / NUM_SEGS;
442 double at;
443 at = 0;
444 Geom::PathVector::const_iterator pit = curve_pv.begin();
445 while (pit != curve_pv.end())
446 {
447 Geom::Path::const_iterator cit = pit->begin();
448 while (cit != pit->end())
449 if (dynamic_cast<Geom::CubicBezier const*>(&*cit))
450 {
451 at += seg_size;
452 if (at <= 1.0 )
453 poly_points.push_back(cit->pointAt(at));
454 else
455 {
456 at = 0.0;
457 ++cit;
458 }
459 }
460 else
461 {
462 poly_points.push_back(cit->finalPoint());
463 ++cit;
464 }
465 ++pit;
466 }
467 return poly_points;
468 }
470 static std::vector<Geom::Point> approxItemWithPoints(SPItem const *item, const Geom::Matrix& item_transform)
471 {
472 // The structure to hold the output
473 std::vector<Geom::Point> poly_points;
475 if (SP_IS_GROUP(item))
476 {
477 SPGroup* group = SP_GROUP(item);
478 // consider all first-order children
479 for (GSList const* i = sp_item_group_item_list(group); i != NULL; i = i->next) {
480 SPItem* child_item = SP_ITEM(i->data);
481 std::vector<Geom::Point> child_points = approxItemWithPoints(child_item, item_transform * child_item->transform);
482 poly_points.insert(poly_points.end(), child_points.begin(), child_points.end());
483 }
484 }
485 else if (SP_IS_SHAPE(item))
486 {
487 SPCurve* item_curve = sp_shape_get_curve(SP_SHAPE(item));
488 // make sure it has an associated curve
489 if (item_curve)
490 {
491 // apply transformations (up to common ancestor)
492 item_curve->transform(item_transform);
493 std::vector<Geom::Point> curve_points = approxCurveWithPoints(item_curve);
494 poly_points.insert(poly_points.end(), curve_points.begin(), curve_points.end());
495 item_curve->unref();
496 }
497 }
499 return poly_points;
500 }
501 static Avoid::Polygon avoid_item_poly(SPItem const *item)
502 {
503 SPDesktop *desktop = inkscape_active_desktop();
504 g_assert(desktop != NULL);
505 double spacing = desktop->namedview->connector_spacing;
507 Geom::Matrix itd_mat = sp_item_i2doc_affine(item);
508 std::vector<Geom::Point> hull_points;
509 hull_points = approxItemWithPoints(item, itd_mat);
511 // create convex hull from all sampled points
512 Geom::ConvexHull hull(hull_points);
514 // enlarge path by "desktop->namedview->connector_spacing"
515 // store expanded convex hull in Avoid::Polygn
516 Avoid::Polygon poly;
518 Geom::Line hull_edge(hull[-1], hull[0]);
519 Geom::Line prev_parallel_hull_edge;
520 prev_parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
521 prev_parallel_hull_edge.versor(hull_edge.versor());
522 int hull_size = hull.boundary.size();
523 for (int i = 0; i <= hull_size; ++i)
524 {
525 hull_edge.setBy2Points(hull[i], hull[i+1]);
526 Geom::Line parallel_hull_edge;
527 parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
528 parallel_hull_edge.versor(hull_edge.versor());
530 // determine the intersection point
532 Geom::OptCrossing int_pt = Geom::intersection(parallel_hull_edge, prev_parallel_hull_edge);
533 if (int_pt)
534 {
535 Avoid::Point avoid_pt((parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
536 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);
537 poly.ps.push_back(avoid_pt);
538 }
539 else
540 {
541 // something went wrong...
542 std::cout<<"conn-avoid-ref.cpp: avoid_item_poly: Geom:intersection failed."<<std::endl;
543 }
544 prev_parallel_hull_edge = parallel_hull_edge;
545 }
546 return poly;
547 }
550 GSList *get_avoided_items(GSList *list, SPObject *from, SPDesktop *desktop,
551 bool initialised)
552 {
553 for (SPObject *child = sp_object_first_child(SP_OBJECT(from)) ;
554 child != NULL; child = SP_OBJECT_NEXT(child) ) {
555 if (SP_IS_ITEM(child) &&
556 !desktop->isLayer(SP_ITEM(child)) &&
557 !SP_ITEM(child)->isLocked() &&
558 !desktop->itemIsHidden(SP_ITEM(child)) &&
559 (!initialised || SP_ITEM(child)->avoidRef->shapeRef)
560 )
561 {
562 list = g_slist_prepend (list, SP_ITEM(child));
563 }
565 if (SP_IS_ITEM(child) && desktop->isLayer(SP_ITEM(child))) {
566 list = get_avoided_items(list, child, desktop, initialised);
567 }
568 }
570 return list;
571 }
574 void avoid_item_move(Geom::Matrix const */*mp*/, SPItem *moved_item)
575 {
576 Avoid::ShapeRef *shapeRef = moved_item->avoidRef->shapeRef;
577 g_assert(shapeRef);
579 Router *router = moved_item->document->router;
580 Avoid::Polygon poly = avoid_item_poly(moved_item);
581 if (!poly.empty()) {
582 router->moveShape(shapeRef, poly);
583 }
584 }
587 void init_avoided_shape_geometry(SPDesktop *desktop)
588 {
589 // Don't count this as changes to the document,
590 // it is basically just late initialisation.
591 SPDocument *document = sp_desktop_document(desktop);
592 bool saved = sp_document_get_undo_sensitive(document);
593 sp_document_set_undo_sensitive(document, false);
595 bool initialised = false;
596 GSList *items = get_avoided_items(NULL, desktop->currentRoot(), desktop,
597 initialised);
599 for ( GSList const *iter = items ; iter != NULL ; iter = iter->next ) {
600 SPItem *item = reinterpret_cast<SPItem *>(iter->data);
601 item->avoidRef->handleSettingChange();
602 }
604 if (items) {
605 g_slist_free(items);
606 }
607 sp_document_set_undo_sensitive(document, saved);
608 }
611 /*
612 Local Variables:
613 mode:c++
614 c-file-style:"stroustrup"
615 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
616 indent-tabs-mode:nil
617 fill-column:99
618 End:
619 */
620 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :