// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * SVG implementation */ /* * Authors: * Tomasz Boczkowski * Jon A. Cruz * * Copyright (C) 2014 Tomasz Boczkowski * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "sp-hatch.h" #include #include #include <2geom/transforms.h> #include #include "style.h" #include "attributes.h" #include "bad-uri-exception.h" #include "document.h" #include "display/drawing.h" #include "display/drawing-pattern.h" #include "sp-defs.h" #include "sp-hatch-path.h" #include "sp-item.h" #include "svg/svg.h" #include "xml/href-attribute-helper.h" SPHatch::SPHatch() : ref(nullptr), // avoiding 'this' in initializer list _hatchUnits(UNITS_OBJECTBOUNDINGBOX), _hatchUnits_set(false), _hatchContentUnits(UNITS_USERSPACEONUSE), _hatchContentUnits_set(false), _hatchTransform_set(false) { ref = new SPHatchReference(this); ref->changedSignal().connect(sigc::mem_fun(*this, &SPHatch::_onRefChanged)); // TODO check that these should start already as unset: _x.unset(); _y.unset(); _pitch.unset(); _rotate.unset(); } SPHatch::~SPHatch() = default; void SPHatch::build(SPDocument* doc, Inkscape::XML::Node* repr) { SPPaintServer::build(doc, repr); readAttr(SPAttr::HATCHUNITS); readAttr(SPAttr::HATCHCONTENTUNITS); readAttr(SPAttr::HATCHTRANSFORM); readAttr(SPAttr::X); readAttr(SPAttr::Y); readAttr(SPAttr::PITCH); readAttr(SPAttr::ROTATE); readAttr(SPAttr::XLINK_HREF); readAttr(SPAttr::STYLE); // Register ourselves doc->addResource("hatch", this); } void SPHatch::release() { if (document) { // Unregister ourselves document->removeResource("hatch", this); } auto children = hatchPaths(); for (auto &v : views) { for (auto child : children) { child->hide(v.key); } v.drawingitem.reset(); } views.clear(); if (ref) { _modified_connection.disconnect(); ref->detach(); delete ref; ref = nullptr; } SPPaintServer::release(); } void SPHatch::child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) { SPObject::child_added(child, ref); auto path_child = cast(document->getObjectByRepr(child)); if (path_child) { for (auto &v : views) { Geom::OptInterval extents = _calculateStripExtents(v.bbox); auto ac = path_child->show(v.drawingitem->drawing(), v.key, extents); path_child->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); if (ac) { v.drawingitem->prependChild(ac); } } } //FIXME: notify all hatches that refer to this child set } void SPHatch::set(SPAttr key, const gchar* value) { switch (key) { case SPAttr::HATCHUNITS: if (value) { if (!std::strcmp(value, "userSpaceOnUse")) { _hatchUnits = UNITS_USERSPACEONUSE; } else { _hatchUnits = UNITS_OBJECTBOUNDINGBOX; } _hatchUnits_set = true; } else { _hatchUnits_set = false; } requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::HATCHCONTENTUNITS: if (value) { if (!std::strcmp(value, "userSpaceOnUse")) { _hatchContentUnits = UNITS_USERSPACEONUSE; } else { _hatchContentUnits = UNITS_OBJECTBOUNDINGBOX; } _hatchContentUnits_set = true; } else { _hatchContentUnits_set = false; } requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::HATCHTRANSFORM: { Geom::Affine t; if (value && sp_svg_transform_read(value, &t)) { _hatchTransform = t; _hatchTransform_set = true; } else { _hatchTransform = Geom::identity(); _hatchTransform_set = false; } requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; } case SPAttr::X: _x.readOrUnset(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::Y: _y.readOrUnset(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::PITCH: _pitch.readOrUnset(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::ROTATE: _rotate.readOrUnset(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::XLINK_HREF: if (value && href == value) { // Href unchanged, do nothing. } else { href.clear(); if (value) { // First, set the href field; it's only used in the "unchanged" check above. href = value; // Now do the attaching, which emits the changed signal. if (value) { try { ref->attach(Inkscape::URI(value)); } catch (Inkscape::BadURIException &e) { g_warning("%s", e.what()); ref->detach(); } } else { ref->detach(); } } } requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; default: if (SP_ATTRIBUTE_IS_CSS(key)) { style->clear(key); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); } else { SPPaintServer::set(key, value); } break; } } bool SPHatch::_hasHatchPatchChildren(SPHatch const *hatch) { for (auto &child: hatch->children) { SPHatchPath const *hatchPath = cast(&child); if (hatchPath) { return true; } } return false; } std::vector SPHatch::hatchPaths() { std::vector list; SPHatch *src = chase_hrefs(this, sigc::ptr_fun(&_hasHatchPatchChildren)); if (src) { for (auto &child: src->children) { auto hatchPath = cast(&child); if (hatchPath) { list.push_back(hatchPath); } } } return list; } std::vector SPHatch::hatchPaths() const { std::vector list; SPHatch const *src = chase_hrefs(this, sigc::ptr_fun(&_hasHatchPatchChildren)); if (src) { for (auto &child: src->children) { SPHatchPath const *hatchPath = cast(&child); if (hatchPath) { list.push_back(hatchPath); } } } return list; } // TODO: ::remove_child and ::order_changed handles - see SPPattern void SPHatch::update(SPCtx* ctx, unsigned int flags) { if (flags & SP_OBJECT_MODIFIED_FLAG) { flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; } flags &= SP_OBJECT_MODIFIED_CASCADE; std::vector children(hatchPaths()); for (auto child : children) { sp_object_ref(child, nullptr); for (auto &v : views) { Geom::OptInterval strip_extents = _calculateStripExtents(v.bbox); child->setStripExtents(v.key, strip_extents); } if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { child->updateDisplay(ctx, flags); } sp_object_unref(child, nullptr); } for (auto &v : views) { _updateView(v); } } void SPHatch::modified(unsigned int flags) { if (flags & SP_OBJECT_MODIFIED_FLAG) { flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; } flags &= SP_OBJECT_MODIFIED_CASCADE; std::vector children(hatchPaths()); for (auto child : children) { sp_object_ref(child, nullptr); if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { child->emitModified(flags); } sp_object_unref(child, nullptr); } } void SPHatch::_onRefChanged(SPObject *old_ref, SPObject *ref) { if (old_ref) { _modified_connection.disconnect(); } auto hatch = cast(ref); if (hatch) { _modified_connection = ref->connectModified(sigc::mem_fun(*this, &SPHatch::_onRefModified)); } if (!_hasHatchPatchChildren(this)) { SPHatch *old_shown = nullptr; SPHatch *new_shown = nullptr; std::vector oldhatchPaths; std::vector newhatchPaths; auto old_hatch = cast(old_ref); if (old_hatch) { old_shown = old_hatch->rootHatch(); oldhatchPaths = old_shown->hatchPaths(); } if (hatch) { new_shown = hatch->rootHatch(); newhatchPaths = new_shown->hatchPaths(); } if (old_shown != new_shown) { for (auto &v : views) { Geom::OptInterval extents = _calculateStripExtents(v.bbox); for (auto child : oldhatchPaths) { child->hide(v.key); } for (auto child : newhatchPaths) { auto cai = child->show(v.drawingitem->drawing(), v.key, extents); child->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); if (cai) { v.drawingitem->appendChild(cai); } } } } } _onRefModified(ref, 0); } void SPHatch::_onRefModified(SPObject */*ref*/, guint /*flags*/) { requestModified(SP_OBJECT_MODIFIED_FLAG); // Conditional to avoid causing infinite loop if there's a cycle in the href chain. } SPHatch *SPHatch::rootHatch() { SPHatch *src = chase_hrefs(this, sigc::ptr_fun(&_hasHatchPatchChildren)); return src ? src : this; // document is broken, we can't get to root; but at least we can return pat which is supposedly a valid hatch } // Access functions that look up fields up the chain of referenced hatchs and return the first one which is set // FIXME: all of them must use chase_hrefs as children() and rootHatch() SPHatch::HatchUnits SPHatch::hatchUnits() const { HatchUnits units = _hatchUnits; for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { if (pat_i->_hatchUnits_set) { units = pat_i->_hatchUnits; break; } } return units; } SPHatch::HatchUnits SPHatch::hatchContentUnits() const { HatchUnits units = _hatchContentUnits; for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { if (pat_i->_hatchContentUnits_set) { units = pat_i->_hatchContentUnits; break; } } return units; } Geom::Affine const &SPHatch::hatchTransform() const { for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { if (pat_i->_hatchTransform_set) { return pat_i->_hatchTransform; } } return _hatchTransform; } gdouble SPHatch::x() const { gdouble val = 0; for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { if (pat_i->_x._set) { val = pat_i->_x.computed; break; } } return val; } gdouble SPHatch::y() const { gdouble val = 0; for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { if (pat_i->_y._set) { val = pat_i->_y.computed; break; } } return val; } gdouble SPHatch::pitch() const { gdouble val = 0; for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { if (pat_i->_pitch._set) { val = pat_i->_pitch.computed; break; } } return val; } gdouble SPHatch::rotate() const { gdouble val = 0; for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { if (pat_i->_rotate._set) { val = pat_i->_rotate.computed; break; } } return val; } guint SPHatch::_countHrefs(SPObject *o) const { if (!o) return 1; guint i = 0; SPStyle *style = o->style; if (style && style->fill.isPaintserver() && is(SP_STYLE_FILL_SERVER(style)) && cast(SP_STYLE_FILL_SERVER(style)) == this) { i++; } if (style && style->stroke.isPaintserver() && is(SP_STYLE_STROKE_SERVER(style)) && cast(SP_STYLE_STROKE_SERVER(style)) == this) { i++; } for (auto &child : o->children) { i += _countHrefs(&child); } return i; } SPHatch *SPHatch::clone_if_necessary(SPItem *item, const gchar *property) { SPHatch *hatch = this; if (hatch->href.empty() || hatch->hrefcount > _countHrefs(item)) { Inkscape::XML::Document *xml_doc = document->getReprDoc(); Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); Inkscape::XML::Node *repr = xml_doc->createElement("svg:hatch"); repr->setAttribute("inkscape:collect", "always"); Glib::ustring parent_ref = Glib::ustring::compose("#%1", getRepr()->attribute("id")); Inkscape::setHrefAttribute(*repr, parent_ref); defsrepr->addChild(repr, nullptr); const gchar *child_id = repr->attribute("id"); SPObject *child = document->getObjectById(child_id); g_assert(is(child)); hatch = cast(child); Glib::ustring href = Glib::ustring::compose("url(#%1)", hatch->getRepr()->attribute("id")); SPCSSAttr *css = sp_repr_css_attr_new(); sp_repr_css_set_property(css, property, href.c_str()); sp_repr_css_change_recursive(item->getRepr(), css, "style"); } return hatch; } void SPHatch::transform_multiply(Geom::Affine postmul, bool set) { if (set) { _hatchTransform = postmul; } else { _hatchTransform = hatchTransform() * postmul; } _hatchTransform_set = true; setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(_hatchTransform)); } bool SPHatch::isValid() const { bool valid = false; if (pitch() > 0) { auto children = hatchPaths(); if (!children.empty()) { valid = true; for (auto c : children) { valid = c->isValid(); if (!valid) { break; } } } } return valid; } Inkscape::DrawingPattern *SPHatch::show(Inkscape::Drawing &drawing, unsigned key, Geom::OptRect const &bbox) { views.emplace_back(make_drawingitem(drawing), bbox, key); auto &v = views.back(); auto ai = v.drawingitem.get(); auto children = hatchPaths(); Geom::OptInterval extents = _calculateStripExtents(bbox); for (auto child : children) { Inkscape::DrawingItem *cai = child->show(drawing, key, extents); if (cai) { ai->appendChild(cai); } } _updateView(v); return ai; } void SPHatch::hide(unsigned int key) { std::vector children(hatchPaths()); for (auto child : children) { child->hide(key); } auto it = std::find_if(views.begin(), views.end(), [=] (auto &v) { return v.key == key; }); if (it != views.end()) { views.erase(it); return; } g_assert_not_reached(); } Geom::Interval SPHatch::bounds() const { Geom::Interval result; auto children = hatchPaths(); for (auto child : children) { if (result.extent() == 0) { result = child->bounds(); } else { result |= child->bounds(); } } return result; } SPHatch::RenderInfo SPHatch::calculateRenderInfo(unsigned key) const { RenderInfo info; for (auto const &v : views) { if (v.key == key) { return _calculateRenderInfo(v); } } g_assert_not_reached(); return info; } void SPHatch::_updateView(View &view) { RenderInfo info = _calculateRenderInfo(view); //The rendering of hatch overflow is implemented by repeated drawing //of hatch paths over one strip. Within each iteration paths are moved by pitch value. //The movement progresses from right to left. This gives the same result //as drawing whole strips in left-to-right order. view.drawingitem->setChildTransform(info.child_transform); view.drawingitem->setPatternToUserTransform(info.pattern_to_user_transform); view.drawingitem->setTileRect(info.tile_rect); view.drawingitem->setStyle(style); view.drawingitem->setOverflow(info.overflow_initial_transform, info.overflow_steps, info.overflow_step_transform); } SPHatch::RenderInfo SPHatch::_calculateRenderInfo(View const &view) const { RenderInfo info; Geom::OptInterval extents = _calculateStripExtents(view.bbox); if (extents) { double tile_x = x(); double tile_y = y(); double tile_width = pitch(); double tile_height = extents->max() - extents->min(); double tile_rotate = rotate(); double tile_render_y = extents->min(); if (view.bbox && (hatchUnits() == UNITS_OBJECTBOUNDINGBOX)) { tile_x *= view.bbox->width(); tile_y *= view.bbox->height(); tile_width *= view.bbox->width(); } // Extent calculated using content units, need to correct. if (view.bbox && (hatchContentUnits() == UNITS_OBJECTBOUNDINGBOX)) { tile_height *= view.bbox->height(); tile_render_y *= view.bbox->height(); } // Pattern size in hatch space Geom::Rect hatch_tile = Geom::Rect::from_xywh(0, tile_render_y, tile_width, tile_height); // Content to bbox Geom::Affine content2ps; if (view.bbox && (hatchContentUnits() == UNITS_OBJECTBOUNDINGBOX)) { content2ps = Geom::Affine(view.bbox->width(), 0.0, 0.0, view.bbox->height(), 0, 0); } // Tile (hatch space) to user. Geom::Affine ps2user = Geom::Translate(tile_x, tile_y) * Geom::Rotate::from_degrees(tile_rotate) * hatchTransform(); info.child_transform = content2ps; info.pattern_to_user_transform = ps2user; info.tile_rect = hatch_tile; if (style->overflow.computed == SP_CSS_OVERFLOW_VISIBLE) { Geom::Interval bounds = this->bounds(); gdouble pitch = this->pitch(); if (view.bbox) { if (hatchUnits() == UNITS_OBJECTBOUNDINGBOX) { pitch *= view.bbox->width(); } if (hatchContentUnits() == UNITS_OBJECTBOUNDINGBOX) { bounds *= view.bbox->width(); } } gdouble overflow_right_strip = floor(bounds.max() / pitch) * pitch; info.overflow_steps = ceil((overflow_right_strip - bounds.min()) / pitch) + 1; info.overflow_step_transform = Geom::Translate(pitch, 0.0); info.overflow_initial_transform = Geom::Translate(-overflow_right_strip, 0.0); } else { info.overflow_steps = 1; } } return info; } //calculates strip extents in content space Geom::OptInterval SPHatch::_calculateStripExtents(Geom::OptRect const &bbox) const { if (!bbox || (bbox->area() == 0)) { return Geom::OptInterval(); } else { double tile_x = x(); double tile_y = y(); double tile_rotate = rotate(); Geom::Affine ps2user = Geom::Translate(tile_x, tile_y) * Geom::Rotate::from_degrees(tile_rotate) * hatchTransform(); Geom::Affine user2ps = ps2user.inverse(); Geom::Interval extents; for (int i = 0; i < 4; ++i) { Geom::Point corner = bbox->corner(i); Geom::Point corner_ps = corner * user2ps; if (i == 0 || corner_ps.y() < extents.min()) { extents.setMin(corner_ps.y()); } if (i == 0 || corner_ps.y() > extents.max()) { extents.setMax(corner_ps.y()); } } if (hatchContentUnits() == UNITS_OBJECTBOUNDINGBOX) { extents /= bbox->height(); } return extents; } } void SPHatch::setBBox(unsigned int key, Geom::OptRect const &bbox) { for (auto &v : views) { if (v.key == key) { v.bbox = bbox; break; } } } SPHatch::View::View(DrawingItemPtr drawingitem, Geom::OptRect const &bbox, unsigned key) : drawingitem(std::move(drawingitem)) , bbox(bbox) , key(key) {} /* 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:fileencoding=utf-8:textwidth=99 :