1 #define __PERSP3D_C__
3 /*
4 * Class modelling a 3D perspective as an SPObject
5 *
6 * Authors:
7 * Maximilian Albert <Anhalter42@gmx.de>
8 *
9 * Copyright (C) 2007 authors
10 *
11 * Released under GNU GPL, read the file 'COPYING' for more information
12 */
14 #include "persp3d.h"
15 #include "perspective-line.h"
16 #include "attributes.h"
17 #include "document-private.h"
18 #include "vanishing-point.h"
19 #include "box3d-context.h"
20 #include "box3d.h"
21 #include "xml/document.h"
22 #include "xml/node-event-vector.h"
23 #include "desktop-handles.h"
24 #include <glibmm/i18n.h>
26 static void persp3d_class_init(Persp3DClass *klass);
27 static void persp3d_init(Persp3D *persp);
29 static void persp3d_build(SPObject *object, SPDocument *document, Inkscape::XML::Node *repr);
30 static void persp3d_release(SPObject *object);
31 static void persp3d_set(SPObject *object, unsigned key, gchar const *value);
32 static void persp3d_update(SPObject *object, SPCtx *ctx, guint flags);
33 static Inkscape::XML::Node *persp3d_write(SPObject *object, Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags);
35 static void persp3d_on_repr_attr_changed (Inkscape::XML::Node * repr, const gchar *key, const gchar *oldval, const gchar *newval, bool is_interactive, void * data);
37 static void persp3d_update_with_point (Persp3DImpl *persp_impl, Proj::Axis const axis, Proj::Pt2 const &new_image);
38 static gchar * persp3d_pt_to_str (Persp3DImpl *persp_impl, Proj::Axis const axis);
40 static SPObjectClass *persp3d_parent_class;
42 static int global_counter = 0;
44 /* Constructor/destructor for the internal class */
46 Persp3DImpl::Persp3DImpl() {
47 tmat = Proj::TransfMat3x4 ();
48 document = NULL;
50 my_counter = global_counter++;
51 }
53 /**
54 * Registers Persp3d class and returns its type.
55 */
56 GType
57 persp3d_get_type()
58 {
59 static GType type = 0;
60 if (!type) {
61 GTypeInfo info = {
62 sizeof(Persp3DClass),
63 NULL, NULL,
64 (GClassInitFunc) persp3d_class_init,
65 NULL, NULL,
66 sizeof(Persp3D),
67 16,
68 (GInstanceInitFunc) persp3d_init,
69 NULL, /* value_table */
70 };
71 type = g_type_register_static(SP_TYPE_OBJECT, "Persp3D", &info, (GTypeFlags)0);
72 }
73 return type;
74 }
76 static Inkscape::XML::NodeEventVector const persp3d_repr_events = {
77 NULL, /* child_added */
78 NULL, /* child_removed */
79 persp3d_on_repr_attr_changed,
80 NULL, /* content_changed */
81 NULL /* order_changed */
82 };
84 /**
85 * Callback to initialize Persp3D vtable.
86 */
87 static void persp3d_class_init(Persp3DClass *klass)
88 {
89 SPObjectClass *sp_object_class = (SPObjectClass *) klass;
91 persp3d_parent_class = (SPObjectClass *) g_type_class_ref(SP_TYPE_OBJECT);
93 sp_object_class->build = persp3d_build;
94 sp_object_class->release = persp3d_release;
95 sp_object_class->set = persp3d_set;
96 sp_object_class->update = persp3d_update;
97 sp_object_class->write = persp3d_write;
98 }
100 /**
101 * Callback to initialize Persp3D object.
102 */
103 static void
104 persp3d_init(Persp3D *persp)
105 {
106 persp->perspective_impl = new Persp3DImpl();
107 }
109 /**
110 * Virtual build: set persp3d attributes from its associated XML node.
111 */
112 static void persp3d_build(SPObject *object, SPDocument *document, Inkscape::XML::Node *repr)
113 {
114 if (((SPObjectClass *) persp3d_parent_class)->build)
115 (* ((SPObjectClass *) persp3d_parent_class)->build)(object, document, repr);
117 /* calls sp_object_set for the respective attributes */
118 // The transformation matrix is updated according to the values we read for the VPs
119 sp_object_read_attr(object, "inkscape:vp_x");
120 sp_object_read_attr(object, "inkscape:vp_y");
121 sp_object_read_attr(object, "inkscape:vp_z");
122 sp_object_read_attr(object, "inkscape:persp3d-origin");
124 if (repr) {
125 repr->addListener (&persp3d_repr_events, object);
126 }
127 }
129 /**
130 * Virtual release of Persp3D members before destruction.
131 */
132 static void persp3d_release(SPObject *object) {
133 Persp3D *persp = SP_PERSP3D(object);
134 delete persp->perspective_impl;
135 SP_OBJECT_REPR(object)->removeListenerByData(object);
136 }
139 /**
140 * Virtual set: set attribute to value.
141 */
142 // FIXME: Currently we only read the finite positions of vanishing points;
143 // should we move VPs into their own repr (as it's done for SPStop, e.g.)?
144 static void
145 persp3d_set(SPObject *object, unsigned key, gchar const *value)
146 {
147 Persp3DImpl *persp_impl = SP_PERSP3D(object)->perspective_impl;
149 switch (key) {
150 case SP_ATTR_INKSCAPE_PERSP3D_VP_X: {
151 if (value) {
152 Proj::Pt2 new_image (value);
153 persp3d_update_with_point (persp_impl, Proj::X, new_image);
154 }
155 break;
156 }
157 case SP_ATTR_INKSCAPE_PERSP3D_VP_Y: {
158 if (value) {
159 Proj::Pt2 new_image (value);
160 persp3d_update_with_point (persp_impl, Proj::Y, new_image);
161 break;
162 }
163 }
164 case SP_ATTR_INKSCAPE_PERSP3D_VP_Z: {
165 if (value) {
166 Proj::Pt2 new_image (value);
167 persp3d_update_with_point (persp_impl, Proj::Z, new_image);
168 break;
169 }
170 }
171 case SP_ATTR_INKSCAPE_PERSP3D_ORIGIN: {
172 if (value) {
173 Proj::Pt2 new_image (value);
174 persp3d_update_with_point (persp_impl, Proj::W, new_image);
175 break;
176 }
177 }
178 default: {
179 if (((SPObjectClass *) persp3d_parent_class)->set)
180 (* ((SPObjectClass *) persp3d_parent_class)->set)(object, key, value);
181 break;
182 }
183 }
185 // FIXME: Is this the right place for resetting the draggers?
186 SPEventContext *ec = inkscape_active_event_context();
187 if (SP_IS_BOX3D_CONTEXT(ec)) {
188 Box3DContext *bc = SP_BOX3D_CONTEXT(ec);
189 bc->_vpdrag->updateDraggers();
190 bc->_vpdrag->updateLines();
191 bc->_vpdrag->updateBoxHandles();
192 bc->_vpdrag->updateBoxReprs();
193 }
194 }
196 static void
197 persp3d_update(SPObject *object, SPCtx *ctx, guint flags)
198 {
199 if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) {
201 /* TODO: Should we update anything here? */
203 }
205 if (((SPObjectClass *) persp3d_parent_class)->update)
206 ((SPObjectClass *) persp3d_parent_class)->update(object, ctx, flags);
207 }
209 Persp3D *
210 persp3d_create_xml_element (SPDocument *document, Persp3DImpl *dup) {// if dup is given, copy the attributes over
211 SPDefs *defs = (SPDefs *) SP_DOCUMENT_DEFS(document);
212 Inkscape::XML::Document *xml_doc = sp_document_repr_doc(document);
213 Inkscape::XML::Node *repr;
215 /* if no perspective is given, create a default one */
216 repr = xml_doc->createElement("inkscape:perspective");
217 repr->setAttribute("sodipodi:type", "inkscape:persp3d");
219 Proj::Pt2 proj_vp_x = Proj::Pt2 (0.0, sp_document_height(document)/2, 1.0);
220 Proj::Pt2 proj_vp_y = Proj::Pt2 (0.0, 1000.0, 0.0);
221 Proj::Pt2 proj_vp_z = Proj::Pt2 (sp_document_width(document), sp_document_height(document)/2, 1.0);
222 Proj::Pt2 proj_origin = Proj::Pt2 (sp_document_width(document)/2, sp_document_height(document)/3, 1.0);
224 if (dup) {
225 proj_vp_x = dup->tmat.column (Proj::X);
226 proj_vp_y = dup->tmat.column (Proj::Y);
227 proj_vp_z = dup->tmat.column (Proj::Z);
228 proj_origin = dup->tmat.column (Proj::W);
229 }
231 gchar *str = NULL;
232 str = proj_vp_x.coord_string();
233 repr->setAttribute("inkscape:vp_x", str);
234 g_free (str);
235 str = proj_vp_y.coord_string();
236 repr->setAttribute("inkscape:vp_y", str);
237 g_free (str);
238 str = proj_vp_z.coord_string();
239 repr->setAttribute("inkscape:vp_z", str);
240 g_free (str);
241 str = proj_origin.coord_string();
242 repr->setAttribute("inkscape:persp3d-origin", str);
243 g_free (str);
245 /* Append the new persp3d to defs */
246 SP_OBJECT_REPR(defs)->addChild(repr, NULL);
247 Inkscape::GC::release(repr);
249 return (Persp3D *) sp_object_get_child_by_repr (SP_OBJECT(defs), repr);
250 }
252 Persp3D *
253 persp3d_document_first_persp (SPDocument *document) {
254 SPDefs *defs = (SPDefs *) SP_DOCUMENT_DEFS(document);
255 Inkscape::XML::Node *repr;
256 for (SPObject *child = sp_object_first_child(defs); child != NULL; child = SP_OBJECT_NEXT(child) ) {
257 repr = SP_OBJECT_REPR(child);
258 if (SP_IS_PERSP3D(child)) {
259 return SP_PERSP3D(child);
260 }
261 }
262 return NULL;
263 }
265 /**
266 * Virtual write: write object attributes to repr.
267 */
268 static Inkscape::XML::Node *
269 persp3d_write(SPObject *object, Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags)
270 {
271 Persp3DImpl *persp_impl = SP_PERSP3D(object)->perspective_impl;
273 if ((flags & SP_OBJECT_WRITE_BUILD & SP_OBJECT_WRITE_EXT) && !repr) {
274 // this is where we end up when saving as plain SVG (also in other circumstances?);
275 // hence we don't set the sodipodi:type attribute
276 repr = xml_doc->createElement("inkscape:perspective");
277 }
279 if (flags & SP_OBJECT_WRITE_EXT) {
280 gchar *str = NULL; // FIXME: Should this be freed each time we set an attribute or only in the end or at all?
281 str = persp3d_pt_to_str (persp_impl, Proj::X);
282 repr->setAttribute("inkscape:vp_x", str);
284 str = persp3d_pt_to_str (persp_impl, Proj::Y);
285 repr->setAttribute("inkscape:vp_y", str);
287 str = persp3d_pt_to_str (persp_impl, Proj::Z);
288 repr->setAttribute("inkscape:vp_z", str);
290 str = persp3d_pt_to_str (persp_impl, Proj::W);
291 repr->setAttribute("inkscape:persp3d-origin", str);
292 }
294 if (((SPObjectClass *) persp3d_parent_class)->write)
295 (* ((SPObjectClass *) persp3d_parent_class)->write)(object, xml_doc, repr, flags);
297 return repr;
298 }
300 /* convenience wrapper around persp3d_get_finite_dir() and persp3d_get_infinite_dir() */
301 Geom::Point persp3d_get_PL_dir_from_pt (Persp3D *persp, Geom::Point const &pt, Proj::Axis axis) {
302 if (persp3d_VP_is_finite(persp->perspective_impl, axis)) {
303 return persp3d_get_finite_dir(persp, pt, axis);
304 } else {
305 return persp3d_get_infinite_dir(persp, axis);
306 }
307 }
309 Geom::Point
310 persp3d_get_finite_dir (Persp3D *persp, Geom::Point const &pt, Proj::Axis axis) {
311 Box3D::PerspectiveLine pl(pt, axis, persp);
312 return pl.direction();
313 }
315 Geom::Point
316 persp3d_get_infinite_dir (Persp3D *persp, Proj::Axis axis) {
317 Proj::Pt2 vp(persp3d_get_VP(persp, axis));
318 if (vp[2] != 0.0) {
319 g_print ("VP should be infinite but is (%f : %f : %f)\n", vp[0], vp[1], vp[2]);
320 g_return_val_if_fail(vp[2] != 0.0, Geom::Point(0.0, 0.0));
321 }
322 return Geom::Point(vp[0], vp[1]);
323 }
325 double
326 persp3d_get_infinite_angle (Persp3D *persp, Proj::Axis axis) {
327 return persp->perspective_impl->tmat.get_infinite_angle(axis);
328 }
330 bool
331 persp3d_VP_is_finite (Persp3DImpl *persp_impl, Proj::Axis axis) {
332 return persp_impl->tmat.has_finite_image(axis);
333 }
335 void
336 persp3d_toggle_VP (Persp3D *persp, Proj::Axis axis, bool set_undo) {
337 persp->perspective_impl->tmat.toggle_finite(axis);
338 // FIXME: Remove this repr update and rely on vp_drag_sel_modified() to do this for us
339 // On the other hand, vp_drag_sel_modified() would update all boxes;
340 // here we can confine ourselves to the boxes of this particular perspective.
341 persp3d_update_box_reprs (persp);
342 SP_OBJECT(persp)->updateRepr(SP_OBJECT_WRITE_EXT);
343 if (set_undo) {
344 sp_document_done(sp_desktop_document(inkscape_active_desktop()), SP_VERB_CONTEXT_3DBOX,
345 _("Toggle vanishing point"));
346 }
347 }
349 /* toggle VPs for the same axis in all perspectives of a given list */
350 void
351 persp3d_toggle_VPs (std::list<Persp3D *> p, Proj::Axis axis) {
352 for (std::list<Persp3D *>::iterator i = p.begin(); i != p.end(); ++i) {
353 persp3d_toggle_VP((*i), axis, false);
354 }
355 sp_document_done(sp_desktop_document(inkscape_active_desktop()), SP_VERB_CONTEXT_3DBOX,
356 _("Toggle multiple vanishing points"));
357 }
359 void
360 persp3d_set_VP_state (Persp3D *persp, Proj::Axis axis, Proj::VPState state) {
361 if (persp3d_VP_is_finite(persp->perspective_impl, axis) != (state == Proj::VP_FINITE)) {
362 persp3d_toggle_VP(persp, axis);
363 }
364 }
366 void
367 persp3d_rotate_VP (Persp3D *persp, Proj::Axis axis, double angle, bool alt_pressed) { // angle is in degrees
368 // FIXME: Most of this functionality should be moved to trans_mat_3x4.(h|cpp)
369 if (persp->perspective_impl->tmat.has_finite_image(axis)) {
370 // don't rotate anything for finite VPs
371 return;
372 }
373 Proj::Pt2 v_dir_proj (persp->perspective_impl->tmat.column(axis));
374 Geom::Point v_dir (v_dir_proj[0], v_dir_proj[1]);
375 double a = Geom::atan2 (v_dir) * 180/M_PI;
376 a += alt_pressed ? 0.5 * ((angle > 0 ) - (angle < 0)) : angle; // the r.h.s. yields +/-0.5 or angle
377 persp->perspective_impl->tmat.set_infinite_direction (axis, a);
379 persp3d_update_box_reprs (persp);
380 SP_OBJECT(persp)->updateRepr(SP_OBJECT_WRITE_EXT);
381 }
383 void
384 persp3d_update_with_point (Persp3DImpl *persp_impl, Proj::Axis const axis, Proj::Pt2 const &new_image) {
385 persp_impl->tmat.set_image_pt (axis, new_image);
386 }
388 void
389 persp3d_apply_affine_transformation (Persp3D *persp, Geom::Matrix const &xform) {
390 persp->perspective_impl->tmat *= xform;
391 persp3d_update_box_reprs(persp);
392 SP_OBJECT(persp)->updateRepr(SP_OBJECT_WRITE_EXT);
393 }
395 gchar *
396 persp3d_pt_to_str (Persp3DImpl *persp_impl, Proj::Axis const axis)
397 {
398 return persp_impl->tmat.pt_to_str(axis);
399 }
401 void
402 persp3d_add_box (Persp3D *persp, SPBox3D *box) {
403 Persp3DImpl *persp_impl = persp->perspective_impl;
405 if (!box) {
406 return;
407 }
408 if (std::find (persp_impl->boxes.begin(), persp_impl->boxes.end(), box) != persp_impl->boxes.end()) {
409 return;
410 }
411 persp_impl->boxes.push_back(box);
412 }
414 void
415 persp3d_remove_box (Persp3D *persp, SPBox3D *box) {
416 Persp3DImpl *persp_impl = persp->perspective_impl;
418 std::vector<SPBox3D *>::iterator i = std::find (persp_impl->boxes.begin(), persp_impl->boxes.end(), box);
419 if (i != persp_impl->boxes.end())
420 persp_impl->boxes.erase(i);
421 }
423 bool
424 persp3d_has_box (Persp3D *persp, SPBox3D *box) {
425 Persp3DImpl *persp_impl = persp->perspective_impl;
427 // FIXME: For some reason, std::find() does not seem to compare pointers "correctly" (or do we need to
428 // provide a proper comparison function?), so we manually traverse the list.
429 for (std::vector<SPBox3D *>::iterator i = persp_impl->boxes.begin(); i != persp_impl->boxes.end(); ++i) {
430 if ((*i) == box) {
431 return true;
432 }
433 }
434 return false;
435 }
437 void
438 persp3d_update_box_displays (Persp3D *persp) {
439 Persp3DImpl *persp_impl = persp->perspective_impl;
441 if (persp_impl->boxes.empty())
442 return;
443 for (std::vector<SPBox3D *>::iterator i = persp_impl->boxes.begin(); i != persp_impl->boxes.end(); ++i) {
444 box3d_position_set(*i);
445 }
446 }
448 void
449 persp3d_update_box_reprs (Persp3D *persp) {
450 if (!persp) {
451 // Hmm, is it an error if this happens?
452 return;
453 }
454 Persp3DImpl *persp_impl = persp->perspective_impl;
456 if (persp_impl->boxes.empty())
457 return;
458 for (std::vector<SPBox3D *>::iterator i = persp_impl->boxes.begin(); i != persp_impl->boxes.end(); ++i) {
459 SP_OBJECT(*i)->updateRepr(SP_OBJECT_WRITE_EXT);
460 box3d_set_z_orders(*i);
461 }
462 }
464 void
465 persp3d_update_z_orders (Persp3D *persp) {
466 Persp3DImpl *persp_impl = persp->perspective_impl;
468 if (persp_impl->boxes.empty())
469 return;
470 for (std::vector<SPBox3D *>::iterator i = persp_impl->boxes.begin(); i != persp_impl->boxes.end(); ++i) {
471 box3d_set_z_orders(*i);
472 }
473 }
475 // FIXME: For some reason we seem to require a vector instead of a list in Persp3D, but in vp_knot_moved_handler()
476 // we need a list of boxes. If we can store a list in Persp3D right from the start, this function becomes
477 // obsolete. We should do this.
478 std::list<SPBox3D *>
479 persp3d_list_of_boxes(Persp3D *persp) {
480 Persp3DImpl *persp_impl = persp->perspective_impl;
482 std::list<SPBox3D *> bx_lst;
483 for (std::vector<SPBox3D *>::iterator i = persp_impl->boxes.begin(); i != persp_impl->boxes.end(); ++i) {
484 bx_lst.push_back(*i);
485 }
486 return bx_lst;
487 }
489 bool
490 persp3d_perspectives_coincide(const Persp3D *lhs, const Persp3D *rhs)
491 {
492 return lhs->perspective_impl->tmat == rhs->perspective_impl->tmat;
493 }
495 void
496 persp3d_absorb(Persp3D *persp1, Persp3D *persp2) {
497 /* double check if we are called in sane situations */
498 g_return_if_fail (persp3d_perspectives_coincide(persp1, persp2) && persp1 != persp2);
500 std::vector<SPBox3D *>::iterator boxes;
502 // Note: We first need to copy the boxes of persp2 into a separate list;
503 // otherwise the loop below gets confused when perspectives are reattached.
504 std::list<SPBox3D *> boxes_of_persp2 = persp3d_list_of_boxes(persp2);
506 for (std::list<SPBox3D *>::iterator i = boxes_of_persp2.begin(); i != boxes_of_persp2.end(); ++i) {
507 box3d_switch_perspectives((*i), persp2, persp1, true);
508 SP_OBJECT(*i)->updateRepr(SP_OBJECT_WRITE_EXT); // so that undo/redo can do its job properly
509 }
510 }
512 static void
513 persp3d_on_repr_attr_changed ( Inkscape::XML::Node * /*repr*/,
514 const gchar */*key*/,
515 const gchar */*oldval*/,
516 const gchar */*newval*/,
517 bool /*is_interactive*/,
518 void * data )
519 {
520 if (!data)
521 return;
523 Persp3D *persp = (Persp3D*) data;
524 persp3d_update_box_displays (persp);
525 }
527 /* checks whether all boxes linked to this perspective are currently selected */
528 bool
529 persp3d_has_all_boxes_in_selection (Persp3D *persp, Inkscape::Selection *selection) {
530 Persp3DImpl *persp_impl = persp->perspective_impl;
532 std::list<SPBox3D *> selboxes = selection->box3DList();
534 for (std::vector<SPBox3D *>::iterator i = persp_impl->boxes.begin(); i != persp_impl->boxes.end(); ++i) {
535 if (std::find(selboxes.begin(), selboxes.end(), *i) == selboxes.end()) {
536 // we have an unselected box in the perspective
537 return false;
538 }
539 }
540 return true;
541 }
543 /* some debugging stuff follows */
545 void
546 persp3d_print_debugging_info (Persp3D *persp) {
547 Persp3DImpl *persp_impl = persp->perspective_impl;
548 g_print ("=== Info for Persp3D %d ===\n", persp_impl->my_counter);
549 gchar * cstr;
550 for (int i = 0; i < 4; ++i) {
551 cstr = persp3d_get_VP(persp, Proj::axes[i]).coord_string();
552 g_print (" VP %s: %s\n", Proj::string_from_axis(Proj::axes[i]), cstr);
553 g_free(cstr);
554 }
555 cstr = persp3d_get_VP(persp, Proj::W).coord_string();
556 g_print (" Origin: %s\n", cstr);
557 g_free(cstr);
559 g_print (" Boxes: ");
560 for (std::vector<SPBox3D *>::iterator i = persp_impl->boxes.begin(); i != persp_impl->boxes.end(); ++i) {
561 g_print ("%d (%d) ", (*i)->my_counter, box3d_get_perspective(*i)->perspective_impl->my_counter);
562 }
563 g_print ("\n");
564 g_print ("========================\n");
565 }
567 void
568 persp3d_print_debugging_info_all(SPDocument *document) {
569 SPDefs *defs = (SPDefs *) SP_DOCUMENT_DEFS(document);
570 Inkscape::XML::Node *repr;
571 for (SPObject *child = sp_object_first_child(defs); child != NULL; child = SP_OBJECT_NEXT(child) ) {
572 repr = SP_OBJECT_REPR(child);
573 if (SP_IS_PERSP3D(child)) {
574 persp3d_print_debugging_info(SP_PERSP3D(child));
575 }
576 }
577 persp3d_print_all_selected();
578 }
580 void
581 persp3d_print_all_selected() {
582 g_print ("\n======================================\n");
583 g_print ("Selected perspectives and their boxes:\n");
585 std::list<Persp3D *> sel_persps = sp_desktop_selection(inkscape_active_desktop())->perspList();
587 for (std::list<Persp3D *>::iterator j = sel_persps.begin(); j != sel_persps.end(); ++j) {
588 Persp3D *persp = SP_PERSP3D(*j);
589 Persp3DImpl *persp_impl = persp->perspective_impl;
590 g_print (" %s (%d): ", SP_OBJECT_REPR(persp)->attribute("id"), persp->perspective_impl->my_counter);
591 for (std::vector<SPBox3D *>::iterator i = persp_impl->boxes.begin();
592 i != persp_impl->boxes.end(); ++i) {
593 g_print ("%d ", (*i)->my_counter);
594 }
595 g_print ("\n");
596 }
597 g_print ("======================================\n\n");
598 }
600 void print_current_persp3d(gchar *func_name, Persp3D *persp) {
601 g_print ("%s: current_persp3d is now %s\n",
602 func_name,
603 persp ? SP_OBJECT_REPR(persp)->attribute("id") : "NULL");
604 }
606 /*
607 Local Variables:
608 mode:c++
609 c-file-style:"stroustrup"
610 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
611 indent-tabs-mode:nil
612 fill-column:99
613 End:
614 */
615 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :