diff options
Diffstat (limited to 'src/object/sp-item-group.cpp')
-rw-r--r-- | src/object/sp-item-group.cpp | 1130 |
1 files changed, 1130 insertions, 0 deletions
diff --git a/src/object/sp-item-group.cpp b/src/object/sp-item-group.cpp new file mode 100644 index 0000000..188fbe7 --- /dev/null +++ b/src/object/sp-item-group.cpp @@ -0,0 +1,1130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <g> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2006 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <glibmm/i18n.h> +#include <string> + +#include "attributes.h" +#include "box3d.h" +#include "display/curve.h" +#include "display/drawing-group.h" +#include "document-undo.h" +#include "document.h" +#include "live_effects/effect.h" +#include "live_effects/lpe-clone-original.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" +#include "persp3d.h" +#include "selection-chemistry.h" +#include "sp-clippath.h" +#include "sp-defs.h" +#include "sp-desc.h" +#include "sp-flowtext.h" +#include "sp-item-transform.h" +#include "sp-mask.h" +#include "sp-offset.h" +#include "sp-path.h" +#include "sp-rect.h" +#include "sp-root.h" +#include "sp-switch.h" +#include "sp-textpath.h" +#include "sp-title.h" +#include "sp-use.h" +#include "style.h" +#include "svg/css-ostringstream.h" +#include "svg/svg.h" +#include "xml/repr.h" +#include "xml/sp-css-attr.h" + +using Inkscape::DocumentUndo; + +static void sp_group_perform_patheffect(SPGroup *group, SPGroup *top_group, Inkscape::LivePathEffect::Effect *lpe, bool write); + +SPGroup::SPGroup() : SPLPEItem(), + _insert_bottom(false), + _layer_mode(SPGroup::GROUP) +{ +} + +SPGroup::~SPGroup() = default; + +void SPGroup::build(SPDocument *document, Inkscape::XML::Node *repr) { + this->readAttr(SPAttr::INKSCAPE_GROUPMODE); + + SPLPEItem::build(document, repr); +} + +void SPGroup::release() { + if (this->_layer_mode == SPGroup::LAYER) { + this->document->removeResource("layer", this); + } + + SPLPEItem::release(); +} + +void SPGroup::child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) { + SPLPEItem::child_added(child, ref); + + SPObject *last_child = this->lastChild(); + if (last_child && last_child->getRepr() == child) { + // optimization for the common special case where the child is being added at the end + SPItem *item = dynamic_cast<SPItem *>(last_child); + if ( item ) { + /* TODO: this should be moved into SPItem somehow */ + SPItemView *v; + + for (v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingItem *ac = item->invoke_show (v->arenaitem->drawing(), v->key, v->flags); + + if (ac) { + v->arenaitem->appendChild(ac); + } + } + } + } else { // general case + SPItem *item = dynamic_cast<SPItem *>(get_child_by_repr(child)); + if ( item ) { + /* TODO: this should be moved into SPItem somehow */ + SPItemView *v; + unsigned position = item->pos_in_parent(); + + for (v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingItem *ac = item->invoke_show (v->arenaitem->drawing(), v->key, v->flags); + + if (ac) { + v->arenaitem->prependChild(ac); + ac->setZOrder(position); + } + } + } + } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/* fixme: hide (Lauris) */ + +void SPGroup::remove_child(Inkscape::XML::Node *child) { + SPLPEItem::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGroup::order_changed (Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) +{ + SPLPEItem::order_changed(child, old_ref, new_ref); + + SPItem *item = dynamic_cast<SPItem *>(get_child_by_repr(child)); + if ( item ) { + /* TODO: this should be moved into SPItem somehow */ + SPItemView *v; + unsigned position = item->pos_in_parent(); + for ( v = item->display ; v != nullptr ; v = v->next ) { + v->arenaitem->setZOrder(position); + } + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGroup::update(SPCtx *ctx, unsigned int flags) { + // std::cout << "SPGroup::update(): " << (getId()?getId():"null") << std::endl; + SPItemCtx *ictx, cctx; + + ictx = (SPItemCtx *) ctx; + cctx = *ictx; + + unsigned childflags = flags; + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector<SPObject*> l=this->childList(true, SPObject::ActionUpdate); + for(auto child : l){ + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + SPItem *item = dynamic_cast<SPItem *>(child); + if (item) { + cctx.i2doc = item->transform * ictx->i2doc; + cctx.i2vp = item->transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, childflags); + } else { + child->updateDisplay(ctx, childflags); + } + } + + sp_object_unref(child); + } + + // For a group, we need to update ourselves *after* updating children. + // this is because the group might contain shapes such as rect or ellipse, + // which recompute their equivalent path (a.k.a curve) in the update callback, + // and this is in turn used when computing bbox. + SPLPEItem::update(ctx, flags); + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *group = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem); + if( this->parent ) { + this->context_style = this->parent->context_style; + } + group->setStyle(this->style, this->context_style); + } + } +} + +void SPGroup::modified(guint flags) { + //std::cout << "SPGroup::modified(): " << (getId()?getId():"null") << std::endl; + SPLPEItem::modified(flags); + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *group = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem); + group->setStyle(this->style); + } + } + + std::vector<SPObject*> l=this->childList(true); + for(auto child : l){ + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +Inkscape::XML::Node* SPGroup::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + std::vector<Inkscape::XML::Node *> l; + + if (!repr) { + if (dynamic_cast<SPSwitch *>(this)) { + repr = xml_doc->createElement("svg:switch"); + } else { + repr = xml_doc->createElement("svg:g"); + } + } + + for (auto& child: children) { + if ( !dynamic_cast<SPTitle *>(&child) && !dynamic_cast<SPDesc *>(&child) ) { + Inkscape::XML::Node *crepr = child.updateRepr(xml_doc, nullptr, flags); + + if (crepr) { + l.push_back(crepr); + } + } + } + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( !dynamic_cast<SPTitle *>(&child) && !dynamic_cast<SPDesc *>(&child) ) { + child.updateRepr(flags); + } + } + } + + if ( flags & SP_OBJECT_WRITE_EXT ) { + const char *value; + if ( _layer_mode == SPGroup::LAYER ) { + value = "layer"; + } else if ( _layer_mode == SPGroup::MASK_HELPER ) { + value = "maskhelper"; + } else if ( flags & SP_OBJECT_WRITE_ALL ) { + value = "group"; + } else { + value = nullptr; + } + + repr->setAttribute("inkscape:groupmode", value); + } + + SPLPEItem::write(xml_doc, repr, flags); + + return repr; +} + +Geom::OptRect SPGroup::bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype) const +{ + Geom::OptRect bbox; + + // TODO CPPIFY: replace this const_cast later + std::vector<SPObject*> l = const_cast<SPGroup*>(this)->childList(false, SPObject::ActionBBox); + for(auto o : l){ + SPItem *item = dynamic_cast<SPItem *>(o); + if (item && !item->isHidden()) { + Geom::Affine const ct(item->transform * transform); + bbox |= item->bounds(bboxtype, ct); + } + } + + return bbox; +} + +void SPGroup::print(SPPrintContext *ctx) { + for(auto& child: children){ + SPObject *o = &child; + SPItem *item = dynamic_cast<SPItem *>(o); + if (item) { + item->invoke_print(ctx); + } + } +} + +const char *SPGroup::typeName() const { + switch (_layer_mode) { + case SPGroup::LAYER: + return "layer"; + case SPGroup::MASK_HELPER: + case SPGroup::GROUP: + default: + return "group"; + } +} + +const char *SPGroup::displayName() const { + switch (_layer_mode) { + case SPGroup::LAYER: + return _("Layer"); + case SPGroup::MASK_HELPER: + return _("Mask Helper"); + case SPGroup::GROUP: + default: + return _("Group"); + } +} + +gchar *SPGroup::description() const { + gint len = this->getItemCount(); + return g_strdup_printf( + ngettext(_("of <b>%d</b> object"), _("of <b>%d</b> objects"), len), len); +} + +void SPGroup::set(SPAttr key, gchar const* value) { + switch (key) { + case SPAttr::INKSCAPE_GROUPMODE: + if ( value && !strcmp(value, "layer") ) { + this->setLayerMode(SPGroup::LAYER); + } else if ( value && !strcmp(value, "maskhelper") ) { + this->setLayerMode(SPGroup::MASK_HELPER); + } else { + this->setLayerMode(SPGroup::GROUP); + } + break; + + default: + SPLPEItem::set(key, value); + break; + } +} + +Inkscape::DrawingItem *SPGroup::show (Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) { + // std::cout << "SPGroup::show(): " << (getId()?getId():"null") << std::endl; + Inkscape::DrawingGroup *ai; + + ai = new Inkscape::DrawingGroup(drawing); + ai->setPickChildren(this->effectiveLayerMode(key) == SPGroup::LAYER); + if( this->parent ) { + this->context_style = this->parent->context_style; + } + ai->setStyle(this->style, this->context_style); + + this->_showChildren(drawing, ai, key, flags); + return ai; +} + +void SPGroup::hide (unsigned int key) { + std::vector<SPObject*> l=this->childList(false, SPObject::ActionShow); + for(auto o : l){ + SPItem *item = dynamic_cast<SPItem *>(o); + if (item) { + item->invoke_hide(key); + } + } + +// SPLPEItem::onHide(key); +} + + +void SPGroup::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + for (auto& o: children) + { + SPItem const *item = dynamic_cast<SPItem const *>(&o); + if (item) { + item->getSnappoints(p, snapprefs); + } + } +} + +/** + * Helper function for ungrouping. Compensates the transform of linked items + * (clones, linked offset, text-on-path, text with shape-inside) who's source is a + * direct child of the group being ungrouped (or will be moved to a different + * group or layer). + * + * @param item An object which may be linked to `expected_source` + * @param expected_source An object who's transform attribute (but not its + * i2doc transform) will change (later) due to moving to a different group + * @param source_transform A transform which will be applied to + * `expected_source` (later) and needs to be compensated in its linked items + * + * @post item and its representation are updated + */ +static void _ungroup_compensate_source_transform(SPItem *item, SPItem const *const expected_source, + Geom::Affine const &source_transform) +{ + if (!item || item->cloned) { + return; + } + + SPItem *source = nullptr; + SPText *item_text = nullptr; + SPOffset *item_offset = nullptr; + SPUse *item_use = nullptr; + SPLPEItem *lpeitemclone = dynamic_cast<SPLPEItem *>(item); + + bool override = false; + if ((item_offset = dynamic_cast<SPOffset *>(item))) { + source = sp_offset_get_source(item_offset); + } else if ((item_text = dynamic_cast<SPText *>(item))) { + source = item_text->get_first_shape_dependency(); + } else if (auto textpath = dynamic_cast<SPTextPath *>(item)) { + item_text = dynamic_cast<SPText*>(textpath->parent); + if (!item_text) + return; + item = item_text; + source = sp_textpath_get_path_item(textpath); + } else if ((item_use = dynamic_cast<SPUse *>(item))) { + source = item_use->get_original(); + } else if (lpeitemclone && lpeitemclone->hasPathEffectOfType(Inkscape::LivePathEffect::CLONE_ORIGINAL)) { + override = true; + } + + if (source != expected_source && !override) { + return; + } + + // FIXME: constructing a transform that would fully preserve the appearance of a + // textpath if it is ungrouped with its path seems to be impossible in general + // case. E.g. if the group was squeezed, to keep the ungrouped textpath squeezed + // as well, we'll need to relink it to some "virtual" path which is inversely + // stretched relative to the actual path, and then squeeze the textpath back so it + // would both fit the actual path _and_ be squeezed as before. It's a bummer. + + auto const adv = item->transform.inverse() * source_transform * item->transform; + double const scale = source_transform.descrim(); + + if (item_text) { + item_text->_adjustFontsizeRecursive(item_text, scale); + } else if (item_offset) { + item_offset->rad *= scale; + } else if (item_use) { + item->transform = Geom::Translate(item_use->x.computed, item_use->y.computed) * item->transform; + item_use->x = 0; + item_use->y = 0; + } + + if (!item_use) { + item->adjust_stroke_width_recursive(scale); + item->adjust_paint_recursive(adv, Geom::identity(), SPItem::PATTERN); + item->adjust_paint_recursive(adv, Geom::identity(), SPItem::HATCH); + item->adjust_paint_recursive(adv, Geom::identity(), SPItem::GRADIENT); + } + + item->transform = source_transform.inverse() * item->transform; + item->updateRepr(); +} + +void sp_item_group_ungroup_handle_clones(SPItem *parent, Geom::Affine const g) +{ + // copy the list because the original may get invalidated + auto hrefListCopy = parent->hrefList; + + for (auto *cobj : hrefListCopy) { + _ungroup_compensate_source_transform(dynamic_cast<SPItem *>(cobj), parent, g); + } +} + +/* + * Get bbox of clip/mask if is a rect to fix PDF import issues + */ +Geom::OptRect bbox_on_rect_clip (SPObject *object) { + SPShape *shape = dynamic_cast<SPShape *>(object); + Geom::OptRect bbox_clip = Geom::OptRect(); + if (shape) { + auto curve = shape->curve(); + if (curve) { + Geom::PathVector pv = curve->get_pathvector(); + std::vector<Geom::Point> nodes = pv.nodes(); + if (pv.size() == 1 && nodes.size() == 4) { + if (Geom::are_near(nodes[0][Geom::X],nodes[3][Geom::X]) && + Geom::are_near(nodes[1][Geom::X],nodes[2][Geom::X]) && + Geom::are_near(nodes[0][Geom::Y],nodes[1][Geom::Y]) && + Geom::are_near(nodes[2][Geom::Y],nodes[3][Geom::Y])) + { + bbox_clip = shape->visualBounds(); + bbox_clip->expandBy(1); + } + } + } + } + return bbox_clip; +} + +/* + * Get clip and item has the same path, PDF fix + */ +bool equal_clip (SPItem *item, SPObject *clip) { + SPShape *shape = dynamic_cast<SPShape *>(item); + SPShape *shape_clip = dynamic_cast<SPShape *>(clip); + bool equal = false; + if (shape && shape_clip) { + auto filter = shape->style->getFilter(); + auto stroke = shape->style->getFillOrStroke(false); + if (!filter && (!stroke || stroke->isNone())) { + SPCurve *curve = shape->curve(); + SPCurve *curve_clip = shape_clip->curve(); + if (curve && curve_clip) { + equal = curve->is_similar(curve_clip, 0.01); + } + } + } + return equal; +} + +void +sp_item_group_ungroup (SPGroup *group, std::vector<SPItem*> &children, bool do_done) +{ + g_return_if_fail (group != nullptr); + + SPDocument *doc = group->document; + SPRoot *root = doc->getRoot(); + SPObject *defs = root->defs; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + doc->onungroup = true; + + Inkscape::XML::Node *grepr = group->getRepr(); + + g_return_if_fail (!strcmp (grepr->name(), "svg:g") + || !strcmp (grepr->name(), "svg:a") + || !strcmp (grepr->name(), "svg:switch") + || !strcmp (grepr->name(), "svg:svg")); + + // this converts the gradient/pattern fill/stroke on the group, if any, to userSpaceOnUse + group->adjust_paint_recursive(Geom::identity(), Geom::identity()); + + SPItem *pitem = dynamic_cast<SPItem *>(group->parent); + g_assert(pitem); + Inkscape::XML::Node *prepr = pitem->getRepr(); + + { + SPBox3D *box = dynamic_cast<SPBox3D *>(group); + if (box) { + group = box->convert_to_group(); + } + } + + group->removeAllPathEffects(false); + bool maskonungroup = prefs->getBool("/options/maskobject/maskonungroup", true); + bool topmost = prefs->getBool("/options/maskobject/topmost", true); + bool remove_original = prefs->getBool("/options/maskobject/remove", true); + int grouping = prefs->getInt("/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_NONE); + SPObject *clip = nullptr; + SPObject *mask = nullptr; + if (maskonungroup) { + Inkscape::ObjectSet tmp_clip_set(doc); + tmp_clip_set.add(group); + Inkscape::ObjectSet tmp_mask_set(doc); + tmp_mask_set.add(group); + auto *clip_obj = group->getClipObject(); + auto *mask_obj = group->getMaskObject(); + prefs->setBool("/options/maskobject/remove", true); + prefs->setBool("/options/maskobject/topmost", true); + prefs->setInt("/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_NONE); + if (clip_obj) { + tmp_clip_set.unsetMask(true, true, false); + tmp_clip_set.remove(group); + tmp_clip_set.group(); + clip = tmp_clip_set.singleItem(); + } + if (mask_obj) { + tmp_mask_set.unsetMask(false, true, false); + tmp_mask_set.remove(group); + tmp_mask_set.group(); + mask = tmp_mask_set.singleItem(); + } + } + /* Step 1 - generate lists of children objects */ + std::vector<Inkscape::XML::Node *> items; + std::vector<Inkscape::XML::Node *> objects; + Geom::Affine const g = i2anc_affine(group, group->parent); + + if (!g.isIdentity()) { + for (auto &child : group->children) { + if (SPItem *citem = dynamic_cast<SPItem *>(&child)) { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(citem); + if (lpeitem) { + for (auto lpe : + lpeitem->getPathEffectsOfType(Inkscape::LivePathEffect::EffectType::CLONE_ORIGINAL)) { + auto clonelpe = dynamic_cast<Inkscape::LivePathEffect::LPECloneOriginal *>(lpe); + if (clonelpe) { + SPObject *linked = clonelpe->linkeditem.getObject(); + if (linked) { + bool breakparent = false; + for (auto &child2 : group->children) { + if (dynamic_cast<SPItem *>(&child2) == linked) { + _ungroup_compensate_source_transform(citem, dynamic_cast<SPItem *>(linked), g); + breakparent = true; + break; + } + } + if (breakparent) { + break; + } + } + } + } + } + sp_item_group_ungroup_handle_clones(citem, g); + } + } + } + + for (auto& child: group->children) { + SPItem *citem = dynamic_cast<SPItem *>(&child); + if (citem) { + /* Merging of style */ + // this converts the gradient/pattern fill/stroke, if any, to userSpaceOnUse; we need to do + // it here _before_ the new transform is set, so as to use the pre-transform bbox + citem->adjust_paint_recursive(Geom::identity(), Geom::identity()); + + child.style->merge( group->style ); + /* + * fixme: We currently make no allowance for the case where child is cloned + * and the group has any style settings. + * + * (This should never occur with documents created solely with the current + * version of inkscape without using the XML editor: we usually apply group + * style changes to children rather than to the group itself.) + * + * If the group has no style settings, then style->merge() should be a no-op. Otherwise + * (i.e. if we change the child's style to compensate for its parent going away) + * then those changes will typically be reflected in any clones of child, + * whereas we'd prefer for Ungroup not to affect the visual appearance. + * + * The only way of preserving styling appearance in general is for child to + * be put into a new group -- a somewhat surprising response to an Ungroup + * command. We could add a new groupmode:transparent that would mostly + * hide the existence of such groups from the user (i.e. editing behaves as + * if the transparent group's children weren't in a group), though that's + * extra complication & maintenance burden and this case is rare. + */ + + // Merging transform + citem->transform *= g; + + child.updateRepr(); + + Inkscape::XML::Node *nrepr = child.getRepr()->duplicate(prepr->document()); + items.push_back(nrepr); + + } else { + Inkscape::XML::Node *nrepr = child.getRepr()->duplicate(prepr->document()); + objects.push_back(nrepr); + } + } + + /* Step 2 - clear group */ + // remember the position of the group + auto insert_after = group->getRepr()->prev(); + + // the group is leaving forever, no heir, clones should take note; its children however are going to reemerge + group->deleteObject(true, false); + + /* Step 3 - add nonitems */ + if (!objects.empty()) { + Inkscape::XML::Node *last_def = defs->getRepr()->lastChild(); + for (auto i=objects.rbegin();i!=objects.rend();++i) { + Inkscape::XML::Node *repr = *i; + if (!sp_repr_is_meta_element(repr)) { + defs->getRepr()->addChild(repr, last_def); + } + Inkscape::GC::release(repr); + } + } + Inkscape::ObjectSet result_mask_set(doc); + Inkscape::ObjectSet result_clip_set(doc); + Geom::OptRect bbox_clip = Geom::OptRect(); + if (clip) { // if !maskonungroup is always null + bbox_clip = bbox_on_rect_clip(clip); + } + /* Step 4 - add items */ + std::vector<SPLPEItem *> lpeitems; + for (auto *repr : items) { + // add item + prepr->addChild(repr, insert_after); + insert_after = repr; + + // fill in the children list if non-null + SPItem *item = static_cast<SPItem *>(doc->getObjectByRepr(repr)); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (item) { + if (lpeitem) { + lpeitems.push_back(lpeitem); + sp_lpe_item_enable_path_effects(lpeitem, false); + children.insert(children.begin(), item); + } else { + item->doWriteTransform(item->transform, nullptr, false); + children.insert(children.begin(), item); + item->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } else { + g_assert_not_reached(); + } + + Inkscape::GC::release(repr); + if (!lpeitem && clip && item) { // if !maskonungroup is always null + Geom::OptRect bbox_item = item->visualBounds(); + if (bbox_item && !equal_clip(item, clip)) { + if (!bbox_clip || !(*bbox_clip).contains(*bbox_item)) { + result_clip_set.add(item); + } + } + } + if (mask && item) { + result_mask_set.add(item); + } + } + + if (mask) { + result_mask_set.add(mask); + result_mask_set.setMask(false,false,true); + } + for (auto lpeitem : lpeitems) { + sp_lpe_item_enable_path_effects(lpeitem, true); + lpeitem->doWriteTransform(lpeitem->transform, nullptr, false); + lpeitem->requestModified(SP_OBJECT_MODIFIED_FLAG); + if (clip && lpeitem) { // if !maskonungroup is always null + Geom::OptRect bbox_item = lpeitem->visualBounds(); + if (bbox_item && !equal_clip(lpeitem, clip)) { + if (!bbox_clip || !(*bbox_clip).contains(*bbox_item)) { + result_clip_set.add(lpeitem); + } + } + } + } + if (clip) { // if !maskonungroup is always null + if (result_clip_set.size()) { + result_clip_set.add(clip); + result_clip_set.setMask(true,false,true); + } else { + clip->deleteObject(true, false); + } + } + prefs->setBool("/options/maskobject/remove", remove_original); // if !maskonungroup become unchanged + prefs->setBool("/options/maskobject/topmost", topmost); + prefs->setBool("/options/maskobject/grouping", grouping); + doc->onungroup = false; + if (do_done) { + DocumentUndo::done(doc, _("Ungroup"), ""); + } +} + +/* + * some API for list aspect of SPGroup + */ + +std::vector<SPItem*> sp_item_group_item_list(SPGroup * group) +{ + std::vector<SPItem*> s; + g_return_val_if_fail(group != nullptr, s); + + for (auto& o: group->children) { + if ( dynamic_cast<SPItem *>(&o) ) { + s.push_back((SPItem*)&o); + } + } + return s; +} + +SPObject *sp_item_group_get_child_by_name(SPGroup *group, SPObject *ref, const gchar *name) +{ + SPObject *child = (ref) ? ref->getNext() : group->firstChild(); + while ( child && strcmp(child->getRepr()->name(), name) ) { + child = child->getNext(); + } + return child; +} + +void SPGroup::setLayerMode(LayerMode mode) { + if ( _layer_mode != mode ) { + if ( mode == LAYER ) { + this->document->addResource("layer", this); + } else if ( _layer_mode == LAYER ) { + this->document->removeResource("layer", this); + } + _layer_mode = mode; + _updateLayerMode(); + } +} + +SPGroup::LayerMode SPGroup::layerDisplayMode(unsigned int dkey) const { + std::map<unsigned int, LayerMode>::const_iterator iter; + iter = _display_modes.find(dkey); + if ( iter != _display_modes.end() ) { + return (*iter).second; + } else { + return GROUP; + } +} + +void SPGroup::setInsertBottom(bool insertbottom) { + if ( _insert_bottom != insertbottom) { + _insert_bottom = insertbottom; + } +} + +void SPGroup::setLayerDisplayMode(unsigned int dkey, SPGroup::LayerMode mode) { + if ( layerDisplayMode(dkey) != mode ) { + _display_modes[dkey] = mode; + _updateLayerMode(dkey); + } +} + +void SPGroup::_updateLayerMode(unsigned int display_key) { + SPItemView *view; + for ( view = this->display ; view ; view = view->next ) { + if ( !display_key || view->key == display_key ) { + Inkscape::DrawingGroup *g = dynamic_cast<Inkscape::DrawingGroup *>(view->arenaitem); + if (g) { + g->setPickChildren(effectiveLayerMode(view->key) == SPGroup::LAYER); + } + } + } +} + +void SPGroup::translateChildItems(Geom::Translate const &tr) +{ + if ( hasChildren() ) { + for (auto& o: children) { + SPItem *item = dynamic_cast<SPItem *>(&o); + if ( item ) { + item->move_rel(tr); + } + } + } +} + +// Recursively (or not) scale child items around a point +void SPGroup::scaleChildItemsRec(Geom::Scale const &sc, Geom::Point const &p, bool noRecurse) +{ + if ( hasChildren() ) { + for (auto& o: children) { + if ( SPDefs *defs = dynamic_cast<SPDefs *>(&o) ) { // select symbols from defs, ignore clips, masks, patterns + for (auto& defschild: defs->children) { + SPGroup *defsgroup = dynamic_cast<SPGroup *>(&defschild); + if (defsgroup) + defsgroup->scaleChildItemsRec(sc, p, false); + } + } else if ( SPItem *item = dynamic_cast<SPItem *>(&o) ) { + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (group && !dynamic_cast<SPBox3D *>(item)) { + /* Using recursion breaks clipping because transforms are applied + in coordinates for draws but nothing in defs is changed + instead change the transform on the entire group, and the transform + is applied after any references to clipping paths. However NOT using + recursion apparently breaks as of r13544 other parts of Inkscape + involved with showing/modifying units. So offer both for use + in different contexts. + */ + if(noRecurse) { + // used for EMF import + Geom::Translate const s(p); + Geom::Affine final = s.inverse() * sc * s; + Geom::Affine tAff = item->i2dt_affine() * final; + item->set_i2d_affine(tAff); + tAff = item->transform; + // Eliminate common rounding error affecting EMF/WMF input. + // When the rounding error persists it converts the simple + // transform=scale() to transform=matrix(). + if(std::abs(tAff[4]) < 1.0e-5 && std::abs(tAff[5]) < 1.0e-5){ + tAff[4] = 0.0; + tAff[5] = 0.0; + } + item->doWriteTransform(tAff, nullptr, true); + } else { + // used for other import + SPItem *sub_item = nullptr; + if (item->getClipObject()) { + sub_item = dynamic_cast<SPItem *>(item->getClipObject()->firstChild()); + } + if (sub_item != nullptr) { + sub_item->doWriteTransform(sub_item->transform*sc, nullptr, true); + } + sub_item = nullptr; + if (item->getMaskObject()) { + sub_item = dynamic_cast<SPItem *>(item->getMaskObject()->firstChild()); + } + if (sub_item != nullptr) { + sub_item->doWriteTransform(sub_item->transform*sc, nullptr, true); + } + item->doWriteTransform(sc.inverse()*item->transform*sc, nullptr, true); + group->scaleChildItemsRec(sc, p, false); + } + } else { +// Geom::OptRect bbox = item->desktopVisualBounds(); +// if (bbox) { // test not needed, this was causing a failure to scale <circle> and <rect> in the clipboard, see LP Bug 1365451 + // Scale item + Geom::Translate const s(p); + Geom::Affine final = s.inverse() * sc * s; + + gchar const *conn_type = nullptr; + SPText *text_item = dynamic_cast<SPText *>(item); + bool is_text_path = text_item && text_item->firstChild() && dynamic_cast<SPTextPath *>(text_item->firstChild()); + if (is_text_path) { + text_item->optimizeTextpathText(); + } else { + SPFlowtext *flowText = dynamic_cast<SPFlowtext *>(item); + if (flowText) { + flowText->optimizeScaledText(); + } else { + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + // Force recalculation from perspective + box->position_set(); + } else if (item->getAttribute("inkscape:connector-type") != nullptr + && (item->getAttribute("inkscape:connection-start") == nullptr + || item->getAttribute("inkscape:connection-end") == nullptr)) { + // Remove and store connector type for transform if disconnected + conn_type = item->getAttribute("inkscape:connector-type"); + item->removeAttribute("inkscape:connector-type"); + } + } + } + + Persp3D *persp = dynamic_cast<Persp3D *>(item); + if (persp) { + persp->apply_affine_transformation(final); + } else if (is_text_path && !item->transform.isIdentity()) { + // Save and reset current transform + Geom::Affine tmp(item->transform); + item->transform = Geom::Affine(); + // Apply scale + item->set_i2d_affine(item->i2dt_affine() * sc); + item->doWriteTransform(item->transform, nullptr, true); + // Scale translation and restore original transform + tmp[4] *= sc[0]; + tmp[5] *= sc[1]; + item->doWriteTransform(tmp, nullptr, true); + } else if (dynamic_cast<SPUse *>(item)) { + // calculate the matrix we need to apply to the clone + // to cancel its induced transform from its original + Geom::Affine move = final.inverse() * item->transform * final; + item->doWriteTransform(move, &move, true); + } else { + item->doWriteTransform(item->transform*sc, nullptr, true); + } + + if (conn_type != nullptr) { + item->setAttribute("inkscape:connector-type", conn_type); + } + + if (item->isCenterSet() && !(final.isTranslation() || final.isIdentity())) { + item->scaleCenter(sc); // All coordinates have been scaled, so also the center must be scaled + item->updateRepr(); + } +// } + } + } + } + } +} + +gint SPGroup::getItemCount() const { + gint len = 0; + for (auto& child: children) { + if (dynamic_cast<SPItem const *>(&child)) { + len++; + } + } + + return len; +} + +void SPGroup::_showChildren (Inkscape::Drawing &drawing, Inkscape::DrawingItem *ai, unsigned int key, unsigned int flags) { + Inkscape::DrawingItem *ac = nullptr; + std::vector<SPObject*> l=this->childList(false, SPObject::ActionShow); + for(auto o : l){ + SPItem * child = dynamic_cast<SPItem *>(o); + if (child) { + ac = child->invoke_show (drawing, key, flags); + if (ac) { + ai->appendChild(ac); + } + } + } +} + +void SPGroup::update_patheffect(bool write) { +#ifdef GROUP_VERBOSE + g_message("sp_group_update_patheffect: %p\n", lpeitem); +#endif + std::vector<SPItem*> const item_list = sp_item_group_item_list(this); + for (auto sub_item : item_list) { + if (sub_item) { + // don't need lpe version < 1 (issue only reply on lower LPE on nested LPEs + // this doesn't happen because it's done at very first stage + // we need to be sure performed to inform lpe original bounds ok, + // if not original_bbox function fail on update groups + SPShape* sub_shape = dynamic_cast<SPShape *>(sub_item); + if (sub_shape && sub_shape->hasPathEffectRecursive()) { + sub_shape->bbox_vis_cache_is_valid = false; + sub_shape->bbox_geom_cache_is_valid = false; + } + SPLPEItem *lpe_item = dynamic_cast<SPLPEItem *>(sub_item); + if (lpe_item) { + lpe_item->update_patheffect(write); + // update satellites + if (!lpe_item->hasPathEffect()) { + if (auto classes = lpe_item->getAttribute("class")) { + auto classdata = Glib::ustring(classes); + size_t pos = classdata.find("UnoptimicedTransforms"); + if (pos != Glib::ustring::npos) { + lpe_item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + } + } + } + } + + this->resetClipPathAndMaskLPE(); + if (hasPathEffect() && pathEffectsEnabled()) { + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (lpe && lpe->isVisible()) { + lpeobj->get_lpe()->doBeforeEffect_impl(this); + sp_group_perform_patheffect(this, this, lpe, write); + lpeobj->get_lpe()->doAfterEffect_impl(this, nullptr); + } + } + } + } +} + +static void +sp_group_perform_patheffect(SPGroup *group, SPGroup *top_group, Inkscape::LivePathEffect::Effect *lpe, bool write) +{ + std::vector<SPItem*> const item_list = sp_item_group_item_list(group); + for (auto sub_item : item_list) { + SPGroup *sub_group = dynamic_cast<SPGroup *>(sub_item); + if (sub_group) { + sp_group_perform_patheffect(sub_group, top_group, lpe, write); + } else { + SPShape* sub_shape = dynamic_cast<SPShape *>(sub_item); + //SPPath* sub_path = dynamic_cast<SPPath *>(sub_item); + SPItem* clipmaskto = dynamic_cast<SPItem *>(sub_item); + if (clipmaskto) { + top_group->applyToClipPath(clipmaskto, lpe); + top_group->applyToMask(clipmaskto, lpe); + } + if (sub_shape) { + auto c = SPCurve::copy(sub_shape->curve()); + bool success = false; + // only run LPEs when the shape has a curve defined + if (c) { + lpe->pathvector_before_effect = c->get_pathvector(); + c->transform(i2anc_affine(sub_shape, top_group)); + sub_shape->setCurveInsync(c.get()); + success = top_group->performOnePathEffect(c.get(), sub_shape, lpe); + c->transform(i2anc_affine(sub_shape, top_group).inverse()); + Inkscape::XML::Node *repr = sub_item->getRepr(); + if (c && success) { + sub_shape->setCurveInsync(c.get()); + if (lpe->lpeversion.param_getSVGValue() != "0") { // we are on 1 or up + sub_shape->bbox_vis_cache_is_valid = false; + sub_shape->bbox_geom_cache_is_valid = false; + } + lpe->pathvector_after_effect = c->get_pathvector(); + if (write) { + repr->setAttribute("d", sp_svg_write_path(lpe->pathvector_after_effect)); +#ifdef GROUP_VERBOSE + g_message("sp_group_perform_patheffect writes 'd' attribute"); +#endif + } + } else { + // LPE was unsuccessful or doeffect stack return null. Read the old 'd'-attribute. + if (gchar const * value = repr->attribute("d")) { + Geom::PathVector pv = sp_svg_read_pathv(value); + sub_shape->setCurve(std::make_unique<SPCurve>(pv)); + } + } + } + } + } + } + SPItem* clipmaskto = dynamic_cast<SPItem *>(group); + if (clipmaskto) { + top_group->applyToClipPath(clipmaskto, lpe); + top_group->applyToMask(clipmaskto, lpe); + } +} + + +// A list of default highlight colours to use when one isn't set. +std::vector<guint32> default_highlights; + +/** + * Generate a highlight colour if one isn't set and return it. + */ +guint32 SPGroup::highlight_color() const { + // Parent must not be a layer (root, or similar) and this group must also be a layer + if (!_highlightColor && !SP_IS_LAYER(parent) && this->_layer_mode == SPGroup::LAYER && !default_highlights.empty()) { + char const * oid = defaultLabel(); + if (oid && *oid) { + // Color based on the last few bits of the label or object id. + return default_highlights[oid[(strlen(oid) - 1)] % default_highlights.size()]; + } + } + return SPItem::highlight_color(); +} + +void set_default_highlight_colors(std::vector<guint32> colors) { + std::swap(default_highlights, colors); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : |