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 "svg/stringstream.h"
23 #include "conn-avoid-ref.h"
24 #include "connection-points.h"
25 #include "sp-conn-end.h"
26 #include "sp-path.h"
27 #include "libavoid/router.h"
28 #include "libavoid/connector.h"
29 #include "libavoid/geomtypes.h"
30 #include "xml/node.h"
31 #include "document.h"
32 #include "desktop.h"
33 #include "desktop-handles.h"
34 #include "sp-namedview.h"
35 #include "inkscape.h"
36 #include <glibmm/i18n.h>
40 using Avoid::Router;
42 static Avoid::Polygon avoid_item_poly(SPItem const *item);
45 SPAvoidRef::SPAvoidRef(SPItem *spitem)
46 : shapeRef(NULL)
47 , item(spitem)
48 , setting(false)
49 , new_setting(false)
50 , _transformed_connection()
51 {
52 }
55 SPAvoidRef::~SPAvoidRef()
56 {
57 _transformed_connection.disconnect();
59 // If the document is being destroyed then the router instance
60 // and the ShapeRefs will have been destroyed with it.
61 const bool routerInstanceExists = (item->document->router != NULL);
63 if (shapeRef && routerInstanceExists) {
64 Router *router = shapeRef->router();
65 router->removeShape(shapeRef);
66 delete shapeRef;
67 }
68 shapeRef = NULL;
69 }
72 void SPAvoidRef::setAvoid(char const *value)
73 {
74 if (SP_OBJECT_IS_CLONED(item)) {
75 // Don't keep avoidance information for cloned objects.
76 return;
77 }
78 new_setting = false;
79 if (value && (strcmp(value, "true") == 0)) {
80 new_setting = true;
81 }
82 }
84 void print_connection_points(std::map<int, ConnectionPoint>& cp)
85 {
86 std::map<int, ConnectionPoint>::iterator i;
87 for (i=cp.begin(); i!=cp.end(); ++i)
88 {
89 const ConnectionPoint& p = i->second;
90 std::cout<<p.id<<" "<<p.type<<" "<<p.pos[Geom::X]<<" "<<p.pos[Geom::Y]<<std::endl;
91 }
92 }
94 void SPAvoidRef::setConnectionPoints(gchar const *value)
95 {
96 std::set<int> updates;
97 std::set<int> deletes;
98 std::set<int> seen;
100 if (value)
101 {
102 /* Rebuild the connection points list.
103 Update the connectors for which
104 the endpoint has changed.
105 */
107 gchar ** strarray = g_strsplit(value, "|", 0);
108 gchar ** iter = strarray;
110 while (*iter != NULL) {
111 ConnectionPoint cp;
112 Inkscape::SVGIStringStream is(*iter);
113 is>>cp;
114 cp.type = ConnPointUserDefined;
116 /* Mark this connection point as seen, so we can delete
117 the other ones.
118 */
119 seen.insert(cp.id);
120 if ( connection_points.find(cp.id) != connection_points.end() )
121 {
122 /* An already existing connection point.
123 Check to see if changed, and, if it is
124 the case, trigger connector update for
125 the connector attached to this connection
126 point. This is done by adding the
127 connection point to a list of connection
128 points to be updated.
129 */
130 if ( connection_points[cp.id] != cp )
131 // The connection point got updated.
132 // Put it in the update list.
133 updates.insert(cp.id);
134 }
135 connection_points[cp.id] = cp;
136 ++iter;
137 }
138 /* Delete the connection points that didn't appear
139 in the new connection point list.
140 */
141 std::map<int, ConnectionPoint>::iterator it;
143 for (it=connection_points.begin(); it!=connection_points.end(); ++it)
144 if ( seen.find(it->first) == seen.end())
145 deletes.insert(it->first);
146 g_strfreev(strarray);
147 }
148 else
149 {
150 /* Delete all the user-defined connection points
151 Actually we do this by adding them to the list
152 of connection points to be deleted.
153 */
154 std::map<int, ConnectionPoint>::iterator it;
156 for (it=connection_points.begin(); it!=connection_points.end(); ++it)
157 deletes.insert(it->first);
158 }
159 /* Act upon updates and deletes.
160 */
161 if (deletes.empty() && updates.empty())
162 // Nothing to do, just return.
163 return;
164 // Get a list of attached connectors.
165 GSList* conns = getAttachedConnectors(Avoid::runningToAndFrom);
166 for (GSList *i = conns; i != NULL; i = i->next)
167 {
168 SPPath* path = SP_PATH(i->data);
169 SPConnEnd** connEnds = path->connEndPair.getConnEnds();
170 for (int ix=0; ix<2; ++ix)
171 if (connEnds[ix]->type == ConnPointUserDefined)
172 if (updates.find(connEnds[ix]->id) != updates.end())
173 if (path->connEndPair.isAutoRoutingConn())
174 path->connEndPair.tellLibavoidNewEndpoints();
175 else
176 {
177 }
178 else
179 if (deletes.find(connEnds[ix]->id) != deletes.end())
180 sp_conn_end_detach(path, ix);
181 }
182 g_slist_free(conns);
183 // Remove all deleted connection points
184 if (deletes.size())
185 for (std::set<int>::iterator it = deletes.begin(); it != deletes.end(); ++it)
186 connection_points.erase(*it);
187 }
189 void SPAvoidRef::setConnectionPointsAttrUndoable(const gchar* value, const gchar* action)
190 {
191 SPDocument* doc = SP_OBJECT_DOCUMENT(item);
193 sp_object_setAttribute( SP_OBJECT(item), "inkscape:connection-points", value, 0 );
194 item->updateRepr();
195 sp_document_ensure_up_to_date(doc);
196 sp_document_done(doc, SP_VERB_CONTEXT_CONNECTOR, action);
197 }
199 void SPAvoidRef::addConnectionPoint(ConnectionPoint &cp)
200 {
201 Inkscape::SVGOStringStream ostr;
202 bool first = true;
203 int newId = 1;
204 if ( connection_points.size() )
205 {
206 for (IdConnectionPointMap::iterator it = connection_points.begin(); ; )
207 {
208 if ( first )
209 {
210 first = false;
211 ostr<<it->second;
212 }
213 else
214 ostr<<'|'<<it->second;
215 IdConnectionPointMap::iterator prev_it = it;
216 ++it;
217 if ( it == connection_points.end() || prev_it->first + 1 != it->first )
218 {
219 newId = prev_it->first + 1;
220 break;
221 }
222 }
223 }
224 cp.id = newId;
225 if ( first )
226 {
227 first = false;
228 ostr<<cp;
229 }
230 else
231 ostr<<'|'<<cp;
233 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Added a new connection point") );
234 }
236 void SPAvoidRef::updateConnectionPoint(ConnectionPoint &cp)
237 {
238 Inkscape::SVGOStringStream ostr;
239 IdConnectionPointMap::iterator cp_pos = connection_points.find( cp.id );
240 if ( cp_pos != connection_points.end() )
241 {
242 bool first = true;
243 for (IdConnectionPointMap::iterator it = connection_points.begin(); it != connection_points.end(); ++it)
244 {
245 ConnectionPoint* to_write;
246 if ( it != cp_pos )
247 to_write = &it->second;
248 else
249 to_write = &cp;
250 if ( first )
251 {
252 first = false;
253 ostr<<*to_write;
254 }
255 else
256 ostr<<'|'<<*to_write;
257 }
258 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Moved a connection point") );
259 }
260 }
262 void SPAvoidRef::deleteConnectionPoint(ConnectionPoint &cp)
263 {
264 Inkscape::SVGOStringStream ostr;
265 IdConnectionPointMap::iterator cp_pos = connection_points.find( cp.id );
266 if ( cp_pos != connection_points.end() )
267 {
268 bool first = true;
269 for (IdConnectionPointMap::iterator it = connection_points.begin(); it != connection_points.end(); ++it)
270 {
271 if ( it != cp_pos )
272 if ( first )
273 {
274 first = false;
275 ostr<<it->second;
276 }
277 else
278 ostr<<'|'<<it->second;
279 }
280 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Removed a connection point") );
281 }
282 }
284 void SPAvoidRef::handleSettingChange(void)
285 {
286 SPDesktop *desktop = inkscape_active_desktop();
287 if (desktop == NULL) {
288 return;
289 }
290 if (sp_desktop_document(desktop) != item->document) {
291 // We don't want to go any further if the active desktop's document
292 // isn't the same as the document that this item is part of. This
293 // case can happen if a new document is loaded from the file chooser
294 // or via the recent file menu. In this case, we can end up here
295 // as a rersult of a sp_document_ensure_up_to_date performed on a
296 // document not yet attached to the active desktop.
297 return;
298 }
300 if (new_setting == setting) {
301 // Don't need to make any changes
302 return;
303 }
304 setting = new_setting;
306 Router *router = item->document->router;
308 _transformed_connection.disconnect();
309 if (new_setting) {
310 Avoid::Polygon poly = avoid_item_poly(item);
311 if (poly.size() > 0) {
312 _transformed_connection = item->connectTransformed(
313 sigc::ptr_fun(&avoid_item_move));
315 const char *id = SP_OBJECT_REPR(item)->attribute("id");
316 g_assert(id != NULL);
318 // Get a unique ID for the item.
319 GQuark itemID = g_quark_from_string(id);
321 shapeRef = new Avoid::ShapeRef(router, poly, itemID);
323 router->addShape(shapeRef);
324 }
325 }
326 else
327 {
328 g_assert(shapeRef);
330 router->removeShape(shapeRef);
331 delete shapeRef;
332 shapeRef = NULL;
333 }
334 }
337 GSList *SPAvoidRef::getAttachedShapes(const unsigned int type)
338 {
339 GSList *list = NULL;
341 Avoid::IntList shapes;
342 GQuark shapeId = g_quark_from_string(item->id);
343 item->document->router->attachedShapes(shapes, shapeId, type);
345 Avoid::IntList::iterator finish = shapes.end();
346 for (Avoid::IntList::iterator i = shapes.begin(); i != finish; ++i) {
347 const gchar *connId = g_quark_to_string(*i);
348 SPObject *obj = item->document->getObjectById(connId);
349 if (obj == NULL) {
350 g_warning("getAttachedShapes: Object with id=\"%s\" is not "
351 "found. Skipping.", connId);
352 continue;
353 }
354 SPItem *shapeItem = SP_ITEM(obj);
355 list = g_slist_prepend(list, shapeItem);
356 }
357 return list;
358 }
361 GSList *SPAvoidRef::getAttachedConnectors(const unsigned int type)
362 {
363 GSList *list = NULL;
365 Avoid::IntList conns;
366 GQuark shapeId = g_quark_from_string(item->id);
367 item->document->router->attachedConns(conns, shapeId, type);
369 Avoid::IntList::iterator finish = conns.end();
370 for (Avoid::IntList::iterator i = conns.begin(); i != finish; ++i) {
371 const gchar *connId = g_quark_to_string(*i);
372 SPObject *obj = item->document->getObjectById(connId);
373 if (obj == NULL) {
374 g_warning("getAttachedConnectors: Object with id=\"%s\" is not "
375 "found. Skipping.", connId);
376 continue;
377 }
378 SPItem *connItem = SP_ITEM(obj);
379 list = g_slist_prepend(list, connItem);
380 }
381 return list;
382 }
384 Geom::Point SPAvoidRef::getConnectionPointPos(const int type, const int id)
385 {
386 g_assert(item);
387 Geom::Point pos;
388 const Geom::Matrix& transform = sp_item_i2doc_affine(item);
389 SPDesktop *desktop = inkscape_active_desktop();
391 if ( type == ConnPointDefault )
392 {
393 // For now, just default to the centre of the item
394 Geom::OptRect bbox = item->getBounds(sp_item_i2doc_affine(item));
395 pos = (bbox) ? bbox->midpoint() : Geom::Point(0, 0);
396 }
397 else
398 {
399 // Get coordinates from the list of connection points
400 // that are attached to the item
401 pos = connection_points[id].pos * transform;
402 }
404 return pos;
405 }
407 bool SPAvoidRef::isValidConnPointId( const int type, const int id )
408 {
409 if ( type < 0 || type > 1 )
410 return false;
411 else
412 {
413 if ( type == ConnPointDefault )
414 if ( id < 0 || id > 8 )
415 return false;
416 else
417 {
418 }
419 else
420 return connection_points.find( id ) != connection_points.end();
421 }
423 return true;
424 }
426 static Avoid::Polygon avoid_item_poly(SPItem const *item)
427 {
428 SPDesktop *desktop = inkscape_active_desktop();
429 g_assert(desktop != NULL);
431 // TODO: The right way to do this is to return the convex hull of
432 // the object, or an approximation in the case of a rounded
433 // object. Specific SPItems will need to have a new
434 // function that returns points for the convex hull.
435 // For some objects it is enough to feed the snappoints to
436 // some convex hull code, though not NR::ConvexHull as this
437 // only keeps the bounding box of the convex hull currently.
439 double spacing = desktop->namedview->connector_spacing;
441 // [sommer] If item is a shape, use an approximation of its convex hull
442 {
443 // MJW: Disable this for the moment. It still has some issues.
444 const bool convex_hull_approximation_enabled = false;
446 if ( convex_hull_approximation_enabled && SP_IS_SHAPE (item) ) {
447 // The number of points to use for approximation
448 const unsigned NUM_POINTS = 64;
450 // printf("[sommer] is a shape\n");
451 SPCurve* curve = sp_shape_get_curve (SP_SHAPE (item));
452 if (curve) {
453 // printf("[sommer] is a curve\n");
455 // apply all transformations
456 Geom::Matrix itd_mat = sp_item_i2doc_affine(item);
457 curve->transform(itd_mat);
459 // iterate over all paths
460 const Geom::PathVector& curve_pv = curve->get_pathvector();
461 std::vector<Geom::Point> hull_points;
462 for (Geom::PathVector::const_iterator i = curve_pv.begin(); i != curve_pv.end(); i++) {
463 const Geom::Path& curve_pv_path = *i;
464 // printf("[sommer] tracing sub-path\n");
466 // FIXME: enlarge path by "desktop->namedview->connector_spacing" (using sp_selected_path_do_offset)?
468 // use appropriate fraction of points for this path (first one gets any remainder)
469 unsigned num_points = NUM_POINTS / curve_pv.size();
470 if (i == curve_pv.begin()) num_points += NUM_POINTS - (num_points * curve_pv.size());
471 printf("[sommer] using %d points for this path\n", num_points);
473 // sample points along the path for approximation of convex hull
474 for (unsigned n = 0; n < num_points; n++) {
475 double at = curve_pv_path.size() / static_cast<double>(num_points) * n;
476 Geom::Point pt = curve_pv_path.pointAt(at);
477 hull_points.push_back(pt);
478 }
479 }
481 curve->unref();
483 // create convex hull from all sampled points
484 Geom::ConvexHull hull(hull_points);
486 // store expanded convex hull in Avoid::Polygn
487 unsigned n = 0;
488 Avoid::Polygon poly;
489 const Geom::Point& old_pt = *hull.boundary.begin();
491 Geom::Line hull_edge(*hull.boundary.begin(), *(hull.boundary.begin()+1));
492 Geom::Line parallel_hull_edge;
493 parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
494 parallel_hull_edge.versor(hull_edge.versor());
495 Geom::Line bisector = Geom::make_angle_bisector_line( *(hull.boundary.end()), *hull.boundary.begin(),
496 *(hull.boundary.begin()+1));
497 Geom::OptCrossing int_pt = Geom::intersection(parallel_hull_edge, bisector);
499 if (int_pt)
500 {
501 Avoid::Point avoid_pt((parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
502 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);
503 // printf("[sommer] %f, %f\n", old_pt[Geom::X], old_pt[Geom::Y]);
504 /* printf("[sommer] %f, %f\n", (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
505 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);*/
506 poly.ps.push_back(avoid_pt);
507 }
508 for (std::vector<Geom::Point>::const_iterator i = hull.boundary.begin() + 1; i != hull.boundary.end(); i++, n++) {
509 const Geom::Point& old_pt = *i;
510 Geom::Line hull_edge(*i, *(i+1));
511 Geom::Line parallel_hull_edge;
512 parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
513 parallel_hull_edge.versor(hull_edge.versor());
514 Geom::Line bisector = Geom::make_angle_bisector_line( *(i-1), *i, *(i+1));
515 Geom::OptCrossing intersect_pt = Geom::intersection(parallel_hull_edge, bisector);
517 if (int_pt)
518 {
519 Avoid::Point avoid_pt((parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
520 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);
521 /* printf("[sommer] %f, %f\n", old_pt[Geom::X], old_pt[Geom::Y]);
522 printf("[sommer] %f, %f\n", (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
523 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);*/
524 poly.ps.push_back(avoid_pt);
525 }
526 }
529 return poly;
530 }// else printf("[sommer] is no curve\n");
531 }// else printf("[sommer] is no shape\n");
532 }
534 Geom::OptRect rHull = item->getBounds(sp_item_i2doc_affine(item));
535 if (!rHull) {
536 return Avoid::Polygon();
537 }
539 // Add a little buffer around the edge of each object.
540 Geom::Rect rExpandedHull = *rHull;
541 rExpandedHull.expandBy(spacing);
542 Avoid::Polygon poly(4);
544 for (size_t n = 0; n < 4; ++n) {
545 Geom::Point hullPoint = rExpandedHull.corner(n);
546 poly.ps[n].x = hullPoint[Geom::X];
547 poly.ps[n].y = hullPoint[Geom::Y];
548 }
550 return poly;
551 }
554 GSList *get_avoided_items(GSList *list, SPObject *from, SPDesktop *desktop,
555 bool initialised)
556 {
557 for (SPObject *child = sp_object_first_child(SP_OBJECT(from)) ;
558 child != NULL; child = SP_OBJECT_NEXT(child) ) {
559 if (SP_IS_ITEM(child) &&
560 !desktop->isLayer(SP_ITEM(child)) &&
561 !SP_ITEM(child)->isLocked() &&
562 !desktop->itemIsHidden(SP_ITEM(child)) &&
563 (!initialised || SP_ITEM(child)->avoidRef->shapeRef)
564 )
565 {
566 list = g_slist_prepend (list, SP_ITEM(child));
567 }
569 if (SP_IS_ITEM(child) && desktop->isLayer(SP_ITEM(child))) {
570 list = get_avoided_items(list, child, desktop, initialised);
571 }
572 }
574 return list;
575 }
578 void avoid_item_move(Geom::Matrix const */*mp*/, SPItem *moved_item)
579 {
580 Avoid::ShapeRef *shapeRef = moved_item->avoidRef->shapeRef;
581 g_assert(shapeRef);
583 Router *router = moved_item->document->router;
584 Avoid::Polygon poly = avoid_item_poly(moved_item);
585 if (!poly.empty()) {
586 router->moveShape(shapeRef, poly);
587 }
588 }
591 void init_avoided_shape_geometry(SPDesktop *desktop)
592 {
593 // Don't count this as changes to the document,
594 // it is basically just late initialisation.
595 SPDocument *document = sp_desktop_document(desktop);
596 bool saved = sp_document_get_undo_sensitive(document);
597 sp_document_set_undo_sensitive(document, false);
599 bool initialised = false;
600 GSList *items = get_avoided_items(NULL, desktop->currentRoot(), desktop,
601 initialised);
603 for ( GSList const *iter = items ; iter != NULL ; iter = iter->next ) {
604 SPItem *item = reinterpret_cast<SPItem *>(iter->data);
605 item->avoidRef->handleSettingChange();
606 }
608 if (items) {
609 g_slist_free(items);
610 }
611 sp_document_set_undo_sensitive(document, saved);
612 }
615 /*
616 Local Variables:
617 mode:c++
618 c-file-style:"stroustrup"
619 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
620 indent-tabs-mode:nil
621 fill-column:99
622 End:
623 */
624 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :