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 "libavoid/shape.h"
32 #include "xml/node.h"
33 #include "document.h"
34 #include "desktop.h"
35 #include "desktop-handles.h"
36 #include "sp-namedview.h"
37 #include "sp-item-group.h"
38 #include "inkscape.h"
39 #include <glibmm/i18n.h>
43 using Avoid::Router;
45 static Avoid::Polygon avoid_item_poly(SPItem const *item);
48 SPAvoidRef::SPAvoidRef(SPItem *spitem)
49 : shapeRef(NULL)
50 , item(spitem)
51 , setting(false)
52 , new_setting(false)
53 , _transformed_connection()
54 {
55 }
58 SPAvoidRef::~SPAvoidRef()
59 {
60 _transformed_connection.disconnect();
62 // If the document is being destroyed then the router instance
63 // and the ShapeRefs will have been destroyed with it.
64 const bool routerInstanceExists = (item->document->router != NULL);
66 if (shapeRef && routerInstanceExists) {
67 // Deleting the shapeRef will remove it completely from
68 // an existing Router instance.
69 delete shapeRef;
70 }
71 shapeRef = NULL;
72 }
75 void SPAvoidRef::setAvoid(char const *value)
76 {
77 if (SP_OBJECT_IS_CLONED(item)) {
78 // Don't keep avoidance information for cloned objects.
79 return;
80 }
81 new_setting = false;
82 if (value && (strcmp(value, "true") == 0)) {
83 new_setting = true;
84 }
85 }
87 void print_connection_points(std::map<int, ConnectionPoint>& cp)
88 {
89 std::map<int, ConnectionPoint>::iterator i;
90 for (i=cp.begin(); i!=cp.end(); ++i)
91 {
92 const ConnectionPoint& p = i->second;
93 std::cout<<p.id<<" "<<p.type<<" "<<p.pos[Geom::X]<<" "<<p.pos[Geom::Y]<<std::endl;
94 }
95 }
97 void SPAvoidRef::setConnectionPoints(gchar const *value)
98 {
99 std::set<int> updates;
100 std::set<int> deletes;
101 std::set<int> seen;
103 if (value)
104 {
105 /* Rebuild the connection points list.
106 Update the connectors for which
107 the endpoint has changed.
108 */
110 gchar ** strarray = g_strsplit(value, "|", 0);
111 gchar ** iter = strarray;
113 while (*iter != NULL) {
114 ConnectionPoint cp;
115 Inkscape::SVGIStringStream is(*iter);
116 is>>cp;
117 cp.type = ConnPointUserDefined;
119 /* Mark this connection point as seen, so we can delete
120 the other ones.
121 */
122 seen.insert(cp.id);
123 if ( connection_points.find(cp.id) != connection_points.end() )
124 {
125 /* An already existing connection point.
126 Check to see if changed, and, if it is
127 the case, trigger connector update for
128 the connector attached to this connection
129 point. This is done by adding the
130 connection point to a list of connection
131 points to be updated.
132 */
133 if ( connection_points[cp.id] != cp )
134 // The connection point got updated.
135 // Put it in the update list.
136 updates.insert(cp.id);
137 }
138 connection_points[cp.id] = cp;
139 ++iter;
140 }
141 /* Delete the connection points that didn't appear
142 in the new connection point list.
143 */
144 std::map<int, ConnectionPoint>::iterator it;
146 for (it=connection_points.begin(); it!=connection_points.end(); ++it)
147 if ( seen.find(it->first) == seen.end())
148 deletes.insert(it->first);
149 g_strfreev(strarray);
150 }
151 else
152 {
153 /* Delete all the user-defined connection points
154 Actually we do this by adding them to the list
155 of connection points to be deleted.
156 */
157 std::map<int, ConnectionPoint>::iterator it;
159 for (it=connection_points.begin(); it!=connection_points.end(); ++it)
160 deletes.insert(it->first);
161 }
162 /* Act upon updates and deletes.
163 */
164 if (deletes.empty() && updates.empty())
165 // Nothing to do, just return.
166 return;
167 // Get a list of attached connectors.
168 GSList* conns = getAttachedConnectors(Avoid::runningToAndFrom);
169 for (GSList *i = conns; i != NULL; i = i->next)
170 {
171 SPPath* path = SP_PATH(i->data);
172 SPConnEnd** connEnds = path->connEndPair.getConnEnds();
173 for (int ix=0; ix<2; ++ix) {
174 if (connEnds[ix]->type == ConnPointUserDefined) {
175 if (updates.find(connEnds[ix]->id) != updates.end()) {
176 if (path->connEndPair.isAutoRoutingConn()) {
177 path->connEndPair.tellLibavoidNewEndpoints();
178 } else {
179 }
180 }
181 else if (deletes.find(connEnds[ix]->id) != deletes.end()) {
182 sp_conn_end_detach(path, ix);
183 }
184 }
185 }
186 }
187 g_slist_free(conns);
188 // Remove all deleted connection points
189 if (deletes.size())
190 for (std::set<int>::iterator it = deletes.begin(); it != deletes.end(); ++it)
191 connection_points.erase(*it);
192 }
194 void SPAvoidRef::setConnectionPointsAttrUndoable(const gchar* value, const gchar* action)
195 {
196 SPDocument* doc = SP_OBJECT_DOCUMENT(item);
198 sp_object_setAttribute( SP_OBJECT(item), "inkscape:connection-points", value, 0 );
199 item->updateRepr();
200 sp_document_ensure_up_to_date(doc);
201 sp_document_done(doc, SP_VERB_CONTEXT_CONNECTOR, action);
202 }
204 void SPAvoidRef::addConnectionPoint(ConnectionPoint &cp)
205 {
206 Inkscape::SVGOStringStream ostr;
207 bool first = true;
208 int newId = 1;
209 if ( connection_points.size() )
210 {
211 for (IdConnectionPointMap::iterator it = connection_points.begin(); ; )
212 {
213 if ( first )
214 {
215 first = false;
216 ostr<<it->second;
217 }
218 else
219 ostr<<'|'<<it->second;
220 IdConnectionPointMap::iterator prev_it = it;
221 ++it;
222 if ( it == connection_points.end() || prev_it->first + 1 != it->first )
223 {
224 newId = prev_it->first + 1;
225 break;
226 }
227 }
228 }
229 cp.id = newId;
230 if ( first )
231 {
232 first = false;
233 ostr<<cp;
234 }
235 else
236 ostr<<'|'<<cp;
238 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Add a new connection point") );
239 }
241 void SPAvoidRef::updateConnectionPoint(ConnectionPoint &cp)
242 {
243 Inkscape::SVGOStringStream ostr;
244 IdConnectionPointMap::iterator cp_pos = connection_points.find( cp.id );
245 if ( cp_pos != connection_points.end() )
246 {
247 bool first = true;
248 for (IdConnectionPointMap::iterator it = connection_points.begin(); it != connection_points.end(); ++it)
249 {
250 ConnectionPoint* to_write;
251 if ( it != cp_pos )
252 to_write = &it->second;
253 else
254 to_write = &cp;
255 if ( first )
256 {
257 first = false;
258 ostr<<*to_write;
259 }
260 else
261 ostr<<'|'<<*to_write;
262 }
263 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Move a connection point") );
264 }
265 }
267 void SPAvoidRef::deleteConnectionPoint(ConnectionPoint &cp)
268 {
269 Inkscape::SVGOStringStream ostr;
270 IdConnectionPointMap::iterator cp_pos = connection_points.find( cp.id );
271 if ( cp_pos != connection_points.end() ) {
272 bool first = true;
273 for (IdConnectionPointMap::iterator it = connection_points.begin(); it != connection_points.end(); ++it) {
274 if ( it != cp_pos ) {
275 if ( first ) {
276 first = false;
277 ostr<<it->second;
278 } else {
279 ostr<<'|'<<it->second;
280 }
281 }
282 }
283 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Remove a connection point") );
284 }
285 }
287 void SPAvoidRef::handleSettingChange(void)
288 {
289 SPDesktop *desktop = inkscape_active_desktop();
290 if (desktop == NULL) {
291 return;
292 }
293 if (sp_desktop_document(desktop) != item->document) {
294 // We don't want to go any further if the active desktop's document
295 // isn't the same as the document that this item is part of. This
296 // case can happen if a new document is loaded from the file chooser
297 // or via the recent file menu. In this case, we can end up here
298 // as a rersult of a sp_document_ensure_up_to_date performed on a
299 // document not yet attached to the active desktop.
300 return;
301 }
303 if (new_setting == setting) {
304 // Don't need to make any changes
305 return;
306 }
307 setting = new_setting;
309 Router *router = item->document->router;
311 _transformed_connection.disconnect();
312 if (new_setting) {
313 Avoid::Polygon poly = avoid_item_poly(item);
314 if (poly.size() > 0) {
315 _transformed_connection = item->connectTransformed(
316 sigc::ptr_fun(&avoid_item_move));
318 const char *id = SP_OBJECT_REPR(item)->attribute("id");
319 g_assert(id != NULL);
321 // Get a unique ID for the item.
322 GQuark itemID = g_quark_from_string(id);
324 shapeRef = new Avoid::ShapeRef(router, poly, itemID);
326 router->addShape(shapeRef);
327 }
328 }
329 else
330 {
331 g_assert(shapeRef);
333 // Deleting the shapeRef will remove it completely from
334 // an existing Router instance.
335 delete shapeRef;
336 shapeRef = NULL;
337 }
338 }
341 GSList *SPAvoidRef::getAttachedShapes(const unsigned int type)
342 {
343 GSList *list = NULL;
345 Avoid::IntList shapes;
346 GQuark shapeId = g_quark_from_string(item->getId());
347 item->document->router->attachedShapes(shapes, shapeId, type);
349 Avoid::IntList::iterator finish = shapes.end();
350 for (Avoid::IntList::iterator i = shapes.begin(); i != finish; ++i) {
351 const gchar *connId = g_quark_to_string(*i);
352 SPObject *obj = item->document->getObjectById(connId);
353 if (obj == NULL) {
354 g_warning("getAttachedShapes: Object with id=\"%s\" is not "
355 "found. Skipping.", connId);
356 continue;
357 }
358 SPItem *shapeItem = SP_ITEM(obj);
359 list = g_slist_prepend(list, shapeItem);
360 }
361 return list;
362 }
365 GSList *SPAvoidRef::getAttachedConnectors(const unsigned int type)
366 {
367 GSList *list = NULL;
369 Avoid::IntList conns;
370 GQuark shapeId = g_quark_from_string(item->getId());
371 item->document->router->attachedConns(conns, shapeId, type);
373 Avoid::IntList::iterator finish = conns.end();
374 for (Avoid::IntList::iterator i = conns.begin(); i != finish; ++i) {
375 const gchar *connId = g_quark_to_string(*i);
376 SPObject *obj = item->document->getObjectById(connId);
377 if (obj == NULL) {
378 g_warning("getAttachedConnectors: Object with id=\"%s\" is not "
379 "found. Skipping.", connId);
380 continue;
381 }
382 SPItem *connItem = SP_ITEM(obj);
383 list = g_slist_prepend(list, connItem);
384 }
385 return list;
386 }
388 Geom::Point SPAvoidRef::getConnectionPointPos(const int type, const int id)
389 {
390 g_assert(item);
391 Geom::Point pos;
392 const Geom::Matrix& transform = sp_item_i2doc_affine(item);
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 {
450 if (cit == pit->begin())
451 {
452 poly_points.push_back(cit->initialPoint());
453 }
455 if (dynamic_cast<Geom::CubicBezier const*>(&*cit))
456 {
457 at += seg_size;
458 if (at <= 1.0 )
459 poly_points.push_back(cit->pointAt(at));
460 else
461 {
462 at = 0.0;
463 ++cit;
464 }
465 }
466 else
467 {
468 poly_points.push_back(cit->finalPoint());
469 ++cit;
470 }
471 }
472 ++pit;
473 }
474 return poly_points;
475 }
477 static std::vector<Geom::Point> approxItemWithPoints(SPItem const *item, const Geom::Matrix& item_transform)
478 {
479 // The structure to hold the output
480 std::vector<Geom::Point> poly_points;
482 if (SP_IS_GROUP(item))
483 {
484 SPGroup* group = SP_GROUP(item);
485 // consider all first-order children
486 for (GSList const* i = sp_item_group_item_list(group); i != NULL; i = i->next) {
487 SPItem* child_item = SP_ITEM(i->data);
488 std::vector<Geom::Point> child_points = approxItemWithPoints(child_item, item_transform * child_item->transform);
489 poly_points.insert(poly_points.end(), child_points.begin(), child_points.end());
490 }
491 }
492 else if (SP_IS_SHAPE(item))
493 {
494 SPCurve* item_curve = sp_shape_get_curve(SP_SHAPE(item));
495 // make sure it has an associated curve
496 if (item_curve)
497 {
498 // apply transformations (up to common ancestor)
499 item_curve->transform(item_transform);
500 std::vector<Geom::Point> curve_points = approxCurveWithPoints(item_curve);
501 poly_points.insert(poly_points.end(), curve_points.begin(), curve_points.end());
502 item_curve->unref();
503 }
504 }
506 return poly_points;
507 }
508 static Avoid::Polygon avoid_item_poly(SPItem const *item)
509 {
510 SPDesktop *desktop = inkscape_active_desktop();
511 g_assert(desktop != NULL);
512 double spacing = desktop->namedview->connector_spacing;
514 Geom::Matrix itd_mat = sp_item_i2doc_affine(item);
515 std::vector<Geom::Point> hull_points;
516 hull_points = approxItemWithPoints(item, itd_mat);
518 // create convex hull from all sampled points
519 Geom::ConvexHull hull(hull_points);
521 // enlarge path by "desktop->namedview->connector_spacing"
522 // store expanded convex hull in Avoid::Polygn
523 Avoid::Polygon poly;
525 Geom::Line hull_edge(hull[-1], hull[0]);
526 Geom::Line prev_parallel_hull_edge;
527 prev_parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
528 prev_parallel_hull_edge.versor(hull_edge.versor());
529 int hull_size = hull.boundary.size();
530 for (int i = 0; i < hull_size; ++i)
531 {
532 hull_edge.setBy2Points(hull[i], hull[i+1]);
533 Geom::Line parallel_hull_edge;
534 parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
535 parallel_hull_edge.versor(hull_edge.versor());
537 // determine the intersection point
539 Geom::OptCrossing int_pt = Geom::intersection(parallel_hull_edge, prev_parallel_hull_edge);
540 if (int_pt)
541 {
542 Avoid::Point avoid_pt((parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
543 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);
544 poly.ps.push_back(avoid_pt);
545 }
546 else
547 {
548 // something went wrong...
549 std::cout<<"conn-avoid-ref.cpp: avoid_item_poly: Geom:intersection failed."<<std::endl;
550 }
551 prev_parallel_hull_edge = parallel_hull_edge;
552 }
553 return poly;
554 }
557 GSList *get_avoided_items(GSList *list, SPObject *from, SPDesktop *desktop,
558 bool initialised)
559 {
560 for (SPObject *child = sp_object_first_child(SP_OBJECT(from)) ;
561 child != NULL; child = SP_OBJECT_NEXT(child) ) {
562 if (SP_IS_ITEM(child) &&
563 !desktop->isLayer(SP_ITEM(child)) &&
564 !SP_ITEM(child)->isLocked() &&
565 !desktop->itemIsHidden(SP_ITEM(child)) &&
566 (!initialised || SP_ITEM(child)->avoidRef->shapeRef)
567 )
568 {
569 list = g_slist_prepend (list, SP_ITEM(child));
570 }
572 if (SP_IS_ITEM(child) && desktop->isLayer(SP_ITEM(child))) {
573 list = get_avoided_items(list, child, desktop, initialised);
574 }
575 }
577 return list;
578 }
581 void avoid_item_move(Geom::Matrix const */*mp*/, SPItem *moved_item)
582 {
583 Avoid::ShapeRef *shapeRef = moved_item->avoidRef->shapeRef;
584 g_assert(shapeRef);
586 Router *router = moved_item->document->router;
587 Avoid::Polygon poly = avoid_item_poly(moved_item);
588 if (!poly.empty()) {
589 router->moveShape(shapeRef, poly);
590 }
591 }
594 void init_avoided_shape_geometry(SPDesktop *desktop)
595 {
596 // Don't count this as changes to the document,
597 // it is basically just late initialisation.
598 SPDocument *document = sp_desktop_document(desktop);
599 bool saved = sp_document_get_undo_sensitive(document);
600 sp_document_set_undo_sensitive(document, false);
602 bool initialised = false;
603 GSList *items = get_avoided_items(NULL, desktop->currentRoot(), desktop,
604 initialised);
606 for ( GSList const *iter = items ; iter != NULL ; iter = iter->next ) {
607 SPItem *item = reinterpret_cast<SPItem *>(iter->data);
608 item->avoidRef->handleSettingChange();
609 }
611 if (items) {
612 g_slist_free(items);
613 }
614 sp_document_set_undo_sensitive(document, saved);
615 }
618 /*
619 Local Variables:
620 mode:c++
621 c-file-style:"stroustrup"
622 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
623 indent-tabs-mode:nil
624 fill-column:99
625 End:
626 */
627 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :