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 // Deleting the shapeRef will remove it completely from
67 // an existing Router instance.
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 // Deleting the shapeRef will remove it completely from
333 // an existing Router instance.
334 delete shapeRef;
335 shapeRef = NULL;
336 }
337 }
340 GSList *SPAvoidRef::getAttachedShapes(const unsigned int type)
341 {
342 GSList *list = NULL;
344 Avoid::IntList shapes;
345 GQuark shapeId = g_quark_from_string(item->getId());
346 item->document->router->attachedShapes(shapes, shapeId, type);
348 Avoid::IntList::iterator finish = shapes.end();
349 for (Avoid::IntList::iterator i = shapes.begin(); i != finish; ++i) {
350 const gchar *connId = g_quark_to_string(*i);
351 SPObject *obj = item->document->getObjectById(connId);
352 if (obj == NULL) {
353 g_warning("getAttachedShapes: Object with id=\"%s\" is not "
354 "found. Skipping.", connId);
355 continue;
356 }
357 SPItem *shapeItem = SP_ITEM(obj);
358 list = g_slist_prepend(list, shapeItem);
359 }
360 return list;
361 }
364 GSList *SPAvoidRef::getAttachedConnectors(const unsigned int type)
365 {
366 GSList *list = NULL;
368 Avoid::IntList conns;
369 GQuark shapeId = g_quark_from_string(item->getId());
370 item->document->router->attachedConns(conns, shapeId, type);
372 Avoid::IntList::iterator finish = conns.end();
373 for (Avoid::IntList::iterator i = conns.begin(); i != finish; ++i) {
374 const gchar *connId = g_quark_to_string(*i);
375 SPObject *obj = item->document->getObjectById(connId);
376 if (obj == NULL) {
377 g_warning("getAttachedConnectors: Object with id=\"%s\" is not "
378 "found. Skipping.", connId);
379 continue;
380 }
381 SPItem *connItem = SP_ITEM(obj);
382 list = g_slist_prepend(list, connItem);
383 }
384 return list;
385 }
387 Geom::Point SPAvoidRef::getConnectionPointPos(const int type, const int id)
388 {
389 g_assert(item);
390 Geom::Point pos;
391 const Geom::Matrix& transform = sp_item_i2doc_affine(item);
392 // TODO investigate why this was asking for the active desktop:
393 SPDesktop *desktop = inkscape_active_desktop();
395 if ( type == ConnPointDefault )
396 {
397 // For now, just default to the centre of the item
398 Geom::OptRect bbox = item->getBounds(sp_item_i2doc_affine(item));
399 pos = (bbox) ? bbox->midpoint() : Geom::Point(0, 0);
400 }
401 else
402 {
403 // Get coordinates from the list of connection points
404 // that are attached to the item
405 pos = connection_points[id].pos * transform;
406 }
408 return pos;
409 }
411 bool SPAvoidRef::isValidConnPointId( const int type, const int id )
412 {
413 if ( type < 0 || type > 1 )
414 return false;
415 else
416 {
417 if ( type == ConnPointDefault )
418 if ( id < 0 || id > 8 )
419 return false;
420 else
421 {
422 }
423 else
424 return connection_points.find( id ) != connection_points.end();
425 }
427 return true;
428 }
430 static std::vector<Geom::Point> approxCurveWithPoints(SPCurve *curve)
431 {
432 // The number of segments to use for not straight curves approximation
433 const unsigned NUM_SEGS = 4;
435 const Geom::PathVector& curve_pv = curve->get_pathvector();
437 // The structure to hold the output
438 std::vector<Geom::Point> poly_points;
440 // Iterate over all curves, adding the endpoints for linear curves and
441 // sampling the other curves
442 double seg_size = 1.0 / NUM_SEGS;
443 double at;
444 at = 0;
445 Geom::PathVector::const_iterator pit = curve_pv.begin();
446 while (pit != curve_pv.end())
447 {
448 Geom::Path::const_iterator cit = pit->begin();
449 while (cit != pit->end())
450 if (dynamic_cast<Geom::CubicBezier const*>(&*cit))
451 {
452 at += seg_size;
453 if (at <= 1.0 )
454 poly_points.push_back(cit->pointAt(at));
455 else
456 {
457 at = 0.0;
458 ++cit;
459 }
460 }
461 else
462 {
463 poly_points.push_back(cit->finalPoint());
464 ++cit;
465 }
466 ++pit;
467 }
468 return poly_points;
469 }
471 static std::vector<Geom::Point> approxItemWithPoints(SPItem const *item, const Geom::Matrix& item_transform)
472 {
473 // The structure to hold the output
474 std::vector<Geom::Point> poly_points;
476 if (SP_IS_GROUP(item))
477 {
478 SPGroup* group = SP_GROUP(item);
479 // consider all first-order children
480 for (GSList const* i = sp_item_group_item_list(group); i != NULL; i = i->next) {
481 SPItem* child_item = SP_ITEM(i->data);
482 std::vector<Geom::Point> child_points = approxItemWithPoints(child_item, item_transform * child_item->transform);
483 poly_points.insert(poly_points.end(), child_points.begin(), child_points.end());
484 }
485 }
486 else if (SP_IS_SHAPE(item))
487 {
488 SPCurve* item_curve = sp_shape_get_curve(SP_SHAPE(item));
489 // make sure it has an associated curve
490 if (item_curve)
491 {
492 // apply transformations (up to common ancestor)
493 item_curve->transform(item_transform);
494 std::vector<Geom::Point> curve_points = approxCurveWithPoints(item_curve);
495 poly_points.insert(poly_points.end(), curve_points.begin(), curve_points.end());
496 item_curve->unref();
497 }
498 }
500 return poly_points;
501 }
502 static Avoid::Polygon avoid_item_poly(SPItem const *item)
503 {
504 SPDesktop *desktop = inkscape_active_desktop();
505 g_assert(desktop != NULL);
506 double spacing = desktop->namedview->connector_spacing;
508 Geom::Matrix itd_mat = sp_item_i2doc_affine(item);
509 std::vector<Geom::Point> hull_points;
510 hull_points = approxItemWithPoints(item, itd_mat);
512 // create convex hull from all sampled points
513 Geom::ConvexHull hull(hull_points);
515 // enlarge path by "desktop->namedview->connector_spacing"
516 // store expanded convex hull in Avoid::Polygn
517 Avoid::Polygon poly;
519 Geom::Line hull_edge(hull[-1], hull[0]);
520 Geom::Line prev_parallel_hull_edge;
521 prev_parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
522 prev_parallel_hull_edge.versor(hull_edge.versor());
523 int hull_size = hull.boundary.size();
524 for (int i = 0; i <= hull_size; ++i)
525 {
526 hull_edge.setBy2Points(hull[i], hull[i+1]);
527 Geom::Line parallel_hull_edge;
528 parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
529 parallel_hull_edge.versor(hull_edge.versor());
531 // determine the intersection point
533 Geom::OptCrossing int_pt = Geom::intersection(parallel_hull_edge, prev_parallel_hull_edge);
534 if (int_pt)
535 {
536 Avoid::Point avoid_pt((parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
537 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);
538 poly.ps.push_back(avoid_pt);
539 }
540 else
541 {
542 // something went wrong...
543 std::cout<<"conn-avoid-ref.cpp: avoid_item_poly: Geom:intersection failed."<<std::endl;
544 }
545 prev_parallel_hull_edge = parallel_hull_edge;
546 }
547 return poly;
548 }
551 GSList *get_avoided_items(GSList *list, SPObject *from, SPDesktop *desktop,
552 bool initialised)
553 {
554 for (SPObject *child = sp_object_first_child(SP_OBJECT(from)) ;
555 child != NULL; child = SP_OBJECT_NEXT(child) ) {
556 if (SP_IS_ITEM(child) &&
557 !desktop->isLayer(SP_ITEM(child)) &&
558 !SP_ITEM(child)->isLocked() &&
559 !desktop->itemIsHidden(SP_ITEM(child)) &&
560 (!initialised || SP_ITEM(child)->avoidRef->shapeRef)
561 )
562 {
563 list = g_slist_prepend (list, SP_ITEM(child));
564 }
566 if (SP_IS_ITEM(child) && desktop->isLayer(SP_ITEM(child))) {
567 list = get_avoided_items(list, child, desktop, initialised);
568 }
569 }
571 return list;
572 }
575 void avoid_item_move(Geom::Matrix const */*mp*/, SPItem *moved_item)
576 {
577 Avoid::ShapeRef *shapeRef = moved_item->avoidRef->shapeRef;
578 g_assert(shapeRef);
580 Router *router = moved_item->document->router;
581 Avoid::Polygon poly = avoid_item_poly(moved_item);
582 if (!poly.empty()) {
583 router->moveShape(shapeRef, poly);
584 }
585 }
588 void init_avoided_shape_geometry(SPDesktop *desktop)
589 {
590 // Don't count this as changes to the document,
591 // it is basically just late initialisation.
592 SPDocument *document = sp_desktop_document(desktop);
593 bool saved = sp_document_get_undo_sensitive(document);
594 sp_document_set_undo_sensitive(document, false);
596 bool initialised = false;
597 GSList *items = get_avoided_items(NULL, desktop->currentRoot(), desktop,
598 initialised);
600 for ( GSList const *iter = items ; iter != NULL ; iter = iter->next ) {
601 SPItem *item = reinterpret_cast<SPItem *>(iter->data);
602 item->avoidRef->handleSettingChange();
603 }
605 if (items) {
606 g_slist_free(items);
607 }
608 sp_document_set_undo_sensitive(document, saved);
609 }
612 /*
613 Local Variables:
614 mode:c++
615 c-file-style:"stroustrup"
616 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
617 indent-tabs-mode:nil
618 fill-column:99
619 End:
620 */
621 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :