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 if (deletes.find(connEnds[ix]->id) != deletes.end()) {
179 sp_conn_end_detach(path, ix);
180 }
181 }
182 }
183 }
184 g_slist_free(conns);
185 // Remove all deleted connection points
186 if (deletes.size())
187 for (std::set<int>::iterator it = deletes.begin(); it != deletes.end(); ++it)
188 connection_points.erase(*it);
189 }
191 void SPAvoidRef::setConnectionPointsAttrUndoable(const gchar* value, const gchar* action)
192 {
193 SPDocument* doc = SP_OBJECT_DOCUMENT(item);
195 sp_object_setAttribute( SP_OBJECT(item), "inkscape:connection-points", value, 0 );
196 item->updateRepr();
197 sp_document_ensure_up_to_date(doc);
198 sp_document_done(doc, SP_VERB_CONTEXT_CONNECTOR, action);
199 }
201 void SPAvoidRef::addConnectionPoint(ConnectionPoint &cp)
202 {
203 Inkscape::SVGOStringStream ostr;
204 bool first = true;
205 int newId = 1;
206 if ( connection_points.size() )
207 {
208 for (IdConnectionPointMap::iterator it = connection_points.begin(); ; )
209 {
210 if ( first )
211 {
212 first = false;
213 ostr<<it->second;
214 }
215 else
216 ostr<<'|'<<it->second;
217 IdConnectionPointMap::iterator prev_it = it;
218 ++it;
219 if ( it == connection_points.end() || prev_it->first + 1 != it->first )
220 {
221 newId = prev_it->first + 1;
222 break;
223 }
224 }
225 }
226 cp.id = newId;
227 if ( first )
228 {
229 first = false;
230 ostr<<cp;
231 }
232 else
233 ostr<<'|'<<cp;
235 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Added a new connection point") );
236 }
238 void SPAvoidRef::updateConnectionPoint(ConnectionPoint &cp)
239 {
240 Inkscape::SVGOStringStream ostr;
241 IdConnectionPointMap::iterator cp_pos = connection_points.find( cp.id );
242 if ( cp_pos != connection_points.end() )
243 {
244 bool first = true;
245 for (IdConnectionPointMap::iterator it = connection_points.begin(); it != connection_points.end(); ++it)
246 {
247 ConnectionPoint* to_write;
248 if ( it != cp_pos )
249 to_write = &it->second;
250 else
251 to_write = &cp;
252 if ( first )
253 {
254 first = false;
255 ostr<<*to_write;
256 }
257 else
258 ostr<<'|'<<*to_write;
259 }
260 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Moved a connection point") );
261 }
262 }
264 void SPAvoidRef::deleteConnectionPoint(ConnectionPoint &cp)
265 {
266 Inkscape::SVGOStringStream ostr;
267 IdConnectionPointMap::iterator cp_pos = connection_points.find( cp.id );
268 if ( cp_pos != connection_points.end() ) {
269 bool first = true;
270 for (IdConnectionPointMap::iterator it = connection_points.begin(); it != connection_points.end(); ++it) {
271 if ( it != cp_pos ) {
272 if ( first ) {
273 first = false;
274 ostr<<it->second;
275 } else {
276 ostr<<'|'<<it->second;
277 }
278 }
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 // TODO investigate why this was asking for the active desktop:
390 SPDesktop *desktop = inkscape_active_desktop();
392 if ( type == ConnPointDefault )
393 {
394 // For now, just default to the centre of the item
395 Geom::OptRect bbox = item->getBounds(sp_item_i2doc_affine(item));
396 pos = (bbox) ? bbox->midpoint() : Geom::Point(0, 0);
397 }
398 else
399 {
400 // Get coordinates from the list of connection points
401 // that are attached to the item
402 pos = connection_points[id].pos * transform;
403 }
405 return pos;
406 }
408 bool SPAvoidRef::isValidConnPointId( const int type, const int id )
409 {
410 if ( type < 0 || type > 1 )
411 return false;
412 else
413 {
414 if ( type == ConnPointDefault )
415 if ( id < 0 || id > 8 )
416 return false;
417 else
418 {
419 }
420 else
421 return connection_points.find( id ) != connection_points.end();
422 }
424 return true;
425 }
427 static Avoid::Polygon avoid_item_poly(SPItem const *item)
428 {
429 SPDesktop *desktop = inkscape_active_desktop();
430 g_assert(desktop != NULL);
432 // TODO: The right way to do this is to return the convex hull of
433 // the object, or an approximation in the case of a rounded
434 // object. Specific SPItems will need to have a new
435 // function that returns points for the convex hull.
436 // For some objects it is enough to feed the snappoints to
437 // some convex hull code, though not NR::ConvexHull as this
438 // only keeps the bounding box of the convex hull currently.
440 double spacing = desktop->namedview->connector_spacing;
442 // [sommer] If item is a shape, use an approximation of its convex hull
443 {
444 // MJW: Disable this for the moment. It still has some issues.
445 const bool convex_hull_approximation_enabled = false;
447 if ( convex_hull_approximation_enabled && SP_IS_SHAPE (item) ) {
448 // The number of points to use for approximation
449 const unsigned NUM_POINTS = 64;
451 // printf("[sommer] is a shape\n");
452 SPCurve* curve = sp_shape_get_curve (SP_SHAPE (item));
453 if (curve) {
454 // printf("[sommer] is a curve\n");
456 // apply all transformations
457 Geom::Matrix itd_mat = sp_item_i2doc_affine(item);
458 curve->transform(itd_mat);
460 // iterate over all paths
461 const Geom::PathVector& curve_pv = curve->get_pathvector();
462 std::vector<Geom::Point> hull_points;
463 for (Geom::PathVector::const_iterator i = curve_pv.begin(); i != curve_pv.end(); i++) {
464 const Geom::Path& curve_pv_path = *i;
465 // printf("[sommer] tracing sub-path\n");
467 // FIXME: enlarge path by "desktop->namedview->connector_spacing" (using sp_selected_path_do_offset)?
469 // use appropriate fraction of points for this path (first one gets any remainder)
470 unsigned num_points = NUM_POINTS / curve_pv.size();
471 if (i == curve_pv.begin()) num_points += NUM_POINTS - (num_points * curve_pv.size());
472 printf("[sommer] using %d points for this path\n", num_points);
474 // sample points along the path for approximation of convex hull
475 for (unsigned n = 0; n < num_points; n++) {
476 double at = curve_pv_path.size() / static_cast<double>(num_points) * n;
477 Geom::Point pt = curve_pv_path.pointAt(at);
478 hull_points.push_back(pt);
479 }
480 }
482 curve->unref();
484 // create convex hull from all sampled points
485 Geom::ConvexHull hull(hull_points);
487 // store expanded convex hull in Avoid::Polygn
488 unsigned n = 0;
489 Avoid::Polygon poly;
490 /*
491 const Geom::Point& old_pt = *hull.boundary.begin();
492 */
494 Geom::Line hull_edge(*hull.boundary.begin(), *(hull.boundary.begin()+1));
495 Geom::Line parallel_hull_edge;
496 parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
497 parallel_hull_edge.versor(hull_edge.versor());
498 Geom::Line bisector = Geom::make_angle_bisector_line( *(hull.boundary.end()), *hull.boundary.begin(),
499 *(hull.boundary.begin()+1));
500 Geom::OptCrossing int_pt = Geom::intersection(parallel_hull_edge, bisector);
502 if (int_pt)
503 {
504 Avoid::Point avoid_pt((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 // printf("[sommer] %f, %f\n", old_pt[Geom::X], old_pt[Geom::Y]);
507 /* printf("[sommer] %f, %f\n", (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
508 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);*/
509 poly.ps.push_back(avoid_pt);
510 }
511 for (std::vector<Geom::Point>::const_iterator i = hull.boundary.begin() + 1; i != hull.boundary.end(); i++, n++) {
512 /*
513 const Geom::Point& old_pt = *i;
514 */
515 Geom::Line hull_edge(*i, *(i+1));
516 Geom::Line parallel_hull_edge;
517 parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
518 parallel_hull_edge.versor(hull_edge.versor());
519 Geom::Line bisector = Geom::make_angle_bisector_line( *(i-1), *i, *(i+1));
520 Geom::OptCrossing intersect_pt = Geom::intersection(parallel_hull_edge, bisector);
522 if (int_pt)
523 {
524 Avoid::Point avoid_pt((parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
525 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);
526 /* printf("[sommer] %f, %f\n", old_pt[Geom::X], old_pt[Geom::Y]);
527 printf("[sommer] %f, %f\n", (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
528 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);*/
529 poly.ps.push_back(avoid_pt);
530 }
531 }
534 return poly;
535 }// else printf("[sommer] is no curve\n");
536 }// else printf("[sommer] is no shape\n");
537 }
539 Geom::OptRect rHull = item->getBounds(sp_item_i2doc_affine(item));
540 if (!rHull) {
541 return Avoid::Polygon();
542 }
544 // Add a little buffer around the edge of each object.
545 Geom::Rect rExpandedHull = *rHull;
546 rExpandedHull.expandBy(spacing);
547 Avoid::Polygon poly(4);
549 for (size_t n = 0; n < 4; ++n) {
550 Geom::Point hullPoint = rExpandedHull.corner(n);
551 poly.ps[n].x = hullPoint[Geom::X];
552 poly.ps[n].y = hullPoint[Geom::Y];
553 }
555 return poly;
556 }
559 GSList *get_avoided_items(GSList *list, SPObject *from, SPDesktop *desktop,
560 bool initialised)
561 {
562 for (SPObject *child = sp_object_first_child(SP_OBJECT(from)) ;
563 child != NULL; child = SP_OBJECT_NEXT(child) ) {
564 if (SP_IS_ITEM(child) &&
565 !desktop->isLayer(SP_ITEM(child)) &&
566 !SP_ITEM(child)->isLocked() &&
567 !desktop->itemIsHidden(SP_ITEM(child)) &&
568 (!initialised || SP_ITEM(child)->avoidRef->shapeRef)
569 )
570 {
571 list = g_slist_prepend (list, SP_ITEM(child));
572 }
574 if (SP_IS_ITEM(child) && desktop->isLayer(SP_ITEM(child))) {
575 list = get_avoided_items(list, child, desktop, initialised);
576 }
577 }
579 return list;
580 }
583 void avoid_item_move(Geom::Matrix const */*mp*/, SPItem *moved_item)
584 {
585 Avoid::ShapeRef *shapeRef = moved_item->avoidRef->shapeRef;
586 g_assert(shapeRef);
588 Router *router = moved_item->document->router;
589 Avoid::Polygon poly = avoid_item_poly(moved_item);
590 if (!poly.empty()) {
591 router->moveShape(shapeRef, poly);
592 }
593 }
596 void init_avoided_shape_geometry(SPDesktop *desktop)
597 {
598 // Don't count this as changes to the document,
599 // it is basically just late initialisation.
600 SPDocument *document = sp_desktop_document(desktop);
601 bool saved = sp_document_get_undo_sensitive(document);
602 sp_document_set_undo_sensitive(document, false);
604 bool initialised = false;
605 GSList *items = get_avoided_items(NULL, desktop->currentRoot(), desktop,
606 initialised);
608 for ( GSList const *iter = items ; iter != NULL ; iter = iter->next ) {
609 SPItem *item = reinterpret_cast<SPItem *>(iter->data);
610 item->avoidRef->handleSettingChange();
611 }
613 if (items) {
614 g_slist_free(items);
615 }
616 sp_document_set_undo_sensitive(document, saved);
617 }
620 /*
621 Local Variables:
622 mode:c++
623 c-file-style:"stroustrup"
624 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
625 indent-tabs-mode:nil
626 fill-column:99
627 End:
628 */
629 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :