1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * Authors:
4 * Lauris Kaplinski <lauris@kaplinski.com>
5 * bulia byak <buliabyak@users.sf.net>
6 * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
7 * Abhishek Sharma
8 * Jon A. Cruz <jon@joncruz.org>
9 *
10 * Copyright (C) 2001-2006 authors
11 * Copyright (C) 2001 Ximian, Inc.
12 *
13 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
14 */
15
16 #include "sp-item.h"
17
18 #include <glibmm/i18n.h>
19
20 #include "bad-uri-exception.h"
21 #include "svg/svg.h"
22 #include "print.h"
23 #include "display/drawing-item.h"
24 #include "attributes.h"
25 #include "document.h"
26
27 #include "inkscape.h"
28 #include "desktop.h"
29 #include "gradient-chemistry.h"
30 #include "conn-avoid-ref.h"
31 #include "conditions.h"
32 #include "filter-chemistry.h"
33
34 #include "sp-clippath.h"
35 #include "sp-desc.h"
36 #include "sp-guide.h"
37 #include "sp-hatch.h"
38 #include "sp-item-rm-unsatisfied-cns.h"
39 #include "sp-mask.h"
40 #include "sp-pattern.h"
41 #include "sp-rect.h"
42 #include "sp-root.h"
43 #include "sp-switch.h"
44 #include "sp-text.h"
45 #include "sp-textpath.h"
46 #include "sp-title.h"
47 #include "sp-use.h"
48
49 #include "style.h"
50 #include "uri.h"
51
52
53 #include "util/find-last-if.h"
54
55 #include "extract-uri.h"
56
57 #include "live_effects/lpeobject.h"
58 #include "live_effects/effect.h"
59 #include "live_effects/lpeobject-reference.h"
60
61 #include "util/units.h"
62
63 #define noSP_ITEM_DEBUG_IDLE
64
65 //#define OBJECT_TRACE
66
67 static SPItemView* sp_item_view_list_remove(SPItemView *list,
68 SPItemView *view);
69
70
SPItem()71 SPItem::SPItem() : SPObject() {
72
73 sensitive = TRUE;
74 bbox_valid = FALSE;
75
76 _highlightColor = nullptr;
77
78 transform_center_x = 0;
79 transform_center_y = 0;
80
81 freeze_stroke_width = false;
82 _is_evaluated = true;
83 _evaluated_status = StatusUnknown;
84
85 transform = Geom::identity();
86 // doc_bbox = Geom::OptRect();
87
88 display = nullptr;
89
90 clip_ref = nullptr;
91 mask_ref = nullptr;
92
93 style->signal_fill_ps_changed.connect(sigc::bind(sigc::ptr_fun(fill_ps_ref_changed), this));
94 style->signal_stroke_ps_changed.connect(sigc::bind(sigc::ptr_fun(stroke_ps_ref_changed), this));
95
96 avoidRef = nullptr;
97 }
98
99 SPItem::~SPItem() = default;
100
getClipObject() const101 SPClipPath *SPItem::getClipObject() const { return clip_ref ? clip_ref->getObject() : nullptr; }
102
getMaskObject() const103 SPMask *SPItem::getMaskObject() const { return mask_ref ? mask_ref->getObject() : nullptr; }
104
getMaskRef()105 SPMaskReference &SPItem::getMaskRef()
106 {
107 if (!mask_ref) {
108 mask_ref = new SPMaskReference(this);
109 mask_ref->changedSignal().connect(sigc::bind(sigc::ptr_fun(mask_ref_changed), this));
110 }
111
112 return *mask_ref;
113 }
114
getClipRef()115 SPClipPathReference &SPItem::getClipRef()
116 {
117 if (!clip_ref) {
118 clip_ref = new SPClipPathReference(this);
119 clip_ref->changedSignal().connect(sigc::bind(sigc::ptr_fun(clip_ref_changed), this));
120 }
121
122 return *clip_ref;
123 }
124
getAvoidRef()125 SPAvoidRef &SPItem::getAvoidRef()
126 {
127 if (!avoidRef) {
128 avoidRef = new SPAvoidRef(this);
129 }
130 return *avoidRef;
131 }
132
isVisibleAndUnlocked() const133 bool SPItem::isVisibleAndUnlocked() const {
134 return (!isHidden() && !isLocked());
135 }
136
isVisibleAndUnlocked(unsigned display_key) const137 bool SPItem::isVisibleAndUnlocked(unsigned display_key) const {
138 return (!isHidden(display_key) && !isLocked());
139 }
140
isLocked() const141 bool SPItem::isLocked() const {
142 for (SPObject const *o = this; o != nullptr; o = o->parent) {
143 SPItem const *item = dynamic_cast<SPItem const *>(o);
144 if (item && !(item->sensitive)) {
145 return true;
146 }
147 }
148 return false;
149 }
150
setLocked(bool locked)151 void SPItem::setLocked(bool locked) {
152 setAttribute("sodipodi:insensitive",
153 ( locked ? "1" : nullptr ));
154 updateRepr();
155 document->_emitModified();
156 }
157
isHidden() const158 bool SPItem::isHidden() const {
159 if (!isEvaluated())
160 return true;
161 return style->display.computed == SP_CSS_DISPLAY_NONE;
162 }
163
setHidden(bool hide)164 void SPItem::setHidden(bool hide) {
165 style->display.set = TRUE;
166 style->display.value = ( hide ? SP_CSS_DISPLAY_NONE : SP_CSS_DISPLAY_INLINE );
167 style->display.computed = style->display.value;
168 style->display.inherit = FALSE;
169 updateRepr();
170 }
171
isHidden(unsigned display_key) const172 bool SPItem::isHidden(unsigned display_key) const {
173 if (!isEvaluated())
174 return true;
175 for ( SPItemView *view(display) ; view ; view = view->next ) {
176 if ( view->key == display_key ) {
177 g_assert(view->arenaitem != nullptr);
178 for ( Inkscape::DrawingItem *arenaitem = view->arenaitem ;
179 arenaitem ; arenaitem = arenaitem->parent() )
180 {
181 if (!arenaitem->visible()) {
182 return true;
183 }
184 }
185 return false;
186 }
187 }
188 return true;
189 }
190
isHighlightSet() const191 bool SPItem::isHighlightSet() const {
192 return _highlightColor != nullptr;
193 }
194
highlight_color() const195 guint32 SPItem::highlight_color() const {
196 if (_highlightColor)
197 {
198 return atoi(_highlightColor) | 0x00000000;
199 }
200 else {
201 SPItem const *item = dynamic_cast<SPItem const *>(parent);
202 if (parent && (parent != this) && item)
203 {
204 return item->highlight_color();
205 }
206 else
207 {
208 static Inkscape::Preferences *prefs = Inkscape::Preferences::get();
209 return prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff);
210 }
211 }
212 }
213
setEvaluated(bool evaluated)214 void SPItem::setEvaluated(bool evaluated) {
215 _is_evaluated = evaluated;
216 _evaluated_status = StatusSet;
217 }
218
resetEvaluated()219 void SPItem::resetEvaluated() {
220 if ( StatusCalculated == _evaluated_status ) {
221 _evaluated_status = StatusUnknown;
222 bool oldValue = _is_evaluated;
223 if ( oldValue != isEvaluated() ) {
224 requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
225 }
226 } if ( StatusSet == _evaluated_status ) {
227 SPSwitch *switchItem = dynamic_cast<SPSwitch *>(parent);
228 if (switchItem) {
229 switchItem->resetChildEvaluated();
230 }
231 }
232 }
233
isEvaluated() const234 bool SPItem::isEvaluated() const {
235 if ( StatusUnknown == _evaluated_status ) {
236 _is_evaluated = sp_item_evaluate(this);
237 _evaluated_status = StatusCalculated;
238 }
239 return _is_evaluated;
240 }
241
isExplicitlyHidden() const242 bool SPItem::isExplicitlyHidden() const
243 {
244 return (style->display.set
245 && style->display.value == SP_CSS_DISPLAY_NONE);
246 }
247
setExplicitlyHidden(bool val)248 void SPItem::setExplicitlyHidden(bool val) {
249 style->display.set = val;
250 style->display.value = ( val ? SP_CSS_DISPLAY_NONE : SP_CSS_DISPLAY_INLINE );
251 style->display.computed = style->display.value;
252 updateRepr();
253 }
254
setCenter(Geom::Point const & object_centre)255 void SPItem::setCenter(Geom::Point const &object_centre) {
256 document->ensureUpToDate();
257
258 // Copied from DocumentProperties::onDocUnitChange()
259 gdouble viewscale = 1.0;
260 Geom::Rect vb = this->document->getRoot()->viewBox;
261 if ( !vb.hasZeroArea() ) {
262 gdouble viewscale_w = this->document->getWidth().value("px") / vb.width();
263 gdouble viewscale_h = this->document->getHeight().value("px")/ vb.height();
264 viewscale = std::min(viewscale_h, viewscale_w);
265 }
266
267 // FIXME this is seriously wrong
268 Geom::OptRect bbox = desktopGeometricBounds();
269 if (bbox) {
270 // object centre is document coordinates (i.e. in pixels), so we need to consider the viewbox
271 // to translate to user units; transform_center_x/y is in user units
272 transform_center_x = (object_centre[Geom::X] - bbox->midpoint()[Geom::X])/viewscale;
273 if (Geom::are_near(transform_center_x, 0)) // rounding error
274 transform_center_x = 0;
275 transform_center_y = (object_centre[Geom::Y] - bbox->midpoint()[Geom::Y])/viewscale;
276 if (Geom::are_near(transform_center_y, 0)) // rounding error
277 transform_center_y = 0;
278 }
279 }
280
281 void
unsetCenter()282 SPItem::unsetCenter() {
283 transform_center_x = 0;
284 transform_center_y = 0;
285 }
286
isCenterSet() const287 bool SPItem::isCenterSet() const {
288 return (transform_center_x != 0 || transform_center_y != 0);
289 }
290
291 // Get the item's transformation center in desktop coordinates (i.e. in pixels)
getCenter() const292 Geom::Point SPItem::getCenter() const {
293 document->ensureUpToDate();
294
295 // Copied from DocumentProperties::onDocUnitChange()
296 gdouble viewscale = 1.0;
297 Geom::Rect vb = this->document->getRoot()->viewBox;
298 if ( !vb.hasZeroArea() ) {
299 gdouble viewscale_w = this->document->getWidth().value("px") / vb.width();
300 gdouble viewscale_h = this->document->getHeight().value("px")/ vb.height();
301 viewscale = std::min(viewscale_h, viewscale_w);
302 }
303
304 // FIXME this is seriously wrong
305 Geom::OptRect bbox = desktopGeometricBounds();
306 if (bbox) {
307 // transform_center_x/y are stored in user units, so we have to take the viewbox into account to translate to document coordinates
308 return bbox->midpoint() + Geom::Point (transform_center_x*viewscale, transform_center_y*viewscale);
309
310 } else {
311 return Geom::Point(0, 0); // something's wrong!
312 }
313
314 }
315
316 void
scaleCenter(Geom::Scale const & sc)317 SPItem::scaleCenter(Geom::Scale const &sc) {
318 transform_center_x *= sc[Geom::X];
319 transform_center_y *= sc[Geom::Y];
320 }
321
322 namespace {
323
is_item(SPObject const & object)324 bool is_item(SPObject const &object) {
325 return dynamic_cast<SPItem const *>(&object) != nullptr;
326 }
327
328 }
329
raiseToTop()330 void SPItem::raiseToTop() {
331 using Inkscape::Algorithms::find_last_if;
332
333 auto topmost = find_last_if(++parent->children.iterator_to(*this), parent->children.end(), &is_item);
334 if (topmost != parent->children.end()) {
335 getRepr()->parent()->changeOrder( getRepr(), topmost->getRepr() );
336 }
337 }
338
raiseOne()339 bool SPItem::raiseOne() {
340 auto next_higher = std::find_if(++parent->children.iterator_to(*this), parent->children.end(), &is_item);
341 if (next_higher != parent->children.end()) {
342 Inkscape::XML::Node *ref = next_higher->getRepr();
343 getRepr()->parent()->changeOrder(getRepr(), ref);
344 return true;
345 }
346 return false;
347 }
348
lowerOne()349 bool SPItem::lowerOne() {
350 using Inkscape::Algorithms::find_last_if;
351
352 auto next_lower = find_last_if(parent->children.begin(), parent->children.iterator_to(*this), &is_item);
353 if (next_lower != parent->children.iterator_to(*this)) {
354 Inkscape::XML::Node *ref = nullptr;
355 if (next_lower != parent->children.begin()) {
356 next_lower--;
357 ref = next_lower->getRepr();
358 }
359 getRepr()->parent()->changeOrder(getRepr(), ref);
360 return true;
361 }
362 return false;
363 }
364
lowerToBottom()365 void SPItem::lowerToBottom() {
366 auto bottom = std::find_if(parent->children.begin(), parent->children.iterator_to(*this), &is_item);
367 if (bottom != parent->children.iterator_to(*this)) {
368 Inkscape::XML::Node *ref = nullptr;
369 if (bottom != parent->children.begin()) {
370 bottom--;
371 ref = bottom->getRepr();
372 }
373 parent->getRepr()->changeOrder(getRepr(), ref);
374 }
375 }
376
moveTo(SPItem * target,bool intoafter)377 void SPItem::moveTo(SPItem *target, bool intoafter) {
378
379 Inkscape::XML::Node *target_ref = ( target ? target->getRepr() : nullptr );
380 Inkscape::XML::Node *our_ref = getRepr();
381
382 if (!target_ref) {
383 // Assume move to the "first" in the top node, find the top node
384 intoafter = false;
385 SPObject* bottom = this->document->getObjectByRepr(our_ref->root())->firstChild();
386 while(!dynamic_cast<SPItem*>(bottom->getNext())){
387 bottom = bottom->getNext();
388 }
389 target_ref = bottom->getRepr();
390 }
391
392 if (target_ref == our_ref) {
393 // Move to ourself ignore
394 return;
395 }
396
397 if (intoafter) {
398 // Move this inside of the target at the end
399 our_ref->parent()->removeChild(our_ref);
400 target_ref->addChild(our_ref, nullptr);
401 } else if (target_ref->parent() != our_ref->parent()) {
402 // Change in parent, need to remove and add
403 our_ref->parent()->removeChild(our_ref);
404 target_ref->parent()->addChild(our_ref, target_ref);
405 } else {
406 // Same parent, just move
407 our_ref->parent()->changeOrder(our_ref, target_ref);
408 }
409 }
410
build(SPDocument * document,Inkscape::XML::Node * repr)411 void SPItem::build(SPDocument *document, Inkscape::XML::Node *repr) {
412 SPItem* object = this;
413
414 object->readAttr(SPAttr::STYLE);
415 object->readAttr(SPAttr::TRANSFORM);
416 object->readAttr(SPAttr::CLIP_PATH);
417 object->readAttr(SPAttr::MASK);
418 object->readAttr(SPAttr::SODIPODI_INSENSITIVE);
419 object->readAttr(SPAttr::TRANSFORM_CENTER_X);
420 object->readAttr(SPAttr::TRANSFORM_CENTER_Y);
421 object->readAttr(SPAttr::CONNECTOR_AVOID);
422 object->readAttr(SPAttr::CONNECTION_POINTS);
423 object->readAttr(SPAttr::INKSCAPE_HIGHLIGHT_COLOR);
424
425 SPObject::build(document, repr);
426 }
427
release()428 void SPItem::release() {
429 SPItem* item = this;
430
431 // Note: do this here before the clip_ref is deleted, since calling
432 // ensureUpToDate() for triggered routing may reference
433 // the deleted clip_ref.
434 delete item->avoidRef;
435
436 // we do NOT disconnect from the changed signal of those before deletion.
437 // The destructor will call *_ref_changed with NULL as the new value,
438 // which will cause the hide() function to be called.
439 delete item->clip_ref;
440 delete item->mask_ref;
441
442 SPObject::release();
443
444 SPPaintServer *fill_ps = style->getFillPaintServer();
445 SPPaintServer *stroke_ps = style->getStrokePaintServer();
446 while (item->display) {
447 if (fill_ps) {
448 fill_ps->hide(item->display->arenaitem->key());
449 }
450 if (stroke_ps) {
451 stroke_ps->hide(item->display->arenaitem->key());
452 }
453 item->display = sp_item_view_list_remove(item->display, item->display);
454 }
455
456 //item->_transformed_signal.~signal();
457 }
458
set(SPAttr key,gchar const * value)459 void SPItem::set(SPAttr key, gchar const* value) {
460 SPItem *item = this;
461 SPItem* object = item;
462
463 switch (key) {
464 case SPAttr::TRANSFORM: {
465 Geom::Affine t;
466 if (value && sp_svg_transform_read(value, &t)) {
467 item->set_item_transform(t);
468 } else {
469 item->set_item_transform(Geom::identity());
470 }
471 break;
472 }
473 case SPAttr::CLIP_PATH: {
474 auto uri = extract_uri(value);
475 if (!uri.empty() || item->clip_ref) {
476 item->getClipRef().try_attach(uri.c_str());
477 }
478 break;
479 }
480 case SPAttr::MASK: {
481 auto uri = extract_uri(value);
482 if (!uri.empty() || item->mask_ref) {
483 item->getMaskRef().try_attach(uri.c_str());
484 }
485 break;
486 }
487 case SPAttr::SODIPODI_INSENSITIVE:
488 {
489 item->sensitive = !value;
490 for (SPItemView *v = item->display; v != nullptr; v = v->next) {
491 v->arenaitem->setSensitive(item->sensitive);
492 }
493 break;
494 }
495 case SPAttr::INKSCAPE_HIGHLIGHT_COLOR:
496 {
497 g_free(item->_highlightColor);
498 if (value) {
499 item->_highlightColor = g_strdup(value);
500 } else {
501 item->_highlightColor = nullptr;
502 }
503 break;
504 }
505 case SPAttr::CONNECTOR_AVOID:
506 if (value || item->avoidRef) {
507 item->getAvoidRef().setAvoid(value);
508 }
509 break;
510 case SPAttr::TRANSFORM_CENTER_X:
511 if (value) {
512 item->transform_center_x = g_strtod(value, nullptr);
513 } else {
514 item->transform_center_x = 0;
515 }
516 object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
517 break;
518 case SPAttr::TRANSFORM_CENTER_Y:
519 if (value) {
520 item->transform_center_y = g_strtod(value, nullptr);
521 item->transform_center_y *= -document->yaxisdir();
522 } else {
523 item->transform_center_y = 0;
524 }
525 object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
526 break;
527 case SPAttr::SYSTEM_LANGUAGE:
528 case SPAttr::REQUIRED_FEATURES:
529 case SPAttr::REQUIRED_EXTENSIONS:
530 {
531 item->resetEvaluated();
532 // pass to default handler
533 }
534 default:
535 if (SP_ATTRIBUTE_IS_CSS(key)) {
536 // Propergate the property change to all clones
537 style->readFromObject(object);
538 object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
539 } else {
540 SPObject::set(key, value);
541 }
542 break;
543 }
544 }
545
clip_ref_changed(SPObject * old_clip,SPObject * clip,SPItem * item)546 void SPItem::clip_ref_changed(SPObject *old_clip, SPObject *clip, SPItem *item)
547 {
548 item->bbox_valid = FALSE; // force a re-evaluation
549 if (old_clip) {
550 SPItemView *v;
551 /* Hide clippath */
552 for (v = item->display; v != nullptr; v = v->next) {
553 SPClipPath *oldPath = dynamic_cast<SPClipPath *>(old_clip);
554 g_assert(oldPath != nullptr);
555 oldPath->hide(v->arenaitem->key());
556 }
557 }
558 SPClipPath *clipPath = dynamic_cast<SPClipPath *>(clip);
559 if (clipPath) {
560 Geom::OptRect bbox = item->geometricBounds();
561 for (SPItemView *v = item->display; v != nullptr; v = v->next) {
562 if (!v->arenaitem->key()) {
563 v->arenaitem->setKey(SPItem::display_key_new(3));
564 }
565 Inkscape::DrawingItem *ai = clipPath->show(
566 v->arenaitem->drawing(),
567 v->arenaitem->key());
568 v->arenaitem->setClip(ai);
569 clipPath->setBBox(v->arenaitem->key(), bbox);
570 clip->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
571 }
572 }
573 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
574 }
575
mask_ref_changed(SPObject * old_mask,SPObject * mask,SPItem * item)576 void SPItem::mask_ref_changed(SPObject *old_mask, SPObject *mask, SPItem *item)
577 {
578 item->bbox_valid = FALSE; // force a re-evaluation
579 if (old_mask) {
580 /* Hide mask */
581 for (SPItemView *v = item->display; v != nullptr; v = v->next) {
582 SPMask *maskItem = dynamic_cast<SPMask *>(old_mask);
583 g_assert(maskItem != nullptr);
584 maskItem->sp_mask_hide(v->arenaitem->key());
585 }
586 }
587 SPMask *maskItem = dynamic_cast<SPMask *>(mask);
588 if (maskItem) {
589 Geom::OptRect bbox = item->geometricBounds();
590 for (SPItemView *v = item->display; v != nullptr; v = v->next) {
591 if (!v->arenaitem->key()) {
592 v->arenaitem->setKey(SPItem::display_key_new(3));
593 }
594 Inkscape::DrawingItem *ai = maskItem->sp_mask_show(
595 v->arenaitem->drawing(),
596 v->arenaitem->key());
597 v->arenaitem->setMask(ai);
598 maskItem->sp_mask_set_bbox(v->arenaitem->key(), bbox);
599 mask->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
600 }
601 }
602 }
603
fill_ps_ref_changed(SPObject * old_ps,SPObject * ps,SPItem * item)604 void SPItem::fill_ps_ref_changed(SPObject *old_ps, SPObject *ps, SPItem *item) {
605 SPPaintServer *old_fill_ps = dynamic_cast<SPPaintServer *>(old_ps);
606 if (old_fill_ps) {
607 for (SPItemView *v =item->display; v != nullptr; v = v->next) {
608 old_fill_ps->hide(v->arenaitem->key());
609 }
610 }
611
612 SPPaintServer *new_fill_ps = dynamic_cast<SPPaintServer *>(ps);
613 if (new_fill_ps) {
614 Geom::OptRect bbox = item->geometricBounds();
615 for (SPItemView *v = item->display; v != nullptr; v = v->next) {
616 if (!v->arenaitem->key()) {
617 v->arenaitem->setKey(SPItem::display_key_new(3));
618 }
619 Inkscape::DrawingPattern *pi = new_fill_ps->show(
620 v->arenaitem->drawing(), v->arenaitem->key(), bbox);
621 v->arenaitem->setFillPattern(pi);
622 if (pi) {
623 new_fill_ps->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
624 }
625 }
626 }
627 }
628
stroke_ps_ref_changed(SPObject * old_ps,SPObject * ps,SPItem * item)629 void SPItem::stroke_ps_ref_changed(SPObject *old_ps, SPObject *ps, SPItem *item) {
630 SPPaintServer *old_stroke_ps = dynamic_cast<SPPaintServer *>(old_ps);
631 if (old_stroke_ps) {
632 for (SPItemView *v =item->display; v != nullptr; v = v->next) {
633 old_stroke_ps->hide(v->arenaitem->key());
634 }
635 }
636
637 SPPaintServer *new_stroke_ps = dynamic_cast<SPPaintServer *>(ps);
638 if (new_stroke_ps) {
639 Geom::OptRect bbox = item->geometricBounds();
640 for (SPItemView *v = item->display; v != nullptr; v = v->next) {
641 if (!v->arenaitem->key()) {
642 v->arenaitem->setKey(SPItem::display_key_new(3));
643 }
644 Inkscape::DrawingPattern *pi = new_stroke_ps->show(
645 v->arenaitem->drawing(), v->arenaitem->key(), bbox);
646 v->arenaitem->setStrokePattern(pi);
647 if (pi) {
648 new_stroke_ps->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
649 }
650 }
651 }
652 }
653
update(SPCtx * ctx,guint flags)654 void SPItem::update(SPCtx* ctx, guint flags) {
655
656 SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx);
657
658 // Any of the modifications defined in sp-object.h might change bbox,
659 // so we invalidate it unconditionally
660 bbox_valid = FALSE;
661
662 viewport = ictx->viewport; // Cache viewport
663
664 if (flags & (SP_OBJECT_CHILD_MODIFIED_FLAG |
665 SP_OBJECT_MODIFIED_FLAG |
666 SP_OBJECT_STYLE_MODIFIED_FLAG) ) {
667 if (flags & SP_OBJECT_MODIFIED_FLAG) {
668 for (SPItemView *v = display; v != nullptr; v = v->next) {
669 v->arenaitem->setTransform(transform);
670 }
671 }
672
673 SPClipPath *clip_path = clip_ref ? clip_ref->getObject() : nullptr;
674 SPMask *mask = mask_ref ? mask_ref->getObject() : nullptr;
675
676 if ( clip_path || mask ) {
677 Geom::OptRect bbox = geometricBounds();
678 if (clip_path) {
679 for (SPItemView *v = display; v != nullptr; v = v->next) {
680 clip_path->setBBox(v->arenaitem->key(), bbox);
681 }
682 }
683 if (mask) {
684 for (SPItemView *v = display; v != nullptr; v = v->next) {
685 mask->sp_mask_set_bbox(v->arenaitem->key(), bbox);
686 }
687 }
688 }
689
690 if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) {
691 for (SPItemView *v = display; v != nullptr; v = v->next) {
692 v->arenaitem->setOpacity(SP_SCALE24_TO_FLOAT(style->opacity.value));
693 v->arenaitem->setAntialiasing(style->shape_rendering.computed == SP_CSS_SHAPE_RENDERING_CRISPEDGES ? 0 : 2);
694 v->arenaitem->setIsolation( style->isolation.value );
695 v->arenaitem->setBlendMode( style->mix_blend_mode.value );
696 v->arenaitem->setVisible(!isHidden());
697 }
698 }
699 }
700 /* Update bounding box in user space, used for filter and objectBoundingBox units */
701 if (style->filter.set && display) {
702 Geom::OptRect item_bbox = geometricBounds();
703 SPItemView *itemview = display;
704 do {
705 if (itemview->arenaitem)
706 itemview->arenaitem->setItemBounds(item_bbox);
707 } while ( (itemview = itemview->next) );
708 }
709
710 // Update libavoid with item geometry (for connector routing).
711 if (avoidRef && document) {
712 avoidRef->handleSettingChange();
713 }
714 }
715
modified(unsigned int)716 void SPItem::modified(unsigned int /*flags*/)
717 {
718 #ifdef OBJECT_TRACE
719 objectTrace( "SPItem::modified" );
720 objectTrace( "SPItem::modified", false );
721 #endif
722 }
723
write(Inkscape::XML::Document * xml_doc,Inkscape::XML::Node * repr,guint flags)724 Inkscape::XML::Node* SPItem::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) {
725 SPItem *item = this;
726 SPItem* object = item;
727
728 // in the case of SP_OBJECT_WRITE_BUILD, the item should always be newly created,
729 // so we need to add any children from the underlying object to the new repr
730 if (flags & SP_OBJECT_WRITE_BUILD) {
731 std::vector<Inkscape::XML::Node *>l;
732 for (auto& child: object->children) {
733 if (dynamic_cast<SPTitle *>(&child) || dynamic_cast<SPDesc *>(&child)) {
734 Inkscape::XML::Node *crepr = child.updateRepr(xml_doc, nullptr, flags);
735 if (crepr) {
736 l.push_back(crepr);
737 }
738 }
739 }
740 for (auto i = l.rbegin(); i!= l.rend(); ++i) {
741 repr->addChild(*i, nullptr);
742 Inkscape::GC::release(*i);
743 }
744 } else {
745 for (auto& child: object->children) {
746 if (dynamic_cast<SPTitle *>(&child) || dynamic_cast<SPDesc *>(&child)) {
747 child.updateRepr(flags);
748 }
749 }
750 }
751
752 repr->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(item->transform));
753
754 if (flags & SP_OBJECT_WRITE_EXT) {
755 repr->setAttribute("sodipodi:insensitive", ( item->sensitive ? nullptr : "true" ));
756 if (item->transform_center_x != 0)
757 sp_repr_set_svg_double (repr, "inkscape:transform-center-x", item->transform_center_x);
758 else
759 repr->removeAttribute("inkscape:transform-center-x");
760 if (item->transform_center_y != 0) {
761 auto y = item->transform_center_y;
762 y *= -document->yaxisdir();
763 sp_repr_set_svg_double (repr, "inkscape:transform-center-y", y);
764 } else
765 repr->removeAttribute("inkscape:transform-center-y");
766 }
767
768 if (item->clip_ref){
769 if (item->clip_ref->getObject()) {
770 auto value = item->clip_ref->getURI()->cssStr();
771 repr->setAttributeOrRemoveIfEmpty("clip-path", value);
772 }
773 }
774 if (item->mask_ref){
775 if (item->mask_ref->getObject()) {
776 auto value = item->mask_ref->getURI()->cssStr();
777 repr->setAttributeOrRemoveIfEmpty("mask", value);
778 }
779 }
780 if (item->_highlightColor){
781 repr->setAttribute("inkscape:highlight-color", item->_highlightColor);
782 } else {
783 repr->removeAttribute("inkscape:highlight-color");
784 }
785
786 SPObject::write(xml_doc, repr, flags);
787
788 return repr;
789 }
790
791 // CPPIFY: make pure virtual
bbox(Geom::Affine const &,SPItem::BBoxType) const792 Geom::OptRect SPItem::bbox(Geom::Affine const & /*transform*/, SPItem::BBoxType /*type*/) const {
793 //throw;
794 return Geom::OptRect();
795 }
796
geometricBounds(Geom::Affine const & transform) const797 Geom::OptRect SPItem::geometricBounds(Geom::Affine const &transform) const
798 {
799 Geom::OptRect bbox;
800
801 // call the subclass method
802 // CPPIFY
803 //bbox = this->bbox(transform, SPItem::GEOMETRIC_BBOX);
804 bbox = const_cast<SPItem*>(this)->bbox(transform, SPItem::GEOMETRIC_BBOX);
805
806 return bbox;
807 }
808
visualBounds(Geom::Affine const & transform,bool wfilter,bool wclip,bool wmask) const809 Geom::OptRect SPItem::visualBounds(Geom::Affine const &transform, bool wfilter, bool wclip, bool wmask) const
810 {
811 using Geom::X;
812 using Geom::Y;
813
814 Geom::OptRect bbox;
815
816
817 SPFilter *filter = style ? style->getFilter() : nullptr;
818 if (filter && wfilter) {
819 // call the subclass method
820 // CPPIFY
821 //bbox = this->bbox(Geom::identity(), SPItem::VISUAL_BBOX);
822 bbox = const_cast<SPItem*>(this)->bbox(Geom::identity(), SPItem::GEOMETRIC_BBOX); // see LP Bug 1229971
823
824 // default filer area per the SVG spec:
825 SVGLength x, y, w, h;
826 Geom::Point minp, maxp;
827 x.set(SVGLength::PERCENT, -0.10, 0);
828 y.set(SVGLength::PERCENT, -0.10, 0);
829 w.set(SVGLength::PERCENT, 1.20, 0);
830 h.set(SVGLength::PERCENT, 1.20, 0);
831
832 // if area is explicitly set, override:
833 if (filter->x._set)
834 x = filter->x;
835 if (filter->y._set)
836 y = filter->y;
837 if (filter->width._set)
838 w = filter->width;
839 if (filter->height._set)
840 h = filter->height;
841
842 double len_x = bbox ? bbox->width() : 0;
843 double len_y = bbox ? bbox->height() : 0;
844
845 x.update(12, 6, len_x);
846 y.update(12, 6, len_y);
847 w.update(12, 6, len_x);
848 h.update(12, 6, len_y);
849
850 if (filter->filterUnits == SP_FILTER_UNITS_OBJECTBOUNDINGBOX && bbox) {
851 minp[X] = bbox->left() + x.computed * (x.unit == SVGLength::PERCENT ? 1.0 : len_x);
852 maxp[X] = minp[X] + w.computed * (w.unit == SVGLength::PERCENT ? 1.0 : len_x);
853 minp[Y] = bbox->top() + y.computed * (y.unit == SVGLength::PERCENT ? 1.0 : len_y);
854 maxp[Y] = minp[Y] + h.computed * (h.unit == SVGLength::PERCENT ? 1.0 : len_y);
855 } else if (filter->filterUnits == SP_FILTER_UNITS_USERSPACEONUSE) {
856 minp[X] = x.computed;
857 maxp[X] = minp[X] + w.computed;
858 minp[Y] = y.computed;
859 maxp[Y] = minp[Y] + h.computed;
860 }
861 bbox = Geom::OptRect(minp, maxp);
862 *bbox *= transform;
863 } else {
864 // call the subclass method
865 // CPPIFY
866 //bbox = this->bbox(transform, SPItem::VISUAL_BBOX);
867 bbox = const_cast<SPItem*>(this)->bbox(transform, SPItem::VISUAL_BBOX);
868 }
869 if (clip_ref && clip_ref->getObject() && wclip) {
870 SPItem *ownerItem = dynamic_cast<SPItem *>(clip_ref->getOwner());
871 g_assert(ownerItem != nullptr);
872 ownerItem->bbox_valid = FALSE; // LP Bug 1349018
873 bbox.intersectWith(clip_ref->getObject()->geometricBounds(transform));
874 }
875 if (mask_ref && mask_ref->getObject() && wmask) {
876 bbox_valid = false; // LP Bug 1349018
877 bbox.intersectWith(mask_ref->getObject()->visualBounds(transform));
878 }
879
880 return bbox;
881 }
882
bounds(BBoxType type,Geom::Affine const & transform) const883 Geom::OptRect SPItem::bounds(BBoxType type, Geom::Affine const &transform) const
884 {
885 if (type == GEOMETRIC_BBOX) {
886 return geometricBounds(transform);
887 } else {
888 return visualBounds(transform);
889 }
890 }
891
documentPreferredBounds() const892 Geom::OptRect SPItem::documentPreferredBounds() const
893 {
894 if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) {
895 return documentBounds(SPItem::VISUAL_BBOX);
896 } else {
897 return documentBounds(SPItem::GEOMETRIC_BBOX);
898 }
899 }
900
901
902
documentGeometricBounds() const903 Geom::OptRect SPItem::documentGeometricBounds() const
904 {
905 return geometricBounds(i2doc_affine());
906 }
907
documentVisualBounds() const908 Geom::OptRect SPItem::documentVisualBounds() const
909 {
910 if (!bbox_valid) {
911 doc_bbox = visualBounds(i2doc_affine());
912 bbox_valid = true;
913 }
914 return doc_bbox;
915 }
documentBounds(BBoxType type) const916 Geom::OptRect SPItem::documentBounds(BBoxType type) const
917 {
918 if (type == GEOMETRIC_BBOX) {
919 return documentGeometricBounds();
920 } else {
921 return documentVisualBounds();
922 }
923 }
924
desktopGeometricBounds() const925 Geom::OptRect SPItem::desktopGeometricBounds() const
926 {
927 return geometricBounds(i2dt_affine());
928 }
929
desktopVisualBounds() const930 Geom::OptRect SPItem::desktopVisualBounds() const
931 {
932 Geom::OptRect ret = documentVisualBounds();
933 if (ret) {
934 *ret *= document->doc2dt();
935 }
936 return ret;
937 }
938
desktopPreferredBounds() const939 Geom::OptRect SPItem::desktopPreferredBounds() const
940 {
941 if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) {
942 return desktopBounds(SPItem::VISUAL_BBOX);
943 } else {
944 return desktopBounds(SPItem::GEOMETRIC_BBOX);
945 }
946 }
947
desktopBounds(BBoxType type) const948 Geom::OptRect SPItem::desktopBounds(BBoxType type) const
949 {
950 if (type == GEOMETRIC_BBOX) {
951 return desktopGeometricBounds();
952 } else {
953 return desktopVisualBounds();
954 }
955 }
956
pos_in_parent() const957 unsigned int SPItem::pos_in_parent() const {
958 g_assert(parent != nullptr);
959 g_assert(SP_IS_OBJECT(parent));
960
961 unsigned int pos = 0;
962
963 for (auto& iter: parent->children) {
964 if (&iter == this) {
965 return pos;
966 }
967
968 if (dynamic_cast<SPItem *>(&iter)) {
969 pos++;
970 }
971 }
972
973 g_assert_not_reached();
974 return 0;
975 }
976
977 // CPPIFY: make pure virtual, see below!
snappoints(std::vector<Inkscape::SnapCandidatePoint> &,Inkscape::SnapPreferences const *) const978 void SPItem::snappoints(std::vector<Inkscape::SnapCandidatePoint> & /*p*/, Inkscape::SnapPreferences const */*snapprefs*/) const {
979 //throw;
980 }
981 /* This will only be called if the derived class doesn't override this.
982 * see for example sp_genericellipse_snappoints in sp-ellipse.cpp
983 * We don't know what shape we could be dealing with here, so we'll just
984 * do nothing
985 */
986
getSnappoints(std::vector<Inkscape::SnapCandidatePoint> & p,Inkscape::SnapPreferences const * snapprefs) const987 void SPItem::getSnappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const
988 {
989 // Get the snappoints of the item
990 // CPPIFY
991 //this->snappoints(p, snapprefs);
992 const_cast<SPItem*>(this)->snappoints(p, snapprefs);
993
994 // Get the snappoints at the item's center
995 if (snapprefs != nullptr && snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_ROTATION_CENTER)) {
996 p.emplace_back(getCenter(), Inkscape::SNAPSOURCE_ROTATION_CENTER, Inkscape::SNAPTARGET_ROTATION_CENTER);
997 }
998
999 // Get the snappoints of clipping paths and mask, if any
1000 std::list<SPObject const *> clips_and_masks;
1001
1002 if (clip_ref) clips_and_masks.push_back(clip_ref->getObject());
1003 if (mask_ref) clips_and_masks.push_back(mask_ref->getObject());
1004
1005 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
1006 for (std::list<SPObject const *>::const_iterator o = clips_and_masks.begin(); o != clips_and_masks.end(); ++o) {
1007 if (*o) {
1008 // obj is a group object, the children are the actual clippers
1009 for(auto& child: (*o)->children) {
1010 SPItem *item = dynamic_cast<SPItem *>(const_cast<SPObject*>(&child));
1011 if (item) {
1012 std::vector<Inkscape::SnapCandidatePoint> p_clip_or_mask;
1013 // Please note the recursive call here!
1014 item->getSnappoints(p_clip_or_mask, snapprefs);
1015 // Take into account the transformation of the item being clipped or masked
1016 for (const auto & p_orig : p_clip_or_mask) {
1017 // All snappoints are in desktop coordinates, but the item's transformation is
1018 // in document coordinates. Hence the awkward construction below
1019 Geom::Point pt = desktop->dt2doc(p_orig.getPoint()) * i2dt_affine();
1020 p.emplace_back(pt, p_orig.getSourceType(), p_orig.getTargetType());
1021 }
1022 }
1023 }
1024 }
1025 }
1026 }
1027
1028 // CPPIFY: make pure virtual
print(SPPrintContext *)1029 void SPItem::print(SPPrintContext* /*ctx*/) {
1030 //throw;
1031 }
1032
invoke_print(SPPrintContext * ctx)1033 void SPItem::invoke_print(SPPrintContext *ctx)
1034 {
1035 if ( !isHidden() ) {
1036 if (!transform.isIdentity() || style->opacity.value != SP_SCALE24_MAX) {
1037 ctx->bind(transform, SP_SCALE24_TO_FLOAT(style->opacity.value));
1038 this->print(ctx);
1039 ctx->release();
1040 } else {
1041 this->print(ctx);
1042 }
1043 }
1044 }
1045
displayName() const1046 const char* SPItem::displayName() const {
1047 return _("Object");
1048 }
1049
description() const1050 gchar* SPItem::description() const {
1051 return g_strdup("");
1052 }
1053
detailedDescription() const1054 gchar *SPItem::detailedDescription() const {
1055 gchar* s = g_strdup_printf("<b>%s</b> %s",
1056 this->displayName(), this->description());
1057
1058 if (s && clip_ref && clip_ref->getObject()) {
1059 gchar *snew = g_strdup_printf (_("%s; <i>clipped</i>"), s);
1060 g_free (s);
1061 s = snew;
1062 }
1063
1064 if (s && mask_ref && mask_ref->getObject()) {
1065 gchar *snew = g_strdup_printf (_("%s; <i>masked</i>"), s);
1066 g_free (s);
1067 s = snew;
1068 }
1069
1070 if ( style && style->filter.href && style->filter.href->getObject() ) {
1071 const gchar *label = style->filter.href->getObject()->label();
1072 gchar *snew = nullptr;
1073
1074 if (label) {
1075 snew = g_strdup_printf (_("%s; <i>filtered (%s)</i>"), s, _(label));
1076 } else {
1077 snew = g_strdup_printf (_("%s; <i>filtered</i>"), s);
1078 }
1079
1080 g_free (s);
1081 s = snew;
1082 }
1083
1084 return s;
1085 }
1086
isFiltered() const1087 bool SPItem::isFiltered() const {
1088 return (style && style->filter.href && style->filter.href->getObject());
1089 }
1090
1091
isInMask() const1092 SPObject* SPItem::isInMask() const {
1093 SPObject* parent = this->parent;
1094 while (parent && !dynamic_cast<SPMask *>(parent)) {
1095 parent = parent->parent;
1096 }
1097 return parent;
1098 }
1099
isInClipPath() const1100 SPObject* SPItem::isInClipPath() const {
1101 SPObject* parent = this->parent;
1102 while (parent && !dynamic_cast<SPClipPath *>(parent)) {
1103 parent = parent->parent;
1104 }
1105 return parent;
1106 }
1107
display_key_new(unsigned numkeys)1108 unsigned SPItem::display_key_new(unsigned numkeys)
1109 {
1110 static unsigned dkey = 0;
1111
1112 dkey += numkeys;
1113
1114 return dkey - numkeys;
1115 }
1116
1117 // CPPIFY: make pure virtual
show(Inkscape::Drawing &,unsigned int,unsigned int)1118 Inkscape::DrawingItem* SPItem::show(Inkscape::Drawing& /*drawing*/, unsigned int /*key*/, unsigned int /*flags*/) {
1119 //throw;
1120 return nullptr;
1121 }
1122
invoke_show(Inkscape::Drawing & drawing,unsigned key,unsigned flags)1123 Inkscape::DrawingItem *SPItem::invoke_show(Inkscape::Drawing &drawing, unsigned key, unsigned flags)
1124 {
1125 Inkscape::DrawingItem *ai = nullptr;
1126
1127 ai = this->show(drawing, key, flags);
1128
1129 if (ai != nullptr) {
1130 Geom::OptRect item_bbox = geometricBounds();
1131
1132 display = sp_item_view_new_prepend(display, this, flags, key, ai);
1133 ai->setTransform(transform);
1134 ai->setOpacity(SP_SCALE24_TO_FLOAT(style->opacity.value));
1135 ai->setIsolation( style->isolation.value );
1136 ai->setBlendMode( style->mix_blend_mode.value );
1137 //ai->setCompositeOperator( style->composite_op.value );
1138 ai->setVisible(!isHidden());
1139 ai->setSensitive(sensitive);
1140 if (clip_ref && clip_ref->getObject()) {
1141 SPClipPath *cp = clip_ref->getObject();
1142
1143 if (!display->arenaitem->key()) {
1144 display->arenaitem->setKey(display_key_new(3));
1145 }
1146 int clip_key = display->arenaitem->key();
1147
1148 // Show and set clip
1149 Inkscape::DrawingItem *ac = cp->show(drawing, clip_key);
1150 ai->setClip(ac);
1151
1152 // Update bbox, in case the clip uses bbox units
1153 cp->setBBox(clip_key, item_bbox);
1154 cp->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1155 }
1156 if (mask_ref && mask_ref->getObject()) {
1157 SPMask *mask = mask_ref->getObject();
1158
1159 if (!display->arenaitem->key()) {
1160 display->arenaitem->setKey(display_key_new(3));
1161 }
1162 int mask_key = display->arenaitem->key();
1163
1164 // Show and set mask
1165 Inkscape::DrawingItem *ac = mask->sp_mask_show(drawing, mask_key);
1166 ai->setMask(ac);
1167
1168 // Update bbox, in case the mask uses bbox units
1169 mask->sp_mask_set_bbox(mask_key, item_bbox);
1170 mask->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1171 }
1172
1173 SPPaintServer *fill_ps = style->getFillPaintServer();
1174 if (fill_ps) {
1175 if (!display->arenaitem->key()) {
1176 display->arenaitem->setKey(display_key_new(3));
1177 }
1178 int fill_key = display->arenaitem->key();
1179
1180 Inkscape::DrawingPattern *ap = fill_ps->show(drawing, fill_key, item_bbox);
1181 ai->setFillPattern(ap);
1182 if (ap) {
1183 fill_ps->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1184 }
1185 }
1186 SPPaintServer *stroke_ps = style->getStrokePaintServer();
1187 if (stroke_ps) {
1188 if (!display->arenaitem->key()) {
1189 display->arenaitem->setKey(display_key_new(3));
1190 }
1191 int stroke_key = display->arenaitem->key();
1192
1193 Inkscape::DrawingPattern *ap = stroke_ps->show(drawing, stroke_key, item_bbox);
1194 ai->setStrokePattern(ap);
1195 if (ap) {
1196 stroke_ps->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1197 }
1198 }
1199 ai->setItem(this);
1200 ai->setItemBounds(geometricBounds());
1201 }
1202
1203 return ai;
1204 }
1205
1206 // CPPIFY: make pure virtual
hide(unsigned int)1207 void SPItem::hide(unsigned int /*key*/) {
1208 //throw;
1209 }
1210
invoke_hide(unsigned key)1211 void SPItem::invoke_hide(unsigned key)
1212 {
1213 this->hide(key);
1214
1215 SPItemView *ref = nullptr;
1216 SPItemView *v = display;
1217 while (v != nullptr) {
1218 SPItemView *next = v->next;
1219 if (v->key == key) {
1220 if (clip_ref && clip_ref->getObject()) {
1221 (clip_ref->getObject())->hide(v->arenaitem->key());
1222 v->arenaitem->setClip(nullptr);
1223 }
1224 if (mask_ref && mask_ref->getObject()) {
1225 mask_ref->getObject()->sp_mask_hide(v->arenaitem->key());
1226 v->arenaitem->setMask(nullptr);
1227 }
1228 SPPaintServer *fill_ps = style->getFillPaintServer();
1229 if (fill_ps) {
1230 fill_ps->hide(v->arenaitem->key());
1231 }
1232 SPPaintServer *stroke_ps = style->getStrokePaintServer();
1233 if (stroke_ps) {
1234 stroke_ps->hide(v->arenaitem->key());
1235 }
1236 if (!ref) {
1237 display = v->next;
1238 } else {
1239 ref->next = v->next;
1240 }
1241 delete v->arenaitem;
1242 g_free(v);
1243 } else {
1244 ref = v;
1245 }
1246 v = next;
1247 }
1248 }
1249
1250 // Adjusters
1251
adjust_pattern(Geom::Affine const & postmul,bool set,PaintServerTransform pt)1252 void SPItem::adjust_pattern(Geom::Affine const &postmul, bool set, PaintServerTransform pt)
1253 {
1254 bool fill = (pt == TRANSFORM_FILL || pt == TRANSFORM_BOTH);
1255 if (fill && style && (style->fill.isPaintserver())) {
1256 SPObject *server = style->getFillPaintServer();
1257 SPPattern *serverPatt = dynamic_cast<SPPattern *>(server);
1258 if ( serverPatt ) {
1259 SPPattern *pattern = serverPatt->clone_if_necessary(this, "fill");
1260 pattern->transform_multiply(postmul, set);
1261 }
1262 }
1263
1264 bool stroke = (pt == TRANSFORM_STROKE || pt == TRANSFORM_BOTH);
1265 if (stroke && style && (style->stroke.isPaintserver())) {
1266 SPObject *server = style->getStrokePaintServer();
1267 SPPattern *serverPatt = dynamic_cast<SPPattern *>(server);
1268 if ( serverPatt ) {
1269 SPPattern *pattern = serverPatt->clone_if_necessary(this, "stroke");
1270 pattern->transform_multiply(postmul, set);
1271 }
1272 }
1273 }
1274
adjust_hatch(Geom::Affine const & postmul,bool set,PaintServerTransform pt)1275 void SPItem::adjust_hatch(Geom::Affine const &postmul, bool set, PaintServerTransform pt)
1276 {
1277 bool fill = (pt == TRANSFORM_FILL || pt == TRANSFORM_BOTH);
1278 if (fill && style && (style->fill.isPaintserver())) {
1279 SPObject *server = style->getFillPaintServer();
1280 SPHatch *serverHatch = dynamic_cast<SPHatch *>(server);
1281 if (serverHatch) {
1282 SPHatch *hatch = serverHatch->clone_if_necessary(this, "fill");
1283 hatch->transform_multiply(postmul, set);
1284 }
1285 }
1286
1287 bool stroke = (pt == TRANSFORM_STROKE || pt == TRANSFORM_BOTH);
1288 if (stroke && style && (style->stroke.isPaintserver())) {
1289 SPObject *server = style->getStrokePaintServer();
1290 SPHatch *serverHatch = dynamic_cast<SPHatch *>(server);
1291 if (serverHatch) {
1292 SPHatch *hatch = serverHatch->clone_if_necessary(this, "stroke");
1293 hatch->transform_multiply(postmul, set);
1294 }
1295 }
1296 }
1297
adjust_gradient(Geom::Affine const & postmul,bool set)1298 void SPItem::adjust_gradient( Geom::Affine const &postmul, bool set )
1299 {
1300 if ( style && style->fill.isPaintserver() ) {
1301 SPPaintServer *server = style->getFillPaintServer();
1302 SPGradient *serverGrad = dynamic_cast<SPGradient *>(server);
1303 if ( serverGrad ) {
1304
1305 /**
1306 * \note Bbox units for a gradient are generally a bad idea because
1307 * with them, you cannot preserve the relative position of the
1308 * object and its gradient after rotation or skew. So now we
1309 * convert them to userspace units which are easy to keep in sync
1310 * just by adding the object's transform to gradientTransform.
1311 * \todo FIXME: convert back to bbox units after transforming with
1312 * the item, so as to preserve the original units.
1313 */
1314 SPGradient *gradient = sp_gradient_convert_to_userspace( serverGrad, this, "fill" );
1315
1316 sp_gradient_transform_multiply( gradient, postmul, set );
1317 }
1318 }
1319
1320 if ( style && style->stroke.isPaintserver() ) {
1321 SPPaintServer *server = style->getStrokePaintServer();
1322 SPGradient *serverGrad = dynamic_cast<SPGradient *>(server);
1323 if ( serverGrad ) {
1324 SPGradient *gradient = sp_gradient_convert_to_userspace( serverGrad, this, "stroke");
1325 sp_gradient_transform_multiply( gradient, postmul, set );
1326 }
1327 }
1328 }
1329
adjust_stroke(gdouble ex)1330 void SPItem::adjust_stroke( gdouble ex )
1331 {
1332 if (freeze_stroke_width) {
1333 return;
1334 }
1335
1336 SPStyle *style = this->style;
1337
1338 if (style && !Geom::are_near(ex, 1.0, Geom::EPSILON)) {
1339 style->stroke_width.computed *= ex;
1340 style->stroke_width.set = TRUE;
1341
1342 if ( !style->stroke_dasharray.values.empty() ) {
1343 for (auto & value : style->stroke_dasharray.values) {
1344 value.value *= ex;
1345 value.computed *= ex;
1346 }
1347 style->stroke_dashoffset.value *= ex;
1348 style->stroke_dashoffset.computed *= ex;
1349 }
1350
1351 updateRepr();
1352 }
1353 }
1354
1355 /**
1356 * Find out the inverse of previous transform of an item (from its repr)
1357 */
sp_item_transform_repr(SPItem * item)1358 Geom::Affine sp_item_transform_repr (SPItem *item)
1359 {
1360 Geom::Affine t_old(Geom::identity());
1361 gchar const *t_attr = item->getRepr()->attribute("transform");
1362 if (t_attr) {
1363 Geom::Affine t;
1364 if (sp_svg_transform_read(t_attr, &t)) {
1365 t_old = t;
1366 }
1367 }
1368
1369 return t_old;
1370 }
1371
1372
adjust_stroke_width_recursive(double expansion)1373 void SPItem::adjust_stroke_width_recursive(double expansion)
1374 {
1375 adjust_stroke (expansion);
1376
1377 // A clone's child is the ghost of its original - we must not touch it, skip recursion
1378 if ( !dynamic_cast<SPUse *>(this) ) {
1379 for (auto& o: children) {
1380 SPItem *item = dynamic_cast<SPItem *>(&o);
1381 if (item) {
1382 item->adjust_stroke_width_recursive(expansion);
1383 }
1384 }
1385 }
1386 }
1387
freeze_stroke_width_recursive(bool freeze)1388 void SPItem::freeze_stroke_width_recursive(bool freeze)
1389 {
1390 freeze_stroke_width = freeze;
1391
1392 // A clone's child is the ghost of its original - we must not touch it, skip recursion
1393 if ( !dynamic_cast<SPUse *>(this) ) {
1394 for (auto& o: children) {
1395 SPItem *item = dynamic_cast<SPItem *>(&o);
1396 if (item) {
1397 item->freeze_stroke_width_recursive(freeze);
1398 }
1399 }
1400 }
1401 }
1402
1403 /**
1404 * Recursively adjust rx and ry of rects.
1405 */
1406 static void
sp_item_adjust_rects_recursive(SPItem * item,Geom::Affine advertized_transform)1407 sp_item_adjust_rects_recursive(SPItem *item, Geom::Affine advertized_transform)
1408 {
1409 SPRect *rect = dynamic_cast<SPRect *>(item);
1410 if (rect) {
1411 rect->compensateRxRy(advertized_transform);
1412 }
1413
1414 for(auto& o: item->children) {
1415 SPItem *itm = dynamic_cast<SPItem *>(&o);
1416 if (itm) {
1417 sp_item_adjust_rects_recursive(itm, advertized_transform);
1418 }
1419 }
1420 }
1421
adjust_paint_recursive(Geom::Affine advertized_transform,Geom::Affine t_ancestors,PaintServerType type)1422 void SPItem::adjust_paint_recursive(Geom::Affine advertized_transform, Geom::Affine t_ancestors, PaintServerType type)
1423 {
1424 // _Before_ full pattern/gradient transform: t_paint * t_item * t_ancestors
1425 // _After_ full pattern/gradient transform: t_paint_new * t_item * t_ancestors * advertised_transform
1426 // By equating these two expressions we get t_paint_new = t_paint * paint_delta, where:
1427 Geom::Affine t_item = sp_item_transform_repr (this);
1428 Geom::Affine paint_delta = t_item * t_ancestors * advertized_transform * t_ancestors.inverse() * t_item.inverse();
1429
1430 // Within text, we do not fork gradients, and so must not recurse to avoid double compensation;
1431 // also we do not recurse into clones, because a clone's child is the ghost of its original -
1432 // we must not touch it
1433 if (!(dynamic_cast<SPText *>(this) || dynamic_cast<SPUse *>(this))) {
1434 for (auto& o: children) {
1435 SPItem *item = dynamic_cast<SPItem *>(&o);
1436 if (item) {
1437 // At the level of the transformed item, t_ancestors is identity;
1438 // below it, it is the accumulated chain of transforms from this level to the top level
1439 item->adjust_paint_recursive(advertized_transform, t_item * t_ancestors, type);
1440 }
1441 }
1442 }
1443
1444 // We recursed into children first, and are now adjusting this object second;
1445 // this is so that adjustments in a tree are done from leaves up to the root,
1446 // and paintservers on leaves inheriting their values from ancestors could adjust themselves properly
1447 // before ancestors themselves are adjusted, probably differently (bug 1286535)
1448
1449 switch (type) {
1450 case PATTERN: {
1451 adjust_pattern(paint_delta);
1452 break;
1453 }
1454 case HATCH: {
1455 adjust_hatch(paint_delta);
1456 break;
1457 }
1458 default: {
1459 adjust_gradient(paint_delta);
1460 }
1461 }
1462 }
1463
1464 // CPPIFY:: make pure virtual?
1465 // Not all SPItems must necessarily have a set transform method!
set_transform(Geom::Affine const & transform)1466 Geom::Affine SPItem::set_transform(Geom::Affine const &transform) {
1467 // throw;
1468 return transform;
1469 }
1470
doWriteTransform(Geom::Affine const & transform,Geom::Affine const * adv,bool compensate)1471 void SPItem::doWriteTransform(Geom::Affine const &transform, Geom::Affine const *adv, bool compensate)
1472 {
1473 // calculate the relative transform, if not given by the adv attribute
1474 Geom::Affine advertized_transform;
1475 if (adv != nullptr) {
1476 advertized_transform = *adv;
1477 } else {
1478 advertized_transform = sp_item_transform_repr (this).inverse() * transform;
1479 }
1480
1481 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
1482 if (compensate) {
1483 // recursively compensating for stroke scaling will not always work, because it can be scaled to zero or infinite
1484 // from which we cannot ever recover by applying an inverse scale; therefore we temporarily block any changes
1485 // to the strokewidth in such a case instead, and unblock these after the transformation
1486 // (as reported in https://bugs.launchpad.net/inkscape/+bug/825840/comments/4)
1487 if (!prefs->getBool("/options/transform/stroke", true)) {
1488 double const expansion = 1. / advertized_transform.descrim();
1489 if (expansion < 1e-9 || expansion > 1e9) {
1490 freeze_stroke_width_recursive(true);
1491 // This will only work if the item has a set_transform method (in this method adjust_stroke() will be called)
1492 // We will still have to apply the inverse scaling to other items, not having a set_transform method
1493 // such as ellipses and stars
1494 // PS: We cannot use this freeze_stroke_width_recursive() trick in all circumstances. For example, it will
1495 // break pasting objects within their group (because in such a case the transformation of the group will affect
1496 // the strokewidth, and has to be compensated for. See https://bugs.launchpad.net/inkscape/+bug/959223/comments/10)
1497 } else {
1498 adjust_stroke_width_recursive(expansion);
1499 }
1500 }
1501
1502 // recursively compensate rx/ry of a rect if requested
1503 if (!prefs->getBool("/options/transform/rectcorners", true)) {
1504 sp_item_adjust_rects_recursive(this, advertized_transform);
1505 }
1506
1507 // recursively compensate pattern fill if it's not to be transformed
1508 if (!prefs->getBool("/options/transform/pattern", true)) {
1509 adjust_paint_recursive(advertized_transform.inverse(), Geom::identity(), PATTERN);
1510 }
1511 if (!prefs->getBool("/options/transform/hatch", true)) {
1512 adjust_paint_recursive(advertized_transform.inverse(), Geom::identity(), HATCH);
1513 }
1514
1515 /// \todo FIXME: add the same else branch as for gradients below, to convert patterns to userSpaceOnUse as well
1516 /// recursively compensate gradient fill if it's not to be transformed
1517 if (!prefs->getBool("/options/transform/gradient", true)) {
1518 adjust_paint_recursive(advertized_transform.inverse(), Geom::identity(), GRADIENT);
1519 } else {
1520 // this converts the gradient/pattern fill/stroke, if any, to userSpaceOnUse; we need to do
1521 // it here _before_ the new transform is set, so as to use the pre-transform bbox
1522 adjust_paint_recursive(Geom::identity(), Geom::identity(), GRADIENT);
1523 }
1524
1525 } // endif(compensate)
1526
1527 gint preserve = prefs->getBool("/options/preservetransform/value", false);
1528 Geom::Affine transform_attr (transform);
1529
1530 // CPPIFY: check this code.
1531 // If onSetTransform is not overridden, CItem::onSetTransform will return the transform it was given as a parameter.
1532 // onSetTransform cannot be pure due to the fact that not all visible Items are transformable.
1533 SPLPEItem * lpeitem = SP_LPE_ITEM(this);
1534 if (lpeitem) {
1535 lpeitem->notifyTransform(transform);
1536 }
1537 if ( // run the object's set_transform (i.e. embed transform) only if:
1538 (dynamic_cast<SPText *>(this) && firstChild() && dynamic_cast<SPTextPath *>(firstChild())) ||
1539 (!preserve && // user did not chose to preserve all transforms
1540 (!clip_ref || !clip_ref->getObject()) && // the object does not have a clippath
1541 (!mask_ref || !mask_ref->getObject()) && // the object does not have a mask
1542 !(!transform.isTranslation() && style && style->getFilter())) // the object does not have a filter, or the transform is translation (which is supposed to not affect filters)
1543 )
1544 {
1545 transform_attr = this->set_transform(transform);
1546 }
1547 if (freeze_stroke_width) {
1548 freeze_stroke_width_recursive(false);
1549 if (compensate) {
1550 if (!prefs->getBool("/options/transform/stroke", true)) {
1551 // Recursively compensate for stroke scaling, depending on user preference
1552 // (As to why we need to do this, see the comment a few lines above near the freeze_stroke_width_recursive(true) call)
1553 double const expansion = 1. / advertized_transform.descrim();
1554 adjust_stroke_width_recursive(expansion);
1555 }
1556 }
1557 }
1558 // this avoid temporary scaling issues on display when near identity
1559 // this must be a bit grater than EPSILON * transform.descrim()
1560 double e = 1e-5 * transform.descrim();
1561 if (transform_attr.isIdentity(e)) {
1562 transform_attr = Geom::Affine();
1563 }
1564 set_item_transform(transform_attr);
1565
1566 // Note: updateRepr comes before emitting the transformed signal since
1567 // it causes clone SPUse's copy of the original object to brought up to
1568 // date with the original. Otherwise, sp_use_bbox returns incorrect
1569 // values if called in code handling the transformed signal.
1570 updateRepr();
1571
1572 if (lpeitem && lpeitem->hasPathEffectRecursive()) {
1573 sp_lpe_item_update_patheffect(lpeitem, true, false);
1574 }
1575
1576 // send the relative transform with a _transformed_signal
1577 _transformed_signal.emit(&advertized_transform, this);
1578 }
1579
1580 // CPPIFY: see below, do not make pure?
event(SPEvent *)1581 gint SPItem::event(SPEvent* /*event*/) {
1582 return FALSE;
1583 }
1584
emitEvent(SPEvent & event)1585 gint SPItem::emitEvent(SPEvent &event)
1586 {
1587 return this->event(&event);
1588 }
1589
set_item_transform(Geom::Affine const & transform_matrix)1590 void SPItem::set_item_transform(Geom::Affine const &transform_matrix)
1591 {
1592 if (!Geom::are_near(transform_matrix, transform, 1e-18)) {
1593 transform = transform_matrix;
1594 /* The SP_OBJECT_USER_MODIFIED_FLAG_B is used to mark the fact that it's only a
1595 transformation. It's apparently not used anywhere else. */
1596 requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_USER_MODIFIED_FLAG_B);
1597 sp_item_rm_unsatisfied_cns(*this);
1598 }
1599 }
1600
1601 //void SPItem::convert_to_guides() const {
1602 // // CPPIFY: If not overridden, call SPItem::convert_to_guides() const, see below!
1603 // this->convert_to_guides();
1604 //}
1605
1606
i2anc_affine(SPObject const * object,SPObject const * const ancestor)1607 Geom::Affine i2anc_affine(SPObject const *object, SPObject const *const ancestor) {
1608 Geom::Affine ret(Geom::identity());
1609 g_return_val_if_fail(object != nullptr, ret);
1610
1611 /* stop at first non-renderable ancestor */
1612 while ( object != ancestor && dynamic_cast<SPItem const *>(object) ) {
1613 SPRoot const *root = dynamic_cast<SPRoot const *>(object);
1614 if (root) {
1615 ret *= root->c2p;
1616 } else {
1617 SPItem const *item = dynamic_cast<SPItem const *>(object);
1618 g_assert(item != nullptr);
1619 ret *= item->transform;
1620 }
1621 object = object->parent;
1622 }
1623 return ret;
1624 }
1625
1626 Geom::Affine
i2i_affine(SPObject const * src,SPObject const * dest)1627 i2i_affine(SPObject const *src, SPObject const *dest) {
1628 g_return_val_if_fail(src != nullptr && dest != nullptr, Geom::identity());
1629 SPObject const *ancestor = src->nearestCommonAncestor(dest);
1630 return i2anc_affine(src, ancestor) * i2anc_affine(dest, ancestor).inverse();
1631 }
1632
getRelativeTransform(SPObject const * dest) const1633 Geom::Affine SPItem::getRelativeTransform(SPObject const *dest) const {
1634 return i2i_affine(this, dest);
1635 }
1636
i2doc_affine() const1637 Geom::Affine SPItem::i2doc_affine() const
1638 {
1639 return i2anc_affine(this, nullptr);
1640 }
1641
i2dt_affine() const1642 Geom::Affine SPItem::i2dt_affine() const
1643 {
1644 Geom::Affine ret(i2doc_affine());
1645 ret *= document->doc2dt();
1646 return ret;
1647 }
1648
1649 // TODO should be named "set_i2dt_affine"
set_i2d_affine(Geom::Affine const & i2dt)1650 void SPItem::set_i2d_affine(Geom::Affine const &i2dt)
1651 {
1652 Geom::Affine dt2p; /* desktop to item parent transform */
1653 if (parent) {
1654 dt2p = static_cast<SPItem *>(parent)->i2dt_affine().inverse();
1655 } else {
1656 dt2p = document->dt2doc();
1657 }
1658
1659 Geom::Affine const i2p( i2dt * dt2p );
1660 set_item_transform(i2p);
1661 }
1662
1663
dt2i_affine() const1664 Geom::Affine SPItem::dt2i_affine() const
1665 {
1666 /* fixme: Implement the right way (Lauris) */
1667 return i2dt_affine().inverse();
1668 }
1669
1670 /* Item views */
1671
sp_item_view_new_prepend(SPItemView * list,SPItem * item,unsigned flags,unsigned key,Inkscape::DrawingItem * drawing_item)1672 SPItemView *SPItem::sp_item_view_new_prepend(SPItemView *list, SPItem *item, unsigned flags, unsigned key, Inkscape::DrawingItem *drawing_item)
1673 {
1674 g_assert(item != nullptr);
1675 g_assert(dynamic_cast<SPItem *>(item) != nullptr);
1676 g_assert(drawing_item != nullptr);
1677
1678 SPItemView *new_view = g_new(SPItemView, 1);
1679
1680 new_view->next = list;
1681 new_view->flags = flags;
1682 new_view->key = key;
1683 new_view->arenaitem = drawing_item;
1684
1685 return new_view;
1686 }
1687
1688 static SPItemView*
sp_item_view_list_remove(SPItemView * list,SPItemView * view)1689 sp_item_view_list_remove(SPItemView *list, SPItemView *view)
1690 {
1691 SPItemView *ret = list;
1692 if (view == list) {
1693 ret = list->next;
1694 } else {
1695 SPItemView *prev;
1696 prev = list;
1697 while (prev->next != view) prev = prev->next;
1698 prev->next = view->next;
1699 }
1700
1701 delete view->arenaitem;
1702 g_free(view);
1703
1704 return ret;
1705 }
1706
get_arenaitem(unsigned key)1707 Inkscape::DrawingItem *SPItem::get_arenaitem(unsigned key)
1708 {
1709 for ( SPItemView *iv = display ; iv ; iv = iv->next ) {
1710 if ( iv->key == key ) {
1711 return iv->arenaitem;
1712 }
1713 }
1714
1715 return nullptr;
1716 }
1717
sp_item_repr_compare_position(SPItem const * first,SPItem const * second)1718 int sp_item_repr_compare_position(SPItem const *first, SPItem const *second)
1719 {
1720 return sp_repr_compare_position(first->getRepr(),
1721 second->getRepr());
1722 }
1723
sp_item_first_item_child(SPObject const * obj)1724 SPItem const *sp_item_first_item_child(SPObject const *obj)
1725 {
1726 return sp_item_first_item_child( const_cast<SPObject *>(obj) );
1727 }
1728
sp_item_first_item_child(SPObject * obj)1729 SPItem *sp_item_first_item_child(SPObject *obj)
1730 {
1731 SPItem *child = nullptr;
1732 for (auto& iter: obj->children) {
1733 SPItem *tmp = dynamic_cast<SPItem *>(&iter);
1734 if ( tmp ) {
1735 child = tmp;
1736 break;
1737 }
1738 }
1739 return child;
1740 }
1741
convert_to_guides() const1742 void SPItem::convert_to_guides() const {
1743 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
1744 int prefs_bbox = prefs->getInt("/tools/bounding_box", 0);
1745
1746 Geom::OptRect bbox = (prefs_bbox == 0) ? desktopVisualBounds() : desktopGeometricBounds();
1747 if (!bbox) {
1748 g_warning ("Cannot determine item's bounding box during conversion to guides.\n");
1749 return;
1750 }
1751
1752 std::list<std::pair<Geom::Point, Geom::Point> > pts;
1753
1754 Geom::Point A((*bbox).min());
1755 Geom::Point C((*bbox).max());
1756 Geom::Point B(A[Geom::X], C[Geom::Y]);
1757 Geom::Point D(C[Geom::X], A[Geom::Y]);
1758
1759 pts.emplace_back(A, B);
1760 pts.emplace_back(B, C);
1761 pts.emplace_back(C, D);
1762 pts.emplace_back(D, A);
1763
1764 sp_guide_pt_pairs_to_guides(document, pts);
1765 }
1766
rotate_rel(Geom::Rotate const & rotation)1767 void SPItem::rotate_rel(Geom::Rotate const &rotation)
1768 {
1769 Geom::Point center = getCenter();
1770 Geom::Translate const s(getCenter());
1771 Geom::Affine affine = Geom::Affine(s).inverse() * Geom::Affine(rotation) * Geom::Affine(s);
1772
1773 // Rotate item.
1774 set_i2d_affine(i2dt_affine() * (Geom::Affine)affine);
1775 // Use each item's own transform writer, consistent with sp_selection_apply_affine()
1776 doWriteTransform(transform);
1777
1778 // Restore the center position (it's changed because the bbox center changed)
1779 if (isCenterSet()) {
1780 setCenter(center * affine);
1781 updateRepr();
1782 }
1783 }
1784
scale_rel(Geom::Scale const & scale)1785 void SPItem::scale_rel(Geom::Scale const &scale)
1786 {
1787 Geom::OptRect bbox = desktopVisualBounds();
1788 if (bbox) {
1789 Geom::Translate const s(bbox->midpoint()); // use getCenter?
1790 set_i2d_affine(i2dt_affine() * s.inverse() * scale * s);
1791 doWriteTransform(transform);
1792 }
1793 }
1794
skew_rel(double skewX,double skewY)1795 void SPItem::skew_rel(double skewX, double skewY)
1796 {
1797 Geom::Point center = getCenter();
1798 Geom::Translate const s(getCenter());
1799
1800 Geom::Affine const skew(1, skewY, skewX, 1, 0, 0);
1801 Geom::Affine affine = Geom::Affine(s).inverse() * skew * Geom::Affine(s);
1802
1803 set_i2d_affine(i2dt_affine() * affine);
1804 doWriteTransform(transform);
1805
1806 // Restore the center position (it's changed because the bbox center changed)
1807 if (isCenterSet()) {
1808 setCenter(center * affine);
1809 updateRepr();
1810 }
1811 }
1812
move_rel(Geom::Translate const & tr)1813 void SPItem::move_rel( Geom::Translate const &tr)
1814 {
1815 set_i2d_affine(i2dt_affine() * tr);
1816
1817 doWriteTransform(transform);
1818 }
1819
1820 /*
1821 Local Variables:
1822 mode:c++
1823 c-file-style:"stroustrup"
1824 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1825 indent-tabs-mode:nil
1826 fill-column:99
1827 End:
1828 */
1829 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
1830