diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
commit | c853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch) | |
tree | 7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/object | |
parent | Initial commit. (diff) | |
download | inkscape-upstream.tar.xz inkscape-upstream.zip |
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
234 files changed, 59323 insertions, 0 deletions
diff --git a/src/object-hierarchy.cpp b/src/object-hierarchy.cpp new file mode 100644 index 0000000..c05bb6f --- /dev/null +++ b/src/object-hierarchy.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Object hierarchy implementation. + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstdio> + +#include "object-hierarchy.h" + +#include "object/sp-object.h" + +namespace Inkscape { + +ObjectHierarchy::ObjectHierarchy(SPObject *top) { + if (top) { + _addBottom(top); + } +} + +ObjectHierarchy::~ObjectHierarchy() { + _clear(); +} + +void ObjectHierarchy::clear() { + _clear(); + _changed_signal.emit(nullptr, nullptr); +} + +void ObjectHierarchy::setTop(SPObject *object) { + if (object == nullptr) { printf("Assertion object != NULL failed\n"); return; } + + if ( top() == object ) { + return; + } + + if (!top()) { + _addTop(object); + } else if (object->isAncestorOf(top())) { + _addTop(object, top()); + } else if ( object == bottom() || object->isAncestorOf(bottom()) ) { + _trimAbove(object); + } else { + _clear(); + _addTop(object); + } + + _changed_signal.emit(top(), bottom()); +} + +void ObjectHierarchy::_addTop(SPObject *senior, SPObject *junior) { + assert(junior != NULL); + assert(senior != NULL); + + SPObject *object = junior->parent; + do { + _addTop(object); + object = object->parent; + } while ( object != senior ); +} + +void ObjectHierarchy::_addTop(SPObject *object) { + assert(object != NULL); + _hierarchy.push_back(_attach(object)); + _added_signal.emit(object); +} + +void ObjectHierarchy::_trimAbove(SPObject *limit) { + while ( !_hierarchy.empty() && _hierarchy.back().object != limit ) { + SPObject *object=_hierarchy.back().object; + + sp_object_ref(object, nullptr); + _detach(_hierarchy.back()); + _hierarchy.pop_back(); + _removed_signal.emit(object); + sp_object_unref(object, nullptr); + } +} + +void ObjectHierarchy::setBottom(SPObject *object) { + if (object == nullptr) { printf("assertion object != NULL failed\n"); return; } + + if ( bottom() == object ) { + return; + } + + if (!top()) { + _addBottom(object); + } else if (bottom()->isAncestorOf(object)) { + _addBottom(bottom(), object); + } else if ( top() == object ) { + _trimBelow(top()); + } else if (top()->isAncestorOf(object)) { + if (object->isAncestorOf(bottom())) { + _trimBelow(object); + } else { // object is a sibling or cousin of bottom() + SPObject *saved_top=top(); + sp_object_ref(saved_top, nullptr); + _clear(); + _addBottom(saved_top); + _addBottom(saved_top, object); + sp_object_unref(saved_top, nullptr); + } + } else { + _clear(); + _addBottom(object); + } + + _changed_signal.emit(top(), bottom()); +} + +void ObjectHierarchy::_trimBelow(SPObject *limit) { + while ( !_hierarchy.empty() && _hierarchy.front().object != limit ) { + SPObject *object=_hierarchy.front().object; + sp_object_ref(object, nullptr); + _detach(_hierarchy.front()); + _hierarchy.pop_front(); + _removed_signal.emit(object); + sp_object_unref(object, nullptr); + } +} + +void ObjectHierarchy::_addBottom(SPObject *senior, SPObject *junior) { + assert(junior != NULL); + assert(senior != NULL); + + if ( junior != senior ) { + _addBottom(senior, junior->parent); + _addBottom(junior); + } +} + +void ObjectHierarchy::_addBottom(SPObject *object) { + assert(object != NULL); + _hierarchy.push_front(_attach(object)); + _added_signal.emit(object); +} + +void ObjectHierarchy::_trim_for_release(SPObject *object) { + this->_trimBelow(object); + assert(!this->_hierarchy.empty()); + assert(this->_hierarchy.front().object == object); + + sp_object_ref(object, nullptr); + this->_detach(this->_hierarchy.front()); + this->_hierarchy.pop_front(); + this->_removed_signal.emit(object); + sp_object_unref(object, nullptr); + + this->_changed_signal.emit(this->top(), this->bottom()); +} + +ObjectHierarchy::Record ObjectHierarchy::_attach(SPObject *object) { + sp_object_ref(object, nullptr); + sigc::connection connection + = object->connectRelease( + sigc::mem_fun(*this, &ObjectHierarchy::_trim_for_release) + ); + return Record(object, connection); +} + +void ObjectHierarchy::_detach(ObjectHierarchy::Record &rec) { + rec.connection.disconnect(); + sp_object_unref(rec.object, nullptr); +} + +} + +/* + 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 : diff --git a/src/object-hierarchy.h b/src/object-hierarchy.h new file mode 100644 index 0000000..3e5049f --- /dev/null +++ b/src/object-hierarchy.h @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_OBJECT_HIERARCHY_H +#define SEEN_INKSCAPE_OBJECT_HIERARCHY_H + +#include <cstddef> +#include <exception> +#include <list> +#include <sigc++/connection.h> +#include <sigc++/signal.h> + +class SPObject; + +namespace Inkscape { + +/** + * An Inkscape::ObjectHierarchy is useful for situations where one wishes + * to keep a reference to an SPObject, but fall back on one of its ancestors + * when that object is removed. + * + * That cannot be accomplished simply by hooking the "release" signal of the + * SPObject, as by the time that signal is emitted, the object's parent + * field has already been cleared. + * + * There are also some subtle refcounting issues to take into account. + * + * @see SPObject + */ +class ObjectHierarchy { +public: + + /** + * Create new object hierarchy. + * @param top The first entry if non-NULL. + */ + ObjectHierarchy(SPObject *top=nullptr); + + ~ObjectHierarchy(); + + bool contains(SPObject *object); + + sigc::connection connectAdded(const sigc::slot<void (SPObject *)> &slot) { + return _added_signal.connect(slot); + } + sigc::connection connectRemoved(const sigc::slot<void (SPObject *)> &slot) { + return _removed_signal.connect(slot); + } + sigc::connection connectChanged(const sigc::slot<void (SPObject *, SPObject *)> &slot) + { + return _changed_signal.connect(slot); + } + + /** + * Remove all entries. + */ + void clear(); + + SPObject *top() { + return !_hierarchy.empty() ? _hierarchy.back().object : nullptr; + } + + /** + * Trim or expand hierarchy on top such that object becomes top entry. + */ + void setTop(SPObject *object); + + SPObject *bottom() { + return !_hierarchy.empty() ? _hierarchy.front().object : nullptr; + } + + /** + * Trim or expand hierarchy at bottom such that object becomes bottom entry. + */ + void setBottom(SPObject *object); + +private: + struct Record { + Record(SPObject *o, sigc::connection c) + : object(o), connection(c) {} + + SPObject *object; + sigc::connection connection; + }; + + ObjectHierarchy(ObjectHierarchy const &); // no copy + + void operator=(ObjectHierarchy const &); // no assign + + /** + * Add hierarchy from junior's parent to senior to this + * hierarchy's top. + */ + void _addTop(SPObject *senior, SPObject *junior); + + /** + * Add object to top of hierarchy. + * \pre object!=NULL. + */ + void _addTop(SPObject *object); + + /** + * Remove all objects above limit from hierarchy. + */ + void _trimAbove(SPObject *limit); + + /** + * Add hierarchy from senior to junior, in range (senior, junior], to this hierarchy's bottom. + */ + void _addBottom(SPObject *senior, SPObject *junior); + + /** + * Add object at bottom of hierarchy. + * \pre object!=NULL + */ + void _addBottom(SPObject *object); + + /** + * Remove all objects under given object. + * @param limit If NULL, remove all. + */ + void _trimBelow(SPObject *limit); + + Record _attach(SPObject *object); + + void _detach(Record &record); + + void _clear() { _trimBelow(nullptr); } + + void _trim_for_release(SPObject *released); + + std::list<Record> _hierarchy; + sigc::signal<void (SPObject *)> _added_signal; + sigc::signal<void (SPObject *)> _removed_signal; + sigc::signal<void (SPObject *, SPObject *)> _changed_signal; +}; + +} + +#endif +/* + 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 : diff --git a/src/object-snapper.cpp b/src/object-snapper.cpp new file mode 100644 index 0000000..20cc350 --- /dev/null +++ b/src/object-snapper.cpp @@ -0,0 +1,866 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Snapping things to objects. + * + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Diederik van Lierop <mail@diedenrezi.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2005 - 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/circle.h> +#include <2geom/line.h> +#include <2geom/path-intersection.h> +#include <2geom/path-sink.h> +#include <memory> + +#include "desktop.h" +#include "display/curve.h" +#include "document.h" +#include "inkscape.h" +#include "live_effects/effect-enum.h" +#include "object/sp-clippath.h" +#include "object/sp-flowtext.h" +#include "object/sp-image.h" +#include "object/sp-item-group.h" +#include "object/sp-mask.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "object/sp-page.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-use.h" +#include "path/path-util.h" // curve_for_item +#include "preferences.h" +#include "snap-enums.h" +#include "style.h" +#include "svg/svg.h" +#include "text-editing.h" +#include "page-manager.h" + +Inkscape::ObjectSnapper::ObjectSnapper(SnapManager *sm, Geom::Coord const d) + : Snapper(sm, d) +{ + _points_to_snap_to = std::make_unique<std::vector<SnapCandidatePoint>>(); + _paths_to_snap_to = std::make_unique<std::vector<SnapCandidatePath>>(); +} + +Inkscape::ObjectSnapper::~ObjectSnapper() +{ + _points_to_snap_to->clear(); + _clear_paths(); +} + +Geom::Coord Inkscape::ObjectSnapper::getSnapperTolerance() const +{ + SPDesktop const *dt = _snapmanager->getDesktop(); + double const zoom = dt ? dt->current_zoom() : 1; + return _snapmanager->snapprefs.getObjectTolerance() / zoom; +} + +bool Inkscape::ObjectSnapper::getSnapperAlwaysSnap() const +{ + return _snapmanager->snapprefs.getObjectTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp +} + +void Inkscape::ObjectSnapper::_collectNodes(SnapSourceType const &t, + bool const &first_point) const +{ + // Now, let's first collect all points to snap to. If we have a whole bunch of points to snap, + // e.g. when translating an item using the selector tool, then we will only do this for the + // first point and store the collection for later use. This significantly improves the performance + if (first_point) { + _points_to_snap_to->clear(); + + // Determine the type of bounding box we should snap to + SPItem::BBoxType bbox_type = SPItem::GEOMETRIC_BBOX; + + bool p_is_a_node = t & SNAPSOURCE_NODE_CATEGORY; + bool p_is_a_bbox = t & SNAPSOURCE_BBOX_CATEGORY; + bool p_is_other = (t & SNAPSOURCE_OTHERS_CATEGORY) || (t & SNAPSOURCE_DATUMS_CATEGORY); + + // A point considered for snapping should be either a node, a bbox corner or a guide/other. Pick only ONE! + if (((p_is_a_node && p_is_a_bbox) || (p_is_a_bbox && p_is_other) || (p_is_a_node && p_is_other))) { + g_warning("Snap warning: node type is ambiguous"); + } + + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CORNER, SNAPTARGET_BBOX_EDGE_MIDPOINT, SNAPTARGET_BBOX_MIDPOINT)) { + Preferences *prefs = Preferences::get(); + bool prefs_bbox = prefs->getBool("/tools/bounding_box"); + bbox_type = !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + } + + // Consider the page border for snapping to + if (auto document = _snapmanager->getDocument()) { + auto ignore_page = _snapmanager->getPageToIgnore(); + for (auto page : document->getPageManager().getPages()) { + if (ignore_page == page) + continue; + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PAGE_EDGE_CORNER)) { + getBBoxPoints(page->getDesktopRect(), _points_to_snap_to.get(), true, + SNAPSOURCE_PAGE_CORNER, SNAPTARGET_PAGE_EDGE_CORNER, + SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED, // No edges + SNAPSOURCE_PAGE_CENTER, SNAPTARGET_PAGE_EDGE_CENTER); + } + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PAGE_MARGIN_CORNER)) { + getBBoxPoints(page->getDesktopMargin(), _points_to_snap_to.get(), true, + SNAPSOURCE_UNDEFINED, SNAPTARGET_PAGE_MARGIN_CORNER, + SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED, // No edges + SNAPSOURCE_UNDEFINED, SNAPTARGET_PAGE_MARGIN_CENTER); + getBBoxPoints(page->getDesktopBleed(), _points_to_snap_to.get(), true, + SNAPSOURCE_UNDEFINED, SNAPTARGET_PAGE_BLEED_CORNER, + SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED, // No edges or center + SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED); + } + } + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PAGE_EDGE_CORNER)) { + // Only the corners get added here. + getBBoxPoints(document->preferredBounds(), _points_to_snap_to.get(), false, + SNAPSOURCE_UNDEFINED, SNAPTARGET_PAGE_EDGE_CORNER, + SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED, + SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED); + } + } + + for (const auto & _candidate : *_snapmanager->_obj_snapper_candidates) { + //Geom::Affine i2doc(Geom::identity()); + SPItem *root_item = _candidate.item; + + auto use = cast<SPUse>(_candidate.item); + if (use) { + root_item = use->root(); + } + g_return_if_fail(root_item); + + //Collect all nodes so we can snap to them + if (p_is_a_node || p_is_other || (p_is_a_bbox && !_snapmanager->snapprefs.getStrictSnapping())) { + // Note: there are two ways in which intersections are considered: + // Method 1: Intersections are calculated for each shape individually, for both the + // snap source and snap target (see sp_shape_snappoints) + // Method 2: Intersections are calculated for each curve or line that we've snapped to, i.e. only for + // the target (see the intersect() method in the SnappedCurve and SnappedLine classes) + // Some differences: + // - Method 1 doesn't find intersections within a set of multiple objects + // - Method 2 only works for targets + // When considering intersections as snap targets: + // - Method 1 only works when snapping to nodes, whereas + // - Method 2 only works when snapping to paths + // - There will be performance differences too! + // If both methods are being used simultaneously, then this might lead to duplicate targets! + + // Well, here we will be looking for snap TARGETS. Both methods can therefore be used. + // When snapping to paths, we will get a collection of snapped lines and snapped curves. findBestSnap() will + // go hunting for intersections (but only when asked to in the prefs of course). In that case we can just + // temporarily block the intersections in sp_item_snappoints, we don't need duplicates. If we're not snapping to + // paths though but only to item nodes then we should still look for the intersections in sp_item_snappoints() + bool old_pref = _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH_INTERSECTION); + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH)) { + // So if we snap to paths, then findBestSnap will find the intersections + // and therefore we temporarily disable SNAPTARGET_PATH_INTERSECTION, which will + // avoid root_item->getSnappoints() below from returning intersections + _snapmanager->snapprefs.setTargetSnappable(SNAPTARGET_PATH_INTERSECTION, false); + } + + // We should not snap a transformation center to any of the centers of the items in the + // current selection (see the comment in SelTrans::centerRequest()) + bool old_pref2 = _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_ROTATION_CENTER); + if (old_pref2) { + std::vector<SPItem*> rotationSource=_snapmanager->getRotationCenterSource(); + for (auto itemlist : rotationSource) { + if (_candidate.item == itemlist) { + // don't snap to this item's rotation center + _snapmanager->snapprefs.setTargetSnappable(SNAPTARGET_ROTATION_CENTER, false); + break; + } + } + } + + root_item->getSnappoints(*_points_to_snap_to, &_snapmanager->snapprefs); + + // restore the original snap preferences + _snapmanager->snapprefs.setTargetSnappable(SNAPTARGET_PATH_INTERSECTION, old_pref); + _snapmanager->snapprefs.setTargetSnappable(SNAPTARGET_ROTATION_CENTER, old_pref2); + } + + //Collect the bounding box's corners so we can snap to them + if (p_is_a_bbox || (!_snapmanager->snapprefs.getStrictSnapping() && p_is_a_node) || p_is_other) { + // Discard the bbox of a clipped path / mask, because we don't want to snap to both the bbox + // of the item AND the bbox of the clipping path at the same time + if (!_candidate.clip_or_mask) { + Geom::OptRect b = root_item->desktopBounds(bbox_type); + getBBoxPoints(b, _points_to_snap_to.get(), true, + _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CORNER), + _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_EDGE_MIDPOINT), + _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_MIDPOINT)); + } + } + } + } +} + +void Inkscape::ObjectSnapper::_snapNodes(IntermSnapResults &isr, + SnapCandidatePoint const &p, + std::vector<SnapCandidatePoint> *unselected_nodes, + SnapConstraint const &c, + Geom::Point const &p_proj_on_constraint) const +{ + // Iterate through all nodes, find out which one is the closest to p, and snap to it! + + _collectNodes(p.getSourceType(), p.getSourceNum() <= 0); + + if (unselected_nodes != nullptr && unselected_nodes->size() > 0) { + g_assert(_points_to_snap_to != nullptr); + _points_to_snap_to->insert(_points_to_snap_to->end(), unselected_nodes->begin(), unselected_nodes->end()); + } + + SnappedPoint s; + bool success = false; + bool strict_snapping = _snapmanager->snapprefs.getStrictSnapping(); + + for (const auto & k : *_points_to_snap_to) { + if (_allowSourceToSnapToTarget(p.getSourceType(), k.getTargetType(), strict_snapping)) { + Geom::Point target_pt = k.getPoint(); + Geom::Coord dist = Geom::L2(target_pt - p.getPoint()); // Default: free (unconstrained) snapping + if (!c.isUndefined()) { + // We're snapping to nodes along a constraint only, so find out if this node + // is at the constraint, while allowing for a small margin + if (Geom::L2(target_pt - c.projection(target_pt)) > 1e-9) { + // The distance from the target point to its projection on the constraint + // is too large, so this point is not on the constraint. Skip it! + continue; + } + dist = Geom::L2(target_pt - p_proj_on_constraint); + } + + if (dist < getSnapperTolerance() && dist < s.getSnapDistance()) { + s = SnappedPoint(target_pt, p.getSourceType(), p.getSourceNum(), k.getTargetType(), dist, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true, k.getTargetBBox()); + success = true; + } + } + } + + if (success) { + isr.points.push_back(s); + } +} + +void Inkscape::ObjectSnapper::_snapTranslatingGuide(IntermSnapResults &isr, + Geom::Point const &p, + Geom::Point const &guide_normal) const +{ + // Iterate through all nodes, find out which one is the closest to this guide, and snap to it! + _collectNodes(SNAPSOURCE_GUIDE, true); + + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION, SNAPTARGET_BBOX_EDGE, SNAPTARGET_PAGE_EDGE_BORDER, SNAPTARGET_TEXT_BASELINE)) { + _collectPaths(p, SNAPSOURCE_GUIDE, true); + _snapPaths(isr, SnapCandidatePoint(p, SNAPSOURCE_GUIDE), nullptr, nullptr); + } + + SnappedPoint s; + + Geom::Coord tol = getSnapperTolerance(); + + for (const auto & k : *_points_to_snap_to) { + Geom::Point target_pt = k.getPoint(); + // Project each node (*k) on the guide line (running through point p) + Geom::Point p_proj = Geom::projection(target_pt, Geom::Line(p, p + Geom::rot90(guide_normal))); + Geom::Coord dist = Geom::L2(target_pt - p_proj); // distance from node to the guide + Geom::Coord dist2 = Geom::L2(p - p_proj); // distance from projection of node on the guide, to the mouse location + if ((dist < tol && dist2 < tol) || getSnapperAlwaysSnap()) { + s = SnappedPoint(target_pt, SNAPSOURCE_GUIDE, 0, k.getTargetType(), dist, tol, getSnapperAlwaysSnap(), false, true, k.getTargetBBox()); + isr.points.push_back(s); + } + } +} + + +/// @todo investigate why Geom::Point p is passed in but ignored. +void Inkscape::ObjectSnapper::_collectPaths(Geom::Point /*p*/, + SnapSourceType const source_type, + bool const &first_point) const +{ + // Now, let's first collect all paths to snap to. If we have a whole bunch of points to snap, + // e.g. when translating an item using the selector tool, then we will only do this for the + // first point and store the collection for later use. This significantly improves the performance + if (first_point) { + _clear_paths(); + + // Determine the type of bounding box we should snap to + SPItem::BBoxType bbox_type = SPItem::GEOMETRIC_BBOX; + + bool p_is_a_node = source_type & SNAPSOURCE_NODE_CATEGORY; + bool p_is_a_bbox = source_type & SNAPSOURCE_BBOX_CATEGORY; + bool p_is_other = (source_type & SNAPSOURCE_OTHERS_CATEGORY) || (source_type & SNAPSOURCE_DATUMS_CATEGORY); + + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_EDGE)) { + Preferences *prefs = Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box", false); + bbox_type = !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + } + + auto document = _snapmanager->getDocument(); + auto &pm = document->getPageManager(); + for (auto page : document->getPageManager().getPages()) { + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PAGE_EDGE_BORDER) && _snapmanager->snapprefs.isAnyCategorySnappable()) { + auto pathv = _getPathvFromRect(page->getDesktopRect()); + _paths_to_snap_to->push_back(SnapCandidatePath(pathv, SNAPTARGET_PAGE_EDGE_BORDER, Geom::OptRect())); + } + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PAGE_MARGIN_BORDER) && _snapmanager->snapprefs.isAnyCategorySnappable()) { + auto margin = _getPathvFromRect(page->getDesktopMargin()); + _paths_to_snap_to->push_back(SnapCandidatePath(margin, SNAPTARGET_PAGE_MARGIN_BORDER, Geom::OptRect())); + auto bleed = _getPathvFromRect(page->getDesktopBleed()); + _paths_to_snap_to->push_back(SnapCandidatePath(bleed, SNAPTARGET_PAGE_BLEED_BORDER, Geom::OptRect())); + } + } + + if (!pm.hasPages()) { + // Consider the page border for snapping + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PAGE_EDGE_BORDER) && _snapmanager->snapprefs.isAnyCategorySnappable()) { + auto pathv = _getPathvFromRect(*(_snapmanager->getDocument()->preferredBounds())); + _paths_to_snap_to->push_back(SnapCandidatePath(pathv, SNAPTARGET_PAGE_EDGE_BORDER, Geom::OptRect())); + } + } + + for (const auto & _candidate : *_snapmanager->_obj_snapper_candidates) { + + /* Transform the requested snap point to this item's coordinates */ + Geom::Affine i2doc(Geom::identity()); + SPItem *root_item = nullptr; + /* We might have a clone at hand, so make sure we get the root item */ + auto use = cast<SPUse>(_candidate.item); + if (use) { + i2doc = use->get_root_transform(); + root_item = use->root(); + g_return_if_fail(root_item); + } else { + i2doc = _candidate.item->i2doc_affine(); + root_item = _candidate.item; + } + + //Build a list of all paths considered for snapping to + + //Add the item's path to snap to + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION, SNAPTARGET_TEXT_BASELINE)) { + if (p_is_other || p_is_a_node || (!_snapmanager->snapprefs.getStrictSnapping() && p_is_a_bbox)) { + if (is<SPText>(root_item) || is<SPFlowtext>(root_item)) { + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_TEXT_BASELINE)) { + // Snap to the text baseline + Text::Layout const *layout = te_get_layout(static_cast<SPItem *>(root_item)); + if (layout != nullptr && layout->outputExists()) { + auto pv = Geom::PathVector(); + pv.push_back(layout->baseline() * root_item->i2dt_affine() * _candidate.additional_affine * _snapmanager->getDesktop()->doc2dt()); + _paths_to_snap_to->push_back(SnapCandidatePath(std::move(pv), SNAPTARGET_TEXT_BASELINE, Geom::OptRect())); + } + } + } else { + // Snapping for example to a traced bitmap is very stressing for + // the CPU, so we'll only snap to paths having no more than 500 nodes + // This also leads to a lag of approx. 500 msec (in my lousy test set-up). + bool very_complex_path = false; + auto path = cast<SPPath>(root_item); + if (path) { + very_complex_path = path->nodesInPath() > 500; + } + + if (!very_complex_path && root_item && _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION)) { + if (auto const shape = cast<SPShape>(root_item)) { + if (auto const curve = shape->curve()) { + auto pv = curve->get_pathvector(); + pv *= root_item->i2dt_affine() * _candidate.additional_affine * _snapmanager->getDesktop()->doc2dt(); // (_edit_transform * _i2d_transform); + _paths_to_snap_to->push_back(SnapCandidatePath(std::move(pv), SNAPTARGET_PATH, Geom::OptRect())); // Perhaps for speed, get a reference to the Geom::pathvector, and store the transformation besides it. + } + } + } + } + } + } + + //Add the item's bounding box to snap to + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_EDGE)) { + if (p_is_other || p_is_a_bbox || (!_snapmanager->snapprefs.getStrictSnapping() && p_is_a_node)) { + // Discard the bbox of a clipped path / mask, because we don't want to snap to both the bbox + // of the item AND the bbox of the clipping path at the same time + if (!_candidate.clip_or_mask) { + if (auto rect = root_item->bounds(bbox_type, i2doc)) { + auto path = _getPathvFromRect(*rect); + rect = root_item->desktopBounds(bbox_type); + _paths_to_snap_to->push_back(SnapCandidatePath(std::move(path), SNAPTARGET_BBOX_EDGE, rect)); + } + } + } + } + } + } +} + +void Inkscape::ObjectSnapper::_snapPaths(IntermSnapResults &isr, + SnapCandidatePoint const &p, + std::vector<SnapCandidatePoint> *unselected_nodes, + SPPath const *selected_path) const +{ + _collectPaths(p.getPoint(), p.getSourceType(), p.getSourceNum() <= 0); + // Now we can finally do the real snapping, using the paths collected above + + SPDesktop const *dt = _snapmanager->getDesktop(); + g_assert(dt != nullptr); + Geom::Point const p_doc = dt->dt2doc(p.getPoint()); + + bool const node_tool_active = _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION) && selected_path != nullptr; + + if (p.getSourceNum() <= 0) { + /* findCandidates() is used for snapping to both paths and nodes. It ignores the path that is + * currently being edited, because that path requires special care: when snapping to nodes + * only the unselected nodes of that path should be considered, and these will be passed on separately. + * This path must not be ignored however when snapping to the paths, so we add it here + * manually when applicable. + * */ + if (node_tool_active) { + // TODO fix the function to be const correct: + if (auto curve = curve_for_item(const_cast<SPPath *>(selected_path))) { + _paths_to_snap_to->push_back(SnapCandidatePath(curve->get_pathvector() * selected_path->i2doc_affine(), + SNAPTARGET_PATH, Geom::OptRect(), true)); + } + } + } + + int num_path = 0; // _paths_to_snap_to contains multiple path_vectors, each containing multiple paths. + // num_path will count the paths, and will not be zeroed for each path_vector. It will + // continue counting + + bool strict_snapping = _snapmanager->snapprefs.getStrictSnapping(); + bool snap_perp = _snapmanager->snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_PATH_PERPENDICULAR); + bool snap_tang = _snapmanager->snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_PATH_TANGENTIAL); + + //dt->snapindicator->remove_debugging_points(); + for (const auto & it_p : *_paths_to_snap_to) { + if (_allowSourceToSnapToTarget(p.getSourceType(), it_p.target_type, strict_snapping)) { + bool const being_edited = node_tool_active && it_p.currently_being_edited; + //if true then this pathvector it_pv is currently being edited in the node tool + + for (auto &it_pv : it_p.path_vector) { + // Find a nearest point for each curve within this path + // n curves will return n time values with 0 <= t <= 1 + std::vector<double> anp = it_pv.nearestTimePerCurve(p_doc); + + //std::cout << "#nearest points = " << anp.size() << " | p = " << p.getPoint() << std::endl; + // Now we will examine each of the nearest points, and determine whether it's within snapping range and if we should snap to it + std::vector<double>::const_iterator np = anp.begin(); + unsigned int index = 0; + for (; np != anp.end(); ++np, index++) { + Geom::Curve const *curve = &it_pv.at(index); + Geom::Point const sp_doc = curve->pointAt(*np); + //dt->snapindicator->set_new_debugging_point(sp_doc*dt->doc2dt()); + bool c1 = true; + bool c2 = true; + if (being_edited) { + /* If the path is being edited, then we should only snap though to stationary pieces of the path + * and not to the pieces that are being dragged around. This way we avoid + * self-snapping. For this we check whether the nodes at both ends of the current + * piece are unselected; if they are then this piece must be stationary + */ + g_assert(unselected_nodes != nullptr); + Geom::Point start_pt = dt->doc2dt(curve->pointAt(0)); + Geom::Point end_pt = dt->doc2dt(curve->pointAt(1)); + c1 = isUnselectedNode(start_pt, unselected_nodes); + c2 = isUnselectedNode(end_pt, unselected_nodes); + /* Unfortunately, this might yield false positives for coincident nodes. Inkscape might therefore mistakenly + * snap to path segments that are not stationary. There are at least two possible ways to overcome this: + * - Linking the individual nodes of the SPPath we have here, to the nodes of the NodePath::SubPath class as being + * used in sp_nodepath_selected_nodes_move. This class has a member variable called "selected". For this the nodes + * should be in the exact same order for both classes, so we can index them + * - Replacing the SPPath being used here by the NodePath::SubPath class; but how? + */ + } + + Geom::Point const sp_dt = dt->doc2dt(sp_doc); + if (!being_edited || (c1 && c2)) { + Geom::Coord dist = Geom::distance(sp_doc, p_doc); + // std::cout << " dist -> " << dist << std::endl; + if (dist < getSnapperTolerance()) { + // Add the curve we have snapped to + Geom::Point sp_tangent_dt = Geom::Point(0,0); + if (p.getSourceType() == Inkscape::SNAPSOURCE_GUIDE_ORIGIN) { + // We currently only use the tangent when snapping guides, so only in this case we will + // actually calculate the tangent to avoid wasting CPU cycles + Geom::Point sp_tangent_doc = curve->unitTangentAt(*np); + sp_tangent_dt = dt->doc2dt(sp_tangent_doc) - dt->doc2dt(Geom::Point(0,0)); + } + isr.curves.emplace_back(sp_dt, sp_tangent_dt, num_path, index, dist, getSnapperTolerance(), getSnapperAlwaysSnap(), false, curve, p.getSourceType(), p.getSourceNum(), it_p.target_type, it_p.target_bbox); + if (snap_tang || snap_perp) { + // For each curve that's within snapping range, we will now also search for tangential and perpendicular snaps + _snapPathsTangPerp(snap_tang, snap_perp, isr, p, curve, dt); + } + } + } + } + num_path++; + } // End of: for (Geom::PathVector::iterator ....) + } + } +} + +/* Returns true if point is coincident with one of the unselected nodes */ +bool Inkscape::ObjectSnapper::isUnselectedNode(Geom::Point const &point, std::vector<SnapCandidatePoint> const *unselected_nodes) const +{ + if (unselected_nodes == nullptr) { + return false; + } + + if (unselected_nodes->size() == 0) { + return false; + } + + for (const auto & unselected_node : *unselected_nodes) { + if (Geom::L2(point - unselected_node.getPoint()) < 1e-4) { + return true; + } + } + + return false; +} + +void Inkscape::ObjectSnapper::_snapPathsConstrained(IntermSnapResults &isr, + SnapCandidatePoint const &p, + SnapConstraint const &c, + Geom::Point const &p_proj_on_constraint, + std::vector<SnapCandidatePoint> *unselected_nodes, + SPPath const *selected_path) const +{ + + _collectPaths(p_proj_on_constraint, p.getSourceType(), p.getSourceNum() <= 0); + + // Now we can finally do the real snapping, using the paths collected above + + SPDesktop const *dt = _snapmanager->getDesktop(); + g_assert(dt != nullptr); + + Geom::Point direction_vector = c.getDirection(); + if (!is_zero(direction_vector)) { + direction_vector = Geom::unit_vector(direction_vector); + } + + // The intersection point of the constraint line with any path, must lie within two points on the + // SnapConstraint: p_min_on_cl and p_max_on_cl. The distance between those points is twice the snapping tolerance + Geom::Point const p_min_on_cl = dt->dt2doc(p_proj_on_constraint - getSnapperTolerance() * direction_vector); + Geom::Point const p_max_on_cl = dt->dt2doc(p_proj_on_constraint + getSnapperTolerance() * direction_vector); + Geom::Coord tolerance = getSnapperTolerance(); + + // PS: Because the paths we're about to snap to are all expressed relative to document coordinate system, we will have + // to convert the snapper coordinates from the desktop coordinates to document coordinates + + Geom::PathVector constraint_path; + if (c.isCircular()) { + Geom::Circle constraint_circle(dt->dt2doc(c.getPoint()), c.getRadius()); + Geom::PathBuilder pb; + pb.feed(constraint_circle); + pb.flush(); + constraint_path = pb.peek(); + } else { + Geom::Path constraint_line; + constraint_line.start(p_min_on_cl); + constraint_line.appendNew<Geom::LineSegment>(p_max_on_cl); + constraint_path.push_back(constraint_line); + } + + bool const node_tool_active = _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION) && selected_path != nullptr; + + //TODO: code duplication + if (p.getSourceNum() <= 0) { + /* findCandidates() is used for snapping to both paths and nodes. It ignores the path that is + * currently being edited, because that path requires special care: when snapping to nodes + * only the unselected nodes of that path should be considered, and these will be passed on separately. + * This path must not be ignored however when snapping to the paths, so we add it here + * manually when applicable. + * */ + if (node_tool_active) { + // TODO fix the function to be const correct: + if (auto curve = curve_for_item(const_cast<SPPath *>(selected_path))) { + _paths_to_snap_to->push_back(SnapCandidatePath(curve->get_pathvector() * selected_path->i2doc_affine(), SNAPTARGET_PATH, Geom::OptRect(), true)); + } + } + } + + bool strict_snapping = _snapmanager->snapprefs.getStrictSnapping(); + + // Find all intersections of the constrained path with the snap target candidates + for (const auto & k : *_paths_to_snap_to) { + if (_allowSourceToSnapToTarget(p.getSourceType(), k.target_type, strict_snapping)) { + // Do the intersection math + std::vector<Geom::PVIntersection> inters = constraint_path.intersect(k.path_vector); + + bool const being_edited = node_tool_active && k.currently_being_edited; + + // Convert the collected intersections to snapped points + for (const auto & inter : inters) { + int index = inter.second.path_index; // index on the second path, which is the target path that we snapped to + Geom::Curve const *curve = &k.path_vector.at(index).at(inter.second.curve_index); + + bool c1 = true; + bool c2 = true; + //TODO: Remove code duplication, see _snapPaths; it's documented in detail there + if (being_edited) { + g_assert(unselected_nodes != nullptr); + Geom::Point start_pt = dt->doc2dt(curve->pointAt(0)); + Geom::Point end_pt = dt->doc2dt(curve->pointAt(1)); + c1 = isUnselectedNode(start_pt, unselected_nodes); + c2 = isUnselectedNode(end_pt, unselected_nodes); + } + + if (!being_edited || (c1 && c2)) { + // Convert to desktop coordinates + Geom::Point p_inters = dt->doc2dt(inter.point()); + // Construct a snapped point + Geom::Coord dist = Geom::L2(p.getPoint() - p_inters); + SnappedPoint s = SnappedPoint(p_inters, p.getSourceType(), p.getSourceNum(), k.target_type, dist, getSnapperTolerance(), getSnapperAlwaysSnap(), true, false, k.target_bbox); + // Store the snapped point + if (dist <= tolerance) { // If the intersection is within snapping range, then we might snap to it + isr.points.push_back(s); + } + } + } + } + } +} + + +void Inkscape::ObjectSnapper::freeSnap(IntermSnapResults &isr, + SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector<SPObject const *> const *it, + std::vector<SnapCandidatePoint> *unselected_nodes) const +{ + if (_snap_enabled == false || _snapmanager->snapprefs.isSourceSnappable(p.getSourceType()) == false || ThisSnapperMightSnap() == false) { + return; + } + + /* Get a list of all the SPItems that we will try to snap to; this only needs to be done for some snappers, and + not for the grid snappers, so we'll do this here and not in the Snapmanager::freeSnap(). This saves us from wasting + precious CPU cycles */ + if (p.getSourceNum() <= 0) { + Geom::Rect const local_bbox_to_snap = bbox_to_snap ? *bbox_to_snap : Geom::Rect(p.getPoint(), p.getPoint()); + _snapmanager->_findCandidates(_snapmanager->getDocument()->getRoot(), it, local_bbox_to_snap, false, Geom::identity()); + } + + _snapNodes(isr, p, unselected_nodes); + + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION, SNAPTARGET_BBOX_EDGE, SNAPTARGET_PAGE_EDGE_BORDER, SNAPTARGET_TEXT_BASELINE)) { + unsigned n = (unselected_nodes == nullptr) ? 0 : unselected_nodes->size(); + if (n > 0) { + /* While editing a path in the node tool, findCandidates must ignore that path because + * of the node snapping requirements (i.e. only unselected nodes must be snapable). + * That path must not be ignored however when snapping to the paths, so we add it here + * manually when applicable + */ + SPPath const *path = nullptr; + if (it != nullptr) { + SPPath const *tmpPath = cast<SPPath>(*it->begin()); + if ((it->size() == 1) && tmpPath) { + path = tmpPath; + } // else: *it->begin() might be a SPGroup, e.g. when editing a LPE of text that has been converted to a group of paths + // as reported in bug #356743. In that case we can just ignore it, i.e. not snap to this item + } + _snapPaths(isr, p, unselected_nodes, path); + } else { + _snapPaths(isr, p, nullptr, nullptr); + } + } +} + +void Inkscape::ObjectSnapper::constrainedSnap( IntermSnapResults &isr, + SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + SnapConstraint const &c, + std::vector<SPObject const *> const *it, + std::vector<SnapCandidatePoint> *unselected_nodes) const +{ + if (_snap_enabled == false || _snapmanager->snapprefs.isSourceSnappable(p.getSourceType()) == false || ThisSnapperMightSnap() == false) { + return; + } + + // project the mouse pointer onto the constraint. Only the projected point will be considered for snapping + Geom::Point pp = c.projection(p.getPoint()); + + /* Get a list of all the SPItems that we will try to snap to; this only needs to be done for some snappers, and + not for the grid snappers, so we'll do this here and not in the Snapmanager::freeSnap(). This saves us from wasting + precious CPU cycles */ + if (p.getSourceNum() <= 0) { + Geom::Rect const local_bbox_to_snap = bbox_to_snap ? *bbox_to_snap : Geom::Rect(pp, pp); // Using the projected point here! Not so in freeSnap()! + _snapmanager->_findCandidates(_snapmanager->getDocument()->getRoot(), it, local_bbox_to_snap, false, Geom::identity()); + } + + // A constrained snap, is a snap in only one degree of freedom (specified by the constraint line). + // This is useful for example when scaling an object while maintaining a fixed aspect ratio. Its + // nodes are only allowed to move in one direction (i.e. in one degree of freedom). + + _snapNodes(isr, p, unselected_nodes, c, pp); + + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION, SNAPTARGET_BBOX_EDGE, SNAPTARGET_PAGE_EDGE_BORDER, SNAPTARGET_TEXT_BASELINE)) { + //TODO: Remove code duplication; see freeSnap() + unsigned n = (unselected_nodes == nullptr) ? 0 : unselected_nodes->size(); + if (n > 0) { + /* While editing a path in the node tool, findCandidates must ignore that path because + * of the node snapping requirements (i.e. only unselected nodes must be snapable). + * That path must not be ignored however when snapping to the paths, so we add it here + * manually when applicable + */ + SPPath const *path = nullptr; + if (it != nullptr) { + SPPath const *tmpPath = cast<SPPath>(*it->begin()); + if ((it->size() == 1) && tmpPath) { + path = tmpPath; + } // else: *it->begin() might be a SPGroup, e.g. when editing a LPE of text that has been converted to a group of paths + // as reported in bug #356743. In that case we can just ignore it, i.e. not snap to this item + } + _snapPathsConstrained(isr, p, c, pp, unselected_nodes, path); + } else { + _snapPathsConstrained(isr, p, c, pp, nullptr, nullptr); + } + } +} + +bool Inkscape::ObjectSnapper::ThisSnapperMightSnap() const +{ + return true; +} + +void Inkscape::ObjectSnapper::_clear_paths() const +{ + _paths_to_snap_to->clear(); +} + +Geom::PathVector Inkscape::ObjectSnapper::_getPathvFromRect(Geom::Rect const rect) const +{ + return SPCurve(rect, true).get_pathvector(); +} + +/** + * Default version of the getBBoxPoints with default corner source types. + */ +void Inkscape::getBBoxPoints(Geom::OptRect const bbox, + std::vector<SnapCandidatePoint> *points, + bool const isTarget, + bool const corners, + bool const edges, + bool const midpoint) +{ + getBBoxPoints(bbox, points, isTarget, + corners ? SNAPSOURCE_BBOX_CORNER : SNAPSOURCE_UNDEFINED, + corners ? SNAPTARGET_BBOX_CORNER : SNAPTARGET_UNDEFINED, + edges ? SNAPSOURCE_BBOX_EDGE_MIDPOINT : SNAPSOURCE_UNDEFINED, + edges ? SNAPTARGET_BBOX_EDGE_MIDPOINT : SNAPTARGET_UNDEFINED, + midpoint ? SNAPSOURCE_BBOX_MIDPOINT : SNAPSOURCE_UNDEFINED, + midpoint ? SNAPTARGET_BBOX_MIDPOINT : SNAPTARGET_UNDEFINED); +} + +void Inkscape::getBBoxPoints(Geom::OptRect const bbox, + std::vector<SnapCandidatePoint> *points, + bool const /*isTarget*/, + Inkscape::SnapSourceType corner_src, + Inkscape::SnapTargetType corner_tgt, + Inkscape::SnapSourceType edge_src, + Inkscape::SnapTargetType edge_tgt, + Inkscape::SnapSourceType mid_src, + Inkscape::SnapTargetType mid_tgt) +{ + if (bbox) { + // collect the corners of the bounding box + for ( unsigned k = 0 ; k < 4 ; k++ ) { + if (corner_src || corner_tgt) { + points->push_back(SnapCandidatePoint(bbox->corner(k), corner_src, -1, corner_tgt, *bbox)); + } + // optionally, collect the midpoints of the bounding box's edges too + if (edge_src || edge_tgt) { + points->push_back(SnapCandidatePoint((bbox->corner(k) + bbox->corner((k+1) % 4))/2, edge_src, -1, edge_tgt, *bbox)); + } + } + if (mid_src || mid_tgt) { + points->push_back(SnapCandidatePoint(bbox->midpoint(), mid_src, -1, mid_tgt, *bbox)); + } + } +} + +bool Inkscape::ObjectSnapper::_allowSourceToSnapToTarget(SnapSourceType source, SnapTargetType target, bool strict_snapping) const +{ + bool allow_this_pair_to_snap = true; + + if (strict_snapping) { // bounding boxes will not snap to nodes/paths and vice versa + if (((source & SNAPSOURCE_BBOX_CATEGORY) && (target & SNAPTARGET_NODE_CATEGORY)) || + ((source & SNAPSOURCE_NODE_CATEGORY) && (target & SNAPTARGET_BBOX_CATEGORY))) { + allow_this_pair_to_snap = false; + } + } + + return allow_this_pair_to_snap; +} + +void Inkscape::ObjectSnapper::_snapPathsTangPerp(bool snap_tang, bool snap_perp, IntermSnapResults &isr, SnapCandidatePoint const &p, Geom::Curve const *curve, SPDesktop const *dt) const +{ + // Here we will try to snap either tangentially or perpendicularly to a single path; for this we need to know where the origin is located of the line that is currently being rotated, + // or we need to know the vector of the guide which is currently being translated + std::vector<std::pair<Geom::Point, bool> > const origins_and_vectors = p.getOriginsAndVectors(); + // Now we will iterate over all the origins and vectors and see which of these will get use a tangential or perpendicular snap + for (const auto & origins_and_vector : origins_and_vectors) { + Geom::Point origin_or_vector_doc = dt->dt2doc(origins_and_vector.first); // "first" contains a Geom::Point, denoting either a point or vector + if (origins_and_vector.second) { // if "second" is true then "first" is a vector, otherwise it's a point + // So we have a vector, which tells us what tangential or perpendicular direction we're looking for + if (curve->degreesOfFreedom() <= 2) { // A LineSegment has order one, and therefore 2 DOF + // When snapping to a point of a line segment that has a specific tangential or normal vector, then either all point + // along that line will be snapped to or no points at all will be snapped to. This is not very useful, so let's skip + // any line segments and lets only snap to higher order curves + continue; + } + // The vector is being treated as a point (relative to the origin), and has been translated to document coordinates accordingly + // We need however to make it a vector again, because also the origin has been transformed + origin_or_vector_doc -= dt->dt2doc(Geom::Point(0,0)); + } + + Geom::Point point_dt; + Geom::Coord dist; + std::vector<double> ts; + + if (snap_tang) { // Find all points that lead to a tangential snap + if (origins_and_vector.second) { // if "second" is true then "first" is a vector, otherwise it's a point + ts = find_tangents_by_vector(origin_or_vector_doc, curve->toSBasis()); + } else { + ts = find_tangents(origin_or_vector_doc, curve->toSBasis()); + } + for (double t : ts) { + point_dt = dt->doc2dt(curve->pointAt(t)); + dist = Geom::distance(point_dt, p.getPoint()); + isr.points.emplace_back(point_dt, p.getSourceType(), p.getSourceNum(), SNAPTARGET_PATH_TANGENTIAL, dist, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + } + } + + if (snap_perp) { // Find all points that lead to a perpendicular snap + if (origins_and_vector.second) { + ts = find_normals_by_vector(origin_or_vector_doc, curve->toSBasis()); + } else { + ts = find_normals(origin_or_vector_doc, curve->toSBasis()); + } + for (double t : ts) { + point_dt = dt->doc2dt(curve->pointAt(t)); + dist = Geom::distance(point_dt, p.getPoint()); + isr.points.emplace_back(point_dt, p.getSourceType(), p.getSourceNum(), SNAPTARGET_PATH_PERPENDICULAR, dist, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + } + } + } +} + +/* + 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 : diff --git a/src/object-snapper.h b/src/object-snapper.h new file mode 100644 index 0000000..3f539bf --- /dev/null +++ b/src/object-snapper.h @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_OBJECT_SNAPPER_H +#define SEEN_OBJECT_SNAPPER_H +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 2005 - 2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include "snapper.h" +#include "snap-candidate.h" + +class SPDesktop; +class SPNamedView; +class SPObject; +class SPPath; +class SPDesktop; + +namespace Inkscape +{ + +/** + * Snapping things to objects. + */ +class ObjectSnapper : public Snapper +{ + +public: + ObjectSnapper(SnapManager *sm, Geom::Coord const d); + ~ObjectSnapper() override; + + /** + * @return true if this Snapper will snap at least one kind of point. + */ + bool ThisSnapperMightSnap() const override; + + /** + * @return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels. + */ + Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom) + + bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance + + void freeSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector<SPObject const *> const *it, + std::vector<SnapCandidatePoint> *unselected_nodes) const override; + + void constrainedSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + SnapConstraint const &c, + std::vector<SPObject const *> const *it, + std::vector<SnapCandidatePoint> *unselected_nodes) const override; + +private: + std::unique_ptr<std::vector<SnapCandidatePoint>> _points_to_snap_to; + std::unique_ptr<std::vector<SnapCandidatePath >> _paths_to_snap_to; + + void _snapNodes(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, // in desktop coordinates + std::vector<SnapCandidatePoint> *unselected_nodes, + SnapConstraint const &c = SnapConstraint(), + Geom::Point const &p_proj_on_constraint = Geom::Point()) const; + + void _snapTranslatingGuide(IntermSnapResults &isr, + Geom::Point const &p, + Geom::Point const &guide_normal) const; + + void _collectNodes(Inkscape::SnapSourceType const &t, + bool const &first_point) const; + + void _snapPaths(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, // in desktop coordinates + std::vector<Inkscape::SnapCandidatePoint> *unselected_nodes, // in desktop coordinates + SPPath const *selected_path) const; + + void _snapPathsConstrained(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, // in desktop coordinates + SnapConstraint const &c, + Geom::Point const &p_proj_on_constraint, + std::vector<SnapCandidatePoint> *unselected_nodes, + SPPath const *selected_path) const; + + void _snapPathsTangPerp(bool snap_tang, + bool snap_perp, + IntermSnapResults &isr, + SnapCandidatePoint const &p, + Geom::Curve const *curve, + SPDesktop const *dt) const; + + bool isUnselectedNode(Geom::Point const &point, std::vector<Inkscape::SnapCandidatePoint> const *unselected_nodes) const; + + /** + * Returns index of first NR_END bpath in array. + */ + void _collectPaths(Geom::Point p, + Inkscape::SnapSourceType const source_type, + bool const &first_point) const; + + void _clear_paths() const; + Geom::PathVector _getBorderPathv() const; + Geom::PathVector _getPathvFromRect(Geom::Rect const rect) const; + bool _allowSourceToSnapToTarget(SnapSourceType source, SnapTargetType target, bool strict_snapping) const; + +}; // end of ObjectSnapper class + +void getBBoxPoints(Geom::OptRect const bbox, std::vector<SnapCandidatePoint> *points, bool const isTarget, bool const corners, bool const edges, bool const midpoint); +void getBBoxPoints(Geom::OptRect const bbox, std::vector<SnapCandidatePoint> *points, bool const isTarget, + Inkscape::SnapSourceType corners, Inkscape::SnapTargetType cornert, + Inkscape::SnapSourceType edges, Inkscape::SnapTargetType edget, + Inkscape::SnapSourceType midpoints, Inkscape::SnapTargetType midpointt); + +} // end of namespace Inkscape + +#endif diff --git a/src/object/CMakeLists.txt b/src/object/CMakeLists.txt new file mode 100644 index 0000000..452fafe --- /dev/null +++ b/src/object/CMakeLists.txt @@ -0,0 +1,186 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + + +set(object_SRC + box3d-side.cpp + box3d.cpp + color-profile.cpp + object-set.cpp + persp3d-reference.cpp + persp3d.cpp + sp-anchor.cpp + sp-clippath.cpp + sp-conn-end-pair.cpp + sp-conn-end.cpp + sp-defs.cpp + sp-desc.cpp + sp-dimensions.cpp + sp-ellipse.cpp + sp-factory.cpp + sp-filter-reference.cpp + sp-filter.cpp + sp-flowdiv.cpp + sp-flowregion.cpp + sp-flowtext.cpp + sp-font-face.cpp + sp-font.cpp + sp-glyph-kerning.cpp + sp-glyph.cpp + sp-gradient-reference.cpp + sp-gradient.cpp + sp-grid.cpp + sp-guide.cpp + sp-hatch-path.cpp + sp-hatch.cpp + sp-image.cpp + sp-item-group.cpp + sp-item-transform.cpp + sp-item.cpp + sp-line.cpp + sp-linear-gradient.cpp + sp-lpe-item.cpp + sp-marker.cpp + sp-mask.cpp + sp-mesh-array.cpp + sp-mesh-gradient.cpp + sp-mesh-patch.cpp + sp-mesh-row.cpp + sp-metadata.cpp + sp-missing-glyph.cpp + sp-namedview.cpp + sp-object-group.cpp + sp-object.cpp + sp-offset.cpp + sp-paint-server.cpp + sp-page.cpp + sp-path.cpp + sp-pattern.cpp + sp-polygon.cpp + sp-polyline.cpp + sp-radial-gradient.cpp + sp-rect.cpp + sp-root.cpp + sp-script.cpp + sp-shape.cpp + sp-shape-reference.cpp + sp-solid-color.cpp + sp-spiral.cpp + sp-star.cpp + sp-stop.cpp + sp-string.cpp + sp-style-elem.cpp + sp-switch.cpp + sp-symbol.cpp + sp-tag-use-reference.cpp + sp-tag-use.cpp + sp-tag.cpp + sp-text.cpp + sp-title.cpp + sp-tref-reference.cpp + sp-tref.cpp + sp-tspan.cpp + sp-use-reference.cpp + sp-use.cpp + uri-references.cpp + uri.cpp + viewbox.cpp + + # ------- + # Headers + box3d-side.h + box3d.h + color-profile.h + object-set.h + persp3d-reference.h + persp3d.h + sp-anchor.h + sp-clippath.h + sp-conn-end-pair.h + sp-conn-end.h + sp-defs.h + sp-desc.h + sp-dimensions.h + sp-ellipse.h + sp-factory.h + sp-filter-reference.h + sp-filter-units.h + sp-filter.h + sp-flowdiv.h + sp-flowregion.h + sp-flowtext.h + sp-font-face.h + sp-font.h + sp-glyph-kerning.h + sp-glyph.h + sp-gradient-reference.h + sp-gradient-spread.h + sp-gradient-units.h + sp-gradient-vector.h + sp-gradient.h + sp-grid.h + sp-guide.h + sp-hatch-path.h + sp-hatch.h + sp-image.h + sp-item-group.h + sp-item-transform.h + sp-item.h + sp-line.h + sp-linear-gradient.h + sp-lpe-item.h + sp-marker-loc.h + sp-marker.h + sp-mask.h + sp-mesh-array.h + sp-mesh-gradient.h + sp-mesh-patch.h + sp-mesh-row.h + sp-metadata.h + sp-missing-glyph.h + sp-namedview.h + sp-object-group.h + sp-object.h + sp-offset.h + sp-paint-server-reference.h + sp-paint-server.h + sp-page.h + sp-path.h + sp-pattern.h + sp-polygon.h + sp-polyline.h + sp-radial-gradient.h + sp-rect.h + sp-root.h + sp-script.h + sp-shape.h + sp-shape-reference.h + sp-solid-color.h + sp-spiral.h + sp-star.h + sp-stop.h + sp-string.h + sp-style-elem.h + sp-switch.h + sp-symbol.h + sp-tag.h + sp-tag-use.h + sp-tag-use-reference.h + sp-text.h + sp-textpath.h + sp-title.h + sp-tref-reference.h + sp-tref.h + sp-tspan.h + sp-use-reference.h + sp-use.h + uri-references.h + uri.h + viewbox.h + weakptr.h + tags.h +) + +add_inkscape_source("${object_SRC}") + +add_subdirectory(filters) +add_subdirectory(algorithms) diff --git a/src/object/README b/src/object/README new file mode 100644 index 0000000..207c265 --- /dev/null +++ b/src/object/README @@ -0,0 +1,114 @@ + +This directory contains classes that are derived from SPObject as well +as closely related code. + +The object tree implements an XML-to-display primitive mapping, and +provides an object hierarchy that can be modified using the +GUI. Changes in the XML tree are automatically propagated to the +object tree via observers, but not the other way around — a function +called updateRepr() must be explicitly called. Relevant nodes of the +object tree contains fully cascaded CSS style information. The object +tree also includes clones of objects that are referenced by the <use> +element in the XML tree (this is needed as clones may have different +styling due to inheritance). + +See: http://wiki.inkscape.org/wiki/index.php/Object_tree + +Object class inheritance: + +SPObject sp-object.h: + ColorProfile color-profile.h: + Persp3D persp3d.h: + SPDefs sp-defs.h: + SPDesc sp-desc.h: + SPFilter sp-filter.h: + SPFlowline sp-flowdiv.h: + SPFlowregionbreak sp-flowdiv.h: + SPFontFace sp-font-face.h: + SPFont sp-font.h: + SPGlyph sp-glyph.h: + SPGlyphKerning sp-glyph-kerning.h: + SPHkern sp-glyph-kerning.h: + SPVkern sp-glyph-kerning.h: + SPGuide sp-guide.h: + SPHatchPath sp-hatch-path.h: + SPItem sp-item.h: + SPFlowdiv sp-flowdiv.h: + SPFlowtspan sp-flowdiv.h: + SPFlowpara sp-flowdiv.h: + SPFlowregion sp-flowregion.h: + SPFlowregionExclude sp-flowregion.h: + SPFlowtext sp-flowtext.h: + SPImage sp-image.h: + SPLPEItem sp-lpe-item.h: + SPGroup sp-item-group.h: + SPBox3D box3d.h: + SPAnchor sp-anchor.h: + SPMarker sp-marker.h: + SPRoot sp-root.h: + SPSwitch sp-switch.h: + SPSymbol sp-symbol.h: + SPShape sp-shape.h: + SPGenericEllipse sp-ellipse.h: + SPLine sp-line.h: + SPOffset sp-offset.h: + SPPath sp-path.h: + SPPolygon sp-polygon.h: + SPStar sp-star.h: + SPPolyLine sp-polyline.h: + Box3DSide box3d-side.h: + SPRect sp-rect.h: + SPSpiral sp-spiral.h: + SPText sp-text.h: + SPTextPath sp-textpath.h: + SPTRef sp-tref.h: + SPTSpan sp-tspan.h: + SPUse sp-use.h: + SPMeshpatch sp-mesh-patch.h: + SPMeshrow sp-mesh-row.h: + SPMetadata sp-metadata.h: + SPMissingGlyph sp-missing-glyph.h: + SPObjectGroup sp-object-group.h: + SPClipPath sp-clippath.h: + SPMask sp-mask.h: + SPNamedView sp-namedview.h: + SPPaintServer sp-paint-server.h: + SPGradient sp-gradient.h: + SPLinearGradient sp-linear-gradient.h: + SPMeshGradient sp-mesh-gradient.h: + SPRadialGradient sp-radial-gradient.h: + SPHatch sp-hatch.h: + SPPattern sp-pattern.h: + SPSolidColor sp-solid-color.h: + SPScript sp-script.h: + SPStop sp-stop.h: + SPString sp-string.h: + SPStyleElem sp-style-elem.h: + SPTag sp-tag.h: + SPTagUse sp-tag-use.h: + SPTitle sp-title.h: + +Other related files: + + object-set.h: + persp3d-reference.h + sp-conn-end-pair.h + sp-conn-end.h + sp-dimensions.h + sp-factory.h + sp-filter-reference.h + sp-filter-units.h + sp-gradient-reference.h + sp-gradient-spread.h + sp-gradient-units.h + sp-gradient-vector.h + sp-item-transform.h + sp-marker-loc.h + sp-mesh-array.h + sp-paint-server-reference.h + sp-tag-use-reference.h + sp-tref-reference.h + sp-use-reference.h + uri.h + uri-references.h + viewbox.h diff --git a/src/object/algorithms/CMakeLists.txt b/src/object/algorithms/CMakeLists.txt new file mode 100644 index 0000000..3101ab2 --- /dev/null +++ b/src/object/algorithms/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(object_algorithms_SRC + graphlayout.cpp + removeoverlap.cpp + unclump.cpp + + # ------- + # Headers + bboxsort.h + graphlayout.h + removeoverlap.h + unclump.h +) + +add_inkscape_source("${object_algorithms_SRC}") diff --git a/src/object/algorithms/bboxsort.h b/src/object/algorithms/bboxsort.h new file mode 100644 index 0000000..6d15c12 --- /dev/null +++ b/src/object/algorithms/bboxsort.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_BBOXSORT_H +#define SEEN_BBOXSORT_H + +/** @file + * @brief Simple helper class for sorting objects based on their bounding boxes. + */ +/* Authors: + * MenTaLguY + * Dmitry Kirsanov + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2007-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * Previously part of the Align and Distribute dialog. + */ + +class BBoxSort { + +public: + BBoxSort(SPItem *item, Geom::Rect const &bounds, Geom::Dim2 orientation, double begin, double end) + : item(item) + , bbox(bounds) + { + anchor = begin * bbox.min()[orientation] + end * bbox.max()[orientation]; + } + + BBoxSort(const BBoxSort &rhs) = default; // Should really be vector of pointers to avoid copying class when sorting. + ~BBoxSort() = default; + + double anchor = 0.0; + SPItem* item = nullptr; + Geom::Rect bbox; +}; + +static bool operator< (const BBoxSort &a, const BBoxSort &b) { + return a.anchor < b.anchor; +} + +#endif // SEEN_BBOXSORT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/algorithms/graphlayout.cpp b/src/object/algorithms/graphlayout.cpp new file mode 100644 index 0000000..c42ca22 --- /dev/null +++ b/src/object/algorithms/graphlayout.cpp @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Interface between Inkscape code (SPItem) and graphlayout functions. + */ +/* + * Authors: + * Tim Dwyer <Tim.Dwyer@infotech.monash.edu.au> + * Abhishek Sharma + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <cstring> +#include <iostream> +#include <list> +#include <map> +#include <string> +#include <valarray> +#include <vector> +#include <array> + +#include <2geom/transforms.h> + +#include "conn-avoid-ref.h" +#include "desktop.h" +#include "graphlayout.h" +#include "inkscape.h" + +#include "3rdparty/adaptagrams/libavoid/router.h" + +#include "3rdparty/adaptagrams/libcola/cola.h" +#include "3rdparty/adaptagrams/libcola/connected_components.h" + +#include "object/sp-item-transform.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "style.h" + +using namespace cola; +using namespace vpsc; + +/** + * Returns true if item is a connector + */ +bool isConnector(SPItem const * const item) { + auto path = cast<SPPath>(item); + return path && path->connEndPair.isAutoRoutingConn(); +} + +struct CheckProgress: TestConvergence { + CheckProgress(double d, unsigned i, std::list<SPItem*> & selected, Rectangles & rs, + std::map<std::string, unsigned> & nodelookup) + : TestConvergence(d, i) + , selected(selected) + , rs(rs) + , nodelookup(nodelookup) + {} + bool operator()(const double new_stress, std::valarray<double> & X, std::valarray<double> & Y) override { + /* This is where, if we wanted to animate the layout, we would need to update + * the positions of all objects and redraw the canvas and maybe sleep a bit + cout << "stress="<<new_stress<<endl; + cout << "x[0]="<<rs[0]->getMinX()<<endl; + for (std::list<SPItem *>::iterator it(selected.begin()); + it != selected.end(); + ++it) + { + SPItem *u=*it; + if(!isConnector(u)) { + Rectangle* r=rs[nodelookup[u->id]]; + Geom::Rect const item_box(sp_item_bbox_desktop(u)); + Geom::Point const curr(item_box.midpoint()); + Geom::Point const dest(r->getCentreX(),r->getCentreY()); + u->move_rel(Geom::Translate(dest - curr)); + } + } + */ + return TestConvergence::operator()(new_stress, X, Y); + } + std::list<SPItem*> & selected; + Rectangles & rs; + std::map<std::string, unsigned> & nodelookup; +}; + +/** + * Scans the items list and places those items that are + * not connectors in filtered + */ +void filterConnectors(std::vector<SPItem*> const & items, std::list<SPItem*> & filtered) { + for (SPItem * item: items) { + if (!isConnector(item)) { + filtered.push_back(item); + } + } +} + +/** + * Takes a list of inkscape items, extracts the graph defined by + * connectors between them, and uses graph layout techniques to find + * a nice layout + */ +void graphlayout(std::vector<SPItem*> const & items) { + if (items.empty()) return; + + std::list<SPItem*> selected; + filterConnectors(items, selected); + std::vector<SPItem*> connectors; + std::copy_if(items.begin(), items.end(), std::back_inserter(connectors), [](SPItem* item){return isConnector(item); }); + + if (selected.size() < 2) return; + + // add the connector spacing to the size of node bounding boxes + // so that connectors can always be routed between shapes + SPDesktop * desktop = SP_ACTIVE_DESKTOP; + double spacing = 0; + if (desktop) spacing = desktop->namedview->connector_spacing + 0.1; + + std::map<std::string, unsigned> nodelookup; + Rectangles rs; + std::vector<Edge> es; + for (SPItem * item: selected) { + Geom::OptRect const item_box = item->desktopVisualBounds(); + if (item_box) { + Geom::Point ll(item_box->min()); + Geom::Point ur(item_box->max()); + nodelookup[item->getId()] = rs.size(); + rs.push_back(new Rectangle(ll[0] - spacing, ur[0] + spacing, + ll[1] - spacing, ur[1] + spacing)); + } else { + // I'm not actually sure if it's possible for something with a + // NULL item-box to be attached to a connector in which case we + // should never get to here... but if such a null box can occur it's + // probably pretty safe to simply ignore + //fprintf(stderr, "NULL item_box found in graphlayout, ignoring!\n"); + } + } + + Inkscape::Preferences * prefs = Inkscape::Preferences::get(); + CompoundConstraints constraints; + double ideal_connector_length = prefs->getDouble("/tools/connector/length", 100.0); + double directed_edge_height_modifier = 1.0; + + bool directed = prefs->getBool("/tools/connector/directedlayout"); + bool avoid_overlaps = prefs->getBool("/tools/connector/avoidoverlaplayout"); + + for (SPItem* conn: connectors) { + auto path = cast<SPPath>(conn); + std::array<SPItem*, 2> attachedItems; + path->connEndPair.getAttachedItems(attachedItems.data()); + if (attachedItems[0] == nullptr) continue; + if (attachedItems[1] == nullptr) continue; + std::map<std::string, unsigned>::iterator i_iter=nodelookup.find(attachedItems[0]->getId()); + if (i_iter == nodelookup.end()) continue; + unsigned rect_index_first = i_iter->second; + i_iter = nodelookup.find(attachedItems[1]->getId()); + if (i_iter == nodelookup.end()) continue; + unsigned rect_index_second = i_iter->second; + es.emplace_back(rect_index_first, rect_index_second); + + if (conn->style->marker_end.set) { + if (directed && strcmp(conn->style->marker_end.value(), "none")) { + constraints.push_back(new SeparationConstraint(YDIM, rect_index_first, rect_index_second, + ideal_connector_length * directed_edge_height_modifier)); + } + } + } + + EdgeLengths elengths(es.size(), 1); + std::vector<Component*> cs; + connectedComponents(rs, es, cs); + for (Component * c: cs) { + if (c->edges.size() < 2) continue; + CheckProgress test(0.0001, 100, selected, rs, nodelookup); + ConstrainedMajorizationLayout alg(c->rects, c->edges, nullptr, ideal_connector_length, elengths, &test); + if (avoid_overlaps) alg.setAvoidOverlaps(); + alg.setConstraints(&constraints); + alg.run(); + } + separateComponents(cs); + + for (SPItem * item: selected) { + if (!isConnector(item)) { + std::map<std::string, unsigned>::iterator i = nodelookup.find(item->getId()); + if (i != nodelookup.end()) { + Rectangle * r = rs[i->second]; + Geom::OptRect item_box = item->desktopVisualBounds(); + if (item_box) { + Geom::Point const curr(item_box->midpoint()); + Geom::Point const dest(r->getCentreX(),r->getCentreY()); + item->move_rel(Geom::Translate(dest - curr)); + } + } + } + } + for (CompoundConstraint * c: constraints) { + delete c; + } + for (Rectangle * r: rs) { + delete r; + } +} +// vim: set cindent +// vim: ts=4 sw=4 et tw=0 wm=0 + +/* + 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 : diff --git a/src/object/algorithms/graphlayout.h b/src/object/algorithms/graphlayout.h new file mode 100644 index 0000000..6c6b601 --- /dev/null +++ b/src/object/algorithms/graphlayout.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * graph layout functions. + */ +/* + * Authors: + * Tim Dwyer <tgdwyer@gmail.com> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_GRAPHLAYOUT_H +#define SEEN_GRAPHLAYOUT_H + +#include <list> +#include <vector> + +class SPItem; + +void graphlayout(std::vector<SPItem*> const &items); + +bool isConnector(SPItem const *const item); + +void filterConnectors(std::vector<SPItem*> const &items, std::list<SPItem *> &filtered); + +#endif // SEEN_GRAPHLAYOUT_H diff --git a/src/object/algorithms/removeoverlap.cpp b/src/object/algorithms/removeoverlap.cpp new file mode 100644 index 0000000..8b3064b --- /dev/null +++ b/src/object/algorithms/removeoverlap.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Interface between Inkscape code (SPItem) and remove-overlaps function. + */ +/* + * Authors: + * Tim Dwyer <tgdwyer@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <utility> + +#include <2geom/transforms.h> + +#include "removeoverlap.h" + +#include "libvpsc/rectangle.h" + +#include "object/sp-item.h" +#include "object/sp-item-transform.h" + + +using vpsc::Rectangle; + +namespace { + +struct Record { + SPItem * item; + Geom::Point midpoint; + Rectangle * vspc_rect; + + Record() : item(nullptr), vspc_rect(nullptr) {} + Record(SPItem * i, Geom::Point m, Rectangle * r) + : item(i), midpoint(m), vspc_rect(r) {} +}; + +} + +/** +* Takes a list of inkscape items and moves them as little as possible +* such that rectangular bounding boxes are separated by at least xGap +* horizontally and yGap vertically +*/ +void removeoverlap(std::vector<SPItem*> const & items, double const xGap, double const yGap) { + std::vector<SPItem*> selected = items; + std::vector<Record> records; + std::vector<Rectangle*> rs; + + Geom::Point const gap(xGap, yGap); + for (SPItem * item: selected) { + using Geom::X; using Geom::Y; + Geom::OptRect item_box(item->desktopVisualBounds()); + if (item_box) { + Geom::Point min(item_box->min() - .5 * gap); + Geom::Point max(item_box->max() + .5 * gap); + // A negative gap is allowed, but will lead to problems when the gap is larger than + // the bounding box (in either X or Y direction, or both); min will have become max + // now, which cannot be handled by Rectangle() which is called below. And how will + // removeRectangleOverlap handle such a case? + // That's why we will enforce some boundaries on min and max here: + if (max[X] < min[X]) { + min[X] = max[X] = (min[X] + max[X]) / 2.; + } + if (max[Y] < min[Y]) { + min[Y] = max[Y] = (min[Y] + max[Y]) / 2.; + } + Rectangle * vspc_rect = new Rectangle(min[X], max[X], min[Y], max[Y]); + records.emplace_back(item, item_box->midpoint(), vspc_rect); + rs.push_back(vspc_rect); + } + } + if (!rs.empty()) { + removeoverlaps(rs); + } + for (Record & rec: records) { + Geom::Point const curr = rec.midpoint; + Geom::Point const dest(rec.vspc_rect->getCentreX(), rec.vspc_rect->getCentreY()); + rec.item->move_rel(Geom::Translate(dest - curr)); + delete rec.vspc_rect; + } +} diff --git a/src/object/algorithms/removeoverlap.h b/src/object/algorithms/removeoverlap.h new file mode 100644 index 0000000..619b091 --- /dev/null +++ b/src/object/algorithms/removeoverlap.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * \brief Remove overlaps function + */ +/* + * Authors: + * Tim Dwyer <tgdwyer@gmail.com> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_REMOVEOVERLAP_H +#define SEEN_REMOVEOVERLAP_H + +#include <vector> + +class SPItem; + +void removeoverlap(std::vector<SPItem*> const &items, double xGap, double yGap); + +#endif // SEEN_REMOVEOVERLAP_H diff --git a/src/object/algorithms/unclump.cpp b/src/object/algorithms/unclump.cpp new file mode 100644 index 0000000..df59f88 --- /dev/null +++ b/src/object/algorithms/unclump.cpp @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Unclumping objects. + */ +/* Authors: + * bulia byak + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "unclump.h" + +#include <2geom/transforms.h> +#include <algorithm> +#include <map> + +#include "object/sp-item.h" + +class Unclump +{ +public: + double dist(SPItem *item1, SPItem *item2); + double average(SPItem *item, std::list<SPItem *> &others); + SPItem *closest(SPItem *item, std::list<SPItem *> &others); + SPItem *farthest(SPItem *item, std::list<SPItem *> &others); + std::vector<SPItem *> unclump_remove_behind(SPItem *item, SPItem *closest, std::list<SPItem *> &rest); + void push(SPItem *from, SPItem *what, double dist); + void pull(SPItem *to, SPItem *what, double dist); + +private: + Geom::Point unclump_center(SPItem *item); + Geom::Point unclump_wh(SPItem *item); + + // Taking bbox of an item is an expensive operation, and we need to do it many times, so here we + // cache the centers, widths, and heights of items + + std::map<const gchar *, Geom::Point> c_cache; + std::map<const gchar *, Geom::Point> wh_cache; +}; + +/** +Center of bbox of item +*/ +Geom::Point Unclump::unclump_center(SPItem *item) +{ + std::map<const gchar *, Geom::Point>::iterator i = c_cache.find(item->getId()); + if (i != c_cache.end()) { + return i->second; + } + + Geom::OptRect r = item->desktopVisualBounds(); + if (r) { + Geom::Point const c = r->midpoint(); + c_cache[item->getId()] = c; + return c; + } else { + // FIXME + return Geom::Point(0, 0); + } +} + +Geom::Point Unclump::unclump_wh(SPItem *item) +{ + Geom::Point wh; + std::map<const gchar *, Geom::Point>::iterator i = wh_cache.find(item->getId()); + if (i != wh_cache.end()) { + wh = i->second; + } else { + Geom::OptRect r = item->desktopVisualBounds(); + if (r) { + wh = r->dimensions(); + wh_cache[item->getId()] = wh; + } else { + wh = Geom::Point(0, 0); + } + } + + return wh; +} + +/** +Distance between "edges" of item1 and item2. An item is considered to be an ellipse inscribed into its w/h, +so its radius (distance from center to edge) depends on the w/h and the angle towards the other item. +May be negative if the edge of item1 is between the center and the edge of item2. +*/ +double Unclump::dist(SPItem *item1, SPItem *item2) +{ + Geom::Point c1 = unclump_center(item1); + Geom::Point c2 = unclump_center(item2); + + Geom::Point wh1 = unclump_wh(item1); + Geom::Point wh2 = unclump_wh(item2); + + // angle from each item's center to the other's, unsqueezed by its w/h, normalized to 0..pi/2 + double a1 = atan2((c2 - c1)[Geom::Y], (c2 - c1)[Geom::X] * wh1[Geom::Y] / wh1[Geom::X]); + a1 = fabs(a1); + if (a1 > M_PI / 2) + a1 = M_PI - a1; + + double a2 = atan2((c1 - c2)[Geom::Y], (c1 - c2)[Geom::X] * wh2[Geom::Y] / wh2[Geom::X]); + a2 = fabs(a2); + if (a2 > M_PI / 2) + a2 = M_PI - a2; + + // get the radius of each item for the given angle + double r1 = 0.5 * (wh1[Geom::X] + (wh1[Geom::Y] - wh1[Geom::X]) * (a1 / (M_PI / 2))); + double r2 = 0.5 * (wh2[Geom::X] + (wh2[Geom::Y] - wh2[Geom::X]) * (a2 / (M_PI / 2))); + + // dist between centers minus angle-adjusted radii + double dist_r = (Geom::L2(c2 - c1) - r1 - r2); + + double stretch1 = wh1[Geom::Y] / wh1[Geom::X]; + double stretch2 = wh2[Geom::Y] / wh2[Geom::X]; + + if ((stretch1 > 1.5 || stretch1 < 0.66) && (stretch2 > 1.5 || stretch2 < 0.66)) { + std::vector<double> dists; + dists.push_back(dist_r); + + // If both objects are not circle-like, find dists between four corners + std::vector<Geom::Point> c1_points(2); + { + double y_closest; + if (c2[Geom::Y] > c1[Geom::Y] + wh1[Geom::Y] / 2) { + y_closest = c1[Geom::Y] + wh1[Geom::Y] / 2; + } else if (c2[Geom::Y] < c1[Geom::Y] - wh1[Geom::Y] / 2) { + y_closest = c1[Geom::Y] - wh1[Geom::Y] / 2; + } else { + y_closest = c2[Geom::Y]; + } + c1_points[0] = Geom::Point(c1[Geom::X], y_closest); + double x_closest; + if (c2[Geom::X] > c1[Geom::X] + wh1[Geom::X] / 2) { + x_closest = c1[Geom::X] + wh1[Geom::X] / 2; + } else if (c2[Geom::X] < c1[Geom::X] - wh1[Geom::X] / 2) { + x_closest = c1[Geom::X] - wh1[Geom::X] / 2; + } else { + x_closest = c2[Geom::X]; + } + c1_points[1] = Geom::Point(x_closest, c1[Geom::Y]); + } + + std::vector<Geom::Point> c2_points(2); + { + double y_closest; + if (c1[Geom::Y] > c2[Geom::Y] + wh2[Geom::Y] / 2) { + y_closest = c2[Geom::Y] + wh2[Geom::Y] / 2; + } else if (c1[Geom::Y] < c2[Geom::Y] - wh2[Geom::Y] / 2) { + y_closest = c2[Geom::Y] - wh2[Geom::Y] / 2; + } else { + y_closest = c1[Geom::Y]; + } + c2_points[0] = Geom::Point(c2[Geom::X], y_closest); + double x_closest; + if (c1[Geom::X] > c2[Geom::X] + wh2[Geom::X] / 2) { + x_closest = c2[Geom::X] + wh2[Geom::X] / 2; + } else if (c1[Geom::X] < c2[Geom::X] - wh2[Geom::X] / 2) { + x_closest = c2[Geom::X] - wh2[Geom::X] / 2; + } else { + x_closest = c1[Geom::X]; + } + c2_points[1] = Geom::Point(x_closest, c2[Geom::Y]); + } + + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + dists.push_back(Geom::L2(c1_points[i] - c2_points[j])); + } + } + + // return the minimum of all dists + return *std::min_element(dists.begin(), dists.end()); + } else { + return dist_r; + } +} + +/** +Average dist from item to others +*/ +double Unclump::average(SPItem *item, std::list<SPItem *> &others) +{ + int n = 0; + double sum = 0; + for (SPItem *other : others) { + if (other == item) + continue; + + n++; + sum += dist(item, other); + } + + if (n != 0) + return sum / n; + else + return 0; +} + +/** +Closest to item among others + */ +SPItem *Unclump::closest(SPItem *item, std::list<SPItem *> &others) +{ + double min = HUGE_VAL; + SPItem *closest = nullptr; + + for (SPItem *other : others) { + if (other == item) + continue; + + double dist = this->dist(item, other); + if (dist < min && fabs(dist) < 1e6) { + min = dist; + closest = other; + } + } + + return closest; +} + +/** +Most distant from item among others + */ +SPItem *Unclump::farthest(SPItem *item, std::list<SPItem *> &others) +{ + double max = -HUGE_VAL; + SPItem *farthest = nullptr; + + for (SPItem *other : others) { + if (other == item) + continue; + + double dist = this->dist(item, other); + if (dist > max && fabs(dist) < 1e6) { + max = dist; + farthest = other; + } + } + + return farthest; +} + +/** +Removes from the \a rest list those items that are "behind" \a closest as seen from \a item, +i.e. those on the other side of the line through \a closest perpendicular to the direction from \a +item to \a closest. Returns a newly created list which must be freed. + */ +std::vector<SPItem *> Unclump::unclump_remove_behind(SPItem *item, SPItem *closest, std::list<SPItem *> &rest) +{ + Geom::Point it = unclump_center(item); + Geom::Point p1 = unclump_center(closest); + + // perpendicular through closest to the direction to item: + Geom::Point perp = Geom::rot90(it - p1); + Geom::Point p2 = p1 + perp; + + // get the standard Ax + By + C = 0 form for p1-p2: + double A = p1[Geom::Y] - p2[Geom::Y]; + double B = p2[Geom::X] - p1[Geom::X]; + double C = p2[Geom::Y] * p1[Geom::X] - p1[Geom::Y] * p2[Geom::X]; + + // substitute the item into it: + double val_item = A * it[Geom::X] + B * it[Geom::Y] + C; + + std::vector<SPItem *> out; + for (SPItem *other : rest) { + if (other == item) + continue; + + Geom::Point o = unclump_center(other); + double val_other = A * o[Geom::X] + B * o[Geom::Y] + C; + + if (val_item * val_other <= 1e-6) { + // different signs, which means item and other are on the different sides of p1-p2 line; skip + } else { + out.push_back(other); + } + } + + return out; +} + +/** +Moves \a what away from \a from by \a dist + */ +void Unclump::push(SPItem *from, SPItem *what, double dist) +{ + Geom::Point it = unclump_center(what); + Geom::Point p = unclump_center(from); + Geom::Point by = dist * Geom::unit_vector(-(p - it)); + + Geom::Affine move = Geom::Translate(by); + + std::map<const gchar *, Geom::Point>::iterator i = c_cache.find(what->getId()); + if (i != c_cache.end()) { + i->second *= move; + } + + // g_print ("push %s at %g,%g from %g,%g by %g,%g, dist %g\n", what->getId(), it[Geom::X],it[Geom::Y], + // p[Geom::X],p[Geom::Y], by[Geom::X],by[Geom::Y], dist); + + what->set_i2d_affine(what->i2dt_affine() * move); + what->doWriteTransform(what->transform); +} + +/** +Moves \a what towards \a to by \a dist + */ +void Unclump::pull(SPItem *to, SPItem *what, double dist) +{ + Geom::Point it = unclump_center(what); + Geom::Point p = unclump_center(to); + Geom::Point by = dist * Geom::unit_vector(p - it); + + Geom::Affine move = Geom::Translate(by); + + std::map<const gchar *, Geom::Point>::iterator i = c_cache.find(what->getId()); + if (i != c_cache.end()) { + i->second *= move; + } + + // g_print ("pull %s at %g,%g to %g,%g by %g,%g, dist %g\n", what->getId(), it[Geom::X],it[Geom::Y], + // p[Geom::X],p[Geom::Y], by[Geom::X],by[Geom::Y], dist); + + what->set_i2d_affine(what->i2dt_affine() * move); + what->doWriteTransform(what->transform); +} + +/** +Unclumps the items in \a items, reducing local unevenness in their distribution. Produces an effect +similar to "engraver dots". The only distribution which is unchanged by unclumping is a hexagonal +grid. May be called repeatedly for stronger effect. + */ +void unclump(std::vector<SPItem *> &items) +{ + Unclump unclump; + + for (SPItem *item : items) { // for each original/clone x: + std::list<SPItem *> nei; + + std::list<SPItem *> rest; + for (size_t i = 0; i < items.size(); i++) { + rest.push_front(items[items.size() - i - 1]); + } + rest.remove(item); + + while (!rest.empty()) { + SPItem *closest = unclump.closest(item, rest); + if (closest) { + nei.push_front(closest); + rest.remove(closest); + std::vector<SPItem *> new_rest = unclump.unclump_remove_behind(item, closest, rest); + rest.clear(); + for (size_t i = 0; i < new_rest.size(); i++) { + rest.push_front(new_rest[new_rest.size() - i - 1]); + } + } else { + break; + } + } + + if ((nei.size()) >= 2) { + double ave = unclump.average(item, nei); + + SPItem *closest = unclump.closest(item, nei); + SPItem *farthest = unclump.farthest(item, nei); + + double dist_closest = unclump.dist(closest, item); + double dist_farthest = unclump.dist(farthest, item); + + // g_print ("NEI %d for item %s closest %s at %g farthest %s at %g ave %g\n", g_slist_length(nei), + // item->getId(), closest->getId(), dist_closest, farthest->getId(), dist_farthest, ave); + + if (fabs(ave) < 1e6 && fabs(dist_closest) < 1e6 && fabs(dist_farthest) < 1e6) { // otherwise the items are + // bogus + // increase these coefficients to make unclumping more aggressive and less stable + // the pull coefficient is a bit bigger to counteract the long-term expansion trend + unclump.push(closest, item, 0.3 * (ave - dist_closest)); + unclump.pull(farthest, item, 0.35 * (dist_farthest - ave)); + } + } + } +} + +/* + 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 : diff --git a/src/object/algorithms/unclump.h b/src/object/algorithms/unclump.h new file mode 100644 index 0000000..313e12d --- /dev/null +++ b/src/object/algorithms/unclump.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Unclumping objects + */ +/* Authors: + * bulia byak + * + * Copyright (C) 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_UNCLUMP_H +#define SEEN_DIALOGS_UNCLUMP_H + +#include <vector> + +class SPItem; + +void unclump(std::vector<SPItem *> &items); + +#endif /* !UNCLUMP_H_SEEN */ + +/* + 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 : diff --git a/src/object/box3d-side.cpp b/src/object/box3d-side.cpp new file mode 100644 index 0000000..d2fefb0 --- /dev/null +++ b/src/object/box3d-side.cpp @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 3D box face implementation + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "box3d-side.h" +#include "document.h" +#include "xml/document.h" +#include "xml/repr.h" +#include "display/curve.h" +#include "svg/svg.h" +#include "attributes.h" +#include "inkscape.h" +#include "object/persp3d.h" +#include "object/persp3d-reference.h" +#include "object/box3d.h" +#include "ui/tools/box3d-tool.h" +#include "desktop-style.h" + +static void box3d_side_compute_corner_ids(Box3DSide *side, unsigned int corners[4]); + +Box3DSide::Box3DSide() : SPPolygon() { + this->dir1 = Box3D::NONE; + this->dir2 = Box3D::NONE; + this->front_or_rear = Box3D::FRONT; +} + +Box3DSide::~Box3DSide() = default; + +void Box3DSide::build(SPDocument * document, Inkscape::XML::Node * repr) { + SPPolygon::build(document, repr); + + this->readAttr(SPAttr::INKSCAPE_BOX3D_SIDE_TYPE); +} + + +Inkscape::XML::Node* Box3DSide::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + // this is where we end up when saving as plain SVG (also in other circumstances?) + // thus we don' set "sodipodi:type" so that the box is only saved as an ordinary svg:path + repr = xml_doc->createElement("svg:path"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + repr->setAttributeInt("inkscape:box3dsidetype", this->dir1 ^ this->dir2 ^ this->front_or_rear); + } + + this->set_shape(); + + /* Duplicate the path */ + SPCurve const *curve = this->_curve.get(); + + //Nulls might be possible if this called iteratively + if ( !curve ) { + return nullptr; + } + + repr->setAttribute("d", sp_svg_write_path(curve->get_pathvector())); + + SPPolygon::write(xml_doc, repr, flags); + + return repr; +} + +void Box3DSide::set(SPAttr key, const gchar* value) { + // TODO: In case the box was recreated (by undo, e.g.) we need to recreate the path + // (along with other info?) from the parent box. + + /* fixme: we should really collect updates */ + switch (key) { + case SPAttr::INKSCAPE_BOX3D_SIDE_TYPE: + if (value) { + guint desc = atoi (value); + + if (!Box3D::is_face_id(desc)) { + g_warning ("desc is not a face id: =%s=", value); + return; + } + + Box3D::Axis plane = (Box3D::Axis) (desc & 0x7); + plane = (Box3D::is_plane(plane) ? plane : Box3D::orth_plane_or_axis(plane)); + this->dir1 = Box3D::extract_first_axis_direction(plane); + this->dir2 = Box3D::extract_second_axis_direction(plane); + this->front_or_rear = (Box3D::FrontOrRear) (desc & 0x8); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + SPPolygon::set(key, value); + break; + } +} + +void Box3DSide::update(SPCtx* ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + flags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; // since we change the description, it's not a "just translation" anymore + } + + if (flags & (SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + this->set_shape(); + } + + SPPolygon::update(ctx, flags); +} + +/* Create a new Box3DSide and append it to the parent box */ +Box3DSide * Box3DSide::createBox3DSide(SPBox3D *box) +{ + Box3DSide *box3d_side = nullptr; + Inkscape::XML::Document *xml_doc = box->document->getReprDoc();; + Inkscape::XML::Node *repr_side = xml_doc->createElement("svg:path"); + repr_side->setAttribute("sodipodi:type", "inkscape:box3dside"); + box3d_side = static_cast<Box3DSide *>(box->appendChildRepr(repr_side)); + return box3d_side; +} + +/* + * Function which return the type attribute for Box3D. + * Acts as a replacement for directly accessing the XML Tree directly. + */ +int Box3DSide::getFaceId() +{ + return this->getIntAttribute("inkscape:box3dsidetype", -1); +} + +void +Box3DSide::position_set () { + this->set_shape(); + + // This call is responsible for live update of the sides during the initial drag + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void Box3DSide::set_shape() { + if (!this->document->getRoot()) { + // avoid a warning caused by sp_document_height() (which is called from sp_item_i2d_affine() below) + // when reading a file containing 3D boxes + return; + } + + SPObject *parent = this->parent; + + auto box = cast<SPBox3D>(parent); + if (!box) { + g_warning("Parent of 3D box side is not a 3D box."); + return; + } + + Persp3D *persp = this->perspective(); + + if (!persp) { + return; + } + + // TODO: Draw the correct quadrangle here + // To do this, determine the perspective of the box, the orientation of the side (e.g., XY-FRONT) + // compute the coordinates of the corners in P^3, project them onto the canvas, and draw the + // resulting path. + + unsigned int corners[4]; + box3d_side_compute_corner_ids(this, corners); + + if (!box->get_corner_screen(corners[0]).isFinite() || + !box->get_corner_screen(corners[1]).isFinite() || + !box->get_corner_screen(corners[2]).isFinite() || + !box->get_corner_screen(corners[3]).isFinite() ) + { + g_warning ("Trying to draw a 3D box side with invalid coordinates."); + return; + } + + SPCurve c; + c.moveto(box->get_corner_screen(corners[0])); + c.lineto(box->get_corner_screen(corners[1])); + c.lineto(box->get_corner_screen(corners[2])); + c.lineto(box->get_corner_screen(corners[3])); + c.closepath(); + + /* Reset the shape's curve to the "original_curve" + * This is very important for LPEs to work properly! (the bbox might be recalculated depending on the curve in shape)*/ + + SPCurve const *before = curveBeforeLPE(); + if (before && before->get_pathvector() != c.get_pathvector()) { + setCurveBeforeLPE(std::move(c)); + sp_lpe_item_update_patheffect(this, true, false); + return; + } + + if (hasPathEffectOnClipOrMaskRecursive(this)) { + setCurveBeforeLPE(std::move(c)); + return; + } + + // This happends on undo, fix bug:#1791784 + setCurveInsync(std::move(c)); +} + +Glib::ustring Box3DSide::axes_string() const +{ + Glib::ustring result(Box3D::string_from_axes((Box3D::Axis) (this->dir1 ^ this->dir2))); + + switch ((Box3D::Axis) (this->dir1 ^ this->dir2)) { + case Box3D::XY: + result += ((this->front_or_rear == Box3D::FRONT) ? "front" : "rear"); + break; + + case Box3D::XZ: + result += ((this->front_or_rear == Box3D::FRONT) ? "top" : "bottom"); + break; + + case Box3D::YZ: + result += ((this->front_or_rear == Box3D::FRONT) ? "right" : "left"); + break; + + default: + break; + } + + return result; +} + +static void +box3d_side_compute_corner_ids(Box3DSide *side, unsigned int corners[4]) { + Box3D::Axis orth = Box3D::third_axis_direction (side->dir1, side->dir2); + + corners[0] = (side->front_or_rear ? orth : 0); + corners[1] = corners[0] ^ side->dir1; + corners[2] = corners[0] ^ side->dir1 ^ side->dir2; + corners[3] = corners[0] ^ side->dir2; +} + +Persp3D * +Box3DSide::perspective() const { + auto box = cast<SPBox3D>(parent); + return box ? box->persp_ref->getObject() : nullptr; +} + +Inkscape::XML::Node *Box3DSide::convert_to_path() const { + // TODO: Copy over all important attributes (see sp_selected_item_to_curved_repr() for an example) + SPDocument *doc = this->document; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("d", this->getAttribute("d")); + repr->setAttribute("style", this->getAttribute("style")); + + return repr; +} + +/* + 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 : diff --git a/src/object/box3d-side.h b/src/object/box3d-side.h new file mode 100644 index 0000000..775662e --- /dev/null +++ b/src/object/box3d-side.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_BOX3D_SIDE_H +#define SEEN_BOX3D_SIDE_H + +/* + * 3D box face implementation + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-polygon.h" +#include "axis-manip.h" + +class SPBox3D; +class Persp3D; + +// FIXME: Would it be better to inherit from SPPath instead? +class Box3DSide final : public SPPolygon { +public: + Box3DSide(); + ~Box3DSide() override; + int tag() const override { return tag_of<decltype(*this)>; } + + Box3D::Axis dir1; + Box3D::Axis dir2; + Box3D::FrontOrRear front_or_rear; + int getFaceId(); + static Box3DSide * createBox3DSide(SPBox3D *box); + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttr key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void update(SPCtx *ctx, unsigned int flags) override; + + void set_shape() override; + + void position_set(); // FIXME: Replace this by Box3DSide::set_shape?? + + Glib::ustring axes_string() const; + + Persp3D *perspective() const; + + + Inkscape::XML::Node *convert_to_path() const; +}; + +#endif // SEEN_BOX3D_SIDE_H + +/* + 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 : diff --git a/src/object/box3d.cpp b/src/object/box3d.cpp new file mode 100644 index 0000000..1efc8bb --- /dev/null +++ b/src/object/box3d.cpp @@ -0,0 +1,1354 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <box3d> implementation + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2007 Authors + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "box3d.h" + +#include <glibmm/i18n.h> +#include "attributes.h" +#include "xml/document.h" +#include "xml/repr.h" + +#include "bad-uri-exception.h" +#include "box3d-side.h" +#include "ui/tools/box3d-tool.h" +#include "perspective-line.h" +#include "persp3d-reference.h" +#include "uri.h" +#include <2geom/line.h> +#include "sp-guide.h" +#include "sp-namedview.h" + +#include "desktop.h" + +#include "include/macros.h" + +static void box3d_ref_changed(SPObject *old_ref, SPObject *ref, SPBox3D *box); + +static gint counter = 0; + +SPBox3D::SPBox3D() : SPGroup() { + this->my_counter = 0; + this->swapped = Box3D::NONE; + + this->persp_href = nullptr; + this->persp_ref = new Persp3DReference(this); + + /* we initialize the z-orders to zero so that they are updated during dragging */ + for (int & z_order : z_orders) { + z_order = 0; + } +} + +SPBox3D::~SPBox3D() = default; + +void SPBox3D::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPGroup::build(document, repr); + + my_counter = counter++; + + /* we initialize the z-orders to zero so that they are updated during dragging */ + for (int & z_order : z_orders) { + z_order = 0; + } + + // TODO: Create/link to the correct perspective + + if ( document ) { + persp_ref->changedSignal().connect(sigc::bind(sigc::ptr_fun(box3d_ref_changed), this)); + + readAttr(SPAttr::INKSCAPE_BOX3D_PERSPECTIVE_ID); + readAttr(SPAttr::INKSCAPE_BOX3D_CORNER0); + readAttr(SPAttr::INKSCAPE_BOX3D_CORNER7); + } +} + +void SPBox3D::release() { + SPBox3D* object = this; + SPBox3D *box = object; + + if (box->persp_href) { + g_free(box->persp_href); + } + + // We have to store this here because the Persp3DReference gets destroyed below, but we need to + // access it to call Persp3D::remove_box(), which cannot be called earlier because the reference + // needs to be destroyed first. + Persp3D *persp = box->get_perspective(); + + if (box->persp_ref) { + box->persp_ref->detach(); + delete box->persp_ref; + box->persp_ref = nullptr; + } + if (persp) { + persp->remove_box (box); + + if (persp->perspective_impl->boxes.empty()) { + SPDocument *doc = box->document; + doc->setCurrentPersp3D(Persp3D::document_first_persp(doc)); + } + } + + SPGroup::release(); +} + +void SPBox3D::set(SPAttr key, const gchar* value) { + SPBox3D* object = this; + SPBox3D *box = object; + + switch (key) { + case SPAttr::INKSCAPE_BOX3D_PERSPECTIVE_ID: + if ( value && box->persp_href && ( strcmp(value, box->persp_href) == 0 ) ) { + /* No change, do nothing. */ + } else { + if (box->persp_href) { + g_free(box->persp_href); + box->persp_href = nullptr; + } + if (value) { + box->persp_href = g_strdup(value); + + // Now do the attaching, which emits the changed signal. + try { + box->persp_ref->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + box->persp_ref->detach(); + } + } else { + // Detach, which emits the changed signal. + box->persp_ref->detach(); + } + } + + // FIXME: Is the following update doubled by some call in either persp3d.cpp or vanishing_point_new.cpp? + box->position_set(); + break; + case SPAttr::INKSCAPE_BOX3D_CORNER0: + if (value && strcmp(value, "0 : 0 : 0 : 0")) { + box->orig_corner0 = Proj::Pt3(value); + box->save_corner0 = box->orig_corner0; + box->position_set(); + } + break; + case SPAttr::INKSCAPE_BOX3D_CORNER7: + if (value && strcmp(value, "0 : 0 : 0 : 0")) { + box->orig_corner7 = Proj::Pt3(value); + box->save_corner7 = box->orig_corner7; + box->position_set(); + } + break; + default: + SPGroup::set(key, value); + break; + } +} + +/** + * Gets called when (re)attached to another perspective. + */ +static void +box3d_ref_changed(SPObject *old_ref, SPObject *ref, SPBox3D *box) +{ + if (old_ref) { + auto oldPersp = cast<Persp3D>(old_ref); + if (oldPersp) { + oldPersp->remove_box(box); + } + } + auto persp = cast<Persp3D>(ref); + if ( persp && (ref != box) ) // FIXME: Comparisons sane? + { + persp->add_box(box); + } +} + +void SPBox3D::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* FIXME?: Perhaps the display updates of box sides should be instantiated from here, but this + causes evil update loops so it's all done from SPBox3D::position_set, which is called from + various other places (like the handlers in shape-editor-knotholders.cpp, vanishing-point.cpp, etc. */ + + } + + // Invoke parent method + SPGroup::update(ctx, flags); +} + +Inkscape::XML::Node* SPBox3D::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + SPBox3D* object = this; + SPBox3D *box = object; + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + // this is where we end up when saving as plain SVG (also in other circumstances?) + // thus we don' set "sodipodi:type" so that the box is only saved as an ordinary svg:g + repr = xml_doc->createElement("svg:g"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + + if (box->persp_href) { + repr->setAttribute("inkscape:perspectiveID", box->persp_href); + } else { + /* box is not yet linked to a perspective; use the document's current perspective */ + SPDocument *doc = object->document; + if (box->persp_ref->getURI()) { + auto uri_string = box->persp_ref->getURI()->str(); + repr->setAttributeOrRemoveIfEmpty("inkscape:perspectiveID", uri_string); + } else { + Glib::ustring href = "#"; + href += doc->getCurrentPersp3D()->getId(); + repr->setAttribute("inkscape:perspectiveID", href); + } + } + + gchar *coordstr0 = box->orig_corner0.coord_string(); + gchar *coordstr7 = box->orig_corner7.coord_string(); + repr->setAttribute("inkscape:corner0", coordstr0); + repr->setAttribute("inkscape:corner7", coordstr7); + g_free(coordstr0); + g_free(coordstr7); + + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + + box->save_corner0 = box->orig_corner0; + box->save_corner7 = box->orig_corner7; + } + + SPGroup::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPBox3D::display_name() { + return _("3D Box"); +} + +void SPBox3D::position_set() +{ + /* This draws the curve and calls requestDisplayUpdate() for each side (the latter is done in + Box3DSide::position_set() to avoid update conflicts with the parent box) */ + for (auto& obj: this->children) { + auto side = cast<Box3DSide>(&obj); + if (side) { + side->position_set(); + } + } +} + +Geom::Affine SPBox3D::set_transform(Geom::Affine const &xform) { + // We don't apply the transform to the box directly but instead to its perspective (which is + // done in sp_selection_apply_affine). Here we only adjust strokes, patterns, etc. + + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + gdouble const sw = hypot(ret[0], ret[1]); + gdouble const sh = hypot(ret[2], ret[3]); + + for (auto& child: children) { + auto childitem = cast<SPItem>(&child); + if (childitem) { + // Adjust stroke width + childitem->adjust_stroke(sqrt(fabs(sw * sh))); + + // Adjust pattern fill + childitem->adjust_pattern(xform); + + // Adjust gradient fill + childitem->adjust_gradient(xform); + } + } + + return Geom::identity(); +} + +static Proj::Pt3 +box3d_get_proj_corner (guint id, Proj::Pt3 const &c0, Proj::Pt3 const &c7) { + return Proj::Pt3 ((id & Box3D::X) ? c7[Proj::X] : c0[Proj::X], + (id & Box3D::Y) ? c7[Proj::Y] : c0[Proj::Y], + (id & Box3D::Z) ? c7[Proj::Z] : c0[Proj::Z], + 1.0); +} + +static Proj::Pt3 +box3d_get_proj_corner (SPBox3D const *box, guint id) { + return Proj::Pt3 ((id & Box3D::X) ? box->orig_corner7[Proj::X] : box->orig_corner0[Proj::X], + (id & Box3D::Y) ? box->orig_corner7[Proj::Y] : box->orig_corner0[Proj::Y], + (id & Box3D::Z) ? box->orig_corner7[Proj::Z] : box->orig_corner0[Proj::Z], + 1.0); +} + +Geom::Point +SPBox3D::get_corner_screen (guint id, bool item_coords) const { + Proj::Pt3 proj_corner (box3d_get_proj_corner (this, id)); + if (!this->get_perspective()) { + return Geom::Point (Geom::infinity(), Geom::infinity()); + } + Geom::Affine const i2d(this->i2dt_affine ()); + if (item_coords) { + return this->get_perspective()->perspective_impl->tmat.image(proj_corner).affine() * i2d.inverse(); + } else { + return this->get_perspective()->perspective_impl->tmat.image(proj_corner).affine(); + } +} + +Proj::Pt3 +SPBox3D::get_proj_center () { + this->orig_corner0.normalize(); + this->orig_corner7.normalize(); + return Proj::Pt3 ((this->orig_corner0[Proj::X] + this->orig_corner7[Proj::X]) / 2, + (this->orig_corner0[Proj::Y] + this->orig_corner7[Proj::Y]) / 2, + (this->orig_corner0[Proj::Z] + this->orig_corner7[Proj::Z]) / 2, + 1.0); +} + +Geom::Point +SPBox3D::get_center_screen () { + Proj::Pt3 proj_center (this->get_proj_center ()); + if (!this->get_perspective()) { + return Geom::Point (Geom::infinity(), Geom::infinity()); + } + Geom::Affine const i2d( this->i2dt_affine() ); + return this->get_perspective()->perspective_impl->tmat.image(proj_center).affine() * i2d.inverse(); +} + +/* + * To keep the snappoint from jumping randomly between the two lines when the mouse pointer is close to + * their intersection, we remember the last snapped line and keep snapping to this specific line as long + * as the distance from the intersection to the mouse pointer is less than remember_snap_threshold. + */ + +// Should we make the threshold settable in the preferences? +static double remember_snap_threshold = 30; +static guint remember_snap_index = 0; + +// constant for sizing the array of points to be considered: +static const int MAX_POINT_COUNT = 4; + +static Proj::Pt3 +box3d_snap (SPBox3D *box, int id, Proj::Pt3 const &pt_proj, Proj::Pt3 const &start_pt) { + double z_coord = start_pt[Proj::Z]; + double diff_x = box->save_corner7[Proj::X] - box->save_corner0[Proj::X]; + double diff_y = box->save_corner7[Proj::Y] - box->save_corner0[Proj::Y]; + double x_coord = start_pt[Proj::X]; + double y_coord = start_pt[Proj::Y]; + Proj::Pt3 A_proj (x_coord, y_coord, z_coord, 1.0); + Proj::Pt3 B_proj (x_coord + diff_x, y_coord, z_coord, 1.0); + Proj::Pt3 C_proj (x_coord + diff_x, y_coord + diff_y, z_coord, 1.0); + Proj::Pt3 D_proj (x_coord, y_coord + diff_y, z_coord, 1.0); + Proj::Pt3 E_proj (x_coord - diff_x, y_coord + diff_y, z_coord, 1.0); + + auto persp_impl = box->get_perspective()->perspective_impl.get(); + Geom::Point A = persp_impl->tmat.image(A_proj).affine(); + Geom::Point B = persp_impl->tmat.image(B_proj).affine(); + Geom::Point C = persp_impl->tmat.image(C_proj).affine(); + Geom::Point D = persp_impl->tmat.image(D_proj).affine(); + Geom::Point E = persp_impl->tmat.image(E_proj).affine(); + Geom::Point pt = persp_impl->tmat.image(pt_proj).affine(); + + // TODO: Replace these lines between corners with lines from a corner to a vanishing point + // (this might help to prevent rounding errors if the box is small) + Box3D::Line pl1(A, B); + Box3D::Line pl2(A, D); + Box3D::Line diag1(A, (id == -1 || (!(id & Box3D::X) == !(id & Box3D::Y))) ? C : E); + Box3D::Line diag2(A, E); // diag2 is only taken into account if id equals -1, i.e., if we are snapping the center + + int num_snap_lines = (id != -1) ? 3 : 4; + Geom::Point snap_pts[MAX_POINT_COUNT]; + + snap_pts[0] = pl1.closest_to (pt); + snap_pts[1] = pl2.closest_to (pt); + snap_pts[2] = diag1.closest_to (pt); + if (id == -1) { + snap_pts[3] = diag2.closest_to (pt); + } + + gdouble const zoom = SP_ACTIVE_DESKTOP->current_zoom(); + + // determine the distances to all potential snapping points + double snap_dists[MAX_POINT_COUNT]; + for (int i = 0; i < num_snap_lines; ++i) { + snap_dists[i] = Geom::L2 (snap_pts[i] - pt) * zoom; + } + + // while we are within a given tolerance of the starting point, + // keep snapping to the same point to avoid jumping + bool within_tolerance = true; + for (int i = 0; i < num_snap_lines; ++i) { + if (snap_dists[i] > remember_snap_threshold) { + within_tolerance = false; + break; + } + } + + // find the closest snapping point + int snap_index = -1; + double snap_dist = Geom::infinity(); + for (int i = 0; i < num_snap_lines; ++i) { + if (snap_dists[i] < snap_dist) { + snap_index = i; + snap_dist = snap_dists[i]; + } + } + + // snap to the closest point (or the previously remembered one + // if we are within tolerance of the starting point) + Geom::Point result; + if (within_tolerance) { + result = snap_pts[remember_snap_index]; + } else { + remember_snap_index = snap_index; + result = snap_pts[snap_index]; + } + return box->get_perspective()->perspective_impl->tmat.preimage (result, z_coord, Proj::Z); +} + +SPBox3D * SPBox3D::createBox3D(SPItem * parent) +{ + SPBox3D *box3d = nullptr; + Inkscape::XML::Document *xml_doc = parent->document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:g"); + repr->setAttribute("sodipodi:type", "inkscape:box3d"); + box3d = reinterpret_cast<SPBox3D *>(parent->appendChildRepr(repr)); + return box3d; +} + +void +SPBox3D::set_corner (const guint id, Geom::Point const &new_pos, const Box3D::Axis movement, bool constrained) { + g_return_if_fail ((movement != Box3D::NONE) && (movement != Box3D::XYZ)); + + this->orig_corner0.normalize(); + this->orig_corner7.normalize(); + + /* update corners 0 and 7 according to which handle was moved and to the axes of movement */ + if (!(movement & Box3D::Z)) { + auto persp_impl = get_perspective()->perspective_impl.get(); + Proj::Pt3 pt_proj (persp_impl->tmat.preimage (new_pos, (id < 4) ? this->orig_corner0[Proj::Z] : + this->orig_corner7[Proj::Z], Proj::Z)); + if (constrained) { + pt_proj = box3d_snap (this, id, pt_proj, box3d_get_proj_corner (id, this->save_corner0, this->save_corner7)); + } + + // normalizing pt_proj is essential because we want to mingle affine coordinates + pt_proj.normalize(); + this->orig_corner0 = Proj::Pt3 ((id & Box3D::X) ? this->save_corner0[Proj::X] : pt_proj[Proj::X], + (id & Box3D::Y) ? this->save_corner0[Proj::Y] : pt_proj[Proj::Y], + this->save_corner0[Proj::Z], + 1.0); + this->orig_corner7 = Proj::Pt3 ((id & Box3D::X) ? pt_proj[Proj::X] : this->save_corner7[Proj::X], + (id & Box3D::Y) ? pt_proj[Proj::Y] : this->save_corner7[Proj::Y], + this->save_corner7[Proj::Z], + 1.0); + } else { + Persp3D *persp = this->get_perspective(); + auto persp_impl = persp->perspective_impl.get(); + Box3D::PerspectiveLine pl(persp_impl->tmat.image( + box3d_get_proj_corner (id, this->save_corner0, this->save_corner7)).affine(), + Proj::Z, persp); + Geom::Point new_pos_snapped(pl.closest_to(new_pos)); + Proj::Pt3 pt_proj (persp_impl->tmat.preimage (new_pos_snapped, + box3d_get_proj_corner (this, id)[(movement & Box3D::Y) ? Proj::X : Proj::Y], + (movement & Box3D::Y) ? Proj::X : Proj::Y)); + bool corner0_move_x = !(id & Box3D::X) && (movement & Box3D::X); + bool corner0_move_y = !(id & Box3D::Y) && (movement & Box3D::Y); + bool corner7_move_x = (id & Box3D::X) && (movement & Box3D::X); + bool corner7_move_y = (id & Box3D::Y) && (movement & Box3D::Y); + // normalizing pt_proj is essential because we want to mingle affine coordinates + pt_proj.normalize(); + this->orig_corner0 = Proj::Pt3 (corner0_move_x ? pt_proj[Proj::X] : this->orig_corner0[Proj::X], + corner0_move_y ? pt_proj[Proj::Y] : this->orig_corner0[Proj::Y], + (id & Box3D::Z) ? this->orig_corner0[Proj::Z] : pt_proj[Proj::Z], + 1.0); + this->orig_corner7 = Proj::Pt3 (corner7_move_x ? pt_proj[Proj::X] : this->orig_corner7[Proj::X], + corner7_move_y ? pt_proj[Proj::Y] : this->orig_corner7[Proj::Y], + (id & Box3D::Z) ? pt_proj[Proj::Z] : this->orig_corner7[Proj::Z], + 1.0); + } + // FIXME: Should we update the box here? If so, how? +} + +void SPBox3D::set_center (Geom::Point const &new_pos, Geom::Point const &old_pos, const Box3D::Axis movement, bool constrained) { + g_return_if_fail ((movement != Box3D::NONE) && (movement != Box3D::XYZ)); + + this->orig_corner0.normalize(); + this->orig_corner7.normalize(); + + Persp3D *persp = this->get_perspective(); + if (!(movement & Box3D::Z)) { + double coord = (this->orig_corner0[Proj::Z] + this->orig_corner7[Proj::Z]) / 2; + double radx = (this->orig_corner7[Proj::X] - this->orig_corner0[Proj::X]) / 2; + double rady = (this->orig_corner7[Proj::Y] - this->orig_corner0[Proj::Y]) / 2; + + Proj::Pt3 pt_proj (persp->perspective_impl->tmat.preimage (new_pos, coord, Proj::Z)); + if (constrained) { + Proj::Pt3 old_pos_proj (persp->perspective_impl->tmat.preimage (old_pos, coord, Proj::Z)); + old_pos_proj.normalize(); + pt_proj = box3d_snap (this, -1, pt_proj, old_pos_proj); + } + // normalizing pt_proj is essential because we want to mingle affine coordinates + pt_proj.normalize(); + this->orig_corner0 = Proj::Pt3 ((movement & Box3D::X) ? pt_proj[Proj::X] - radx : this->orig_corner0[Proj::X], + (movement & Box3D::Y) ? pt_proj[Proj::Y] - rady : this->orig_corner0[Proj::Y], + this->orig_corner0[Proj::Z], + 1.0); + this->orig_corner7 = Proj::Pt3 ((movement & Box3D::X) ? pt_proj[Proj::X] + radx : this->orig_corner7[Proj::X], + (movement & Box3D::Y) ? pt_proj[Proj::Y] + rady : this->orig_corner7[Proj::Y], + this->orig_corner7[Proj::Z], + 1.0); + } else { + double coord = (this->orig_corner0[Proj::X] + this->orig_corner7[Proj::X]) / 2; + double radz = (this->orig_corner7[Proj::Z] - this->orig_corner0[Proj::Z]) / 2; + + Box3D::PerspectiveLine pl(old_pos, Proj::Z, persp); + Geom::Point new_pos_snapped(pl.closest_to(new_pos)); + Proj::Pt3 pt_proj (persp->perspective_impl->tmat.preimage (new_pos_snapped, coord, Proj::X)); + + /* normalizing pt_proj is essential because we want to mingle affine coordinates */ + pt_proj.normalize(); + this->orig_corner0 = Proj::Pt3 (this->orig_corner0[Proj::X], + this->orig_corner0[Proj::Y], + pt_proj[Proj::Z] - radz, + 1.0); + this->orig_corner7 = Proj::Pt3 (this->orig_corner7[Proj::X], + this->orig_corner7[Proj::Y], + pt_proj[Proj::Z] + radz, + 1.0); + } +} + +/* + * Manipulates corner1 through corner4 to contain the indices of the corners + * from which the perspective lines in the direction of 'axis' emerge + */ +void SPBox3D::corners_for_PLs (Proj::Axis axis, + Geom::Point &corner1, Geom::Point &corner2, Geom::Point &corner3, Geom::Point &corner4) const +{ + Persp3D *persp = this->get_perspective(); + g_return_if_fail (persp); + auto persp_impl = persp->perspective_impl.get(); + //this->orig_corner0.normalize(); + //this->orig_corner7.normalize(); + double coord = (this->orig_corner0[axis] > this->orig_corner7[axis]) ? + this->orig_corner0[axis] : + this->orig_corner7[axis]; + + Proj::Pt3 c1, c2, c3, c4; + // FIXME: This can certainly be done more elegantly/efficiently than by a case-by-case analysis. + switch (axis) { + case Proj::X: + c1 = Proj::Pt3 (coord, this->orig_corner0[Proj::Y], this->orig_corner0[Proj::Z], 1.0); + c2 = Proj::Pt3 (coord, this->orig_corner7[Proj::Y], this->orig_corner0[Proj::Z], 1.0); + c3 = Proj::Pt3 (coord, this->orig_corner7[Proj::Y], this->orig_corner7[Proj::Z], 1.0); + c4 = Proj::Pt3 (coord, this->orig_corner0[Proj::Y], this->orig_corner7[Proj::Z], 1.0); + break; + case Proj::Y: + c1 = Proj::Pt3 (this->orig_corner0[Proj::X], coord, this->orig_corner0[Proj::Z], 1.0); + c2 = Proj::Pt3 (this->orig_corner7[Proj::X], coord, this->orig_corner0[Proj::Z], 1.0); + c3 = Proj::Pt3 (this->orig_corner7[Proj::X], coord, this->orig_corner7[Proj::Z], 1.0); + c4 = Proj::Pt3 (this->orig_corner0[Proj::X], coord, this->orig_corner7[Proj::Z], 1.0); + break; + case Proj::Z: + c1 = Proj::Pt3 (this->orig_corner7[Proj::X], this->orig_corner7[Proj::Y], coord, 1.0); + c2 = Proj::Pt3 (this->orig_corner7[Proj::X], this->orig_corner0[Proj::Y], coord, 1.0); + c3 = Proj::Pt3 (this->orig_corner0[Proj::X], this->orig_corner0[Proj::Y], coord, 1.0); + c4 = Proj::Pt3 (this->orig_corner0[Proj::X], this->orig_corner7[Proj::Y], coord, 1.0); + break; + default: + return; + } + corner1 = persp_impl->tmat.image(c1).affine(); + corner2 = persp_impl->tmat.image(c2).affine(); + corner3 = persp_impl->tmat.image(c3).affine(); + corner4 = persp_impl->tmat.image(c4).affine(); +} + +/* Auxiliary function: Checks whether the half-line from A to B crosses the line segment joining C and D */ +static bool +box3d_half_line_crosses_joining_line (Geom::Point const &A, Geom::Point const &B, + Geom::Point const &C, Geom::Point const &D) { + Geom::Point n0 = (B - A).ccw(); + double d0 = dot(n0,A); + + Geom::Point n1 = (D - C).ccw(); + double d1 = dot(n1,C); + + Geom::Line lineAB(A,B); + Geom::Line lineCD(C,D); + + Geom::OptCrossing inters = Geom::OptCrossing(); // empty by default + try + { + inters = Geom::intersection(lineAB, lineCD); + } + catch (Geom::InfiniteSolutions& e) + { + // We're probably dealing with parallel lines, so they don't really cross + return false; + } + + if (!inters) { + return false; + } + + Geom::Point E = lineAB.pointAt((*inters).ta); // the point of intersection + + if ((dot(C,n0) < d0) == (dot(D,n0) < d0)) { + // C and D lie on the same side of the line AB + return false; + } + if ((dot(A,n1) < d1) != (dot(B,n1) < d1)) { + // A and B lie on different sides of the line CD + return true; + } else if (Geom::distance(E,A) < Geom::distance(E,B)) { + // The line CD passes on the "wrong" side of A + return false; + } + + // The line CD passes on the "correct" side of A + return true; +} + +static bool +box3d_XY_axes_are_swapped (SPBox3D *box) { + Persp3D *persp = box->get_perspective(); + g_return_val_if_fail(persp, false); + Box3D::PerspectiveLine l1(box->get_corner_screen(3, false), Proj::X, persp); + Box3D::PerspectiveLine l2(box->get_corner_screen(3, false), Proj::Y, persp); + Geom::Point v1(l1.direction()); + Geom::Point v2(l2.direction()); + v1.normalize(); + v2.normalize(); + + return (v1[Geom::X]*v2[Geom::Y] - v1[Geom::Y]*v2[Geom::X] > 0); +} + +static inline void +box3d_aux_set_z_orders (int z_orders[6], int a, int b, int c, int d, int e, int f) { + // TODO add function argument: SPDocument *doc = box->document + auto doc = SP_ACTIVE_DOCUMENT; + + if (doc->is_yaxisdown()) { + std::swap(a, f); + std::swap(b, e); + std::swap(c, d); + } + + z_orders[0] = a; + z_orders[1] = b; + z_orders[2] = c; + z_orders[3] = d; + z_orders[4] = e; + z_orders[5] = f; +} + + +/* + * In standard perspective we have: + * 2 = front face + * 1 = top face + * 0 = left face + * 3 = right face + * 4 = bottom face + * 5 = rear face + */ + +/* All VPs infinite */ +static void +box3d_set_new_z_orders_case0 (SPBox3D *box, int z_orders[6], Box3D::Axis central_axis) { + bool swapped = box3d_XY_axes_are_swapped(box); + + switch(central_axis) { + case Box3D::X: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 0, 4, 1, 3, 5); + } else { + box3d_aux_set_z_orders (z_orders, 3, 1, 5, 2, 4, 0); + } + break; + case Box3D::Y: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 4, 0, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 4, 1, 3, 2); + } + break; + case Box3D::Z: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 0, 1, 4, 3, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 3, 4, 1, 0, 2); + } + break; + case Box3D::NONE: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 3, 4, 1, 0, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 1, 4, 3, 2); + } + break; + default: + g_assert_not_reached(); + break; + } +} + +/* Precisely one finite VP */ +static void +box3d_set_new_z_orders_case1 (SPBox3D *box, int z_orders[6], Box3D::Axis central_axis, Box3D::Axis fin_axis) { + Persp3D *persp = box->get_perspective(); + Geom::Point vp(persp->get_VP(Box3D::toProj(fin_axis)).affine()); + + // note: in some of the case distinctions below we rely upon the fact that oaxis1 and oaxis2 are ordered + Box3D::Axis oaxis1 = Box3D::get_remaining_axes(fin_axis).first; + Box3D::Axis oaxis2 = Box3D::get_remaining_axes(fin_axis).second; + int inside1 = 0; + int inside2 = 0; + inside1 = box->pt_lies_in_PL_sector (vp, 3, 3 ^ oaxis2, oaxis1); + inside2 = box->pt_lies_in_PL_sector (vp, 3, 3 ^ oaxis1, oaxis2); + + bool swapped = box3d_XY_axes_are_swapped(box); + + switch(central_axis) { + case Box3D::X: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 4, 0, 1, 3, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 3, 1, 0, 2, 4); + } + break; + case Box3D::Y: + if (inside2 > 0) { + box3d_aux_set_z_orders (z_orders, 1, 2, 3, 0, 5, 4); + } else if (inside2 < 0) { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 4, 0, 5); + } else { + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 5, 0, 4); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 4, 1, 3, 2); + } + } + break; + case Box3D::Z: + if (inside2) { + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 1, 3, 0, 4, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 3, 4, 0, 1, 2); + } + } else if (inside1) { + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 0, 1, 4, 3, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 3, 4, 1, 0, 2); + } + } else { + // "regular" case + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 0, 1, 2, 5, 4, 3); + } else { + box3d_aux_set_z_orders (z_orders, 5, 3, 4, 0, 2, 1); + } + } + break; + case Box3D::NONE: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 3, 4, 5, 0, 1); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 1, 3, 2, 4); + } + break; + default: + g_assert_not_reached(); + } +} + +/* Precisely 2 finite VPs */ +static void +box3d_set_new_z_orders_case2 (SPBox3D *box, int z_orders[6], Box3D::Axis central_axis, Box3D::Axis /*infinite_axis*/) { + bool swapped = box3d_XY_axes_are_swapped(box); + + int insidexy = box->VP_lies_in_PL_sector (Proj::X, 3, 3 ^ Box3D::Z, Box3D::Y); + //int insidexz = box->VP_lies_in_PL_sector (Proj::X, 3, 3 ^ Box3D::Y, Box3D::Z); + + int insideyx = box->VP_lies_in_PL_sector (Proj::Y, 3, 3 ^ Box3D::Z, Box3D::X); + int insideyz = box->VP_lies_in_PL_sector (Proj::Y, 3, 3 ^ Box3D::X, Box3D::Z); + + //int insidezx = box->VP_lies_in_PL_sector (Proj::Z, 3, 3 ^ Box3D::Y, Box3D::X); + int insidezy = box->VP_lies_in_PL_sector (Proj::Z, 3, 3 ^ Box3D::X, Box3D::Y); + + switch(central_axis) { + case Box3D::X: + if (!swapped) { + if (insidezy == -1) { + box3d_aux_set_z_orders (z_orders, 2, 4, 0, 1, 3, 5); + } else if (insidexy == 1) { + box3d_aux_set_z_orders (z_orders, 2, 4, 0, 5, 1, 3); + } else { + box3d_aux_set_z_orders (z_orders, 2, 4, 0, 1, 3, 5); + } + } else { + if (insideyz == -1) { + box3d_aux_set_z_orders (z_orders, 3, 1, 5, 0, 2, 4); + } else { + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 3, 1, 5, 2, 4, 0); + } else { + if (insidexy == 0) { + box3d_aux_set_z_orders (z_orders, 3, 5, 1, 0, 2, 4); + } else { + box3d_aux_set_z_orders (z_orders, 3, 1, 5, 0, 2, 4); + } + } + } + } + break; + case Box3D::Y: + if (!swapped) { + if (insideyz == 1) { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 0, 5, 4); + } else { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 5, 0, 4); + } + } else { + if (insideyx == 1) { + box3d_aux_set_z_orders (z_orders, 4, 0, 5, 1, 3, 2); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 4, 1, 3, 2); + } + } + break; + case Box3D::Z: + if (!swapped) { + if (insidezy == 1) { + box3d_aux_set_z_orders (z_orders, 2, 1, 0, 4, 3, 5); + } else if (insidexy == -1) { + box3d_aux_set_z_orders (z_orders, 2, 1, 0, 5, 4, 3); + } else { + box3d_aux_set_z_orders (z_orders, 2, 0, 1, 5, 3, 4); + } + } else { + box3d_aux_set_z_orders (z_orders, 3, 4, 5, 1, 0, 2); + } + break; + case Box3D::NONE: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 3, 4, 1, 0, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 1, 4, 3, 2); + } + break; + default: + g_assert_not_reached(); + break; + } +} + +/* + * It can happen that during dragging the box is everted. + * In this case the opposite sides in this direction need to be swapped + */ +static Box3D::Axis +box3d_everted_directions (SPBox3D *box) { + Box3D::Axis ev = Box3D::NONE; + + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + + if (box->orig_corner0[Proj::X] < box->orig_corner7[Proj::X]) + ev = (Box3D::Axis) (ev ^ Box3D::X); + if (box->orig_corner0[Proj::Y] < box->orig_corner7[Proj::Y]) + ev = (Box3D::Axis) (ev ^ Box3D::Y); + if (box->orig_corner0[Proj::Z] > box->orig_corner7[Proj::Z]) // FIXME: Remove the need to distinguish signs among the cases + ev = (Box3D::Axis) (ev ^ Box3D::Z); + + return ev; +} + +static void +box3d_swap_sides(int z_orders[6], Box3D::Axis axis) { + int pos1 = -1; + int pos2 = -1; + + for (int i = 0; i < 6; ++i) { + if (!(Box3D::int_to_face(z_orders[i]) & axis)) { + if (pos1 == -1) { + pos1 = i; + } else { + pos2 = i; + break; + } + } + } + + if ((pos1 != -1) && (pos2 != -1)){ + int tmp = z_orders[pos1]; + z_orders[pos1] = z_orders[pos2]; + z_orders[pos2] = tmp; + } +} + + +bool +SPBox3D::recompute_z_orders () { + Persp3D *persp = this->get_perspective(); + + if (!persp) + return false; + + int z_orders[6]; + + Geom::Point c3(this->get_corner_screen(3, false)); + + // determine directions from corner3 to the VPs + int num_finite = 0; + Box3D::Axis axis_finite = Box3D::NONE; + Box3D::Axis axis_infinite = Box3D::NONE; + Geom::Point dirs[3]; + for (int i = 0; i < 3; ++i) { + dirs[i] = persp->get_PL_dir_from_pt(c3, Box3D::toProj(Box3D::axes[i])); + if (Persp3D::VP_is_finite(persp->perspective_impl.get(), Proj::axes[i])) { + num_finite++; + axis_finite = Box3D::axes[i]; + } else { + axis_infinite = Box3D::axes[i]; + } + } + + // determine the "central" axis (if there is one) + Box3D::Axis central_axis = Box3D::NONE; + if(Box3D::lies_in_sector(dirs[0], dirs[1], dirs[2])) { + central_axis = Box3D::Z; + } else if(Box3D::lies_in_sector(dirs[1], dirs[2], dirs[0])) { + central_axis = Box3D::X; + } else if(Box3D::lies_in_sector(dirs[2], dirs[0], dirs[1])) { + central_axis = Box3D::Y; + } + + switch (num_finite) { + case 0: + // TODO: Remark: In this case (and maybe one of the others, too) the z-orders for all boxes + // coincide, hence only need to be computed once in a more central location. + box3d_set_new_z_orders_case0(this, z_orders, central_axis); + break; + case 1: + box3d_set_new_z_orders_case1(this, z_orders, central_axis, axis_finite); + break; + case 2: + case 3: + box3d_set_new_z_orders_case2(this, z_orders, central_axis, axis_infinite); + break; + default: + /* + * For each VP F, check whether the half-line from the corner3 to F crosses the line segment + * joining the other two VPs. If this is the case, it determines the "central" corner from + * which the visible sides can be deduced. Otherwise, corner3 is the central corner. + */ + // FIXME: We should eliminate the use of Geom::Point altogether + Box3D::Axis central_axis = Box3D::NONE; + Geom::Point vp_x = persp->get_VP(Proj::X).affine(); + Geom::Point vp_y = persp->get_VP(Proj::Y).affine(); + Geom::Point vp_z = persp->get_VP(Proj::Z).affine(); + Geom::Point vpx(vp_x[Geom::X], vp_x[Geom::Y]); + Geom::Point vpy(vp_y[Geom::X], vp_y[Geom::Y]); + Geom::Point vpz(vp_z[Geom::X], vp_z[Geom::Y]); + + Geom::Point c3 = this->get_corner_screen(3, false); + Geom::Point corner3(c3[Geom::X], c3[Geom::Y]); + + if (box3d_half_line_crosses_joining_line (corner3, vpx, vpy, vpz)) { + central_axis = Box3D::X; + } else if (box3d_half_line_crosses_joining_line (corner3, vpy, vpz, vpx)) { + central_axis = Box3D::Y; + } else if (box3d_half_line_crosses_joining_line (corner3, vpz, vpx, vpy)) { + central_axis = Box3D::Z; + } + + // FIXME: At present, this is not used. Why is it calculated? + /* + unsigned int central_corner = 3 ^ central_axis; + if (central_axis == Box3D::Z) { + central_corner = central_corner ^ Box3D::XYZ; + } + if (box3d_XY_axes_are_swapped(this)) { + central_corner = central_corner ^ Box3D::XYZ; + } + */ + + Geom::Point c1(this->get_corner_screen(1, false)); + Geom::Point c2(this->get_corner_screen(2, false)); + Geom::Point c7(this->get_corner_screen(7, false)); + + Geom::Point corner1(c1[Geom::X], c1[Geom::Y]); + Geom::Point corner2(c2[Geom::X], c2[Geom::Y]); + Geom::Point corner7(c7[Geom::X], c7[Geom::Y]); + // FIXME: At present we don't use the information about central_corner computed above. + switch (central_axis) { + case Box3D::Y: + if (!box3d_half_line_crosses_joining_line(vpz, vpy, corner3, corner2)) { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 5, 0, 4); + } else { + // degenerate case + box3d_aux_set_z_orders (z_orders, 2, 1, 3, 0, 5, 4); + } + break; + + case Box3D::Z: + if (box3d_half_line_crosses_joining_line(vpx, vpz, corner3, corner1)) { + // degenerate case + box3d_aux_set_z_orders (z_orders, 2, 0, 1, 4, 3, 5); + } else if (box3d_half_line_crosses_joining_line(vpx, vpy, corner3, corner7)) { + // degenerate case + box3d_aux_set_z_orders (z_orders, 2, 1, 0, 5, 3, 4); + } else { + box3d_aux_set_z_orders (z_orders, 2, 1, 0, 3, 4, 5); + } + break; + + case Box3D::X: + if (box3d_half_line_crosses_joining_line(vpz, vpx, corner3, corner1)) { + // degenerate case + box3d_aux_set_z_orders (z_orders, 2, 1, 0, 4, 5, 3); + } else { + box3d_aux_set_z_orders (z_orders, 2, 4, 0, 5, 1, 3); + } + break; + + case Box3D::NONE: + box3d_aux_set_z_orders (z_orders, 2, 3, 4, 1, 0, 5); + break; + + default: + g_assert_not_reached(); + break; + } // end default case + } + + // TODO: If there are still errors in z-orders of everted boxes, we need to choose a variable corner + // instead of the hard-coded corner #3 in the computations above + Box3D::Axis ev = box3d_everted_directions(this); + for (auto & axe : Box3D::axes) { + if (ev & axe) { + box3d_swap_sides(z_orders, axe); + } + } + + // Check whether anything actually changed + for (int i = 0; i < 6; ++i) { + if (this->z_orders[i] != z_orders[i]) { + for (int j = i; j < 6; ++j) { + this->z_orders[j] = z_orders[j]; + } + return true; + } + } + return false; +} + +static std::map<int, Box3DSide *> box3d_get_sides(SPBox3D *box) +{ + std::map<int, Box3DSide *> sides; + for (auto& obj: box->children) { + auto side = cast<Box3DSide>(&obj); + if (side) { + sides[Box3D::face_to_int(side->getFaceId())] = side; + } + } + sides.erase(-1); + return sides; +} + + +// TODO: Check whether the box is everted in any direction and swap the sides opposite to this direction +void +SPBox3D::set_z_orders () { + // For efficiency reasons, we only set the new z-orders if something really changed + if (this->recompute_z_orders ()) { + std::map<int, Box3DSide *> sides = box3d_get_sides(this); + std::map<int, Box3DSide *>::iterator side; + for (int z_order : this->z_orders) { + side = sides.find(z_order); + if (side != sides.end()) { + ((*side).second)->lowerToBottom(); + } + } + } +} + +/* + * Auxiliary function for z-order recomputing: + * Determines whether \a pt lies in the sector formed by the two PLs from the corners with IDs + * \a i21 and \a id2 to the VP in direction \a axis. If the VP is infinite, we say that \a pt + * lies in the sector if it lies between the two (parallel) PLs. + * \ret * 0 if \a pt doesn't lie in the sector + * * 1 if \a pt lies in the sector and either VP is finite of VP is infinite and the direction + * from the edge between the two corners to \a pt points towards the VP + * * -1 otherwise + */ +// TODO: Maybe it would be useful to have a similar method for projective points pt because then we +// can use it for VPs and perhaps merge the case distinctions during z-order recomputation. +int +SPBox3D::pt_lies_in_PL_sector (Geom::Point const &pt, int id1, int id2, Box3D::Axis axis) const { + Persp3D *persp = this->get_perspective(); + + // the two corners + Geom::Point c1(this->get_corner_screen(id1, false)); + Geom::Point c2(this->get_corner_screen(id2, false)); + + int ret = 0; + if (Persp3D::VP_is_finite(persp->perspective_impl.get(), Box3D::toProj(axis))) { + Geom::Point vp(persp->get_VP(Box3D::toProj(axis)).affine()); + Geom::Point v1(c1 - vp); + Geom::Point v2(c2 - vp); + Geom::Point w(pt - vp); + ret = static_cast<int>(Box3D::lies_in_sector(v1, v2, w)); + } else { + Box3D::PerspectiveLine pl1(c1, Box3D::toProj(axis), persp); + Box3D::PerspectiveLine pl2(c2, Box3D::toProj(axis), persp); + if (pl1.lie_on_same_side(pt, c2) && pl2.lie_on_same_side(pt, c1)) { + // test whether pt lies "towards" or "away from" the VP + Box3D::Line edge(c1,c2); + Geom::Point c3(this->get_corner_screen(id1 ^ axis, false)); + if (edge.lie_on_same_side(pt, c3)) { + ret = 1; + } else { + ret = -1; + } + } + } + return ret; +} + +int +SPBox3D::VP_lies_in_PL_sector (Proj::Axis vpdir, int id1, int id2, Box3D::Axis axis) const { + Persp3D *persp = this->get_perspective(); + + if (!Persp3D::VP_is_finite(persp->perspective_impl.get(), vpdir)) { + return 0; + } else { + return this->pt_lies_in_PL_sector(persp->get_VP(vpdir).affine(), id1, id2, axis); + } +} + +/* swap the coordinates of corner0 and corner7 along the specified axis */ +static void +box3d_swap_coords(SPBox3D *box, Proj::Axis axis, bool smaller = true) { + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + if ((box->orig_corner0[axis] < box->orig_corner7[axis]) != smaller) { + double tmp = box->orig_corner0[axis]; + box->orig_corner0[axis] = box->orig_corner7[axis]; + box->orig_corner7[axis] = tmp; + } + // Should we also swap the coordinates of save_corner0 and save_corner7? +} + +/* ensure that the coordinates of corner0 and corner7 are in the correct order (to prevent everted boxes) */ +void +SPBox3D::relabel_corners() { + box3d_swap_coords(this, Proj::X, false); + box3d_swap_coords(this, Proj::Y, false); + box3d_swap_coords(this, Proj::Z, true); +} + +static void +box3d_check_for_swapped_coords(SPBox3D *box, Proj::Axis axis, bool smaller) { + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + + if ((box->orig_corner0[axis] < box->orig_corner7[axis]) != smaller) { + box->swapped = (Box3D::Axis) (box->swapped | Proj::toAffine(axis)); + } else { + box->swapped = (Box3D::Axis) (box->swapped & ~Proj::toAffine(axis)); + } +} + +static void +box3d_exchange_coords(SPBox3D *box) { + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + + for (int i = 0; i < 3; ++i) { + if (box->swapped & Box3D::axes[i]) { + double tmp = box->orig_corner0[i]; + box->orig_corner0[i] = box->orig_corner7[i]; + box->orig_corner7[i] = tmp; + } + } +} + +void +SPBox3D::check_for_swapped_coords() { + box3d_check_for_swapped_coords(this, Proj::X, false); + box3d_check_for_swapped_coords(this, Proj::Y, false); + box3d_check_for_swapped_coords(this, Proj::Z, true); + + box3d_exchange_coords(this); +} + +static void box3d_extract_boxes_rec(SPObject *obj, std::list<SPBox3D *> &boxes) { + auto box = cast<SPBox3D>(obj); + if (box) { + boxes.push_back(box); + } else if (is<SPGroup>(obj)) { + for (auto& child: obj->children) { + box3d_extract_boxes_rec(&child, boxes); + } + } +} + +std::list<SPBox3D *> +SPBox3D::extract_boxes(SPObject *obj) { + std::list<SPBox3D *> boxes; + box3d_extract_boxes_rec(obj, boxes); + return boxes; +} + +Persp3D * +SPBox3D::get_perspective() const { + if(this->persp_ref) { + return this->persp_ref->getObject(); + } + return nullptr; +} + +void +SPBox3D::switch_perspectives(Persp3D *old_persp, Persp3D *new_persp, bool recompute_corners) { + if (recompute_corners) { + this->orig_corner0.normalize(); + this->orig_corner7.normalize(); + double z0 = this->orig_corner0[Proj::Z]; + double z7 = this->orig_corner7[Proj::Z]; + Geom::Point corner0_screen = this->get_corner_screen(0, false); + Geom::Point corner7_screen = this->get_corner_screen(7, false); + + this->orig_corner0 = new_persp->perspective_impl->tmat.preimage(corner0_screen, z0, Proj::Z); + this->orig_corner7 = new_persp->perspective_impl->tmat.preimage(corner7_screen, z7, Proj::Z); + } + + old_persp->remove_box (this); + new_persp->add_box (this); + + Glib::ustring href = "#"; + href += new_persp->getId(); + this->setAttribute("inkscape:perspectiveID", href); +} + +/* Converts the 3D box to an ordinary SPGroup, adds it to the XML tree at the same position as + the original box and deletes the latter */ +SPGroup *SPBox3D::convert_to_group() +{ + SPDocument *doc = this->document; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + // remember position of the box + int pos = this->getPosition(); + + // remember important attributes + gchar const *id = this->getAttribute("id"); + gchar const *style = this->getAttribute("style"); + gchar const *mask = this->getAttribute("mask"); + gchar const *clip_path = this->getAttribute("clip-path"); + + // create a new group and add the sides (converted to ordinary paths) as its children + Inkscape::XML::Node *grepr = xml_doc->createElement("svg:g"); + + for (auto& obj: this->children) { + auto side = cast<Box3DSide>(&obj); + if (side) { + Inkscape::XML::Node *repr = side->convert_to_path(); + grepr->appendChild(repr); + } else { + g_warning("Non-side item encountered as child of a 3D box."); + } + } + + // add the new group to the box's parent and set remembered position + SPObject *parent = this->parent; + parent->appendChild(grepr); + grepr->setPosition(pos); + grepr->setAttributeOrRemoveIfEmpty("style", style); + grepr->setAttributeOrRemoveIfEmpty("mask", mask); + grepr->setAttributeOrRemoveIfEmpty("clip-path", clip_path); + + this->deleteObject(true); + + grepr->setAttribute("id", id); + + auto group = cast<SPGroup>(doc->getObjectByRepr(grepr)); + g_assert(group != nullptr); + return group; +} + +const char *SPBox3D::displayName() const { + return _("3D Box"); +} + +gchar *SPBox3D::description() const { + // We could put more details about the 3d box here + return g_strdup(""); +} + +static inline void +box3d_push_back_corner_pair(SPBox3D const *box, std::list<std::pair<Geom::Point, Geom::Point> > &pts, int c1, int c2) { + pts.emplace_back(box->get_corner_screen(c1, false), + box->get_corner_screen(c2, false)); +} + +void SPBox3D::convert_to_guides() const { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (!prefs->getBool("/tools/shapes/3dbox/convertguides", true)) { + this->convert_to_guides(); + return; + } + + std::list<std::pair<Geom::Point, Geom::Point> > pts; + + /* perspective lines in X direction */ + box3d_push_back_corner_pair(this, pts, 0, 1); + box3d_push_back_corner_pair(this, pts, 2, 3); + box3d_push_back_corner_pair(this, pts, 4, 5); + box3d_push_back_corner_pair(this, pts, 6, 7); + + /* perspective lines in Y direction */ + box3d_push_back_corner_pair(this, pts, 0, 2); + box3d_push_back_corner_pair(this, pts, 1, 3); + box3d_push_back_corner_pair(this, pts, 4, 6); + box3d_push_back_corner_pair(this, pts, 5, 7); + + /* perspective lines in Z direction */ + box3d_push_back_corner_pair(this, pts, 0, 4); + box3d_push_back_corner_pair(this, pts, 1, 5); + box3d_push_back_corner_pair(this, pts, 2, 6); + box3d_push_back_corner_pair(this, pts, 3, 7); + + sp_guide_pt_pairs_to_guides(this->document, pts); +} + +/* + 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 : diff --git a/src/object/box3d.h b/src/object/box3d.h new file mode 100644 index 0000000..204c9a0 --- /dev/null +++ b/src/object/box3d.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_BOX3D_H +#define SEEN_SP_BOX3D_H + +/* + * SVG <box3d> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Maximilian Albert <Anhalter42@gmx.de> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org. + * + * Copyright (C) 2007 Authors + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-item-group.h" +#include "proj_pt.h" +#include "axis-manip.h" + +#define SP_TYPE_BOX3D (box3d_get_type ()) + +class Persp3D; +class Persp3DReference; + +class SPBox3D final : public SPGroup { +public: + SPBox3D(); + ~SPBox3D() override; + int tag() const override { return tag_of<decltype(*this)>; } + + int z_orders[6]; // z_orders[i] holds the ID of the face at position #i in the group (from top to bottom) + + char *persp_href; + Persp3DReference *persp_ref; + + Proj::Pt3 orig_corner0; + Proj::Pt3 orig_corner7; + + Proj::Pt3 save_corner0; + Proj::Pt3 save_corner7; + + Box3D::Axis swapped; // to indicate which coordinates are swapped during dragging + + int my_counter; // for debugging only + + /** + * Create a SPBox3D and append it to the parent. + */ + static SPBox3D * createBox3D(SPItem * parent); + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + virtual const char* display_name(); + Geom::Affine set_transform(Geom::Affine const &transform) override; + void convert_to_guides() const override; + const char* displayName() const override; + char *description() const override; + + void position_set (); + Geom::Point get_corner_screen (unsigned int id, bool item_coords = true) const; + Proj::Pt3 get_proj_center (); + Geom::Point get_center_screen (); + + void set_corner (unsigned int id, Geom::Point const &new_pos, Box3D::Axis movement, bool constrained); + void set_center (Geom::Point const &new_pos, Geom::Point const &old_pos, Box3D::Axis movement, bool constrained); + void corners_for_PLs (Proj::Axis axis, Geom::Point &corner1, Geom::Point &corner2, Geom::Point &corner3, Geom::Point &corner4) const; + bool recompute_z_orders (); + void set_z_orders (); + + int pt_lies_in_PL_sector (Geom::Point const &pt, int id1, int id2, Box3D::Axis axis) const; + int VP_lies_in_PL_sector (Proj::Axis vpdir, int id1, int id2, Box3D::Axis axis) const; + + void relabel_corners(); + void check_for_swapped_coords(); + + static std::list<SPBox3D *> extract_boxes(SPObject *obj); + + Persp3D *get_perspective() const; + void switch_perspectives(Persp3D *old_persp, Persp3D *new_persp, bool recompute_corners = false); + + SPGroup *convert_to_group(); +}; + +#endif // SEEN_SP_BOX3D_H + +/* + 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 : diff --git a/src/object/color-profile.cpp b/src/object/color-profile.cpp new file mode 100644 index 0000000..f02c2f9 --- /dev/null +++ b/src/object/color-profile.cpp @@ -0,0 +1,1284 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#define noDEBUG_LCMS + +#include <gdkmm/rgba.h> + +#include <glib/gstdio.h> +#include <fcntl.h> +#include <glib/gi18n.h> + +#ifdef DEBUG_LCMS +#include <gtk/gtk.h> +#endif // DEBUG_LCMS + +#include <unistd.h> +#include <cstring> +#include <utility> +#include <io/sys.h> +#include <io/resource.h> + +#ifdef _WIN32 +#include <windows.h> +#endif + +#include <lcms2.h> + +#include "xml/repr.h" +#include "xml/href-attribute-helper.h" +#include "color.h" +#include "color-profile.h" +#include "cms-system.h" +#include "color-profile-cms-fns.h" +#include "attributes.h" +#include "inkscape.h" +#include "document.h" +#include "preferences.h" +#include <glibmm/checksum.h> +#include <glibmm/convert.h> +#include "uri.h" + +#ifdef _WIN32 +#include <icm.h> +#endif // _WIN32 + +using Inkscape::ColorProfile; +using Inkscape::ColorProfileImpl; + +namespace +{ +cmsHPROFILE getSystemProfileHandle(); +cmsHPROFILE getProofProfileHandle(); +void loadProfiles(); +} + +#ifdef DEBUG_LCMS +extern guint update_in_progress; +#define DEBUG_MESSAGE_SCISLAC(key, ...) \ +{\ + Inkscape::Preferences *prefs = Inkscape::Preferences::get();\ + bool dump = prefs->getBool(Glib::ustring("/options/scislac/") + #key);\ + bool dumpD = prefs->getBool(Glib::ustring("/options/scislac/") + #key"D");\ + bool dumpD2 = prefs->getBool(Glib::ustring("/options/scislac/") + #key"D2");\ + dumpD &= ( (update_in_progress == 0) || dumpD2 );\ + if ( dump )\ + {\ + g_message( __VA_ARGS__ );\ +\ + }\ + if ( dumpD )\ + {\ + GtkWidget *dialog = gtk_message_dialog_new(NULL,\ + GTK_DIALOG_DESTROY_WITH_PARENT, \ + GTK_MESSAGE_INFO, \ + GTK_BUTTONS_OK, \ + __VA_ARGS__ \ + );\ + g_signal_connect_swapped(dialog, "response",\ + G_CALLBACK(gtk_widget_destroy), \ + dialog); \ + gtk_widget_show_all( dialog );\ + }\ +} + + +#define DEBUG_MESSAGE(key, ...)\ +{\ + g_message( __VA_ARGS__ );\ +} + +#else +#define DEBUG_MESSAGE_SCISLAC(key, ...) +#define DEBUG_MESSAGE(key, ...) +#endif // DEBUG_LCMS + +namespace Inkscape { + +class ColorProfileImpl { +public: + static cmsHPROFILE _sRGBProf; + static cmsHPROFILE _NullProf; + + ColorProfileImpl(); + + static cmsUInt32Number _getInputFormat( cmsColorSpaceSignature space ); + + static cmsHPROFILE getNULLProfile(); + static cmsHPROFILE getSRGBProfile(); + + void _clearProfile(); + + cmsHPROFILE _profHandle; + cmsProfileClassSignature _profileClass; + cmsColorSpaceSignature _profileSpace; + cmsHTRANSFORM _transf; + cmsHTRANSFORM _revTransf; + cmsHTRANSFORM _gamutTransf; +}; + +cmsColorSpaceSignature asICColorSpaceSig(ColorSpaceSig const & sig) +{ + return ColorSpaceSigWrapper(sig); +} + +cmsProfileClassSignature asICColorProfileClassSig(ColorProfileClassSig const & sig) +{ + return ColorProfileClassSigWrapper(sig); +} + +} // namespace Inkscape + +ColorProfileImpl::ColorProfileImpl() + : + _profHandle(nullptr), + _profileClass(cmsSigInputClass), + _profileSpace(cmsSigRgbData), + _transf(nullptr), + _revTransf(nullptr), + _gamutTransf(nullptr) +{ +} + + +cmsHPROFILE ColorProfileImpl::_sRGBProf = nullptr; + +cmsHPROFILE ColorProfileImpl::getSRGBProfile() { + if ( !_sRGBProf ) { + _sRGBProf = cmsCreate_sRGBProfile(); + } + return ColorProfileImpl::_sRGBProf; +} + +cmsHPROFILE ColorProfileImpl::_NullProf = nullptr; + +cmsHPROFILE ColorProfileImpl::getNULLProfile() { + if ( !_NullProf ) { + _NullProf = cmsCreateNULLProfile(); + } + return _NullProf; +} + +ColorProfile::FilePlusHome::FilePlusHome(Glib::ustring filename, bool isInHome) : filename(std::move(filename)), isInHome(isInHome) { +} + +ColorProfile::FilePlusHome::FilePlusHome(const ColorProfile::FilePlusHome &filePlusHome) : FilePlusHome(filePlusHome.filename, filePlusHome.isInHome) { +} + +bool ColorProfile::FilePlusHome::operator<(FilePlusHome const &other) const { + // if one is from home folder, other from global folder, sort home folder first. cf bug 1457126 + bool result; + if (this->isInHome != other.isInHome) result = this->isInHome; + else result = this->filename < other.filename; + return result; +} + +ColorProfile::FilePlusHomeAndName::FilePlusHomeAndName(ColorProfile::FilePlusHome filePlusHome, Glib::ustring name) + : FilePlusHome(filePlusHome), name(std::move(name)) { +} + +bool ColorProfile::FilePlusHomeAndName::operator<(ColorProfile::FilePlusHomeAndName const &other) const { + bool result; + if (this->isInHome != other.isInHome) result = this->isInHome; + else result = this->name < other.name; + return result; +} + + +ColorProfile::ColorProfile() : SPObject() { + this->impl = new ColorProfileImpl(); + + this->href = nullptr; + this->local = nullptr; + this->name = nullptr; + this->intentStr = nullptr; + this->rendering_intent = Inkscape::RENDERING_INTENT_UNKNOWN; +} + +ColorProfile::~ColorProfile() = default; + +bool ColorProfile::operator<(ColorProfile const &other) const { + gchar *a_name_casefold = g_utf8_casefold(this->name, -1 ); + gchar *b_name_casefold = g_utf8_casefold(other.name, -1 ); + int result = g_strcmp0(a_name_casefold, b_name_casefold); + g_free(a_name_casefold); + g_free(b_name_casefold); + return result < 0; +} + +/** + * Callback: free object + */ +void ColorProfile::release() { + // Unregister ourselves + if ( this->document ) { + this->document->removeResource("iccprofile", this); + } + + if ( this->href ) { + g_free( this->href ); + this->href = nullptr; + } + + if ( this->local ) { + g_free( this->local ); + this->local = nullptr; + } + + if ( this->name ) { + g_free( this->name ); + this->name = nullptr; + } + + if ( this->intentStr ) { + g_free( this->intentStr ); + this->intentStr = nullptr; + } + + this->impl->_clearProfile(); + + delete this->impl; + this->impl = nullptr; + + SPObject::release(); +} + +void ColorProfileImpl::_clearProfile() +{ + _profileSpace = cmsSigRgbData; + + if ( _transf ) { + cmsDeleteTransform( _transf ); + _transf = nullptr; + } + if ( _revTransf ) { + cmsDeleteTransform( _revTransf ); + _revTransf = nullptr; + } + if ( _gamutTransf ) { + cmsDeleteTransform( _gamutTransf ); + _gamutTransf = nullptr; + } + if ( _profHandle ) { + cmsCloseProfile( _profHandle ); + _profHandle = nullptr; + } +} + +/** + * Callback: set attributes from associated repr. + */ +void ColorProfile::build(SPDocument *document, Inkscape::XML::Node *repr) { + g_assert(this->href == nullptr); + g_assert(this->local == nullptr); + g_assert(this->name == nullptr); + g_assert(this->intentStr == nullptr); + + SPObject::build(document, repr); + + this->readAttr(SPAttr::XLINK_HREF); + this->readAttr(SPAttr::ID); + this->readAttr(SPAttr::LOCAL); + this->readAttr(SPAttr::NAME); + this->readAttr(SPAttr::RENDERING_INTENT); + + // Register + if ( document ) { + document->addResource( "iccprofile", this ); + } +} + + +/** + * Callback: set attribute. + */ +void ColorProfile::set(SPAttr key, gchar const *value) { + switch (key) { + case SPAttr::XLINK_HREF: + if ( this->href ) { + g_free( this->href ); + this->href = nullptr; + } + if ( value ) { + this->href = g_strdup( value ); + if ( *this->href ) { + + // TODO open filename and URIs properly + //FILE* fp = fopen_utf8name( filename, "r" ); + //LCMSAPI cmsHPROFILE LCMSEXPORT cmsOpenProfileFromMem(LPVOID MemPtr, cmsUInt32Number dwSize); + + // Try to open relative + SPDocument *doc = this->document; + if (!doc) { + doc = SP_ACTIVE_DOCUMENT; + g_warning("this has no document. using active"); + } + //# 1. Get complete filename of document + gchar const *docbase = doc->getDocumentFilename(); + + Inkscape::URI docUri(""); + if (docbase) { // The file has already been saved + docUri = Inkscape::URI::from_native_filename(docbase); + } + + this->impl->_clearProfile(); + + try { + auto hrefUri = Inkscape::URI(this->href, docUri); + auto contents = hrefUri.getContents(); + this->impl->_profHandle = cmsOpenProfileFromMem(contents.data(), contents.size()); + } catch (...) { + g_warning("Failed to open CMS profile URI '%.100s'", this->href); + } + + if ( this->impl->_profHandle ) { + this->impl->_profileSpace = cmsGetColorSpace( this->impl->_profHandle ); + this->impl->_profileClass = cmsGetDeviceClass( this->impl->_profHandle ); + } + DEBUG_MESSAGE( lcmsOne, "cmsOpenProfileFromFile( '%s'...) = %p", fullname, (void*)this->impl->_profHandle ); + } + } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::LOCAL: + if ( this->local ) { + g_free( this->local ); + this->local = nullptr; + } + this->local = g_strdup( value ); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::NAME: + if ( this->name ) { + g_free( this->name ); + this->name = nullptr; + } + this->name = g_strdup( value ); + DEBUG_MESSAGE( lcmsTwo, "<color-profile> name set to '%s'", this->name ); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::RENDERING_INTENT: + if ( this->intentStr ) { + g_free( this->intentStr ); + this->intentStr = nullptr; + } + this->intentStr = g_strdup( value ); + + if ( value ) { + if ( strcmp( value, "auto" ) == 0 ) { + this->rendering_intent = RENDERING_INTENT_AUTO; + } else if ( strcmp( value, "perceptual" ) == 0 ) { + this->rendering_intent = RENDERING_INTENT_PERCEPTUAL; + } else if ( strcmp( value, "relative-colorimetric" ) == 0 ) { + this->rendering_intent = RENDERING_INTENT_RELATIVE_COLORIMETRIC; + } else if ( strcmp( value, "saturation" ) == 0 ) { + this->rendering_intent = RENDERING_INTENT_SATURATION; + } else if ( strcmp( value, "absolute-colorimetric" ) == 0 ) { + this->rendering_intent = RENDERING_INTENT_ABSOLUTE_COLORIMETRIC; + } else { + this->rendering_intent = RENDERING_INTENT_UNKNOWN; + } + } else { + this->rendering_intent = RENDERING_INTENT_UNKNOWN; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPObject::set(key, value); + break; + } +} + +/** + * Callback: write attributes to associated repr. + */ +Inkscape::XML::Node* ColorProfile::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:color-profile"); + } + + if ( (flags & SP_OBJECT_WRITE_ALL) || this->href ) { + Inkscape::setHrefAttribute(*repr, this->href ); + } + + if ( (flags & SP_OBJECT_WRITE_ALL) || this->local ) { + repr->setAttribute( "local", this->local ); + } + + if ( (flags & SP_OBJECT_WRITE_ALL) || this->name ) { + repr->setAttribute( "name", this->name ); + } + + if ( (flags & SP_OBJECT_WRITE_ALL) || this->intentStr ) { + repr->setAttribute( "rendering-intent", this->intentStr ); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + + +struct MapMap { + cmsColorSpaceSignature space; + cmsUInt32Number inForm; +}; + +cmsUInt32Number ColorProfileImpl::_getInputFormat( cmsColorSpaceSignature space ) +{ + MapMap possible[] = { + {cmsSigXYZData, TYPE_XYZ_16}, + {cmsSigLabData, TYPE_Lab_16}, + //cmsSigLuvData + {cmsSigYCbCrData, TYPE_YCbCr_16}, + {cmsSigYxyData, TYPE_Yxy_16}, + {cmsSigRgbData, TYPE_RGB_16}, + {cmsSigGrayData, TYPE_GRAY_16}, + {cmsSigHsvData, TYPE_HSV_16}, + {cmsSigHlsData, TYPE_HLS_16}, + {cmsSigCmykData, TYPE_CMYK_16}, + {cmsSigCmyData, TYPE_CMY_16}, + }; + + int index = 0; + for ( guint i = 0; i < G_N_ELEMENTS(possible); i++ ) { + if ( possible[i].space == space ) { + index = i; + break; + } + } + + return possible[index].inForm; +} + +static int getLcmsIntent( guint svgIntent ) +{ + int intent = INTENT_PERCEPTUAL; + switch ( svgIntent ) { + case Inkscape::RENDERING_INTENT_RELATIVE_COLORIMETRIC: + intent = INTENT_RELATIVE_COLORIMETRIC; + break; + case Inkscape::RENDERING_INTENT_SATURATION: + intent = INTENT_SATURATION; + break; + case Inkscape::RENDERING_INTENT_ABSOLUTE_COLORIMETRIC: + intent = INTENT_ABSOLUTE_COLORIMETRIC; + break; + case Inkscape::RENDERING_INTENT_PERCEPTUAL: + case Inkscape::RENDERING_INTENT_UNKNOWN: + case Inkscape::RENDERING_INTENT_AUTO: + default: + intent = INTENT_PERCEPTUAL; + } + return intent; +} + +static ColorProfile *bruteFind(SPDocument *document, gchar const *name) +{ + std::vector<SPObject *> current = document->getResourceList("iccprofile"); + for (auto *obj : current) { + if (auto prof = cast<ColorProfile>(obj)) { + if ( prof->name && (strcmp(prof->name, name) == 0) ) { + return prof; + } + } + } + + return nullptr; +} + +cmsHPROFILE Inkscape::CMSSystem::getHandle( SPDocument* document, guint* intent, gchar const* name ) +{ + cmsHPROFILE prof = nullptr; + + auto *thing = bruteFind(document, name); + if ( thing ) { + prof = thing->impl->_profHandle; + } + + if ( intent ) { + *intent = thing ? thing->rendering_intent : (guint)RENDERING_INTENT_UNKNOWN; + } + + DEBUG_MESSAGE( lcmsThree, "<color-profile> queried for profile of '%s'. Returning %p with intent of %d", name, prof, (intent? *intent:0) ); + + return prof; +} + +Inkscape::ColorSpaceSig ColorProfile::getColorSpace() const { + return ColorSpaceSigWrapper(impl->_profileSpace); +} + +Inkscape::ColorProfileClassSig ColorProfile::getProfileClass() const { + return ColorProfileClassSigWrapper(impl->_profileClass); +} + +cmsHTRANSFORM ColorProfile::getTransfToSRGB8() +{ + if ( !impl->_transf && impl->_profHandle ) { + int intent = getLcmsIntent(rendering_intent); + impl->_transf = cmsCreateTransform( impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, intent, 0 ); + } + return impl->_transf; +} + +cmsHTRANSFORM ColorProfile::getTransfFromSRGB8() +{ + if ( !impl->_revTransf && impl->_profHandle ) { + int intent = getLcmsIntent(rendering_intent); + impl->_revTransf = cmsCreateTransform( ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), intent, 0 ); + } + return impl->_revTransf; +} + +cmsHTRANSFORM ColorProfile::getTransfGamutCheck() +{ + if ( !impl->_gamutTransf ) { + impl->_gamutTransf = cmsCreateProofingTransform(ColorProfileImpl::getSRGBProfile(), + TYPE_BGRA_8, + ColorProfileImpl::getNULLProfile(), + TYPE_GRAY_8, + impl->_profHandle, + INTENT_RELATIVE_COLORIMETRIC, + INTENT_RELATIVE_COLORIMETRIC, + (cmsFLAGS_GAMUTCHECK | cmsFLAGS_SOFTPROOFING)); + } + return impl->_gamutTransf; +} + +bool ColorProfile::GamutCheck(SPColor color) +{ + guint32 val = color.toRGBA32(0); + + cmsUInt16Number oldAlarmCodes[cmsMAXCHANNELS] = {0}; + cmsGetAlarmCodes(oldAlarmCodes); + cmsUInt16Number newAlarmCodes[cmsMAXCHANNELS] = {0}; + newAlarmCodes[0] = ~0; + cmsSetAlarmCodes(newAlarmCodes); + + cmsUInt8Number outofgamut = 0; + guchar check_color[4] = { + static_cast<guchar>(SP_RGBA32_R_U(val)), + static_cast<guchar>(SP_RGBA32_G_U(val)), + static_cast<guchar>(SP_RGBA32_B_U(val)), + 255}; + + cmsHTRANSFORM gamutCheck = ColorProfile::getTransfGamutCheck(); + if (gamutCheck) { + cmsDoTransform(gamutCheck, &check_color, &outofgamut, 1); + } + + cmsSetAlarmCodes(oldAlarmCodes); + + return (outofgamut != 0); +} + +class ProfileInfo +{ +public: + ProfileInfo( cmsHPROFILE prof, Glib::ustring path ); + + Glib::ustring const& getName() {return _name;} + Glib::ustring const& getPath() {return _path;} + cmsColorSpaceSignature getSpace() {return _profileSpace;} + cmsProfileClassSignature getClass() {return _profileClass;} + +private: + Glib::ustring _path; + Glib::ustring _name; + cmsColorSpaceSignature _profileSpace; + cmsProfileClassSignature _profileClass; +}; + +ProfileInfo::ProfileInfo(cmsHPROFILE prof, Glib::ustring path) + : _path(std::move(path)) + , _name(ColorProfile::getNameFromProfile(prof)) + , _profileSpace(cmsGetColorSpace(prof)) + , _profileClass(cmsGetDeviceClass(prof)) +{ +} + + + +static std::vector<ProfileInfo> knownProfiles; + +std::vector<Glib::ustring> Inkscape::CMSSystem::getDisplayNames() +{ + loadProfiles(); + std::vector<Glib::ustring> result; + + for (auto & knownProfile : knownProfiles) { + if ( knownProfile.getClass() == cmsSigDisplayClass && knownProfile.getSpace() == cmsSigRgbData ) { + result.push_back( knownProfile.getName() ); + } + } + std::sort(result.begin(), result.end()); + + return result; +} + +std::vector<Glib::ustring> Inkscape::CMSSystem::getSoftproofNames() +{ + loadProfiles(); + std::vector<Glib::ustring> result; + + for (auto & knownProfile : knownProfiles) { + if ( knownProfile.getClass() == cmsSigOutputClass ) { + result.push_back( knownProfile.getName() ); + } + } + std::sort(result.begin(), result.end()); + + return result; +} + +Glib::ustring Inkscape::CMSSystem::getPathForProfile(Glib::ustring const& name) +{ + loadProfiles(); + Glib::ustring result; + + for (auto & knownProfile : knownProfiles) { + if ( name == knownProfile.getName() ) { + result = knownProfile.getPath(); + break; + } + } + + return result; +} + +void Inkscape::CMSSystem::doTransform(cmsHTRANSFORM transform, void *inBuf, void *outBuf, unsigned int size) +{ + cmsDoTransform(transform, inBuf, outBuf, size); +} + +bool Inkscape::CMSSystem::isPrintColorSpace(ColorProfile const *profile) +{ + bool isPrint = false; + if ( profile ) { + ColorSpaceSigWrapper colorspace = profile->getColorSpace(); + isPrint = (colorspace == cmsSigCmykData) || (colorspace == cmsSigCmyData); + } + return isPrint; +} + +gint Inkscape::CMSSystem::getChannelCount(ColorProfile const *profile) +{ + return profile ? profile->getChannelCount() : 0; +} + +gint ColorProfile::getChannelCount() const +{ + return cmsChannelsOf(asICColorSpaceSig(getColorSpace())); +} + +// the bool return value tells if it's a user's directory or a system location +// note that this will treat places under $HOME as system directories when they are found via $XDG_DATA_DIRS +std::set<ColorProfile::FilePlusHome> ColorProfile::getBaseProfileDirs() { + static bool warnSet = false; + if (!warnSet) { + warnSet = true; + } + std::set<ColorProfile::FilePlusHome> sources; + + // first try user's local dir + gchar* path = g_build_filename(g_get_user_data_dir(), "color", "icc", nullptr); + sources.insert(FilePlusHome(path, true)); + g_free(path); + + // search colord ICC store paths + // (see https://github.com/hughsie/colord/blob/fe10f76536bb27614ced04e0ff944dc6fb4625c0/lib/colord/cd-icc-store.c#L590) + + // user store + path = g_build_filename(g_get_user_data_dir(), "icc", nullptr); + sources.insert(FilePlusHome(path, true)); + g_free(path); + + path = g_build_filename(g_get_home_dir(), ".color", "icc", nullptr); + sources.insert(FilePlusHome(path, true)); + g_free(path); + + // machine store + sources.insert(FilePlusHome("/var/lib/color/icc", false)); + sources.insert(FilePlusHome("/var/lib/colord/icc", false)); + + const gchar* const * dataDirs = g_get_system_data_dirs(); + for ( int i = 0; dataDirs[i]; i++ ) { + gchar* path = g_build_filename(dataDirs[i], "color", "icc", nullptr); + sources.insert(FilePlusHome(path, false)); + g_free(path); + } + + // On OS X: + { + sources.insert(FilePlusHome("/System/Library/ColorSync/Profiles", false)); + sources.insert(FilePlusHome("/Library/ColorSync/Profiles", false)); + + gchar *path = g_build_filename(g_get_home_dir(), "Library", "ColorSync", "Profiles", nullptr); + sources.insert(FilePlusHome(path, true)); + g_free(path); + } + +#ifdef _WIN32 + wchar_t pathBuf[MAX_PATH + 1]; + pathBuf[0] = 0; + DWORD pathSize = sizeof(pathBuf); + g_assert(sizeof(wchar_t) == sizeof(gunichar2)); + if ( GetColorDirectoryW( NULL, pathBuf, &pathSize ) ) { + gchar * utf8Path = g_utf16_to_utf8( (gunichar2*)(&pathBuf[0]), -1, NULL, NULL, NULL ); + if ( !g_utf8_validate(utf8Path, -1, NULL) ) { + g_warning( "GetColorDirectoryW() resulted in invalid UTF-8" ); + } else { + sources.insert(FilePlusHome(utf8Path, false)); + } + g_free( utf8Path ); + } +#endif // _WIN32 + + return sources; +} + +static bool isIccFile( gchar const *filepath ) +{ + bool isIccFile = false; + GStatBuf st; + if ( g_stat(filepath, &st) == 0 && (st.st_size > 128) ) { + //0-3 == size + //36-39 == 'acsp' 0x61637370 + int fd = g_open( filepath, O_RDONLY, S_IRWXU); + if ( fd != -1 ) { + guchar scratch[40] = {0}; + size_t len = sizeof(scratch); + + //size_t left = 40; + ssize_t got = read(fd, scratch, len); + if ( got != -1 ) { + size_t calcSize = (scratch[0] << 24) | (scratch[1] << 16) | (scratch[2] << 8) | scratch[3]; + if ( calcSize > 128 && calcSize <= static_cast<size_t>(st.st_size) ) { + isIccFile = (scratch[36] == 'a') && (scratch[37] == 'c') && (scratch[38] == 's') && (scratch[39] == 'p'); + } + } + + close(fd); + if (isIccFile) { + cmsHPROFILE prof = cmsOpenProfileFromFile( filepath, "r" ); + if ( prof ) { + cmsProfileClassSignature profClass = cmsGetDeviceClass(prof); + if ( profClass == cmsSigNamedColorClass ) { + isIccFile = false; // Ignore named color profiles for now. + } + cmsCloseProfile( prof ); + } + } + } + } + return isIccFile; +} + +std::set<ColorProfile::FilePlusHome > ColorProfile::getProfileFiles() +{ + std::set<FilePlusHome> files; + using Inkscape::IO::Resource::get_filenames; + + for (auto &path: ColorProfile::getBaseProfileDirs()) { + for(auto &filename: get_filenames(path.filename, {".icc", ".icm"})) { + if ( isIccFile(filename.c_str()) ) { + files.insert(FilePlusHome(filename, path.isInHome)); + } + } + } + + return files; +} + +std::set<ColorProfile::FilePlusHomeAndName> ColorProfile::getProfileFilesWithNames() +{ + std::set<FilePlusHomeAndName> result; + + for (auto &profile: getProfileFiles()) { + cmsHPROFILE hProfile = cmsOpenProfileFromFile(profile.filename.c_str(), "r"); + if ( hProfile ) { + Glib::ustring name = getNameFromProfile(hProfile); + result.insert( FilePlusHomeAndName(profile, name) ); + cmsCloseProfile(hProfile); + } + } + + return result; +} + +void errorHandlerCB(cmsContext /*contextID*/, cmsUInt32Number errorCode, char const *errorText) +{ + g_message("lcms: Error %d", errorCode); + g_message(" %p", errorText); + //g_message("lcms: Error %d; %s", errorCode, errorText); +} + +Glib::ustring ColorProfile::getNameFromProfile(cmsHPROFILE profile) +{ + Glib::ustring nameStr; + if ( profile ) { + cmsUInt32Number byteLen = cmsGetProfileInfo(profile, cmsInfoDescription, "en", "US", nullptr, 0); + if (byteLen > 0) { + // TODO investigate wchar_t and cmsGetProfileInfo() + std::vector<char> data(byteLen); + cmsUInt32Number readLen = cmsGetProfileInfoASCII(profile, cmsInfoDescription, + "en", "US", + data.data(), data.size()); + if (readLen < data.size()) { + data.resize(readLen); + } + nameStr = Glib::ustring(data.begin(), data.end()); + } + if (nameStr.empty() || !g_utf8_validate(nameStr.c_str(), -1, nullptr)) { + nameStr = _("(invalid UTF-8 string)"); + } + } + return nameStr; +} + +/** + * Cleans up name to remove disallowed characters. + * Some discussion at http://markmail.org/message/bhfvdfptt25kgtmj + * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z' + * Allowed ASCII remaining chars add: '-', '.', '0'-'9', + * + * @param str the string to clean up. + */ +void ColorProfile::sanitizeName(std::string &str) +{ + if (str.size() > 0) { + char val = str.at(0); + if (((val < 'A') || (val > 'Z')) && ((val < 'a') || (val > 'z')) && (val != '_') && (val != ':')) { + str.insert(0, "_"); + } + for (int i = 1; i < str.size(); i++) { + char val = str.at(i); + if (((val < 'A') || (val > 'Z')) && ((val < 'a') || (val > 'z')) && ((val < '0') || (val > '9')) && + (val != '_') && (val != ':') && (val != '-') && (val != '.')) { + if (str.at(i - 1) == '-') { + str.erase(i, 1); + i--; + } else { + str.replace(i, 1, "-"); + } + } + } + if (str.at(str.size() - 1) == '-') { + str.pop_back(); + } + } +} + +namespace { + +/** + * This function loads or refreshes data in knownProfiles. + * Call it at the start of every call that requires this data. + */ +void loadProfiles() +{ + static bool error_handler_set = false; + if (!error_handler_set) { + //cmsSetLogErrorHandler(errorHandlerCB); + //g_message("LCMS error handler set"); + error_handler_set = true; + } + + static bool profiles_searched = false; + if ( !profiles_searched ) { + knownProfiles.clear(); + + for (auto &profile: ColorProfile::getProfileFiles()) { + cmsHPROFILE prof = cmsOpenProfileFromFile( profile.filename.c_str(), "r" ); + if ( prof ) { + ProfileInfo info( prof, Glib::filename_to_utf8( profile.filename.c_str() ) ); + cmsCloseProfile( prof ); + prof = nullptr; + + bool sameName = false; + for(auto &knownProfile: knownProfiles) { + if ( knownProfile.getName() == info.getName() ) { + sameName = true; + break; + } + } + + if ( !sameName ) { + knownProfiles.push_back(info); + } + } + } + profiles_searched = true; + } +} +} // namespace + +static bool gamutWarn = false; + +static Gdk::RGBA lastGamutColor("#808080"); + +static bool lastBPC = false; +static int lastIntent = INTENT_PERCEPTUAL; +static int lastProofIntent = INTENT_PERCEPTUAL; +static cmsHTRANSFORM transf = nullptr; + +namespace { +cmsHPROFILE getSystemProfileHandle() +{ + static cmsHPROFILE theOne = nullptr; + static Glib::ustring lastURI; + + loadProfiles(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring uri = prefs->getString("/options/displayprofile/uri"); + + if ( !uri.empty() ) { + if ( uri != lastURI ) { + lastURI.clear(); + if ( theOne ) { + cmsCloseProfile( theOne ); + } + if ( transf ) { + cmsDeleteTransform( transf ); + transf = nullptr; + } + theOne = cmsOpenProfileFromFile( uri.data(), "r" ); + if ( theOne ) { + // a display profile must have the proper stuff + cmsColorSpaceSignature space = cmsGetColorSpace(theOne); + cmsProfileClassSignature profClass = cmsGetDeviceClass(theOne); + + if ( profClass != cmsSigDisplayClass ) { + g_warning("Not a display profile"); + cmsCloseProfile( theOne ); + theOne = nullptr; + } else if ( space != cmsSigRgbData ) { + g_warning("Not an RGB profile"); + cmsCloseProfile( theOne ); + theOne = nullptr; + } else { + lastURI = uri; + } + } + } + } else if ( theOne ) { + cmsCloseProfile( theOne ); + theOne = nullptr; + lastURI.clear(); + if ( transf ) { + cmsDeleteTransform( transf ); + transf = nullptr; + } + } + + return theOne; +} + + +cmsHPROFILE getProofProfileHandle() +{ + static cmsHPROFILE theOne = nullptr; + static Glib::ustring lastURI; + + loadProfiles(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool which = prefs->getBool( "/options/softproof/enable"); + Glib::ustring uri = prefs->getString("/options/softproof/uri"); + + if ( which && !uri.empty() ) { + if ( lastURI != uri ) { + lastURI.clear(); + if ( theOne ) { + cmsCloseProfile( theOne ); + } + if ( transf ) { + cmsDeleteTransform( transf ); + transf = nullptr; + } + theOne = cmsOpenProfileFromFile( uri.data(), "r" ); + if ( theOne ) { + // a display profile must have the proper stuff + cmsColorSpaceSignature space = cmsGetColorSpace(theOne); + cmsProfileClassSignature profClass = cmsGetDeviceClass(theOne); + + (void)space; + (void)profClass; +/* + if ( profClass != cmsSigDisplayClass ) { + g_warning("Not a display profile"); + cmsCloseProfile( theOne ); + theOne = 0; + } else if ( space != cmsSigRgbData ) { + g_warning("Not an RGB profile"); + cmsCloseProfile( theOne ); + theOne = 0; + } else { +*/ + lastURI = uri; +/* + } +*/ + } + } + } else if ( theOne ) { + cmsCloseProfile( theOne ); + theOne = nullptr; + lastURI.clear(); + if ( transf ) { + cmsDeleteTransform( transf ); + transf = nullptr; + } + } + + return theOne; +} +} // namespace + +static void free_transforms(); + +cmsHTRANSFORM Inkscape::CMSSystem::getDisplayTransform() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display"); + if ( fromDisplay ) { + if ( transf ) { + cmsDeleteTransform(transf); + transf = nullptr; + } + return nullptr; + } + + bool warn = prefs->getBool( "/options/softproof/gamutwarn"); + int intent = prefs->getIntLimited( "/options/displayprofile/intent", 0, 0, 3 ); + int proofIntent = prefs->getIntLimited( "/options/softproof/intent", 0, 0, 3 ); + bool bpc = prefs->getBool( "/options/softproof/bpc"); + Glib::ustring colorStr = prefs->getString("/options/softproof/gamutcolor"); + Gdk::RGBA gamutColor( colorStr.empty() ? "#808080" : colorStr ); + + if ( (warn != gamutWarn) + || (lastIntent != intent) + || (lastProofIntent != proofIntent) + || (bpc != lastBPC) + || (gamutColor != lastGamutColor) + ) { + gamutWarn = warn; + free_transforms(); + lastIntent = intent; + lastProofIntent = proofIntent; + lastBPC = bpc; + lastGamutColor = gamutColor; + } + + // Fetch these now, as they might clear the transform as a side effect. + cmsHPROFILE hprof = getSystemProfileHandle(); + cmsHPROFILE proofProf = hprof ? getProofProfileHandle() : nullptr; + + if ( !transf ) { + if ( hprof && proofProf ) { + cmsUInt32Number dwFlags = cmsFLAGS_SOFTPROOFING; + if ( gamutWarn ) { + dwFlags |= cmsFLAGS_GAMUTCHECK; + + auto gamutColor_r = gamutColor.get_red_u(); + auto gamutColor_g = gamutColor.get_green_u(); + auto gamutColor_b = gamutColor.get_blue_u(); + + cmsUInt16Number newAlarmCodes[cmsMAXCHANNELS] = {0}; + newAlarmCodes[0] = gamutColor_r; + newAlarmCodes[1] = gamutColor_g; + newAlarmCodes[2] = gamutColor_b; + newAlarmCodes[3] = ~0; + cmsSetAlarmCodes(newAlarmCodes); + } + if ( bpc ) { + dwFlags |= cmsFLAGS_BLACKPOINTCOMPENSATION; + } + transf = cmsCreateProofingTransform( ColorProfileImpl::getSRGBProfile(), TYPE_BGRA_8, hprof, TYPE_BGRA_8, proofProf, intent, proofIntent, dwFlags ); + } else if ( hprof ) { + transf = cmsCreateTransform( ColorProfileImpl::getSRGBProfile(), TYPE_BGRA_8, hprof, TYPE_BGRA_8, intent, 0 ); + } + } + + return transf; +} + + +class MemProfile { +public: + MemProfile(); + ~MemProfile(); + + std::string id; + cmsHPROFILE hprof; + cmsHTRANSFORM transf; +}; + +MemProfile::MemProfile() : + id(), + hprof(nullptr), + transf(nullptr) +{ +} + +MemProfile::~MemProfile() += default; + +static std::vector<MemProfile> perMonitorProfiles; + +void free_transforms() +{ + if ( transf ) { + cmsDeleteTransform(transf); + transf = nullptr; + } + + for ( auto profile : perMonitorProfiles ) { + if ( profile.transf ) { + cmsDeleteTransform(profile.transf); + profile.transf = nullptr; + } + } +} + +std::string Inkscape::CMSSystem::getDisplayId(int monitor) +{ + std::string id; + + if ( monitor >= 0 && monitor < static_cast<int>(perMonitorProfiles.size()) ) { + MemProfile& item = perMonitorProfiles[monitor]; + id = item.id; + } + + return id; +} + +Glib::ustring Inkscape::CMSSystem::setDisplayPer( gpointer buf, guint bufLen, int monitor ) +{ + while ( static_cast<int>(perMonitorProfiles.size()) <= monitor ) { + MemProfile tmp; + perMonitorProfiles.push_back(tmp); + } + MemProfile& item = perMonitorProfiles[monitor]; + + if ( item.hprof ) { + cmsCloseProfile( item.hprof ); + item.hprof = nullptr; + } + + Glib::ustring id; + + if ( buf && bufLen ) { + gsize len = bufLen; // len is an inout parameter + id = Glib::Checksum::compute_checksum(Glib::Checksum::CHECKSUM_MD5, + reinterpret_cast<guchar*>(buf), len); + + // Note: if this is not a valid profile, item.hprof will be set to null. + item.hprof = cmsOpenProfileFromMem(buf, bufLen); + } + item.id = id; + + return id; +} + +cmsHTRANSFORM Inkscape::CMSSystem::getDisplayPer(std::string const &id) +{ + cmsHTRANSFORM result = nullptr; + if ( id.empty() ) { + return nullptr; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool found = false; + + for ( auto it2 = perMonitorProfiles.begin(); it2 != perMonitorProfiles.end() && !found; ++it2 ) { + if ( id == it2->id ) { + MemProfile& item = *it2; + + bool warn = prefs->getBool( "/options/softproof/gamutwarn"); + int intent = prefs->getIntLimited( "/options/displayprofile/intent", 0, 0, 3 ); + int proofIntent = prefs->getIntLimited( "/options/softproof/intent", 0, 0, 3 ); + bool bpc = prefs->getBool( "/options/softproof/bpc"); + Glib::ustring colorStr = prefs->getString("/options/softproof/gamutcolor"); + Gdk::RGBA gamutColor( colorStr.empty() ? "#808080" : colorStr ); + + if ( (warn != gamutWarn) + || (lastIntent != intent) + || (lastProofIntent != proofIntent) + || (bpc != lastBPC) + || (gamutColor != lastGamutColor) + ) { + gamutWarn = warn; + free_transforms(); + lastIntent = intent; + lastProofIntent = proofIntent; + lastBPC = bpc; + lastGamutColor = gamutColor; + } + + // Fetch these now, as they might clear the transform as a side effect. + cmsHPROFILE proofProf = item.hprof ? getProofProfileHandle() : nullptr; + + if ( !item.transf ) { + if ( item.hprof && proofProf ) { + cmsUInt32Number dwFlags = cmsFLAGS_SOFTPROOFING; + if ( gamutWarn ) { + dwFlags |= cmsFLAGS_GAMUTCHECK; + auto gamutColor_r = gamutColor.get_red_u(); + auto gamutColor_g = gamutColor.get_green_u(); + auto gamutColor_b = gamutColor.get_blue_u(); + + cmsUInt16Number newAlarmCodes[cmsMAXCHANNELS] = {0}; + newAlarmCodes[0] = gamutColor_r; + newAlarmCodes[1] = gamutColor_g; + newAlarmCodes[2] = gamutColor_b; + newAlarmCodes[3] = ~0; + cmsSetAlarmCodes(newAlarmCodes); + } + if ( bpc ) { + dwFlags |= cmsFLAGS_BLACKPOINTCOMPENSATION; + } + item.transf = cmsCreateProofingTransform( ColorProfileImpl::getSRGBProfile(), TYPE_BGRA_8, item.hprof, TYPE_BGRA_8, proofProf, intent, proofIntent, dwFlags ); + } else if ( item.hprof ) { + item.transf = cmsCreateTransform( ColorProfileImpl::getSRGBProfile(), TYPE_BGRA_8, item.hprof, TYPE_BGRA_8, intent, 0 ); + } + } + + result = item.transf; + found = true; + } + } + + return result; +} + + + + +/* + 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 : diff --git a/src/object/color-profile.h b/src/object/color-profile.h new file mode 100644 index 0000000..0a92553 --- /dev/null +++ b/src/object/color-profile.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLOR_PROFILE_H +#define SEEN_COLOR_PROFILE_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <set> +#include <vector> + +#include <glibmm/ustring.h> +#include "cms-color-types.h" + +#include "sp-object.h" + +struct SPColor; + +namespace Inkscape { + +enum { + RENDERING_INTENT_UNKNOWN = 0, + RENDERING_INTENT_AUTO = 1, + RENDERING_INTENT_PERCEPTUAL = 2, + RENDERING_INTENT_RELATIVE_COLORIMETRIC = 3, + RENDERING_INTENT_SATURATION = 4, + RENDERING_INTENT_ABSOLUTE_COLORIMETRIC = 5 +}; + +class ColorProfileImpl; + + +/** + * Color Profile. + */ +class ColorProfile final : public SPObject { +public: + ColorProfile(); + ~ColorProfile() override; + int tag() const override { return tag_of<decltype(*this)>; } + + bool operator<(ColorProfile const &other) const; + + static Glib::ustring getNameFromProfile(cmsHPROFILE profile); + static void sanitizeName(std::string &str); + + friend cmsHPROFILE colorprofile_get_handle( SPDocument*, unsigned int*, char const* ); + friend class CMSSystem; + + class FilePlusHome { + public: + FilePlusHome(Glib::ustring filename, bool isInHome); + FilePlusHome(const FilePlusHome &filePlusHome); + bool operator<(FilePlusHome const &other) const; + Glib::ustring filename; + bool isInHome; + }; + class FilePlusHomeAndName: public FilePlusHome { + public: + FilePlusHomeAndName(FilePlusHome filePlusHome, Glib::ustring name); + bool operator<(FilePlusHomeAndName const &other) const; + Glib::ustring name; + }; + + static std::set<FilePlusHome> getBaseProfileDirs(); + static std::set<FilePlusHome> getProfileFiles(); + static std::set<FilePlusHomeAndName> getProfileFilesWithNames(); + //icColorSpaceSignature getColorSpace() const; + ColorSpaceSig getColorSpace() const; + //icProfileClassSignature getProfileClass() const; + ColorProfileClassSig getProfileClass() const; + cmsHTRANSFORM getTransfToSRGB8(); + cmsHTRANSFORM getTransfFromSRGB8(); + cmsHTRANSFORM getTransfGamutCheck(); + bool GamutCheck(SPColor color); + int getChannelCount() const; + + char* href; + char* local; + char* name; + char* intentStr; + unsigned int rendering_intent; // FIXME: type the enum and hold that instead + +protected: + ColorProfileImpl *impl; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttr key, char const* value) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +} // namespace Inkscape + +#endif // !SEEN_COLOR_PROFILE_H + +/* + 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 : diff --git a/src/object/filters/CMakeLists.txt b/src/object/filters/CMakeLists.txt new file mode 100644 index 0000000..9d45bf2 --- /dev/null +++ b/src/object/filters/CMakeLists.txt @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(filters_SRC + sp-filter-primitive.cpp + blend.cpp + colormatrix.cpp + componenttransfer-funcnode.cpp + componenttransfer.cpp + composite.cpp + convolvematrix.cpp + diffuselighting.cpp + displacementmap.cpp + distantlight.cpp + flood.cpp + gaussian-blur.cpp + image.cpp + merge.cpp + mergenode.cpp + morphology.cpp + offset.cpp + pointlight.cpp + specularlighting.cpp + spotlight.cpp + slot-resolver.cpp + tile.cpp + turbulence.cpp + + + # ------- + # Headers + sp-filter-primitive.h + blend.h + colormatrix.h + componenttransfer-funcnode.h + componenttransfer.h + composite.h + convolvematrix.h + diffuselighting.h + displacementmap.h + distantlight.h + flood.h + gaussian-blur.h + image.h + merge.h + mergenode.h + morphology.h + offset.h + pointlight.h + specularlighting.h + spotlight.h + slot-resolver.h + tile.h + turbulence.h +) + +add_inkscape_source("${filters_SRC}") diff --git a/src/object/filters/blend.cpp b/src/object/filters/blend.cpp new file mode 100644 index 0000000..ed314ed --- /dev/null +++ b/src/object/filters/blend.cpp @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feBlend> implementation. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Abhishek Sharma + * + * Copyright (C) 2006,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> + +#include "blend.h" +#include "attributes.h" +#include "display/nr-filter.h" +#include "object/sp-filter.h" +#include "xml/repr.h" +#include "slot-resolver.h" +#include "util/optstr.h" + +void SPFeBlend::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::MODE); + readAttr(SPAttr::IN2); +} + +static SPBlendMode read_mode(char const *value) +{ + if (!value) { + return SP_CSS_BLEND_NORMAL; + } + + switch (value[0]) { + case 'n': + if (std::strcmp(value, "normal") == 0) + return SP_CSS_BLEND_NORMAL; + break; + case 'm': + if (std::strcmp(value, "multiply") == 0) + return SP_CSS_BLEND_MULTIPLY; + break; + case 's': + if (std::strcmp(value, "screen") == 0) + return SP_CSS_BLEND_SCREEN; + if (std::strcmp(value, "saturation") == 0) + return SP_CSS_BLEND_SATURATION; + break; + case 'd': + if (std::strcmp(value, "darken") == 0) + return SP_CSS_BLEND_DARKEN; + if (std::strcmp(value, "difference") == 0) + return SP_CSS_BLEND_DIFFERENCE; + break; + case 'l': + if (std::strcmp(value, "lighten") == 0) + return SP_CSS_BLEND_LIGHTEN; + if (std::strcmp(value, "luminosity") == 0) + return SP_CSS_BLEND_LUMINOSITY; + break; + case 'o': + if (std::strcmp(value, "overlay") == 0) + return SP_CSS_BLEND_OVERLAY; + break; + case 'c': + if (std::strcmp(value, "color-dodge") == 0) + return SP_CSS_BLEND_COLORDODGE; + if (std::strcmp(value, "color-burn") == 0) + return SP_CSS_BLEND_COLORBURN; + if (std::strcmp(value, "color") == 0) + return SP_CSS_BLEND_COLOR; + break; + case 'h': + if (std::strcmp(value, "hard-light") == 0) + return SP_CSS_BLEND_HARDLIGHT; + if (std::strcmp(value, "hue") == 0) + return SP_CSS_BLEND_HUE; + break; + case 'e': + if (std::strcmp(value, "exclusion") == 0) + return SP_CSS_BLEND_EXCLUSION; + default: + std::cerr << "SPBlendMode: Unimplemented mode: " << value << std::endl; + // do nothing by default + break; + } + + return SP_CSS_BLEND_NORMAL; +} + +void SPFeBlend::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::MODE: { + auto mode = ::read_mode(value); + if (mode != blend_mode) { + blend_mode = mode; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::IN2: { + if (Inkscape::Util::assign(in2_name, value)) { + requestModified(SP_OBJECT_MODIFIED_FLAG); + invalidate_parent_slots(); + } + break; + } + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +Inkscape::XML::Node *SPFeBlend::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = doc->createElement("svg:feBlend"); + } + + repr->setAttributeOrRemoveIfEmpty("in2", Inkscape::Util::to_cstr(in2_name)); + + char const *mode; + switch (blend_mode) { + case SP_CSS_BLEND_NORMAL: + mode = "normal"; break; + case SP_CSS_BLEND_MULTIPLY: + mode = "multiply"; break; + case SP_CSS_BLEND_SCREEN: + mode = "screen"; break; + case SP_CSS_BLEND_DARKEN: + mode = "darken"; break; + case SP_CSS_BLEND_LIGHTEN: + mode = "lighten"; break; + case SP_CSS_BLEND_OVERLAY: + mode = "overlay"; break; + case SP_CSS_BLEND_COLORDODGE: + mode = "color-dodge"; break; + case SP_CSS_BLEND_COLORBURN: + mode = "color-burn"; break; + case SP_CSS_BLEND_HARDLIGHT: + mode = "hard-light"; break; + case SP_CSS_BLEND_SOFTLIGHT: + mode = "soft-light"; break; + case SP_CSS_BLEND_DIFFERENCE: + mode = "difference"; break; + case SP_CSS_BLEND_EXCLUSION: + mode = "exclusion"; break; + case SP_CSS_BLEND_HUE: + mode = "hue"; break; + case SP_CSS_BLEND_SATURATION: + mode = "saturation"; break; + case SP_CSS_BLEND_COLOR: + mode = "color"; break; + case SP_CSS_BLEND_LUMINOSITY: + mode = "luminosity"; break; + default: + mode = nullptr; + } + + repr->setAttribute("mode", mode); + + return SPFilterPrimitive::write(doc, repr, flags); +} + +void SPFeBlend::resolve_slots(SlotResolver &resolver) +{ + in2_slot = resolver.read(in2_name); + SPFilterPrimitive::resolve_slots(resolver); +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeBlend::build_renderer(Inkscape::DrawingItem*) const +{ + auto blend = std::make_unique<Inkscape::Filters::FilterBlend>(); + build_renderer_common(blend.get()); + + blend->set_mode(blend_mode); + blend->set_input(1, in2_slot); + + return blend; +} + +/* + 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 : diff --git a/src/object/filters/blend.h b/src/object/filters/blend.h new file mode 100644 index 0000000..02afb90 --- /dev/null +++ b/src/object/filters/blend.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG blend filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2006,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEBLEND_H_SEEN +#define SP_FEBLEND_H_SEEN + +#include "sp-filter-primitive.h" +#include "display/nr-filter-blend.h" + +class SPFeBlend final + : public SPFilterPrimitive +{ +public: + SPBlendMode get_blend_mode() const { return blend_mode; } + int get_in2() const { return in2_slot; } + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; + + void resolve_slots(SlotResolver &) override; + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; + +private: + SPBlendMode blend_mode = SP_CSS_BLEND_NORMAL; + + std::optional<std::string> in2_name; + int in2_slot = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +}; + +#endif // SP_FEBLEND_H_SEEN + +/* + 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 : diff --git a/src/object/filters/colormatrix.cpp b/src/object/filters/colormatrix.cpp new file mode 100644 index 0000000..aab0281 --- /dev/null +++ b/src/object/filters/colormatrix.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feColorMatrix> implementation. + */ +/* + * Authors: + * Felipe Sanches <juca@members.fsf.org> + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2007 Felipe C. da S. Sanches + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> + +#include "attributes.h" +#include "display/nr-filter.h" +#include "svg/svg.h" +#include "colormatrix.h" +#include "util/numeric/converters.h" +#include "xml/repr.h" + +void SPFeColorMatrix::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::TYPE); + readAttr(SPAttr::VALUES); +} + +static Inkscape::Filters::FilterColorMatrixType read_type(char const *str) +{ + if (!str) { + return Inkscape::Filters::COLORMATRIX_MATRIX; //matrix is default + } + + switch (str[0]) { + case 'm': + if (std::strcmp(str, "matrix") == 0) return Inkscape::Filters::COLORMATRIX_MATRIX; + break; + case 's': + if (std::strcmp(str, "saturate") == 0) return Inkscape::Filters::COLORMATRIX_SATURATE; + break; + case 'h': + if (std::strcmp(str, "hueRotate") == 0) return Inkscape::Filters::COLORMATRIX_HUEROTATE; + break; + case 'l': + if (std::strcmp(str, "luminanceToAlpha") == 0) return Inkscape::Filters::COLORMATRIX_LUMINANCETOALPHA; + break; + } + + return Inkscape::Filters::COLORMATRIX_MATRIX; //matrix is default +} + +void SPFeColorMatrix::set(SPAttr key, char const *str) +{ + auto set_default_value = [this] { + switch (type) { + case Inkscape::Filters::COLORMATRIX_MATRIX: + values = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; + break; + case Inkscape::Filters::COLORMATRIX_SATURATE: + // Default value for saturate is 1.0 ("values" not used). + value = 1; + break; + case Inkscape::Filters::COLORMATRIX_HUEROTATE: + value = 0; + break; + case Inkscape::Filters::COLORMATRIX_LUMINANCETOALPHA: + // value, values not used. + break; + } + }; + + switch (key) { + case SPAttr::TYPE: { + auto n_type = ::read_type(str); + if (type != n_type){ + type = n_type; + if (!value_set) set_default_value(); + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::VALUES: + if (str) { + values = Inkscape::Util::read_vector(str); + value = Inkscape::Util::read_number(str, Inkscape::Util::NO_WARNING); + value_set = true; + } else { + set_default_value(); + value_set = false; + } + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPFilterPrimitive::set(key, str); + break; + } +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeColorMatrix::build_renderer(Inkscape::DrawingItem*) const +{ + auto colormatrix = std::make_unique<Inkscape::Filters::FilterColorMatrix>(); + build_renderer_common(colormatrix.get()); + + colormatrix->set_type(type); + colormatrix->set_value(value); + colormatrix->set_values(values); + + return colormatrix; +} + +/* + 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 : diff --git a/src/object/filters/colormatrix.h b/src/object/filters/colormatrix.h new file mode 100644 index 0000000..0fb3b28 --- /dev/null +++ b/src/object/filters/colormatrix.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG color matrix filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FECOLORMATRIX_H_SEEN +#define SP_FECOLORMATRIX_H_SEEN + +#include <vector> +#include "sp-filter-primitive.h" +#include "display/nr-filter-colormatrix.h" + +class SPFeColorMatrix final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + + Inkscape::Filters::FilterColorMatrixType get_type() const { return type; } + std::vector<double> const &get_values() const { return values; } + +private: + Inkscape::Filters::FilterColorMatrixType type = Inkscape::Filters::COLORMATRIX_MATRIX; + double value = 0.0; + std::vector<double> values; + bool value_set = false; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttr key, char const *value) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FECOLORMATRIX_H_SEEN + +/* + 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 : diff --git a/src/object/filters/componenttransfer-funcnode.cpp b/src/object/filters/componenttransfer-funcnode.cpp new file mode 100644 index 0000000..05c771c --- /dev/null +++ b/src/object/filters/componenttransfer-funcnode.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <funcR>, <funcG>, <funcB> and <funcA> implementations. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * Abhishek Sharma + * + * Copyright (C) 2006, 2007, 2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "document.h" +#include "componenttransfer.h" +#include "componenttransfer-funcnode.h" +#include "util/numeric/converters.h" +#include "xml/repr.h" + +void SPFeFuncNode::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + readAttr(SPAttr::TYPE); + readAttr(SPAttr::TABLEVALUES); + readAttr(SPAttr::SLOPE); + readAttr(SPAttr::INTERCEPT); + readAttr(SPAttr::AMPLITUDE); + readAttr(SPAttr::EXPONENT); + readAttr(SPAttr::OFFSET); + + document->addResource("fefuncnode", this); +} + +void SPFeFuncNode::release() +{ + if (document) { + document->removeResource("fefuncnode", this); + } + + tableValues.clear(); + + SPObject::release(); +} + +static Inkscape::Filters::FilterComponentTransferType sp_feComponenttransfer_read_type(char const *value) +{ + if (!value) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_ERROR; //type attribute is REQUIRED. + } + + switch (value[0]) { + case 'i': + if (!std::strcmp(value, "identity")) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_IDENTITY; + } + break; + case 't': + if (!std::strcmp(value, "table")) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_TABLE; + } + break; + case 'd': + if (!std::strcmp(value, "discrete")) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_DISCRETE; + } + break; + case 'l': + if (!std::strcmp(value, "linear")) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_LINEAR; + } + break; + case 'g': + if (!std::strcmp(value, "gamma")) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_GAMMA; + } + break; + default: + break; + } + + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_ERROR; //type attribute is REQUIRED. +} + +void SPFeFuncNode::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::TYPE: { + auto const new_type = sp_feComponenttransfer_read_type(value); + + if (type != new_type) { + type = new_type; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::TABLEVALUES: { + if (value) { + tableValues = Inkscape::Util::read_vector(value); + } else { + tableValues.clear(); + } + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::SLOPE: { + auto new_slope = value ? Inkscape::Util::read_number(value) : 1; + + if (slope != new_slope) { + slope = new_slope; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::INTERCEPT: { + auto new_intercept = value ? Inkscape::Util::read_number(value) : 0; + + if (intercept != new_intercept) { + intercept = new_intercept; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::AMPLITUDE: { + auto new_amplitude = value ? Inkscape::Util::read_number(value) : 1; + + if (amplitude != new_amplitude) { + amplitude = new_amplitude; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::EXPONENT: { + auto new_exponent = value ? Inkscape::Util::read_number(value) : 1; + + if (exponent != new_exponent) { + exponent = new_exponent; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::OFFSET: { + auto new_offset = value ? Inkscape::Util::read_number(value) : 0; + + if (offset != new_offset) { + offset = new_offset; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + SPObject::set(key, value); + break; + } +} + +/* + 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 : diff --git a/src/object/filters/componenttransfer-funcnode.h b/src/object/filters/componenttransfer-funcnode.h new file mode 100644 index 0000000..5f01db6 --- /dev/null +++ b/src/object/filters/componenttransfer-funcnode.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_FECOMPONENTTRANSFER_FUNCNODE_H_SEEN +#define SP_FECOMPONENTTRANSFER_FUNCNODE_H_SEEN + +/** \file + * SVG <filter> implementation, see sp-filter.cpp. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-object.h" +#include "display/nr-filter-component-transfer.h" + +class SPFeFuncNode final + : public SPObject +{ +public: + enum Channel + { + R, G, B, A + }; + + SPFeFuncNode(Channel channel) + : channel(channel) {} + int tag() const override { return tag_of<decltype(*this)>; } + + Inkscape::Filters::FilterComponentTransferType type = Inkscape::Filters::COMPONENTTRANSFER_TYPE_IDENTITY; + std::vector<double> tableValues; + double slope = 1; + double intercept = 0; + double amplitude = 1; + double exponent = 1; + double offset = 0; + Channel channel; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const *value) override; +}; + +#endif // SP_FECOMPONENTTRANSFER_FUNCNODE_H_SEEN + +/* + 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 : diff --git a/src/object/filters/componenttransfer.cpp b/src/object/filters/componenttransfer.cpp new file mode 100644 index 0000000..d1939a5 --- /dev/null +++ b/src/object/filters/componenttransfer.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feComponentTransfer> implementation. + */ +/* + * Authors: + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "document.h" +#include "componenttransfer.h" +#include "componenttransfer-funcnode.h" +#include "display/nr-filter.h" +#include "xml/repr.h" + +void SPFeComponentTransfer::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + document->addResource("feComponentTransfer", this); +} + +void SPFeComponentTransfer::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + SPFilterPrimitive::child_added(child, ref); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeComponentTransfer::remove_child(Inkscape::XML::Node *child) +{ + SPFilterPrimitive::remove_child(child); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeComponentTransfer::release() +{ + if (document) { + document->removeResource("feComponentTransfer", this); + } + SPFilterPrimitive::release(); +} + +void SPFeComponentTransfer::modified(unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + for (auto &c : children) { + if (cflags || (c.mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + c.emitModified(cflags); + } + } +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeComponentTransfer::build_renderer(Inkscape::DrawingItem*) const +{ + auto componenttransfer = std::make_unique<Inkscape::Filters::FilterComponentTransfer>(); + build_renderer_common(componenttransfer.get()); + + bool set[4] = {false, false, false, false}; + for (auto const &node : children) { + auto funcNode = cast<SPFeFuncNode>(&node); + if (!funcNode) { + continue; + } + + int i; + switch (funcNode->channel) { + case SPFeFuncNode::R: i = 0; break; + case SPFeFuncNode::G: i = 1; break; + case SPFeFuncNode::B: i = 2; break; + case SPFeFuncNode::A: i = 3; break; + default: + g_warning("Unrecognized channel for component transfer."); + goto nested_break; + } + + componenttransfer->type[i] = funcNode->type; + componenttransfer->tableValues[i] = funcNode->tableValues; + componenttransfer->slope[i] = funcNode->slope; + componenttransfer->intercept[i] = funcNode->intercept; + componenttransfer->amplitude[i] = funcNode->amplitude; + componenttransfer->exponent[i] = funcNode->exponent; + componenttransfer->offset[i] = funcNode->offset; + + set[i] = true; + } +nested_break:; + + // Set any types not explicitly set to the identity transform + for (int i = 0; i < 4; i++) { + if (!set[i]) { + componenttransfer->type[i] = Inkscape::Filters::COMPONENTTRANSFER_TYPE_IDENTITY; + } + } + + return componenttransfer; +} + +/* + 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 : diff --git a/src/object/filters/componenttransfer.h b/src/object/filters/componenttransfer.h new file mode 100644 index 0000000..08c29fe --- /dev/null +++ b/src/object/filters/componenttransfer.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG component transferfilter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FECOMPONENTTRANSFER_H_SEEN +#define SP_FECOMPONENTTRANSFER_H_SEEN + +#include "sp-filter-primitive.h" + +namespace Inkscape { +namespace Filters { +class FilterComponentTransfer; +} // namespace Filters +} // namespace Inkscape + +class SPFeComponentTransfer final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void modified(unsigned flags) override; + + void child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) override; + void remove_child(Inkscape::XML::Node *child) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FECOMPONENTTRANSFER_H_SEEN + +/* + 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 : diff --git a/src/object/filters/composite.cpp b/src/object/filters/composite.cpp new file mode 100644 index 0000000..c3823dd --- /dev/null +++ b/src/object/filters/composite.cpp @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feComposite> implementation. + */ +/* + * Authors: + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "composite.h" +#include "attributes.h" +#include "display/nr-filter.h" +#include "display/nr-filter-composite.h" +#include "object/sp-filter.h" +#include "svg/svg.h" +#include "util/numeric/converters.h" +#include "xml/repr.h" +#include "slot-resolver.h" +#include "util/optstr.h" + +void SPFeComposite::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::OPERATOR); + readAttr(SPAttr::K1); + readAttr(SPAttr::K2); + readAttr(SPAttr::K3); + readAttr(SPAttr::K4); + readAttr(SPAttr::IN2); +} + +static FeCompositeOperator read_operator(char const *value) +{ + if (!value) { + return COMPOSITE_DEFAULT; + } + + if (std::strcmp(value, "over") == 0) { + return COMPOSITE_OVER; + } else if (std::strcmp(value, "in") == 0) { + return COMPOSITE_IN; + } else if (std::strcmp(value, "out") == 0) { + return COMPOSITE_OUT; + } else if (std::strcmp(value, "atop") == 0) { + return COMPOSITE_ATOP; + } else if (std::strcmp(value, "xor") == 0) { + return COMPOSITE_XOR; + } else if (std::strcmp(value, "arithmetic") == 0) { + return COMPOSITE_ARITHMETIC; + } else if (std::strcmp(value, "lighter") == 0) { + return COMPOSITE_LIGHTER; + } + + std::cerr << "Inkscape::Filters::FilterCompositeOperator: Unimplemented operator: " << value << std::endl; + return COMPOSITE_DEFAULT; +} + +void SPFeComposite::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::OPERATOR: { + auto n_op = ::read_operator(value); + if (n_op != composite_operator) { + composite_operator = n_op; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + + case SPAttr::K1: { + double n_k = value ? Inkscape::Util::read_number(value) : 0.0; + if (n_k != k1) { + k1 = n_k; + if (composite_operator == COMPOSITE_ARITHMETIC) + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + + case SPAttr::K2: { + double n_k = value ? Inkscape::Util::read_number(value) : 0.0; + if (n_k != k2) { + k2 = n_k; + if (composite_operator == COMPOSITE_ARITHMETIC) + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + + case SPAttr::K3: { + double n_k = value ? Inkscape::Util::read_number(value) : 0.0; + if (n_k != k3) { + k3 = n_k; + if (composite_operator == COMPOSITE_ARITHMETIC) + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + + case SPAttr::K4: { + double n_k = value ? Inkscape::Util::read_number(value) : 0.0; + if (n_k != k4) { + k4 = n_k; + if (composite_operator == COMPOSITE_ARITHMETIC) + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + + case SPAttr::IN2: { + if (Inkscape::Util::assign(in2_name, value)) { + requestModified(SP_OBJECT_MODIFIED_FLAG); + invalidate_parent_slots(); + } + break; + } + + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +Inkscape::XML::Node *SPFeComposite::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) +{ + if (!repr) { + repr = doc->createElement("svg:feComposite"); + } + + repr->setAttributeOrRemoveIfEmpty("in2", Inkscape::Util::to_cstr(in2_name)); + + char const *comp_op; + switch (composite_operator) { + case COMPOSITE_OVER: + comp_op = "over"; break; + case COMPOSITE_IN: + comp_op = "in"; break; + case COMPOSITE_OUT: + comp_op = "out"; break; + case COMPOSITE_ATOP: + comp_op = "atop"; break; + case COMPOSITE_XOR: + comp_op = "xor"; break; + case COMPOSITE_ARITHMETIC: + comp_op = "arithmetic"; break; + case COMPOSITE_LIGHTER: + comp_op = "lighter"; break; + default: + comp_op = nullptr; + } + repr->setAttribute("operator", comp_op); + + if (composite_operator == COMPOSITE_ARITHMETIC) { + repr->setAttributeSvgDouble("k1", k1); + repr->setAttributeSvgDouble("k2", k2); + repr->setAttributeSvgDouble("k3", k3); + repr->setAttributeSvgDouble("k4", k4); + } else { + repr->removeAttribute("k1"); + repr->removeAttribute("k2"); + repr->removeAttribute("k3"); + repr->removeAttribute("k4"); + } + + return SPFilterPrimitive::write(doc, repr, flags); +} + +void SPFeComposite::resolve_slots(SlotResolver &resolver) +{ + in2_slot = resolver.read(in2_name); + SPFilterPrimitive::resolve_slots(resolver); +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeComposite::build_renderer(Inkscape::DrawingItem*) const +{ + auto composite = std::make_unique<Inkscape::Filters::FilterComposite>(); + build_renderer_common(composite.get()); + + composite->set_operator(composite_operator); + composite->set_input(1, in2_slot); + + if (composite_operator == COMPOSITE_ARITHMETIC) { + composite->set_arithmetic(k1, k2, k3, k4); + } + + return composite; +} + +/* + 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 : diff --git a/src/object/filters/composite.h b/src/object/filters/composite.h new file mode 100644 index 0000000..c119dcf --- /dev/null +++ b/src/object/filters/composite.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG composite filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FECOMPOSITE_H_SEEN +#define SP_FECOMPOSITE_H_SEEN + +#include "sp-filter-primitive.h" +#include "display/nr-filter-types.h" + +enum FeCompositeOperator +{ + // Default value is 'over', but let's distinguish specifying the + // default and implicitly using the default + COMPOSITE_DEFAULT, + COMPOSITE_OVER, /* Source Over */ + COMPOSITE_IN, /* Source In */ + COMPOSITE_OUT, /* Source Out */ + COMPOSITE_ATOP, /* Source Atop */ + COMPOSITE_XOR, + COMPOSITE_ARITHMETIC, /* Not a fundamental PorterDuff operator, nor Cairo */ + COMPOSITE_LIGHTER, /* Plus, Add (Not a fundamental PorterDuff operator */ + COMPOSITE_ENDOPERATOR /* Cairo Saturate is not included in CSS */ +}; + +class SPFeComposite final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + + FeCompositeOperator get_composite_operator() const { return composite_operator; } + int get_in2() const { return in2_slot; } + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) override; + + void resolve_slots(SlotResolver &) override; + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; + +private: + FeCompositeOperator composite_operator = COMPOSITE_DEFAULT; + double k1 = 0.0; + double k2 = 0.0; + double k3 = 0.0; + double k4 = 0.0; + + std::optional<std::string> in2_name; + int in2_slot = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +}; + +#endif // SP_FECOMPOSITE_H_SEEN + +/* + 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 : diff --git a/src/object/filters/convolvematrix.cpp b/src/object/filters/convolvematrix.cpp new file mode 100644 index 0000000..6793b29 --- /dev/null +++ b/src/object/filters/convolvematrix.cpp @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feConvolveMatrix> implementation. + */ +/* + * Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <cmath> +#include <vector> + +#include "convolvematrix.h" +#include "attributes.h" +#include "display/nr-filter.h" +#include "util/numeric/converters.h" +#include "xml/repr.h" + +void SPFeConvolveMatrix::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::ORDER); + readAttr(SPAttr::KERNELMATRIX); + readAttr(SPAttr::DIVISOR); + readAttr(SPAttr::BIAS); + readAttr(SPAttr::TARGETX); + readAttr(SPAttr::TARGETY); + readAttr(SPAttr::EDGEMODE); + readAttr(SPAttr::KERNELUNITLENGTH); + readAttr(SPAttr::PRESERVEALPHA); +} + +static Inkscape::Filters::FilterConvolveMatrixEdgeMode read_edgemode(char const *value) +{ + if (!value) { + return Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_DUPLICATE; // duplicate is default + } + + switch (value[0]) { + case 'd': + if (std::strcmp(value, "duplicate") == 0) { + return Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_DUPLICATE; + } + break; + case 'w': + if (std::strcmp(value, "wrap") == 0) { + return Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_WRAP; + } + break; + case 'n': + if (std::strcmp(value, "none") == 0) { + return Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_NONE; + } + break; + } + + return Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_DUPLICATE; //duplicate is default +} + +void SPFeConvolveMatrix::set(SPAttr key, gchar const *value) +{ + switch (key) { + case SPAttr::ORDER: + order.set(value); + + // From SVG spec: If <orderY> is not provided, it defaults to <orderX>. + if (!order.optNumIsSet()) { + order.setOptNumber(order.getNumber()); + } + + if (!targetXIsSet) { + targetX = std::floor(order.getNumber() / 2); + } + + if (!targetYIsSet) { + targetY = std::floor(order.getOptNumber() / 2); + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::KERNELMATRIX: + if (value) { + kernelMatrixIsSet = true; + kernelMatrix = Inkscape::Util::read_vector(value); + + if (!divisorIsSet) { + divisor = 0; + + for (double i : kernelMatrix) { + divisor += i; + } + + if (divisor == 0) { + divisor = 1; + } + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + g_warning("For feConvolveMatrix you MUST pass a kernelMatrix parameter!"); + } + break; + case SPAttr::DIVISOR: { + if (value) { + double n_num = Inkscape::Util::read_number(value); + + if (n_num == 0) { + // This should actually be an error, but given our UI it is more useful to simply set divisor to the default. + if (kernelMatrixIsSet) { + for (double i : kernelMatrix) { + n_num += i; + } + } + + if (n_num == 0) { + n_num = 1; + } + + if (divisorIsSet || divisor != n_num) { + divisorIsSet = false; + divisor = n_num; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } else if (!divisorIsSet || divisor != n_num) { + divisorIsSet = true; + divisor = n_num; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } + break; + } + case SPAttr::BIAS: { + double n_num = 0; + if (value) { + n_num = Inkscape::Util::read_number(value); + } + if (n_num != bias) { + bias = n_num; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::TARGETX: + if (value) { + int n_int = Inkscape::Util::read_number(value); + + if (n_int < 0 || n_int > order.getNumber()) { + g_warning("targetX must be a value between 0 and orderX! Assuming floor(orderX/2) as default value."); + n_int = std::floor(order.getNumber() / 2.0); + } + + targetXIsSet = true; + + if (n_int != targetX) { + targetX = n_int; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } + break; + case SPAttr::TARGETY: + if (value) { + int n_int = Inkscape::Util::read_number(value); + + if (n_int < 0 || n_int > order.getOptNumber()) { + g_warning("targetY must be a value between 0 and orderY! Assuming floor(orderY/2) as default value."); + n_int = std::floor(order.getOptNumber() / 2.0); + } + + targetYIsSet = true; + + if (n_int != targetY){ + targetY = n_int; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } + break; + case SPAttr::EDGEMODE: { + auto n_mode = ::read_edgemode(value); + if (n_mode != edgeMode) { + edgeMode = n_mode; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::KERNELUNITLENGTH: + kernelUnitLength.set(value); + + //From SVG spec: If the <dy> value is not specified, it defaults to the same value as <dx>. + if (!kernelUnitLength.optNumIsSet()) { + kernelUnitLength.setOptNumber(kernelUnitLength.getNumber()); + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::PRESERVEALPHA: { + bool read_bool = Inkscape::Util::read_bool(value, false); + if (read_bool != preserveAlpha) { + preserveAlpha = read_bool; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeConvolveMatrix::build_renderer(Inkscape::DrawingItem*) const +{ + auto convolve = std::make_unique<Inkscape::Filters::FilterConvolveMatrix>(); + build_renderer_common(convolve.get()); + + convolve->set_targetX(targetX); + convolve->set_targetY(targetY); + convolve->set_orderX(order.getNumber()); + convolve->set_orderY(order.getOptNumber()); + convolve->set_kernelMatrix(kernelMatrix); + convolve->set_divisor(divisor); + convolve->set_bias(bias); + convolve->set_preserveAlpha(preserveAlpha); + + return convolve; +} + +/* + 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 : diff --git a/src/object/filters/convolvematrix.h b/src/object/filters/convolvematrix.h new file mode 100644 index 0000000..fe9342c --- /dev/null +++ b/src/object/filters/convolvematrix.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG matrix convolution filter effect + */ +/* + * Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FECONVOLVEMATRIX_H_SEEN +#define SP_FECONVOLVEMATRIX_H_SEEN + +#include <vector> +#include "sp-filter-primitive.h" +#include "number-opt-number.h" +#include "display/nr-filter-convolve-matrix.h" + +class SPFeConvolveMatrix final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + + NumberOptNumber get_order() const { return order; } + std::vector<double> const &get_kernel_matrix() const { return kernelMatrix; } + +private: + double bias = 0.0; + Inkscape::Filters::FilterConvolveMatrixEdgeMode edgeMode = Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_DUPLICATE; + bool preserveAlpha = false; + + double divisor = 0.0; + int targetX = 1; + int targetY = 1; + std::vector<double> kernelMatrix; + + bool divisorIsSet = false; + bool targetXIsSet = false; + bool targetYIsSet = false; + bool kernelMatrixIsSet = false; + + NumberOptNumber order = NumberOptNumber(3, 3); + NumberOptNumber kernelUnitLength; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FECONVOLVEMATRIX_H_SEEN + +/* + 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 : diff --git a/src/object/filters/diffuselighting.cpp b/src/object/filters/diffuselighting.cpp new file mode 100644 index 0000000..4cf03cb --- /dev/null +++ b/src/object/filters/diffuselighting.cpp @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feDiffuseLighting> implementation. + */ +/* + * Authors: + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Jean-Rene Reinhard <jr@komite.net> + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "diffuselighting.h" +#include "distantlight.h" +#include "pointlight.h" +#include "spotlight.h" + +#include "strneq.h" +#include "attributes.h" + +#include "display/nr-filter.h" +#include "display/nr-filter-diffuselighting.h" + +#include "svg/svg.h" +#include "svg/svg-color.h" + +#include "xml/repr.h" + +void SPFeDiffuseLighting::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::SURFACESCALE); + readAttr(SPAttr::DIFFUSECONSTANT); + readAttr(SPAttr::KERNELUNITLENGTH); + readAttr(SPAttr::LIGHTING_COLOR); +} + +void SPFeDiffuseLighting::set(SPAttr key, char const *value) +{ + // TODO test forbidden values + switch (key) { + case SPAttr::SURFACESCALE: { + char *end_ptr = nullptr; + + if (value) { + surfaceScale = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + surfaceScale_set = true; + } + } + + if (!value || !end_ptr) { + surfaceScale = 1; + surfaceScale_set = false; + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::DIFFUSECONSTANT: { + char *end_ptr = nullptr; + + if (value) { + diffuseConstant = g_ascii_strtod(value, &end_ptr); + + if (end_ptr && diffuseConstant >= 0) { + diffuseConstant_set = true; + } else { + end_ptr = nullptr; + g_warning("this: diffuseConstant should be a positive number ... defaulting to 1"); + } + } + + if (!value || !end_ptr) { + diffuseConstant = 1; + diffuseConstant_set = false; + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::KERNELUNITLENGTH: + // TODO kernelUnit + // kernelUnitLength.set(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::LIGHTING_COLOR: { + char const *end_ptr = nullptr; + lighting_color = sp_svg_read_color(value, &end_ptr, 0xffffffff); + + // if a value was read + if (end_ptr) { + while (g_ascii_isspace(*end_ptr)) { + ++end_ptr; + } + + if (std::strncmp(end_ptr, "icc-color(", 10) == 0) { + icc.emplace(); + if (!sp_svg_read_icc_color(end_ptr, &*icc)) { + icc.reset(); + } + } + + lighting_color_set = true; + } else { + // lighting_color already contains the default value + lighting_color_set = false; + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +void SPFeDiffuseLighting::modified(unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + for (auto c : childList(true)) { + if (cflags || (c->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + c->emitModified(cflags); + } + sp_object_unref(c, nullptr); + } +} + +Inkscape::XML::Node *SPFeDiffuseLighting::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) +{ + // TODO: Don't just clone, but create a new repr node and write all relevant values _and children_ into it. + if (!repr) { + repr = getRepr()->duplicate(doc); + //repr = doc->createElement("svg:feDiffuseLighting"); + } + + if (surfaceScale_set) { + repr->setAttributeCssDouble("surfaceScale", surfaceScale); + } else { + repr->removeAttribute("surfaceScale"); + } + + if (diffuseConstant_set) { + repr->setAttributeCssDouble("diffuseConstant", diffuseConstant); + } else { + repr->removeAttribute("diffuseConstant"); + } + + /*TODO kernelUnits */ + if (lighting_color_set) { + char c[64]; + sp_svg_write_color(c, sizeof(c), lighting_color); + repr->setAttribute("lighting-color", c); + } else { + repr->removeAttribute("lighting-color"); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeDiffuseLighting::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + SPFilterPrimitive::child_added(child, ref); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeDiffuseLighting::remove_child(Inkscape::XML::Node *child) +{ + SPFilterPrimitive::remove_child(child); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeDiffuseLighting::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) +{ + SPFilterPrimitive::order_changed(child, old_ref, new_ref); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeDiffuseLighting::build_renderer(Inkscape::DrawingItem*) const +{ + auto diffuselighting = std::make_unique<Inkscape::Filters::FilterDiffuseLighting>(); + build_renderer_common(diffuselighting.get()); + + diffuselighting->diffuseConstant = diffuseConstant; + diffuselighting->surfaceScale = surfaceScale; + diffuselighting->lighting_color = lighting_color; + if (icc) { + diffuselighting->set_icc(*icc); + } + + // We assume there is at most one child + diffuselighting->light_type = Inkscape::Filters::NO_LIGHT; + + if (auto l = cast<SPFeDistantLight>(firstChild())) { + diffuselighting->light_type = Inkscape::Filters::DISTANT_LIGHT; + diffuselighting->light.distant.azimuth = l->azimuth; + diffuselighting->light.distant.elevation = l->elevation; + } else if (auto l = cast<SPFePointLight>(firstChild())) { + diffuselighting->light_type = Inkscape::Filters::POINT_LIGHT; + diffuselighting->light.point.x = l->x; + diffuselighting->light.point.y = l->y; + diffuselighting->light.point.z = l->z; + } else if (auto l = cast<SPFeSpotLight>(firstChild())) { + diffuselighting->light_type = Inkscape::Filters::SPOT_LIGHT; + diffuselighting->light.spot.x = l->x; + diffuselighting->light.spot.y = l->y; + diffuselighting->light.spot.z = l->z; + diffuselighting->light.spot.pointsAtX = l->pointsAtX; + diffuselighting->light.spot.pointsAtY = l->pointsAtY; + diffuselighting->light.spot.pointsAtZ = l->pointsAtZ; + diffuselighting->light.spot.limitingConeAngle = l->limitingConeAngle; + diffuselighting->light.spot.specularExponent = l->specularExponent; + } + + return diffuselighting; +} + +/* + 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 : diff --git a/src/object/filters/diffuselighting.h b/src/object/filters/diffuselighting.h new file mode 100644 index 0000000..221b385 --- /dev/null +++ b/src/object/filters/diffuselighting.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG diffuse lighting filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2006-2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEDIFFUSELIGHTING_H_SEEN +#define SP_FEDIFFUSELIGHTING_H_SEEN + +#include <optional> +#include <cstdint> +#include "sp-filter-primitive.h" +#include "svg/svg-icc-color.h" +#include "number-opt-number.h" + +struct SVGICCColor; + +namespace Inkscape { +namespace Filters { +class FilterDiffuseLighting; +} // namespace Filters +} // namespace Inkscape + +class SPFeDiffuseLighting final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + +private: + float surfaceScale = 1.0f; + float diffuseConstant = 1.0f; + uint32_t lighting_color = 0xffffffff; + + bool surfaceScale_set = false; + bool diffuseConstant_set = false; + bool lighting_color_set = false; + + NumberOptNumber kernelUnitLength; // TODO + std::optional<SVGICCColor> icc; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + void modified(unsigned flags) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; + + void child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_repr, Inkscape::XML::Node *new_repr) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FEDIFFUSELIGHTING_H_SEEN + +/* + 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 : diff --git a/src/object/filters/displacementmap.cpp b/src/object/filters/displacementmap.cpp new file mode 100644 index 0000000..ba642f8 --- /dev/null +++ b/src/object/filters/displacementmap.cpp @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feDisplacementMap> implementation. + */ +/* + * Authors: + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "displacementmap.h" +#include "attributes.h" +#include "display/nr-filter-displacement-map.h" +#include "display/nr-filter.h" +#include "object/sp-filter.h" +#include "svg/svg.h" +#include "util/numeric/converters.h" +#include "xml/repr.h" +#include "slot-resolver.h" +#include "util/optstr.h" + +void SPFeDisplacementMap::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::SCALE); + readAttr(SPAttr::IN2); + readAttr(SPAttr::XCHANNELSELECTOR); + readAttr(SPAttr::YCHANNELSELECTOR); +} + +static FilterDisplacementMapChannelSelector read_channel_selector(char const *value) +{ + if (!value) return DISPLACEMENTMAP_CHANNEL_ALPHA; + + switch (value[0]) { + case 'R': + return DISPLACEMENTMAP_CHANNEL_RED; + break; + case 'G': + return DISPLACEMENTMAP_CHANNEL_GREEN; + break; + case 'B': + return DISPLACEMENTMAP_CHANNEL_BLUE; + break; + case 'A': + return DISPLACEMENTMAP_CHANNEL_ALPHA; + break; + default: + // error + g_warning("Invalid attribute for Channel Selector. Valid modes are 'R', 'G', 'B' or 'A'"); + break; + } + + return DISPLACEMENTMAP_CHANNEL_ALPHA; // default is Alpha Channel +} + +void SPFeDisplacementMap::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::XCHANNELSELECTOR: { + auto n_selector = ::read_channel_selector(value); + if (n_selector != xChannelSelector) { + xChannelSelector = n_selector; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::YCHANNELSELECTOR: { + auto n_selector = ::read_channel_selector(value); + if (n_selector != yChannelSelector) { + yChannelSelector = n_selector; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::SCALE: { + double n_num = value ? Inkscape::Util::read_number(value) : 0.0; + if (n_num != scale) { + scale = n_num; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::IN2: { + if (Inkscape::Util::assign(in2_name, value)) { + requestModified(SP_OBJECT_MODIFIED_FLAG); + invalidate_parent_slots(); + } + break; + } + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +static char const *get_channelselector_name(FilterDisplacementMapChannelSelector selector) +{ + switch (selector) { + case DISPLACEMENTMAP_CHANNEL_RED: + return "R"; + case DISPLACEMENTMAP_CHANNEL_GREEN: + return "G"; + case DISPLACEMENTMAP_CHANNEL_BLUE: + return "B"; + case DISPLACEMENTMAP_CHANNEL_ALPHA: + return "A"; + default: + return nullptr; + } +} + +Inkscape::XML::Node *SPFeDisplacementMap::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) +{ + if (!repr) { + repr = doc->createElement("svg:feDisplacementMap"); + } + + repr->setAttributeOrRemoveIfEmpty("in2", Inkscape::Util::to_cstr(in2_name)); + repr->setAttributeSvgDouble("scale", scale); + repr->setAttribute("xChannelSelector", get_channelselector_name(xChannelSelector)); + repr->setAttribute("yChannelSelector", get_channelselector_name(yChannelSelector)); + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeDisplacementMap::resolve_slots(SlotResolver &resolver) +{ + in2_slot = resolver.read(in2_name); + SPFilterPrimitive::resolve_slots(resolver); +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeDisplacementMap::build_renderer(Inkscape::DrawingItem*) const +{ + auto displacement_map = std::make_unique<Inkscape::Filters::FilterDisplacementMap>(); + build_renderer_common(displacement_map.get()); + + displacement_map->set_input(1, in2_slot); + displacement_map->set_scale(scale); + displacement_map->set_channel_selector(0, xChannelSelector); + displacement_map->set_channel_selector(1, yChannelSelector); + + return displacement_map; +} + +/* + 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 : diff --git a/src/object/filters/displacementmap.h b/src/object/filters/displacementmap.h new file mode 100644 index 0000000..5804cf5 --- /dev/null +++ b/src/object/filters/displacementmap.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG displacement map filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEDISPLACEMENTMAP_H_SEEN +#define SP_FEDISPLACEMENTMAP_H_SEEN + +#include "sp-filter-primitive.h" +#include "display/nr-filter-types.h" + +enum FilterDisplacementMapChannelSelector +{ + DISPLACEMENTMAP_CHANNEL_RED, + DISPLACEMENTMAP_CHANNEL_GREEN, + DISPLACEMENTMAP_CHANNEL_BLUE, + DISPLACEMENTMAP_CHANNEL_ALPHA, + DISPLACEMENTMAP_CHANNEL_ENDTYPE +}; + +class SPFeDisplacementMap final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + + int get_in2() const { return in2_slot; } + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; + + void resolve_slots(SlotResolver &) override; + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; + +private: + double scale = 0.0; + FilterDisplacementMapChannelSelector xChannelSelector = DISPLACEMENTMAP_CHANNEL_ALPHA; + FilterDisplacementMapChannelSelector yChannelSelector = DISPLACEMENTMAP_CHANNEL_ALPHA; + + std::optional<std::string> in2_name; + int in2_slot = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +}; + +#endif // SP_FEDISPLACEMENTMAP_H_SEEN + +/* + 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 : diff --git a/src/object/filters/distantlight.cpp b/src/object/filters/distantlight.cpp new file mode 100644 index 0000000..f09705e --- /dev/null +++ b/src/object/filters/distantlight.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <fedistantlight> implementation. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Jean-Rene Reinhard <jr@komite.net> + * Abhishek Sharma + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "distantlight.h" +#include "diffuselighting.h" +#include "specularlighting.h" + +#include "attributes.h" +#include "document.h" + +#include "xml/repr.h" + +SPFeDistantLight::SPFeDistantLight() + : azimuth(0) + , azimuth_set(false) + , elevation(0) + , elevation_set(false) +{ +} + +SPFeDistantLight::~SPFeDistantLight() = default; + +void SPFeDistantLight::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + readAttr(SPAttr::AZIMUTH); + readAttr(SPAttr::ELEVATION); + + document->addResource("fedistantlight", this); +} + +void SPFeDistantLight::release() +{ + if (document) { + document->removeResource("fedistantlight", this); + } + + SPObject::release(); +} + +void SPFeDistantLight::set(SPAttr key, char const *value) +{ + auto read_float = [=] (float &var, float def = 0) -> bool { + if (value) { + char *end_ptr; + auto tmp = g_ascii_strtod(value, &end_ptr); + if (end_ptr) { + var = tmp; + return true; + } + } + var = def; + return false; + }; + + switch (key) { + case SPAttr::AZIMUTH: + azimuth_set = read_float(azimuth); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::ELEVATION: + elevation_set = read_float(elevation); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPObject::set(key, value); + break; + } +} + +Inkscape::XML::Node *SPFeDistantLight::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) +{ + if (!repr) { + repr = getRepr()->duplicate(doc); + } + + if (azimuth_set) { + repr->setAttributeCssDouble("azimuth", azimuth); + } + + if (elevation_set) { + repr->setAttributeCssDouble("elevation", elevation); + } + + SPObject::write(doc, repr, flags); + + return repr; +} + +/* + 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 : diff --git a/src/object/filters/distantlight.h b/src/object/filters/distantlight.h new file mode 100644 index 0000000..255d7de --- /dev/null +++ b/src/object/filters/distantlight.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_FEDISTANTLIGHT_H_SEEN +#define SP_FEDISTANTLIGHT_H_SEEN + +/** \file + * SVG <filter> implementation, see sp-filter.cpp. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-object.h" + +class SPFeDistantLight final + : public SPObject +{ +public: + SPFeDistantLight(); + ~SPFeDistantLight() override; + int tag() const override { return tag_of<decltype(*this)>; } + + /// azimuth attribute + float azimuth; + bool azimuth_set : 1; + /// elevation attribute + float elevation; + bool elevation_set : 1; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const *value) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; +}; + +#endif // SP_FEDISTANTLIGHT_H_SEEN + +/* + 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 : diff --git a/src/object/filters/flood.cpp b/src/object/filters/flood.cpp new file mode 100644 index 0000000..815aea2 --- /dev/null +++ b/src/object/filters/flood.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feFlood> implementation. + */ +/* + * Authors: + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "flood.h" +#include "strneq.h" +#include "attributes.h" +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "display/nr-filter.h" +#include "display/nr-filter-flood.h" +#include "xml/repr.h" + +void SPFeFlood::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::FLOOD_OPACITY); + readAttr(SPAttr::FLOOD_COLOR); +} + +void SPFeFlood::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::FLOOD_COLOR: { + char const *end_ptr = nullptr; + uint32_t n_color = sp_svg_read_color(value, &end_ptr, 0x0); + + bool modified = false; + if (n_color != color) { + color = n_color; + modified = true; + } + + if (end_ptr) { + while (g_ascii_isspace(*end_ptr)) { + ++end_ptr; + } + + if (std::strncmp(end_ptr, "icc-color(", 10) == 0) { + icc.emplace(); + + if (!sp_svg_read_icc_color(end_ptr, &*icc)) { + icc.reset(); + } + + modified = true; + } + } + + if (modified) { + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::FLOOD_OPACITY: { + double n_opacity; + if (value) { + char *end_ptr = nullptr; + n_opacity = g_ascii_strtod(value, &end_ptr); + + if (end_ptr && *end_ptr) { + g_warning("Unable to convert \"%s\" to number", value); + n_opacity = 1; + } + } else { + n_opacity = 1; + } + + if (n_opacity != opacity) { + opacity = n_opacity; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeFlood::build_renderer(Inkscape::DrawingItem*) const +{ + auto flood = std::make_unique<Inkscape::Filters::FilterFlood>(); + build_renderer_common(flood.get()); + + flood->set_opacity(opacity); + flood->set_color(color); + if (icc) { + flood->set_icc(*icc); + } + + return flood; +} + +/* + 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 : diff --git a/src/object/filters/flood.h b/src/object/filters/flood.h new file mode 100644 index 0000000..99cc4cf --- /dev/null +++ b/src/object/filters/flood.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG flood filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEFLOOD_H_SEEN +#define SP_FEFLOOD_H_SEEN + +#include <optional> +#include <cstdint> +#include "sp-filter-primitive.h" +#include "svg/svg-icc-color.h" + +class SPFeFlood final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + +private: + uint32_t color = 0x0; + double opacity = 1.0; + std::optional<SVGICCColor> icc; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FEFLOOD_H_SEEN + +/* + 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 : diff --git a/src/object/filters/gaussian-blur.cpp b/src/object/filters/gaussian-blur.cpp new file mode 100644 index 0000000..a0b1968 --- /dev/null +++ b/src/object/filters/gaussian-blur.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <gaussianBlur> implementation. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Abhishek Sharma + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "gaussian-blur.h" +#include "attributes.h" +#include "display/nr-filter.h" +#include "display/nr-filter-gaussian.h" +#include "svg/svg.h" +#include "util/numeric/converters.h" +#include "xml/repr.h" + +void SPGaussianBlur::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + readAttr(SPAttr::STDDEVIATION); +} + +void SPGaussianBlur::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::STDDEVIATION: + stdDeviation.set(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPGaussianBlur::build_renderer(Inkscape::DrawingItem*) const +{ + auto blur = std::make_unique<Inkscape::Filters::FilterGaussian>(); + build_renderer_common(blur.get()); + + float num = stdDeviation.getNumber(); + + if (num >= 0.0) { + float optnum = stdDeviation.getOptNumber(); + if (optnum >= 0.0) { + blur->set_deviation(num, optnum); + } else { + blur->set_deviation(num); + } + } + + return blur; +} + +void SPGaussianBlur::set_deviation(const NumberOptNumber &stdDeviation) +{ + double num = stdDeviation.getNumber(); + std::string arg = Inkscape::Util::format_number(num); + + double optnum = stdDeviation.getOptNumber(); + if (optnum != num && optnum != -1) { + arg += " " + Inkscape::Util::format_number(optnum); + } + getRepr()->setAttribute("stdDeviation", arg); +} + +/** Calculate the region taken up by gaussian blur + * + * @param region The original shape's region or previous primitive's region output. + */ +Geom::Rect SPGaussianBlur::calculate_region(Geom::Rect const ®ion) const +{ + double x = stdDeviation.getNumber(); + double y = stdDeviation.getOptNumber(); + if (y == -1.0) { + y = x; + } + // If not within the default 10% margin (see + // http://www.w3.org/TR/SVG11/filters.html#FilterEffectsRegion), specify margins + // The 2.4 is an empirical coefficient: at that distance the cutoff is practically invisible + // (the opacity at 2.4 * radius is about 3e-3) + auto r = region; + r.expandBy(2.4 * x, 2.4 * y); + return r; +} + +/* + 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 : diff --git a/src/object/filters/gaussian-blur.h b/src/object/filters/gaussian-blur.h new file mode 100644 index 0000000..3fef726 --- /dev/null +++ b/src/object/filters/gaussian-blur.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG Gaussian blur filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_GAUSSIANBLUR_H_SEEN +#define SP_GAUSSIANBLUR_H_SEEN + +#include "sp-filter-primitive.h" +#include "number-opt-number.h" + +class SPGaussianBlur final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + + Geom::Rect calculate_region(Geom::Rect const ®ion) const override; + + NumberOptNumber const &get_std_deviation() const { return stdDeviation; } + void set_deviation(const NumberOptNumber &stdDeviation); + +private: + NumberOptNumber stdDeviation; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_GAUSSIANBLUR_H_SEEN + +/* + 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 : diff --git a/src/object/filters/image.cpp b/src/object/filters/image.cpp new file mode 100644 index 0000000..6f0c879 --- /dev/null +++ b/src/object/filters/image.cpp @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feImage> implementation. + */ +/* + * Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2007 Felipe Sanches + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "image.h" + +#include <sigc++/bind.h> + +#include "attributes.h" + +#include "bad-uri-exception.h" +#include "document.h" +#include "display/cairo-utils.h" +#include "display/drawing-image.h" + +#include "object/sp-image.h" +#include "object/uri.h" +#include "object/uri-references.h" + +#include "display/nr-filter-image.h" +#include "display/nr-filter.h" + +#include "xml/repr.h" + +SPFeImage::SPFeImage() + : elemref(std::make_unique<Inkscape::URIReference>(this)) {} + +void SPFeImage::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::XLINK_HREF); + readAttr(SPAttr::PRESERVEASPECTRATIO); +} + +void SPFeImage::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::XLINK_HREF: + href = value ? value : ""; + reread_href(); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::PRESERVEASPECTRATIO: + /* Copied from sp-image.cpp */ + /* Do setup before, so we can use break to escape */ + aspect_align = SP_ASPECT_XMID_YMID; // Default + aspect_clip = SP_ASPECT_MEET; // Default + requestModified(SP_OBJECT_MODIFIED_FLAG); + if (value) { + int len; + char c[256]; + char const *p, *e; + unsigned int align, clip; + p = value; + while (*p && *p == 32) p += 1; + if (!*p) break; + e = p; + while (*e && *e != 32) e += 1; + len = e - p; + if (len > 8) break; + std::memcpy(c, value, len); + c[len] = 0; + /* Now the actual part */ + if (!std::strcmp(c, "none")) { + align = SP_ASPECT_NONE; + } else if (!std::strcmp(c, "xMinYMin")) { + align = SP_ASPECT_XMIN_YMIN; + } else if (!std::strcmp(c, "xMidYMin")) { + align = SP_ASPECT_XMID_YMIN; + } else if (!std::strcmp(c, "xMaxYMin")) { + align = SP_ASPECT_XMAX_YMIN; + } else if (!std::strcmp(c, "xMinYMid")) { + align = SP_ASPECT_XMIN_YMID; + } else if (!std::strcmp(c, "xMidYMid")) { + align = SP_ASPECT_XMID_YMID; + } else if (!std::strcmp(c, "xMaxYMid")) { + align = SP_ASPECT_XMAX_YMID; + } else if (!std::strcmp(c, "xMinYMax")) { + align = SP_ASPECT_XMIN_YMAX; + } else if (!std::strcmp(c, "xMidYMax")) { + align = SP_ASPECT_XMID_YMAX; + } else if (!std::strcmp(c, "xMaxYMax")) { + align = SP_ASPECT_XMAX_YMAX; + } else { + g_warning("Illegal preserveAspectRatio: %s", c); + break; + } + clip = SP_ASPECT_MEET; + while (*e && *e == 32) e += 1; + if (*e) { + if (!std::strcmp(e, "meet")) { + clip = SP_ASPECT_MEET; + } else if (!std::strcmp(e, "slice")) { + clip = SP_ASPECT_SLICE; + } else { + break; + } + } + aspect_align = align; + aspect_clip = clip; + } else { + aspect_align = SP_ASPECT_XMID_YMID; // Default + aspect_clip = SP_ASPECT_MEET; // Default + } + break; + + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +void SPFeImage::try_load_image() +{ + /* TODO: If feImageHref is absolute, then use that (preferably handling the + * case that it's not a file URI). Otherwise, go up the tree looking + * for an xml:base attribute, and use that as the base URI for resolving + * the relative feImageHref URI. Otherwise, if document->base is valid, + * then use that as the base URI. Otherwise, use feImageHref directly + * (i.e. interpreting it as relative to our current working directory). + * (See http://www.w3.org/TR/xmlbase/#resolution .) */ + + auto try_assign = [this] (char const *name) { + if (!g_file_test(name, G_FILE_TEST_IS_REGULAR)) { + return false; + } + + auto img = Inkscape::Pixbuf::create_from_file(name); + if (!img) { + return false; + } + + // Rendering code expects cairo format, so ensure this before making pixbuf immutable. + img->ensurePixelFormat(Inkscape::Pixbuf::PF_CAIRO); + + pixbuf.reset(img); + return true; + }; + + if (try_assign(href.data())) { + // pass + } else { + auto fullname = g_build_filename(document->getDocumentBase(), href.data(), nullptr); + if (try_assign(fullname)) { + // pass + } else { + pixbuf.reset(); + } + g_free(fullname); + } +} + +void SPFeImage::reread_href() +{ + // Disconnect from modification signals. + _href_changed_connection.disconnect(); + if (type == ELEM) { + _href_modified_connection.disconnect(); + } + + for (auto &v : views) { + destroy_view(v); + } + + // Set type, elemref, elem and pixbuf. + try { + elemref->attach(Inkscape::URI(href.data())); + } catch (Inkscape::BadURIException const &) { + elemref->detach(); + } + pixbuf.reset(); + if (auto obj = elemref->getObject()) { + elem = cast<SPItem>(obj); + if (elem) { + type = ELEM; + } else { + type = NONE; + g_warning("SPFeImage::reread_href: %s points to non-item element", href.data()); + } + } else { + try_load_image(); + if (pixbuf) { + type = IMAGE; + } else { + type = NONE; + g_warning("SPFeImage::reread_href: failed to load image: %s", href.data()); + } + } + + for (auto &v : views) { + create_view(v); + } + + // Connect to modification signals. + _href_changed_connection = elemref->changedSignal().connect([this] (SPObject*, SPObject *to) { on_href_changed(to); }); + if (type == ELEM) { + _href_modified_connection = elemref->getObject()->connectModified([this] (SPObject*, unsigned) { on_href_modified(); }); + } +} + +void SPFeImage::on_href_changed(SPObject *new_obj) +{ + if (type == ELEM) { + _href_modified_connection.disconnect(); + } + + for (auto &v : views) { + destroy_view(v); + } + + // Set type and image. + pixbuf.reset(); + if (new_obj) { + elem = cast<SPItem>(new_obj); + if (elem) { + type = ELEM; + } else { + type = NONE; + g_warning("SPFeImage::on_href_changed: %s points to non-item element", href.data()); + } + } else { + try_load_image(); + if (pixbuf) { + type = IMAGE; + } else { + type = NONE; + g_warning("SPFeImage::on_href_changed: failed to load image: %s", href.data()); + } + } + + for (auto &v : views) { + create_view(v); + } + + if (type == ELEM) { + _href_modified_connection = elem->connectModified([this] (SPObject*, unsigned) { on_href_modified(); }); + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeImage::on_href_modified() +{ + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeImage::release() +{ + _href_changed_connection.disconnect(); + _href_modified_connection.disconnect(); + elemref.reset(); + pixbuf.reset(); + + // All views on this element should have been closed prior to release. + assert(views.empty()); + + SPFilterPrimitive::release(); +} + +void SPFeImage::destroy_view(View &v) +{ + if (type == ELEM) { + elem->invoke_hide(v.inner_key); + } else if (type == IMAGE) { + v.child->unlink(); + } + + // Defensive-coding measure: clear filter renderer immediately. + v.parent->setFilterRenderer(nullptr); +} + +void SPFeImage::create_view(View &v) +{ + if (type == ELEM) { + auto ai = elem->invoke_show(v.parent->drawing(), v.inner_key, SP_ITEM_SHOW_DISPLAY); + v.child = ai; + if (!v.child) { + g_warning("SPFeImage::show: error creating DrawingItem for SVG Element"); + } + } else if (type == IMAGE) { + auto ai = new Inkscape::DrawingImage(v.parent->drawing()); + ai->setStyle(style); + ai->setPixbuf(pixbuf); + ai->setOrigin(Geom::Point(0, 0)); + ai->setScale(1.0, 1.0); + ai->setClipbox(Geom::Rect(0, 0, pixbuf->width(), pixbuf->height())); + v.child = ai; + } +} + +void SPFeImage::show(Inkscape::DrawingItem *parent) +{ + views.emplace_back(); + auto &v = views.back(); + + v.parent = parent; + v.inner_key = SPItem::display_key_new(1); + + create_view(v); +} + +void SPFeImage::hide(Inkscape::DrawingItem *parent) +{ + auto it = std::find_if(views.begin(), views.end(), [parent] (auto &v) { + return v.parent == parent; + }); + assert(it != views.end()); + auto &v = *it; + + destroy_view(v); + + views.erase(it); +} + +/* + * Check if the object is being used in the filter's definition + * and returns true if it is being used (to avoid infinite loops) + */ +bool SPFeImage::valid_for(SPObject const *obj) const +{ + // elem could be nullptr, but this should still work. + return obj && cast<SPItem>(obj) != elem; +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeImage::build_renderer(Inkscape::DrawingItem *parent) const +{ + Inkscape::DrawingItem *child = nullptr; + + if (type != NONE) { + auto it = std::find_if(views.begin(), views.end(), [parent] (auto &v) { + return v.parent == parent; + }); + assert(it != views.end()); + child = it->child; + } + + auto image = std::make_unique<Inkscape::Filters::FilterImage>(); + build_renderer_common(image.get()); + + image->item = child; + image->from_element = type == ELEM; + image->set_align(aspect_align); + image->set_clip(aspect_clip); + + return image; +} + +/* + 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 : diff --git a/src/object/filters/image.h b/src/object/filters/image.h new file mode 100644 index 0000000..d568cca --- /dev/null +++ b/src/object/filters/image.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG image filter effect + *//* + * Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEIMAGE_H_SEEN +#define SP_FEIMAGE_H_SEEN + +#include <memory> +#include "sp-filter-primitive.h" +#include "enums.h" + +class SPItem; + +namespace Inkscape { +class URIReference; +class DrawingItem; +class Drawing; +class Pixbuf; +} // namespace Inksacpe + +class SPFeImage final + : public SPFilterPrimitive +{ +public: + SPFeImage(); + int tag() const override { return tag_of<decltype(*this)>; } + +private: + std::string href; + + // preserveAspectRatio + unsigned char aspect_align = SP_ASPECT_XMID_YMID; + unsigned char aspect_clip = SP_ASPECT_MEET; + + enum Type + { + ELEM, // If href points to an element that is an SPItem. + IMAGE, // If href points to non-element that is an image filename. + NONE // Neither of the above. + }; + Type type = NONE; + std::unique_ptr<Inkscape::URIReference> elemref; // Tracks href if it is a valid URI. + SPItem *elem; // If type == ELEM, the referenced element. + std::shared_ptr<Inkscape::Pixbuf const> pixbuf; // If type == IMAGE, the loaded image. + + sigc::connection _href_changed_connection; // Tracks the reference being reattached. + sigc::connection _href_modified_connection; // If type == ELEM, tracks the referenced object being modified. + + void try_load_image(); + void reread_href(); + + void on_href_changed(SPObject *new_elem); + void on_href_modified(); + + struct View + { + Inkscape::DrawingItem *parent; // The item to which the filter is applied. + Inkscape::DrawingItem *child; // The element or image shown by the filter. + unsigned inner_key; // The display key at which child is shown at. + }; + std::vector<View> views; + void create_view(View &v); + void destroy_view(View &v); + + bool valid_for(SPObject const *obj) const override; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const *value) override; + + void show(Inkscape::DrawingItem *item) override; + void hide(Inkscape::DrawingItem *item) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FEIMAGE_H_SEEN + +/* + 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 : diff --git a/src/object/filters/merge.cpp b/src/object/filters/merge.cpp new file mode 100644 index 0000000..2481180 --- /dev/null +++ b/src/object/filters/merge.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feMerge> implementation. + */ +/* + * Authors: + * hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "svg/svg.h" +#include "xml/repr.h" + +#include "merge.h" +#include "mergenode.h" +#include "display/nr-filter.h" +#include "display/nr-filter-merge.h" + +void SPFeMerge::modified(unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + for (auto &c : children) { + if (cflags || (c.mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + c.emitModified(cflags); + } + } +} + +void SPFeMerge::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + SPFilterPrimitive::child_added(child, ref); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeMerge::remove_child(Inkscape::XML::Node *child) +{ + SPFilterPrimitive::remove_child(child); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeMerge::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) +{ + SPFilterPrimitive::order_changed(child, old_ref, new_ref); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeMerge::resolve_slots(SlotResolver &resolver) +{ + for (auto &input : children) { + if (auto node = cast<SPFeMergeNode>(&input)) { + node->resolve_slots(std::as_const(resolver)); + } + } + SPFilterPrimitive::resolve_slots(resolver); +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeMerge::build_renderer(Inkscape::DrawingItem*) const +{ + auto merge = std::make_unique<Inkscape::Filters::FilterMerge>(); + build_renderer_common(merge.get()); + + int in_nr = 0; + + for (auto const &input : children) { + if (auto node = cast<SPFeMergeNode>(&input)) { + merge->set_input(in_nr, node->get_in()); + in_nr++; + } + } + + return merge; +} + +/* + 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 : diff --git a/src/object/filters/merge.h b/src/object/filters/merge.h new file mode 100644 index 0000000..9a7159f --- /dev/null +++ b/src/object/filters/merge.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG merge filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FEMERGE_H_SEEN +#define SP_FEMERGE_H_SEEN + +#include "sp-filter-primitive.h" + +class SPFeMerge final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void modified(unsigned flags) override; + + void child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) override; + + void resolve_slots(SlotResolver &) override; + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FEMERGE_H_SEEN + +/* + 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 : diff --git a/src/object/filters/mergenode.cpp b/src/object/filters/mergenode.cpp new file mode 100644 index 0000000..e7dcfbf --- /dev/null +++ b/src/object/filters/mergenode.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * feMergeNode implementation. A feMergeNode contains the name of one + * input image for feMerge. + */ +/* + * Authors: + * Kees Cook <kees@outflux.net> + * Niko Kiirala <niko@kiirala.com> + * Abhishek Sharma + * + * Copyright (C) 2004,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "mergenode.h" +#include "merge.h" +#include "object/sp-filter.h" + +#include "attributes.h" +#include "xml/repr.h" +#include "slot-resolver.h" +#include "util/optstr.h" + +void SPFeMergeNode::build(SPDocument */*document*/, Inkscape::XML::Node */*repr*/) +{ + readAttr(SPAttr::IN_); +} + +void SPFeMergeNode::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::IN_: + if (Inkscape::Util::assign(in_name, value)) { + requestModified(SP_OBJECT_MODIFIED_FLAG); + invalidate_parent_slots(); + } + break; + default: + SPObject::set(key, value); + break; + } +} + +void SPFeMergeNode::invalidate_parent_slots() +{ + if (auto merge = cast<SPFeMerge>(parent)) { + merge->invalidate_parent_slots(); + } +} + +void SPFeMergeNode::resolve_slots(SlotResolver const &resolver) +{ + in_slot = resolver.read(in_name); +} + +/* + 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 : diff --git a/src/object/filters/mergenode.h b/src/object/filters/mergenode.h new file mode 100644 index 0000000..01d2f27 --- /dev/null +++ b/src/object/filters/mergenode.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_FEMERGENODE_H_SEEN +#define SP_FEMERGENODE_H_SEEN + +/** \file + * feMergeNode implementation. A feMergeNode stores information about one + * input image for feMerge filter primitive. + */ +/* + * Authors: + * Kees Cook <kees@outflux.net> + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2004,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <optional> +#include <string> +#include "object/sp-object.h" +#include "display/nr-filter-types.h" + +class SlotResolver; + +class SPFeMergeNode final + : public SPObject +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + + int get_in() const { return in_slot; } + + void invalidate_parent_slots(); + void resolve_slots(SlotResolver const &); + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + +private: + std::optional<std::string> in_name; + int in_slot = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +}; + +#endif // SP_FEMERGENODE_H_SEEN + +/* + 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 : diff --git a/src/object/filters/morphology.cpp b/src/object/filters/morphology.cpp new file mode 100644 index 0000000..dd3fb6f --- /dev/null +++ b/src/object/filters/morphology.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feMorphology> implementation. + */ +/* + * Authors: + * Felipe Sanches <juca@members.fsf.org> + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> + +#include "attributes.h" +#include "svg/svg.h" +#include "morphology.h" +#include "xml/repr.h" +#include "display/nr-filter.h" + +void SPFeMorphology::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::OPERATOR); + readAttr(SPAttr::RADIUS); +} + +static Inkscape::Filters::FilterMorphologyOperator read_operator(char const *value) +{ + if (!value) { + return Inkscape::Filters::MORPHOLOGY_OPERATOR_ERODE; // erode is default + } + + switch (value[0]) { + case 'e': + if (std::strcmp(value, "erode") == 0) { + return Inkscape::Filters::MORPHOLOGY_OPERATOR_ERODE; + } + break; + case 'd': + if (std::strcmp(value, "dilate") == 0) { + return Inkscape::Filters::MORPHOLOGY_OPERATOR_DILATE; + } + break; + } + + return Inkscape::Filters::MORPHOLOGY_OPERATOR_ERODE; // erode is default +} + +void SPFeMorphology::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::OPERATOR: { + auto n_op = ::read_operator(value); + if (n_op != Operator) { + Operator = n_op; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::RADIUS: + radius.set(value); + + // From SVG spec: If <y-radius> is not provided, it defaults to <x-radius>. + if (!radius.optNumIsSet()) { + radius.setOptNumber(radius.getNumber()); + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeMorphology::build_renderer(Inkscape::DrawingItem*) const +{ + auto morphology = std::make_unique<Inkscape::Filters::FilterMorphology>(); + build_renderer_common(morphology.get()); + + morphology->set_operator(Operator); + morphology->set_xradius(radius.getNumber()); + morphology->set_yradius(radius.getOptNumber()); + + return morphology; +} + +/** + * Calculate the region taken up by a mophoplogy primitive + * + * @param region The original shape's region or previous primitive's region output. + */ +Geom::Rect SPFeMorphology::calculate_region(Geom::Rect const ®ion) const +{ + auto r = region; + if (Operator == Inkscape::Filters::MORPHOLOGY_OPERATOR_DILATE) { + if (radius.optNumIsSet()) { + r.expandBy(radius.getNumber(), radius.getOptNumber()); + } else { + r.expandBy(radius.getNumber()); + } + } else if (Operator == Inkscape::Filters::MORPHOLOGY_OPERATOR_ERODE) { + if (radius.optNumIsSet()) { + r.expandBy(-1 * radius.getNumber(), -1 * radius.getOptNumber()); + } else { + r.expandBy(-1 * radius.getNumber()); + } + } + return r; +} + +/* + 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 : diff --git a/src/object/filters/morphology.h b/src/object/filters/morphology.h new file mode 100644 index 0000000..0aaf48a --- /dev/null +++ b/src/object/filters/morphology.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * @brief SVG morphology filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEMORPHOLOGY_H_SEEN +#define SP_FEMORPHOLOGY_H_SEEN + +#include "sp-filter-primitive.h" +#include "number-opt-number.h" +#include "display/nr-filter-morphology.h" + +class SPFeMorphology final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + Geom::Rect calculate_region(Geom::Rect const ®ion) const override; + +private: + Inkscape::Filters::FilterMorphologyOperator Operator = Inkscape::Filters::MORPHOLOGY_OPERATOR_ERODE; + NumberOptNumber radius = NumberOptNumber(0); + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FEMORPHOLOGY_H_SEEN + +/* + 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 : diff --git a/src/object/filters/offset.cpp b/src/object/filters/offset.cpp new file mode 100644 index 0000000..acf47d7 --- /dev/null +++ b/src/object/filters/offset.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feOffset> implementation. + */ +/* + * Authors: + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Abhishek Sharma + * + * Copyright (C) 2006,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/transforms.h> + +#include "offset.h" +#include "attributes.h" + +#include "display/nr-filter.h" +#include "display/nr-filter-offset.h" + +#include "util/numeric/converters.h" +#include "svg/svg.h" +#include "xml/repr.h" + +void SPFeOffset::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::DX); + readAttr(SPAttr::DY); +} + +void SPFeOffset::set(SPAttr key, char const *value) +{ + switch(key) { + case SPAttr::DX: { + double read_num = value ? Inkscape::Util::read_number(value) : 0.0; + if (read_num != dx) { + dx = read_num; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::DY: + { + double read_num = value ? Inkscape::Util::read_number(value) : 0.0; + if (read_num != dy) { + dy = read_num; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeOffset::build_renderer(Inkscape::DrawingItem*) const +{ + auto offset = std::make_unique<Inkscape::Filters::FilterOffset>(); + build_renderer_common(offset.get()); + + offset->set_dx(dx); + offset->set_dy(dy); + + return offset; +} + +/** + * Calculate the region taken up by an offset + * + * @param region The original shape's region or previous primitive's region output. + */ +Geom::Rect SPFeOffset::calculate_region(Geom::Rect const ®ion) const +{ + // Because blur calculates its drawing space based on the resulting region. + // An offset will actually harm blur's ability to draw, even though it shouldn't + // A future fix would require the blur to figure out its region minus any downstream + // offset (this affects drop-shadows). + // TODO: region *= Geom::Translate(dx, dy); + auto r = region; + r.unionWith(r * Geom::Translate(dx, dy)); + return r; +} + +/* + 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 : diff --git a/src/object/filters/offset.h b/src/object/filters/offset.h new file mode 100644 index 0000000..80b2716 --- /dev/null +++ b/src/object/filters/offset.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG offset filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEOFFSET_H_SEEN +#define SP_FEOFFSET_H_SEEN + +#include "sp-filter-primitive.h" + +class SPFeOffset final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + + Geom::Rect calculate_region(Geom::Rect const ®ion) const override; + +private: + double dx = 0.0; + double dy = 0.0; + + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FEOFFSET_H_SEEN + +/* + 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 : diff --git a/src/object/filters/pointlight.cpp b/src/object/filters/pointlight.cpp new file mode 100644 index 0000000..68b5eb1 --- /dev/null +++ b/src/object/filters/pointlight.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <fepointlight> implementation. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Jean-Rene Reinhard <jr@komite.net> + * Abhishek Sharma + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "pointlight.h" +#include "diffuselighting.h" +#include "specularlighting.h" + +#include "attributes.h" +#include "document.h" + +#include "xml/node.h" +#include "xml/repr.h" + +SPFePointLight::SPFePointLight() + : x(0) + , x_set(false) + , y(0) + , y_set(false) + , z(0) + , z_set(false) +{ +} + +SPFePointLight::~SPFePointLight() = default; + +void SPFePointLight::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + readAttr(SPAttr::X); + readAttr(SPAttr::Y); + readAttr(SPAttr::Z); + + document->addResource("fepointlight", this); +} + +void SPFePointLight::release() +{ + if (document) { + document->removeResource("fepointlight", this); + } + + SPObject::release(); +} + + +void SPFePointLight::set(SPAttr key, char const *value) +{ + auto read_float = [=] (float &var, float def = 0) -> bool { + if (value) { + char *end_ptr; + auto tmp = g_ascii_strtod(value, &end_ptr); + if (end_ptr) { + var = tmp; + return true; + } + } + var = def; + return false; + }; + + switch (key) { + case SPAttr::X: + x_set = read_float(x); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::Y: + y_set = read_float(y); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::Z: + z_set = read_float(z); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPObject::set(key, value); + break; + } +} + +Inkscape::XML::Node *SPFePointLight::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) { + if (!repr) { + repr = getRepr()->duplicate(doc); + } + + if (x_set) + repr->setAttributeCssDouble("x", x); + if (y_set) + repr->setAttributeCssDouble("y", y); + if (z_set) + repr->setAttributeCssDouble("z", z); + + SPObject::write(doc, repr, flags); + + return repr; +} + +/* + 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 : diff --git a/src/object/filters/pointlight.h b/src/object/filters/pointlight.h new file mode 100644 index 0000000..6edeaed --- /dev/null +++ b/src/object/filters/pointlight.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <filter> implementation, see sp-filter.cpp. + */ +#ifndef SP_FEPOINTLIGHT_H_SEEN +#define SP_FEPOINTLIGHT_H_SEEN + +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-object.h" + +class SPFePointLight final + : public SPObject +{ +public: + SPFePointLight(); + ~SPFePointLight() override; + int tag() const override { return tag_of<decltype(*this)>; } + + /// x coordinate of the light source + float x; + bool x_set : 1; + /// y coordinate of the light source + float y; + bool y_set : 1; + /// z coordinate of the light source + float z; + bool z_set : 1; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const *value) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; +}; + +#endif // SP_FEPOINTLIGHT_H_SEEN + +/* + 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 : diff --git a/src/object/filters/slot-resolver.cpp b/src/object/filters/slot-resolver.cpp new file mode 100644 index 0000000..cc0097b --- /dev/null +++ b/src/object/filters/slot-resolver.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <cstring> +#include <optional> +#include "slot-resolver.h" +#include "display/nr-filter-types.h" + +static auto read_special_name(std::string const &name) -> std::optional<int> +{ + static auto const dict = std::unordered_map<std::string, int>{ + { "SourceGraphic", Inkscape::Filters::NR_FILTER_SOURCEGRAPHIC }, + { "SourceAlpha", Inkscape::Filters::NR_FILTER_SOURCEALPHA }, + { "StrokePaint", Inkscape::Filters::NR_FILTER_STROKEPAINT }, + { "FillPaint", Inkscape::Filters::NR_FILTER_FILLPAINT }, + { "BackgroundImage", Inkscape::Filters::NR_FILTER_BACKGROUNDIMAGE }, + { "BackgroundAlpha", Inkscape::Filters::NR_FILTER_BACKGROUNDALPHA } + }; + + if (auto it = dict.find(name); it != dict.end()) { + return it->second; + } + + return {}; +} + +int SlotResolver::read(std::optional<std::string> const &name) const +{ + return name ? read(*name) : Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +} + +int SlotResolver::read(std::string const &name) const +{ + if (auto ret = read_special_name(name)) { + return *ret; + } + + if (auto it = map.find(name); it != map.end()) { + return it->second; + } + + return Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +} + +int SlotResolver::write(std::optional<std::string> const &name) +{ + return name ? write(*name) : Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +} + +int SlotResolver::write(std::string const &name) +{ + auto [it, ret] = map.try_emplace(name); + + if (ret) { + it->second = next; + next++; + } + + return it->second; +} diff --git a/src/object/filters/slot-resolver.h b/src/object/filters/slot-resolver.h new file mode 100644 index 0000000..b4dc473 --- /dev/null +++ b/src/object/filters/slot-resolver.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SLOTRESOLVER_H +#define SLOTRESOLVER_H +/* + * Author: PBS <pbs3141@gmail.com> + * Copyright (C) 2022 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <string> +#include <unordered_map> +#include <optional> + +class SlotResolver final +{ +public: + int read(std::optional<std::string> const &name) const; + int read(std::string const &name) const; + int write(std::optional<std::string> const &name); + int write(std::string const &name); + +private: + std::unordered_map<std::string, int> map; + int next = 1; +}; + +#endif // SLOTRESOLVER_H diff --git a/src/object/filters/sp-filter-primitive.cpp b/src/object/filters/sp-filter-primitive.cpp new file mode 100644 index 0000000..8d63cb2 --- /dev/null +++ b/src/object/filters/sp-filter-primitive.cpp @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Superclass for all the filter primitives + * + */ +/* + * Authors: + * Kees Cook <kees@outflux.net> + * Niko Kiirala <niko@kiirala.com> + * Abhishek Sharma + * + * Copyright (C) 2004-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> + +#include "sp-filter-primitive.h" +#include "attributes.h" +#include "display/nr-filter-primitive.h" +#include "style.h" +#include "slot-resolver.h" +#include "util/optstr.h" + +SPFilterPrimitive::SPFilterPrimitive() +{ + // We must keep track if a value is set or not, if not set then the region defaults to 0%, 0%, + // 100%, 100% ("x", "y", "width", "height") of the -> filter <- region. If set then + // percentages are in terms of bounding box or viewbox, depending on value of "primitiveUnits" + + // NB: SVGLength.set takes prescaled percent values: 1 means 100% + x.unset(SVGLength::PERCENT, 0, 0); + y.unset(SVGLength::PERCENT, 0, 0); + width.unset(SVGLength::PERCENT, 1, 0); + height.unset(SVGLength::PERCENT, 1, 0); +} + +SPFilterPrimitive::~SPFilterPrimitive() = default; + +void SPFilterPrimitive::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + readAttr(SPAttr::STYLE); // struct not derived from SPItem, we need to do this ourselves. + readAttr(SPAttr::IN_); + readAttr(SPAttr::RESULT); + readAttr(SPAttr::X); + readAttr(SPAttr::Y); + readAttr(SPAttr::WIDTH); + readAttr(SPAttr::HEIGHT); + + SPObject::build(document, repr); +} + +void SPFilterPrimitive::release() +{ + SPObject::release(); +} + +void SPFilterPrimitive::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::IN_: + if (Inkscape::Util::assign(in_name, value)) { + requestModified(SP_OBJECT_MODIFIED_FLAG); + invalidate_parent_slots(); + } + break; + case SPAttr::RESULT: + if (Inkscape::Util::assign(out_name, value)) { + requestModified(SP_OBJECT_MODIFIED_FLAG); + invalidate_parent_slots(); + } + break; + case SPAttr::X: + x.readOrUnset(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::Y: + y.readOrUnset(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::WIDTH: + width.readOrUnset(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::HEIGHT: + height.readOrUnset(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPObject::set(key, value); + break; + } +} + +void SPFilterPrimitive::update(SPCtx *ctx, unsigned flags) +{ + auto ictx = static_cast<SPItemCtx const*>(ctx); + + // Do here since we know viewport (Bounding box case handled during rendering) + if (auto parent_filter = cast<SPFilter>(parent); + parent_filter && + parent_filter->primitiveUnits == SP_FILTER_UNITS_USERSPACEONUSE) + { + calcDimsFromParentViewport(ictx, true); + } +} + +Inkscape::XML::Node *SPFilterPrimitive::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) +{ + if (!repr) { + repr = getRepr()->duplicate(doc); + } + + repr->setAttributeOrRemoveIfEmpty("in", Inkscape::Util::to_cstr(in_name)); + repr->setAttributeOrRemoveIfEmpty("result", Inkscape::Util::to_cstr(out_name)); + + // Do we need to add x, y, width, height? + SPObject::write(doc, repr, flags); + + return repr; +} + +void SPFilterPrimitive::invalidate_parent_slots() +{ + if (auto filter = cast<SPFilter>(parent)) { + filter->invalidate_slots(); + } +} + +void SPFilterPrimitive::resolve_slots(SlotResolver &resolver) +{ + in_slot = resolver.read(in_name); + out_slot = resolver.write(out_name); +} + +// Common initialization for filter primitives +void SPFilterPrimitive::build_renderer_common(Inkscape::Filters::FilterPrimitive *primitive) const +{ + g_assert(primitive); + + primitive->set_input(in_slot); + primitive->set_output(out_slot); + + /* TODO: place here code to handle input images, filter area etc. */ + // We don't know current viewport or bounding box, this is wrong approach. + primitive->set_subregion(x, y, width, height); + + // Give renderer access to filter properties + primitive->setStyle(style); +} + +/* Calculate the region taken up by this filter, given the previous region. + * + * @param current_region The original shape's region or previous primitive's calculate_region output. + */ +Geom::Rect SPFilterPrimitive::calculate_region(Geom::Rect const ®ion) const +{ + return region; // No change. +} + +/* + 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 : diff --git a/src/object/filters/sp-filter-primitive.h b/src/object/filters/sp-filter-primitive.h new file mode 100644 index 0000000..4935170 --- /dev/null +++ b/src/object/filters/sp-filter-primitive.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_FILTER_PRIMITIVE_H +#define SEEN_SP_FILTER_PRIMITIVE_H + +/** \file + * Document level base class for all SVG filter primitives. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <optional> +#include <memory> +#include <string> +#include "2geom/rect.h" +#include "object/sp-object.h" +#include "object/sp-dimensions.h" +#include "display/nr-filter-types.h" + +namespace Inkscape { +class Drawing; +class DrawingItem; +namespace Filters { +class Filter; +class FilterPrimitive; +} // namespace Filters +} // namespace Inkscape + +class SlotResolver; + +class SPFilterPrimitive + : public SPObject + , public SPDimensions +{ +public: + SPFilterPrimitive(); + ~SPFilterPrimitive() override; + int tag() const override { return tag_of<decltype(*this)>; } + + int get_in() const { return in_slot; } + int get_out() const { return out_slot; } + + virtual void show(Inkscape::DrawingItem *item) {} + virtual void hide(Inkscape::DrawingItem *item) {} + + virtual std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const = 0; + + /* Calculate the filter's effect on the region */ + virtual Geom::Rect calculate_region(Geom::Rect const ®ion) const; + + /* Return true if the object should be allowed to use this filter */ + virtual bool valid_for(SPObject const *obj) const + { + // This is used by feImage to stop infinite loops. + return true; + }; + + void invalidate_parent_slots(); + virtual void resolve_slots(SlotResolver &); + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const *value) override; + void update(SPCtx *ctx, unsigned flags) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; + + // Common initialization for filter primitives. + void build_renderer_common(Inkscape::Filters::FilterPrimitive *primitive) const; + +private: + std::optional<std::string> in_name, out_name; + int in_slot = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; + int out_slot = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +}; + +#endif // SEEN_SP_FILTER_PRIMITIVE_H + +/* + 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 : diff --git a/src/object/filters/specularlighting.cpp b/src/object/filters/specularlighting.cpp new file mode 100644 index 0000000..ef6253a --- /dev/null +++ b/src/object/filters/specularlighting.cpp @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feSpecularLighting> implementation. + */ +/* + * Authors: + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Jean-Rene Reinhard <jr@komite.net> + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "specularlighting.h" +#include "distantlight.h" +#include "pointlight.h" +#include "spotlight.h" + +#include "attributes.h" +#include "strneq.h" + +#include "display/nr-filter.h" +#include "display/nr-filter-specularlighting.h" + +#include "object/sp-object.h" + +#include "svg/svg.h" +#include "svg/svg-color.h" + +#include "xml/repr.h" + +void SPFeSpecularLighting::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::SURFACESCALE); + readAttr(SPAttr::SPECULARCONSTANT); + readAttr(SPAttr::SPECULAREXPONENT); + readAttr(SPAttr::KERNELUNITLENGTH); + readAttr(SPAttr::LIGHTING_COLOR); +} + +void SPFeSpecularLighting::set(SPAttr key, char const *value) +{ + // TODO test forbidden values + switch (key) { + case SPAttr::SURFACESCALE: { + char *end_ptr = nullptr; + if (value) { + surfaceScale = g_ascii_strtod(value, &end_ptr); + if (end_ptr) { + surfaceScale_set = true; + } else { + g_warning("this: surfaceScale should be a number ... defaulting to 1"); + } + } + // if the attribute is not set or has an unreadable value + if (!value || !end_ptr) { + surfaceScale = 1; + surfaceScale_set = FALSE; + } + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::SPECULARCONSTANT: { + char *end_ptr = nullptr; + if (value) { + specularConstant = g_ascii_strtod(value, &end_ptr); + if (end_ptr && specularConstant >= 0) { + specularConstant_set = true; + } else { + end_ptr = nullptr; + g_warning("this: specularConstant should be a positive number ... defaulting to 1"); + } + } + if (!value || !end_ptr) { + specularConstant = 1; + specularConstant_set = FALSE; + } + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::SPECULAREXPONENT: { + char *end_ptr = nullptr; + if (value) { + specularExponent = g_ascii_strtod(value, &end_ptr); + if (specularExponent >= 1 && specularExponent <= 128) { + specularExponent_set = true; + } else { + end_ptr = nullptr; + g_warning("this: specularExponent should be a number in range [1, 128] ... defaulting to 1"); + } + } + if (!value || !end_ptr) { + specularExponent = 1; + specularExponent_set = FALSE; + } + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::KERNELUNITLENGTH: + // TODO kernelUnit + // kernelUnitLength.set(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::LIGHTING_COLOR: { + char const *end_ptr = nullptr; + lighting_color = sp_svg_read_color(value, &end_ptr, 0xffffffff); + // if a value was read + if (end_ptr) { + while (g_ascii_isspace(*end_ptr)) { + ++end_ptr; + } + if (strneq(end_ptr, "icc-color(", 10)) { + if (!icc) icc.emplace(); + if (!sp_svg_read_icc_color(end_ptr, &*icc)) { + icc.reset(); + } + } + lighting_color_set = true; + } else { + // lighting_color already contains the default value + lighting_color_set = false; + } + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +void SPFeSpecularLighting::modified(unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + for (auto c : childList(true)) { + if (cflags || (c->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + c->emitModified(cflags); + } + sp_object_unref(c, nullptr); + } +} + +Inkscape::XML::Node *SPFeSpecularLighting::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) +{ + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values _and children_ into it */ + if (!repr) { + repr = getRepr()->duplicate(doc); + //repr = doc->createElement("svg:feSpecularLighting"); + } + + if (surfaceScale_set) { + repr->setAttributeCssDouble("surfaceScale", surfaceScale); + } + + if (specularConstant_set) { + repr->setAttributeCssDouble("specularConstant", specularConstant); + } + + if (specularExponent_set) { + repr->setAttributeCssDouble("specularExponent", specularExponent); + } + + // TODO kernelUnits + if (lighting_color_set) { + char c[64]; + sp_svg_write_color(c, sizeof(c), lighting_color); + repr->setAttribute("lighting-color", c); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeSpecularLighting::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + SPFilterPrimitive::child_added(child, ref); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeSpecularLighting::remove_child(Inkscape::XML::Node *child) +{ + SPFilterPrimitive::remove_child(child); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeSpecularLighting::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) +{ + SPFilterPrimitive::order_changed(child, old_ref, new_ref); + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeSpecularLighting::build_renderer(Inkscape::DrawingItem*) const +{ + auto specularlighting = std::make_unique<Inkscape::Filters::FilterSpecularLighting>(); + build_renderer_common(specularlighting.get()); + + specularlighting->specularConstant = specularConstant; + specularlighting->specularExponent = specularExponent; + specularlighting->surfaceScale = surfaceScale; + specularlighting->lighting_color = lighting_color; + if (icc) { + specularlighting->set_icc(*icc); + } + + // We assume there is at most one child + specularlighting->light_type = Inkscape::Filters::NO_LIGHT; + + if (auto l = cast<SPFeDistantLight>(firstChild())) { + specularlighting->light_type = Inkscape::Filters::DISTANT_LIGHT; + specularlighting->light.distant.azimuth = l->azimuth; + specularlighting->light.distant.elevation = l->elevation; + } else if (auto l = cast<SPFePointLight>(firstChild())) { + specularlighting->light_type = Inkscape::Filters::POINT_LIGHT; + specularlighting->light.point.x = l->x; + specularlighting->light.point.y = l->y; + specularlighting->light.point.z = l->z; + } else if (auto l = cast<SPFeSpotLight>(firstChild())) { + specularlighting->light_type = Inkscape::Filters::SPOT_LIGHT; + specularlighting->light.spot.x = l->x; + specularlighting->light.spot.y = l->y; + specularlighting->light.spot.z = l->z; + specularlighting->light.spot.pointsAtX = l->pointsAtX; + specularlighting->light.spot.pointsAtY = l->pointsAtY; + specularlighting->light.spot.pointsAtZ = l->pointsAtZ; + specularlighting->light.spot.limitingConeAngle = l->limitingConeAngle; + specularlighting->light.spot.specularExponent = l->specularExponent; + } + + return specularlighting; +} + +/* + 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 : diff --git a/src/object/filters/specularlighting.h b/src/object/filters/specularlighting.h new file mode 100644 index 0000000..34d6c36 --- /dev/null +++ b/src/object/filters/specularlighting.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG specular lighting filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2006 Hugo Rodrigues + * 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FESPECULARLIGHTING_H_SEEN +#define SP_FESPECULARLIGHTING_H_SEEN + +#include <optional> +#include <cstdint> +#include "svg/svg-icc-color.h" +#include "sp-filter-primitive.h" +#include "number-opt-number.h" + +struct SVGICCColor; + +namespace Inkscape { +namespace Filters { +class FilterSpecularLighting; +} // namespace Filters +} // namespace Inkscape + +class SPFeSpecularLighting final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + +private: + float surfaceScale = 1.0f; + float specularConstant = 1.0f; + float specularExponent = 1.0f; + uint32_t lighting_color = 0xffffffff; + + bool surfaceScale_set = false; + bool specularConstant_set = false; + bool specularExponent_set = false; + bool lighting_color_set = false; + + NumberOptNumber kernelUnitLength; // TODO + std::optional<SVGICCColor> icc; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + void modified(unsigned flags) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; + + void child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_repr, Inkscape::XML::Node *new_repr) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FESPECULARLIGHTING_H_SEEN + +/* + 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 : diff --git a/src/object/filters/spotlight.cpp b/src/object/filters/spotlight.cpp new file mode 100644 index 0000000..5d80c62 --- /dev/null +++ b/src/object/filters/spotlight.cpp @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <fespotlight> implementation. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Jean-Rene Reinhard <jr@komite.net> + * Abhishek Sharma + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spotlight.h" +#include "diffuselighting.h" +#include "specularlighting.h" + +#include "attributes.h" +#include "document.h" + +#include "xml/repr.h" + +SPFeSpotLight::SPFeSpotLight() + : x(0) + , x_set(false) + , y(0) + , y_set(false) + , z(0) + , z_set(false) + , pointsAtX(0) + , pointsAtX_set(false) + , pointsAtY(0) + , pointsAtY_set(false) + , pointsAtZ(0) + , pointsAtZ_set(false) + , specularExponent(1) + , specularExponent_set(false) + , limitingConeAngle(90) + , limitingConeAngle_set(false) +{ +} + +SPFeSpotLight::~SPFeSpotLight() = default; + +void SPFeSpotLight::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + readAttr(SPAttr::X); + readAttr(SPAttr::Y); + readAttr(SPAttr::Z); + readAttr(SPAttr::POINTSATX); + readAttr(SPAttr::POINTSATY); + readAttr(SPAttr::POINTSATZ); + readAttr(SPAttr::SPECULAREXPONENT); + readAttr(SPAttr::LIMITINGCONEANGLE); + + document->addResource("fespotlight", this); +} + +void SPFeSpotLight::release() { + if (document) { + document->removeResource("fespotlight", this); + } + + SPObject::release(); +} + +void SPFeSpotLight::set(SPAttr key, char const *value) +{ + auto read_float = [=] (float &var, float def = 0) -> bool { + if (value) { + char *end_ptr; + auto tmp = g_ascii_strtod(value, &end_ptr); + if (end_ptr) { + var = tmp; + return true; + } + } + var = def; + return false; + }; + + switch (key) { + case SPAttr::X: + x_set = read_float(x); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::Y: + y_set = read_float(y); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::Z: + z_set = read_float(z); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::POINTSATX: + pointsAtX_set = read_float(pointsAtX); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::POINTSATY: + pointsAtY_set = read_float(pointsAtY); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::POINTSATZ: + pointsAtZ_set = read_float(pointsAtZ); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::SPECULAREXPONENT: + specularExponent_set = read_float(specularExponent, 1); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::LIMITINGCONEANGLE: + limitingConeAngle_set = read_float(limitingConeAngle, 90); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPObject::set(key, value); + break; + } +} + +Inkscape::XML::Node *SPFeSpotLight::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) { + if (!repr) { + repr = getRepr()->duplicate(doc); + } + + if (x_set) + repr->setAttributeCssDouble("x", x); + if (y_set) + repr->setAttributeCssDouble("y", y); + if (z_set) + repr->setAttributeCssDouble("z", z); + if (pointsAtX_set) + repr->setAttributeCssDouble("pointsAtX", pointsAtX); + if (pointsAtY_set) + repr->setAttributeCssDouble("pointsAtY", pointsAtY); + if (pointsAtZ_set) + repr->setAttributeCssDouble("pointsAtZ", pointsAtZ); + if (specularExponent_set) + repr->setAttributeCssDouble("specularExponent", specularExponent); + if (limitingConeAngle_set) + repr->setAttributeCssDouble("limitingConeAngle", limitingConeAngle); + + SPObject::write(doc, repr, flags); + + return repr; +} + +/* + 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 : diff --git a/src/object/filters/spotlight.h b/src/object/filters/spotlight.h new file mode 100644 index 0000000..23136b2 --- /dev/null +++ b/src/object/filters/spotlight.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_FESPOTLIGHT_H_SEEN +#define SP_FESPOTLIGHT_H_SEEN + +/** \file + * SVG <filter> implementation, see sp-filter.cpp. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-object.h" + +class SPFeSpotLight final + : public SPObject +{ +public: + SPFeSpotLight(); + ~SPFeSpotLight() override; + int tag() const override { return tag_of<decltype(*this)>; } + + /// x coordinate of the light source + float x; + bool x_set : 1; + /// y coordinate of the light source + float y; + bool y_set : 1; + /// z coordinate of the light source + float z; + bool z_set : 1; + /// x coordinate of the point the source is pointing at + float pointsAtX; + bool pointsAtX_set : 1; + /// y coordinate of the point the source is pointing at + float pointsAtY; + bool pointsAtY_set : 1; + /// z coordinate of the point the source is pointing at + float pointsAtZ; + bool pointsAtZ_set : 1; + /// specular exponent (focus of the light) + float specularExponent; + bool specularExponent_set : 1; + /// limiting cone angle + float limitingConeAngle; + bool limitingConeAngle_set : 1; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const *value) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; +}; + +#endif // SP_FESPOTLIGHT_H_SEEN + +/* + 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 : diff --git a/src/object/filters/tile.cpp b/src/object/filters/tile.cpp new file mode 100644 index 0000000..e0af9b8 --- /dev/null +++ b/src/object/filters/tile.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feTile> implementation. + */ +/* + * Authors: + * hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tile.h" +#include "attributes.h" +#include "display/nr-filter.h" +#include "display/nr-filter-tile.h" +#include "svg/svg.h" +#include "xml/repr.h" + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeTile::build_renderer(Inkscape::DrawingItem*) const +{ + auto tile = std::make_unique<Inkscape::Filters::FilterTile>(); + build_renderer_common(tile.get()); + return tile; +} + +/* + 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 : diff --git a/src/object/filters/tile.h b/src/object/filters/tile.h new file mode 100644 index 0000000..80b9f7e --- /dev/null +++ b/src/object/filters/tile.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG tile filter effect + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FETILE_H_SEEN +#define SP_FETILE_H_SEEN + +#include "sp-filter-primitive.h" + +class SPFeTile final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FETILE_H_SEEN + +/* + 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 : diff --git a/src/object/filters/turbulence.cpp b/src/object/filters/turbulence.cpp new file mode 100644 index 0000000..6f21eac --- /dev/null +++ b/src/object/filters/turbulence.cpp @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <feTurbulence> implementation. + */ +/* + * Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * hugo Rodrigues <haa.rodrigues@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2007 Felipe Sanches + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "svg/svg.h" +#include "turbulence.h" +#include "util/numeric/converters.h" +#include "xml/repr.h" +#include "display/nr-filter.h" + +void SPFeTurbulence::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + readAttr(SPAttr::BASEFREQUENCY); + readAttr(SPAttr::NUMOCTAVES); + readAttr(SPAttr::SEED); + readAttr(SPAttr::STITCHTILES); + readAttr(SPAttr::TYPE); +} + +static bool read_stitchtiles(char const *value) +{ + if (!value) { + return false; // 'noStitch' is default + } + + switch (value[0]) { + case 's': + if (std::strcmp(value, "stitch") == 0) { + return true; + } + break; + case 'n': + if (std::strcmp(value, "noStitch") == 0) { + return false; + } + break; + } + + return false; // 'noStitch' is default +} + +static Inkscape::Filters::FilterTurbulenceType read_type(char const *value) +{ + if (!value) { + return Inkscape::Filters::TURBULENCE_TURBULENCE; // 'turbulence' is default + } + + switch (value[0]) { + case 'f': + if (std::strcmp(value, "fractalNoise") == 0) { + return Inkscape::Filters::TURBULENCE_FRACTALNOISE; + } + break; + case 't': + if (std::strcmp(value, "turbulence") == 0) { + return Inkscape::Filters::TURBULENCE_TURBULENCE; + } + break; + } + + return Inkscape::Filters::TURBULENCE_TURBULENCE; // 'turbulence' is default +} + +void SPFeTurbulence::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::BASEFREQUENCY: + baseFrequency.set(value); + + // From SVG spec: If two <number>s are provided, the first number represents + // a base frequency in the X direction and the second value represents a base + // frequency in the Y direction. If one number is provided, then that value is + // used for both X and Y. + if (baseFrequency.optNumIsSet() == false) { + baseFrequency.setOptNumber(baseFrequency.getNumber()); + } + + updated = false; + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::NUMOCTAVES: { + int n_int = value ? (int)std::floor(Inkscape::Util::read_number(value)) : 1; + if (n_int != numOctaves){ + numOctaves = n_int; + updated = false; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::SEED: { + double n_num = value ? Inkscape::Util::read_number(value) : 0; + if (n_num != seed){ + seed = n_num; + updated = false; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::STITCHTILES: { + bool n_bool = ::read_stitchtiles(value); + if (n_bool != stitchTiles){ + stitchTiles = n_bool; + updated = false; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::TYPE: { + auto n_type = ::read_type(value); + if (n_type != type) { + type = n_type; + updated = false; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +Inkscape::XML::Node *SPFeTurbulence::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) +{ + // TODO: Don't just clone, but create a new repr node and write all relevant values into it. + if (!repr) { + repr = getRepr()->duplicate(doc); + } + + SPFilterPrimitive::write(doc, repr, flags); + + // turbulence doesn't take input + repr->removeAttribute("in"); + + return repr; +} + +std::unique_ptr<Inkscape::Filters::FilterPrimitive> SPFeTurbulence::build_renderer(Inkscape::DrawingItem*) const +{ + auto turbulence = std::make_unique<Inkscape::Filters::FilterTurbulence>(); + build_renderer_common(turbulence.get()); + + turbulence->set_baseFrequency(0, baseFrequency.getNumber()); + turbulence->set_baseFrequency(1, baseFrequency.getOptNumber()); + turbulence->set_numOctaves(numOctaves); + turbulence->set_seed(seed); + turbulence->set_stitchTiles(stitchTiles); + turbulence->set_type(type); + turbulence->set_updated(updated); + + return turbulence; +} + +/* + 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 : diff --git a/src/object/filters/turbulence.h b/src/object/filters/turbulence.h new file mode 100644 index 0000000..5680602 --- /dev/null +++ b/src/object/filters/turbulence.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG turbulence filter effect + *//* + * Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FETURBULENCE_H_SEEN +#define SP_FETURBULENCE_H_SEEN + +#include "sp-filter-primitive.h" +#include "number-opt-number.h" +#include "display/nr-filter-turbulence.h" + +class SPFeTurbulence final + : public SPFilterPrimitive +{ +public: + int tag() const override { return tag_of<decltype(*this)>; } + +private: + int numOctaves = 0; + double seed = 0.0f; + bool stitchTiles = false; + Inkscape::Filters::FilterTurbulenceType type = Inkscape::Filters::TURBULENCE_FRACTALNOISE; + bool updated = false; + + NumberOptNumber baseFrequency; + SVGLength x, y, height, width; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; + + std::unique_ptr<Inkscape::Filters::FilterPrimitive> build_renderer(Inkscape::DrawingItem *item) const override; +}; + +#endif // SP_FETURBULENCE_H_SEEN + +/* + 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 : diff --git a/src/object/object-set.cpp b/src/object/object-set.cpp new file mode 100644 index 0000000..394413a --- /dev/null +++ b/src/object/object-set.cpp @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Multiindex container for selection + * + * Authors: + * Adrian Boguszewski + * + * Copyright (C) 2016 Adrian Boguszewski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object-set.h" + +#include <boost/range/adaptor/filtered.hpp> +#include <boost/range/adaptor/transformed.hpp> +#include <glib.h> +#include <sigc++/sigc++.h> + +#include "box3d.h" +#include "persp3d.h" +#include "preferences.h" + +namespace Inkscape { + +bool ObjectSet::add(SPObject* object, bool nosignal) { + g_return_val_if_fail(object != nullptr, false); + + // any ancestor is in the set - do nothing + if (_anyAncestorIsInSet(object)) { + return false; + } + + // very nice function, but changes selection behavior (probably needs new selection option to deal with it) + // check if there is mutual ancestor for some elements, which can replace all of them in the set +// object = _getMutualAncestor(object); + + // remove all descendants from the set + _removeDescendantsFromSet(object); + + _add(object); + if (!nosignal) + _emitChanged(); + return true; +} + +void ObjectSet::add(XML::Node *repr) +{ + if (document() && repr) { + SPObject *obj = document()->getObjectByRepr(repr); + assert(obj == document()->getObjectById(repr->attribute("id"))); + add(obj); + } +} + +bool ObjectSet::remove(SPObject* object) { + g_return_val_if_fail(object != nullptr, false); + + // object is the top of subtree + if (includes(object)) { + _remove(object); + _emitChanged(); + return true; + } + + // any ancestor of object is in the set + if (_anyAncestorIsInSet(object)) { + _removeAncestorsFromSet(object); + _emitChanged(); + return true; + } + + // no object nor any parent in the set + return false; +} + +void ObjectSet::_emitChanged(bool persist_selection_context /*= false*/) { + _sibling_state.clear(); +} + +bool ObjectSet::includes(SPObject *object, bool anyAncestor) { + g_return_val_if_fail(object != nullptr, false); + if (anyAncestor) { + return _anyAncestorIsInSet(object); + } else { + return _container.get<hashed>().find(object) != _container.get<hashed>().end(); + } +} + +bool ObjectSet::includes(Inkscape::XML::Node *node, bool anyAncestor) +{ + if (node) { + return includes(document()->getObjectByRepr(node), anyAncestor); + } + return false; +} + +SPObject * +ObjectSet::includesAncestor(SPObject *object) { + g_return_val_if_fail(object != nullptr, nullptr); + SPObject* o = object; + while (o != nullptr) { + if (includes(o)) { + return o; + } + o = o->parent; + } + return nullptr; +} + +void ObjectSet::clear() { + _clear(); + _emitChanged(); +} + +int ObjectSet::size() { + return _container.size(); +} + +bool ObjectSet::_anyAncestorIsInSet(SPObject *object) { + SPObject* o = object; + while (o != nullptr) { + if (includes(o)) { + return true; + } + o = o->parent; + } + + return false; +} + +void ObjectSet::_removeDescendantsFromSet(SPObject *object) { + for (auto& child: object->children) { + if (includes(&child)) { + _remove(&child); + // there is certainly no children of this child in the set + continue; + } + + _removeDescendantsFromSet(&child); + } +} + +void ObjectSet::_disconnect(SPObject *object) { + _releaseConnections[object].disconnect(); + _releaseConnections.erase(object); + _remove3DBoxesRecursively(object); + _releaseSignals(object); +} + +void ObjectSet::_remove(SPObject *object) { + _disconnect(object); + _container.get<hashed>().erase(object); +} + +void ObjectSet::_add(SPObject *object) { + _releaseConnections[object] = object->connectRelease(sigc::hide_return(sigc::mem_fun(*this, &ObjectSet::remove))); + _container.push_back(object); + _add3DBoxesRecursively(object); + _connectSignals(object); +} + +void ObjectSet::_clear() { + for (auto object: _container) + _disconnect(object); + _container.clear(); +} + +SPObject *ObjectSet::_getMutualAncestor(SPObject *object) { + SPObject *o = object; + + bool flag = true; + while (o->parent != nullptr) { + for (auto &child: o->parent->children) { + if(&child != o && !includes(&child)) { + flag = false; + break; + } + } + if (!flag) { + break; + } + o = o->parent; + } + return o; +} + +void ObjectSet::_removeAncestorsFromSet(SPObject *object) { + SPObject* o = object; + while (o->parent != nullptr) { + for (auto &child: o->parent->children) { + if (&child != o) { + _add(&child); + } + } + if (includes(o->parent)) { + _remove(o->parent); + break; + } + o = o->parent; + } +} + +ObjectSet::~ObjectSet() { + _clear(); +} + +void ObjectSet::toggle(SPObject *obj) { + if (includes(obj)) { + remove(obj); + } else { + add(obj); + } +} + +bool ObjectSet::isEmpty() { + return _container.size() == 0; +} + +SPObject *ObjectSet::single() { + return _container.size() == 1 ? *_container.begin() : nullptr; +} + +SPItem *ObjectSet::singleItem() { + if (_container.size() == 1) { + SPObject* obj = *_container.begin(); + if (is<SPItem>(obj)) { + return cast<SPItem>(obj); + } + } + + return nullptr; +} + +SPItem *ObjectSet::firstItem() const +{ + return _container.size() ? cast<SPItem>(_container.front()) : nullptr; +} + +SPItem *ObjectSet::lastItem() const +{ + return _container.size() ? cast<SPItem>(_container.back()) : nullptr; +} + +SPItem *ObjectSet::smallestItem(CompareSize compare) { + return _sizeistItem(true, compare); +} + +SPItem *ObjectSet::largestItem(CompareSize compare) { + return _sizeistItem(false, compare); +} + +SPItem *ObjectSet::_sizeistItem(bool sml, CompareSize compare) { + auto items = this->items(); + gdouble max = sml ? 1e18 : 0; + SPItem *ist = nullptr; + + for (auto *item : items) { + Geom::OptRect obox = item->documentPreferredBounds(); + if (!obox || obox.empty()) { + continue; + } + + Geom::Rect bbox = *obox; + + gdouble size = compare == AREA ? bbox.area() : + (compare == VERTICAL ? bbox.height() : bbox.width()); + size = sml ? size : size * -1; + if (size < max) { + max = size; + ist = item; + } + } + + return ist; +} + +SPObjectRange ObjectSet::objects() { + return SPObjectRange(_container.get<random_access>().begin(), _container.get<random_access>().end()); +} + +Inkscape::XML::Node *ObjectSet::singleRepr() { + SPObject *obj = single(); + return obj ? obj->getRepr() : nullptr; +} + +Inkscape::XML::Node *ObjectSet::topRepr() const +{ + auto const &nodes = const_cast<ObjectSet *>(this)->xmlNodes(); + + if (nodes.empty()) { + return nullptr; + } + +#ifdef _LIBCPP_VERSION + // workaround for + // static_assert(__is_cpp17_forward_iterator<_ForwardIterator>::value + auto const n = std::vector<Inkscape::XML::Node *>(nodes.begin(), nodes.end()); +#else + auto const& n = nodes; +#endif + + return *std::max_element(n.begin(), n.end(), sp_repr_compare_position_bool); +} + +void ObjectSet::set(SPObject *object, bool persist_selection_context) { + _clear(); + _add(object); + _emitChanged(persist_selection_context); +} + +void ObjectSet::set(XML::Node *repr) +{ + if (document() && repr) { + SPObject *obj = document()->getObjectByRepr(repr); + assert(obj == document()->getObjectById(repr->attribute("id"))); + set(obj); + } +} + +int ObjectSet::setBetween(SPObject *obj_a, SPObject *obj_b) +{ + auto parent = obj_a->parent; + if (!obj_b) + obj_b = lastItem(); + + if (!obj_a || !obj_b || parent != obj_b->parent) { + return 0; + } else if (obj_a == obj_b) { + set(obj_a); + return 1; + } + clear(); + + int count = 0; + int min = std::min(obj_a->getPosition(), obj_b->getPosition()); + int max = std::max(obj_a->getPosition(), obj_b->getPosition()); + for (int i = min ; i <= max ; i++) { + if (auto child = parent->nthChild(i)) { + count += add(child); + } + } + return count; +} + + +void ObjectSet::setReprList(std::vector<XML::Node*> const &list) { + if(!document()) + return; + clear(); + for (auto iter = list.rbegin(); iter != list.rend(); ++iter) { +#if 0 + // This can fail when pasting a clone into a new document + SPObject *obj = document()->getObjectByRepr(*iter); + assert(obj == document()->getObjectById((*iter)->attribute("id"))); +#else + SPObject *obj = document()->getObjectById((*iter)->attribute("id")); +#endif + if (obj) { + add(obj, true); + } + } + _emitChanged(); +} + +void ObjectSet::enforceIds() +{ + bool idAssigned = false; + auto items = this->items(); + for (auto *item : items) { + if (!item->getId()) { + // Selected object does not have an ID, so assign it a unique ID + auto id = item->generate_unique_id(); + item->setAttribute("id", id); + idAssigned = true; + } + } + if (idAssigned) { + SPDocument *document = _desktop->getDocument(); + if (document) { + document->setModifiedSinceSave(true); + } + } +} + +Geom::OptRect ObjectSet::bounds(SPItem::BBoxType type) const +{ + return (type == SPItem::GEOMETRIC_BBOX) ? + geometricBounds() : visualBounds(); +} + +Geom::OptRect ObjectSet::geometricBounds() const +{ + auto items = const_cast<ObjectSet *>(this)->items(); + + Geom::OptRect bbox; + for (auto *item : items) { + bbox.unionWith(item->desktopGeometricBounds()); + } + return bbox; +} + +Geom::OptRect ObjectSet::visualBounds() const +{ + auto items = const_cast<ObjectSet *>(this)->items(); + + Geom::OptRect bbox; + for (auto *item : items) { + bbox.unionWith(item->desktopVisualBounds()); + } + return bbox; +} + +Geom::OptRect ObjectSet::strokedBounds() const +{ + auto items = const_cast<ObjectSet *>(this)->items(); + + Geom::OptRect bbox; + for (auto *item : items) { + bbox.unionWith(item->visualBounds(item->i2doc_affine(), false, true, true)); + } + if (bbox) { + *bbox *= _desktop->getDocument()->doc2dt(); + } + return bbox; +} + +Geom::OptRect ObjectSet::preferredBounds() const +{ + if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) { + return bounds(SPItem::VISUAL_BBOX); + } else { + return bounds(SPItem::GEOMETRIC_BBOX); + } +} + +Geom::OptRect ObjectSet::documentBounds(SPItem::BBoxType type) const +{ + Geom::OptRect bbox; + auto items = const_cast<ObjectSet *>(this)->items(); + if (items.empty()) return bbox; + + for (auto *item : items) { + bbox |= item->documentBounds(type); + } + + return bbox; +} + +// If we have a selection of multiple items, then the center of the first item +// will be returned; this is also the case in SelTrans::centerRequest() +std::optional<Geom::Point> ObjectSet::center() const { + auto items = const_cast<ObjectSet *>(this)->items(); + if (!items.empty()) { + SPItem *first = items.back(); // from the first item in selection + if (first->isCenterSet()) { // only if set explicitly + return first->getCenter(); + } + } + Geom::OptRect bbox = preferredBounds(); + if (bbox) { + return bbox->midpoint(); + } else { + return std::optional<Geom::Point>(); + } +} + +std::list<Persp3D *> const ObjectSet::perspList() { + std::list<Persp3D *> pl; + for (auto & _3dboxe : _3dboxes) { + Persp3D *persp = _3dboxe->get_perspective(); + if (std::find(pl.begin(), pl.end(), persp) == pl.end()) + pl.push_back(persp); + } + return pl; +} + +std::list<SPBox3D *> const ObjectSet::box3DList(Persp3D *persp) { + std::list<SPBox3D *> boxes; + if (persp) { + for (auto box : _3dboxes) { + if (persp == box->get_perspective()) { + boxes.push_back(box); + } + } + } else { + boxes = _3dboxes; + } + return boxes; +} + +void ObjectSet::_add3DBoxesRecursively(SPObject *obj) { + std::list<SPBox3D *> boxes = SPBox3D::extract_boxes(obj); + + for (auto box : boxes) { + _3dboxes.push_back(box); + } +} + +void ObjectSet::_remove3DBoxesRecursively(SPObject *obj) { + std::list<SPBox3D *> boxes = SPBox3D::extract_boxes(obj); + + for (auto box : boxes) { + std::list<SPBox3D *>::iterator b = std::find(_3dboxes.begin(), _3dboxes.end(), box); + if (b == _3dboxes.end()) { + g_warning ("Warning! Trying to remove unselected box from selection."); + return; + } + _3dboxes.erase(b); + } +} + +} // namespace Inkscape + +/* + 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 : diff --git a/src/object/object-set.h b/src/object/object-set.h new file mode 100644 index 0000000..875877f --- /dev/null +++ b/src/object/object-set.h @@ -0,0 +1,577 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Multiindex container for selection + * + * Authors: + * Adrian Boguszewski + * Marc Jeanmougin + * + * Copyright (C) 2016 Adrian Boguszewski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_PROTOTYPE_OBJECTSET_H +#define INKSCAPE_PROTOTYPE_OBJECTSET_H + +#include <string> +#include <unordered_map> + +#include <boost/multi_index_container.hpp> +#include <boost/multi_index/identity.hpp> +#include <boost/multi_index/sequenced_index.hpp> +#include <boost/multi_index/hashed_index.hpp> +#include <boost/multi_index/random_access_index.hpp> +#include <boost/range/adaptor/filtered.hpp> +#include <boost/range/adaptor/transformed.hpp> +#include <boost/range/sub_range.hpp> +#include <boost/range/any_range.hpp> +#include <boost/type_traits.hpp> +#include <boost/utility/enable_if.hpp> + +#include <sigc++/connection.h> + +#include <inkgc/gc-soft-ptr.h> + +#include "sp-object.h" +#include "sp-item.h" +#include "sp-item-group.h" +#include "desktop.h" +#include "document.h" + +enum BoolOpErrors { + DONE, + DONE_NO_PATH, + DONE_NO_ACTION, + ERR_TOO_LESS_PATHS_1, + ERR_TOO_LESS_PATHS_2, + ERR_NO_PATHS, + ERR_Z_ORDER +}; + +// boolean operation +enum bool_op +{ + bool_op_union, // A OR B + bool_op_inters, // A AND B + bool_op_diff, // A \ B + bool_op_symdiff, // A XOR B + bool_op_cut, // coupure (pleines) + bool_op_slice // coupure (contour) +}; +typedef enum bool_op BooleanOp; + +/** + * SiblingState enums are used to associate the current state + * while grabbing objects. + * Specifically used by ObjectSet.applyAffine() to manage transforms + * while dragging objects + */ +enum class SiblingState { + SIBLING_NONE, // no relation to item + SIBLING_CLONE_ORIGINAL, // moving both a clone and its original or any ancestor + SIBLING_OFFSET_SOURCE, // moving both an offset and its source + SIBLING_TEXT_PATH, // moving both a text-on-path and its path + SIBLING_TEXT_FLOW_FRAME, // moving both a flowtext and its frame + SIBLING_TEXT_SHAPE_INSIDE, // moving object containing sub object +}; + +class SPBox3D; +class Persp3D; + +namespace Inkscape { + +namespace XML { +class Node; +} + +struct hashed{}; +struct random_access{}; + +struct is_item { + bool operator()(SPObject* obj) { + return is<SPItem>(obj); + } +}; + +struct is_group { + bool operator()(SPObject* obj) { + return is<SPGroup>(obj); + } +}; + +struct object_to_item { + typedef SPItem* result_type; + SPItem* operator()(SPObject* obj) const { + return cast<SPItem>(obj); + } +}; + +struct object_to_node { + typedef XML::Node* result_type; + XML::Node* operator()(SPObject* obj) const { + return obj->getRepr(); + } +}; + +struct object_to_group { + typedef SPGroup* result_type; + SPGroup* operator()(SPObject* obj) const { + return cast<SPGroup>(obj); + } +}; + +typedef boost::multi_index_container< + SPObject*, + boost::multi_index::indexed_by< + boost::multi_index::sequenced<>, + boost::multi_index::random_access< + boost::multi_index::tag<random_access>>, + boost::multi_index::hashed_unique< + boost::multi_index::tag<hashed>, + boost::multi_index::identity<SPObject*>> + >> MultiIndexContainer; + +typedef boost::any_range< + SPObject*, + boost::random_access_traversal_tag, + SPObject* const&, + std::ptrdiff_t> SPObjectRange; + +class ObjectSet { +public: + enum CompareSize {HORIZONTAL, VERTICAL, AREA}; + typedef decltype(MultiIndexContainer().get<random_access>() | boost::adaptors::filtered(is_item()) | boost::adaptors::transformed(object_to_item())) SPItemRange; + typedef decltype(MultiIndexContainer().get<random_access>() | boost::adaptors::filtered(is_group()) | boost::adaptors::transformed(object_to_group())) SPGroupRange; + typedef decltype(MultiIndexContainer().get<random_access>() | boost::adaptors::filtered(is_item()) | boost::adaptors::transformed(object_to_node())) XMLNodeRange; + + ObjectSet(SPDesktop* desktop): _desktop(desktop) { + if (desktop) + _document = desktop->getDocument(); + }; + ObjectSet(SPDocument* doc): _desktop(nullptr), _document(doc) {}; + ObjectSet(): _desktop(nullptr), _document(nullptr) {}; // Used in spray-tool.h. + virtual ~ObjectSet(); + + void setDocument(SPDocument* doc){ + _document = doc; + } + + + /** + * Add an SPObject to the set of selected objects. + * + * @param obj the SPObject to add + * @param nosignal true if no signals should be sent + */ + bool add(SPObject* object, bool nosignal = false); + + /** + * Add an XML node's SPObject to the set of selected objects. + * + * @param the xml node of the item to add + */ + void add(XML::Node *repr); + + /** Add items from an STL iterator range to the selection. + * \param from the begin iterator + * \param to the end iterator + */ + template <typename InputIterator> + void add(InputIterator from, InputIterator to) { + for(auto it = from; it != to; ++it) { + _add(*it); + } + _emitChanged(); + } + + /** + * Removes an item from the set of selected objects. + * + * It is ok to call this method for an unselected item. + * + * @param item the item to unselect + * + * @return is success + */ + bool remove(SPObject* object); + + /** + * Returns true if the given object is selected. + */ + bool includes(SPObject *object, bool anyAncestor = false); + bool includes(Inkscape::XML::Node *node, bool anyAncestor = false); + + /** + * Returns ancestor if the given object has ancestor selected. + */ + SPObject * includesAncestor(SPObject *object); + + /** + * Set the selection to a single specific object. + * + * @param obj the object to select + */ + void set(SPObject *object, bool persist_selection_context = false); + void set(XML::Node *repr); + /** + * Unselects all selected objects. + */ + void clear(); + + /** + * Returns size of the selection. + */ + int size(); + + /** + * Returns true if no items are selected. + */ + bool isEmpty(); + + /** + * Removes an item if selected, adds otherwise. + * + * @param item the item to unselect + */ + void toggle(SPObject *obj); + + /** + * Returns a single selected object. + * + * @return NULL unless exactly one object is selected + */ + SPObject *single(); + + /** + * Returns a single selected item. + * + * @return NULL unless exactly one object is selected + */ + SPItem *singleItem(); + + /** + * Returns the first selected item, returns nullptr if no items selected. + */ + SPItem *firstItem() const; + + /** + * Returns the last selected item, returns nullptr if no items selected. + */ + SPItem *lastItem() const; + + /** + * Returns the smallest item from this selection. + */ + SPItem *smallestItem(CompareSize compare); + + /** + * Returns the largest item from this selection. + */ + SPItem *largestItem(CompareSize compare); + + /** Returns the list of selected objects. */ + SPObjectRange objects(); + + /** Returns a range of selected SPItems. */ + SPItemRange items() { + return SPItemRange(_container.get<random_access>() + | boost::adaptors::filtered(is_item()) + | boost::adaptors::transformed(object_to_item())); + }; + + std::vector<SPItem*> items_vector() { + auto i = items(); + return {i.begin(), i.end()}; + } + + /** Returns a range of selected groups. */ + SPGroupRange groups() { + return SPGroupRange (_container.get<random_access>() + | boost::adaptors::filtered(is_group()) + | boost::adaptors::transformed(object_to_group())); + } + + /** Returns a range of the xml nodes of all selected objects. */ + XMLNodeRange xmlNodes() { + return XMLNodeRange(_container.get<random_access>() + | boost::adaptors::filtered(is_item()) + | boost::adaptors::transformed(object_to_node())); + } + + /** + * Returns a single selected object's xml node. + * + * @return NULL unless exactly one object is selected + */ + XML::Node *singleRepr(); + + /** + * The top-most item, or NULL if the selection is empty. + */ + XML::Node *topRepr() const; + + /** + * Selects exactly the specified objects. + * + * @param objs the objects to select + */ + template <class T> + typename boost::enable_if<boost::is_base_of<SPObject, T>, void>::type + setList(const std::vector<T*> &objs) { + _clear(); + addList(objs); + } + + /** + * Attempt to select all the items between two child items. Must have the same parent. + * + * @param obj_a - The first item, doesn't have to appear first in the list. + * @param obj_b - The last item, doesn't have to appear last in the list (optional) + * If selection already contains one item, will select from-to that. + * If not set, will use the lastItem selected in the list. + * + * @returns the number of items added. + */ + int setBetween(SPObject *obj_a, SPObject *obj_b = nullptr); + + /** + * Selects the objects with the same IDs as those in `list`. + * + * @todo How about adding `setIdList(std::vector<Glib::ustring> const &list)` + * + * @param list the repr list to add + */ + void setReprList(std::vector<XML::Node*> const &list); + + /** + * Assign IDs to selected objects that don't have an ID attribute + * Checks if the object's id attribute is NULL. If it is, assign it a unique ID + */ + void enforceIds(); + + /** + * Adds the specified objects to selection, without deselecting first. + * + * @param objs the objects to select + */ + template <class T> + typename boost::enable_if<boost::is_base_of<SPObject, T>, void>::type + addList(const std::vector<T*> &objs) { + for (auto obj: objs) { + if (!includes(obj)) { + add(obj, true); + } + } + _emitChanged(); + } + + /** Returns the bounding rectangle of the selection. */ + Geom::OptRect bounds(SPItem::BBoxType type) const; + Geom::OptRect visualBounds() const; + Geom::OptRect geometricBounds() const; + Geom::OptRect strokedBounds() const; + + /** + * Returns either the visual or geometric bounding rectangle of the selection, based on the + * preferences specified for the selector tool + */ + Geom::OptRect preferredBounds() const; + + /* Returns the bounding rectangle of the selectionin document coordinates.*/ + Geom::OptRect documentBounds(SPItem::BBoxType type) const; + + /** + * Returns the rotation/skew center of the selection. + */ + std::optional<Geom::Point> center() const; + + /** Returns a list of all perspectives which have a 3D box in the current selection. + (these may also be nested in groups) */ + std::list<Persp3D *> const perspList(); + + /** + * Returns a list of all 3D boxes in the current selection which are associated to @c + * persp. If @c pers is @c NULL, return all selected boxes. + */ + std::list<SPBox3D *> const box3DList(Persp3D *persp = nullptr); + + /** + * Returns the desktop the selection is bound to + * + * @return the desktop the selection is bound to, or NULL if in console mode + */ + SPDesktop *desktop() { return _desktop; } + + /** + * Returns the document the selection is bound to + * + * @return the document the selection is bound to, or NULL if in console mode + */ + SPDocument *document() { return _document; } + + //item groups operations + //in selection-chemistry.cpp + void deleteItems(bool skip_undo = false); + void duplicate(bool suppressDone = false, bool duplicateLayer = false); + void clone(); + + /** + * @brief Unlink all directly selected clones. + * @param skip_undo If this is set to true the call to DocumentUndo::done is omitted. + * @return True if anything was unlinked, otherwise false. + */ + bool unlink(const bool skip_undo = false, const bool silent = false); + /** + * @brief Recursively unlink any clones present in the current selection, + * including clones which are used to clip other objects, groups of clones etc. + * @return true if anything was unlinked, otherwise false. + */ + bool unlinkRecursive(const bool skip_undo = false, const bool force = false, const bool silent = false); + void removeLPESRecursive(bool keep_paths); + void relink(); + void cloneOriginal(); + void cloneOriginalPathLPE(bool allow_transforms = false, bool sync = false, bool skip_undo = false); + Inkscape::XML::Node* group(bool is_anchor = false); + void popFromGroup(); + void ungroup(bool skip_undo = false); + void ungroup_all(bool skip_undo = false); + + //z-order management + //in selection-chemistry.cpp + void stackUp(bool skip_undo = false); + void raise(bool skip_undo = false); + void raiseToTop(bool skip_undo = false); + void stackDown(bool skip_undo = false); + void lower(bool skip_undo = false); + void lowerToBottom(bool skip_undo = false); + void toNextLayer(bool skip_undo = false); + void toPrevLayer(bool skip_undo = false); + void toLayer(SPObject *layer); + void toLayer(SPObject *layer, Inkscape::XML::Node *after); + + //clipboard management + //in selection-chemistry.cpp + void copy(); + void cut(); + void pasteStyle(); + void pasteSize(bool apply_x, bool apply_y); + void pasteSizeSeparately(bool apply_x, bool apply_y); + void pastePathEffect(); + + //path operations + //in path-chemistry.cpp + void combine(bool skip_undo = false, bool silent = false); + void breakApart(bool skip_undo = false, bool overlapping = true, bool silent = false); + void toCurves(bool skip_undo = false, bool clonesjustunlink = false); + void toLPEItems(); + void pathReverse(); + + // path operations + // in path/path-object-set.cpp + bool strokesToPaths(bool legacy = false, bool skip_undo = false); + bool simplifyPaths(bool skip_undo = false); + + // Boolean operations + // in splivarot.cpp + bool pathUnion(const bool skip_undo = false, bool silent = false); + bool pathIntersect(const bool skip_undo = false, bool silent = false); + bool pathDiff(const bool skip_undo = false, bool silent = false); + bool pathSymDiff(const bool skip_undo = false, bool silent = false); + bool pathCut(const bool skip_undo = false, bool silent = false); + bool pathSlice(const bool skip_undo = false, bool silent = false); + + //Other path operations + //in selection-chemistry.cpp + void toMarker(bool apply = true); + void toGuides(); + void toSymbol(); + void unSymbol(); + void tile(bool apply = true); //"Object to Pattern" + void untile(); + void createBitmapCopy(); + void setMask(bool apply_clip_path, bool apply_to_layer, bool remove_original); + void editMask(bool clip); + void unsetMask(const bool apply_clip_path, const bool delete_helper_group, bool remove_original); + void setClipGroup(); + + // moves + // in selection-chemistry.cpp + void removeLPE(); + void removeFilter(); + void reapplyAffine(); + void clearLastAffine(); + void applyAffine(Geom::Affine const &affine, bool set_i2d=true,bool compensate=true, bool adjust_transf_center=true); + void removeTransform(); + void setScaleAbsolute(double, double, double, double); + void setScaleRelative(const Geom::Point&, const Geom::Scale&); + void rotateRelative(const Geom::Point&, double); + void skewRelative(const Geom::Point&, double, double); + void moveRelative(const Geom::Point &move, bool compensate = true); + void moveRelative(double dx, double dy); + void rotate(double); + void rotateScreen(double); + void scaleGrow(double); + void scaleScreen(double); + void scale(double); + void move(double dx, double dy); + void moveScreen(double dx, double dy); + + // various + bool fitCanvas(bool with_margins, bool skip_undo = false); + void swapFillStroke(); + void fillBetweenMany(); + + SiblingState getSiblingState(SPItem *item); + void insertSiblingState(SPObject *object, SiblingState state); + void clearSiblingStates(); + +protected: + virtual void _connectSignals(SPObject* object) {}; + virtual void _releaseSignals(SPObject* object) {}; + virtual void _emitChanged(bool persist_selection_context = false); + void _add(SPObject* object); + void _clear(); + void _remove(SPObject* object); + bool _anyAncestorIsInSet(SPObject *object); + void _removeDescendantsFromSet(SPObject *object); + void _removeAncestorsFromSet(SPObject *object); + SPItem *_sizeistItem(bool sml, CompareSize compare); + SPObject *_getMutualAncestor(SPObject *object); + virtual void _add3DBoxesRecursively(SPObject *obj); + virtual void _remove3DBoxesRecursively(SPObject *obj); + + MultiIndexContainer _container; + GC::soft_ptr<SPDesktop> _desktop; + GC::soft_ptr<SPDocument> _document; + std::list<SPBox3D *> _3dboxes; + std::unordered_map<SPObject*, sigc::connection> _releaseConnections; + +private: + BoolOpErrors pathBoolOp(bool_op bop, const bool skip_undo, const bool checked = false, + const Glib::ustring icon_name = nullptr, const Glib::ustring description = "", + bool silent = false); + void _disconnect(SPObject* object); + std::map<SPObject *, SiblingState> _sibling_state; + + Geom::Affine _last_affine; +}; + +typedef ObjectSet::SPItemRange SPItemRange; +typedef ObjectSet::SPGroupRange SPGroupRange; +typedef ObjectSet::XMLNodeRange XMLNodeRange; + +} // namespace Inkscape + +#endif //INKSCAPE_PROTOTYPE_OBJECTSET_H + +/* + 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 : diff --git a/src/object/persp3d-reference.cpp b/src/object/persp3d-reference.cpp new file mode 100644 index 0000000..decef35 --- /dev/null +++ b/src/object/persp3d-reference.cpp @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to the inkscape:perspectiveID attribute + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2007 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "persp3d-reference.h" +#include "uri.h" + +static void persp3dreference_href_changed(SPObject *old_ref, SPObject *ref, Persp3DReference *persp3dref); +static void persp3dreference_delete_self(SPObject *deleted, Persp3DReference *persp3dref); +static void persp3dreference_source_modified(SPObject *iSource, guint flags, Persp3DReference *persp3dref); + +Persp3DReference::Persp3DReference(SPObject* i_owner) : URIReference(i_owner) +{ + owner=i_owner; + persp_href = nullptr; + persp_repr = nullptr; + persp = nullptr; + _changed_connection = changedSignal().connect(sigc::bind(sigc::ptr_fun(persp3dreference_href_changed), this)); // listening to myself, this should be virtual instead +} + +Persp3DReference::~Persp3DReference() +{ + _changed_connection.disconnect(); // to do before unlinking + + quit_listening(); + unlink(); +} + +bool +Persp3DReference::_acceptObject(SPObject *obj) const +{ + return is<Persp3D>(obj) && URIReference::_acceptObject(obj); +; + /* effic: Don't bother making this an inline function: _acceptObject is a virtual function, + typically called from a context where the runtime type is not known at compile time. */ +} + +void +Persp3DReference::unlink() +{ + g_free(persp_href); + persp_href = nullptr; + detach(); +} + +void +Persp3DReference::start_listening(Persp3D* to) +{ + if ( to == nullptr ) { + return; + } + persp = to; + persp_repr = to->getRepr(); + _delete_connection = to->connectDelete(sigc::bind(sigc::ptr_fun(&persp3dreference_delete_self), this)); + _modified_connection = to->connectModified(sigc::bind<2>(sigc::ptr_fun(&persp3dreference_source_modified), this)); +} + +void +Persp3DReference::quit_listening() +{ + if ( persp == nullptr ) { + return; + } + _modified_connection.disconnect(); + _delete_connection.disconnect(); + persp_repr = nullptr; + persp = nullptr; +} + +static void +persp3dreference_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, Persp3DReference *persp3dref) +{ + persp3dref->quit_listening(); + + if (auto refobj = persp3dref->getObject()) { + persp3dref->start_listening(refobj); + } + + persp3dref->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +static void +persp3dreference_delete_self(SPObject */*deleted*/, Persp3DReference *persp3dref) +{ + g_return_if_fail(persp3dref->owner); + persp3dref->owner->deleteObject(); +} + +static void +persp3dreference_source_modified(SPObject */*iSource*/, guint /*flags*/, Persp3DReference *persp3dref) +{ + persp3dref->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + + +/* + 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 : diff --git a/src/object/persp3d-reference.h b/src/object/persp3d-reference.h new file mode 100644 index 0000000..6fc0adb --- /dev/null +++ b/src/object/persp3d-reference.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PERSP3D_REFERENCE_H +#define SEEN_PERSP3D_REFERENCE_H + +/* + * The reference corresponding to the inkscape:perspectiveID attribute + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2007 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "uri-references.h" +#include "persp3d.h" + +class SPObject; + +namespace Inkscape { +namespace XML { +class Node; +} +} + +class Persp3DReference : public Inkscape::URIReference { +public: + Persp3DReference(SPObject *obj); + ~Persp3DReference() override; + + Persp3D *getObject() const { + return cast<Persp3D>(URIReference::getObject()); + } + + SPObject *owner; + + // concerning the Persp3D (we only use SPBox3D) that is referred to: + char *persp_href; + Inkscape::XML::Node *persp_repr; + Persp3D *persp; + + sigc::connection _changed_connection; + sigc::connection _modified_connection; + sigc::connection _delete_connection; + + void link(char* to); + void unlink(); + void start_listening(Persp3D* to); + void quit_listening(); + +protected: + bool _acceptObject(SPObject *obj) const override; +}; + +#endif /* !SEEN_PERSP3D_REFERENCE_H */ + +/* + 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 : diff --git a/src/object/persp3d.cpp b/src/object/persp3d.cpp new file mode 100644 index 0000000..c6b40a7 --- /dev/null +++ b/src/object/persp3d.cpp @@ -0,0 +1,586 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Class modelling a 3D perspective as an SPObject + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "persp3d.h" + +#include <glibmm/i18n.h> + + +#include "attributes.h" +#include "box3d.h" +#include "desktop.h" +#include "document-undo.h" +#include "perspective-line.h" +#include "sp-defs.h" +#include "sp-root.h" +#include "vanishing-point.h" + +#include "svg/stringstream.h" +#include "ui/icon-names.h" +#include "ui/tools/box3d-tool.h" +#include "util/units.h" +#include "xml/node.h" +#include "xml/node-observer.h" + +void Persp3DNodeObserver::notifyAttributeChanged(Inkscape::XML::Node &, GQuark, Inkscape::Util::ptr_shared, Inkscape::Util::ptr_shared) +{ + auto persp = static_cast<Persp3D*>(this); + persp->update_box_displays(); +} + +using Inkscape::DocumentUndo; + +static int global_counter = 0; + +/* Constructor/destructor for the internal class */ + +Persp3DImpl::Persp3DImpl() +{ + my_counter = global_counter++; +} + +Persp3D::Persp3D() + : perspective_impl(std::make_unique<Persp3DImpl>()) +{} + +Persp3D::~Persp3D() = default; + + +/** + * Virtual build: set persp3d attributes from its associated XML node. + */ +void Persp3D::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObject::build(document, repr); + + this->readAttr(SPAttr::INKSCAPE_PERSP3D_VP_X); + this->readAttr(SPAttr::INKSCAPE_PERSP3D_VP_Y); + this->readAttr(SPAttr::INKSCAPE_PERSP3D_VP_Z); + this->readAttr(SPAttr::INKSCAPE_PERSP3D_ORIGIN); + + if (repr) { + repr->addObserver(nodeObserver()); + } +} + +/** + * Virtual release of Persp3D members before destruction. + */ +void Persp3D::release() +{ + getRepr()->removeObserver(nodeObserver()); + perspective_impl.reset(); + + SPObject::release(); +} + +/** + * Apply viewBox and legacy desktop transformation to point loaded from SVG + */ +static Proj::Pt2 legacy_transform_forward(Proj::Pt2 pt, SPDocument const *doc) { + // Read values are in 'user units'. + auto root = doc->getRoot(); + if (root->viewBox_set) { + pt[0] *= root->width.computed / root->viewBox.width(); + pt[1] *= root->height.computed / root->viewBox.height(); + } + + // <inkscape:perspective> stores inverted y-axis coordinates + if (doc->is_yaxisdown()) { + pt[1] *= -1; + if (pt[2]) { + pt[1] += doc->getHeight().value("px"); + } + } + + return pt; +} + +/** + * Apply viewBox and legacy desktop transformation to point to be written to SVG + */ +static Proj::Pt2 legacy_transform_backward(Proj::Pt2 pt, SPDocument const *doc) { + // <inkscape:perspective> stores inverted y-axis coordinates + if (doc->is_yaxisdown()) { + pt[1] *= -1; + if (pt[2]) { + pt[1] += doc->getHeight().value("px"); + } + } + + // Written values are in 'user units'. + auto root = doc->getRoot(); + if (root->viewBox_set) { + pt[0] *= root->viewBox.width() / root->width.computed; + pt[1] *= root->viewBox.height() / root->height.computed; + } + + return pt; +} + +/** + * Virtual set: set attribute to value. + */ +// FIXME: Currently we only read the finite positions of vanishing points; +// should we move VPs into their own repr (as it's done for SPStop, e.g.)? +void Persp3D::set(SPAttr key, gchar const *value) { + + switch (key) { + case SPAttr::INKSCAPE_PERSP3D_VP_X: { + if (value) { + Proj::Pt2 pt (value); + Proj::Pt2 ptn = legacy_transform_forward(pt, document); + perspective_impl->tmat.set_image_pt( Proj::X, ptn ); + } + break; + } + case SPAttr::INKSCAPE_PERSP3D_VP_Y: { + if (value) { + Proj::Pt2 pt (value); + Proj::Pt2 ptn = legacy_transform_forward(pt, document); + perspective_impl->tmat.set_image_pt( Proj::Y, ptn ); + } + break; + } + case SPAttr::INKSCAPE_PERSP3D_VP_Z: { + if (value) { + Proj::Pt2 pt (value); + Proj::Pt2 ptn = legacy_transform_forward(pt, document); + perspective_impl->tmat.set_image_pt( Proj::Z, ptn ); + } + break; + } + case SPAttr::INKSCAPE_PERSP3D_ORIGIN: { + if (value) { + Proj::Pt2 pt (value); + Proj::Pt2 ptn = legacy_transform_forward(pt, document); + perspective_impl->tmat.set_image_pt( Proj::W, ptn ); + } + break; + } + default: { + SPObject::set(key, value); + break; + } + } + + // FIXME: Is this the right place for resetting the draggers? PROBABLY NOT! + if (!SP_ACTIVE_DESKTOP) { + // Maybe in commandline mode. + return; + } + + auto ec = Inkscape::Application::instance().active_desktop()->getEventContext(); + if (auto bc = dynamic_cast<Inkscape::UI::Tools::Box3dTool*>(ec)) { + bc->_vpdrag->updateDraggers(); + bc->_vpdrag->updateLines(); + bc->_vpdrag->updateBoxHandles(); + bc->_vpdrag->updateBoxReprs(); + } +} + +void Persp3D::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* TODO: Should we update anything here? */ + + } + + SPObject::update(ctx, flags); +} + +Persp3D * +Persp3D::create_xml_element(SPDocument *document) { + SPDefs *defs = document->getDefs(); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *repr; + + /* if no perspective is given, create a default one */ + repr = xml_doc->createElement("inkscape:perspective"); + repr->setAttribute("sodipodi:type", "inkscape:persp3d"); + + // Use 'user-units' + double width = document->getWidth().value("px"); + double height = document->getHeight().value("px"); + if( document->getRoot()->viewBox_set ) { + Geom::Rect vb = document->getRoot()->viewBox; + width = vb.width(); + height = vb.height(); + } + + Proj::Pt2 proj_vp_x = Proj::Pt2 (0.0, height/2.0, 1.0); + Proj::Pt2 proj_vp_y = Proj::Pt2 (0.0, 1000.0, 0.0); + Proj::Pt2 proj_vp_z = Proj::Pt2 (width, height/2.0, 1.0); + Proj::Pt2 proj_origin = Proj::Pt2 (width/2.0, height/3.0, 1.0 ); + + gchar *str = nullptr; + str = proj_vp_x.coord_string(); + repr->setAttribute("inkscape:vp_x", str); + g_free (str); + str = proj_vp_y.coord_string(); + repr->setAttribute("inkscape:vp_y", str); + g_free (str); + str = proj_vp_z.coord_string(); + repr->setAttribute("inkscape:vp_z", str); + g_free (str); + str = proj_origin.coord_string(); + repr->setAttribute("inkscape:persp3d-origin", str); + g_free (str); + + /* Append the new persp3d to defs */ + defs->getRepr()->addChild(repr, nullptr); + Inkscape::GC::release(repr); + + return reinterpret_cast<Persp3D *>( defs->get_child_by_repr(repr) ); +} + +Persp3D * +Persp3D::document_first_persp(SPDocument *document) +{ + Persp3D *first = nullptr; + for (auto& child: document->getDefs()->children) { + if (is<Persp3D>(&child)) { + first = cast<Persp3D>(&child); + break; + } + } + return first; +} + +/** + * Virtual write: write object attributes to repr. + */ +Inkscape::XML::Node* Persp3D::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + + if ((flags & SP_OBJECT_WRITE_BUILD & SP_OBJECT_WRITE_EXT) && !repr) { + // this is where we end up when saving as plain SVG (also in other circumstances?); + // hence we don't set the sodipodi:type attribute + repr = xml_doc->createElement("inkscape:perspective"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + { + Proj::Pt2 pt = perspective_impl->tmat.column( Proj::X ); + Inkscape::SVGOStringStream os; + pt = legacy_transform_backward(pt, document); + os << pt[0] << " : " << pt[1] << " : " << pt[2]; + repr->setAttribute("inkscape:vp_x", os.str()); + } + { + Proj::Pt2 pt = perspective_impl->tmat.column( Proj::Y ); + Inkscape::SVGOStringStream os; + pt = legacy_transform_backward(pt, document); + os << pt[0] << " : " << pt[1] << " : " << pt[2]; + repr->setAttribute("inkscape:vp_y", os.str()); + } + { + Proj::Pt2 pt = perspective_impl->tmat.column( Proj::Z ); + Inkscape::SVGOStringStream os; + pt = legacy_transform_backward(pt, document); + os << pt[0] << " : " << pt[1] << " : " << pt[2]; + repr->setAttribute("inkscape:vp_z", os.str()); + } + { + Proj::Pt2 pt = perspective_impl->tmat.column( Proj::W ); + Inkscape::SVGOStringStream os; + pt = legacy_transform_backward(pt, document); + os << pt[0] << " : " << pt[1] << " : " << pt[2]; + repr->setAttribute("inkscape:persp3d-origin", os.str()); + } + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* convenience wrapper around Persp3D::get_finite_dir() and Persp3D::get_infinite_dir() */ +Geom::Point +Persp3D::get_PL_dir_from_pt (Geom::Point const &pt, Proj::Axis axis) const { + if (Persp3D::VP_is_finite(this->perspective_impl.get(), axis)) { + return this->get_finite_dir(pt, axis); + } else { + return this->get_infinite_dir(axis); + } +} + +Geom::Point +Persp3D::get_finite_dir (Geom::Point const &pt, Proj::Axis axis) const { + Box3D::PerspectiveLine pl(pt, axis, this); + return pl.direction(); +} + +Geom::Point +Persp3D::get_infinite_dir (Proj::Axis axis) const { + Proj::Pt2 vp(this->get_VP(axis)); + if (vp[2] != 0.0) { + g_warning ("VP should be infinite but is (%f : %f : %f)", vp[0], vp[1], vp[2]); + g_return_val_if_fail(vp[2] != 0.0, Geom::Point(0.0, 0.0)); + } + return Geom::Point(vp[0], vp[1]); +} + +double +Persp3D::get_infinite_angle (Proj::Axis axis) const { + return this->perspective_impl->tmat.get_infinite_angle(axis); +} + +bool +Persp3D::VP_is_finite (Persp3DImpl *persp_impl, Proj::Axis axis) { + return persp_impl->tmat.has_finite_image(axis); +} + +void +Persp3D::toggle_VP (Proj::Axis axis, bool set_undo) { + this->perspective_impl->tmat.toggle_finite(axis); + // FIXME: Remove this repr update and rely on vp_drag_sel_modified() to do this for us + // On the other hand, vp_drag_sel_modified() would update all boxes; + // here we can confine ourselves to the boxes of this particular perspective. + this->update_box_reprs(); + this->updateRepr(SP_OBJECT_WRITE_EXT); + if (set_undo) { + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), _("Toggle vanishing point"), INKSCAPE_ICON("draw-cuboid")); + } +} + +/* toggle VPs for the same axis in all perspectives of a given list */ +void +Persp3D::toggle_VPs (std::list<Persp3D *> list, Proj::Axis axis) { + for (Persp3D *persp : list) { + persp->toggle_VP(axis, false); + } + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), _("Toggle multiple vanishing points"), INKSCAPE_ICON("draw-cuboid")); +} + +void +Persp3D::set_VP_state (Proj::Axis axis, Proj::VPState state) { + if (Persp3D::VP_is_finite(this->perspective_impl.get(), axis) != (state == Proj::VP_FINITE)) { + this->toggle_VP(axis); + } +} + +void +Persp3D::rotate_VP (Proj::Axis axis, double angle, bool alt_pressed) { // angle is in degrees + // FIXME: Most of this functionality should be moved to trans_mat_3x4.(h|cpp) + if (this->perspective_impl->tmat.has_finite_image(axis)) { + // don't rotate anything for finite VPs + return; + } + Proj::Pt2 v_dir_proj (this->perspective_impl->tmat.column(axis)); + Geom::Point v_dir (v_dir_proj[0], v_dir_proj[1]); + double a = Geom::atan2 (v_dir) * 180/M_PI; + a += alt_pressed ? 0.5 * ((angle > 0 ) - (angle < 0)) : angle; // the r.h.s. yields +/-0.5 or angle + this->perspective_impl->tmat.set_infinite_direction (axis, a); + + this->update_box_reprs (); + this->updateRepr(SP_OBJECT_WRITE_EXT); +} + +void +Persp3D::apply_affine_transformation (Geom::Affine const &xform) { + this->perspective_impl->tmat *= xform; + this->update_box_reprs(); + this->updateRepr(SP_OBJECT_WRITE_EXT); +} + +void +Persp3D::add_box (SPBox3D *box) { + auto persp_impl = perspective_impl.get(); + + if (!box) { + return; + } + if (std::find (persp_impl->boxes.begin(), persp_impl->boxes.end(), box) != persp_impl->boxes.end()) { + return; + } + persp_impl->boxes.push_back(box); +} + +void +Persp3D::remove_box (SPBox3D *box) { + auto persp_impl = perspective_impl.get(); + + std::vector<SPBox3D *>::iterator i = std::find (persp_impl->boxes.begin(), persp_impl->boxes.end(), box); + if (i != persp_impl->boxes.end()) + persp_impl->boxes.erase(i); +} + +bool +Persp3D::has_box (SPBox3D *box) const { + auto persp_impl = perspective_impl.get(); + + // FIXME: For some reason, std::find() does not seem to compare pointers "correctly" (or do we need to + // provide a proper comparison function?), so we manually traverse the list. + for (auto & boxe : persp_impl->boxes) { + if (boxe == box) { + return true; + } + } + return false; +} + +void +Persp3D::update_box_displays () { + auto persp_impl = perspective_impl.get(); + + if (persp_impl->boxes.empty()) + return; + for (auto & boxe : persp_impl->boxes) { + boxe->position_set(); + } +} + +void +Persp3D::update_box_reprs () { + auto persp_impl = perspective_impl.get(); + + if (!persp_impl || persp_impl->boxes.empty()) + return; + for (auto & boxe : persp_impl->boxes) { + boxe->updateRepr(SP_OBJECT_WRITE_EXT); + boxe->set_z_orders(); + } +} + +void +Persp3D::update_z_orders () { + auto persp_impl = perspective_impl.get(); + + if (!persp_impl || persp_impl->boxes.empty()) + return; + for (auto & boxe : persp_impl->boxes) { + boxe->set_z_orders(); + } +} + +// FIXME: For some reason we seem to require a vector instead of a list in Persp3D, but in vp_knot_moved_handler() +// we need a list of boxes. If we can store a list in Persp3D right from the start, this function becomes +// obsolete. We should do this. +std::list<SPBox3D *> +Persp3D::list_of_boxes() const { + auto persp_impl = perspective_impl.get(); + + std::list<SPBox3D *> bx_lst; + for (auto & boxe : persp_impl->boxes) { + bx_lst.push_back(boxe); + } + return bx_lst; +} + +bool +Persp3D::perspectives_coincide(const Persp3D *other) const +{ + return this->perspective_impl->tmat == other->perspective_impl->tmat; +} + +void +Persp3D::absorb(Persp3D *other) { + /* double check if we are called in sane situations */ + g_return_if_fail (this->perspectives_coincide(other) && this != other); + + // Note: We first need to copy the boxes of other into a separate list; + // otherwise the loop below gets confused when perspectives are reattached. + std::list<SPBox3D *> boxes_of_persp2 = other->list_of_boxes(); + + for (auto & box : boxes_of_persp2) { + box->switch_perspectives(other, this, true); + box->updateRepr(SP_OBJECT_WRITE_EXT); // so that undo/redo can do its job properly + } +} + +/* checks whether all boxes linked to this perspective are currently selected */ +bool +Persp3D::has_all_boxes_in_selection (Inkscape::ObjectSet *set) const { + auto persp_impl = perspective_impl.get(); + + std::list<SPBox3D *> selboxes = set->box3DList(); + + for (auto & boxe : persp_impl->boxes) { + if (std::find(selboxes.begin(), selboxes.end(), boxe) == selboxes.end()) { + // we have an unselected box in the perspective + return false; + } + } + return true; +} + +/* some debugging stuff follows */ + +void +Persp3D::print_debugging_info () const { + auto persp_impl = perspective_impl.get(); + g_print ("=== Info for Persp3D %d ===\n", persp_impl->my_counter); + gchar * cstr; + for (auto & axe : Proj::axes) { + cstr = this->get_VP(axe).coord_string(); + g_print (" VP %s: %s\n", Proj::string_from_axis(axe), cstr); + g_free(cstr); + } + cstr = this->get_VP(Proj::W).coord_string(); + g_print (" Origin: %s\n", cstr); + g_free(cstr); + + g_print (" Boxes: "); + for (auto & boxe : persp_impl->boxes) { + g_print ("%d (%d) ", boxe->my_counter, boxe->get_perspective()->perspective_impl->my_counter); + } + g_print ("\n"); + g_print ("========================\n"); +} + +void +Persp3D::print_debugging_info_all(SPDocument *document) +{ + for (auto& child: document->getDefs()->children) { + if (is<Persp3D>(&child)) { + cast<Persp3D>(&child)->print_debugging_info(); + } + } + Persp3D::print_all_selected(); +} + +void +Persp3D::print_all_selected() { + g_print ("\n======================================\n"); + g_print ("Selected perspectives and their boxes:\n"); + + std::list<Persp3D *> sel_persps = SP_ACTIVE_DESKTOP->getSelection()->perspList(); + + for (auto & sel_persp : sel_persps) { + auto persp = sel_persp; + auto persp_impl = persp->perspective_impl.get(); + g_print (" %s (%d): ", persp->getRepr()->attribute("id"), persp->perspective_impl->my_counter); + for (auto & boxe : persp_impl->boxes) { + g_print ("%d ", boxe->my_counter); + } + g_print ("\n"); + } + g_print ("======================================\n\n"); + } + +void print_current_persp3d(gchar *func_name, Persp3D *persp) { + g_print ("%s: current_persp3d is now %s\n", + func_name, + persp ? persp->getRepr()->attribute("id") : "NULL"); +} + +/* + 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 : diff --git a/src/object/persp3d.h b/src/object/persp3d.h new file mode 100644 index 0000000..17a8c43 --- /dev/null +++ b/src/object/persp3d.h @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PERSP3D_H +#define SEEN_PERSP3D_H + +/* + * Implementation of 3D perspectives as SPObjects + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <list> +#include <map> +#include <vector> + +#include "document.h" +#include "inkscape.h" // for SP_ACTIVE_DOCUMENT +#include "sp-object.h" +#include "transf_mat_3x4.h" +#include "xml/node-observer.h" + +class SPBox3D; +class Persp3D; + +class Persp3DNodeObserver : public Inkscape::XML::NodeObserver +{ + friend class Persp3D; + ~Persp3DNodeObserver() override = default; // can only exist as a direct base of Persp3D + + void notifyAttributeChanged(Inkscape::XML::Node &, GQuark, Inkscape::Util::ptr_shared, Inkscape::Util::ptr_shared) final; +}; + +class Persp3DImpl +{ +public: + Persp3DImpl(); + + Proj::TransfMat3x4 tmat{Proj::TransfMat3x4()}; + + // Also write the list of boxes into the xml repr and vice versa link boxes to their persp3d? + std::vector<SPBox3D *> boxes; + SPDocument *document{nullptr}; + + // for debugging only + int my_counter; +}; + +class Persp3D final + : public SPObject + , private Persp3DNodeObserver +{ +public: + Persp3D(); + ~Persp3D() override; + int tag() const override { return tag_of<decltype(*this)>; } + + std::unique_ptr<Persp3DImpl> perspective_impl; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttr key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + + +public: + // FIXME: Make more of these inline! + static Persp3D * get_from_repr (Inkscape::XML::Node *repr) { + return cast<Persp3D>(SP_ACTIVE_DOCUMENT->getObjectByRepr(repr)); + } + Proj::Pt2 get_VP (Proj::Axis axis) const { + return perspective_impl->tmat.column(axis); + } + Geom::Point get_PL_dir_from_pt (Geom::Point const &pt, Proj::Axis axis) const; // convenience wrapper around the following two + Geom::Point get_finite_dir (Geom::Point const &pt, Proj::Axis axis) const; + Geom::Point get_infinite_dir (Proj::Axis axis) const; + double get_infinite_angle (Proj::Axis axis) const; + static bool VP_is_finite (Persp3DImpl *persp_impl, Proj::Axis axis); + void toggle_VP (Proj::Axis axis, bool set_undo = true); + static void toggle_VPs (std::list<Persp3D *>, Proj::Axis axis); + void set_VP_state (Proj::Axis axis, Proj::VPState state); + void rotate_VP (Proj::Axis axis, double angle, bool alt_pressed); // angle is in degrees + void apply_affine_transformation (Geom::Affine const &xform); + + void add_box (SPBox3D *box); + void remove_box (SPBox3D *box); + bool has_box (SPBox3D *box) const; + + void update_box_displays (); + void update_box_reprs (); + void update_z_orders (); + unsigned int num_boxes () const { return perspective_impl->boxes.size(); } + std::list<SPBox3D *> list_of_boxes() const; + + bool perspectives_coincide(Persp3D const *rhs) const; + void absorb(Persp3D *persp2); + + static Persp3D * create_xml_element (SPDocument *document); + static Persp3D * document_first_persp (SPDocument *document); + + bool has_all_boxes_in_selection (Inkscape::ObjectSet *set) const; + + void print_debugging_info () const; + static void print_debugging_info_all(SPDocument *doc); + static void print_all_selected(); + +private: + friend Persp3DNodeObserver; // for static_cast + Persp3DNodeObserver &nodeObserver() { return *this; } +}; + +#endif /* __PERSP3D_H__ */ + +/* + 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 : diff --git a/src/object/sp-anchor.cpp b/src/object/sp-anchor.cpp new file mode 100644 index 0000000..a1d5474 --- /dev/null +++ b/src/object/sp-anchor.cpp @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <a> element implementation + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 2017 Martin Owens + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_ANCHOR_VERBOSE + +#include <glibmm/i18n.h> +#include "xml/quote.h" +#include "xml/repr.h" +#include "xml/href-attribute-helper.h" +#include "attributes.h" +#include "sp-anchor.h" +#include "ui/view/svg-view-widget.h" +#include "document.h" + +SPAnchor::SPAnchor() : SPGroup() { + this->href = nullptr; + this->type = nullptr; + this->title = nullptr; + this->page = nullptr; +} + +SPAnchor::~SPAnchor() = default; + +void SPAnchor::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPGroup::build(document, repr); + + this->readAttr(SPAttr::XLINK_TYPE); + this->readAttr(SPAttr::XLINK_ROLE); + this->readAttr(SPAttr::XLINK_ARCROLE); + this->readAttr(SPAttr::XLINK_TITLE); + this->readAttr(SPAttr::XLINK_SHOW); + this->readAttr(SPAttr::XLINK_ACTUATE); + this->readAttr(SPAttr::XLINK_HREF); + this->readAttr(SPAttr::TARGET); +} + +void SPAnchor::release() { + if (this->href) { + g_free(this->href); + this->href = nullptr; + } + if (this->type) { + g_free(this->type); + this->type = nullptr; + } + if (this->title) { + g_free(this->title); + this->title = nullptr; + } + if (this->page) { + g_free(this->page); + this->page = nullptr; + } + + SPGroup::release(); +} + +void SPAnchor::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::XLINK_HREF: + g_free(this->href); + this->href = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + this->updatePageAnchor(); + break; + case SPAttr::XLINK_TYPE: + g_free(this->type); + this->type = g_strdup(value); + this->updatePageAnchor(); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::XLINK_ROLE: + case SPAttr::XLINK_ARCROLE: + case SPAttr::XLINK_TITLE: + g_free(this->title); + this->title = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::XLINK_SHOW: + case SPAttr::XLINK_ACTUATE: + case SPAttr::TARGET: + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPGroup::set(key, value); + break; + } +} + +/* + * Detect if this anchor qualifies as a page link and append + * the new page document to this document. + */ +void SPAnchor::updatePageAnchor() { + if (this->type && !strcmp(this->type, "page")) { + if (this->href && !this->page) { + this->page = this->document->createChildDoc(this->href); + } + } +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPAnchor::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:a"); + } + + Inkscape::setHrefAttribute(*repr, this->href); + if (this->type) repr->setAttribute("xlink:type", this->type); + if (this->title) repr->setAttribute("xlink:title", this->title); + + if (repr != this->getRepr()) { + // XML Tree being directly used while it shouldn't be in the + // below COPY_ATTR lines + COPY_ATTR(repr, this->getRepr(), "xlink:role"); + COPY_ATTR(repr, this->getRepr(), "xlink:arcrole"); + COPY_ATTR(repr, this->getRepr(), "xlink:show"); + COPY_ATTR(repr, this->getRepr(), "xlink:actuate"); + COPY_ATTR(repr, this->getRepr(), "target"); + } + + SPGroup::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPAnchor::typeName() const { + return "link"; +} + +const char* SPAnchor::displayName() const { + return C_("Hyperlink|Noun", "Link"); +} + +gchar* SPAnchor::description() const { + if (this->href) { + char *quoted_href = xml_quote_strdup(this->href); + char *ret = g_strdup_printf(_("to %s"), quoted_href); + g_free(quoted_href); + return ret; + } else { + return g_strdup (_("without URI")); + } +} + +/* fixme: We should forward event to appropriate container/view */ +/* The only use of SPEvent appears to be here, to change the cursor in Inkview when over a link (and + * which hasn't worked since at least 0.48). GUI code should not be here. */ +int SPAnchor::event(SPEvent* event) { + + switch (event->type) { + case SPEvent::ACTIVATE: + if (this->href) { + // If this actually worked, it could be useful to open a webpage with the link. + g_message("Activated xlink:href=\"%s\"", this->href); + return TRUE; + } + break; + + case SPEvent::MOUSEOVER: + { + if (event->view) { + event->view->mouseover(); + } + break; + } + + case SPEvent::MOUSEOUT: + { + if (event->view) { + event->view->mouseout(); + } + break; + } + + default: + break; + } + + return FALSE; +} + +/* + 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 : diff --git a/src/object/sp-anchor.h b/src/object/sp-anchor.h new file mode 100644 index 0000000..77cd6b8 --- /dev/null +++ b/src/object/sp-anchor.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_ANCHOR_H +#define SEEN_SP_ANCHOR_H + +/* + * SVG <a> element implementation + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-item-group.h" + +class SPAnchor final : public SPGroup { +public: + SPAnchor(); + ~SPAnchor() override; + int tag() const override { return tag_of<decltype(*this)>; } + + char *href; + char *type; + char *title; + SPDocument *page; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const* value) override; + virtual void updatePageAnchor(); + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + const char* typeName() const override; + const char* displayName() const override; + char* description() const override; + int event(SPEvent *event) override; +}; + +#endif diff --git a/src/object/sp-clippath.cpp b/src/object/sp-clippath.cpp new file mode 100644 index 0000000..1d741e5 --- /dev/null +++ b/src/object/sp-clippath.cpp @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <clipPath> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2001-2002 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include "xml/repr.h" + +#include "enums.h" +#include "attributes.h" +#include "document.h" +#include "style.h" + +#include <2geom/transforms.h> + +#include "sp-clippath.h" +#include "sp-item.h" +#include "sp-defs.h" + +#include "display/drawing-item.h" +#include "display/drawing-group.h" + +SPClipPath::SPClipPath() +{ + clipPathUnits_set = false; + clipPathUnits = SP_CONTENT_UNITS_USERSPACEONUSE; +} + +SPClipPath::~SPClipPath() = default; + +void SPClipPath::build(SPDocument *doc, Inkscape::XML::Node *repr) +{ + SPObjectGroup::build(doc, repr); + + readAttr(SPAttr::STYLE); + readAttr(SPAttr::CLIPPATHUNITS); + + doc->addResource("clipPath", this); +} + +void SPClipPath::release() +{ + if (document) { + document->removeResource("clipPath", this); + } + + views.clear(); + + SPObjectGroup::release(); +} + +void SPClipPath::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::CLIPPATHUNITS: + clipPathUnits = SP_CONTENT_UNITS_USERSPACEONUSE; + clipPathUnits_set = false; + + if (value) { + if (!std::strcmp(value, "userSpaceOnUse")) { + clipPathUnits_set = true; + } else if (!std::strcmp(value, "objectBoundingBox")) { + clipPathUnits = SP_CONTENT_UNITS_OBJECTBOUNDINGBOX; + clipPathUnits_set = true; + } + } + + 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 { + SPObjectGroup::set(key, value); + } + break; + } +} + +void SPClipPath::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + SPObjectGroup::child_added(child, ref); + + if (auto item = cast<SPItem>(document->getObjectByRepr(child))) { + for (auto &v : views) { + auto ac = item->invoke_show(v.drawingitem->drawing(), v.key, SP_ITEM_REFERENCE_FLAGS); + if (ac) { + v.drawingitem->prependChild(ac); + } + } + } +} + +void SPClipPath::update(SPCtx *ctx, unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + for (auto c : childList(true)) { + if (cflags || (c->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + c->updateDisplay(ctx, cflags); + } + sp_object_unref(c); + } + + for (auto &v : views) { + update_view(v); + } +} + +void SPClipPath::update_view(View &v) +{ + if (clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && v.bbox) { + v.drawingitem->setChildTransform(Geom::Scale(v.bbox->dimensions()) * Geom::Translate(v.bbox->min())); + } else { + v.drawingitem->setChildTransform(Geom::identity()); + } +} + +void SPClipPath::modified(unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + for (auto c : childList(true)) { + if (cflags || (c->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + c->emitModified(cflags); + } + sp_object_unref(c); + } +} + +Inkscape::XML::Node *SPClipPath::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:clipPath"); + } + + SPObjectGroup::write(xml_doc, repr, flags); + + return repr; +} + +Inkscape::DrawingItem *SPClipPath::show(Inkscape::Drawing &drawing, unsigned key, Geom::OptRect const &bbox) +{ + views.emplace_back(make_drawingitem<Inkscape::DrawingGroup>(drawing), bbox, key); + auto &v = views.back(); + auto root = v.drawingitem.get(); + + for (auto &child : children) { + if (auto item = cast<SPItem>(&child)) { + auto ac = item->invoke_show(drawing, key, SP_ITEM_REFERENCE_FLAGS); + if (ac) { + // The order is not important in clippath. + root->appendChild(ac); + } + } + } + + root->setStyle(style); + + update_view(v); + + return root; +} + +void SPClipPath::hide(unsigned key) +{ + for (auto &child : children) { + if (auto item = cast<SPItem>(&child)) { + item->invoke_hide(key); + } + } + + auto it = std::find_if(views.begin(), views.end(), [=] (auto &v) { + return v.key == key; + }); + + if (it == views.end()) { + return; + } + + views.erase(it); +} + +void SPClipPath::setBBox(unsigned key, Geom::OptRect const &bbox) +{ + auto it = std::find_if(views.begin(), views.end(), [=] (auto &v) { + return v.key == key; + }); + assert(it != views.end()); + auto &v = *it; + + v.bbox = bbox; + update_view(v); +} + +Geom::OptRect SPClipPath::geometricBounds(Geom::Affine const &transform) const +{ + Geom::OptRect bbox; + for (auto &child : children) { + if (auto item = cast<SPItem>(&child)) { + bbox.unionWith(item->geometricBounds(item->transform * transform)); + } + } + return bbox; +} + +// Create a mask element (using passed elements), add it to <defs> +char const *SPClipPath::create(std::vector<Inkscape::XML::Node*> &reprs, SPDocument *document) +{ + auto defsrepr = document->getDefs()->getRepr(); + + auto xml_doc = document->getReprDoc(); + auto repr = xml_doc->createElement("svg:clipPath"); + repr->setAttribute("clipPathUnits", "userSpaceOnUse"); + + defsrepr->appendChild(repr); + auto id = repr->attribute("id"); + auto clip_path_object = document->getObjectById(id); + + for (auto node : reprs) { + clip_path_object->appendChildRepr(node); + } + + Inkscape::GC::release(repr); + return id; +} + +SPClipPath::View::View(DrawingItemPtr<Inkscape::DrawingGroup> drawingitem, Geom::OptRect const &bbox, unsigned key) + : drawingitem(std::move(drawingitem)) + , bbox(bbox) + , key(key) {} + +bool SPClipPathReference::_acceptObject(SPObject *obj) const +{ + if (!is<SPClipPath>(obj)) { + return false; + } + + if (URIReference::_acceptObject(obj)) { + return true; + } + + auto const owner = getOwner(); + //XML Tree being used directly here while it shouldn't be... + auto const owner_repr = owner->getRepr(); + //XML Tree being used directly here while it shouldn't be... + auto const obj_repr = obj->getRepr(); + char const *owner_name = ""; + char const *owner_clippath = ""; + char const *obj_name = ""; + char const *obj_id = ""; + if (owner_repr) { + owner_name = owner_repr->name(); + owner_clippath = owner_repr->attribute("clippath"); + } + if (obj_repr) { + obj_name = obj_repr->name(); + obj_id = obj_repr->attribute("id"); + } + std::printf("WARNING: Ignoring recursive clippath reference " + "<%s clippath=\"%s\"> in <%s id=\"%s\">", + owner_name, owner_clippath, + obj_name, obj_id); + + return false; +} + +/* + 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 : diff --git a/src/object/sp-clippath.h b/src/object/sp-clippath.h new file mode 100644 index 0000000..2ffceef --- /dev/null +++ b/src/object/sp-clippath.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CLIPPATH_H +#define SEEN_SP_CLIPPATH_H + +/* + * SVG <clipPath> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001-2002 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <vector> +#include <cstdio> +#include <2geom/rect.h> +#include "sp-object-group.h" +#include "uri-references.h" +#include "display/drawing-item-ptr.h" + +namespace Inkscape { +class Drawing; +class DrawingItem; +class DrawingGroup; +} // namespace Inkscape + +class SPClipPath final + : public SPObjectGroup +{ +public: + SPClipPath(); + ~SPClipPath() override; + int tag() const override { return tag_of<decltype(*this)>; } + + bool clippath_units() const { return clipPathUnits; } + + // Fixme: Hack used by cairo-renderer. + Geom::OptRect get_last_bbox() const { return views.back().bbox; } + + static char const *create(std::vector<Inkscape::XML::Node*> &reprs, SPDocument *document); + + Inkscape::DrawingItem *show(Inkscape::Drawing &drawing, unsigned key, Geom::OptRect const &bbox); + void hide(unsigned key); + void setBBox(unsigned key, Geom::OptRect const &bbox); + + Geom::OptRect geometricBounds(Geom::Affine const &transform) const; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const *value) override; + void update(SPCtx *ctx, unsigned flags) override; + void modified(unsigned flags) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; + + void child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) override; + +private: + bool clipPathUnits_set : 1; + bool clipPathUnits : 1; + + struct View + { + DrawingItemPtr<Inkscape::DrawingGroup> drawingitem; + Geom::OptRect bbox; + unsigned key; + View(DrawingItemPtr<Inkscape::DrawingGroup> drawingitem, Geom::OptRect const &bbox, unsigned key); + }; + std::vector<View> views; + void update_view(View &v); +}; + +class SPClipPathReference + : public Inkscape::URIReference +{ +public: + SPClipPathReference(SPObject *obj) + : URIReference(obj) {} + + SPClipPath *getObject() const + { + return static_cast<SPClipPath*>(URIReference::getObject()); + } + + sigc::connection modified_connection; + +protected: + /** + * If the owner element of this reference (the element with <... clippath="...">) + * is a child of the clippath it refers to, return false. + * \return false if obj is not a clippath or if obj is a parent of this + * reference's owner element. True otherwise. + */ + bool _acceptObject(SPObject *obj) const override; +}; + +#endif // SEEN_SP_CLIPPATH_H + +/* + 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 : diff --git a/src/object/sp-conn-end-pair.cpp b/src/object/sp-conn-end-pair.cpp new file mode 100644 index 0000000..b245129 --- /dev/null +++ b/src/object/sp-conn-end-pair.cpp @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A class for handling connector endpoint movement and libavoid interaction. + * + * Authors: + * Peter Moulder <pmoulder@mail.csse.monash.edu.au> + * Michael Wybrow <mjwybrow@users.sourceforge.net> + * Abhishek Sharma + * + * * Copyright (C) 2004-2005 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> +#include <glibmm/stringutils.h> + +#include "attributes.h" +#include "sp-conn-end.h" +#include "uri.h" +#include "display/curve.h" +#include "xml/repr.h" +#include "sp-path.h" +#include "sp-use.h" +#include "3rdparty/adaptagrams/libavoid/router.h" +#include "document.h" +#include "sp-item-group.h" + + +SPConnEndPair::SPConnEndPair(SPPath *const owner) + : _path(owner) + , _connRef(nullptr) + , _connType(SP_CONNECTOR_NOAVOID) + , _connCurvature(0.0) + , _transformed_connection() +{ + for (unsigned handle_ix = 0; handle_ix <= 1; ++handle_ix) { + this->_connEnd[handle_ix] = new SPConnEnd(owner); + this->_connEnd[handle_ix]->_changed_connection + = this->_connEnd[handle_ix]->ref.changedSignal() + .connect(sigc::bind(sigc::ptr_fun(sp_conn_end_href_changed), + this->_connEnd[handle_ix], owner, handle_ix)); + } +} + +SPConnEndPair::~SPConnEndPair() +{ + for (auto & handle_ix : this->_connEnd) { + delete handle_ix; + handle_ix = nullptr; + } +} + +void SPConnEndPair::release() +{ + for (auto & handle_ix : this->_connEnd) { + handle_ix->_changed_connection.disconnect(); + handle_ix->_delete_connection.disconnect(); + handle_ix->_transformed_connection.disconnect(); + g_free(handle_ix->href); + handle_ix->href = nullptr; + handle_ix->ref.detach(); + } + + // If the document is being destroyed then the router instance + // and the ConnRefs will have been destroyed with it. + const bool routerInstanceExists = (_path->document->getRouter() != nullptr); + + if (_connRef && routerInstanceExists) { + _connRef->router()->deleteConnector(_connRef); + } + _connRef = nullptr; + + _transformed_connection.disconnect(); +} + +void sp_conn_end_pair_build(SPObject *object) +{ + object->readAttr(SPAttr::CONNECTOR_TYPE); + object->readAttr(SPAttr::CONNECTION_START); + object->readAttr(SPAttr::CONNECTION_START_POINT); + object->readAttr(SPAttr::CONNECTION_END); + object->readAttr(SPAttr::CONNECTION_END_POINT); + object->readAttr(SPAttr::CONNECTOR_CURVATURE); +} + + +static void avoid_conn_transformed(Geom::Affine const */*mp*/, SPItem *moved_item) +{ + auto path = cast<SPPath>(moved_item); + if (path->connEndPair.isAutoRoutingConn()) { + path->connEndPair.tellLibavoidNewEndpoints(); + } +} + + +void SPConnEndPair::setAttr(const SPAttr key, gchar const *const value) +{ + switch (key) { + case SPAttr::CONNECTOR_TYPE: + if (value && (strcmp(value, "polyline") == 0 || strcmp(value, "orthogonal") == 0)) { + int new_conn_type = strcmp(value, "polyline") ? SP_CONNECTOR_ORTHOGONAL : SP_CONNECTOR_POLYLINE; + + if (!_connRef) { + _connType = new_conn_type; + Avoid::Router *router = _path->document->getRouter(); + _connRef = new Avoid::ConnRef(router); + _connRef->setRoutingType(new_conn_type == SP_CONNECTOR_POLYLINE ? + Avoid::ConnType_PolyLine : Avoid::ConnType_Orthogonal); + _transformed_connection = _path->connectTransformed(sigc::ptr_fun(&avoid_conn_transformed)); + } else if (new_conn_type != _connType) { + _connType = new_conn_type; + _connRef->setRoutingType(new_conn_type == SP_CONNECTOR_POLYLINE ? + Avoid::ConnType_PolyLine : Avoid::ConnType_Orthogonal); + sp_conn_reroute_path(_path); + } + } else { + _connType = SP_CONNECTOR_NOAVOID; + + if (_connRef) { + _connRef->router()->deleteConnector(_connRef); + _connRef = nullptr; + _transformed_connection.disconnect(); + } + } + break; + case SPAttr::CONNECTOR_CURVATURE: + if (value) { + _connCurvature = g_strtod(value, nullptr); + if (_connRef && _connRef->isInitialised()) { + // Redraw the connector, but only if it has been initialised. + sp_conn_reroute_path(_path); + } + } + break; + case SPAttr::CONNECTION_START: + this->_connEnd[0]->setAttacherHref(value); + break; + case SPAttr::CONNECTION_START_POINT: + this->_connEnd[0]->setAttacherSubHref(value); + break; + case SPAttr::CONNECTION_END: + this->_connEnd[1]->setAttacherHref(value); + break; + case SPAttr::CONNECTION_END_POINT: + this->_connEnd[1]->setAttacherSubHref(value); + break; + } +} + +void SPConnEndPair::writeRepr(Inkscape::XML::Node *const repr) const +{ + char const * const attrs[] = { + "inkscape:connection-start", "inkscape:connection-end"}; + char const * const point_attrs[] = { + "inkscape:connection-start-point", "inkscape:connection-end-point"}; + for (unsigned handle_ix = 0; handle_ix < 2; ++handle_ix) { + const Inkscape::URI* U = this->_connEnd[handle_ix]->ref.getURI(); + if (U) { + auto str = U->str(); + repr->setAttribute(attrs[handle_ix], str); + } + const Inkscape::URI* P = this->_connEnd[handle_ix]->sub_ref.getURI(); + if (P) { + auto str = P->str(); + repr->setAttribute(point_attrs[handle_ix], str); + } + } + if (_connType == SP_CONNECTOR_POLYLINE || _connType == SP_CONNECTOR_ORTHOGONAL) { + repr->setAttribute("inkscape:connector-curvature", Glib::Ascii::dtostr(_connCurvature)); + repr->setAttribute("inkscape:connector-type", _connType == SP_CONNECTOR_POLYLINE ? "polyline" : "orthogonal" ); + } +} + +void SPConnEndPair::getAttachedItems(SPItem *h2attItem[2]) const { + for (unsigned h = 0; h < 2; ++h) { + auto obj = this->_connEnd[h]->ref.getObject(); + auto sub_obj = this->_connEnd[h]->sub_ref.getObject(); + + if(sub_obj) { + // For sub objects, we have to go fishing for the virtual/shadow + // object which has the correct position for this use/symbol + auto use = cast<SPUse>(obj); + if(use) { + auto root = use->root(); + bool found = false; + for (auto& child: root->children) { + if(!g_strcmp0(child.getAttribute("id"), sub_obj->getId())) { + h2attItem[h] = (SPItem *) &child; + found = true; + } + } + if(!found) { + g_warning("Couldn't find sub connector point!"); + } + } + } else { + h2attItem[h] = obj; + } + + // Deal with the case of the attached object being an empty group. + // A group containing no items does not have a valid bbox, so + // causes problems for the auto-routing code. Also, since such a + // group no longer has an onscreen representation and can only be + // selected through the XML editor, it makes sense just to detach + // connectors from them. + if (auto group = cast<SPGroup>(h2attItem[h])) { + if (group->getItemCount() == 0) { + // This group is empty, so detach. + sp_conn_end_detach(_path, h); + h2attItem[h] = nullptr; + } + } + } +} + +void SPConnEndPair::getEndpoints(Geom::Point endPts[]) const +{ + SPCurve const *curve = _path->curveForEdit(); + SPItem *h2attItem[2] = {nullptr}; + getAttachedItems(h2attItem); + Geom::Affine i2d = _path->i2doc_affine(); + + for (unsigned h = 0; h < 2; ++h) { + if (h2attItem[h]) { + endPts[h] = h2attItem[h]->getAvoidRef().getConnectionPointPos(); + } else if (!curve->is_empty()) { + if (h == 0) { + endPts[h] = *(curve->first_point()) * i2d; + } else { + endPts[h] = *(curve->last_point()) * i2d; + } + } + } +} + +gdouble SPConnEndPair::getCurvature() const +{ + return _connCurvature; +} + +SPConnEnd** SPConnEndPair::getConnEnds() +{ + return _connEnd; +} + +bool SPConnEndPair::isOrthogonal() const +{ + return _connType == SP_CONNECTOR_ORTHOGONAL; +} + + +static void redrawConnectorCallback(void *ptr) +{ + auto path = static_cast<SPPath *>(ptr); + if (path->document == nullptr) { + // This can happen when the document is being destroyed. + return; + } + sp_conn_redraw_path(path); +} + +void SPConnEndPair::rerouteFromManipulation() +{ + sp_conn_reroute_path_immediate(_path); +} + + +// Called from SPPath::update to initialise the endpoints. +void SPConnEndPair::update() +{ + if (_connType != SP_CONNECTOR_NOAVOID) { + g_assert(_connRef != nullptr); + if (!_connRef->isInitialised()) { + _updateEndPoints(); + _connRef->setCallback(&redrawConnectorCallback, _path); + } + } +} + +void SPConnEndPair::_updateEndPoints() +{ + Geom::Point endPt[2]; + getEndpoints(endPt); + + Avoid::Point src(endPt[0][Geom::X], endPt[0][Geom::Y]); + Avoid::Point dst(endPt[1][Geom::X], endPt[1][Geom::Y]); + + _connRef->setEndpoints(src, dst); +} + + +bool SPConnEndPair::isAutoRoutingConn() const +{ + return _connType != SP_CONNECTOR_NOAVOID; +} + +void SPConnEndPair::makePathInvalid() +{ + g_assert(_connRef != nullptr); + + _connRef->makePathInvalid(); +} + +// Redraws the curve along the recalculated route +// Straight or curved +SPCurve SPConnEndPair::createCurve(Avoid::ConnRef *connRef, const gdouble curvature) +{ + g_assert(connRef != nullptr); + + bool straight = curvature<1e-3; + + Avoid::PolyLine route = connRef->displayRoute(); + if (!straight) route = route.curvedPolyline(curvature); + connRef->calcRouteDist(); + + SPCurve curve; + + curve.moveto(Geom::Point(route.ps[0].x, route.ps[0].y)); + int pn = route.size(); + for (int i = 1; i < pn; ++i) { + Geom::Point p(route.ps[i].x, route.ps[i].y); + if (straight) { + curve.lineto(p); + } else { + switch (route.ts[i]) { + case 'M': + curve.moveto(p); + break; + case 'L': + curve.lineto(p); + break; + case 'C': + g_assert(i + 2 < pn); + curve.curveto(p, Geom::Point(route.ps[i+1].x, route.ps[i+1].y), + Geom::Point(route.ps[i+2].x, route.ps[i+2].y)); + i+=2; + break; + } + } + } + + return curve; +} + +void SPConnEndPair::tellLibavoidNewEndpoints(bool const processTransaction) +{ + if (_connRef == nullptr || !isAutoRoutingConn()) { + // Do nothing + return; + } + makePathInvalid(); + + _updateEndPoints(); + if (processTransaction) { + _connRef->router()->processTransaction(); + } + return; +} + +bool SPConnEndPair::reroutePathFromLibavoid() +{ + if (!_connRef || !isAutoRoutingConn()) { + // Do nothing + return false; + } + + auto curve = createCurve(_connRef, _connCurvature); + + auto doc2item = _path->i2doc_affine().inverse(); + curve.transform(doc2item); + + _path->setCurve(std::move(curve)); + + return true; +} + +/* + 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 : diff --git a/src/object/sp-conn-end-pair.h b/src/object/sp-conn-end-pair.h new file mode 100644 index 0000000..64881bc --- /dev/null +++ b/src/object/sp-conn-end-pair.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CONN_END_PAIR +#define SEEN_SP_CONN_END_PAIR + +/* + * A class for handling connector endpoint movement and libavoid interaction. + * + * Authors: + * Peter Moulder <pmoulder@mail.csse.monash.edu.au> + * + * * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "3rdparty/adaptagrams/libavoid/connector.h" +#include "attributes.h" + + +class SPConnEnd; +class SPCurve; +class SPPath; +class SPItem; +class SPObject; + +namespace Geom { class Point; } +namespace Inkscape { +namespace XML { +class Node; +} +} + +class SPConnEndPair { +public: + SPConnEndPair(SPPath *); + ~SPConnEndPair(); + void release(); + void setAttr(const SPAttr key, char const *const value); + void writeRepr(Inkscape::XML::Node *const repr) const; + void getAttachedItems(SPItem *[2]) const; + void getEndpoints(Geom::Point endPts[]) const; + double getCurvature() const; + SPConnEnd **getConnEnds(); + bool isOrthogonal() const; + static SPCurve createCurve(Avoid::ConnRef *connRef, double curvature); + void tellLibavoidNewEndpoints(bool const processTransaction = false); + bool reroutePathFromLibavoid(); + void makePathInvalid(); + void update(); + bool isAutoRoutingConn() const; + void rerouteFromManipulation(); + +private: + void _updateEndPoints(); + + SPConnEnd *_connEnd[2]; + + SPPath *_path; + + // libavoid's internal representation of the item. + Avoid::ConnRef *_connRef; + + int _connType; + double _connCurvature; + + // A sigc connection for transformed signal. + sigc::connection _transformed_connection; +}; + + +void sp_conn_end_pair_build(SPObject *object); + + +// _connType options: +enum { + SP_CONNECTOR_NOAVOID, // Basic connector - a straight line. + SP_CONNECTOR_POLYLINE, // Object avoiding polyline. + SP_CONNECTOR_ORTHOGONAL // Object avoiding orthogonal polyline (only horizontal and vertical segments). +}; + +#endif /* !SEEN_SP_CONN_END_PAIR */ + +/* + 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 : diff --git a/src/object/sp-conn-end.cpp b/src/object/sp-conn-end.cpp new file mode 100644 index 0000000..2958251 --- /dev/null +++ b/src/object/sp-conn-end.cpp @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-conn-end.h" + +#include <cstring> +#include <string> +#include <limits> + +#include "bad-uri-exception.h" +#include "display/curve.h" +#include "xml/repr.h" +#include "sp-path.h" +#include "uri.h" +#include "document.h" +#include "sp-item-group.h" +#include "2geom/path-intersection.h" + + +static void change_endpts(SPPath *path, double endPos[2]); + +SPConnEnd::SPConnEnd(SPObject *const owner) + : ref(owner) + , sub_ref(owner) + , href(nullptr) + , sub_href(nullptr) + // Default to center connection endpoint + , _changed_connection() + , _delete_connection() + , _transformed_connection() +{ +} + +static SPObject const *get_nearest_common_ancestor(SPObject const *const obj, SPItem const *const objs[2]) +{ + SPObject const *anc_sofar = obj; + for (unsigned i = 0; i < 2; ++i) { + if ( objs[i] != nullptr ) { + anc_sofar = anc_sofar->nearestCommonAncestor(objs[i]); + } + } + return anc_sofar; +} + + +static bool try_get_intersect_point_with_item_recursive(Geom::PathVector& conn_pv, SPItem* item, + const Geom::Affine& item_transform, double& intersect_pos) +{ + double initial_pos = intersect_pos; + // if this is a group... + if (is<SPGroup>(item)) { + auto group = cast<SPGroup>(item); + + // consider all first-order children + double child_pos = 0.0; + std::vector<SPItem*> g = group->item_list(); + for (auto child_item : g) { + try_get_intersect_point_with_item_recursive(conn_pv, child_item, + item_transform * child_item->transform, child_pos); + if (intersect_pos < child_pos) + intersect_pos = child_pos; + } + return intersect_pos != initial_pos; + } + + // if this is not a shape, nothing to be done + auto shape = cast<SPShape>(item); + if (!shape) + return false; + + // make sure it has an associated curve + if (!shape->curve()) return false; + + // apply transformations (up to common ancestor) + auto const curve_pv = shape->curve()->get_pathvector() * item_transform; + Geom::CrossingSet cross = crossings(conn_pv, curve_pv); + // iterate over all Crossings + //TODO: check correctness of the following code: inner loop uses loop variable + // with a name identical to the loop variable of the outer loop. Then rename. + for (const auto & cr : cross) { + for (const auto & cr_pt : cr) { + if ( intersect_pos < cr_pt.ta) + intersect_pos = cr_pt.ta; + } + } + + return intersect_pos != initial_pos; +} + + +// This function returns the outermost intersection point between the path (a connector) +// and the item given. If the item is a group, then the component items are considered. +// The transforms given should be to a common ancestor of both the path and item. +// +static bool try_get_intersect_point_with_item(SPPath* conn, SPItem* item, + const Geom::Affine& item_transform, const Geom::Affine& conn_transform, + const bool at_start, double& intersect_pos) +{ + // Copy the curve and apply transformations up to common ancestor. + auto conn_pv = conn->curve()->get_pathvector() * conn_transform; + + // If this is not the starting point, use Geom::Path::reverse() to reverse the path + if (!at_start) { + // connectors are actually a single path, so consider the first element from a Geom::PathVector + conn_pv[0] = conn_pv[0].reversed(); + } + + // We start with the intersection point at the beginning of the path + intersect_pos = 0.0; + + // Find the intersection. + bool result = try_get_intersect_point_with_item_recursive(conn_pv, item, item_transform, intersect_pos); + + if (!result) { + // No intersection point has been found (why?) + // just default to connector end + intersect_pos = 0; + } + // If not at the starting point, recompute position with respect to original path + if (!at_start) { + intersect_pos = conn_pv[0].size() - intersect_pos; + } + + return result; +} + + +static void sp_conn_get_route_and_redraw(SPPath *const path, const bool updatePathRepr = true) +{ + // Get the new route around obstacles. + bool rerouted = path->connEndPair.reroutePathFromLibavoid(); + if (!rerouted) { + return; + } + + SPItem *h2attItem[2] = {nullptr}; + path->connEndPair.getAttachedItems(h2attItem); + + SPObject const *const ancestor = get_nearest_common_ancestor(path, h2attItem); + Geom::Affine const path2anc(i2anc_affine(path, ancestor)); + + // Set sensible values in case there the connector ends are not + // attached to any shapes. + Geom::PathVector conn_pv = path->curve()->get_pathvector(); + double endPos[2] = { 0.0, static_cast<double>(conn_pv[0].size()) }; + + for (unsigned h = 0; h < 2; ++h) { + // Assume center point for all + if (h2attItem[h]) { + Geom::Affine h2i2anc = i2anc_affine(h2attItem[h], ancestor); + try_get_intersect_point_with_item(path, h2attItem[h], h2i2anc, path2anc, + (h == 0), endPos[h]); + } + } + change_endpts(path, endPos); + if (updatePathRepr) { + path->updateRepr(); + path->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +static void sp_conn_end_shape_modified(SPPath *path) +{ + if (path->connEndPair.isAutoRoutingConn()) { + path->connEndPair.tellLibavoidNewEndpoints(); + } +} + +void sp_conn_reroute_path(SPPath *const path) +{ + if (path->connEndPair.isAutoRoutingConn()) { + path->connEndPair.tellLibavoidNewEndpoints(); + } +} + + +void sp_conn_reroute_path_immediate(SPPath *const path) +{ + if (path->connEndPair.isAutoRoutingConn()) { + path->connEndPair.tellLibavoidNewEndpoints(true); + } + // Don't update the path repr or else connector dragging is slowed by + // constant update of values to the xml editor, and each step is also + // needlessly remembered by undo/redo. + sp_conn_get_route_and_redraw(path, false); +} + +void sp_conn_redraw_path(SPPath *const path) +{ + sp_conn_get_route_and_redraw(path); +} + + +static void change_endpts(SPPath *path, double endPos[2]) +{ + // Use Geom::Path::portion to cut the curve at the end positions + if (endPos[0] > endPos[1]) { + // Path is "negative", reset the curve and return + path->setCurve({}); + return; + } + const Geom::Path& old_path = path->curve()->get_pathvector()[0]; + Geom::PathVector new_path_vector; + new_path_vector.push_back(old_path.portion(endPos[0], endPos[1])); + path->setCurve(SPCurve(std::move(new_path_vector))); +} + +static void sp_conn_end_deleted(SPObject *, SPObject *const owner, unsigned const handle_ix) +{ + char const * const attrs[] = { + "inkscape:connection-start", "inkscape:connection-end"}; + owner->removeAttribute(attrs[handle_ix]); + + char const * const point_attrs[] = { + "inkscape:connection-start-point", "inkscape:connection-end-point"}; + owner->removeAttribute(point_attrs[handle_ix]); + /* I believe this will trigger sp_conn_end_href_changed. */ +} + +void sp_conn_end_detach(SPObject *const owner, unsigned const handle_ix) +{ + sp_conn_end_deleted(nullptr, owner, handle_ix); +} + +void SPConnEnd::setAttacherHref(gchar const *value) +{ + if (g_strcmp0(value, href) != 0) { + g_free(href); + href = g_strdup(value); + if (!ref.try_attach(value)) { + g_free(href); + href = nullptr; + } + } +} + +void SPConnEnd::setAttacherSubHref(gchar const *value) +{ + // TODO This could check the URI object is actually a sub-object + // of the set href. It should be done here and in setAttacherHref + if (g_strcmp0(value, sub_href) != 0) { + g_free(sub_href); + sub_href = g_strdup(value); + if (!sub_ref.try_attach(value)) { + g_free(sub_href); + sub_href = nullptr; + } + } +} + +void sp_conn_end_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, SPConnEnd *connEnd, SPPath *path, unsigned handle_ix) +{ + if (!connEnd) return; + connEnd->_delete_connection.disconnect(); + connEnd->_transformed_connection.disconnect(); + + if (connEnd->href) { + if (auto refobj = connEnd->ref.getObject()) { + connEnd->_delete_connection = refobj->connectDelete(sigc::bind(sigc::ptr_fun(&sp_conn_end_deleted), path, handle_ix)); + connEnd->_transformed_connection = refobj->connectModified([path] (SPObject *, unsigned) { + sp_conn_end_shape_modified(path); + }); + } + } +} + +/* + 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 : diff --git a/src/object/sp-conn-end.h b/src/object/sp-conn-end.h new file mode 100644 index 0000000..5c6a707 --- /dev/null +++ b/src/object/sp-conn-end.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_CONN_END +#define SEEN_SP_CONN_END + +#include <cstddef> +#include <sigc++/connection.h> + +#include "sp-use-reference.h" +#include "conn-avoid-ref.h" + +class SPPath; + +class SPConnEnd { +public: + SPConnEnd(SPObject *owner); + + /* + * Ref points to the main object the connection end is attached to + * while sub_ref points to a sub-object that may be inside a symbol + * or clone object id. Sub_ref is optional. + */ + SPUseReference ref; + SPUseReference sub_ref; + char *href = nullptr; + char *sub_href = nullptr; + + /** Change of href string (not a modification of the attributes of the referrent). */ + sigc::connection _changed_connection; + + /** Called when the attached object gets deleted. */ + sigc::connection _delete_connection; + + /** A sigc connection for transformed signal, used to do move compensation. */ + sigc::connection _transformed_connection; + + void setAttacherHref(char const * value); + void setAttacherSubHref(char const * value); + + +private: + SPConnEnd(SPConnEnd const &) = delete; // no copy + SPConnEnd &operator=(SPConnEnd const &) = delete; // no assign +}; + +void sp_conn_end_href_changed(SPObject *old_ref, SPObject *ref, + SPConnEnd *connEnd, SPPath *path, unsigned const handle_ix); +void sp_conn_reroute_path(SPPath *const path); +void sp_conn_reroute_path_immediate(SPPath *const path); +void sp_conn_redraw_path(SPPath *const path); +void sp_conn_end_detach(SPObject *const owner, unsigned const handle_ix); + +#endif /* !SEEN_SP_CONN_END */ + +/* + 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 : diff --git a/src/object/sp-defs.cpp b/src/object/sp-defs.cpp new file mode 100644 index 0000000..f7de6c7 --- /dev/null +++ b/src/object/sp-defs.cpp @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <defs> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2000-2002 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * fixme: We should really check childrens validity - currently everything + * flips in + */ + +#include "sp-defs.h" +#include "xml/repr.h" +#include "document.h" +#include "attributes.h" + +SPDefs::SPDefs() : SPObject() { +} + +SPDefs::~SPDefs() = default; + +void SPDefs::build(SPDocument* doc, Inkscape::XML::Node* repr) { + this->readAttr(SPAttr::STYLE); + SPObject::build(doc, repr); +} + +void SPDefs::release() { + SPObject::release(); +} + +void SPDefs::update(SPCtx *ctx, guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector<SPObject*> l(this->childList(true)); + for(auto child : l){ + if (flags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->updateDisplay(ctx, flags); + } + sp_object_unref(child); + } +} + +void SPDefs::modified(unsigned int flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector<SPObject *> l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + 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* SPDefs::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + + if (!repr) { + repr = xml_doc->createElement("svg:defs"); + } + + std::vector<Inkscape::XML::Node *> l; + for (auto& child: children) { + 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) { + child.updateRepr(flags); + } + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* + 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 : diff --git a/src/object/sp-defs.h b/src/object/sp-defs.h new file mode 100644 index 0000000..2b2ec4f --- /dev/null +++ b/src/object/sp-defs.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_DEFS_H +#define SEEN_SP_DEFS_H + +/* + * SVG <defs> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 2000-2002 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +class SPDefs final : public SPObject { +public: + SPDefs(); + ~SPDefs() override; + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif // !SEEN_SP_DEFS_H + +/* + 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 : diff --git a/src/object/sp-desc.cpp b/src/object/sp-desc.cpp new file mode 100644 index 0000000..3b739c4 --- /dev/null +++ b/src/object/sp-desc.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <desc> implementation + * + * Authors: + * Jeff Schiller <codedread@gmail.com> + * + * Copyright (C) 2008 Jeff Schiller + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-desc.h" +#include "xml/repr.h" + +SPDesc::SPDesc() : SPObject() { +} + +SPDesc::~SPDesc() = default; + +/** + * Writes it's settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPDesc::write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) { + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPObject::write(doc, repr, flags); + + return repr; +} diff --git a/src/object/sp-desc.h b/src/object/sp-desc.h new file mode 100644 index 0000000..baa8065 --- /dev/null +++ b/src/object/sp-desc.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_DESC_H +#define SEEN_SP_DESC_H + +/* + * SVG <desc> implementation + * + * Authors: + * Jeff Schiller <codedread@gmail.com> + * + * Copyright (C) 2008 Jeff Schiller + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +class SPDesc final : public SPObject { +public: + SPDesc(); + ~SPDesc() override; + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif diff --git a/src/object/sp-dimensions.cpp b/src/object/sp-dimensions.cpp new file mode 100644 index 0000000..b5d8c47 --- /dev/null +++ b/src/object/sp-dimensions.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG dimensions implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Edward Flick (EAF) + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2005 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-dimensions.h" +#include "sp-item.h" +#include "svg/svg.h" + +/** + * Update computed x/y/width/height for "percent" units and/or from its + * referencing clone parent. + * + * @param assign_to_set Set `_set` to true for x/y/width/height. + * @param use If not NULL, then overwrite computed width and height from there. + */ +void SPDimensions::calcDimsFromParentViewport(const SPItemCtx *ictx, bool assign_to_set, // + SPDimensions const *use) +{ +#define ASSIGN(field) { if (assign_to_set) { field._set = true; } } + + auto const *effectivewidth = &this->width; + auto const *effectiveheight = &this->height; + + if (use) { + assert(!assign_to_set); + + if (use->width._set) { + effectivewidth = &use->width; + } + + if (use->height._set) { + effectiveheight = &use->height; + } + } + + if (this->x.unit == SVGLength::PERCENT) { + ASSIGN(x); + this->x.computed = this->x.value * ictx->viewport.width(); + } + + if (this->y.unit == SVGLength::PERCENT) { + ASSIGN(y); + this->y.computed = this->y.value * ictx->viewport.height(); + } + + if (effectivewidth->unit == SVGLength::PERCENT) { + ASSIGN(width); + this->width.computed = effectivewidth->value * ictx->viewport.width(); + } else { + this->width.computed = effectivewidth->computed; + } + + if (effectiveheight->unit == SVGLength::PERCENT) { + ASSIGN(height); + this->height.computed = effectiveheight->value * ictx->viewport.height(); + } else { + this->height.computed = effectiveheight->computed; + } +} + +/** + * Write the geometric properties (x/y/width/height) to XML attributes, if they are set. + */ +void SPDimensions::writeDimensions(Inkscape::XML::Node *repr) const +{ + if (x._set) { + repr->setAttribute("x", sp_svg_length_write_with_units(x)); + } + if (y._set) { + repr->setAttribute("y", sp_svg_length_write_with_units(y)); + } + if (width._set) { + repr->setAttribute("width", sp_svg_length_write_with_units(width)); + } + if (height._set) { + repr->setAttribute("height", sp_svg_length_write_with_units(height)); + } +} + +/* + 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 : diff --git a/src/object/sp-dimensions.h b/src/object/sp-dimensions.h new file mode 100644 index 0000000..337b6d1 --- /dev/null +++ b/src/object/sp-dimensions.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_DIMENSIONS_H__ +#define SP_DIMENSIONS_H__ + +/* + * dimensions helper class, common code used by root, image and others + * + * Authors: + * Shlomi Fish + * Copyright (C) 2017 Shlomi Fish, authors + * + * Released under dual Expat and GNU GPL, read the file 'COPYING' for more information + * + */ + +#include "svg/svg-length.h" + +namespace Inkscape::XML { +class Node; +} // namespace Inkscape::XML + +class SPItemCtx; + +class SPDimensions { + +public: + SVGLength x; + SVGLength y; + SVGLength width; + SVGLength height; + void calcDimsFromParentViewport(const SPItemCtx *ictx, bool assign_to_set = false, + SPDimensions const *use = nullptr); + void writeDimensions(Inkscape::XML::Node *) const; +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-ellipse.cpp b/src/object/sp-ellipse.cpp new file mode 100644 index 0000000..7f97d58 --- /dev/null +++ b/src/object/sp-ellipse.cpp @@ -0,0 +1,764 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <ellipse> and related implementations + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Mitsuru Oka + * bulia byak <buliabyak@users.sf.net> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2013 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm.h> +#include <glibmm/i18n.h> + +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" + +#include <2geom/angle.h> +#include <2geom/circle.h> +#include <2geom/ellipse.h> +#include <2geom/path-sink.h> + +#include "attributes.h" +#include "display/curve.h" +#include "document.h" +#include "preferences.h" +#include "snap-candidate.h" +#include "sp-ellipse.h" +#include "style.h" +#include "svg/svg.h" +#include "svg/path-string.h" + +#define SP_2PI (2 * M_PI) + +SPGenericEllipse::SPGenericEllipse() + : SPShape() + , start(0) + , end(SP_2PI) + , type(SP_GENERIC_ELLIPSE_UNDEFINED) + , arc_type(SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE) +{ +} + +SPGenericEllipse::~SPGenericEllipse() += default; + +/* + * Ellipse and rect is the only SP object who's repr element tag name changes + * during it's lifetime. During undo and redo these changes can cause + * the SP object to become unstuck from the repr's true state. + */ +void SPGenericEllipse::tag_name_changed(gchar const* oldname, gchar const* newname) +{ + const std::string typeString = newname; + if (typeString == "svg:circle") { + type = SP_GENERIC_ELLIPSE_CIRCLE; + } else if (typeString == "svg:ellipse") { + type = SP_GENERIC_ELLIPSE_ELLIPSE; + } else if (typeString == "svg:path") { + type = SP_GENERIC_ELLIPSE_ARC; + } else { + type = SP_GENERIC_ELLIPSE_UNDEFINED; + } +} + +void SPGenericEllipse::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + // std::cout << "SPGenericEllipse::build: Entrance: " << this->type + // << " (" << g_quark_to_string(repr->code()) << ")" << std::endl; + + switch ( type ) { + case SP_GENERIC_ELLIPSE_ARC: + this->readAttr(SPAttr::SODIPODI_CX); + this->readAttr(SPAttr::SODIPODI_CY); + this->readAttr(SPAttr::SODIPODI_RX); + this->readAttr(SPAttr::SODIPODI_RY); + this->readAttr(SPAttr::SODIPODI_START); + this->readAttr(SPAttr::SODIPODI_END); + this->readAttr(SPAttr::SODIPODI_OPEN); + this->readAttr(SPAttr::SODIPODI_ARC_TYPE); + break; + + case SP_GENERIC_ELLIPSE_CIRCLE: + this->readAttr(SPAttr::CX); + this->readAttr(SPAttr::CY); + this->readAttr(SPAttr::R); + break; + + case SP_GENERIC_ELLIPSE_ELLIPSE: + this->readAttr(SPAttr::CX); + this->readAttr(SPAttr::CY); + this->readAttr(SPAttr::RX); + this->readAttr(SPAttr::RY); + break; + + default: + std::cerr << "SPGenericEllipse::build() unknown defined type." << std::endl; + } + + // std::cout << " cx: " << cx.write() << std::endl; + // std::cout << " cy: " << cy.write() << std::endl; + // std::cout << " rx: " << rx.write() << std::endl; + // std::cout << " ry: " << ry.write() << std::endl; + SPShape::build(document, repr); +} + +void SPGenericEllipse::set(SPAttr key, gchar const *value) +{ + // There are multiple ways to set internal cx, cy, rx, and ry (via SVG attributes or Sodipodi + // attributes) thus we don't want to unset them if a read fails (e.g., when we explicitly clear + // an attribute by setting it to NULL). + + // We must update the SVGLengths immediately or nodes may be misplaced after they are moved. + double const w = viewport.width(); + double const h = viewport.height(); + double const d = hypot(w, h) / sqrt(2); // diagonal + double const em = style->font_size.computed; + double const ex = em * 0.5; + + SVGLength t; + switch (key) { + case SPAttr::CX: + case SPAttr::SODIPODI_CX: + if( t.read(value) ) cx = t; + cx.update( em, ex, w ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::CY: + case SPAttr::SODIPODI_CY: + if( t.read(value) ) cy = t; + cy.update( em, ex, h ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::RX: + case SPAttr::SODIPODI_RX: + if( t.read(value) && t.value > 0.0 ) rx = t; + rx.update( em, ex, w ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::RY: + case SPAttr::SODIPODI_RY: + if( t.read(value) && t.value > 0.0 ) ry = t; + ry.update( em, ex, h ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::R: + if( t.read(value) && t.value > 0.0 ) { + this->ry = this->rx = t; + } + rx.update( em, ex, d ); + ry.update( em, ex, d ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_START: + if (value) { + sp_svg_number_read_d(value, &this->start); + } else { + this->start = 0; + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_END: + if (value) { + sp_svg_number_read_d(value, &this->end); + } else { + this->end = 2 * M_PI; + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_OPEN: + // This is for reading in old files. + if ((!value) || strcmp(value,"true")) { + // We rely on this to reset arc_type when changing an arc to + // an ellipse/circle, so it is drawn as a closed path. + // A clone will not even change it's this->type + this->arc_type = SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE; + } else { + this->arc_type = SP_GENERIC_ELLIPSE_ARC_TYPE_ARC; + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_ARC_TYPE: + // To read in old files that use 'open', we need to not set if value is null. + // We could also check inkscape version. + if (value) { + if (!strcmp(value,"arc")) { + this->arc_type = SP_GENERIC_ELLIPSE_ARC_TYPE_ARC; + } else if (!strcmp(value,"chord")) { + this->arc_type = SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD; + } else { + this->arc_type = SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE; + } + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPShape::set(key, value); + break; + } +} + +void SPGenericEllipse::update(SPCtx *ctx, guint flags) +{ + // std::cout << "\nSPGenericEllipse::update: Entrance" << std::endl; + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + Geom::Rect const &viewbox = ((SPItemCtx const *) ctx)->viewport; + + double const dx = viewbox.width(); + double const dy = viewbox.height(); + double const dr = hypot(dx, dy) / sqrt(2); + double const em = this->style->font_size.computed; + double const ex = em * 0.5; // fixme: get from pango or libnrtype + + this->cx.update(em, ex, dx); + this->cy.update(em, ex, dy); + this->rx.update(em, ex, dr); + this->ry.update(em, ex, dr); + + this->set_shape(); + } + + SPShape::update(ctx, flags); + // std::cout << "SPGenericEllipse::update: Exit\n" << std::endl; +} + +Inkscape::XML::Node *SPGenericEllipse::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + // std::cout << "\nSPGenericEllipse::write: Entrance (" + // << (repr == NULL ? " NULL" : g_quark_to_string(repr->code())) + // << ")" << std::endl; + + GenericEllipseType new_type = SP_GENERIC_ELLIPSE_UNDEFINED; + if (_isSlice() || hasPathEffectOnClipOrMaskRecursive(this) ) { + new_type = SP_GENERIC_ELLIPSE_ARC; + } else if ( rx.computed == ry.computed ) { + new_type = SP_GENERIC_ELLIPSE_CIRCLE; + } else { + new_type = SP_GENERIC_ELLIPSE_ELLIPSE; + } + // std::cout << " new_type: " << new_type << std::endl; + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + + switch ( new_type ) { + + case SP_GENERIC_ELLIPSE_ARC: + repr = xml_doc->createElement("svg:path"); + break; + case SP_GENERIC_ELLIPSE_CIRCLE: + repr = xml_doc->createElement("svg:circle"); + break; + case SP_GENERIC_ELLIPSE_ELLIPSE: + repr = xml_doc->createElement("svg:ellipse"); + break; + case SP_GENERIC_ELLIPSE_UNDEFINED: + default: + std::cerr << "SPGenericEllipse::write(): unknown type." << std::endl; + } + } + + if (type != new_type) { + switch (new_type) { + case SP_GENERIC_ELLIPSE_ARC: + repr->setCodeUnsafe(g_quark_from_string("svg:path")); + break; + case SP_GENERIC_ELLIPSE_CIRCLE: + repr->setCodeUnsafe(g_quark_from_string("svg:circle")); + break; + case SP_GENERIC_ELLIPSE_ELLIPSE: + repr->setCodeUnsafe(g_quark_from_string("svg:ellipse")); + break; + default: + std::cerr << "SPGenericEllipse::write(): unknown type." << std::endl; + } + type = new_type; + } + + // std::cout << " type: " << g_quark_to_string( repr->code() ) << std::endl; + // std::cout << " cx: " << cx.write() << " " << cx.computed + // << " cy: " << cy.write() << " " << cy.computed + // << " rx: " << rx.write() << " " << rx.computed + // << " ry: " << ry.write() << " " << ry.computed << std::endl; + + switch ( type ) { + case SP_GENERIC_ELLIPSE_UNDEFINED: + case SP_GENERIC_ELLIPSE_ARC: + + repr->removeAttribute("cx"); + repr->removeAttribute("cy"); + repr->removeAttribute("rx"); + repr->removeAttribute("ry"); + repr->removeAttribute("r"); + + if (flags & SP_OBJECT_WRITE_EXT) { + + repr->setAttribute("sodipodi:type", "arc"); + repr->setAttributeSvgLength("sodipodi:cx", cx); + repr->setAttributeSvgLength("sodipodi:cy", cy); + repr->setAttributeSvgLength("sodipodi:rx", rx); + repr->setAttributeSvgLength("sodipodi:ry", ry); + + // write start and end only if they are non-trivial; otherwise remove + if (_isSlice()) { + repr->setAttributeSvgDouble("sodipodi:start", start); + repr->setAttributeSvgDouble("sodipodi:end", end); + + switch ( arc_type ) { + case SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE: + repr->removeAttribute("sodipodi:open"); // For backwards compat. + repr->setAttribute("sodipodi:arc-type", "slice"); + break; + case SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD: + // A chord's path isn't "open" but its fill most closely resembles an arc. + repr->setAttribute("sodipodi:open", "true"); // For backwards compat. + repr->setAttribute("sodipodi:arc-type", "chord"); + break; + case SP_GENERIC_ELLIPSE_ARC_TYPE_ARC: + repr->setAttribute("sodipodi:open", "true"); // For backwards compat. + repr->setAttribute("sodipodi:arc-type", "arc"); + break; + default: + std::cerr << "SPGenericEllipse::write: unknown arc-type." << std::endl; + } + } else { + repr->removeAttribute("sodipodi:end"); + repr->removeAttribute("sodipodi:start"); + repr->removeAttribute("sodipodi:open"); + repr->removeAttribute("sodipodi:arc-type"); + } + } + + // write d= + set_elliptical_path_attribute(repr); + break; + + case SP_GENERIC_ELLIPSE_CIRCLE: + repr->setAttributeSvgLength("cx", cx); + repr->setAttributeSvgLength("cy", cy); + repr->setAttributeSvgLength("r", rx); + repr->removeAttribute("rx"); + repr->removeAttribute("ry"); + repr->removeAttribute("sodipodi:cx"); + repr->removeAttribute("sodipodi:cy"); + repr->removeAttribute("sodipodi:rx"); + repr->removeAttribute("sodipodi:ry"); + repr->removeAttribute("sodipodi:end"); + repr->removeAttribute("sodipodi:start"); + repr->removeAttribute("sodipodi:open"); + repr->removeAttribute("sodipodi:arc-type"); + repr->removeAttribute("sodipodi:type"); + repr->removeAttribute("d"); + break; + + case SP_GENERIC_ELLIPSE_ELLIPSE: + repr->setAttributeSvgLength("cx", cx); + repr->setAttributeSvgLength("cy", cy); + repr->setAttributeSvgLength("rx", rx); + repr->setAttributeSvgLength("ry", ry); + repr->removeAttribute("r"); + repr->removeAttribute("sodipodi:cx"); + repr->removeAttribute("sodipodi:cy"); + repr->removeAttribute("sodipodi:rx"); + repr->removeAttribute("sodipodi:ry"); + repr->removeAttribute("sodipodi:end"); + repr->removeAttribute("sodipodi:start"); + repr->removeAttribute("sodipodi:open"); + repr->removeAttribute("sodipodi:arc-type"); + repr->removeAttribute("sodipodi:type"); + repr->removeAttribute("d"); + break; + + default: + std::cerr << "SPGenericEllipse::write: unknown type." << std::endl; + } + + set_shape(); // evaluate SPCurve + + SPShape::write(xml_doc, repr, flags); + + return repr; +} + +const char *SPGenericEllipse::typeName() const +{ + switch (type) { + case SP_GENERIC_ELLIPSE_UNDEFINED: + case SP_GENERIC_ELLIPSE_ARC: + return "arc"; + case SP_GENERIC_ELLIPSE_CIRCLE: + case SP_GENERIC_ELLIPSE_ELLIPSE: + default: + return "circle"; // + } +} + +const char *SPGenericEllipse::displayName() const +{ + switch ( type ) { + case SP_GENERIC_ELLIPSE_UNDEFINED: + case SP_GENERIC_ELLIPSE_ARC: + if (_isSlice()) { + switch ( arc_type ) { + case SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE: + return _("Slice"); + break; + case SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD: + return _("Chord"); + break; + case SP_GENERIC_ELLIPSE_ARC_TYPE_ARC: + return _("Arc"); + break; + } + } // fallback to ellipse + case SP_GENERIC_ELLIPSE_ELLIPSE: + return _("Ellipse"); + case SP_GENERIC_ELLIPSE_CIRCLE: + return _("Circle"); + default: + return "Unknown ellipse: ERROR"; + } +} + +// Create path for rendering shape on screen +void SPGenericEllipse::set_shape() +{ + // std::cout << "SPGenericEllipse::set_shape: Entrance" << std::endl; + if (checkBrokenPathEffect()) { + return; + } + if (Geom::are_near(this->rx.computed, 0) || Geom::are_near(this->ry.computed, 0)) { + return; + } + + this->normalize(); + + // For simplicity, we use a circle with center (0, 0) and radius 1 for our calculations. + Geom::Circle circle(0, 0, 1); + + if (!this->_isSlice()) { + start = 0.0; + end = 2.0*M_PI; + } + double incr = end - start; // arc angle + if (incr < 0.0) incr += 2.0*M_PI; + + int numsegs = 1 + int(incr*2.0/M_PI); // number of arc segments + if (numsegs > 4) numsegs = 4; + + incr = incr/numsegs; // limit arc angle to less than 90 degrees + Geom::Path path(Geom::Point::polar(start)); + Geom::EllipticalArc* arc; + for (int seg = 0; seg < numsegs; seg++) { + arc = circle.arc(Geom::Point::polar(start + seg*incr), Geom::Point::polar(start + (seg + 0.5)*incr), Geom::Point::polar(start + (seg + 1.0)*incr)); + path.append(*arc); + delete arc; + } + Geom::PathBuilder pb; + pb.append(path); + if (this->_isSlice() && this->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE) { + pb.lineTo(Geom::Point(0, 0)); + } + + if (this->arc_type != SP_GENERIC_ELLIPSE_ARC_TYPE_ARC) { + pb.closePath(); + } else { + pb.flush(); + } + + auto c = SPCurve(pb.peek()); + + // gchar *str = sp_svg_write_path(curve->get_pathvector()); + // std::cout << " path: " << str << std::endl; + // g_free(str); + + // Stretching / moving the calculated shape to fit the actual dimensions. + Geom::Affine aff = Geom::Scale(rx.computed, ry.computed) * Geom::Translate(cx.computed, cy.computed); + c.transform(aff); + prepareShapeForLPE(&c); +} + +Geom::Affine SPGenericEllipse::set_transform(Geom::Affine const &xform) +{ + if (pathEffectsEnabled() && !optimizeTransforms()) { + return xform; + } + + /* Calculate ellipse start in parent coords. */ + Geom::Point pos(Geom::Point(this->cx.computed, this->cy.computed) * xform); + + /* This function takes care of translation and scaling, we return whatever parts we can't + handle. */ + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + gdouble const sw = hypot(ret[0], ret[1]); + gdouble const sh = hypot(ret[2], ret[3]); + + if (sw > 1e-9) { + ret[0] /= sw; + ret[1] /= sw; + } else { + ret[0] = 1.0; + ret[1] = 0.0; + } + + if (sh > 1e-9) { + ret[2] /= sh; + ret[3] /= sh; + } else { + ret[2] = 0.0; + ret[3] = 1.0; + } + + if (this->rx._set) { + this->rx.scale( sw ); + } + + if (this->ry._set) { + this->ry.scale( sh ); + } + + /* Find start in item coords */ + pos = pos * ret.inverse(); + this->cx = pos[Geom::X]; + this->cy = pos[Geom::Y]; + + this->set_shape(); + + // Adjust stroke width + if (!g_strcmp0(getAttribute("sodipodi:arc-type"), "slice") || + !g_strcmp0(getAttribute("sodipodi:arc-type"), "chord") || + !g_strcmp0(getAttribute("sodipodi:arc-type"), "arc")) + { + double const expansion = transform.descrim(); + adjust_stroke_width_recursive(expansion); + } + this->adjust_stroke(sqrt(fabs(sw * sh))); + + // Adjust pattern fill + this->adjust_pattern(xform * ret.inverse()); + + // Adjust gradient fill + this->adjust_gradient(xform * ret.inverse()); + + return ret; +} + +void SPGenericEllipse::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const +{ + // CPPIFY: is this call necessary? + const_cast<SPGenericEllipse*>(this)->normalize(); + + Geom::Affine const i2dt = this->i2dt_affine(); + + // Snap to the 4 quadrant points of the ellipse, but only if the arc + // spans far enough to include them + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_ELLIPSE_QUADRANT_POINT)) { + for (double angle = 0; angle < SP_2PI; angle += M_PI_2) { + if (Geom::AngleInterval(this->start, this->end, true).contains(angle)) { + Geom::Point pt = this->getPointAtAngle(angle) * i2dt; + p.emplace_back(pt, Inkscape::SNAPSOURCE_ELLIPSE_QUADRANT_POINT, Inkscape::SNAPTARGET_ELLIPSE_QUADRANT_POINT); + } + } + } + + double cx = this->cx.computed; + double cy = this->cy.computed; + + + bool slice = this->_isSlice(); + + // Add the centre, if we have a closed slice or when explicitly asked for + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_NODE_CUSP) && slice && + this->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE) { + Geom::Point pt = Geom::Point(cx, cy) * i2dt; + p.emplace_back(pt, Inkscape::SNAPSOURCE_NODE_CUSP, Inkscape::SNAPTARGET_NODE_CUSP); + } + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT)) { + Geom::Point pt = Geom::Point(cx, cy) * i2dt; + p.emplace_back(pt, Inkscape::SNAPSOURCE_OBJECT_MIDPOINT, Inkscape::SNAPTARGET_OBJECT_MIDPOINT); + } + + // And if we have a slice, also snap to the endpoints + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_NODE_CUSP) && slice) { + // Add the start point, if it's not coincident with a quadrant point + if (!Geom::are_near(std::fmod(this->start, M_PI_2), 0)) { + Geom::Point pt = this->getPointAtAngle(this->start) * i2dt; + p.emplace_back(pt, Inkscape::SNAPSOURCE_NODE_CUSP, Inkscape::SNAPTARGET_NODE_CUSP); + } + + // Add the end point, if it's not coincident with a quadrant point + if (!Geom::are_near(std::fmod(this->end, M_PI_2), 0)) { + Geom::Point pt = this->getPointAtAngle(this->end) * i2dt; + p.emplace_back(pt, Inkscape::SNAPSOURCE_NODE_CUSP, Inkscape::SNAPTARGET_NODE_CUSP); + } + } +} + +void SPGenericEllipse::modified(guint flags) +{ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + this->set_shape(); + } + + SPShape::modified(flags); +} + +void SPGenericEllipse::update_patheffect(bool write) { + SPShape::update_patheffect(write); +} + +void SPGenericEllipse::normalize() +{ + Geom::AngleInterval a(this->start, this->end, true); + + this->start = a.initialAngle().radians0(); + this->end = a.finalAngle().radians0(); +} + +Geom::Point SPGenericEllipse::getPointAtAngle(double arg) const +{ + return Geom::Point::polar(arg) * Geom::Scale(rx.computed, ry.computed) * Geom::Translate(cx.computed, cy.computed); +} + +/* + * set_elliptical_path_attribute: + * + * Convert center to endpoint parameterization and set it to repr. + * + * See SVG 1.0 Specification W3C Recommendation + * ``F.6 Elliptical arc implementation notes'' for more detail. + */ +bool SPGenericEllipse::set_elliptical_path_attribute(Inkscape::XML::Node *repr) +{ + // Make sure our pathvector is up to date. + this->set_shape(); + + if (_curve) { + repr->setAttribute("d", sp_svg_write_path(_curve->get_pathvector())); + } else { + repr->removeAttribute("d"); + } + + return true; +} + +void SPGenericEllipse::position_set(gdouble x, gdouble y, gdouble rx, gdouble ry) +{ + this->cx = x; + this->cy = y; + this->rx = rx; + this->ry = ry; + + Inkscape::Preferences * prefs = Inkscape::Preferences::get(); + + // those pref values are in degrees, while we want radians + if (prefs->getDouble("/tools/shapes/arc/start", 0.0) != 0) { + this->start = Geom::Angle::from_degrees(prefs->getDouble("/tools/shapes/arc/start", 0.0)).radians0(); + } + + if (prefs->getDouble("/tools/shapes/arc/end", 0.0) != 0) { + this->end = Geom::Angle::from_degrees(prefs->getDouble("/tools/shapes/arc/end", 0.0)).radians0(); + } + + this->arc_type = (GenericEllipseArcType)prefs->getInt("/tools/shapes/arc/arc_type", 0); + if (this->type != SP_GENERIC_ELLIPSE_ARC && _isSlice()) { + // force an update while creating shapes, so correct rendering is shown initially + updateRepr(); + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +bool SPGenericEllipse::_isSlice() const +{ + Geom::AngleInterval a(this->start, this->end, true); + + return !(Geom::are_near(a.extent(), 0) || Geom::are_near(a.extent(), SP_2PI)); +} + +/** +Returns the ratio in which the vector from p0 to p1 is stretched by transform + */ +gdouble SPGenericEllipse::vectorStretch(Geom::Point p0, Geom::Point p1, Geom::Affine xform) { + if (p0 == p1) { + return 0; + } + + return (Geom::distance(p0 * xform, p1 * xform) / Geom::distance(p0, p1)); +} + +void SPGenericEllipse::setVisibleRx(gdouble rx) { + if (rx == 0) { + this->rx.unset(); + } else { + this->rx = rx / SPGenericEllipse::vectorStretch( + Geom::Point(this->cx.computed + 1, this->cy.computed), + Geom::Point(this->cx.computed, this->cy.computed), + this->i2doc_affine()); + } + + this->updateRepr(); +} + +void SPGenericEllipse::setVisibleRy(gdouble ry) { + if (ry == 0) { + this->ry.unset(); + } else { + this->ry = ry / SPGenericEllipse::vectorStretch( + Geom::Point(this->cx.computed, this->cy.computed + 1), + Geom::Point(this->cx.computed, this->cy.computed), + this->i2doc_affine()); + } + + this->updateRepr(); +} + +gdouble SPGenericEllipse::getVisibleRx() const { + if (!this->rx._set) { + return 0; + } + + return this->rx.computed * SPGenericEllipse::vectorStretch( + Geom::Point(this->cx.computed + 1, this->cy.computed), + Geom::Point(this->cx.computed, this->cy.computed), + this->i2doc_affine()); +} + +gdouble SPGenericEllipse::getVisibleRy() const { + if (!this->ry._set) { + return 0; + } + + return this->ry.computed * SPGenericEllipse::vectorStretch( + Geom::Point(this->cx.computed, this->cy.computed + 1), + Geom::Point(this->cx.computed, this->cy.computed), + this->i2doc_affine()); +} + +/* + 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 : diff --git a/src/object/sp-ellipse.h b/src/object/sp-ellipse.h new file mode 100644 index 0000000..c48022e --- /dev/null +++ b/src/object/sp-ellipse.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * SVG <ellipse> and related implementations + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Mitsuru Oka + * Tavmjong Bah + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2013 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_ELLIPSE_H +#define SEEN_SP_ELLIPSE_H + +#include "svg/svg-length.h" +#include "sp-shape.h" + +enum GenericEllipseType { + SP_GENERIC_ELLIPSE_UNDEFINED, // FIXME shouldn't exist + SP_GENERIC_ELLIPSE_ARC, + SP_GENERIC_ELLIPSE_CIRCLE, + SP_GENERIC_ELLIPSE_ELLIPSE +}; + +enum GenericEllipseArcType { + SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE, // Default + SP_GENERIC_ELLIPSE_ARC_TYPE_ARC, + SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD +}; + +class SPGenericEllipse final : public SPShape { +public: + SPGenericEllipse(); + ~SPGenericEllipse() override; + int tag() const override { return tag_of<decltype(*this)>; } + + // Regardless of type, the ellipse/circle/arc is stored + // internally with these variables. (Circle radius is rx). + SVGLength cx; + SVGLength cy; + SVGLength rx; + SVGLength ry; + + // Return slice, chord, or arc. + GenericEllipseArcType arcType() { return arc_type; }; + void setArcType(GenericEllipseArcType type) { arc_type = type; }; + + double start, end; + GenericEllipseType type; + GenericEllipseArcType arc_type; + + void tag_name_changed(gchar const* oldname, gchar const* newname) override; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + + void set(SPAttr key, char const *value) override; + void update(SPCtx *ctx, unsigned int flags) override; + + Inkscape::XML::Node *write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + const char *typeName() const override; + const char *displayName() const override; + + void set_shape() override; + void update_patheffect(bool write) override; + Geom::Affine set_transform(Geom::Affine const &xform) override; + + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + + void modified(unsigned int flags) override; + + /** + * @brief Makes sure that start and end lie between 0 and 2 * PI. + */ + void normalize(); + + Geom::Point getPointAtAngle(double arg) const; + + bool set_elliptical_path_attribute(Inkscape::XML::Node *repr); + void position_set(double x, double y, double rx, double ry); + + double getVisibleRx() const; + void setVisibleRx(double rx); + + double getVisibleRy() const; + void setVisibleRy(double ry); + + bool is_whole() const { return !_isSlice(); } + +protected: + /** + * @brief Determines whether the shape is a part of an ellipse. + */ + bool _isSlice() const; + +private: + static double vectorStretch(Geom::Point p0, Geom::Point p1, Geom::Affine xform); +}; + +#endif + +/* + 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 : diff --git a/src/object/sp-factory.cpp b/src/object/sp-factory.cpp new file mode 100644 index 0000000..ddd9afc --- /dev/null +++ b/src/object/sp-factory.cpp @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Factory for SPObject tree + * + * Authors: + * Markus Engel + * PBS <pbs3141@gmail.com> + * + * Copyright (C) 2022 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-factory.h" + +// primary +#include "box3d.h" +#include "box3d-side.h" +#include "color-profile.h" +#include "persp3d.h" +#include "sp-anchor.h" +#include "sp-clippath.h" +#include "sp-defs.h" +#include "sp-desc.h" +#include "sp-ellipse.h" +#include "sp-filter.h" +#include "sp-flowdiv.h" +#include "sp-flowregion.h" +#include "sp-flowtext.h" +#include "sp-font.h" +#include "sp-font-face.h" +#include "sp-glyph.h" +#include "sp-glyph-kerning.h" +#include "sp-grid.h" +#include "sp-guide.h" +#include "sp-hatch.h" +#include "sp-hatch-path.h" +#include "sp-image.h" +#include "sp-line.h" +#include "sp-linear-gradient.h" +#include "sp-marker.h" +#include "sp-mask.h" +#include "sp-mesh-gradient.h" +#include "sp-mesh-patch.h" +#include "sp-mesh-row.h" +#include "sp-metadata.h" +#include "sp-missing-glyph.h" +#include "sp-namedview.h" +#include "sp-offset.h" +#include "sp-page.h" +#include "sp-path.h" +#include "sp-pattern.h" +#include "sp-polyline.h" +#include "sp-radial-gradient.h" +#include "sp-rect.h" +#include "sp-root.h" +#include "sp-script.h" +#include "sp-solid-color.h" +#include "sp-spiral.h" +#include "sp-star.h" +#include "sp-stop.h" +#include "sp-string.h" +#include "sp-style-elem.h" +#include "sp-switch.h" +#include "sp-symbol.h" +#include "sp-tag.h" +#include "sp-tag-use.h" +#include "sp-text.h" +#include "sp-textpath.h" +#include "sp-title.h" +#include "sp-tref.h" +#include "sp-tspan.h" +#include "sp-use.h" +#include "live_effects/lpeobject.h" + +// filters +#include "filters/blend.h" +#include "filters/colormatrix.h" +#include "filters/componenttransfer.h" +#include "filters/componenttransfer-funcnode.h" +#include "filters/composite.h" +#include "filters/convolvematrix.h" +#include "filters/diffuselighting.h" +#include "filters/displacementmap.h" +#include "filters/distantlight.h" +#include "filters/flood.h" +#include "filters/gaussian-blur.h" +#include "filters/image.h" +#include "filters/merge.h" +#include "filters/mergenode.h" +#include "filters/morphology.h" +#include "filters/offset.h" +#include "filters/pointlight.h" +#include "filters/specularlighting.h" +#include "filters/spotlight.h" +#include "filters/tile.h" +#include "filters/turbulence.h" + +#include <unordered_map> + +namespace { + +class Factory +{ +public: + SPObject *create(std::string const &id) const + { + auto it = map.find(id); + + if (it == map.end()) { + std::cerr << "WARNING: unknown type: " << id << std::endl; + return nullptr; + } + + return it->second(); + } + + bool supportsId(std::string const &id) const + { + return map.find(id) != map.end(); + } + + static Factory const &get() + { + static Factory const singleton; + return singleton; + } + +private: + using Func = SPObject*(*)(); + + template <typename T> + static Func constexpr make = [] () -> SPObject* { return new T; }; + static Func constexpr null = [] () -> SPObject* { return nullptr; }; + + std::unordered_map<std::string, Func> const map = + { + // primary + { "inkscape:box3d", make<SPBox3D> }, + { "inkscape:box3dside", make<Box3DSide> }, + { "svg:color-profile", make<Inkscape::ColorProfile> }, + { "inkscape:persp3d", make<Persp3D> }, + { "svg:a", make<SPAnchor> }, + { "svg:clipPath", make<SPClipPath> }, + { "svg:defs", make<SPDefs> }, + { "svg:desc", make<SPDesc> }, + { "svg:ellipse", [] () -> SPObject* { + auto e = new SPGenericEllipse; + e->type = SP_GENERIC_ELLIPSE_ELLIPSE; + return e; + }}, + { "svg:circle", [] () -> SPObject* { + auto c = new SPGenericEllipse; + c->type = SP_GENERIC_ELLIPSE_CIRCLE; + return c; + }}, + { "arc", [] () -> SPObject* { + auto a = new SPGenericEllipse; + a->type = SP_GENERIC_ELLIPSE_ARC; + return a; + }}, + { "svg:filter", make<SPFilter> }, + { "svg:flowDiv", make<SPFlowdiv> }, + { "svg:flowSpan", make<SPFlowtspan> }, + { "svg:flowPara", make<SPFlowpara> }, + { "svg:flowLine", make<SPFlowline> }, + { "svg:flowRegionBreak", make<SPFlowregionbreak> }, + { "svg:flowRegion", make<SPFlowregion> }, + { "svg:flowRegionExclude", make<SPFlowregionExclude> }, + { "svg:flowRoot", make<SPFlowtext> }, + { "svg:font", make<SPFont> }, + { "svg:font-face", make<SPFontFace> }, + { "svg:glyph", make<SPGlyph> }, + { "svg:hkern", make<SPHkern> }, + { "svg:vkern", make<SPVkern> }, + { "sodipodi:guide", make<SPGuide> }, + { "inkscape:page", make<SPPage> }, + { "svg:hatch", make<SPHatch> }, + { "svg:hatchpath", make<SPHatchPath> }, + { "svg:hatchPath", [] () -> SPObject* { + std::cerr << "Warning: <hatchPath> has been renamed <hatchpath>" << std::endl; + return new SPHatchPath; + }}, + { "svg:image", make<SPImage> }, + { "svg:g", make<SPGroup> }, + { "svg:line", make<SPLine> }, + { "svg:linearGradient", make<SPLinearGradient> }, + { "svg:marker", make<SPMarker> }, + { "svg:mask", make<SPMask> }, + { "svg:mesh", [] () -> SPObject* { // SVG 2 old + std::cerr << "Warning: <mesh> has been renamed <meshgradient>." << std::endl; + std::cerr << "Warning: <mesh> has been repurposed as a shape that tightly wraps a <meshgradient>." << std::endl; + return new SPMeshGradient; + }}, + { "svg:meshGradient", [] () -> SPObject* { // SVG 2 old + std::cerr << "Warning: <meshGradient> has been renamed <meshgradient>" << std::endl; + return new SPMeshGradient; + }}, + { "svg:meshgradient", [] () -> SPObject* { // SVG 2 + return new SPMeshGradient; + }}, + { "svg:meshPatch", [] () -> SPObject* { + std::cerr << "Warning: <meshPatch> and <meshRow> have been renamed <meshpatch> and <meshrow>" << std::endl; + return new SPMeshpatch; + }}, + { "svg:meshpatch", make<SPMeshpatch> }, + { "svg:meshRow", make<SPMeshrow> }, + { "svg:meshrow", make<SPMeshrow> }, + { "svg:metadata", make<SPMetadata> }, + { "svg:missing-glyph", make<SPMissingGlyph> }, + { "sodipodi:namedview", make<SPNamedView> }, + { "inkscape:offset", make<SPOffset> }, + { "svg:path", make<SPPath> }, + { "svg:pattern", make<SPPattern> }, + { "svg:polygon", make<SPPolygon> }, + { "svg:polyline", make<SPPolyLine> }, + { "svg:radialGradient", make<SPRadialGradient> }, + { "svg:rect", make<SPRect> }, + { "rect", make<SPRect> }, // LPE rect; + { "svg:svg", make<SPRoot> }, + { "svg:script", make<SPScript> }, + { "svg:solidColor", [] () -> SPObject* { + std::cerr << "Warning: <solidColor> has been renamed <solidcolor>" << std::endl; + return new SPSolidColor; + }}, + { "svg:solidColor", [] () -> SPObject* { + std::cerr << "Warning: <solidColor> has been renamed <solidcolor>" << std::endl; + return new SPSolidColor; + }}, + { "svg:solidcolor", make<SPSolidColor> }, + { "spiral", make<SPSpiral> }, + { "star", make<SPStar> }, + { "svg:stop", make<SPStop> }, + { "string", make<SPString> }, + { "svg:style", make<SPStyleElem> }, + { "svg:switch", make<SPSwitch> }, + { "svg:symbol", make<SPSymbol> }, + { "inkscape:tag", make<SPTag> }, + { "inkscape:tagref", make<SPTagUse> }, + { "svg:text", make<SPText> }, + { "svg:title", make<SPTitle> }, + { "svg:tref", make<SPTRef> }, + { "svg:tspan", make<SPTSpan> }, + { "svg:textPath", make<SPTextPath> }, + { "svg:use", make<SPUse> }, + { "inkscape:path-effect", make<LivePathEffectObject> }, + + // filters + { "svg:feBlend", make<SPFeBlend> }, + { "svg:feColorMatrix", make<SPFeColorMatrix> }, + { "svg:feComponentTransfer", make<SPFeComponentTransfer> }, + { "svg:feFuncR", [] () -> SPObject* { return new SPFeFuncNode(SPFeFuncNode::R); }}, + { "svg:feFuncG", [] () -> SPObject* { return new SPFeFuncNode(SPFeFuncNode::G); }}, + { "svg:feFuncB", [] () -> SPObject* { return new SPFeFuncNode(SPFeFuncNode::B); }}, + { "svg:feFuncA", [] () -> SPObject* { return new SPFeFuncNode(SPFeFuncNode::A); }}, + { "svg:feComposite", make<SPFeComposite> }, + { "svg:feConvolveMatrix", make<SPFeConvolveMatrix> }, + { "svg:feDiffuseLighting", make<SPFeDiffuseLighting> }, + { "svg:feDisplacementMap", make<SPFeDisplacementMap> }, + { "svg:feDistantLight", make<SPFeDistantLight> }, + { "svg:feFlood", make<SPFeFlood> }, + { "svg:feGaussianBlur", make<SPGaussianBlur> }, + { "svg:feImage", make<SPFeImage> }, + { "svg:feMerge", make<SPFeMerge> }, + { "svg:feMergeNode", make<SPFeMergeNode> }, + { "svg:feMorphology", make<SPFeMorphology> }, + { "svg:feOffset", make<SPFeOffset> }, + { "svg:fePointLight", make<SPFePointLight> }, + { "svg:feSpecularLighting", make<SPFeSpecularLighting> }, + { "svg:feSpotLight", make<SPFeSpotLight> }, + { "svg:feTile", make<SPFeTile> }, + { "svg:feTurbulence", make<SPFeTurbulence> }, + { "inkscape:grid", make<SPGrid> }, + + // ignore + { "rdf:RDF", null }, // no SP node yet + { "inkscape:clipboard", null }, // SP node not necessary + { "inkscape:templateinfo", null }, // metadata for templates + { "inkscape:_templateinfo", null }, // metadata for templates + { "", null } // comments + }; +}; + +} // namespace + +SPObject *SPFactory::createObject(std::string const &id) +{ + return Factory::get().create(id); +} + +bool SPFactory::supportsType(std::string const &id) +{ + return Factory::get().supportsId(id); +} + +std::string NodeTraits::get_type_string(Inkscape::XML::Node const &node) +{ + switch (node.type()) { + case Inkscape::XML::NodeType::TEXT_NODE: + return "string"; + case Inkscape::XML::NodeType::ELEMENT_NODE: + if (auto sptype = node.attribute("sodipodi:type")) { + return sptype; + } + return node.name(); + default: + return ""; + } +} + +/* + 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 : diff --git a/src/object/sp-factory.h b/src/object/sp-factory.h new file mode 100644 index 0000000..b8eba25 --- /dev/null +++ b/src/object/sp-factory.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Factory for SPObject tree + * + * Authors: + * Markus Engel + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FACTORY_SEEN +#define SP_FACTORY_SEEN + +#include <string> + +class SPObject; + +namespace Inkscape { +namespace XML { +class Node; +} +} + +struct SPFactory { + static SPObject *createObject(std::string const &id); + static bool supportsType(std::string const &id); +}; + +struct NodeTraits { + static std::string get_type_string(Inkscape::XML::Node const &node); +}; + +#endif + +/* + 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 : diff --git a/src/object/sp-filter-reference.cpp b/src/object/sp-filter-reference.cpp new file mode 100644 index 0000000..11140f2 --- /dev/null +++ b/src/object/sp-filter-reference.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-filter.h" +#include "sp-filter-reference.h" + +bool +SPFilterReference::_acceptObject(SPObject *obj) const +{ + return is<SPFilter>(obj) && URIReference::_acceptObject(obj); + /* effic: Don't bother making this an inline function: _acceptObject is a virtual function, + typically called from a context where the runtime type is not known at compile time. */ +} + + +/* + 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 : diff --git a/src/object/sp-filter-reference.h b/src/object/sp-filter-reference.h new file mode 100644 index 0000000..2230e01 --- /dev/null +++ b/src/object/sp-filter-reference.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_FILTER_REFERENCE_H +#define SEEN_SP_FILTER_REFERENCE_H + +#include "uri-references.h" +#include "sp-filter.h" // Required for the static_cast. + +class SPObject; +class SPDocument; + +class SPFilterReference : public Inkscape::URIReference { +public: + SPFilterReference(SPObject *obj) : URIReference(obj) {} + SPFilterReference(SPDocument *doc) : URIReference(doc) {} + + SPFilter *getObject() const { + return static_cast<SPFilter *>(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject *obj) const override; +}; + +#endif /* !SEEN_SP_FILTER_REFERENCE_H */ + +/* + 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 : diff --git a/src/object/sp-filter-units.h b/src/object/sp-filter-units.h new file mode 100644 index 0000000..5c7ccbc --- /dev/null +++ b/src/object/sp-filter-units.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_FILTER_UNITS_H +#define SEEN_SP_FILTER_UNITS_H + +enum SPFilterUnits { + SP_FILTER_UNITS_OBJECTBOUNDINGBOX, + SP_FILTER_UNITS_USERSPACEONUSE +}; + +#endif /* !SEEN_SP_FILTER_UNITS_H */ + +/* + 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 : diff --git a/src/object/sp-filter.cpp b/src/object/sp-filter.cpp new file mode 100644 index 0000000..cb06588 --- /dev/null +++ b/src/object/sp-filter.cpp @@ -0,0 +1,586 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <filter> implementation. + */ +/* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-filter.h" + +#include <cstring> +#include <utility> +#include <vector> +#include <unordered_map> + +#include <2geom/transforms.h> +#include <glibmm.h> + +#include "attributes.h" +#include "bad-uri-exception.h" +#include "display/drawing-item.h" +#include "display/nr-filter.h" +#include "document.h" +#include "filters/sp-filter-primitive.h" +#include "sp-filter-reference.h" +#include "uri.h" +#include "filters/slot-resolver.h" +#include "xml/href-attribute-helper.h" + +SPFilter::SPFilter() + : filterUnits(SP_FILTER_UNITS_OBJECTBOUNDINGBOX) + , filterUnits_set(false) + , primitiveUnits(SP_FILTER_UNITS_USERSPACEONUSE) + , primitiveUnits_set(false) +{ + href = std::make_unique<SPFilterReference>(this); + + // Gets called when the filter is (re)attached to another filter. + href->changedSignal().connect([this] (SPObject *old_ref, SPObject *ref) { + if (old_ref) { + modified_connection.disconnect(); + } + + if (is<SPFilter>(ref) && ref != this) { + modified_connection = ref->connectModified([this] (SPObject*, unsigned) { + requestModified(SP_OBJECT_MODIFIED_FLAG); + }); + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); + }); + + x = 0; + y = 0; + width = 0; + height = 0; + auto_region = true; +} + +SPFilter::~SPFilter() = default; + +void SPFilter::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + // Read values of key attributes from XML nodes into object. + readAttr(SPAttr::STYLE); // struct not derived from SPItem, we need to do this ourselves. + readAttr(SPAttr::FILTERUNITS); + readAttr(SPAttr::PRIMITIVEUNITS); + readAttr(SPAttr::X); + readAttr(SPAttr::Y); + readAttr(SPAttr::WIDTH); + readAttr(SPAttr::HEIGHT); + readAttr(SPAttr::AUTO_REGION); + readAttr(SPAttr::FILTERRES); + readAttr(SPAttr::XLINK_HREF); + _refcount = 0; + + SPObject::build(document, repr); + + document->addResource("filter", this); +} + +void SPFilter::release() +{ + document->removeResource("filter", this); + + if (href) { + modified_connection.disconnect(); + href->detach(); + href.reset(); + } + + SPObject::release(); +} + +void SPFilter::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::FILTERUNITS: + if (value) { + if (!std::strcmp(value, "userSpaceOnUse")) { + filterUnits = SP_FILTER_UNITS_USERSPACEONUSE; + } else { + filterUnits = SP_FILTER_UNITS_OBJECTBOUNDINGBOX; + } + filterUnits_set = true; + } else { + filterUnits = SP_FILTER_UNITS_OBJECTBOUNDINGBOX; + filterUnits_set = false; + } + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::PRIMITIVEUNITS: + if (value) { + if (!std::strcmp(value, "objectBoundingBox")) { + primitiveUnits = SP_FILTER_UNITS_OBJECTBOUNDINGBOX; + } else { + primitiveUnits = SP_FILTER_UNITS_USERSPACEONUSE; + } + primitiveUnits_set = true; + } else { + primitiveUnits = SP_FILTER_UNITS_USERSPACEONUSE; + primitiveUnits_set = false; + } + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::X: + x.readOrUnset(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::Y: + y.readOrUnset(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::WIDTH: + width.readOrUnset(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::HEIGHT: + height.readOrUnset(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::AUTO_REGION: + auto_region = !value || std::strcmp(value, "false"); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::FILTERRES: + filterRes.set(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::XLINK_HREF: + if (value) { + try { + href->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException const &e) { + g_warning("%s", e.what()); + href->detach(); + } + } else { + href->detach(); + } + break; + default: + // See if any parents need this value. + SPObject::set(key, value); + break; + } +} + +/** + * Returns the number of references to the filter. + */ +unsigned SPFilter::getRefCount() +{ + // NOTE: this is currently updated by sp_style_filter_ref_changed() in style.cpp + return _refcount; +} + +void SPFilter::update(SPCtx *ctx, unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + ensure_slots(); + + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + auto ictx = static_cast<SPItemCtx*>(ctx); + + // Do here since we know viewport (Bounding box case handled during rendering) + // Note: This only works for root viewport since this routine is not called after + // setting a new viewport. A true fix requires a strategy like SPItemView or SPMarkerView. + if (filterUnits == SP_FILTER_UNITS_USERSPACEONUSE) { + calcDimsFromParentViewport(ictx, true); + } + } + + // Update filter primitives in order to update filter primitive area + for (auto &c : children) { + if (cflags || (c.uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + c.updateDisplay(ctx, cflags); + } + } + + SPObject::update(ctx, flags); +} + +void SPFilter::modified(unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + // We are not an LPE, do not update filter regions on load. + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG)) { + update_filter_all_regions(); + } + + for (auto &c : children) { + if (cflags || (c.mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + c.emitModified(cflags); + } + } + + for (auto item : views) { + item->setFilterRenderer(build_renderer(item)); + } +} + +Inkscape::XML::Node *SPFilter::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) +{ + // Original from sp-item-group.cpp + if (flags & SP_OBJECT_WRITE_BUILD) { + if (!repr) { + repr = doc->createElement("svg:filter"); + } + + std::vector<Inkscape::XML::Node *> l; + for (auto &child : children) { + auto crepr = child.updateRepr(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) { + child.updateRepr(flags); + } + } + + if ((flags & SP_OBJECT_WRITE_ALL) || filterUnits_set) { + switch (filterUnits) { + case SP_FILTER_UNITS_USERSPACEONUSE: + repr->setAttribute("filterUnits", "userSpaceOnUse"); + break; + default: + repr->setAttribute("filterUnits", "objectBoundingBox"); + break; + } + } + + if ((flags & SP_OBJECT_WRITE_ALL) || primitiveUnits_set) { + switch (primitiveUnits) { + case SP_FILTER_UNITS_OBJECTBOUNDINGBOX: + repr->setAttribute("primitiveUnits", "objectBoundingBox"); + break; + default: + repr->setAttribute("primitiveUnits", "userSpaceOnUse"); + break; + } + } + + if (x._set) { + repr->setAttributeSvgDouble("x", x.computed); + } else { + repr->removeAttribute("x"); + } + + if (y._set) { + repr->setAttributeSvgDouble("y", y.computed); + } else { + repr->removeAttribute("y"); + } + + if (width._set) { + repr->setAttributeSvgDouble("width", width.computed); + } else { + repr->removeAttribute("width"); + } + + if (height._set) { + repr->setAttributeSvgDouble("height", height.computed); + } else { + repr->removeAttribute("height"); + } + + if (filterRes.getNumber() >= 0) { + auto tmp = filterRes.getValueString(); + repr->setAttribute("filterRes", tmp); + } else { + repr->removeAttribute("filterRes"); + } + + if (href->getURI()) { + auto uri_string = href->getURI()->str(); + auto href_key = Inkscape::getHrefAttribute(*repr).first; + repr->setAttributeOrRemoveIfEmpty(href_key, uri_string); + } + + SPObject::write(doc, repr, flags); + + return repr; +} + +/** + * Update the filter's region based on its detectable href links + * + * Automatic region only updated if auto_region is false + * and filterUnits is not UserSpaceOnUse + */ +void SPFilter::update_filter_all_regions() +{ + if (!auto_region || filterUnits == SP_FILTER_UNITS_USERSPACEONUSE) { + return; + } + + // Combine all items into one region for updating. + Geom::OptRect opt_r; + for (auto &obj : hrefList) { + auto item = cast<SPItem>(obj); + opt_r.unionWith(get_automatic_filter_region(item)); + } + if (opt_r) { + Geom::Rect region = *opt_r; + set_filter_region(region.left(), region.top(), region.width(), region.height()); + } +} + +/** + * Update the filter region based on the object's bounding box + * + * @param item - The item whose coords are used as the basis for the area. + */ +void SPFilter::update_filter_region(SPItem *item) +{ + if (!auto_region || filterUnits == SP_FILTER_UNITS_USERSPACEONUSE) { + return; // No adjustment for dead box + } + + auto region = get_automatic_filter_region(item); + + // Set the filter region into this filter object + set_filter_region(region.left(), region.top(), region.width(), region.height()); +} + +/** + * Generate a filter region based on the item and return it. + * + * @param item - The item whose coords are used as the basis for the area. + */ +Geom::Rect SPFilter::get_automatic_filter_region(SPItem const *item) const +{ + // Calling bbox instead of visualBound() avoids re-requesting filter regions + Geom::OptRect v_box = item->bbox(Geom::identity(), SPItem::VISUAL_BBOX); + Geom::OptRect g_box = item->bbox(Geom::identity(), SPItem::GEOMETRIC_BBOX); + if (!v_box || !g_box) { + return Geom::Rect(); // No adjustment for dead box + } + + // Because the filter box is in geometric bounding box units, it must ALSO + // take account of the visualBox, so even if the filter does NOTHING to the + // size of an object, we must add the difference between the geometric and + // visual boxes ourselves or find them cut off by renderers of all kinds. + Geom::Rect inbox = *g_box; + Geom::Rect outbox = *v_box; + for (auto &primitive_obj : children) { + auto primitive = cast<SPFilterPrimitive>(&primitive_obj); + if (primitive) { + // Update the region with the primitive's options + outbox = primitive->calculate_region(outbox); + } + } + + // Include the original visual bounding-box in the result + outbox.unionWith(v_box); + // Scale outbox to width/height scale of input, this scales the geometric + // into the visual bounding box requiring any changes to it to re-run this. + outbox *= Geom::Translate(-inbox.left(), -inbox.top()); + outbox *= Geom::Scale(1.0 / inbox.width(), 1.0 / inbox.height()); + return outbox; +} + +/** + * Set the filter region attributes from a bounding box + */ +void SPFilter::set_filter_region(double x, double y, double width, double height) +{ + if (width != 0 && height != 0) { + // TODO: set it in UserSpaceOnUse instead? + auto repr = getRepr(); + repr->setAttributeSvgDouble("x", x); + repr->setAttributeSvgDouble("y", y); + repr->setAttributeSvgDouble("width", width); + repr->setAttributeSvgDouble("height", height); + } +} + +/** + * Check each filter primitive for conflicts with this object. + */ +bool SPFilter::valid_for(SPObject const *obj) const +{ + for (auto &primitive_obj : children) { + auto primitive = cast<SPFilterPrimitive>(&primitive_obj); + if (primitive && !primitive->valid_for(obj)) { + return false; + } + } + return true; +} + +void SPFilter::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + SPObject::child_added(child, ref); + + if (auto f = cast<SPFilterPrimitive>(get_child_by_repr(child))) { + for (auto &v : views) { + f->show(v); + } + } + + invalidate_slots(); +} + +void SPFilter::remove_child(Inkscape::XML::Node *child) +{ + if (auto f = cast<SPFilterPrimitive>(get_child_by_repr(child))) { + for (auto &v : views) { + f->hide(v); + } + } + + SPObject::remove_child(child); + + invalidate_slots(); +} + +void SPFilter::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_repr, Inkscape::XML::Node *new_repr) +{ + SPObject::order_changed(child, old_repr, new_repr); + invalidate_slots(); +} + +void SPFilter::invalidate_slots() +{ + if (!slots_valid) return; + slots_valid = false; + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFilter::ensure_slots() +{ + if (slots_valid) return; + slots_valid = true; + + SlotResolver resolver; + + for (auto &c : children) { + if (auto prim = cast<SPFilterPrimitive>(&c)) { + prim->resolve_slots(resolver); + } + } +} + +std::unique_ptr<Inkscape::Filters::Filter> SPFilter::build_renderer(Inkscape::DrawingItem *item) +{ + auto nr_filter = std::make_unique<Inkscape::Filters::Filter>(primitive_count()); + + ensure_slots(); + + nr_filter->set_filter_units(filterUnits); + nr_filter->set_primitive_units(primitiveUnits); + nr_filter->set_x(x); + nr_filter->set_y(y); + nr_filter->set_width(width); + nr_filter->set_height(height); + + if (filterRes.getNumber() >= 0) { + if (filterRes.getOptNumber() >= 0) { + nr_filter->set_resolution(filterRes.getNumber(), filterRes.getOptNumber()); + } else { + nr_filter->set_resolution(filterRes.getNumber()); + } + } + + nr_filter->clear_primitives(); + for (auto &primitive_obj : children) { + if (auto primitive = cast<SPFilterPrimitive>(&primitive_obj)) { + nr_filter->add_primitive(primitive->build_renderer(item)); + } + } + + return nr_filter; +} + +int SPFilter::primitive_count() const +{ + int count = 0; + + for (auto const &primitive_obj : children) { + if (is<SPFilterPrimitive>(&primitive_obj)) { + count++; + } + } + + return count; +} + +Glib::ustring SPFilter::get_new_result_name() const +{ + int largest = 0; + + for (auto const &primitive_obj : children) { + if (is<SPFilterPrimitive>(&primitive_obj)) { + auto repr = primitive_obj.getRepr(); + auto result = repr->attribute("result"); + if (result) { + int index; + if (std::sscanf(result, "result%5d", &index) == 1) { + if (index > largest) { + largest = index; + } + } + } + } + } + + return "result" + Glib::Ascii::dtostr(largest + 1); +} + +void SPFilter::show(Inkscape::DrawingItem *item) +{ + views.emplace_back(item); + + for (auto &c : children) { + if (auto f = cast<SPFilterPrimitive>(&c)) { + f->show(item); + } + } + + item->setFilterRenderer(build_renderer(item)); +} + +void SPFilter::hide(Inkscape::DrawingItem *item) +{ + auto it = std::find(views.begin(), views.end(), item); + assert(it != views.end()); + views.erase(it); + + for (auto &c : children) { + if (auto f = cast<SPFilterPrimitive>(&c)) { + f->hide(item); + } + } + + item->setFilterRenderer(nullptr); +} + +/* + 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 : diff --git a/src/object/sp-filter.h b/src/object/sp-filter.h new file mode 100644 index 0000000..cb33782 --- /dev/null +++ b/src/object/sp-filter.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <filter> element + *//* + * Authors: + * Hugo Rodrigues <haa.rodrigues@gmail.com> + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FILTER_H_SEEN +#define SP_FILTER_H_SEEN + +#include <memory> +#include <glibmm/ustring.h> + +#include "helper/auto-connection.h" +#include "number-opt-number.h" +#include "sp-dimensions.h" +#include "sp-filter-units.h" +#include "sp-item.h" +#include "sp-object.h" + +namespace Inkscape { +class Drawing; +class DrawingItem; +namespace Filters { class Filter; } +} // namespace Inkscape + +class SPFilterReference; +class SPFilterPrimitive; + +class SPFilter + : public SPObject + , public SPDimensions +{ +public: + SPFilter(); + ~SPFilter() override; + int tag() const override { return tag_of<decltype(*this)>; } + + /// Returns a renderer for this filter, for use by the DrawingItem item. + std::unique_ptr<Inkscape::Filters::Filter> build_renderer(Inkscape::DrawingItem *item); + + /// Returns the number of filter primitives in this SPFilter object. + int primitive_count() const; + + void update_filter_all_regions(); + void update_filter_region(SPItem *item); + void set_filter_region(double x, double y, double width, double height); + Geom::Rect get_automatic_filter_region(SPItem const *item) const; + + /// Checks each filter primitive to make sure the object won't cause issues + bool valid_for(SPObject const *obj) const; + + /// Returns a result image name that is not in use inside this filter. + Glib::ustring get_new_result_name() const; + + void show(Inkscape::DrawingItem *item); + void hide(Inkscape::DrawingItem *item); + + SPFilterUnits filterUnits; + bool filterUnits_set : 1; + SPFilterUnits primitiveUnits; + bool primitiveUnits_set : 1; + NumberOptNumber filterRes; + std::unique_ptr<SPFilterReference> href; + bool auto_region; + + Inkscape::auto_connection modified_connection; + + unsigned getRefCount(); + unsigned _refcount = 0; + + void invalidate_slots(); + void ensure_slots(); + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const *value) override; + void update(SPCtx *ctx, unsigned flags) override; + void modified(unsigned flags) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; + + void child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void order_changed(Inkscape::XML::Node* child, Inkscape::XML::Node* old_repr, Inkscape::XML::Node* new_repr) override; + +private: + bool slots_valid = true; + + std::vector<Inkscape::DrawingItem*> views; +}; + +#endif // SP_FILTER_H_SEEN + +/* + 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 : diff --git a/src/object/sp-flowdiv.cpp b/src/object/sp-flowdiv.cpp new file mode 100644 index 0000000..0fb9983 --- /dev/null +++ b/src/object/sp-flowdiv.cpp @@ -0,0 +1,467 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + */ + +#include "xml/repr.h" +#include "sp-flowdiv.h" +#include "sp-string.h" +#include "document.h" + +SPFlowdiv::SPFlowdiv() : SPItem() { +} + +SPFlowdiv::~SPFlowdiv() = default; + +void SPFlowdiv::release() { + SPItem::release(); +} + +void SPFlowdiv::update(SPCtx *ctx, unsigned int flags) { + SPItemCtx *ictx = reinterpret_cast<SPItemCtx *>(ctx); + SPItemCtx 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; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + if (is<SPItem>(child)) { + SPItem const &chi = *cast<SPItem>(child); + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, childflags); + } else { + child->updateDisplay(ctx, childflags); + } + } + + sp_object_unref(child); + } + + SPItem::update(ctx, flags); +} + +void SPFlowdiv::modified(unsigned int flags) { + SPItem::modified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector<SPObject *> l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + + +void SPFlowdiv::build(SPDocument *doc, Inkscape::XML::Node *repr) { + this->_requireSVGVersion(Inkscape::Version(1, 2)); + + SPItem::build(doc, repr); +} + +void SPFlowdiv::set(SPAttr key, const gchar* value) { + SPItem::set(key, value); +} + + +Inkscape::XML::Node* SPFlowdiv::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( flags & SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowDiv"); + } + + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + Inkscape::XML::Node* c_repr = nullptr; + + if ( is<SPFlowtspan>(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( is<SPFlowpara>(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( is<SPString>(&child) ) { + c_repr = xml_doc->createTextNode(cast<SPString>(&child)->string.c_str()); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( is<SPFlowtspan>(&child) ) { + child.updateRepr(flags); + } else if ( is<SPFlowpara>(&child) ) { + child.updateRepr(flags); + } else if ( is<SPString>(&child) ) { + child.getRepr()->setContent(cast<SPString>(&child)->string.c_str()); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + + +/* + * + */ + +SPFlowtspan::SPFlowtspan() : SPItem() { +} + +SPFlowtspan::~SPFlowtspan() = default; + +void SPFlowtspan::release() { + SPItem::release(); +} + +void SPFlowtspan::update(SPCtx *ctx, unsigned int flags) { + SPItemCtx *ictx = reinterpret_cast<SPItemCtx *>(ctx); + SPItemCtx 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; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + if (is<SPItem>(child)) { + SPItem const &chi = *cast<SPItem>(child); + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, childflags); + } else { + child->updateDisplay(ctx, childflags); + } + } + + sp_object_unref(child); + } + + SPItem::update(ctx, flags); +} + +void SPFlowtspan::modified(unsigned int flags) { + SPItem::modified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector<SPObject *> l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + + +void SPFlowtspan::build(SPDocument *doc, Inkscape::XML::Node *repr) { + SPItem::build(doc, repr); +} + +void SPFlowtspan::set(SPAttr key, const gchar* value) { + SPItem::set(key, value); +} + +Inkscape::XML::Node *SPFlowtspan::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( flags&SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowSpan"); + } + + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + Inkscape::XML::Node* c_repr = nullptr; + + if ( is<SPFlowtspan>(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( is<SPFlowpara>(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( is<SPString>(&child) ) { + c_repr = xml_doc->createTextNode(cast<SPString>(&child)->string.c_str()); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( is<SPFlowtspan>(&child) ) { + child.updateRepr(flags); + } else if ( is<SPFlowpara>(&child) ) { + child.updateRepr(flags); + } else if ( is<SPString>(&child) ) { + child.getRepr()->setContent(cast<SPString>(&child)->string.c_str()); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + + +/* + * + */ +SPFlowpara::SPFlowpara() : SPItem() { +} + +SPFlowpara::~SPFlowpara() = default; + +void SPFlowpara::release() { + SPItem::release(); +} + +void SPFlowpara::update(SPCtx *ctx, unsigned int flags) { + SPItemCtx *ictx = reinterpret_cast<SPItemCtx *>(ctx); + SPItemCtx cctx = *ictx; + + SPItem::update(ctx, flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector<SPObject *> l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (flags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + if (is<SPItem>(child)) { + SPItem const &chi = *cast<SPItem>(child); + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, flags); + } else { + child->updateDisplay(ctx, flags); + } + } + sp_object_unref(child); + } +} + +void SPFlowpara::modified(unsigned int flags) { + SPItem::modified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector<SPObject *> l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + + +void SPFlowpara::build(SPDocument *doc, Inkscape::XML::Node *repr) { + SPItem::build(doc, repr); +} + +void SPFlowpara::set(SPAttr key, const gchar* value) { + SPItem::set(key, value); +} + +Inkscape::XML::Node *SPFlowpara::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( flags&SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowPara"); + } + + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + Inkscape::XML::Node* c_repr = nullptr; + + if ( is<SPFlowtspan>(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( is<SPFlowpara>(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( is<SPString>(&child) ) { + c_repr = xml_doc->createTextNode(cast<SPString>(&child)->string.c_str()); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( is<SPFlowtspan>(&child) ) { + child.updateRepr(flags); + } else if ( is<SPFlowpara>(&child) ) { + child.updateRepr(flags); + } else if ( is<SPString>(&child) ) { + child.getRepr()->setContent(cast<SPString>(&child)->string.c_str()); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + + +/* + * + */ + +SPFlowline::SPFlowline() : SPObject() { +} + +SPFlowline::~SPFlowline() = default; + +void SPFlowline::release() { + SPObject::release(); +} + +void SPFlowline::modified(unsigned int flags) { + SPObject::modified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; +} + +Inkscape::XML::Node *SPFlowline::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( flags & SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowLine"); + } + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + + +/* + * + */ + +SPFlowregionbreak::SPFlowregionbreak() : SPObject() { +} + +SPFlowregionbreak::~SPFlowregionbreak() = default; + +void SPFlowregionbreak::release() { + SPObject::release(); +} + +void SPFlowregionbreak::modified(unsigned int flags) { + SPObject::modified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; +} + +Inkscape::XML::Node *SPFlowregionbreak::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( flags & SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowLine"); + } + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + + +/* + 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 : diff --git a/src/object/sp-flowdiv.h b/src/object/sp-flowdiv.h new file mode 100644 index 0000000..0792652 --- /dev/null +++ b/src/object/sp-flowdiv.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_FLOWDIV_H +#define SEEN_SP_ITEM_FLOWDIV_H + +/* + */ + +#include "sp-object.h" +#include "sp-item.h" + +// these 3 are derivatives of SPItem to get the automatic style handling +class SPFlowdiv final : public SPItem { +public: + SPFlowdiv(); + ~SPFlowdiv() override; + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + void set(SPAttr key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +class SPFlowtspan final : public SPItem { +public: + SPFlowtspan(); + ~SPFlowtspan() override; + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + void set(SPAttr key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +class SPFlowpara final : public SPItem { +public: + SPFlowpara(); + ~SPFlowpara() override; + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + void set(SPAttr key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +// these do not need any style +class SPFlowline final : public SPObject { +public: + SPFlowline(); + ~SPFlowline() override; + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void release() override; + void modified(unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +class SPFlowregionbreak final : public SPObject { +public: + SPFlowregionbreak(); + ~SPFlowregionbreak() override; + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void release() override; + void modified(unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif diff --git a/src/object/sp-flowregion.cpp b/src/object/sp-flowregion.cpp new file mode 100644 index 0000000..879b67d --- /dev/null +++ b/src/object/sp-flowregion.cpp @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + */ + +#include <glibmm/i18n.h> + +#include <xml/repr.h> +#include "display/curve.h" +#include "sp-shape.h" +#include "sp-text.h" +#include "sp-use.h" +#include "style.h" +#include "document.h" +#include "sp-title.h" +#include "sp-desc.h" + +#include "sp-flowregion.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + + +static void GetDest(SPObject* child,Shape **computed); + + +SPFlowregion::SPFlowregion() : SPItem() { +} + +SPFlowregion::~SPFlowregion() { + for (auto & it : this->computed) { + delete it; + } +} + +void SPFlowregion::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPItem::child_added(child, ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/* fixme: hide (Lauris) */ + +void SPFlowregion::remove_child(Inkscape::XML::Node * child) { + SPItem::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +void SPFlowregion::update(SPCtx *ctx, unsigned int flags) { + SPItemCtx *ictx = reinterpret_cast<SPItemCtx *>(ctx); + SPItemCtx 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; + + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + g_assert(child != nullptr); + auto item = cast<SPItem>(child); + + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + if (item) { + SPItem const &chi = *item; + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, childflags); + } else { + child->updateDisplay(ctx, childflags); + } + } + + sp_object_unref(child); + } + + SPItem::update(ctx, flags); + + this->UpdateComputed(); +} + +void SPFlowregion::UpdateComputed() +{ + for (auto & it : computed) { + delete it; + } + computed.clear(); + + for (auto& child: children) { + Shape *shape = nullptr; + GetDest(&child, &shape); + computed.push_back(shape); + } +} + +void SPFlowregion::modified(guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector<SPObject *>l; + + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + g_assert(child != nullptr); + + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +Inkscape::XML::Node *SPFlowregion::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowRegion"); + } + + std::vector<Inkscape::XML::Node *> l; + for (auto& child: children) { + if (!is<SPTitle>(&child) && !is<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); + } + + for (auto& child: children) { + if (!is<SPTitle>(&child) && !is<SPDesc>(&child)) { + child.updateRepr(flags); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + this->UpdateComputed(); // copied from update(), see LP Bug 1339305 + + return repr; +} + +const char* SPFlowregion::typeName() const { + return "text-flow"; +} + +const char* SPFlowregion::displayName() const { + // TRANSLATORS: "Flow region" is an area where text is allowed to flow + return _("Flow Region"); +} + +SPFlowregionExclude::SPFlowregionExclude() : SPItem() { + this->computed = nullptr; +} + +SPFlowregionExclude::~SPFlowregionExclude() { + if (this->computed) { + delete this->computed; + this->computed = nullptr; + } +} + +void SPFlowregionExclude::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPItem::child_added(child, ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/* fixme: hide (Lauris) */ + +void SPFlowregionExclude::remove_child(Inkscape::XML::Node * child) { + SPItem::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +void SPFlowregionExclude::update(SPCtx *ctx, unsigned int flags) { + SPItemCtx *ictx = reinterpret_cast<SPItemCtx *>(ctx); + SPItemCtx cctx = *ictx; + + SPItem::update(ctx, flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector<SPObject *> l; + + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + g_assert(child != nullptr); + + if (flags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + auto item = cast<SPItem>(child); + if (item) { + SPItem const &chi = *item; + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, flags); + } else { + child->updateDisplay(ctx, flags); + } + } + + sp_object_unref(child); + } + + this->UpdateComputed(); +} + + +void SPFlowregionExclude::UpdateComputed() +{ + if (computed) { + delete computed; + computed = nullptr; + } + + for (auto& child: children) { + GetDest(&child, &computed); + } +} + +void SPFlowregionExclude::modified(guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector<SPObject*> l; + + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + g_assert(child != nullptr); + + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +Inkscape::XML::Node *SPFlowregionExclude::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowRegionExclude"); + } + + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + 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) { + child.updateRepr(flags); + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPFlowregionExclude::typeName() const { + return "text-flow"; +} + +const char* SPFlowregionExclude::displayName() const { + /* TRANSLATORS: A region "cut out of" a flow region; text is not allowed to flow inside the + * flow excluded region. flowRegionExclude in SVG 1.2: see + * http://www.w3.org/TR/2004/WD-SVG12-20041027/flow.html#flowRegion-elem and + * http://www.w3.org/TR/2004/WD-SVG12-20041027/flow.html#flowRegionExclude-elem. */ + return _("Flow Excluded Region"); +} + +static void UnionShape(Shape *&base_shape, Shape const *add_shape) +{ + if (base_shape == nullptr) + base_shape = new Shape; + + if (base_shape->hasEdges() == false) { + base_shape->Copy(const_cast<Shape *>(add_shape)); + } else if (add_shape->hasEdges()) { + Shape *temp = new Shape; + temp->Booleen(const_cast<Shape *>(add_shape), base_shape, bool_op_union); + delete base_shape; + base_shape = temp; + } +} + +static void GetDest(SPObject* child,Shape **computed) +{ + auto item = cast<SPItem>(child); + if (item == nullptr) + return; + + std::optional<SPCurve> curve; + Geom::Affine tr_mat; + + SPObject* u_child = child; + auto use = cast<SPUse>(item); + if ( use ) { + u_child = use->child; + tr_mat = use->getRelativeTransform(child->parent); + } else { + tr_mat = item->transform; + } + auto shape = cast<SPShape>(u_child); + if ( shape ) { + if (!shape->curve()) { + shape->set_shape(); + } + curve = SPCurve::ptr_to_opt(shape->curve()); + } else { + auto text = cast<SPText>(u_child); + if ( text ) { + curve = text->getNormalizedBpath(); + } + } + + if ( curve ) { + Path* temp=new Path; + temp->LoadPathVector(curve->get_pathvector(), tr_mat, true); + Shape* n_shp=new Shape; + temp->Convert(0.25); + temp->Fill(n_shp,0); + Shape* uncross=new Shape; + SPStyle* style = u_child->style; + if ( style && style->fill_rule.computed == SP_WIND_RULE_EVENODD ) { + uncross->ConvertToShape(n_shp,fill_oddEven); + } else { + uncross->ConvertToShape(n_shp,fill_nonZero); + } + UnionShape(*computed, uncross); + delete uncross; + delete n_shp; + delete temp; + } +} + +/* + 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 : diff --git a/src/object/sp-flowregion.h b/src/object/sp-flowregion.h new file mode 100644 index 0000000..6b2d258 --- /dev/null +++ b/src/object/sp-flowregion.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_FLOWREGION_H +#define SEEN_SP_ITEM_FLOWREGION_H + +/* + */ + +#include "sp-item.h" + +class Path; +class Shape; +class flow_dest; + +class SPFlowregion final : public SPItem { +public: + SPFlowregion(); + ~SPFlowregion() override; + int tag() const override { return tag_of<decltype(*this)>; } + + std::vector<Shape*> computed; + + void UpdateComputed(); + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void update(SPCtx *ctx, unsigned int flags) override; + void modified(guint flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + const char* typeName() const override; + const char* displayName() const override; +}; + +class SPFlowregionExclude final : public SPItem { +public: + SPFlowregionExclude(); + ~SPFlowregionExclude() override; + int tag() const override { return tag_of<decltype(*this)>; } + + Shape *computed; + + void UpdateComputed(); + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void update(SPCtx *ctx, unsigned int flags) override; + void modified(guint flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + const char* typeName() const override; + const char* displayName() const override; +}; + +#endif diff --git a/src/object/sp-flowtext.cpp b/src/object/sp-flowtext.cpp new file mode 100644 index 0000000..4f7afbd --- /dev/null +++ b/src/object/sp-flowtext.cpp @@ -0,0 +1,802 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + */ + +#include <glibmm/i18n.h> +#include <cstring> +#include <string> + +#include "attributes.h" +#include "xml/repr.h" +#include "style.h" +#include "inkscape.h" +#include "document.h" + +#include "desktop.h" +#include "desktop-style.h" +#include "svg/svg.h" +#include "snap-candidate.h" +#include "snap-preferences.h" + +#include "text-tag-attributes.h" +#include "text-editing.h" + +#include "sp-flowdiv.h" +#include "sp-flowregion.h" +#include "sp-flowtext.h" +#include "sp-rect.h" +#include "sp-string.h" +#include "sp-text.h" +#include "sp-use.h" + +#include "libnrtype/font-instance.h" +#include "libnrtype/font-factory.h" + +#include "livarot/Shape.h" + +#include "display/curve.h" +#include "display/drawing-text.h" + +#include "layer-manager.h" + +SPFlowtext::SPFlowtext() : SPItem(), + par_indent(0), + _optimizeScaledText(false) +{ +} + +SPFlowtext::~SPFlowtext() = default; + +void SPFlowtext::release() +{ + view_style_attachments.clear(); + SPItem::release(); +} + +void SPFlowtext::child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) { + SPItem::child_added(child, ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +/* fixme: hide (Lauris) */ + +void SPFlowtext::remove_child(Inkscape::XML::Node* child) { + SPItem::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFlowtext::update(SPCtx* ctx, unsigned int flags) { + SPItemCtx *ictx = (SPItemCtx *) ctx; + SPItemCtx 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; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + g_assert(child != nullptr); + + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + auto item = cast<SPItem>(child); + if (item) { + SPItem const &chi = *item; + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, childflags); + } else { + child->updateDisplay(ctx, childflags); + } + } + + sp_object_unref(child); + } + + SPItem::update(ctx, flags); + + this->rebuildLayout(); + + Geom::OptRect pbox = this->geometricBounds(); + + for (auto &v : views) { + auto &sa = view_style_attachments[v.key]; + sa.unattachAll(); + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + _clearFlow(g); + g->setStyle(style); + // pass the bbox of the flowtext object as paintbox (used for paintserver fills) + layout.show(g, sa, pbox); + } +} + +void SPFlowtext::modified(unsigned int flags) { + SPObject *region = nullptr; + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + // FIXME: the below stanza is copied over from sp_text_modified, consider factoring it out + if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG )) { + Geom::OptRect pbox = geometricBounds(); + + for (auto &v : views) { + auto &sa = view_style_attachments[v.key]; + sa.unattachAll(); + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + _clearFlow(g); + g->setStyle(style); + layout.show(g, sa, pbox); + } + } + + for (auto& o: children) { + if (is<SPFlowregion>(&o)) { + region = &o; + break; + } + } + + if (region) { + if (flags || (region->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + region->emitModified(flags); // pass down to the region only + } + } +} + +void SPFlowtext::build(SPDocument* doc, Inkscape::XML::Node* repr) { + this->_requireSVGVersion(Inkscape::Version(1, 2)); + + SPItem::build(doc, repr); + + this->readAttr(SPAttr::LAYOUT_OPTIONS); // must happen after css has been read +} + +void SPFlowtext::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::LAYOUT_OPTIONS: { + // deprecated attribute, read for backward compatibility only + //XML Tree being directly used while it shouldn't be. + SPCSSAttr *opts = sp_repr_css_attr(this->getRepr(), "inkscape:layoutOptions"); + { + gchar const *val = sp_repr_css_property(opts, "justification", nullptr); + + if (val != nullptr && !this->style->text_align.set) { + if ( strcmp(val, "0") == 0 || strcmp(val, "false") == 0 ) { + this->style->text_align.value = SP_CSS_TEXT_ALIGN_LEFT; + } else { + this->style->text_align.value = SP_CSS_TEXT_ALIGN_JUSTIFY; + } + + this->style->text_align.set = TRUE; + this->style->text_align.inherit = FALSE; + this->style->text_align.computed = this->style->text_align.value; + } + } + /* no equivalent css attribute for these two (yet) + { + gchar const *val = sp_repr_css_property(opts, "layoutAlgo", NULL); + if ( val == NULL ) { + group->algo = 0; + } else { + if ( strcmp(val, "better") == 0 ) { // knuth-plass, never worked for general cases + group->algo = 2; + } else if ( strcmp(val, "simple") == 0 ) { // greedy, but allowed lines to be compressed by up to 20% if it would make them fit + group->algo = 1; + } else if ( strcmp(val, "default") == 0 ) { // the same one we use, a standard greedy + group->algo = 0; + } + } + } + */ + { // This would probably translate to padding-left, if SPStyle had it. + gchar const *val = sp_repr_css_property(opts, "par-indent", nullptr); + + if ( val == nullptr ) { + this->par_indent = 0.0; + } else { + this->par_indent = g_ascii_strtod(val, nullptr); + } + } + + sp_repr_css_attr_unref(opts); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + + default: + SPItem::set(key, value); + break; + } +} + +Inkscape::XML::Node* SPFlowtext::write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) { + if ( flags & SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = doc->createElement("svg:flowRoot"); + } + + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + Inkscape::XML::Node *c_repr = nullptr; + + if (is<SPFlowdiv>(&child) || is<SPFlowpara>(&child) || is<SPFlowregion>(&child) || is<SPFlowregionExclude>(&child)) { + c_repr = child.updateRepr(doc, nullptr, flags); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if (is<SPFlowdiv>(&child) || is<SPFlowpara>(&child) || is<SPFlowregion>(&child) || is<SPFlowregionExclude>(&child)) { + child.updateRepr(flags); + } + } + } + + this->rebuildLayout(); // copied from update(), see LP Bug 1339305 + + SPItem::write(doc, repr, flags); + + return repr; +} + +Geom::OptRect SPFlowtext::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const { + return this->layout.bounds(transform, type == SPItem::VISUAL_BBOX); +} + +void SPFlowtext::print(SPPrintContext *ctx) { + Geom::OptRect pbox, bbox, dbox; + pbox = this->geometricBounds(); + bbox = this->desktopVisualBounds(); + dbox = Geom::Rect::from_xywh(Geom::Point(0,0), this->document->getDimensions()); + + Geom::Affine const ctm (this->i2dt_affine()); + + this->layout.print(ctx, pbox, dbox, bbox, ctm); +} + +const char* SPFlowtext::typeName() const { + return "text"; +} + +const char* SPFlowtext::displayName() const { + if (has_internal_frame()) { + return _("Flowed Text"); + } else { + return _("Linked Flowed Text"); + } +} + +gchar* SPFlowtext::description() const { + int const nChars = layout.iteratorToCharIndex(layout.end()); + char const *trunc = (layout.inputTruncated()) ? _(" [truncated]") : ""; + + return g_strdup_printf(ngettext("(%d character%s)", "(%d characters%s)", nChars), nChars, trunc); +} + +void SPFlowtext::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_TEXT_BASELINE)) { + // Choose a point on the baseline for snapping from or to, with the horizontal position + // of this point depending on the text alignment (left vs. right) + Inkscape::Text::Layout const *layout = te_get_layout((SPItem *) this); + + if (layout != nullptr && layout->outputExists()) { + std::optional<Geom::Point> pt = layout->baselineAnchorPoint(); + + if (pt) { + p.emplace_back((*pt) * this->i2dt_affine(), Inkscape::SNAPSOURCE_TEXT_ANCHOR, Inkscape::SNAPTARGET_TEXT_ANCHOR); + } + } + } +} + +Inkscape::DrawingItem* SPFlowtext::show(Inkscape::Drawing &drawing, unsigned int key, unsigned int /*flags*/) { + Inkscape::DrawingGroup *flowed = new Inkscape::DrawingGroup(drawing); + flowed->setPickChildren(false); + flowed->setStyle(this->style); + + // pass the bbox of the flowtext object as paintbox (used for paintserver fills) + Geom::OptRect bbox = this->geometricBounds(); + layout.show(flowed, view_style_attachments[key], bbox); + + return flowed; +} + +void SPFlowtext::hide(unsigned key) +{ + view_style_attachments.erase(key); + + for (auto &v : views) { + if (v.key == key) { + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + _clearFlow(g); + } + } +} + +/* + * + */ +void SPFlowtext::_buildLayoutInput(SPObject *root, Shape const *exclusion_shape, std::list<Shape> *shapes, SPObject **pending_line_break_object) +{ + Inkscape::Text::Layout::OptionalTextTagAttrs pi; + bool with_indent = false; + + if (is<SPFlowpara>(root) || is<SPFlowdiv>(root)) { + + layout.wrap_mode = Inkscape::Text::Layout::WRAP_SHAPE_INSIDE; + + layout.strut.reset(); + if (style) { + auto font = FontFactory::get().FaceFromStyle(style); + if (font) { + font->FontMetrics(layout.strut.ascent, layout.strut.descent, layout.strut.xheight); + } + layout.strut *= style->font_size.computed; + if (style->line_height.normal ) { + layout.strut.computeEffective( Inkscape::Text::Layout::LINE_HEIGHT_NORMAL ); + } else if (style->line_height.unit == SP_CSS_UNIT_NONE) { + layout.strut.computeEffective( style->line_height.computed ); + } else { + if( style->font_size.computed > 0.0 ) { + layout.strut.computeEffective( style->line_height.computed/style->font_size.computed ); + } + } + } + + // emulate par-indent with the first char's kern + SPObject *t = root; + SPFlowtext *ft = nullptr; + while (t && !ft) { + ft = cast<SPFlowtext>(t); + t = t->parent; + } + + if (ft) { + double indent = ft->par_indent; + if (indent != 0) { + with_indent = true; + SVGLength sl; + sl.value = sl.computed = indent; + sl._set = true; + pi.dx.push_back(sl); + } + } + } + + if (*pending_line_break_object) { + if (is<SPFlowregionbreak>(*pending_line_break_object)) { + layout.appendControlCode(Inkscape::Text::Layout::SHAPE_BREAK, *pending_line_break_object); + } else { + layout.appendControlCode(Inkscape::Text::Layout::PARAGRAPH_BREAK, *pending_line_break_object); + } + *pending_line_break_object = nullptr; + } + + for (auto& child: root->children) { + auto str = cast<SPString>(&child); + if (str) { + if (*pending_line_break_object) { + if (is<SPFlowregionbreak>(*pending_line_break_object)) + layout.appendControlCode(Inkscape::Text::Layout::SHAPE_BREAK, *pending_line_break_object); + else { + layout.appendControlCode(Inkscape::Text::Layout::PARAGRAPH_BREAK, *pending_line_break_object); + } + *pending_line_break_object = nullptr; + } + if (with_indent) { + layout.appendText(str->string, root->style, &child, &pi); + } else { + layout.appendText(str->string, root->style, &child); + } + } else { + auto region = cast<SPFlowregion>(&child); + if (region) { + std::vector<Shape*> const &computed = region->computed; + for (auto it : computed) { + shapes->push_back(Shape()); + if (exclusion_shape->hasEdges()) { + shapes->back().Booleen(it, const_cast<Shape*>(exclusion_shape), bool_op_diff); + } else { + shapes->back().Copy(it); + } + layout.appendWrapShape(&shapes->back()); + } + } + //Xml Tree is being directly used while it shouldn't be. + else if (!is<SPFlowregionExclude>(&child) && !sp_repr_is_meta_element(child.getRepr())) { + _buildLayoutInput(&child, exclusion_shape, shapes, pending_line_break_object); + } + } + } + + if (is<SPFlowdiv>(root) || is<SPFlowpara>(root) || is<SPFlowregionbreak>(root) || is<SPFlowline>(root)) { + if (!root->hasChildren()) { + layout.appendText("", root->style, root); + } + *pending_line_break_object = root; + } +} + +Shape* SPFlowtext::_buildExclusionShape() const +{ + Shape *shape = new Shape(); + Shape *shape_temp = new Shape(); + + for (auto& child: children) { + // RH: is it right that this shouldn't be recursive? + auto c_child = cast<SPFlowregionExclude>(const_cast<SPObject*>(&child)); + if ( c_child && c_child->computed && c_child->computed->hasEdges() ) { + if (shape->hasEdges()) { + shape_temp->Booleen(shape, c_child->computed, bool_op_union); + std::swap(shape, shape_temp); + } else { + shape->Copy(c_child->computed); + } + } + } + + delete shape_temp; + + return shape; +} + +void SPFlowtext::rebuildLayout() +{ + std::list<Shape> shapes; + + layout.clear(); + Shape *exclusion_shape = _buildExclusionShape(); + SPObject *pending_line_break_object = nullptr; + _buildLayoutInput(this, exclusion_shape, &shapes, &pending_line_break_object); + delete exclusion_shape; + layout.calculateFlow(); +#if DEBUG_TEXTLAYOUT_DUMPASTEXT + g_print("%s", layout.dumpAsText().c_str()); +#endif +} + +void SPFlowtext::_clearFlow(Inkscape::DrawingGroup *in_arena) +{ + in_arena->clearChildren(); +} + +SPCurve SPFlowtext::getNormalizedBpath() const +{ + return layout.convertToCurves(); +} + +Inkscape::XML::Node *SPFlowtext::getAsText() +{ + if (!this->layout.outputExists()) { + return nullptr; + } + + Inkscape::XML::Document *xml_doc = this->document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:text"); + repr->setAttribute("xml:space", "preserve"); + repr->setAttribute("style", this->getRepr()->attribute("style")); + Geom::Point anchor_point = this->layout.characterAnchorPoint(this->layout.begin()); + repr->setAttributeSvgDouble("x", anchor_point[Geom::X]); + repr->setAttributeSvgDouble("y", anchor_point[Geom::Y]); + + for (Inkscape::Text::Layout::iterator it = this->layout.begin() ; it != this->layout.end() ; ) { + Inkscape::XML::Node *line_tspan = xml_doc->createElement("svg:tspan"); + line_tspan->setAttribute("sodipodi:role", "line"); + + Inkscape::Text::Layout::iterator it_line_end = it; + it_line_end.nextStartOfLine(); + + while (it != it_line_end) { + + Inkscape::XML::Node *span_tspan = xml_doc->createElement("svg:tspan"); + Geom::Point anchor_point = this->layout.characterAnchorPoint(it); + // use kerning to simulate justification and whatnot + Inkscape::Text::Layout::iterator it_span_end = it; + it_span_end.nextStartOfSpan(); + Inkscape::Text::Layout::OptionalTextTagAttrs attrs; + this->layout.simulateLayoutUsingKerning(it, it_span_end, &attrs); + // set x,y attributes only when we need to + bool set_x = false; + bool set_y = false; + if (!this->transform.isIdentity()) { + set_x = set_y = true; + } else { + Inkscape::Text::Layout::iterator it_chunk_start = it; + it_chunk_start.thisStartOfChunk(); + if (it == it_chunk_start) { + set_x = true; + // don't set y so linespacing adjustments and things will still work + } + Inkscape::Text::Layout::iterator it_shape_start = it; + it_shape_start.thisStartOfShape(); + if (it == it_shape_start) + set_y = true; + } + if (set_x && !attrs.dx.empty()) + attrs.dx[0] = 0.0; + TextTagAttributes(attrs).writeTo(span_tspan); + if (set_x) + span_tspan->setAttributeSvgDouble("x", anchor_point[Geom::X]); // FIXME: this will pick up the wrong end of counter-directional runs + if (set_y) + span_tspan->setAttributeSvgDouble("y", anchor_point[Geom::Y]); + if (line_tspan->childCount() == 0) { + line_tspan->setAttributeSvgDouble("x", anchor_point[Geom::X]); // FIXME: this will pick up the wrong end of counter-directional runs + line_tspan->setAttributeSvgDouble("y", anchor_point[Geom::Y]); + } + + SPObject *source_obj = nullptr; + Glib::ustring::iterator span_text_start_iter; + this->layout.getSourceOfCharacter(it, &source_obj, &span_text_start_iter); + + Glib::ustring style_text = (cast<SPString>(source_obj) ? source_obj->parent : source_obj) + ->style->writeIfDiff(this->style); + span_tspan->setAttributeOrRemoveIfEmpty("style", style_text); + + auto str = cast<SPString>(source_obj); + if (str) { + Glib::ustring *string = &(str->string); // TODO fixme: dangerous, unsafe premature-optimization + SPObject *span_end_obj = nullptr; + Glib::ustring::iterator span_text_end_iter; + this->layout.getSourceOfCharacter(it_span_end, &span_end_obj, &span_text_end_iter); + if (span_end_obj != source_obj) { + if (it_span_end == this->layout.end()) { + span_text_end_iter = span_text_start_iter; + for (int i = this->layout.iteratorToCharIndex(it_span_end) - this->layout.iteratorToCharIndex(it) ; i ; --i) + ++span_text_end_iter; + } else + span_text_end_iter = string->end(); // spans will never straddle a source boundary + } + + if (span_text_start_iter != span_text_end_iter) { + Glib::ustring new_string; + while (span_text_start_iter != span_text_end_iter) + new_string += *span_text_start_iter++; // grr. no substr() with iterators + Inkscape::XML::Node *new_text = xml_doc->createTextNode(new_string.c_str()); + span_tspan->appendChild(new_text); + Inkscape::GC::release(new_text); + } + } + it = it_span_end; + + line_tspan->appendChild(span_tspan); + Inkscape::GC::release(span_tspan); + } + repr->appendChild(line_tspan); + Inkscape::GC::release(line_tspan); + } + + return repr; +} + +SPItem const *SPFlowtext::get_frame(SPItem const *after) const +{ + SPItem *item = const_cast<SPFlowtext *>(this)->get_frame(after); + return item; +} + +SPItem *SPFlowtext::get_frame(SPItem const *after) +{ + SPItem *frame = nullptr; + + SPObject *region = nullptr; + for (auto& o: children) { + if (is<SPFlowregion>(&o)) { + region = &o; + break; + } + } + + if (region) { + bool past = false; + + for (auto& o: region->children) { + auto item = cast<SPItem>(&o); + if (item) { + if ( (after == nullptr) || past ) { + frame = item; + } else { + if (item == after) { + past = true; + } + } + } + } + + auto use = cast<SPUse>(frame); + if ( use ) { + frame = use->get_original(); + } + } + return frame; +} + +bool SPFlowtext::has_internal_frame() const +{ + SPItem const *frame = get_frame(nullptr); + + return (frame && isAncestorOf(frame) && cast<SPRect>(frame)); +} + + +SPItem *create_flowtext_with_internal_frame (SPDesktop *desktop, Geom::Point p0, Geom::Point p1) +{ + SPDocument *doc = desktop->getDocument(); + auto const parent = desktop->layerManager().currentLayer(); + assert(parent); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *root_repr = xml_doc->createElement("svg:flowRoot"); + root_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create + root_repr->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(parent->i2doc_affine().inverse())); + + /* Set style */ + sp_desktop_apply_style_tool(desktop, root_repr, "/tools/text", true); + + auto ft_item = cast<SPItem>(parent->appendChildRepr(root_repr)); + g_assert(ft_item != nullptr); + SPObject *root_object = doc->getObjectByRepr(root_repr); + g_assert(cast<SPFlowtext>(root_object) != nullptr); + + Inkscape::XML::Node *region_repr = xml_doc->createElement("svg:flowRegion"); + root_repr->appendChild(region_repr); + SPObject *region_object = doc->getObjectByRepr(region_repr); + g_assert(cast<SPFlowregion>(region_object) != nullptr); + + Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect"); // FIXME: use path!!! after rects are converted to use path + region_repr->appendChild(rect_repr); + + auto rect = cast<SPRect>(doc->getObjectByRepr(rect_repr)); + g_assert(rect != nullptr); + + p0 *= desktop->dt2doc(); + p1 *= desktop->dt2doc(); + using Geom::X; + using Geom::Y; + Geom::Coord const x0 = MIN(p0[X], p1[X]); + Geom::Coord const y0 = MIN(p0[Y], p1[Y]); + Geom::Coord const x1 = MAX(p0[X], p1[X]); + Geom::Coord const y1 = MAX(p0[Y], p1[Y]); + Geom::Coord const w = x1 - x0; + Geom::Coord const h = y1 - y0; + + rect->setPosition(x0, y0, w, h); + rect->updateRepr(); + + Inkscape::XML::Node *para_repr = xml_doc->createElement("svg:flowPara"); + root_repr->appendChild(para_repr); + SPObject *para_object = doc->getObjectByRepr(para_repr); + g_assert(cast<SPFlowpara>(para_object) != nullptr); + + Inkscape::XML::Node *text = xml_doc->createTextNode(""); + para_repr->appendChild(text); + + Inkscape::GC::release(root_repr); + Inkscape::GC::release(region_repr); + Inkscape::GC::release(para_repr); + Inkscape::GC::release(rect_repr); + + return ft_item; +} + +void SPFlowtext::fix_overflow_flowregion(bool inverse) +{ + SPObject *object = this; + for (auto child : object->childList(false)) { + auto flowregion = cast<SPFlowregion>(child); + if (flowregion) { + object = flowregion; + for (auto childshapes : object->childList(false)) { + Geom::Scale scale = Geom::Scale(1000); //200? maybe find better way to fix overglow issue removing new lines... + if (inverse) { + scale = scale.inverse(); + } + cast<SPItem>(childshapes)->doWriteTransform(scale, nullptr, true); + } + break; + } + } +} + +Geom::Affine SPFlowtext::set_transform (Geom::Affine const &xform) +{ + if ((this->_optimizeScaledText && !xform.withoutTranslation().isNonzeroUniformScale()) + || (!this->_optimizeScaledText && !xform.isNonzeroUniformScale())) { + this->_optimizeScaledText = false; + return xform; + } + this->_optimizeScaledText = false; + + SPText *text = reinterpret_cast<SPText *>(this); + + double const ex = xform.descrim(); + if (ex == 0) { + return xform; + } + + SPObject *region = nullptr; + for (auto& o: children) { + if (is<SPFlowregion>(&o)) { + region = &o; + break; + } + } + if (region) { + auto rect = cast<SPRect>(region->firstChild()); + if (rect) { + rect->set_i2d_affine(xform * rect->i2dt_affine()); + rect->doWriteTransform(rect->transform, nullptr, true); + } + } + + Geom::Affine ret(xform); + ret[0] /= ex; + ret[1] /= ex; + ret[2] /= ex; + ret[3] /= ex; + + // Adjust font size + text->_adjustFontsizeRecursive (this, ex); + + // Adjust stroke width + this->adjust_stroke_width_recursive (ex); + + // Adjust pattern fill + this->adjust_pattern(xform * ret.inverse()); + + // Adjust gradient fill + this->adjust_gradient(xform * ret.inverse()); + + return Geom::Affine(); +} + +/** + * Get the position of the baseline point for this text object. + */ +std::optional<Geom::Point> SPFlowtext::getBaselinePoint() const +{ + if (layout.outputExists()) { + return layout.baselineAnchorPoint(); + } + return std::optional<Geom::Point>(); +} + +/* + 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 : diff --git a/src/object/sp-flowtext.h b/src/object/sp-flowtext.h new file mode 100644 index 0000000..da40d97 --- /dev/null +++ b/src/object/sp-flowtext.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_FLOWTEXT_H +#define SEEN_SP_ITEM_FLOWTEXT_H + +/* + */ + +#include <2geom/forward.h> + +#include "libnrtype/Layout-TNG.h" +#include "libnrtype/style-attachments.h" +#include "sp-item.h" +#include "desktop.h" +#include "display/curve.h" + +#include <memory> + +namespace Inkscape { + +class DrawingGroup; + +} // namespace Inkscape + +class SPFlowtext final : public SPItem { +public: + SPFlowtext(); + ~SPFlowtext() override; + int tag() const override { return tag_of<decltype(*this)>; } + + /** Completely recalculates the layout. */ + void rebuildLayout(); + + /** Converts the flowroot in into a \<text\> tree, keeping all the formatting and positioning, + but losing the automatic wrapping ability. */ + Inkscape::XML::Node *getAsText(); + + // TODO check if these should return SPRect instead of SPItem + + SPItem *get_frame(SPItem const *after); + + SPItem const *get_frame(SPItem const *after) const; + + std::optional<Geom::Point> getBaselinePoint() const; + + bool has_internal_frame() const; + +//semiprivate: (need to be accessed by the C-style functions still) + Inkscape::Text::Layout layout; + std::unordered_map<unsigned, Inkscape::Text::StyleAttachments> view_style_attachments; + + /** discards the drawing objects representing this text. */ + void _clearFlow(Inkscape::DrawingGroup* in_arena); + + double par_indent; + + bool _optimizeScaledText; + + /** Converts the text object to its component curves */ + SPCurve getNormalizedBpath() const; + + /** Optimize scaled flow text on next set_transform. */ + void optimizeScaledText() { _optimizeScaledText = true; } + +private: + /** Recursively walks the xml tree adding tags and their contents. */ + void _buildLayoutInput(SPObject *root, Shape const *exclusion_shape, std::list<Shape> *shapes, SPObject **pending_line_break_object); + + /** calculates the union of all the \<flowregionexclude\> children + of this flowroot. */ + Shape* _buildExclusionShape() const; + +public: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void set(SPAttr key, const char* value) override; + Geom::Affine set_transform(Geom::Affine const& xform) override; + + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + void fix_overflow_flowregion(bool inverse); + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + void print(SPPrintContext *ctx) override; + const char* typeName() const override; + const char* displayName() const override; + char* description() const override; + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void hide(unsigned int key) override; + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; +}; + +SPItem *create_flowtext_with_internal_frame (SPDesktop *desktop, Geom::Point p1, Geom::Point p2); + +#endif // SEEN_SP_ITEM_FLOWTEXT_H + +/* + 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 : diff --git a/src/object/sp-font-face.cpp b/src/object/sp-font-face.cpp new file mode 100644 index 0000000..5685a73 --- /dev/null +++ b/src/object/sp-font-face.cpp @@ -0,0 +1,825 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <font-face> element implementation + * + * Section 20.8.3 of the W3C SVG 1.1 spec + * available at: + * http://www.w3.org/TR/SVG/fonts.html#FontFaceElement + * + * Author: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Abhishek Sharma + * + * Copyright (C) 2008, Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/repr.h" +#include "attributes.h" +#include "sp-font-face.h" +#include "document.h" + +#include <cstring> + +static std::vector<FontFaceStyleType> sp_read_fontFaceStyleType(gchar const *value){ + std::vector<FontFaceStyleType> v; + + if (!value){ + v.push_back(SP_FONTFACE_STYLE_ALL); + return v; + } + + if (strncmp(value, "all", 3) == 0){ + value += 3; + while(value[0]==',' || value[0]==' ') + value++; + v.push_back(SP_FONTFACE_STYLE_ALL); + return v; + } + + while(value[0]!='\0'){ + switch(value[0]){ + case 'n': + if (strncmp(value, "normal", 6) == 0){ + v.push_back(SP_FONTFACE_STYLE_NORMAL); + value += 6; + } + break; + case 'i': + if (strncmp(value, "italic", 6) == 0){ + v.push_back(SP_FONTFACE_STYLE_ITALIC); + value += 6; + } + break; + case 'o': + if (strncmp(value, "oblique", 7) == 0){ + v.push_back(SP_FONTFACE_STYLE_OBLIQUE); + value += 7; + } + break; + } + while(value[0]==',' || value[0]==' ') + value++; + } + return v; +} + +static std::vector<FontFaceVariantType> sp_read_fontFaceVariantType(gchar const *value){ + std::vector<FontFaceVariantType> v; + + if (!value){ + v.push_back(SP_FONTFACE_VARIANT_NORMAL); + return v; + } + + while(value[0]!='\0'){ + switch(value[0]){ + case 'n': + if (strncmp(value, "normal", 6) == 0){ + v.push_back(SP_FONTFACE_VARIANT_NORMAL); + value += 6; + } + break; + case 's': + if (strncmp(value, "small-caps", 10) == 0){ + v.push_back(SP_FONTFACE_VARIANT_SMALL_CAPS); + value += 10; + } + break; + } + while(value[0]==',' || value[0]==' ') + value++; + } + return v; +} + +static std::vector<FontFaceWeightType> sp_read_fontFaceWeightType(gchar const *value){ + std::vector<FontFaceWeightType> v; + + if (!value){ + v.push_back(SP_FONTFACE_WEIGHT_ALL); + return v; + } + + if (strncmp(value, "all", 3) == 0){ + value += 3; + while(value[0]==',' || value[0]==' ') + value++; + v.push_back(SP_FONTFACE_WEIGHT_ALL); + return v; + } + + while(value[0]!='\0'){ + switch(value[0]){ + case 'n': + if (strncmp(value, "normal", 6) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_NORMAL); + value += 6; + } + break; + case 'b': + if (strncmp(value, "bold", 4) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_BOLD); + value += 4; + } + break; + case '1': + if (strncmp(value, "100", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_100); + value += 3; + } + break; + case '2': + if (strncmp(value, "200", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_200); + value += 3; + } + break; + case '3': + if (strncmp(value, "300", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_300); + value += 3; + } + break; + case '4': + if (strncmp(value, "400", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_400); + value += 3; + } + break; + case '5': + if (strncmp(value, "500", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_500); + value += 3; + } + break; + case '6': + if (strncmp(value, "600", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_600); + value += 3; + } + break; + case '7': + if (strncmp(value, "700", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_700); + value += 3; + } + break; + case '8': + if (strncmp(value, "800", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_800); + value += 3; + } + break; + case '9': + if (strncmp(value, "900", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_900); + value += 3; + } + break; + } + while(value[0]==',' || value[0]==' ') + value++; + } + return v; +} + +static std::vector<FontFaceStretchType> sp_read_fontFaceStretchType(gchar const *value){ + std::vector<FontFaceStretchType> v; + + if (!value){ + v.push_back(SP_FONTFACE_STRETCH_NORMAL); + return v; + } + + if (strncmp(value, "all", 3) == 0){ + value += 3; + while(value[0]==',' || value[0]==' ') + value++; + v.push_back(SP_FONTFACE_STRETCH_ALL); + return v; + } + + while(value[0]!='\0'){ + switch(value[0]){ + case 'n': + if (strncmp(value, "normal", 6) == 0){ + v.push_back(SP_FONTFACE_STRETCH_NORMAL); + value += 6; + } + break; + case 'u': + if (strncmp(value, "ultra-condensed", 15) == 0){ + v.push_back(SP_FONTFACE_STRETCH_ULTRA_CONDENSED); + value += 15; + } + if (strncmp(value, "ultra-expanded", 14) == 0){ + v.push_back(SP_FONTFACE_STRETCH_ULTRA_EXPANDED); + value += 14; + } + break; + case 'e': + if (strncmp(value, "expanded", 8) == 0){ + v.push_back(SP_FONTFACE_STRETCH_EXPANDED); + value += 8; + } + if (strncmp(value, "extra-condensed", 15) == 0){ + v.push_back(SP_FONTFACE_STRETCH_EXTRA_CONDENSED); + value += 15; + } + if (strncmp(value, "extra-expanded", 14) == 0){ + v.push_back(SP_FONTFACE_STRETCH_EXTRA_EXPANDED); + value += 14; + } + break; + case 'c': + if (strncmp(value, "condensed", 9) == 0){ + v.push_back(SP_FONTFACE_STRETCH_CONDENSED); + value += 9; + } + break; + case 's': + if (strncmp(value, "semi-condensed", 14) == 0){ + v.push_back(SP_FONTFACE_STRETCH_SEMI_CONDENSED); + value += 14; + } + if (strncmp(value, "semi-expanded", 13) == 0){ + v.push_back(SP_FONTFACE_STRETCH_SEMI_EXPANDED); + value += 13; + } + break; + } + while(value[0]==',' || value[0]==' ') + value++; + } + return v; +} + +SPFontFace::SPFontFace() : SPObject() { + std::vector<FontFaceStyleType> style; + style.push_back(SP_FONTFACE_STYLE_ALL); + this->font_style = style; + + std::vector<FontFaceVariantType> variant; + variant.push_back(SP_FONTFACE_VARIANT_NORMAL); + this->font_variant = variant; + + std::vector<FontFaceWeightType> weight; + weight.push_back(SP_FONTFACE_WEIGHT_ALL); + this->font_weight = weight; + + std::vector<FontFaceStretchType> stretch; + stretch.push_back(SP_FONTFACE_STRETCH_NORMAL); + this->font_stretch = stretch; + this->font_family = nullptr; + + //this->font_style = ; + //this->font_variant = ; + //this->font_weight = ; + //this->font_stretch = ; + this->font_size = nullptr; + //this->unicode_range = ; + this->units_per_em = 1000; + //this->panose_1 = ; + this->stemv = 0; + this->stemh = 0; + this->slope = 0; + this->cap_height = 0; + this->x_height = 0; + this->accent_height = 0; + this->ascent = 0; + this->descent = 0; + this->widths = nullptr; + this->bbox = nullptr; + this->ideographic = 0; + this->alphabetic = 0; + this->mathematical = 0; + this->hanging = 0; + this->v_ideographic = 0; + this->v_alphabetic = 0; + this->v_mathematical = 0; + this->v_hanging = 0; + this->underline_position = 0; + this->underline_thickness = 0; + this->strikethrough_position = 0; + this->strikethrough_thickness = 0; + this->overline_position = 0; + this->overline_thickness = 0; +} + +SPFontFace::~SPFontFace() = default; + +void SPFontFace::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObject::build(document, repr); + + this->readAttr(SPAttr::FONT_FAMILY); + this->readAttr(SPAttr::FONT_STYLE); + this->readAttr(SPAttr::FONT_VARIANT); + this->readAttr(SPAttr::FONT_WEIGHT); + this->readAttr(SPAttr::FONT_STRETCH); + this->readAttr(SPAttr::FONT_SIZE); + this->readAttr(SPAttr::UNICODE_RANGE); + this->readAttr(SPAttr::UNITS_PER_EM); + this->readAttr(SPAttr::PANOSE_1); + this->readAttr(SPAttr::STEMV); + this->readAttr(SPAttr::STEMH); + this->readAttr(SPAttr::SLOPE); + this->readAttr(SPAttr::CAP_HEIGHT); + this->readAttr(SPAttr::X_HEIGHT); + this->readAttr(SPAttr::ACCENT_HEIGHT); + this->readAttr(SPAttr::ASCENT); + this->readAttr(SPAttr::DESCENT); + this->readAttr(SPAttr::WIDTHS); + this->readAttr(SPAttr::BBOX); + this->readAttr(SPAttr::IDEOGRAPHIC); + this->readAttr(SPAttr::ALPHABETIC); + this->readAttr(SPAttr::MATHEMATICAL); + this->readAttr(SPAttr::HANGING); + this->readAttr(SPAttr::V_IDEOGRAPHIC); + this->readAttr(SPAttr::V_ALPHABETIC); + this->readAttr(SPAttr::V_MATHEMATICAL); + this->readAttr(SPAttr::V_HANGING); + this->readAttr(SPAttr::UNDERLINE_POSITION); + this->readAttr(SPAttr::UNDERLINE_THICKNESS); + this->readAttr(SPAttr::STRIKETHROUGH_POSITION); + this->readAttr(SPAttr::STRIKETHROUGH_THICKNESS); + this->readAttr(SPAttr::OVERLINE_POSITION); + this->readAttr(SPAttr::OVERLINE_THICKNESS); +} + +/** + * Callback for child_added event. + */ +void SPFontFace::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObject::child_added(child, ref); + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +/** + * Callback for remove_child event. + */ +void SPFontFace::remove_child(Inkscape::XML::Node *child) { + SPObject::remove_child(child); + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFontFace::release() { + SPObject::release(); +} + +void SPFontFace::set(SPAttr key, const gchar *value) { + std::vector<FontFaceStyleType> style; + std::vector<FontFaceVariantType> variant; + std::vector<FontFaceWeightType> weight; + std::vector<FontFaceStretchType> stretch; + + switch (key) { + case SPAttr::FONT_FAMILY: + if (this->font_family) { + g_free(this->font_family); + } + + this->font_family = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::FONT_STYLE: + style = sp_read_fontFaceStyleType(value); + + if (this->font_style.size() != style.size()){ + this->font_style = style; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + for (unsigned int i=0;i<style.size();i++){ + if (style[i] != this->font_style[i]){ + this->font_style = style; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + } + } + break; + case SPAttr::FONT_VARIANT: + variant = sp_read_fontFaceVariantType(value); + + if (this->font_variant.size() != variant.size()){ + this->font_variant = variant; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + for (unsigned int i=0;i<variant.size();i++){ + if (variant[i] != this->font_variant[i]){ + this->font_variant = variant; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + } + } + break; + case SPAttr::FONT_WEIGHT: + weight = sp_read_fontFaceWeightType(value); + + if (this->font_weight.size() != weight.size()){ + this->font_weight = weight; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + for (unsigned int i=0;i<weight.size();i++){ + if (weight[i] != this->font_weight[i]){ + this->font_weight = weight; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + } + } + break; + case SPAttr::FONT_STRETCH: + stretch = sp_read_fontFaceStretchType(value); + + if (this->font_stretch.size() != stretch.size()){ + this->font_stretch = stretch; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + for (unsigned int i=0;i<stretch.size();i++){ + if (stretch[i] != this->font_stretch[i]){ + this->font_stretch = stretch; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + } + } + break; + case SPAttr::UNITS_PER_EM: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->units_per_em){ + this->units_per_em = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::STEMV: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->stemv){ + this->stemv = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::STEMH: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->stemh){ + this->stemh = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::SLOPE: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->slope){ + this->slope = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::CAP_HEIGHT: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->cap_height){ + this->cap_height = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::X_HEIGHT: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->x_height){ + this->x_height = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::ACCENT_HEIGHT: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->accent_height){ + this->accent_height = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::ASCENT: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->ascent){ + this->ascent = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::DESCENT: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->descent){ + this->descent = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::IDEOGRAPHIC: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->ideographic){ + this->ideographic = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::ALPHABETIC: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->alphabetic){ + this->alphabetic = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::MATHEMATICAL: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->mathematical){ + this->mathematical = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::HANGING: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->hanging){ + this->hanging = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::V_IDEOGRAPHIC: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->v_ideographic){ + this->v_ideographic = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::V_ALPHABETIC: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->v_alphabetic){ + this->v_alphabetic = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::V_MATHEMATICAL: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->v_mathematical){ + this->v_mathematical = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::V_HANGING: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->v_hanging){ + this->v_hanging = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::UNDERLINE_POSITION: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->underline_position){ + this->underline_position = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::UNDERLINE_THICKNESS: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->underline_thickness){ + this->underline_thickness = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::STRIKETHROUGH_POSITION: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->strikethrough_position){ + this->strikethrough_position = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::STRIKETHROUGH_THICKNESS: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->strikethrough_thickness){ + this->strikethrough_thickness = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::OVERLINE_POSITION: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->overline_position){ + this->overline_position = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::OVERLINE_THICKNESS: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->overline_thickness){ + this->overline_thickness = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + SPObject::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFontFace::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG)) { + this->readAttr(SPAttr::FONT_FAMILY); + this->readAttr(SPAttr::FONT_STYLE); + this->readAttr(SPAttr::FONT_VARIANT); + this->readAttr(SPAttr::FONT_WEIGHT); + this->readAttr(SPAttr::FONT_STRETCH); + this->readAttr(SPAttr::FONT_SIZE); + this->readAttr(SPAttr::UNICODE_RANGE); + this->readAttr(SPAttr::UNITS_PER_EM); + this->readAttr(SPAttr::PANOSE_1); + this->readAttr(SPAttr::STEMV); + this->readAttr(SPAttr::STEMH); + this->readAttr(SPAttr::SLOPE); + this->readAttr(SPAttr::CAP_HEIGHT); + this->readAttr(SPAttr::X_HEIGHT); + this->readAttr(SPAttr::ACCENT_HEIGHT); + this->readAttr(SPAttr::ASCENT); + this->readAttr(SPAttr::DESCENT); + this->readAttr(SPAttr::WIDTHS); + this->readAttr(SPAttr::BBOX); + this->readAttr(SPAttr::IDEOGRAPHIC); + this->readAttr(SPAttr::ALPHABETIC); + this->readAttr(SPAttr::MATHEMATICAL); + this->readAttr(SPAttr::HANGING); + this->readAttr(SPAttr::V_IDEOGRAPHIC); + this->readAttr(SPAttr::V_ALPHABETIC); + this->readAttr(SPAttr::V_MATHEMATICAL); + this->readAttr(SPAttr::V_HANGING); + this->readAttr(SPAttr::UNDERLINE_POSITION); + this->readAttr(SPAttr::UNDERLINE_THICKNESS); + this->readAttr(SPAttr::STRIKETHROUGH_POSITION); + this->readAttr(SPAttr::STRIKETHROUGH_THICKNESS); + this->readAttr(SPAttr::OVERLINE_POSITION); + this->readAttr(SPAttr::OVERLINE_THICKNESS); + } + + SPObject::update(ctx, flags); +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPFontFace::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:font-face"); + } + + //TODO: + //repr->setAttributeSvgDouble("font-family", face->font_family); + //repr->setAttributeSvgDouble("font-style", face->font_style); + //repr->setAttributeSvgDouble("font-variant", face->font_variant); + //repr->setAttributeSvgDouble("font-weight", face->font_weight); + //repr->setAttributeSvgDouble("font-stretch", face->font_stretch); + //repr->setAttributeSvgDouble("font-size", face->font_size); + //repr->setAttributeSvgDouble("unicode-range", face->unicode_range); + repr->setAttributeSvgDouble("units-per-em", this->units_per_em); + //repr->setAttributeSvgDouble("panose-1", face->panose_1); + repr->setAttributeSvgDouble("stemv", this->stemv); + repr->setAttributeSvgDouble("stemh", this->stemh); + repr->setAttributeSvgDouble("slope", this->slope); + repr->setAttributeSvgDouble("cap-height", this->cap_height); + repr->setAttributeSvgDouble("x-height", this->x_height); + repr->setAttributeSvgDouble("accent-height", this->accent_height); + repr->setAttributeSvgDouble("ascent", this->ascent); + repr->setAttributeSvgDouble("descent", this->descent); + //repr->setAttributeSvgDouble("widths", face->widths); + //repr->setAttributeSvgDouble("bbox", face->bbox); + repr->setAttributeSvgDouble("ideographic", this->ideographic); + repr->setAttributeSvgDouble("alphabetic", this->alphabetic); + repr->setAttributeSvgDouble("mathematical", this->mathematical); + repr->setAttributeSvgDouble("hanging", this->hanging); + repr->setAttributeSvgDouble("v-ideographic", this->v_ideographic); + repr->setAttributeSvgDouble("v-alphabetic", this->v_alphabetic); + repr->setAttributeSvgDouble("v-mathematical", this->v_mathematical); + repr->setAttributeSvgDouble("v-hanging", this->v_hanging); + repr->setAttributeSvgDouble("underline-position", this->underline_position); + repr->setAttributeSvgDouble("underline-thickness", this->underline_thickness); + repr->setAttributeSvgDouble("strikethrough-position", this->strikethrough_position); + repr->setAttributeSvgDouble("strikethrough-thickness", this->strikethrough_thickness); + repr->setAttributeSvgDouble("overline-position", this->overline_position); + repr->setAttributeSvgDouble("overline-thickness", this->overline_thickness); + + if (repr != this->getRepr()) { + // In all COPY_ATTR given below the XML tree is + // being used directly while it shouldn't be. + COPY_ATTR(repr, this->getRepr(), "font-family"); + COPY_ATTR(repr, this->getRepr(), "font-style"); + COPY_ATTR(repr, this->getRepr(), "font-variant"); + COPY_ATTR(repr, this->getRepr(), "font-weight"); + COPY_ATTR(repr, this->getRepr(), "font-stretch"); + COPY_ATTR(repr, this->getRepr(), "font-size"); + COPY_ATTR(repr, this->getRepr(), "unicode-range"); + COPY_ATTR(repr, this->getRepr(), "units-per-em"); + COPY_ATTR(repr, this->getRepr(), "panose-1"); + COPY_ATTR(repr, this->getRepr(), "stemv"); + COPY_ATTR(repr, this->getRepr(), "stemh"); + COPY_ATTR(repr, this->getRepr(), "slope"); + COPY_ATTR(repr, this->getRepr(), "cap-height"); + COPY_ATTR(repr, this->getRepr(), "x-height"); + COPY_ATTR(repr, this->getRepr(), "accent-height"); + COPY_ATTR(repr, this->getRepr(), "ascent"); + COPY_ATTR(repr, this->getRepr(), "descent"); + COPY_ATTR(repr, this->getRepr(), "widths"); + COPY_ATTR(repr, this->getRepr(), "bbox"); + COPY_ATTR(repr, this->getRepr(), "ideographic"); + COPY_ATTR(repr, this->getRepr(), "alphabetic"); + COPY_ATTR(repr, this->getRepr(), "mathematical"); + COPY_ATTR(repr, this->getRepr(), "hanging"); + COPY_ATTR(repr, this->getRepr(), "v-ideographic"); + COPY_ATTR(repr, this->getRepr(), "v-alphabetic"); + COPY_ATTR(repr, this->getRepr(), "v-mathematical"); + COPY_ATTR(repr, this->getRepr(), "v-hanging"); + COPY_ATTR(repr, this->getRepr(), "underline-position"); + COPY_ATTR(repr, this->getRepr(), "underline-thickness"); + COPY_ATTR(repr, this->getRepr(), "strikethrough-position"); + COPY_ATTR(repr, this->getRepr(), "strikethrough-thickness"); + COPY_ATTR(repr, this->getRepr(), "overline-position"); + COPY_ATTR(repr, this->getRepr(), "overline-thickness"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} +/* + 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 : diff --git a/src/object/sp-font-face.h b/src/object/sp-font-face.h new file mode 100644 index 0000000..be242d6 --- /dev/null +++ b/src/object/sp-font-face.h @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_FONTFACE_H +#define SEEN_SP_FONTFACE_H + +#include <vector> + +/* + * SVG <font-face> element implementation + * + * Section 20.8.3 of the W3C SVG 1.1 spec + * available at: + * http://www.w3.org/TR/SVG/fonts.html#FontFaceElement + * + * Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +enum FontFaceStyleType{ + SP_FONTFACE_STYLE_ALL, + SP_FONTFACE_STYLE_NORMAL, + SP_FONTFACE_STYLE_ITALIC, + SP_FONTFACE_STYLE_OBLIQUE +}; + +enum FontFaceVariantType{ + SP_FONTFACE_VARIANT_NORMAL, + SP_FONTFACE_VARIANT_SMALL_CAPS +}; + +enum FontFaceWeightType{ + SP_FONTFACE_WEIGHT_ALL, + SP_FONTFACE_WEIGHT_NORMAL, + SP_FONTFACE_WEIGHT_BOLD, + SP_FONTFACE_WEIGHT_100, + SP_FONTFACE_WEIGHT_200, + SP_FONTFACE_WEIGHT_300, + SP_FONTFACE_WEIGHT_400, + SP_FONTFACE_WEIGHT_500, + SP_FONTFACE_WEIGHT_600, + SP_FONTFACE_WEIGHT_700, + SP_FONTFACE_WEIGHT_800, + SP_FONTFACE_WEIGHT_900 +}; + +enum FontFaceStretchType{ + SP_FONTFACE_STRETCH_ALL, + SP_FONTFACE_STRETCH_NORMAL, + SP_FONTFACE_STRETCH_ULTRA_CONDENSED, + SP_FONTFACE_STRETCH_EXTRA_CONDENSED, + SP_FONTFACE_STRETCH_CONDENSED, + SP_FONTFACE_STRETCH_SEMI_CONDENSED, + SP_FONTFACE_STRETCH_SEMI_EXPANDED, + SP_FONTFACE_STRETCH_EXPANDED, + SP_FONTFACE_STRETCH_EXTRA_EXPANDED, + SP_FONTFACE_STRETCH_ULTRA_EXPANDED +}; + +enum FontFaceUnicodeRangeType{ + FONTFACE_UNICODERANGE_FIXME_HERE, +}; + +class SPFontFace final : public SPObject { +public: + SPFontFace(); + ~SPFontFace() override; + int tag() const override { return tag_of<decltype(*this)>; } + + char* font_family; + std::vector<FontFaceStyleType> font_style; + std::vector<FontFaceVariantType> font_variant; + std::vector<FontFaceWeightType> font_weight; + std::vector<FontFaceStretchType> font_stretch; + char* font_size; + std::vector<FontFaceUnicodeRangeType> unicode_range; + double units_per_em; + std::vector<int> panose_1; + double stemv; + double stemh; + double slope; + double cap_height; + double x_height; + double accent_height; + double ascent; + double descent; + char* widths; + char* bbox; + double ideographic; + double alphabetic; + double mathematical; + double hanging; + double v_ideographic; + double v_alphabetic; + double v_mathematical; + double v_hanging; + double underline_position; + double underline_thickness; + double strikethrough_position; + double strikethrough_thickness; + double overline_position; + double overline_thickness; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void set(SPAttr key, const char* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif //#ifndef __SP_FONTFACE_H__ diff --git a/src/object/sp-font.cpp b/src/object/sp-font.cpp new file mode 100644 index 0000000..a1cd5b7 --- /dev/null +++ b/src/object/sp-font.cpp @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <font> element implementation + * + * Author: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Abhishek Sharma + * + * Copyright (C) 2008, Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/repr.h" +#include "attributes.h" +#include "sp-font.h" +#include "sp-glyph.h" +#include "document.h" + +#include "display/nr-svgfonts.h" + + +//I think we should have extra stuff here and in the set method in order to set default value as specified at http://www.w3.org/TR/SVG/fonts.html + +// TODO determine better values and/or make these dynamic: +double FNT_DEFAULT_ADV = 1024; // TODO determine proper default +double FNT_DEFAULT_ASCENT = 768; // TODO determine proper default +double FNT_UNITS_PER_EM = 1024; // TODO determine proper default + +SPFont::SPFont() : SPObject() { + this->horiz_origin_x = 0; + this->horiz_origin_y = 0; + this->horiz_adv_x = FNT_DEFAULT_ADV; + this->vert_origin_x = FNT_DEFAULT_ADV / 2.0; + this->vert_origin_y = FNT_DEFAULT_ASCENT; + this->vert_adv_y = FNT_UNITS_PER_EM; +} + +SPFont::~SPFont() = default; + +void SPFont::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObject::build(document, repr); + + this->readAttr(SPAttr::HORIZ_ORIGIN_X); + this->readAttr(SPAttr::HORIZ_ORIGIN_Y); + this->readAttr(SPAttr::HORIZ_ADV_X); + this->readAttr(SPAttr::VERT_ORIGIN_X); + this->readAttr(SPAttr::VERT_ORIGIN_Y); + this->readAttr(SPAttr::VERT_ADV_Y); + + document->addResource("font", this); +} + +/** + * Callback for child_added event. + */ +void SPFont::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObject::child_added(child, ref); + + if (!_block) this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +/** + * Callback for remove_child event. + */ +void SPFont::remove_child(Inkscape::XML::Node* child) { + SPObject::remove_child(child); + + if (!_block) this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFont::release() { + this->document->removeResource("font", this); + + SPObject::release(); +} + +void SPFont::set(SPAttr key, const gchar *value) { + // TODO these are floating point, so some epsilon comparison would be good + switch (key) { + case SPAttr::HORIZ_ORIGIN_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->horiz_origin_x){ + this->horiz_origin_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::HORIZ_ORIGIN_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->horiz_origin_y){ + this->horiz_origin_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::HORIZ_ADV_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : FNT_DEFAULT_ADV; + + if (number != this->horiz_adv_x){ + this->horiz_adv_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::VERT_ORIGIN_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : FNT_DEFAULT_ADV / 2.0; + + if (number != this->vert_origin_x){ + this->vert_origin_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::VERT_ORIGIN_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : FNT_DEFAULT_ASCENT; + + if (number != this->vert_origin_y){ + this->vert_origin_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::VERT_ADV_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : FNT_UNITS_PER_EM; + + if (number != this->vert_adv_y){ + this->vert_adv_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + SPObject::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFont::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG)) { + this->readAttr(SPAttr::HORIZ_ORIGIN_X); + this->readAttr(SPAttr::HORIZ_ORIGIN_Y); + this->readAttr(SPAttr::HORIZ_ADV_X); + this->readAttr(SPAttr::VERT_ORIGIN_X); + this->readAttr(SPAttr::VERT_ORIGIN_Y); + this->readAttr(SPAttr::VERT_ADV_Y); + } + + SPObject::update(ctx, flags); +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPFont::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:font"); + } + + repr->setAttributeSvgDouble("horiz-origin-x", this->horiz_origin_x); + repr->setAttributeSvgDouble("horiz-origin-y", this->horiz_origin_y); + repr->setAttributeSvgDouble("horiz-adv-x", this->horiz_adv_x); + repr->setAttributeSvgDouble("vert-origin-x", this->vert_origin_x); + repr->setAttributeSvgDouble("vert-origin-y", this->vert_origin_y); + repr->setAttributeSvgDouble("vert-adv-y", this->vert_adv_y); + + if (repr != this->getRepr()) { + // All the below COPY_ATTR functions are directly using + // the XML Tree while they shouldn't + COPY_ATTR(repr, this->getRepr(), "horiz-origin-x"); + COPY_ATTR(repr, this->getRepr(), "horiz-origin-y"); + COPY_ATTR(repr, this->getRepr(), "horiz-adv-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-y"); + COPY_ATTR(repr, this->getRepr(), "vert-adv-y"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +using Inkscape::XML::Node; + +SPGlyph* SPFont::create_new_glyph(const char* name, const char* unicode) { + + Inkscape::XML::Document* xml_doc = document->getReprDoc(); + + // create a new glyph + Inkscape::XML::Node* grepr = xml_doc->createElement("svg:glyph"); + + grepr->setAttribute("glyph-name", name); + grepr->setAttribute("unicode", unicode); + + // Append the new glyph node to the current font + getRepr()->appendChild(grepr); + Inkscape::GC::release(grepr); + + // get corresponding object + auto g = cast<SPGlyph>(document->getObjectByRepr(grepr)); + + g_assert(g != nullptr); + + g->setCollectionPolicy(SPObject::COLLECT_WITH_PARENT); + + return g; +} + +void SPFont::sort_glyphs() { + auto* repr = getRepr(); + g_assert(repr); + + std::vector<std::pair<SPGlyph*, Node*>> glyphs; + glyphs.reserve(repr->childCount()); + + // collect all glyphs (SPGlyph and their representations) + for (auto&& node : children) { + if (auto g = cast<SPGlyph>(&node)) { + glyphs.emplace_back(g, g->getRepr()); + // keep representation around as it gets removed + g->getRepr()->anchor(); + } + } + + // now sort by unicode point + std::stable_sort(begin(glyphs), end(glyphs), [](const std::pair<SPGlyph*, Node*>& a, const std::pair<SPGlyph*, Node*>& b) { + // compare individual unicode points in each string one by one to establish glyph order + // note: ustring operator< doesn't work as expected + const auto& str1 = a.first->unicode; + const auto& str2 = b.first->unicode; + return std::lexicographical_compare(str1.begin(), str1.end(), str2.begin(), str2.end()); + }); + + // remove all glyph nodes from the document; block notifications + _block = true; + + for (auto&& glyph : glyphs) { + repr->removeChild(glyph.second); + } + + // re-add them in the desired order + for (auto&& glyph : glyphs) { + repr->appendChild(glyph.second); + glyph.second->release(); + } + + _block = false; + // notify listeners about the change + parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/* + 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 : diff --git a/src/object/sp-font.h b/src/object/sp-font.h new file mode 100644 index 0000000..56ceb27 --- /dev/null +++ b/src/object/sp-font.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_FONT_H_SEEN +#define SP_FONT_H_SEEN + +/* + * SVG <font> element implementation + * + * Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" +class SPGlyph; + +class SPFont final : public SPObject { +public: + SPFont(); + ~SPFont() override; + int tag() const override { return tag_of<decltype(*this)>; } + + double horiz_origin_x; + double horiz_origin_y; + double horiz_adv_x; + double vert_origin_x; + double vert_origin_y; + double vert_adv_y; + + // add new glyph to the font with optional name and given unicode string (code point, or code points for the glyph) + SPGlyph* create_new_glyph(const char* name, const char* unicode); + + // sort glyphs in the font by "unicode" attribute (code points) + void sort_glyphs(); + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void set(SPAttr key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + +private: + bool _block = false; +}; + +#endif //#ifndef SP_FONT_H_SEEN diff --git a/src/object/sp-glyph-kerning.cpp b/src/object/sp-glyph-kerning.cpp new file mode 100644 index 0000000..f924773 --- /dev/null +++ b/src/object/sp-glyph-kerning.cpp @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * SVG <hkern> and <vkern> elements implementation + * W3C SVG 1.1 spec, page 476, section 20.7 + * + * Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Abhishek Sharma + * + * Copyright (C) 2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/repr.h" +#include "attributes.h" +#include "sp-glyph-kerning.h" + +#include "document.h" +#include <cstring> + + +SPGlyphKerning::SPGlyphKerning() + : SPObject() +//TODO: correct these values: + , u1(nullptr) + , g1(nullptr) + , u2(nullptr) + , g2(nullptr) + , k(0) +{ +} + +void SPGlyphKerning::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + this->readAttr(SPAttr::U1); + this->readAttr(SPAttr::G1); + this->readAttr(SPAttr::U2); + this->readAttr(SPAttr::G2); + this->readAttr(SPAttr::K); +} + +void SPGlyphKerning::release() +{ + SPObject::release(); +} + +GlyphNames::GlyphNames(const gchar* value) +{ + names = value ? g_strdup(value) : nullptr; +} + +GlyphNames::~GlyphNames() +{ + if (names) { + g_free(names); + } +} + +bool GlyphNames::contains(const char* name) +{ + if (!(this->names) || !name) { + return false; + } + + std::istringstream is(this->names); + std::string str; + std::string s(name); + + while (is >> str) { + if (str == s) { + return true; + } + } + + return false; +} + +void SPGlyphKerning::set(SPAttr key, const gchar *value) +{ + switch (key) { + case SPAttr::U1: + { + if (this->u1) { + delete this->u1; + } + + this->u1 = new UnicodeRange(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::U2: + { + if (this->u2) { + delete this->u2; + } + + this->u2 = new UnicodeRange(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::G1: + { + if (this->g1) { + delete this->g1; + } + + this->g1 = new GlyphNames(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::G2: + { + if (this->g2) { + delete this->g2; + } + + this->g2 = new GlyphNames(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::K: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->k){ + this->k = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + { + SPObject::set(key, value); + break; + } + } +} + +/** + * Receives update notifications. + */ +void SPGlyphKerning::update(SPCtx *ctx, guint flags) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) { + /* do something to trigger redisplay, updates? */ + this->readAttr(SPAttr::U1); + this->readAttr(SPAttr::U2); + this->readAttr(SPAttr::G2); + this->readAttr(SPAttr::K); + } + + SPObject::update(ctx, flags); +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPGlyphKerning::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:glyphkerning"); // fix this! + } + + if (repr != this->getRepr()) { + // All the COPY_ATTR functions below use + // XML Tree directly, while they shouldn't. + COPY_ATTR(repr, this->getRepr(), "u1"); + COPY_ATTR(repr, this->getRepr(), "g1"); + COPY_ATTR(repr, this->getRepr(), "u2"); + COPY_ATTR(repr, this->getRepr(), "g2"); + COPY_ATTR(repr, this->getRepr(), "k"); + } + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* + 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 : diff --git a/src/object/sp-glyph-kerning.h b/src/object/sp-glyph-kerning.h new file mode 100644 index 0000000..c48d2c2 --- /dev/null +++ b/src/object/sp-glyph-kerning.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <hkern> and <vkern> elements implementation + * + * Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_GLYPH_KERNING_H +#define SEEN_SP_GLYPH_KERNING_H + +#include "sp-object.h" +#include "unicoderange.h" + +class GlyphNames { +public: + GlyphNames(char const* value); + ~GlyphNames(); + bool contains(char const* name); +private: + char* names; +}; + +class SPGlyphKerning : public SPObject { +public: + SPGlyphKerning(); + ~SPGlyphKerning() override = default; + int tag() const override { return tag_of<decltype(*this)>; } + + // FIXME encapsulation + UnicodeRange* u1; + GlyphNames* g1; + UnicodeRange* u2; + GlyphNames* g2; + double k; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttr key, char const* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +class SPHkern final : public SPGlyphKerning { + ~SPHkern() override = default; + int tag() const override { return tag_of<decltype(*this)>; } +}; + +class SPVkern final : public SPGlyphKerning { + ~SPVkern() override = default; + int tag() const override { return tag_of<decltype(*this)>; } +}; + +#endif // !SEEN_SP_GLYPH_KERNING_H + +/* + 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 : diff --git a/src/object/sp-glyph.cpp b/src/object/sp-glyph.cpp new file mode 100644 index 0000000..e8fecdf --- /dev/null +++ b/src/object/sp-glyph.cpp @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifdef HAVE_CONFIG_H +#endif + +/* + * SVG <glyph> element implementation + * + * Author: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Abhishek Sharma + * + * Copyright (C) 2008, Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/repr.h" +#include "attributes.h" +#include "sp-glyph.h" +#include "document.h" + +SPGlyph::SPGlyph() + : SPObject() +//TODO: correct these values: + , d(nullptr) + , orientation(GLYPH_ORIENTATION_BOTH) + , arabic_form(GLYPH_ARABIC_FORM_INITIAL) + , lang(nullptr) + , horiz_adv_x(0) + , vert_origin_x(0) + , vert_origin_y(0) + , vert_adv_y(0) +{ +} + +void SPGlyph::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + this->readAttr(SPAttr::UNICODE); + this->readAttr(SPAttr::GLYPH_NAME); + this->readAttr(SPAttr::D); + this->readAttr(SPAttr::ORIENTATION); + this->readAttr(SPAttr::ARABIC_FORM); + this->readAttr(SPAttr::LANG); + this->readAttr(SPAttr::HORIZ_ADV_X); + this->readAttr(SPAttr::VERT_ORIGIN_X); + this->readAttr(SPAttr::VERT_ORIGIN_Y); + this->readAttr(SPAttr::VERT_ADV_Y); +} + +void SPGlyph::release() { + SPObject::release(); +} + +static glyphArabicForm sp_glyph_read_arabic_form(gchar const *value){ + if (!value) { + return GLYPH_ARABIC_FORM_INITIAL; //TODO: verify which is the default default (for me, the spec is not clear) + } + + switch(value[0]){ + case 'i': + if (strncmp(value, "initial", 7) == 0) { + return GLYPH_ARABIC_FORM_INITIAL; + } + + if (strncmp(value, "isolated", 8) == 0) { + return GLYPH_ARABIC_FORM_ISOLATED; + } + break; + case 'm': + if (strncmp(value, "medial", 6) == 0) { + return GLYPH_ARABIC_FORM_MEDIAL; + } + break; + case 't': + if (strncmp(value, "terminal", 8) == 0) { + return GLYPH_ARABIC_FORM_TERMINAL; + } + break; + } + + return GLYPH_ARABIC_FORM_INITIAL; //TODO: VERIFY DEFAULT! +} + +static glyphOrientation sp_glyph_read_orientation(gchar const *value) +{ + if (!value) { + return GLYPH_ORIENTATION_BOTH; + } + + switch(value[0]){ + case 'h': + return GLYPH_ORIENTATION_HORIZONTAL; + break; + case 'v': + return GLYPH_ORIENTATION_VERTICAL; + break; + } + +//ERROR? TODO: VERIFY PROPER ERROR HANDLING + return GLYPH_ORIENTATION_BOTH; +} + +void SPGlyph::set(SPAttr key, const gchar *value) +{ + switch (key) { + case SPAttr::UNICODE: + { + this->unicode.clear(); + + if (value) { + this->unicode.append(value); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::GLYPH_NAME: + { + this->glyph_name.clear(); + + if (value) { + this->glyph_name.append(value); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::D: + { + if (this->d) { + g_free(this->d); + } + + this->d = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::ORIENTATION: + { + glyphOrientation orient = sp_glyph_read_orientation(value); + + if (this->orientation != orient){ + this->orientation = orient; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::ARABIC_FORM: + { + glyphArabicForm form = sp_glyph_read_arabic_form(value); + + if (this->arabic_form != form){ + this->arabic_form = form; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::LANG: + { + if (this->lang) { + g_free(this->lang); + } + + this->lang = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::HORIZ_ADV_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->horiz_adv_x){ + this->horiz_adv_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::VERT_ORIGIN_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->vert_origin_x){ + this->vert_origin_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::VERT_ORIGIN_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->vert_origin_y){ + this->vert_origin_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::VERT_ADV_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->vert_adv_y){ + this->vert_adv_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + { + SPObject::set(key, value); + break; + } + } +} + +/** + * Receives update notifications. + */ +void SPGlyph::update(SPCtx *ctx, guint flags) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) { + /* do something to trigger redisplay, updates? */ + this->readAttr(SPAttr::UNICODE); + this->readAttr(SPAttr::GLYPH_NAME); + this->readAttr(SPAttr::D); + this->readAttr(SPAttr::ORIENTATION); + this->readAttr(SPAttr::ARABIC_FORM); + this->readAttr(SPAttr::LANG); + this->readAttr(SPAttr::HORIZ_ADV_X); + this->readAttr(SPAttr::VERT_ORIGIN_X); + this->readAttr(SPAttr::VERT_ORIGIN_Y); + this->readAttr(SPAttr::VERT_ADV_Y); + } + + SPObject::update(ctx, flags); +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPGlyph::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:glyph"); + } + + /* I am commenting out this part because I am not certain how does it work. I will have to study it later. Juca + repr->setAttribute("unicode", glyph->unicode); + repr->setAttribute("glyph-name", glyph->glyph_name); + repr->setAttribute("d", glyph->d); + repr->setAttributeSvgDouble("orientation", (double) glyph->orientation); + repr->setAttributeSvgDouble("arabic-form", (double) glyph->arabic_form); + repr->setAttribute("lang", glyph->lang); + repr->setAttributeSvgDouble("horiz-adv-x", glyph->horiz_adv_x); + repr->setAttributeSvgDouble("vert-origin-x", glyph->vert_origin_x); + repr->setAttributeSvgDouble("vert-origin-y", glyph->vert_origin_y); + repr->setAttributeSvgDouble("vert-adv-y", glyph->vert_adv_y); + */ + + if (repr != this->getRepr()) { + // All the COPY_ATTR functions below use + // XML Tree directly while they shouldn't. + COPY_ATTR(repr, this->getRepr(), "unicode"); + COPY_ATTR(repr, this->getRepr(), "glyph-name"); + COPY_ATTR(repr, this->getRepr(), "d"); + COPY_ATTR(repr, this->getRepr(), "orientation"); + COPY_ATTR(repr, this->getRepr(), "arabic-form"); + COPY_ATTR(repr, this->getRepr(), "lang"); + COPY_ATTR(repr, this->getRepr(), "horiz-adv-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-y"); + COPY_ATTR(repr, this->getRepr(), "vert-adv-y"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* + 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 : diff --git a/src/object/sp-glyph.h b/src/object/sp-glyph.h new file mode 100644 index 0000000..b318875 --- /dev/null +++ b/src/object/sp-glyph.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_GLYPH_H +#define SEEN_SP_GLYPH_H + +#include "sp-object.h" + +enum glyphArabicForm { + GLYPH_ARABIC_FORM_INITIAL, + GLYPH_ARABIC_FORM_MEDIAL, + GLYPH_ARABIC_FORM_TERMINAL, + GLYPH_ARABIC_FORM_ISOLATED, +}; + +enum glyphOrientation { + GLYPH_ORIENTATION_HORIZONTAL, + GLYPH_ORIENTATION_VERTICAL, + GLYPH_ORIENTATION_BOTH +}; + +/* + * SVG <glyph> element + */ + +class SPGlyph final : public SPObject { +public: + SPGlyph(); + ~SPGlyph() override = default; + int tag() const override { return tag_of<decltype(*this)>; } + + // FIXME encapsulation + Glib::ustring unicode; + Glib::ustring glyph_name; + char* d; + glyphOrientation orientation; + glyphArabicForm arabic_form; + char* lang; + double horiz_adv_x; + double vert_origin_x; + double vert_origin_y; + double vert_adv_y; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttr key, const char* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif // !SEEN_SP_GLYPH_H + +/* + 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 : diff --git a/src/object/sp-gradient-reference.cpp b/src/object/sp-gradient-reference.cpp new file mode 100644 index 0000000..8608db2 --- /dev/null +++ b/src/object/sp-gradient-reference.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-gradient-reference.h" + +bool SPGradientReference::_acceptObject(SPObject *obj) const +{ + return is<SPGradient>(obj) && URIReference::_acceptObject(obj); + /* effic: Don't bother making this an inline function: _acceptObject is a virtual function, + typically called from a context where the runtime type is not known at compile time. */ +} + +/* + 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 : diff --git a/src/object/sp-gradient-reference.h b/src/object/sp-gradient-reference.h new file mode 100644 index 0000000..0470c6f --- /dev/null +++ b/src/object/sp-gradient-reference.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GRADIENT_REFERENCE_H +#define SEEN_SP_GRADIENT_REFERENCE_H + +#include "uri-references.h" +#include "sp-gradient.h" + +class SPGradientReference : public Inkscape::URIReference +{ +public: + SPGradientReference(SPGradient *grad) : URIReference(grad) {} + + SPGradient *getObject() const { + return static_cast<SPGradient *>(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject *obj) const override; +}; + +#endif /* !SEEN_SP_GRADIENT_REFERENCE_H */ + +/* + 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 : diff --git a/src/object/sp-gradient-spread.h b/src/object/sp-gradient-spread.h new file mode 100644 index 0000000..d28bec7 --- /dev/null +++ b/src/object/sp-gradient-spread.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GRADIENT_SPREAD_H +#define SEEN_SP_GRADIENT_SPREAD_H + +enum SPGradientSpread { + SP_GRADIENT_SPREAD_PAD, + SP_GRADIENT_SPREAD_REFLECT, + SP_GRADIENT_SPREAD_REPEAT, + SP_GRADIENT_SPREAD_UNDEFINED = INT_MAX +}; + +#endif /* !SEEN_SP_GRADIENT_SPREAD_H */ + +/* + 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 : diff --git a/src/object/sp-gradient-units.h b/src/object/sp-gradient-units.h new file mode 100644 index 0000000..394f25a --- /dev/null +++ b/src/object/sp-gradient-units.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GRADIENT_UNITS_H +#define SEEN_SP_GRADIENT_UNITS_H + +enum SPGradientUnits { + SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX, + SP_GRADIENT_UNITS_USERSPACEONUSE +}; + +#endif /* !SEEN_SP_GRADIENT_UNITS_H */ + +/* + 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 : diff --git a/src/object/sp-gradient-vector.h b/src/object/sp-gradient-vector.h new file mode 100644 index 0000000..893d41e --- /dev/null +++ b/src/object/sp-gradient-vector.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GRADIENT_VECTOR_H +#define SEEN_SP_GRADIENT_VECTOR_H + +#include <vector> +#include "color.h" + +/** + * Differs from SPStop in that SPStop mirrors the \<stop\> element in the document, whereas + * SPGradientStop shows more the effective stop color. + * + * For example, SPGradientStop has no currentColor option: currentColor refers to the color + * property value of the gradient where currentColor appears, so we interpret currentColor before + * copying from SPStop to SPGradientStop. + */ +struct SPGradientStop { + double offset; + SPColor color; + float opacity; +}; + +/** + * The effective gradient vector, after copying stops from the referenced gradient if necessary. + */ +struct SPGradientVector { + bool built; + std::vector<SPGradientStop> stops; +}; + +#endif /* !SEEN_SP_GRADIENT_VECTOR_H */ + +/* + 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 : diff --git a/src/object/sp-gradient.cpp b/src/object/sp-gradient.cpp new file mode 100644 index 0000000..1a81abd --- /dev/null +++ b/src/object/sp-gradient.cpp @@ -0,0 +1,1225 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SPGradient, SPStop, SPLinearGradient, SPRadialGradient, + * SPMeshGradient, SPMeshRow, SPMeshPatch + */ +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2004 David Turner + * Copyright (C) 2009 Jasper van de Gronde + * Copyright (C) 2011 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#define noSP_GRADIENT_VERBOSE +//#define OBJECT_TRACE + +#include "sp-gradient.h" + +#include <cstring> +#include <string> + +#include <2geom/transforms.h> + +#include <cairo.h> + +#include <sigc++/functors/ptr_fun.h> +#include <sigc++/adaptors/bind.h> + +#include "attributes.h" +#include "bad-uri-exception.h" +#include "document.h" +#include "gradient-chemistry.h" + +#include "sp-gradient-reference.h" +#include "sp-linear-gradient.h" +#include "sp-radial-gradient.h" +#include "sp-mesh-gradient.h" +#include "sp-mesh-row.h" +#include "sp-mesh-patch.h" +#include "sp-stop.h" + +#include "display/cairo-utils.h" + +#include "svg/svg.h" +#include "svg/css-ostringstream.h" +#include "xml/href-attribute-helper.h" + +bool SPGradient::hasStops() const +{ + return has_stops; +} + +bool SPGradient::hasPatches() const +{ + return has_patches; +} + +bool SPGradient::isUnitsSet() const +{ + return units_set; +} + +SPGradientUnits SPGradient::getUnits() const +{ + return units; +} + +bool SPGradient::isSpreadSet() const +{ + return spread_set; +} + +SPGradientSpread SPGradient::getSpread() const +{ + return spread; +} + +void SPGradient::setSwatch( bool swatch ) +{ + if ( swatch != isSwatch() ) { + this->swatch = swatch; // to make isSolid() work, this happens first + gchar const* paintVal = swatch ? (isSolid() ? "solid" : "gradient") : nullptr; + setAttribute( "inkscape:swatch", paintVal); + + requestModified( SP_OBJECT_MODIFIED_FLAG ); + } +} + +void SPGradient::setPinned(bool pinned) +{ + if (pinned != isPinned()) { + setAttribute("inkscape:pinned", pinned ? "true" : "false"); + requestModified(SP_OBJECT_MODIFIED_FLAG); + } +} + + +/** + * return true if this gradient is "equivalent" to that gradient. + * Equivalent meaning they have the same stop count, same stop colors and same stop opacity + * @param that - A gradient to compare this to + */ +bool SPGradient::isEquivalent(SPGradient *that) +{ + //TODO Make this work for mesh gradients + + bool status = false; + + while(true){ // not really a loop, used to avoid deep nesting or multiple exit points from function + if (this->getStopCount() != that->getStopCount()) { break; } + if (this->hasStops() != that->hasStops()) { break; } + if (!this->getVector() || !that->getVector()) { break; } + if (this->isSwatch() != that->isSwatch()) { break; } + if ( this->isSwatch() ){ + // drop down to check stops. + } + else if ( + (is<SPLinearGradient>(this) && is<SPLinearGradient>(that)) || + (is<SPRadialGradient>(this) && is<SPRadialGradient>(that)) || + (is<SPMeshGradient>(this) && is<SPMeshGradient>(that))) { + if(!this->isAligned(that))break; + } + else { break; } // this should never happen, some unhandled type of gradient + + SPStop *as = this->getVector()->getFirstStop(); + SPStop *bs = that->getVector()->getFirstStop(); + + bool effective = true; + while (effective && (as && bs)) { + if (!as->getColor().isClose(bs->getColor(), 0.001) || + as->offset != bs->offset || as->getOpacity() != bs->getOpacity() ) { + effective = false; + break; + } + else { + as = as->getNextStop(); + bs = bs->getNextStop(); + } + } + if (!effective) break; + + status = true; + break; + } + return status; +} + +/** + * return true if this gradient is "aligned" to that gradient. + * Aligned means that they have exactly the same coordinates and transform. + * @param that - A gradient to compare this to + */ +bool SPGradient::isAligned(SPGradient *that) +{ + bool status = false; + + /* Some gradients have coordinates/other values specified, some don't. + yes/yes check the coordinates/other values + no/no aligned (because both have all default values) + yes/no not aligned + no/yes not aligned + It is NOT safe to just compare the computed values because if that field has + not been set the computed value could be full of garbage. + + In theory the yes/no and no/yes cases could be aligned if the specified value + matches the default value. + */ + + while(true){ // not really a loop, used to avoid deep nesting or multiple exit points from function + if(this->gradientTransform_set != that->gradientTransform_set) { break; } + if(this->gradientTransform_set && + (this->gradientTransform != that->gradientTransform)) { break; } + if (is<SPLinearGradient>(this) && is<SPLinearGradient>(that)) { + auto sg = cast<SPLinearGradient>(this); + auto tg = cast<SPLinearGradient>(that); + + if( sg->x1._set != tg->x1._set) { break; } + if( sg->y1._set != tg->y1._set) { break; } + if( sg->x2._set != tg->x2._set) { break; } + if( sg->y2._set != tg->y2._set) { break; } + if( sg->x1._set && sg->y1._set && sg->x2._set && sg->y2._set) { + if( (sg->x1.computed != tg->x1.computed) || + (sg->y1.computed != tg->y1.computed) || + (sg->x2.computed != tg->x2.computed) || + (sg->y2.computed != tg->y2.computed) ) { break; } + } else if( sg->x1._set || sg->y1._set || sg->x2._set || sg->y2._set) { break; } // some mix of set and not set + // none set? assume aligned and fall through + } else if (is<SPRadialGradient>(this) && is<SPLinearGradient>(that)) { + auto sg = cast<SPRadialGradient>(this); + auto tg = cast<SPRadialGradient>(that); + + if( sg->cx._set != tg->cx._set) { break; } + if( sg->cy._set != tg->cy._set) { break; } + if( sg->r._set != tg->r._set) { break; } + if( sg->fx._set != tg->fx._set) { break; } + if( sg->fy._set != tg->fy._set) { break; } + if( sg->cx._set && sg->cy._set && sg->fx._set && sg->fy._set && sg->r._set) { + if( (sg->cx.computed != tg->cx.computed) || + (sg->cy.computed != tg->cy.computed) || + (sg->r.computed != tg->r.computed ) || + (sg->fx.computed != tg->fx.computed) || + (sg->fy.computed != tg->fy.computed) ) { break; } + } else if( sg->cx._set || sg->cy._set || sg->fx._set || sg->fy._set || sg->r._set ) { break; } // some mix of set and not set + // none set? assume aligned and fall through + } else if (is<SPMeshGradient>(this) && is<SPMeshGradient>(that)) { + auto sg = cast<SPMeshGradient>(this); + auto tg = cast<SPMeshGradient>(that); + + if( sg->x._set != !tg->x._set) { break; } + if( sg->y._set != !tg->y._set) { break; } + if( sg->x._set && sg->y._set) { + if( (sg->x.computed != tg->x.computed) || + (sg->y.computed != tg->y.computed) ) { break; } + } else if( sg->x._set || sg->y._set) { break; } // some mix of set and not set + // none set? assume aligned and fall through + } else { + break; + } + status = true; + break; + } + return status; +} + +/* + * Gradient + */ +SPGradient::SPGradient() : SPPaintServer(), units(), + spread(), + ref(nullptr), + state(2), + vector() { + + this->ref = new SPGradientReference(this); + this->ref->changedSignal().connect(sigc::bind(sigc::ptr_fun(SPGradient::gradientRefChanged), this)); + + /** \todo + * Fixme: reprs being rearranged (e.g. via the XML editor) + * may require us to clear the state. + */ + this->state = SP_GRADIENT_STATE_UNKNOWN; + + this->units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX; + this->units_set = FALSE; + + this->gradientTransform = Geom::identity(); + this->gradientTransform_set = FALSE; + + this->spread = SP_GRADIENT_SPREAD_PAD; + this->spread_set = FALSE; + + this->has_stops = FALSE; + this->has_patches = FALSE; + + this->vector.built = false; + this->vector.stops.clear(); +} + +SPGradient::~SPGradient() = default; + +/** + * Virtual build: set gradient attributes from its associated repr. + */ +void SPGradient::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + // Work-around in case a swatch had been marked for immediate collection: + if ( repr->attribute("inkscape:swatch") && repr->attribute("inkscape:collect") ) { + repr->removeAttribute("inkscape:collect"); + } + + this->readAttr(SPAttr::STYLE); + + SPPaintServer::build(document, repr); + + for (auto& ochild: children) { + if (is<SPStop>(&ochild)) { + this->has_stops = TRUE; + break; + } + if (is<SPMeshrow>(&ochild)) { + for (auto& ochild2: ochild.children) { + if (is<SPMeshpatch>(&ochild2)) { + this->has_patches = TRUE; + break; + } + } + if (this->has_patches == TRUE) { + break; + } + } + } + + this->readAttr(SPAttr::GRADIENTUNITS); + this->readAttr(SPAttr::GRADIENTTRANSFORM); + this->readAttr(SPAttr::SPREADMETHOD); + this->readAttr(SPAttr::XLINK_HREF); + this->readAttr(SPAttr::INKSCAPE_SWATCH); + this->readAttr(SPAttr::INKSCAPE_PINNED); + + // Register ourselves + document->addResource("gradient", this); +} + +/** + * Virtual release of SPGradient members before destruction. + */ +void SPGradient::release() +{ + +#ifdef SP_GRADIENT_VERBOSE + g_print("Releasing this %s\n", this->getId()); +#endif + + if (this->document) { + // Unregister ourselves + this->document->removeResource("gradient", this); + } + + if (this->ref) { + this->modified_connection.disconnect(); + this->ref->detach(); + delete this->ref; + this->ref = nullptr; + } + + SPPaintServer::release(); +} + +/** + * Set gradient attribute to value. + */ +void SPGradient::set(SPAttr key, gchar const *value) +{ +#ifdef OBJECT_TRACE + std::stringstream temp; + temp << "SPGradient::set: " << sp_attribute_name(key) << " " << (value?value:"null"); + objectTrace( temp.str() ); +#endif + + switch (key) { + case SPAttr::GRADIENTUNITS: + if (value) { + if (!strcmp(value, "userSpaceOnUse")) { + this->units = SP_GRADIENT_UNITS_USERSPACEONUSE; + } else { + this->units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX; + } + + this->units_set = TRUE; + } else { + this->units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX; + this->units_set = FALSE; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::GRADIENTTRANSFORM: { + Geom::Affine t; + if (value && sp_svg_transform_read(value, &t)) { + this->gradientTransform = t; + this->gradientTransform_set = TRUE; + } else { + this->gradientTransform = Geom::identity(); + this->gradientTransform_set = FALSE; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::SPREADMETHOD: + if (value) { + if (!strcmp(value, "reflect")) { + this->spread = SP_GRADIENT_SPREAD_REFLECT; + } else if (!strcmp(value, "repeat")) { + this->spread = SP_GRADIENT_SPREAD_REPEAT; + } else { + this->spread = SP_GRADIENT_SPREAD_PAD; + } + + this->spread_set = TRUE; + } else { + this->spread_set = FALSE; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::XLINK_HREF: + if (value) { + try { + this->ref->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + this->ref->detach(); + } + } else { + this->ref->detach(); + } + break; + + case SPAttr::INKSCAPE_PINNED: + { + if (value) { + this->_pinned = !strcmp(value, "true"); + } + break; + } + case SPAttr::INKSCAPE_SWATCH: + { + bool newVal = (value != nullptr); + bool modified = false; + + if (newVal != this->swatch) { + this->swatch = newVal; + modified = true; + } + + if (newVal) { + // Might need to flip solid/gradient + Glib::ustring paintVal = ( this->hasStops() && (this->getStopCount() <= 1) ) ? "solid" : "gradient"; + + if ( paintVal != value ) { + this->setAttribute( "inkscape:swatch", paintVal); + modified = true; + } + } + + if (modified) { + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } + break; + default: + SPPaintServer::set(key, value); + break; + } + +#ifdef OBJECT_TRACE + objectTrace( "SPGradient::set", false ); +#endif +} + +/** + * Gets called when the gradient is (re)attached to another gradient. + */ +void SPGradient::gradientRefChanged(SPObject *old_ref, SPObject *ref, SPGradient *gr) +{ + if (old_ref) { + gr->modified_connection.disconnect(); + } + if ( is<SPGradient>(ref) + && ref != gr ) + { + gr->modified_connection = ref->connectModified(sigc::bind<2>(sigc::ptr_fun(&SPGradient::gradientRefModified), gr)); + } + + // Per SVG, all unset attributes must be inherited from linked gradient. + // So, as we're now (re)linked, we assign linkee's values to this gradient if they are not yet set - + // but without setting the _set flags. + // FIXME: do the same for gradientTransform too + if (!gr->units_set) { + gr->units = gr->fetchUnits(); + } + if (!gr->spread_set) { + gr->spread = gr->fetchSpread(); + } + + /// \todo Fixme: what should the flags (second) argument be? */ + gradientRefModified(ref, 0, gr); +} + +/** + * Callback for child_added event. + */ +void SPGradient::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + this->invalidateVector(); + + SPPaintServer::child_added(child, ref); + + SPObject *ochild = this->get_child_by_repr(child); + if ( ochild && is<SPStop>(ochild) ) { + this->has_stops = TRUE; + if ( this->getStopCount() > 1 ) { + gchar const * attr = this->getAttribute("inkscape:swatch"); + if ( attr && strcmp(attr, "gradient") ) { + this->setAttribute( "inkscape:swatch", "gradient" ); + } + } + } + if ( ochild && is<SPMeshrow>(ochild) ) { + this->has_patches = TRUE; + } + + /// \todo Fixme: should we schedule "modified" here? + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Callback for remove_child event. + */ +void SPGradient::remove_child(Inkscape::XML::Node *child) +{ + this->invalidateVector(); + + SPPaintServer::remove_child(child); + + this->has_stops = FALSE; + this->has_patches = FALSE; + for (auto& ochild: children) { + if (is<SPStop>(&ochild)) { + this->has_stops = TRUE; + break; + } + if (is<SPMeshrow>(&ochild)) { + for (auto& ochild2: ochild.children) { + if (is<SPMeshpatch>(&ochild2)) { + this->has_patches = TRUE; + break; + } + } + if (this->has_patches == TRUE) { + break; + } + } + } + + if ( this->getStopCount() <= 1 ) { + gchar const * attr = this->getAttribute("inkscape:swatch"); + + if ( attr && strcmp(attr, "solid") ) { + this->setAttribute( "inkscape:swatch", "solid" ); + } + } + + /* Fixme: should we schedule "modified" here? */ + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Callback for modified event. + */ +void SPGradient::modified(guint flags) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPGradient::modified" ); +#endif + if (flags & SP_OBJECT_CHILD_MODIFIED_FLAG) { + if (is<SPMeshGradient>(this)) { + this->invalidateArray(); + } else { + this->invalidateVector(); + } + } + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + if (is<SPMeshGradient>(this)) { + this->ensureArray(); + } else { + this->ensureVector(); + } + } + + if (flags & SP_OBJECT_MODIFIED_FLAG) flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + flags &= SP_OBJECT_MODIFIED_CASCADE; + + // FIXME: climb up the ladder of hrefs + std::vector<SPObject *> l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } + +#ifdef OBJECT_TRACE + objectTrace( "SPGradient::modified", false ); +#endif +} + +SPStop* SPGradient::getFirstStop() +{ + SPStop* first = nullptr; + for (auto& ochild: children) { + if (is<SPStop>(&ochild)) { + first = cast<SPStop>(&ochild); + break; + } + } + return first; +} + +int SPGradient::getStopCount() const +{ + int count = 0; + // fixed off-by one count + SPStop *stop = const_cast<SPGradient*>(this)->getFirstStop(); + while (stop) { + count++; + stop = stop->getNextStop(); + } + + return count; +} + +/** + * Write gradient attributes to repr. + */ +Inkscape::XML::Node *SPGradient::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPGradient::write" ); +#endif + + SPPaintServer::write(xml_doc, repr, flags); + + if (flags & SP_OBJECT_WRITE_BUILD) { + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + 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); + } + } + + if (this->ref->getURI()) { + auto uri_string = this->ref->getURI()->str(); + auto href_key = Inkscape::getHrefAttribute(*repr).first; + repr->setAttributeOrRemoveIfEmpty(href_key, uri_string); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->units_set) { + switch (this->units) { + case SP_GRADIENT_UNITS_USERSPACEONUSE: + repr->setAttribute("gradientUnits", "userSpaceOnUse"); + break; + default: + repr->setAttribute("gradientUnits", "objectBoundingBox"); + break; + } + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->gradientTransform_set) { + auto c = sp_svg_transform_write(this->gradientTransform); + repr->setAttributeOrRemoveIfEmpty("gradientTransform", c); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->spread_set) { + /* FIXME: Ensure that this->spread is the inherited value + * if !this->spread_set. Not currently happening: see SPGradient::modified. + */ + switch (this->spread) { + case SP_GRADIENT_SPREAD_REFLECT: + repr->setAttribute("spreadMethod", "reflect"); + break; + case SP_GRADIENT_SPREAD_REPEAT: + repr->setAttribute("spreadMethod", "repeat"); + break; + default: + repr->setAttribute("spreadMethod", "pad"); + break; + } + } + + if ( (flags & SP_OBJECT_WRITE_EXT) && this->isSwatch() ) { + if ( this->isSolid() ) { + repr->setAttribute( "inkscape:swatch", "solid" ); + } else { + repr->setAttribute( "inkscape:swatch", "gradient" ); + } + } else { + repr->removeAttribute("inkscape:swatch"); + } + +#ifdef OBJECT_TRACE + objectTrace( "SPGradient::write", false ); +#endif + return repr; +} + +/** + * Forces the vector to be built, if not present (i.e., changed). + * + * \pre is<SPGradient>(gradient). + */ +void SPGradient::ensureVector() +{ + if ( !vector.built ) { + rebuildVector(); + } +} + +/** + * Forces the array to be built, if not present (i.e., changed). + * + * \pre is<SPGradient>(gradient). + */ +void SPGradient::ensureArray() +{ + //std::cout << "SPGradient::ensureArray()" << std::endl; + if ( !array.built ) { + rebuildArray(); + } +} + +/** + * Set units property of gradient and emit modified. + */ +void SPGradient::setUnits(SPGradientUnits units) +{ + if (units != this->units) { + this->units = units; + units_set = TRUE; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } +} + +/** + * Set spread property of gradient and emit modified. + */ +void SPGradient::setSpread(SPGradientSpread spread) +{ + if (spread != this->spread) { + this->spread = spread; + spread_set = TRUE; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } +} + +/** + * Returns the first of {src, src-\>ref-\>getObject(), + * src-\>ref-\>getObject()-\>ref-\>getObject(),...} + * for which \a match is true, or NULL if none found. + * + * The raison d'être of this routine is that it correctly handles cycles in the href chain (e.g., if + * a gradient gives itself as its href, or if each of two gradients gives the other as its href). + * + * \pre is<SPGradient>(src). + */ +static SPGradient * +chase_hrefs(SPGradient *const src, bool (*match)(SPGradient const *)) +{ + g_return_val_if_fail(src, NULL); + + /* Use a pair of pointers for detecting loops: p1 advances half as fast as p2. If there is a + loop, then once p1 has entered the loop, we'll detect it the next time the distance between + p1 and p2 is a multiple of the loop size. */ + SPGradient *p1 = src, *p2 = src; + bool do1 = false; + for (;;) { + if (match(p2)) { + return p2; + } + + p2 = p2->ref->getObject(); + if (!p2) { + return p2; + } + if (do1) { + p1 = p1->ref->getObject(); + } + do1 = !do1; + + if ( p2 == p1 ) { + /* We've been here before, so return NULL to indicate that no matching gradient found + * in the chain. */ + return nullptr; + } + } +} + +/** + * True if gradient has stops. + */ +static bool has_stopsFN(SPGradient const *gr) +{ + return gr->hasStops(); +} + +/** + * True if gradient has patches (i.e. a mesh). + */ +static bool has_patchesFN(SPGradient const *gr) +{ + return gr->hasPatches(); +} + +/** + * True if gradient has spread set. + */ +static bool has_spread_set(SPGradient const *gr) +{ + return gr->isSpreadSet(); +} + +/** + * True if gradient has units set. + */ +static bool +has_units_set(SPGradient const *gr) +{ + return gr->isUnitsSet(); +} + + +SPGradient *SPGradient::getVector(bool force_vector) +{ + SPGradient * src = chase_hrefs(this, has_stopsFN); + if (src == nullptr) { + src = this; + } + + if (force_vector) { + src = sp_gradient_ensure_vector_normalized(src); + } + return src; +} + +SPGradient *SPGradient::getArray(bool force_vector) +{ + SPGradient * src = chase_hrefs(this, has_patchesFN); + if (src == nullptr) { + src = this; + } + return src; +} + +/** + * Returns the effective spread of given gradient (climbing up the refs chain if needed). + * + * \pre is<SPGradient>(gradient). + */ +SPGradientSpread SPGradient::fetchSpread() +{ + SPGradient const *src = chase_hrefs(this, has_spread_set); + return ( src + ? src->spread + : SP_GRADIENT_SPREAD_PAD ); // pad is the default +} + +/** + * Returns the effective units of given gradient (climbing up the refs chain if needed). + * + * \pre is<SPGradient>(gradient). + */ +SPGradientUnits SPGradient::fetchUnits() +{ + SPGradient const *src = chase_hrefs(this, has_units_set); + return ( src + ? src->units + : SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX ); // bbox is the default +} + + +/** + * Clears the gradient's svg:stop children from its repr. + */ +void +SPGradient::repr_clear_vector() +{ + Inkscape::XML::Node *repr = getRepr(); + + /* Collect stops from original repr */ + std::vector<Inkscape::XML::Node *> l; + for (Inkscape::XML::Node *child = repr->firstChild() ; child != nullptr; child = child->next() ) { + if (!strcmp(child->name(), "svg:stop")) { + l.push_back(child); + } + } + /* Remove all stops */ + for (auto i=l.rbegin();i!=l.rend();++i) { + /** \todo + * fixme: This should work, unless we make gradient + * into generic group. + */ + sp_repr_unparent(*i); + } +} + +/** + * Writes the gradient's internal vector (whether from its own stops, or + * inherited from refs) into the gradient repr as svg:stop elements. + */ +void +SPGradient::repr_write_vector() +{ + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *repr = getRepr(); + + /* We have to be careful, as vector may be our own, so construct repr list at first */ + std::vector<Inkscape::XML::Node *> l; + + for (auto & stop : vector.stops) { + Inkscape::CSSOStringStream os; + Inkscape::XML::Node *child = xml_doc->createElement("svg:stop"); + child->setAttributeCssDouble("offset", stop.offset); + /* strictly speaking, offset an SVG <number> rather than a CSS one, but exponents make no + * sense for offset proportions. */ + auto obj = cast<SPStop>(document->getObjectByRepr(child)); + obj->setColor(stop.color, stop.opacity); + /* Order will be reversed here */ + l.push_back(child); + } + + repr_clear_vector(); + + /* And insert new children from list */ + for (auto i=l.rbegin();i!=l.rend();++i) { + Inkscape::XML::Node *child = *i; + repr->addChild(child, nullptr); + Inkscape::GC::release(child); + } +} + + +void SPGradient::gradientRefModified(SPObject */*href*/, guint /*flags*/, SPGradient *gradient) +{ + if ( gradient->invalidateVector() ) { + gradient->requestModified(SP_OBJECT_MODIFIED_FLAG); + // Conditional to avoid causing infinite loop if there's a cycle in the href chain. + } +} + +/** Return true if change made. */ +bool SPGradient::invalidateVector() +{ + bool ret = false; + + if (vector.built) { + vector.built = false; + vector.stops.clear(); + ret = true; + } + + return ret; +} + +/** Return true if change made. */ +bool SPGradient::invalidateArray() +{ + bool ret = false; + + if (array.built) { + array.built = false; + // array.clear(); + ret = true; + } + + return ret; +} + +/** Creates normalized color vector */ +void SPGradient::rebuildVector() +{ + gint len = 0; + for (auto& child: children) { + if (is<SPStop>(&child)) { + len ++; + } + } + + has_stops = (len != 0); + + vector.stops.clear(); + + SPGradient *reffed = ref ? ref->getObject() : nullptr; + if ( !hasStops() && reffed ) { + /* Copy vector from referenced gradient */ + vector.built = true; // Prevent infinite recursion. + reffed->ensureVector(); + if (!reffed->vector.stops.empty()) { + vector.built = reffed->vector.built; + vector.stops.assign(reffed->vector.stops.begin(), reffed->vector.stops.end()); + return; + } + } + + for (auto& child: children) { + if (is<SPStop>(&child)) { + auto stop = cast<SPStop>(&child); + + SPGradientStop gstop; + if (!vector.stops.empty()) { + // "Each gradient offset value is required to be equal to or greater than the + // previous gradient stop's offset value. If a given gradient stop's offset + // value is not equal to or greater than all previous offset values, then the + // offset value is adjusted to be equal to the largest of all previous offset + // values." + gstop.offset = MAX(stop->offset, vector.stops.back().offset); + } else { + gstop.offset = stop->offset; + } + + // "Gradient offset values less than 0 (or less than 0%) are rounded up to + // 0%. Gradient offset values greater than 1 (or greater than 100%) are rounded + // down to 100%." + gstop.offset = CLAMP(gstop.offset, 0, 1); + + gstop.color = stop->getColor(); + gstop.opacity = stop->getOpacity(); + + vector.stops.push_back(gstop); + } + } + + // Normalize per section 13.2.4 of SVG 1.1. + if (vector.stops.empty()) { + /* "If no stops are defined, then painting shall occur as if 'none' were specified as the + * paint style." + */ + { + SPGradientStop gstop; + gstop.offset = 0.0; + gstop.color.set( 0x00000000 ); + gstop.opacity = 0.0; + vector.stops.push_back(gstop); + } + { + SPGradientStop gstop; + gstop.offset = 1.0; + gstop.color.set( 0x00000000 ); + gstop.opacity = 0.0; + vector.stops.push_back(gstop); + } + } else { + /* "If one stop is defined, then paint with the solid color fill using the color defined + * for that gradient stop." + */ + if (vector.stops.front().offset > 0.0) { + // If the first one is not at 0, then insert a copy of the first at 0. + SPGradientStop gstop; + gstop.offset = 0.0; + gstop.color = vector.stops.front().color; + gstop.opacity = vector.stops.front().opacity; + vector.stops.insert(vector.stops.begin(), gstop); + } + if (vector.stops.back().offset < 1.0) { + // If the last one is not at 1, then insert a copy of the last at 1. + SPGradientStop gstop; + gstop.offset = 1.0; + gstop.color = vector.stops.back().color; + gstop.opacity = vector.stops.back().opacity; + vector.stops.push_back(gstop); + } + } + + vector.built = true; +} + +/** Creates normalized color mesh patch array */ +void SPGradient::rebuildArray() +{ + // std::cout << "SPGradient::rebuildArray()" << std::endl; + + if( !is<SPMeshGradient>(this) ) { + g_warning( "SPGradient::rebuildArray() called for non-mesh gradient" ); + return; + } + + array.read( cast<SPMeshGradient>( this ) ); + has_patches = array.patch_columns() > 0; +} + +Geom::Affine +SPGradient::get_g2d_matrix(Geom::Affine const &ctm, Geom::Rect const &bbox) const +{ + if (getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) { + return ( Geom::Scale(bbox.dimensions()) + * Geom::Translate(bbox.min()) + * Geom::Affine(ctm) ); + } else { + return ctm; + } +} + +Geom::Affine +SPGradient::get_gs2d_matrix(Geom::Affine const &ctm, Geom::Rect const &bbox) const +{ + if (getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) { + return ( gradientTransform + * Geom::Scale(bbox.dimensions()) + * Geom::Translate(bbox.min()) + * Geom::Affine(ctm) ); + } else { + return gradientTransform * ctm; + } +} + +void +SPGradient::set_gs2d_matrix(Geom::Affine const &ctm, + Geom::Rect const &bbox, Geom::Affine const &gs2d) +{ + gradientTransform = gs2d * ctm.inverse(); + if (getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX ) { + gradientTransform = ( gradientTransform + * Geom::Translate(-bbox.min()) + * Geom::Scale(bbox.dimensions()).inverse() ); + } + gradientTransform_set = TRUE; + + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + + + +/* CAIRO RENDERING STUFF */ + +void +sp_gradient_pattern_common_setup(cairo_pattern_t *cp, + SPGradient *gr, + Geom::OptRect const &bbox, + double opacity) +{ + // set spread type + switch (gr->getSpread()) { + case SP_GRADIENT_SPREAD_REFLECT: + cairo_pattern_set_extend(cp, CAIRO_EXTEND_REFLECT); + break; + case SP_GRADIENT_SPREAD_REPEAT: + cairo_pattern_set_extend(cp, CAIRO_EXTEND_REPEAT); + break; + case SP_GRADIENT_SPREAD_PAD: + default: + cairo_pattern_set_extend(cp, CAIRO_EXTEND_PAD); + break; + } + + // add stops + if (!is<SPMeshGradient>(gr)) { + for (auto & stop : gr->vector.stops) + { + // multiply stop opacity by paint opacity + cairo_pattern_add_color_stop_rgba(cp, stop.offset, + stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity * opacity); + } + } + + // set pattern transform matrix + Geom::Affine gs2user = gr->gradientTransform; + if (gr->getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX && bbox) { + Geom::Affine bbox2user(bbox->width(), 0, 0, bbox->height(), bbox->left(), bbox->top()); + gs2user *= bbox2user; + } + ink_cairo_pattern_set_matrix(cp, gs2user.inverse()); +} + +cairo_pattern_t * +SPGradient::create_preview_pattern(double width) +{ + cairo_pattern_t *pat = nullptr; + + if (!is<SPMeshGradient>(this)) { + ensureVector(); + + pat = cairo_pattern_create_linear(0, 0, width, 0); + + for (auto & stop : vector.stops) + { + cairo_pattern_add_color_stop_rgba(pat, stop.offset, + stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity); + } + } else { + + // For the moment, use the top row of nodes for preview. + unsigned columns = array.patch_columns(); + + double offset = 1.0/double(columns); + + pat = cairo_pattern_create_linear(0, 0, width, 0); + + for (unsigned i = 0; i < columns+1; ++i) { + SPMeshNode* node = array.node( 0, i*3 ); + cairo_pattern_add_color_stop_rgba(pat, i*offset, + node->color.v.c[0], node->color.v.c[1], node->color.v.c[2], node->opacity); + } + } + + return pat; +} + +bool SPGradient::isSolid() const +{ + if (swatch && hasStops() && getStopCount() == 1) { + return true; + } + return false; +} + +/* + 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 : + diff --git a/src/object/sp-gradient.h b/src/object/sp-gradient.h new file mode 100644 index 0000000..7913e37 --- /dev/null +++ b/src/object/sp-gradient.h @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_GRADIENT_H +#define SEEN_SP_GRADIENT_H +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyrigt (C) 2010 Jon A. Cruz + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/affine.h> +#include <cstddef> +#include <glibmm/ustring.h> +#include <sigc++/connection.h> +#include <vector> + +#include "sp-paint-server.h" +#include "sp-gradient-spread.h" +#include "sp-gradient-units.h" +#include "sp-gradient-vector.h" +#include "sp-mesh-array.h" + +class SPGradientReference; +class SPStop; + +enum SPGradientType { + SP_GRADIENT_TYPE_UNKNOWN, + SP_GRADIENT_TYPE_LINEAR, + SP_GRADIENT_TYPE_RADIAL, + SP_GRADIENT_TYPE_MESH +}; + +enum SPGradientState { + SP_GRADIENT_STATE_UNKNOWN, + SP_GRADIENT_STATE_VECTOR, + SP_GRADIENT_STATE_PRIVATE +}; + +enum GrPointType { + POINT_LG_BEGIN = 0, //start enum at 0 (for indexing into gr_knot_shapes array for example) + POINT_LG_END, + POINT_LG_MID, + POINT_RG_CENTER, + POINT_RG_R1, + POINT_RG_R2, + POINT_RG_FOCUS, + POINT_RG_MID1, + POINT_RG_MID2, + POINT_MG_CORNER, + POINT_MG_HANDLE, + POINT_MG_TENSOR, + // insert new point types here. + + POINT_G_INVALID +}; + +namespace Inkscape { + +enum PaintTarget { + FOR_FILL, + FOR_STROKE +}; + +/** + * Convenience function to access a common vector of all enum values. + */ +std::vector<PaintTarget> const &allPaintTargets(); + +} // namespace Inkscape + +/** + * Gradient + * + * Implement spread, stops list + * \todo fixme: Implement more here (Lauris) + */ +class SPGradient + : public SPPaintServer +{ +public: + SPGradient(); + ~SPGradient() override; + int tag() const override { return tag_of<decltype(*this)>; } + +private: + /** gradientUnits attribute */ + SPGradientUnits units; + unsigned int units_set : 1; +public: + + /** gradientTransform attribute */ + Geom::Affine gradientTransform; + unsigned int gradientTransform_set : 1; + +private: + /** spreadMethod attribute */ + SPGradientSpread spread; + unsigned int spread_set : 1; + + /** Gradient stops */ + unsigned int has_stops : 1; + + /** Gradient patches */ + unsigned int has_patches : 1; + + /** Pinned in swatches dialog */ + bool _pinned = false; + +public: + /** Reference (href) */ + SPGradientReference *ref; + + /** State in Inkscape gradient system */ + unsigned int state; + + /** Linear and Radial Gradients */ + + /** Composed vector */ + SPGradientVector vector; + + sigc::connection modified_connection; + + bool hasStops() const; + + SPStop* getFirstStop(); + int getStopCount() const; + + bool isEquivalent(SPGradient *b); + bool isAligned(SPGradient *b); + + /** Mesh Gradients **************/ + + /** Composed array (for mesh gradients) */ + SPMeshNodeArray array; + SPMeshNodeArray array_smoothed; // Smoothed version of array + + bool hasPatches() const; + + + /** All Gradients **************/ + bool isUnitsSet() const; + SPGradientUnits getUnits() const; + void setUnits(SPGradientUnits units); + + + bool isSpreadSet() const; + SPGradientSpread getSpread() const; + +/** + * Returns private vector of given gradient (the gradient at the end of the href chain which has + * stops), optionally normalizing it. + * + * \pre is<SPGradient>(gradient). + * \pre There exists a gradient in the chain that has stops. + */ + SPGradient *getVector(bool force_private = false); + SPGradient const *getVector(bool force_private = false) const + { + return const_cast<SPGradient *>(this)->getVector(force_private); + } + + /** + * Returns private mesh of given gradient (the gradient at the end of the href chain which has + * patches), optionally normalizing it. + */ + SPGradient *getArray(bool force_private = false); + + //static GType getType(); + + /** Forces vector to be built, if not present (i.e. changed) */ + void ensureVector(); + + /** Forces array (mesh) to be built, if not present (i.e. changed) */ + void ensureArray(); + + /** + * Set spread property of gradient and emit modified. + */ + void setSpread(SPGradientSpread spread); + + SPGradientSpread fetchSpread(); + SPGradientUnits fetchUnits(); + + void setSwatch(bool swatch = true); + void setPinned(bool pinned = true); + bool isPinned() const { return _pinned; } + + bool isSolid() const; + + static void gradientRefModified(SPObject *href, unsigned int flags, SPGradient *gradient); + static void gradientRefChanged(SPObject *old_ref, SPObject *ref, SPGradient *gr); + + /* Gradient repr methods */ + void repr_write_vector(); + void repr_clear_vector(); + + cairo_pattern_t *create_preview_pattern(double width); + + /** Transforms to/from gradient position space in given environment */ + Geom::Affine get_g2d_matrix(Geom::Affine const &ctm, + Geom::Rect const &bbox) const; + Geom::Affine get_gs2d_matrix(Geom::Affine const &ctm, + Geom::Rect const &bbox) const; + void set_gs2d_matrix(Geom::Affine const &ctm, Geom::Rect const &bbox, + Geom::Affine const &gs2d); + +private: + bool invalidateVector(); + bool invalidateArray(); + void rebuildVector(); + void rebuildArray(); + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + void child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) override; + void remove_child(Inkscape::XML::Node *child) override; + + void set(SPAttr key, char const *value) override; +}; + +void +sp_gradient_pattern_common_setup(cairo_pattern_t *cp, + SPGradient *gr, + Geom::OptRect const &bbox, + double opacity); + +#endif // SEEN_SP_GRADIENT_H + +/* + 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 : diff --git a/src/object/sp-grid.cpp b/src/object/sp-grid.cpp new file mode 100644 index 0000000..1d37022 --- /dev/null +++ b/src/object/sp-grid.cpp @@ -0,0 +1,678 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape SPGrid implementation + * + * Authors: + * James Ferrarelli + * Johan Engelen <johan@shouraizou.nl> + * Lauris Kaplinski + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * Tavmong Bah <tavmjong@free.fr> + * see git history + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-grid.h" +#include "sp-namedview.h" + +#include "display/control/canvas-item-grid.h" +#include "display/control/canvas-item-ptr.h" + +#include "attributes.h" +#include "desktop.h" +#include "document.h" +#include "grid-snapper.h" +#include "page-manager.h" +#include "snapper.h" +#include "svg/svg-color.h" +#include "svg/svg-length.h" +#include "util/units.h" + +#include <glibmm/i18n.h> +#include <string> +#include <optional> + +using Inkscape::Util::unit_table; + +SPGrid::SPGrid() + : _visible(true) + , _enabled(true) + , _dotted(false) + , _snap_to_visible_only(true) + , _legacy(false) + , _pixel(true) + , _grid_type(GridType::RECTANGULAR) +{ } + +void SPGrid::create_new(SPDocument *document, Inkscape::XML::Node *parent, GridType type) +{ + auto new_node = document->getReprDoc()->createElement("inkscape:grid"); + if (type == GridType::AXONOMETRIC) { + new_node->setAttribute("type", "axonomgrid"); + } + + parent->appendChild(new_node); + + auto new_grid = dynamic_cast<SPGrid *>(document->getObjectByRepr(new_node)); + if (new_grid) + new_grid->setPrefValues(); + + Inkscape::GC::release(new_node); +} + +SPGrid::~SPGrid() = default; + +void SPGrid::build(SPDocument *doc, Inkscape::XML::Node *repr) +{ + SPObject::build(doc, repr); + + readAttr(SPAttr::TYPE); + readAttr(SPAttr::UNITS); + readAttr(SPAttr::ORIGINX); + readAttr(SPAttr::ORIGINY); + readAttr(SPAttr::SPACINGX); + readAttr(SPAttr::SPACINGY); + readAttr(SPAttr::ANGLE_X); + readAttr(SPAttr::ANGLE_Z); + readAttr(SPAttr::COLOR); + readAttr(SPAttr::EMPCOLOR); + readAttr(SPAttr::VISIBLE); + readAttr(SPAttr::ENABLED); + readAttr(SPAttr::OPACITY); + readAttr(SPAttr::EMPOPACITY); + readAttr(SPAttr::MAJOR_LINE_INTERVAL); + readAttr(SPAttr::DOTTED); + readAttr(SPAttr::SNAP_TO_VISIBLE_ONLY); + + _checkOldGrid(doc, repr); + + _page_selected_connection = document->getPageManager().connectPageSelected([=](void *) { update(nullptr, 0); }); + _page_modified_connection = document->getPageManager().connectPageModified([=](void *) { update(nullptr, 0); }); + + doc->addResource("grid", this); +} + +void SPGrid::release() +{ + if (document) { + document->removeResource("grid", this); + } + + assert(views.empty()); + + _page_selected_connection.disconnect(); + _page_modified_connection.disconnect(); + + SPObject::release(); +} + +static std::optional<GridType> readGridType(char const *value) +{ + if (!value) { + return {}; + } else if (!std::strcmp(value, "xygrid")) { + return GridType::RECTANGULAR; + } else if (!std::strcmp(value, "axonomgrid")) { + return GridType::AXONOMETRIC; + } else { + return {}; + } +} + +void SPGrid::set(SPAttr key, const gchar* value) +{ + switch (key) { + case SPAttr::TYPE: { + auto const grid_type = readGridType(value).value_or(GridType::RECTANGULAR); // default + if (grid_type != _grid_type) { + _grid_type = grid_type; + _recreateViews(); + } + break; + } + case SPAttr::UNITS: + _display_unit = unit_table.getUnit(value); + break; + case SPAttr::ORIGINX: + _origin_x.read(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::ORIGINY: + _origin_y.read(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::SPACINGX: + _spacing_x.read(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::SPACINGY: + _spacing_y.read(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::ANGLE_X: // only meaningful for axonomgrid + _angle_x.read(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::ANGLE_Z: // only meaningful for axonomgrid + _angle_z.read(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::COLOR: + _minor_color = (_minor_color & 0xff) | sp_svg_read_color(value, GRID_DEFAULT_MINOR_COLOR); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::EMPCOLOR: + _major_color = (_major_color & 0xff) | sp_svg_read_color(value, GRID_DEFAULT_MAJOR_COLOR); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::VISIBLE: + _visible.read(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::ENABLED: + _enabled.read(value); + if (_snapper) _snapper->setEnabled(_enabled); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::OPACITY: + sp_ink_read_opacity(value, &_minor_color, GRID_DEFAULT_MINOR_COLOR); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::EMPOPACITY: + sp_ink_read_opacity(value, &_major_color, GRID_DEFAULT_MAJOR_COLOR); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::MAJOR_LINE_INTERVAL: + _major_line_interval = value ? std::max(std::stoi(value), 1) : 5; + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::DOTTED: // only meaningful for rectangular grid + _dotted.read(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::SNAP_TO_VISIBLE_ONLY: + _snap_to_visible_only.read(value); + if (_snapper) _snapper->setSnapVisibleOnly(_snap_to_visible_only); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPObject::set(key, value); + break; + } +} + +/** + * checks for old grid attriubte keys from version 0.46, and + * sets the old defaults to the newer attribute keys + */ +void SPGrid::_checkOldGrid(SPDocument *doc, Inkscape::XML::Node *repr) +{ + // set old settings + const char* gridspacingx = "1px"; + const char* gridspacingy = "1px"; + const char* gridoriginy = "0px"; + const char* gridoriginx = "0px"; + const char* gridempspacing = "5"; + const char* gridcolor = "#3f3fff"; + const char* gridempcolor = "#3f3fff"; + const char* gridopacity = "0.15"; + const char* gridempopacity = "0.38"; + + if (auto originx = repr->attribute("gridoriginx")) { + gridoriginx = originx; + _legacy = true; + } + if (auto originy = repr->attribute("gridoriginy")) { + gridoriginy = originy; + _legacy = true; + } + if (auto spacingx = repr->attribute("gridspacingx")) { + gridspacingx = spacingx; + _legacy = true; + } + if (auto spacingy = repr->attribute("gridspacingy")) { + gridspacingy = spacingy; + _legacy = true; + } + if (auto minorcolor = repr->attribute("gridcolor")) { + gridcolor = minorcolor; + _legacy = true; + } + if (auto majorcolor = repr->attribute("gridempcolor")) { + gridempcolor = majorcolor; + _legacy = true; + } + if (auto majorinterval = repr->attribute("gridempspacing")) { + gridempspacing = majorinterval; + _legacy = true; + } + if (auto minoropacity = repr->attribute("gridopacity")) { + gridopacity = minoropacity; + _legacy = true; + } + if (auto majoropacity = repr->attribute("gridempopacity")) { + gridempopacity = majoropacity; + _legacy = true; + } + + if (_legacy) { + // generate new xy grid with the correct settings + // first create the child xml node, then hook it to repr. This order is important, to not set off listeners to repr before the new node is complete. + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *newnode = xml_doc->createElement("inkscape:grid"); + newnode->setAttribute("id", "GridFromPre046Settings"); + newnode->setAttribute("type", getSVGType()); + newnode->setAttribute("originx", gridoriginx); + newnode->setAttribute("originy", gridoriginy); + newnode->setAttribute("spacingx", gridspacingx); + newnode->setAttribute("spacingy", gridspacingy); + newnode->setAttribute("color", gridcolor); + newnode->setAttribute("empcolor", gridempcolor); + newnode->setAttribute("opacity", gridopacity); + newnode->setAttribute("empopacity", gridempopacity); + newnode->setAttribute("empspacing", gridempspacing); + + repr->appendChild(newnode); + Inkscape::GC::release(newnode); + + // remove all old settings + repr->removeAttribute("gridoriginx"); + repr->removeAttribute("gridoriginy"); + repr->removeAttribute("gridspacingx"); + repr->removeAttribute("gridspacingy"); + repr->removeAttribute("gridcolor"); + repr->removeAttribute("gridempcolor"); + repr->removeAttribute("gridopacity"); + repr->removeAttribute("gridempopacity"); + repr->removeAttribute("gridempspacing"); + } + else if (repr->attribute("id")) { + // fix v1.2 grids without spacing, units, origin defined + auto fix = [=](SPAttr attr, const char* value) { + auto key = sp_attribute_name(attr); + if (!repr->attribute(key)) { + repr->setAttribute(key, value); + set(attr, value); + } + }; + + fix(SPAttr::ORIGINX, "0"); + fix(SPAttr::ORIGINY, "0"); + fix(SPAttr::SPACINGY, "1"); + switch (readGridType(repr->attribute("type")).value_or(GridType::RECTANGULAR)) { + case GridType::RECTANGULAR: + fix(SPAttr::SPACINGX, "1"); + break; + case GridType::AXONOMETRIC: + fix(SPAttr::ANGLE_X, "30"); + fix(SPAttr::ANGLE_Z, "30"); + break; + default: + break; + } + + const char* unit = nullptr; + if (auto nv = repr->parent()) { + // check display unit from named view (parent) + unit = nv->attribute("units"); + // check document units if there are no display units defined + if (!unit) { + unit = nv->attribute("inkscape:document-units"); + auto display_units = sp_parse_document_units(unit); + unit = display_units->abbr.c_str(); + } + } + fix(SPAttr::UNITS, unit ? unit : "px"); + } +} + +/* + * The grid needs to be initialized based on user preferences. + * When a grid is created by either DocumentProperties or SPNamedView, + * update the attributes to the corresponding grid type. + */ +void SPGrid::setPrefValues() +{ + auto prefs = Inkscape::Preferences::get(); + + std::string prefix; + switch (getType()) { + case GridType::RECTANGULAR: prefix = "/options/grids/xy"; break; + case GridType::AXONOMETRIC: prefix = "/options/grids/axonom"; break; + default: g_assert_not_reached(); break; + } + + auto display_unit = document->getDisplayUnit(); + auto unit_pref = prefs->getString(prefix + "/units", display_unit->abbr); + setUnit(unit_pref); + + _display_unit = unit_table.getUnit(unit_pref); + + // Origin and Spacing are the only two properties that vary depending on selected units + // SPGrid should only store values in document units, convert whatever preferences are set to "px" + // and then scale "px" to the document unit. + using Inkscape::Util::Quantity; + auto scale = document->getDocumentScale().inverse(); + setOrigin(Geom::Point( + Quantity::convert(prefs->getDouble(prefix + "/origin_x"), _display_unit, "px"), + Quantity::convert(prefs->getDouble(prefix + "/origin_y"), _display_unit, "px")) * scale); + + setSpacing(Geom::Point( + Quantity::convert(prefs->getDouble(prefix + "/spacing_x"), _display_unit, "px"), + Quantity::convert(prefs->getDouble(prefix + "/spacing_y"), _display_unit, "px")) * scale); + + setMajorColor(prefs->getColor(prefix + "/empcolor", GRID_DEFAULT_MAJOR_COLOR)); + setMinorColor(prefs->getColor(prefix + "/color", GRID_DEFAULT_MINOR_COLOR)); + setMajorLineInterval(prefs->getInt(prefix + "/empspacing")); + + // these prefs are bound specifically to one type of grid + setDotted(prefs->getBool("/options/grids/xy/dotted")); + setAngleX(prefs->getDouble("/options/grids/axonom/angle_x")); + setAngleZ(prefs->getDouble("/options/grids/axonom/angle_z")); +} + +static CanvasItemPtr<Inkscape::CanvasItemGrid> create_view(GridType grid_type, Inkscape::CanvasItemGroup *canvasgrids) +{ + switch (grid_type) { + case GridType::RECTANGULAR: return make_canvasitem<Inkscape::CanvasItemGridXY> (canvasgrids); break; + case GridType::AXONOMETRIC: return make_canvasitem<Inkscape::CanvasItemGridAxonom>(canvasgrids); break; + default: g_assert_not_reached(); return {}; + } +} + +void SPGrid::_recreateViews() +{ + // handle change in grid type requiring all views to be recreated as a different type + for (auto &view : views) { + view = create_view(_grid_type, view->get_parent()); + } +} + +// update internal state on XML change +void SPGrid::modified(unsigned int flags) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) { + updateRepr(); + } +} + +// tell canvas to redraw grid +void SPGrid::update(SPCtx *ctx, unsigned int flags) +{ + auto [origin, spacing] = getEffectiveOriginAndSpacing(); + + for (auto &view : views) { + view->set_visible(_visible && _enabled); + if (_enabled) { + view->set_origin(origin); + view->set_spacing(spacing); + view->set_major_color(_major_color); + view->set_minor_color(_minor_color); + view->set_dotted(_dotted); + view->set_major_line_interval(_major_line_interval); + + if (auto axonom = dynamic_cast<Inkscape::CanvasItemGridAxonom *>(view.get())) { + axonom->set_angle_x(_angle_x.computed); + axonom->set_angle_z(_angle_z.computed); + } + } + } +} + +/** + * creates a new grid canvasitem for the SPDesktop given as parameter. Keeps a link to this canvasitem in the views list. + */ +void SPGrid::show(SPDesktop *desktop) +{ + if (!desktop) return; + + // check if there is already a canvasitem on this desktop linking to this grid + for (auto &view : views) { + if (desktop->getCanvasGrids() == view->get_parent()) { + return; + } + } + + // create designated canvasitem for this grid + views.emplace_back(create_view(_grid_type, desktop->getCanvasGrids())); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGrid::hide(SPDesktop const *desktop) +{ + if (!desktop) return; + + for (auto it = views.begin(); it != views.end(); ++it) { + auto view = it->get(); + if (view->get_parent() == desktop->getCanvasGrids()) { + views.erase(it); + break; + } + } +} + +void SPGrid::scale(const Geom::Scale &scale) +{ + setOrigin( getOrigin() * scale ); + setSpacing( getSpacing() * scale ); +} + +Inkscape::Snapper *SPGrid::snapper() +{ + if (!_snapper) { + // lazily create + _snapper = std::make_unique<Inkscape::GridSnapper>(this, &document->getNamedView()->snap_manager, 0); + _snapper->setEnabled(_enabled); + _snapper->setSnapVisibleOnly(_snap_to_visible_only); + } + return _snapper.get(); +} + +static auto ensure_min(double s) +{ + return std::max(s, 0.00001); // clamping, spacing must be > 0 +} + +static auto ensure_min(Geom::Point const &s) +{ + return Geom::Point(ensure_min(s.x()), ensure_min(s.y())); +} + +std::pair<Geom::Point, Geom::Point> SPGrid::getEffectiveOriginAndSpacing() const +{ + auto origin = getOrigin(); + auto spacing = ensure_min(getSpacing()); + + auto const scale = document->getDocumentScale(); + origin *= scale; + spacing *= scale; + + auto prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/origincorrection/page", true)) + origin *= document->getPageManager().getSelectedPageAffine(); + + return { origin, spacing }; +} + +const char *SPGrid::displayName() const +{ + switch (_grid_type) { + case GridType::RECTANGULAR: return _("Rectangular Grid"); + case GridType::AXONOMETRIC: return _("Axonometric Grid"); + default: g_assert_not_reached(); + } +} + +const char *SPGrid::getSVGType() const +{ + switch (_grid_type) { + case GridType::RECTANGULAR: return "xygrid"; + case GridType::AXONOMETRIC: return "axonomgrid"; + default: g_assert_not_reached(); + } +} + +void SPGrid::setSVGType(char const *svgtype) +{ + auto target_type = readGridType(svgtype); + if (!target_type || *target_type == _grid_type) { + return; + } + + getRepr()->setAttribute("type", svgtype); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +// finds the canvasitem active in the current active view +Inkscape::CanvasItemGrid *SPGrid::getAssociatedView(SPDesktop const *desktop) +{ + for (auto &view : views) { + if (desktop->getCanvasGrids() == view->get_parent()) { + return view.get(); + } + } + return nullptr; +} + +void SPGrid::setVisible(bool v) +{ + getRepr()->setAttributeBoolean("visible", v); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +bool SPGrid::isEnabled() const +{ + return _enabled; +} + +void SPGrid::setEnabled(bool v) +{ + getRepr()->setAttributeBoolean("enabled", v); + + if (_snapper) _snapper->setEnabled(v); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +// returns values in "px" +Geom::Point SPGrid::getOrigin() const +{ + return Geom::Point(_origin_x.computed, _origin_y.computed); +} + +void SPGrid::setOrigin(Geom::Point const &new_origin) +{ + Inkscape::XML::Node *repr = getRepr(); + repr->setAttributeSvgDouble("originx", new_origin[Geom::X]); + repr->setAttributeSvgDouble("originy", new_origin[Geom::Y]); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGrid::setMajorColor(const guint32 color) +{ + char color_str[16]; + sp_svg_write_color(color_str, 16, color); + + getRepr()->setAttribute("empcolor", color_str); + + double opacity = (color & 0xff) / 255.0; // convert to value between [0.0, 1.0] + getRepr()->setAttributeSvgDouble("empopacity", opacity); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGrid::setMinorColor(const guint32 color) +{ + char color_str[16]; + sp_svg_write_color(color_str, 16, color); + + + getRepr()->setAttribute("color", color_str); + + double opacity = (color & 0xff) / 255.0; // convert to value between [0.0, 1.0] + getRepr()->setAttributeSvgDouble("opacity", opacity); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +// returns values in "px" +Geom::Point SPGrid::getSpacing() const +{ + return Geom::Point(_spacing_x.computed, _spacing_y.computed); +} + +void SPGrid::setSpacing(const Geom::Point &spacing) +{ + Inkscape::XML::Node *repr = getRepr(); + repr->setAttributeSvgDouble("spacingx", spacing[Geom::X]); + repr->setAttributeSvgDouble("spacingy", spacing[Geom::Y]); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGrid::setMajorLineInterval(const guint32 interval) +{ + getRepr()->setAttributeInt("empspacing", interval); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGrid::setDotted(bool v) +{ + getRepr()->setAttributeBoolean("dotted", v); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGrid::setSnapToVisibleOnly(bool v) +{ + getRepr()->setAttributeBoolean("snapvisiblegridlinesonly", v); + if (_snapper) _snapper->setSnapVisibleOnly(v); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGrid::setAngleX(double deg) +{ + getRepr()->setAttributeSvgDouble("gridanglex", deg); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGrid::setAngleZ(double deg) +{ + getRepr()->setAttributeSvgDouble("gridanglez", deg); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +const char *SPGrid::typeName() const +{ + switch (_grid_type) { + case GridType::RECTANGULAR: return "grid-rectangular"; + case GridType::AXONOMETRIC: return "grid-axonometric"; + default: g_assert_not_reached(); return "grid"; + } +} + +const Inkscape::Util::Unit *SPGrid::getUnit() const +{ + return _display_unit; +} + +void SPGrid::setUnit(const Glib::ustring &units) +{ + if (units.empty()) return; + + getRepr()->setAttribute("units", units.c_str()); + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} diff --git a/src/object/sp-grid.h b/src/object/sp-grid.h new file mode 100644 index 0000000..bf08269 --- /dev/null +++ b/src/object/sp-grid.h @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape SPGrid implementation + * + * Authors: + * James Ferrarelli + * Johan Engelen <johan@shouraizou.nl> + * Lauris Kaplinski + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * Tavmong Bah <tavmjong@free.fr> + * see git history + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GRID_H_ +#define SEEN_SP_GRID_H_ + +#include "display/control/canvas-item-ptr.h" +#include "object/sp-object.h" +#include "svg/svg-bool.h" +#include "svg/svg-length.h" +#include "svg/svg-angle.h" +#include <memory> +#include <vector> + +class SPDesktop; + +namespace Inkscape { + class CanvasItemGrid; + class Snapper; + + namespace Util { + class Unit; + } +} // namespace Inkscape + +enum class GridType +{ + RECTANGULAR, + AXONOMETRIC +}; + +class SPGrid final : public SPObject { +public: + SPGrid(); + ~SPGrid() override; + int tag() const override { return tag_of<decltype(*this)>; } + + static void create_new(SPDocument *doc, Inkscape::XML::Node *parent, GridType type); + + void setPrefValues(); + + void show(SPDesktop *desktop); + void hide(SPDesktop const *desktop); + + bool isEnabled() const; + void setEnabled(bool v); + + bool isVisible() const { return isEnabled() && _visible; } + void setVisible(bool v); + + bool isDotted() const { return _dotted; } + void setDotted(bool v); + + bool getSnapToVisibleOnly() const { return _snap_to_visible_only; } + void setSnapToVisibleOnly(bool v); + + guint32 getMajorColor() const { return _major_color; } + void setMajorColor(const guint32 color); + + guint32 getMinorColor() const { return _minor_color; } + void setMinorColor(const guint32 color); + + Geom::Point getOrigin() const; + void setOrigin(Geom::Point const &new_origin); + + Geom::Point getSpacing() const; + void setSpacing(Geom::Point const &spacing); + + guint32 getMajorLineInterval() const { return _major_line_interval; } + void setMajorLineInterval(guint32 interval); + + double getAngleX() const { return _angle_x.computed; } + void setAngleX(double deg); + + double getAngleZ() const { return _angle_z.computed; } + void setAngleZ(double deg); + + const char *typeName() const; + const char *displayName() const; + + GridType getType() const { return _grid_type; } + const char *getSVGType() const; + void setSVGType(const char *svgtype); + + void setUnit(const Glib::ustring &units); + const Inkscape::Util::Unit *getUnit() const; + + bool isPixel() const { return _pixel; } + bool isLegacy() const { return _legacy; } + + void scale(const Geom::Scale &scale); + Inkscape::CanvasItemGrid *getAssociatedView(SPDesktop const *desktop); + + Inkscape::Snapper *snapper(); + + std::pair<Geom::Point, Geom::Point> getEffectiveOriginAndSpacing() const; + + std::vector<CanvasItemPtr<Inkscape::CanvasItemGrid>> views; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, const char *value) override; + void release() override; + void modified(unsigned int flags) override; + void update(SPCtx *ctx, unsigned int flags) override; + +private: + void _checkOldGrid(SPDocument *doc, Inkscape::XML::Node *repr); + void _recreateViews(); + + SVGBool _visible; + SVGBool _enabled; + SVGBool _snap_to_visible_only; + SVGBool _dotted; + SVGLength _origin_x; + SVGLength _origin_y; + SVGLength _spacing_x; + SVGLength _spacing_y; + SVGAngle _angle_x; // only for axonomgrid, stored in degrees + SVGAngle _angle_z; // only for axonomgrid, stored in degrees + + guint32 _major_line_interval; + + guint32 _major_color; + guint32 _minor_color; + + bool _pixel; // is in user units + bool _legacy; // a grid from versions prior to inkscape 0.98 + + GridType _grid_type; + + std::unique_ptr<Inkscape::Snapper> _snapper; + + Inkscape::Util::Unit const *_display_unit; + + sigc::connection _page_selected_connection; + sigc::connection _page_modified_connection; +}; + +#endif // SEEN_SP_GRID_H_ diff --git a/src/object/sp-guide.cpp b/src/object/sp-guide.cpp new file mode 100644 index 0000000..c85020a --- /dev/null +++ b/src/object/sp-guide.cpp @@ -0,0 +1,537 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape guideline implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Peter Moulder <pmoulder@mail.csse.monash.edu.au> + * Johan Engelen + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2000-2002 authors + * Copyright (C) 2004 Monash University + * Copyright (C) 2007 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-guide.h" + +#include <cstring> +#include <glibmm/i18n.h> +#include <vector> + +#include "attributes.h" +#include "desktop-events.h" +#include "desktop.h" +#include "display/control/canvas-item-guideline.h" +#include "document-undo.h" +#include "inkscape.h" +#include "object/sp-page.h" +#include "page-manager.h" +#include "sp-namedview.h" +#include "sp-root.h" +#include "svg/stringstream.h" +#include "svg/svg-color.h" +#include "svg/svg.h" +#include "ui/widget/canvas.h" // Should really be here +#include "util/numeric/converters.h" +#include "xml/repr.h" + +using Inkscape::DocumentUndo; + + +SPGuide::SPGuide() + : SPObject() + , label(nullptr) + , locked(false) + , normal_to_line(Geom::Point(0.,1.)) + , point_on_line(Geom::Point(0.,0.)) + , color(0x0086e599) + , hicolor(0xff00007f) +{} + +void SPGuide::setColor(guint32 color) +{ + this->color = color; + for (auto &view : views) { + view->set_stroke(color); + } +} + +void SPGuide::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + this->readAttr(SPAttr::INKSCAPE_COLOR); + this->readAttr(SPAttr::INKSCAPE_LABEL); + this->readAttr(SPAttr::INKSCAPE_LOCKED); + this->readAttr(SPAttr::ORIENTATION); + this->readAttr(SPAttr::POSITION); + + /* Register */ + document->addResource("guide", this); +} + +void SPGuide::release() +{ + views.clear(); + + if (this->document) { + // Unregister ourselves + this->document->removeResource("guide", this); + } + + SPObject::release(); +} + +void SPGuide::set(SPAttr key, const gchar *value) { + switch (key) { + case SPAttr::INKSCAPE_COLOR: + if (value) { + this->setColor(sp_svg_read_color(value, 0x0000ff00) | 0x7f); + } + break; + case SPAttr::INKSCAPE_LABEL: + // this->label already freed in sp_guideline_set_label (src/display/guideline.cpp) + // see bug #1498444, bug #1469514 + if (value) { + this->label = g_strdup(value); + } else { + this->label = nullptr; + } + + this->set_label(this->label, false); + break; + case SPAttr::INKSCAPE_LOCKED: + if (value) { + this->set_locked(Inkscape::Util::read_bool(value, false), false); + } + break; + case SPAttr::ORIENTATION: + { + if (value && !strcmp(value, "horizontal")) { + /* Visual representation of a horizontal line, constrain vertically (y coordinate). */ + this->normal_to_line = Geom::Point(0., 1.); + } else if (value && !strcmp(value, "vertical")) { + this->normal_to_line = Geom::Point(1., 0.); + } else if (value) { + gchar ** strarray = g_strsplit(value, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2 && (fabs(newx) > 1e-6 || fabs(newy) > 1e-6)) { + Geom::Point direction(newx, newy); + + // <sodipodi:guide> stores inverted y-axis coordinates + if (document->is_yaxisdown()) { + direction[Geom::X] *= -1.0; + } + + direction.normalize(); + this->normal_to_line = direction; + } else { + // default to vertical line for bad arguments + this->normal_to_line = Geom::Point(1., 0.); + } + } else { + // default to vertical line for bad arguments + this->normal_to_line = Geom::Point(1., 0.); + } + this->set_normal(this->normal_to_line, false); + } + break; + case SPAttr::POSITION: + { + if (value) { + gchar ** strarray = g_strsplit(value, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + // If root viewBox set, interpret guides in terms of viewBox (90/96) + SPRoot *root = document->getRoot(); + if( root->viewBox_set ) { + if(Geom::are_near((root->width.computed * root->viewBox.height()) / (root->viewBox.width() * root->height.computed), 1.0, Geom::EPSILON)) { + // for uniform scaling, try to reduce numerical error + double vbunit2px = (root->width.computed / root->viewBox.width() + root->height.computed / root->viewBox.height())/2.0; + newx = newx * vbunit2px; + newy = newy * vbunit2px; + } else { + newx = newx * root->width.computed / root->viewBox.width(); + newy = newy * root->height.computed / root->viewBox.height(); + } + } + this->point_on_line = Geom::Point(newx, newy); + } else if (success == 1) { + // before 0.46 style guideline definition. + const gchar *attr = this->getRepr()->attribute("orientation"); + if (attr && !strcmp(attr, "horizontal")) { + this->point_on_line = Geom::Point(0, newx); + } else { + this->point_on_line = Geom::Point(newx, 0); + } + } + + // <sodipodi:guide> stores inverted y-axis coordinates + if (document->is_yaxisdown()) { + this->point_on_line[Geom::Y] = document->getHeight().value("px") - this->point_on_line[Geom::Y]; + } + } else { + // default to (0,0) for bad arguments + this->point_on_line = Geom::Point(0,0); + } + // update position in non-committing way + // fixme: perhaps we need to add an update method instead, and request_update here + this->moveto(this->point_on_line, false); + } + break; + default: + SPObject::set(key, value); + break; + } +} + +/* Only used internally and in sp-line.cpp */ +SPGuide *SPGuide::createSPGuide(SPDocument *doc, Geom::Point const &pt1, Geom::Point const &pt2) +{ + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::XML::Node *repr = xml_doc->createElement("sodipodi:guide"); + + Geom::Point n = Geom::rot90(pt2 - pt1); + + // If root viewBox set, interpret guides in terms of viewBox (90/96) + double newx = pt1.x(); + double newy = pt1.y(); + + SPRoot *root = doc->getRoot(); + + // <sodipodi:guide> stores inverted y-axis coordinates + if (doc->is_yaxisdown()) { + newy = doc->getHeight().value("px") - newy; + n[Geom::X] *= -1.0; + } + + if( root->viewBox_set ) { + // check to see if scaling is uniform + if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) { + double px2vbunit = (root->viewBox.width()/root->width.computed + root->viewBox.height()/root->height.computed)/2.0; + newx = newx * px2vbunit; + newy = newy * px2vbunit; + } else { + newx = newx * root->viewBox.width() / root->width.computed; + newy = newy * root->viewBox.height() / root->height.computed; + } + } + + repr->setAttributePoint("position", Geom::Point( newx, newy )); + repr->setAttributePoint("orientation", n); + + SPNamedView *namedview = doc->getNamedView(); + if (namedview) { + if (namedview->lockguides) { + repr->setAttribute("inkscape:locked", "true"); + } + namedview->appendChild(repr); + } + Inkscape::GC::release(repr); + + auto guide = cast<SPGuide>(doc->getObjectByRepr(repr)); + return guide; +} + +SPGuide *SPGuide::duplicate(){ + return SPGuide::createSPGuide( + document, + point_on_line, + Geom::Point( + point_on_line[Geom::X] + normal_to_line[Geom::Y], + point_on_line[Geom::Y] - normal_to_line[Geom::X] + ) + ); +} + +void sp_guide_pt_pairs_to_guides(SPDocument *doc, std::list<std::pair<Geom::Point, Geom::Point> > &pts) +{ + for (auto & pt : pts) { + SPGuide::createSPGuide(doc, pt.first, pt.second); + } +} + +void sp_guide_create_guides_around_page(SPDocument *doc) +{ + std::list<std::pair<Geom::Point, Geom::Point> > pts; + + Geom::Rect bounds = doc->getPageManager().getSelectedPageRect(); + + pts.emplace_back(bounds.corner(0), bounds.corner(1)); + pts.emplace_back(bounds.corner(1), bounds.corner(2)); + pts.emplace_back(bounds.corner(2), bounds.corner(3)); + pts.emplace_back(bounds.corner(3), bounds.corner(0)); + + sp_guide_pt_pairs_to_guides(doc, pts); + DocumentUndo::done(doc, _("Create Guides Around the Current Page"), ""); +} + +void sp_guide_delete_all_guides(SPDocument *doc) +{ + std::vector<SPObject *> current = doc->getResourceList("guide"); + while (!current.empty()){ + auto guide = cast<SPGuide>(*(current.begin())); + guide->remove(true); + current = doc->getResourceList("guide"); + } + + DocumentUndo::done(doc, _("Delete All Guides"),""); +} + +// Actually, create a new guide. +void SPGuide::showSPGuide(Inkscape::CanvasItemGroup *group) +{ + Glib::ustring ulabel = (label?label:""); + auto item = new Inkscape::CanvasItemGuideLine(group, ulabel, point_on_line, normal_to_line); + item->set_stroke(color); + item->set_locked(locked); + + item->connect_event(sigc::bind(sigc::ptr_fun(&sp_dt_guide_event), item, this)); + + // Ensure event forwarding by the guide handle ("the dot") to the corresponding line + auto dot = item->dot(); + auto dot_handler = [=](GdkEvent *ev) { return sp_dt_guide_event(ev, item, this); }; + dot->connect_event(dot_handler); + + views.emplace_back(item); +} + +void SPGuide::showSPGuide() +{ + for (auto &view : views) { + view->show(); + } +} + +// Actually deleted guide from a particular canvas. +void SPGuide::hideSPGuide(Inkscape::UI::Widget::Canvas *canvas) +{ + g_assert(canvas != nullptr); + for (auto it = views.begin(); it != views.end(); ++it) { + if (canvas == (*it)->get_canvas()) { // A guide can be displayed on more than one desktop with the same document. + views.erase(it); + return; + } + } + + assert(false); +} + +void SPGuide::hideSPGuide() +{ + for (auto &view : views) { + view->hide(); + } +} + +void SPGuide::sensitize(Inkscape::UI::Widget::Canvas *canvas, bool sensitive) +{ + g_assert(canvas != nullptr); + + for (auto &view : views) { + if (canvas == view->get_canvas()) { + view->set_pickable(sensitive); + return; + } + } + + assert(false); +} + +/** + * \arg commit False indicates temporary moveto in response to motion event while dragging, + * true indicates a "committing" version: in response to button release event after + * dragging a guideline, or clicking OK in guide editing dialog. + */ +void SPGuide::moveto(Geom::Point const point_on_line, bool const commit) +{ + if(this->locked) { + return; + } + + for (auto &view : views) { + view->set_origin(point_on_line); + } + + /* Calling Inkscape::XML::Node::setAttributePoint must precede calling sp_item_notify_moveto in the commit + case, so that the guide's new position is available for sp_item_rm_unsatisfied_cns. */ + if (commit) { + // If root viewBox set, interpret guides in terms of viewBox (90/96) + double newx = point_on_line.x(); + double newy = point_on_line.y(); + + // <sodipodi:guide> stores inverted y-axis coordinates + if (document->is_yaxisdown()) { + newy = document->getHeight().value("px") - newy; + } + + SPRoot *root = document->getRoot(); + if( root->viewBox_set ) { + // check to see if scaling is uniform + if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) { + double px2vbunit = (root->viewBox.width()/root->width.computed + root->viewBox.height()/root->height.computed)/2.0; + newx = newx * px2vbunit; + newy = newy * px2vbunit; + } else { + newx = newx * root->viewBox.width() / root->width.computed; + newy = newy * root->viewBox.height() / root->height.computed; + } + } + + //XML Tree being used here directly while it shouldn't be. + getRepr()->setAttributePoint("position", Geom::Point(newx, newy) ); + } +} + +/** + * \arg commit False indicates temporary moveto in response to motion event while dragging, + * true indicates a "committing" version: in response to button release event after + * dragging a guideline, or clicking OK in guide editing dialog. + */ +void SPGuide::set_normal(Geom::Point const normal_to_line, bool const commit) +{ + if(this->locked) { + return; + } + for (auto &view : views) { + view->set_normal(normal_to_line); + } + + /* Calling sp_repr_set_svg_point must precede calling sp_item_notify_moveto in the commit + case, so that the guide's new position is available for sp_item_rm_unsatisfied_cns. */ + if (commit) { + //XML Tree being used directly while it shouldn't be + auto normal = normal_to_line; + + // <sodipodi:guide> stores inverted y-axis coordinates + if (document->is_yaxisdown()) { + normal[Geom::X] *= -1.0; + } + + getRepr()->setAttributePoint("orientation", normal); + } +} + +void SPGuide::set_color(const unsigned r, const unsigned g, const unsigned b, bool const commit) +{ + this->color = (r << 24) | (g << 16) | (b << 8) | 0x7f; + + if (! views.empty()) { + views[0]->set_stroke(color); + } + + if (commit) { + std::ostringstream os; + os << "rgb(" << r << "," << g << "," << b << ")"; + //XML Tree being used directly while it shouldn't be + setAttribute("inkscape:color", os.str()); + } +} + +void SPGuide::set_locked(const bool locked, bool const commit) +{ + this->locked = locked; + if ( !views.empty() ) { + views[0]->set_locked(locked); + } + + if (commit) { + setAttribute("inkscape:locked", locked ? "true" : "false"); + } +} + +void SPGuide::set_label(const char* label, bool const commit) +{ + if (!views.empty()) { + views[0]->set_label(label ? label : ""); + } + + if (commit) { + //XML Tree being used directly while it shouldn't be + setAttribute("inkscape:label", label); + } +} + +/** + * Returns a human-readable description of the guideline for use in dialog boxes and status bar. + * If verbose is false, only positioning information is included (useful for dialogs). + * + * The caller is responsible for freeing the string. + */ +char* SPGuide::description(bool const verbose) const +{ + using Geom::X; + using Geom::Y; + + char *descr = nullptr; + if ( !this->document ) { + // Guide has probably been deleted and no longer has an attached namedview. + descr = g_strdup(_("Deleted")); + } else { + SPNamedView *namedview = this->document->getNamedView(); + + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(this->point_on_line[X], "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(this->point_on_line[Y], "px"); + Glib::ustring position_string_x = x_q.string(namedview->display_units); + Glib::ustring position_string_y = y_q.string(namedview->display_units); + + gchar *shortcuts = g_strdup_printf("; %s", _("<b>Shift+drag</b> to rotate, <b>Ctrl+drag</b> to move origin, <b>Del</b> to delete")); + + if ( are_near(this->normal_to_line, Geom::Point(1., 0.)) || + are_near(this->normal_to_line, -Geom::Point(1., 0.)) ) { + descr = g_strdup_printf(_("vertical, at %s"), position_string_x.c_str()); + } else if ( are_near(this->normal_to_line, Geom::Point(0., 1.)) || + are_near(this->normal_to_line, -Geom::Point(0., 1.)) ) { + descr = g_strdup_printf(_("horizontal, at %s"), position_string_y.c_str()); + } else { + double const radians = this->angle(); + double const degrees = Geom::deg_from_rad(radians); + int const degrees_int = (int) round(degrees); + descr = g_strdup_printf(_("at %d degrees, through (%s,%s)"), + degrees_int, position_string_x.c_str(), position_string_y.c_str()); + } + + if (verbose) { + gchar *oldDescr = descr; + descr = g_strconcat(oldDescr, shortcuts, nullptr); + g_free(oldDescr); + } + + g_free(shortcuts); + } + + return descr; +} + +bool SPGuide::remove(bool force) +{ + if (this->locked && !force) + return false; + + //XML Tree being used directly while it shouldn't be. + sp_repr_unparent(this->getRepr()); + + return true; +} + +/* + 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 : diff --git a/src/object/sp-guide.h b/src/object/sp-guide.h new file mode 100644 index 0000000..a7f20a6 --- /dev/null +++ b/src/object/sp-guide.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SPGuide -- a guideline + *//* + * Authors: + * Lauris Kaplinski 2000 + * Johan Engelen 2007 + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GUIDE_H +#define SEEN_SP_GUIDE_H + +#include <2geom/point.h> +#include <vector> + +#include "display/control/canvas-item-ptr.h" +#include "sp-object.h" + +typedef unsigned int guint32; +extern "C" { + typedef void (*GCallback) (); +} + +class SPDesktop; + +namespace Inkscape { + class CanvasItemGroup; + class CanvasItemGuideLine; + +namespace UI::Widget { + class Canvas; + +} +} // namespace Inkscape + + +/* Represents the constraint on p that dot(g.direction, p) == g.position. */ +class SPGuide final : public SPObject { +public: + SPGuide(); + ~SPGuide() override = default; + int tag() const override { return tag_of<decltype(*this)>; } + + void set_color(const unsigned r, const unsigned g, const unsigned b, bool const commit); + void setColor(guint32 c); + void setHiColor(guint32 h) { hicolor = h; } + + guint32 getColor() const { return color; } + guint32 getHiColor() const { return hicolor; } + Geom::Point getPoint() const { return point_on_line; } + Geom::Point getNormal() const { return normal_to_line; } + + void moveto(Geom::Point const point_on_line, bool const commit); + void set_normal(Geom::Point const normal_to_line, bool const commit); + + void set_label(const char* label, bool const commit); + char const* getLabel() const { return label; } + + void set_locked(const bool locked, bool const commit); + bool getLocked() const { return locked; } + + static SPGuide *createSPGuide(SPDocument *doc, Geom::Point const &pt1, Geom::Point const &pt2); + SPGuide *duplicate(); + + void showSPGuide(Inkscape::CanvasItemGroup *group); + void hideSPGuide(Inkscape::UI::Widget::Canvas *canvas); + void showSPGuide(); // argument-free versions + void hideSPGuide(); + bool remove(bool force=false); + + void sensitize(Inkscape::UI::Widget::Canvas *canvas, bool sensitive); + + bool isHorizontal() const { return (normal_to_line[Geom::X] == 0.); }; + bool isVertical() const { return (normal_to_line[Geom::Y] == 0.); }; + + char* description(bool const verbose = true) const; + + double angle() const { return std::atan2( - normal_to_line[Geom::X], normal_to_line[Geom::Y] ); } + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttr key, const char* value) override; + + char* label; + std::vector<CanvasItemPtr<Inkscape::CanvasItemGuideLine>> views; // See display/control/guideline.h. + bool locked; + Geom::Point normal_to_line; + Geom::Point point_on_line; + + guint32 color; + guint32 hicolor; +}; + +// These functions rightfully belong to SPDesktop. What gives?! +void sp_guide_pt_pairs_to_guides(SPDocument *doc, std::list<std::pair<Geom::Point, Geom::Point> > &pts); +void sp_guide_create_guides_around_page(SPDocument *doc); +void sp_guide_delete_all_guides(SPDocument *doc); + +#endif // SEEN_SP_GUIDE_H + +/* + 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 : diff --git a/src/object/sp-hatch-path.cpp b/src/object/sp-hatch-path.cpp new file mode 100644 index 0000000..6cf0cdb --- /dev/null +++ b/src/object/sp-hatch-path.cpp @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG <hatchPath> implementation + */ +/* + * Author: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2014 Tomasz Boczkowski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <string> +#include <2geom/path.h> + +#include "style.h" +#include "svg/svg.h" +#include "display/curve.h" +#include "display/drawing.h" +#include "display/drawing-shape.h" +#include "helper/geom.h" +#include "attributes.h" +#include "sp-item.h" +#include "sp-hatch-path.h" +#include "svg/css-ostringstream.h" + +SPHatchPath::SPHatchPath() = default; + +SPHatchPath::~SPHatchPath() = default; + +void SPHatchPath::build(SPDocument *doc, Inkscape::XML::Node *repr) +{ + SPObject::build(doc, repr); + + readAttr(SPAttr::D); + readAttr(SPAttr::OFFSET); + readAttr(SPAttr::STYLE); + + style->fill.setNone(); +} + +void SPHatchPath::release() +{ + views.clear(); + SPObject::release(); +} + +void SPHatchPath::set(SPAttr key, gchar const *value) +{ + switch (key) { + case SPAttr::D: + if (value) { + Geom::PathVector pv; + _readHatchPathVector(value, pv, _continuous); + _curve.emplace(std::move(pv)); + } else { + _curve.reset(); + } + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::OFFSET: + offset.readOrUnset(value); + 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 { + SPObject::set(key, value); + } + break; + } +} + +void SPHatchPath::update(SPCtx *ctx, unsigned int flags) +{ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + flags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; + } + + if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + if (style->stroke_width.unit == SP_CSS_UNIT_PERCENT) { + //TODO: Check specification + + SPItemCtx *ictx = static_cast<SPItemCtx *>(ctx); + double const aw = (ictx) ? 1.0 / ictx->i2vp.descrim() : 1.0; + style->stroke_width.computed = style->stroke_width.value * aw; + + for (auto &v : views) { + v.drawingitem->setStyle(style); + } + } + } + + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG)) { + for (auto &v : views) { + _updateView(v); + } + } +} + +bool SPHatchPath::isValid() const +{ + return !_curve || _repeatLength() > 0; +} + +Inkscape::DrawingItem *SPHatchPath::show(Inkscape::Drawing &drawing, unsigned int key, Geom::OptInterval extents) +{ + views.emplace_back(make_drawingitem<Inkscape::DrawingShape>(drawing), extents, key); + auto &v = views.back(); + auto s = v.drawingitem.get(); + + _updateView(v); + + return s; +} + +void SPHatchPath::hide(unsigned int 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(); +} + +void SPHatchPath::setStripExtents(unsigned int key, Geom::OptInterval const &extents) +{ + for (auto &v : views) { + if (v.key == key) { + v.extents = extents; + break; + } + } +} + +Geom::Interval SPHatchPath::bounds() const +{ + Geom::OptRect bbox; + Geom::Interval result; + + Geom::Affine transform = Geom::Translate(offset.computed, 0); + if (!_curve) { + SPCurve test_curve; + test_curve.moveto(Geom::Point(0, 0)); + test_curve.moveto(Geom::Point(0, 1)); + bbox = bounds_exact_transformed(test_curve.get_pathvector(), transform); + } else { + bbox = bounds_exact_transformed(_curve->get_pathvector(), transform); + } + + double stroke_width = style->stroke_width.computed; + result.setMin(bbox->left() - stroke_width / 2); + result.setMax(bbox->right() + stroke_width / 2); + return result; +} + +SPCurve SPHatchPath::calculateRenderCurve(unsigned key) const +{ + for (auto const &v : views) { + if (v.key == key) { + return _calculateRenderCurve(v); + } + } + g_assert_not_reached(); + return SPCurve{}; +} + +gdouble SPHatchPath::_repeatLength() const +{ + gdouble val = 0; + + if (_curve && _curve->last_point()) { + val = _curve->last_point()->y(); + } + + return val; +} + +void SPHatchPath::_updateView(View &view) +{ + auto calculated_curve = _calculateRenderCurve(view); + + Geom::Affine offset_transform = Geom::Translate(offset.computed, 0); + view.drawingitem->setTransform(offset_transform); + style->fill.setNone(); + view.drawingitem->setStyle(style); + view.drawingitem->setPath(std::make_shared<SPCurve>(std::move(calculated_curve))); +} + +SPCurve SPHatchPath::_calculateRenderCurve(View const &view) const +{ + SPCurve calculated_curve; + + if (!view.extents) { + return calculated_curve; + } + + if (!_curve) { + calculated_curve.moveto(0, view.extents->min()); + calculated_curve.lineto(0, view.extents->max()); + //TODO: if hatch has a dasharray defined, adjust line ends + } else { + gdouble repeatLength = _repeatLength(); + if (repeatLength > 0) { + gdouble initial_y = floor(view.extents->min() / repeatLength) * repeatLength; + int segment_cnt = ceil((view.extents->extent()) / repeatLength) + 1; + + auto segment = *_curve; + segment.transform(Geom::Translate(0, initial_y)); + + Geom::Affine step_transform = Geom::Translate(0, repeatLength); + for (int i = 0; i < segment_cnt; ++i) { + if (_continuous) { + calculated_curve.append_continuous(segment); + } else { + calculated_curve.append(segment); + } + segment.transform(step_transform); + } + } + } + return calculated_curve; +} + +void SPHatchPath::_readHatchPathVector(char const *str, Geom::PathVector &pathv, bool &continous_join) +{ + if (!str) { + return; + } + + pathv = sp_svg_read_pathv(str); + + if (!pathv.empty()) { + continous_join = false; + } else { + Glib::ustring str2 = Glib::ustring::compose("M0,0 %1", str); + pathv = sp_svg_read_pathv(str2.c_str()); + if (pathv.empty()) { + return; + } + + gdouble last_point_x = pathv.back().finalPoint().x(); + Inkscape::CSSOStringStream stream; + stream << last_point_x; + Glib::ustring str3 = Glib::ustring::compose("M%1,0 %2", stream.str(), str); + Geom::PathVector pathv3 = sp_svg_read_pathv(str3.c_str()); + + //Path can be composed of relative commands only. In this case final point + //coordinates would depend on first point position. If this happens, fall + //back to using 0,0 as first path point + if (pathv3.back().finalPoint().y() == pathv.back().finalPoint().y()) { + pathv = pathv3; + } + continous_join = true; + } +} + +SPHatchPath::View::View(DrawingItemPtr<Inkscape::DrawingShape> drawingitem, Geom::OptInterval const &extents, unsigned key) + : drawingitem(std::move(drawingitem)) + , extents(extents) + , 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 : diff --git a/src/object/sp-hatch-path.h b/src/object/sp-hatch-path.h new file mode 100644 index 0000000..19a4d48 --- /dev/null +++ b/src/object/sp-hatch-path.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG <hatchPath> implementation + */ +/* + * Author: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2014 Tomasz Boczkowski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_HATCH_PATH_H +#define SEEN_SP_HATCH_PATH_H + +#include <vector> +#include <cstddef> +#include <optional> +#include <glibmm/ustring.h> +#include <sigc++/connection.h> +#include <2geom/generic-interval.h> +#include <2geom/pathvector.h> + +#include "svg/svg-length.h" +#include "object/sp-object.h" +#include "display/curve.h" +#include "display/drawing-item-ptr.h" + +namespace Inkscape { + +class Drawing; +class DrawingShape; +class DrawingItem; + +} // namespace Inkscape + +class SPHatchPath final : public SPObject +{ +public: + SPHatchPath(); + ~SPHatchPath() override; + int tag() const override { return tag_of<decltype(*this)>; } + + SVGLength offset; + + bool isValid() const; + + Inkscape::DrawingItem *show(Inkscape::Drawing &drawing, unsigned int key, Geom::OptInterval extents); + void hide(unsigned int key); + + void setStripExtents(unsigned int key, Geom::OptInterval const &extents); + Geom::Interval bounds() const; + + SPCurve calculateRenderCurve(unsigned key) const; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttr key, const gchar* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + +private: + struct View + { + DrawingItemPtr<Inkscape::DrawingShape> drawingitem; + Geom::OptInterval extents; + unsigned key; + View(DrawingItemPtr<Inkscape::DrawingShape> drawingitem, Geom::OptInterval const &extents, unsigned key); + }; + std::vector<View> views; + + gdouble _repeatLength() const; + void _updateView(View &view); + SPCurve _calculateRenderCurve(View const &view) const; + + void _readHatchPathVector(char const *str, Geom::PathVector &pathv, bool &continous_join); + + std::optional<SPCurve> _curve; + bool _continuous = false; +}; + +#endif // SEEN_SP_HATCH_PATH_H + +/* + 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 : diff --git a/src/object/sp-hatch.cpp b/src/object/sp-hatch.cpp new file mode 100644 index 0000000..6d9eddd --- /dev/null +++ b/src/object/sp-hatch.cpp @@ -0,0 +1,762 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG <hatch> implementation + */ +/* + * Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2014 Tomasz Boczkowski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-hatch.h" + +#include <cstring> +#include <string> + +#include <2geom/transforms.h> +#include <sigc++/functors/mem_fun.h> + +#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<SPHatchPath>(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<SPHatchPath>(&child); + if (hatchPath) { + return true; + } + } + return false; +} + +std::vector<SPHatchPath*> SPHatch::hatchPaths() +{ + std::vector<SPHatchPath*> list; + SPHatch *src = chase_hrefs<SPHatch>(this, sigc::ptr_fun(&_hasHatchPatchChildren)); + + if (src) { + for (auto &child: src->children) { + auto hatchPath = cast<SPHatchPath>(&child); + if (hatchPath) { + list.push_back(hatchPath); + } + } + } + return list; +} + +std::vector<SPHatchPath const*> SPHatch::hatchPaths() const +{ + std::vector<SPHatchPath const*> list; + SPHatch const *src = chase_hrefs<SPHatch const>(this, sigc::ptr_fun(&_hasHatchPatchChildren)); + + if (src) { + for (auto &child: src->children) { + SPHatchPath const *hatchPath = cast<SPHatchPath>(&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<SPHatchPath *> 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<SPHatchPath *> 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<SPHatch>(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<SPHatchPath *> oldhatchPaths; + std::vector<SPHatchPath *> newhatchPaths; + + auto old_hatch = cast<SPHatch>(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<SPHatch>(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<SPHatch>(SP_STYLE_FILL_SERVER(style)) && + cast<SPHatch>(SP_STYLE_FILL_SERVER(style)) == this) { + i++; + } + if (style && style->stroke.isPaintserver() && is<SPHatch>(SP_STYLE_STROKE_SERVER(style)) && + cast<SPHatch>(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<SPHatch>(child)); + + hatch = cast<SPHatch>(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<Inkscape::DrawingPattern>(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<SPHatchPath *> 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<Inkscape::DrawingPattern> 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 : diff --git a/src/object/sp-hatch.h b/src/object/sp-hatch.h new file mode 100644 index 0000000..6e9923a --- /dev/null +++ b/src/object/sp-hatch.h @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG <hatch> implementation + */ +/* + * Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2014 Tomasz Boczkowski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_HATCH_H +#define SEEN_SP_HATCH_H + +#include <vector> +#include <cstddef> +#include <glibmm/ustring.h> +#include <sigc++/connection.h> + +#include "svg/svg-length.h" +#include "svg/svg-angle.h" +#include "sp-paint-server.h" +#include "uri-references.h" +#include "display/drawing-item-ptr.h" + +class SPHatchReference; +class SPHatchPath; +class SPItem; + +namespace Inkscape { +class Drawing; +class DrawingPattern; +namespace XML { class Node; } +} // namespace Inkscape + +class SPHatch final : public SPPaintServer +{ +public: + enum HatchUnits + { + UNITS_USERSPACEONUSE, + UNITS_OBJECTBOUNDINGBOX + }; + + struct RenderInfo + { + Geom::Affine child_transform; + Geom::Affine pattern_to_user_transform; + Geom::Rect tile_rect; + + int overflow_steps = 0; + Geom::Affine overflow_step_transform; + Geom::Affine overflow_initial_transform; + }; + + SPHatch(); + ~SPHatch() override; + int tag() const override { return tag_of<decltype(*this)>; } + + // Reference (href) + Glib::ustring href; + SPHatchReference *ref; + + gdouble x() const; + gdouble y() const; + gdouble pitch() const; + gdouble rotate() const; + HatchUnits hatchUnits() const; + HatchUnits hatchContentUnits() const; + Geom::Affine const &hatchTransform() const; + SPHatch *rootHatch(); //TODO: const + + std::vector<SPHatchPath *> hatchPaths(); + std::vector<SPHatchPath const *> hatchPaths() const; + + SPHatch *clone_if_necessary(SPItem *item, const gchar *property); + void transform_multiply(Geom::Affine postmul, bool set); + + bool isValid() const override; + + Inkscape::DrawingPattern *show(Inkscape::Drawing &drawing, unsigned key, Geom::OptRect const &bbox) override; + void hide(unsigned key) override; + + RenderInfo calculateRenderInfo(unsigned key) const; + Geom::Interval bounds() const; + void setBBox(unsigned int key, Geom::OptRect const &bbox) override; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void set(SPAttr key, const gchar* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + +private: + struct View + { + DrawingItemPtr<Inkscape::DrawingPattern> drawingitem; + Geom::OptRect bbox; + unsigned key; + View(DrawingItemPtr<Inkscape::DrawingPattern> drawingitem, Geom::OptRect const &bbox, unsigned key); + }; + std::vector<View> views; + + static bool _hasHatchPatchChildren(SPHatch const *hatch); + + void _updateView(View &view); + RenderInfo _calculateRenderInfo(View const &view) const; + Geom::OptInterval _calculateStripExtents(Geom::OptRect const &bbox) const; + + /** + * Count how many times hatch is used by the styles of o and its descendants + */ + guint _countHrefs(SPObject *o) const; + + /** + * Gets called when the hatch is reattached to another <hatch> + */ + void _onRefChanged(SPObject *old_ref, SPObject *ref); + + /** + * Gets called when the referenced <hatch> is changed + */ + void _onRefModified(SPObject *ref, guint flags); + + // patternUnits and patternContentUnits attribute + HatchUnits _hatchUnits : 1; + bool _hatchUnits_set : 1; + HatchUnits _hatchContentUnits : 1; + bool _hatchContentUnits_set : 1; + + // hatchTransform attribute + Geom::Affine _hatchTransform; + bool _hatchTransform_set : 1; + + // Strip + SVGLength _x; + SVGLength _y; + SVGLength _pitch; + SVGAngle _rotate; + + sigc::connection _modified_connection; +}; + +class SPHatchReference : public Inkscape::URIReference +{ +public: + SPHatchReference(SPHatch *obj) + : URIReference(obj) + {} + + SPHatch *getObject() const + { + return static_cast<SPHatch*>(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject *obj) const override + { + return is<SPHatch>(obj) && URIReference::_acceptObject(obj); + } +}; + +#endif // SEEN_SP_HATCH_H + +/* + 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 : diff --git a/src/object/sp-image.cpp b/src/object/sp-image.cpp new file mode 100644 index 0000000..12cdb48 --- /dev/null +++ b/src/object/sp-image.cpp @@ -0,0 +1,952 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <image> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Edward Flick (EAF) + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2005 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cstring> +#include <algorithm> +#include <string> +#include <glibmm.h> +#include <glib/gstdio.h> +#include <2geom/rect.h> +#include <2geom/transforms.h> +#include <glibmm/i18n.h> +#include <giomm/error.h> + +#include "snap-candidate.h" +#include "snap-preferences.h" +#include "display/drawing-image.h" +#include "display/cairo-utils.h" +#include "display/curve.h" +// Added for preserveAspectRatio support -- EAF +#include "attributes.h" +#include "print.h" +#include "document.h" +#include "sp-image.h" +#include "sp-clippath.h" +#include "xml/quote.h" +#include "xml/href-attribute-helper.h" +#include "preferences.h" +#include "io/sys.h" + +#include "cms-system.h" +#include "color-profile.h" +#include <lcms2.h> + +//#define DEBUG_LCMS +#ifdef DEBUG_LCMS +#define DEBUG_MESSAGE(key, ...)\ +{\ + g_message( __VA_ARGS__ );\ +} +#include <gtk/gtk.h> +#else +#define DEBUG_MESSAGE(key, ...) +#endif // DEBUG_LCMS +/* + * SPImage + */ + +// TODO: give these constants better names: +#define MAGIC_EPSILON 1e-9 +#define MAGIC_EPSILON_TOO 1e-18 +// TODO: also check if it is correct to be using two different epsilon values + +static void sp_image_set_curve(SPImage *image); +static void sp_image_update_arenaitem (SPImage *img, Inkscape::DrawingImage *ai); +static void sp_image_update_canvas_image (SPImage *image); + +#ifdef DEBUG_LCMS +extern guint update_in_progress; +#define DEBUG_MESSAGE_SCISLAC(key, ...) \ +{\ + Inkscape::Preferences *prefs = Inkscape::Preferences::get();\ + bool dump = prefs->getBool("/options/scislac/" #key);\ + bool dumpD = prefs->getBool("/options/scislac/" #key "D");\ + bool dumpD2 = prefs->getBool("/options/scislac/" #key "D2");\ + dumpD &&= ( (update_in_progress == 0) || dumpD2 );\ + if ( dump )\ + {\ + g_message( __VA_ARGS__ );\ +\ + }\ + if ( dumpD )\ + {\ + GtkWidget *dialog = gtk_message_dialog_new(NULL,\ + GTK_DIALOG_DESTROY_WITH_PARENT, \ + GTK_MESSAGE_INFO, \ + GTK_BUTTONS_OK, \ + __VA_ARGS__ \ + );\ + g_signal_connect_swapped(dialog, "response",\ + G_CALLBACK(gtk_widget_destroy), \ + dialog); \ + gtk_widget_show_all( dialog );\ + }\ +} +#else // DEBUG_LCMS +#define DEBUG_MESSAGE_SCISLAC(key, ...) +#endif // DEBUG_LCMS + +SPImage::SPImage() : SPItem(), SPViewBox() { + + this->x.unset(); + this->y.unset(); + this->width.unset(); + this->height.unset(); + this->clipbox = Geom::Rect(); + this->sx = this->sy = 1.0; + this->ox = this->oy = 0.0; + this->dpi = 96.00; + this->prev_width = 0.0; + this->prev_height = 0.0; + + this->href = nullptr; + this->color_profile = nullptr; +} + +SPImage::~SPImage() = default; + +void SPImage::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPItem::build(document, repr); + + this->readAttr(SPAttr::XLINK_HREF); + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + this->readAttr(SPAttr::WIDTH); + this->readAttr(SPAttr::HEIGHT); + this->readAttr(SPAttr::SVG_DPI); + this->readAttr(SPAttr::PRESERVEASPECTRATIO); + this->readAttr(SPAttr::COLOR_PROFILE); + + /* Register */ + document->addResource("image", this); +} + +void SPImage::release() { + if (this->document) { + // Unregister ourselves + this->document->removeResource("image", this); + } + + if (this->href) { + g_free (this->href); + this->href = nullptr; + } + + pixbuf.reset(); + + if (this->color_profile) { + g_free (this->color_profile); + this->color_profile = nullptr; + } + + curve.reset(); + + SPItem::release(); +} + +void SPImage::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::XLINK_HREF: + g_free (this->href); + this->href = (value) ? g_strdup (value) : nullptr; + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_IMAGE_HREF_MODIFIED_FLAG); + break; + + case SPAttr::X: + /* ex, em not handled correctly. */ + if (!this->x.read(value)) { + this->x.unset(); + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::Y: + /* ex, em not handled correctly. */ + if (!this->y.read(value)) { + this->y.unset(); + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::WIDTH: + /* ex, em not handled correctly. */ + if (!this->width.read(value)) { + this->width.unset(); + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::HEIGHT: + /* ex, em not handled correctly. */ + if (!this->height.read(value)) { + this->height.unset(); + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SVG_DPI: + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_IMAGE_HREF_MODIFIED_FLAG); + break; + + case SPAttr::PRESERVEASPECTRATIO: + set_preserveAspectRatio( value ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::COLOR_PROFILE: + if ( this->color_profile ) { + g_free (this->color_profile); + } + + this->color_profile = (value) ? g_strdup (value) : nullptr; + + if ( value ) { + DEBUG_MESSAGE( lcmsFour, "<this> color-profile set to '%s'", value ); + } else { + DEBUG_MESSAGE( lcmsFour, "<this> color-profile cleared" ); + } + + // TODO check on this HREF_MODIFIED flag + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_IMAGE_HREF_MODIFIED_FLAG); + break; + + + default: + SPItem::set(key, value); + break; + } + + sp_image_set_curve(this); //creates a curve at the image's boundary for snapping +} + +// BLIP +void SPImage::apply_profile(Inkscape::Pixbuf *pixbuf) { + + // TODO: this will prevent using MIME data when exporting. + // Integrate color correction into loading. + pixbuf->ensurePixelFormat(Inkscape::Pixbuf::PF_GDK); + int imagewidth = pixbuf->width(); + int imageheight = pixbuf->height(); + int rowstride = pixbuf->rowstride(); + guchar* px = pixbuf->pixels(); + + if ( px ) { + DEBUG_MESSAGE( lcmsFive, "in <image>'s sp_image_update. About to call colorprofile_get_handle()" ); + + guint profIntent = Inkscape::RENDERING_INTENT_UNKNOWN; + cmsHPROFILE prof = Inkscape::CMSSystem::getHandle( this->document, + &profIntent, + this->color_profile ); + if ( prof ) { + cmsProfileClassSignature profileClass = cmsGetDeviceClass( prof ); + if ( profileClass != cmsSigNamedColorClass ) { + int intent = INTENT_PERCEPTUAL; + + switch ( profIntent ) { + case Inkscape::RENDERING_INTENT_RELATIVE_COLORIMETRIC: + intent = INTENT_RELATIVE_COLORIMETRIC; + break; + case Inkscape::RENDERING_INTENT_SATURATION: + intent = INTENT_SATURATION; + break; + case Inkscape::RENDERING_INTENT_ABSOLUTE_COLORIMETRIC: + intent = INTENT_ABSOLUTE_COLORIMETRIC; + break; + case Inkscape::RENDERING_INTENT_PERCEPTUAL: + case Inkscape::RENDERING_INTENT_UNKNOWN: + case Inkscape::RENDERING_INTENT_AUTO: + default: + intent = INTENT_PERCEPTUAL; + } + + cmsHPROFILE destProf = cmsCreate_sRGBProfile(); + cmsHTRANSFORM transf = cmsCreateTransform( prof, + TYPE_RGBA_8, + destProf, + TYPE_RGBA_8, + intent, 0 ); + if ( transf ) { + guchar* currLine = px; + for ( int y = 0; y < imageheight; y++ ) { + // Since the types are the same size, we can do the transformation in-place + cmsDoTransform( transf, currLine, currLine, imagewidth ); + currLine += rowstride; + } + + cmsDeleteTransform( transf ); + } else { + DEBUG_MESSAGE( lcmsSix, "in <image>'s sp_image_update. Unable to create LCMS transform." ); + } + + cmsCloseProfile( destProf ); + } else { + DEBUG_MESSAGE( lcmsSeven, "in <image>'s sp_image_update. Profile type is named color. Can't transform." ); + } + } else { + DEBUG_MESSAGE( lcmsEight, "in <image>'s sp_image_update. No profile found." ); + } + } +} + +void SPImage::update(SPCtx *ctx, unsigned int flags) { + SPItem::update(ctx, flags); + + if (flags & SP_IMAGE_HREF_MODIFIED_FLAG) { + pixbuf.reset(); + if (href) { + Inkscape::Pixbuf *pb = nullptr; + double svgdpi = 96; + if (getRepr()->attribute("inkscape:svg-dpi")) { + svgdpi = g_ascii_strtod(getRepr()->attribute("inkscape:svg-dpi"), nullptr); + } + dpi = svgdpi; + pb = readImage(Inkscape::getHrefAttribute(*getRepr()).second, + getRepr()->attribute("sodipodi:absref"), + document->getDocumentBase(), svgdpi); + if (!pb) { + missing = true; + // Passing in our previous size allows us to preserve the image's expected size. + auto broken_width = width._set ? width.computed : 640; + auto broken_height = height._set ? height.computed : 640; + pb = getBrokenImage(broken_width, broken_height); + } + else { + missing = false; + } + + if (pb) { + if (color_profile) apply_profile(pb); + pb->ensurePixelFormat(Inkscape::Pixbuf::PF_CAIRO); // Expected by rendering code, so convert now before making immutable. + pixbuf = std::shared_ptr<Inkscape::Pixbuf>(pb); + } + } + } + + SPItemCtx *ictx = (SPItemCtx *) ctx; + + // Why continue without a pixbuf? So we can display "Missing Image" png. + // Eventually, we should properly support SVG image type (i.e. render it ourselves). + if (this->pixbuf) { + if (!this->x._set) { + this->x.unit = SVGLength::PX; + this->x.computed = 0; + } + + if (!this->y._set) { + this->y.unit = SVGLength::PX; + this->y.computed = 0; + } + + if (!this->width._set) { + this->width.unit = SVGLength::PX; + this->width.computed = this->pixbuf->width(); + } + + if (!this->height._set) { + this->height.unit = SVGLength::PX; + this->height.computed = this->pixbuf->height(); + } + } + + // Calculate x, y, width, height from parent/initial viewport, see sp-root.cpp + this->calcDimsFromParentViewport(ictx); + + // Image creates a new viewport + ictx->viewport = Geom::Rect::from_xywh(this->x.computed, this->y.computed, + this->width.computed, this->height.computed); + + this->clipbox = ictx->viewport; + + this->ox = this->x.computed; + this->oy = this->y.computed; + + if (this->pixbuf) { + + // Viewbox is either from SVG (not supported) or dimensions of pixbuf (PNG, JPG) + this->viewBox = Geom::Rect::from_xywh(0, 0, this->pixbuf->width(), this->pixbuf->height()); + this->viewBox_set = true; + + // SPItemCtx rctx = + get_rctx( ictx ); + + this->ox = c2p[4]; + this->oy = c2p[5]; + this->sx = c2p[0]; + this->sy = c2p[3]; + } + + // TODO: eliminate ox, oy, sx, sy + + sp_image_update_canvas_image ((SPImage *) this); + + // don't crash with missing xlink:href attribute + if (!this->pixbuf) { + return; + } + + double proportion_pixbuf = this->pixbuf->height() / (double)this->pixbuf->width(); + double proportion_image = this->height.computed / (double)this->width.computed; + if (this->prev_width && + (this->prev_width != this->pixbuf->width() || this->prev_height != this->pixbuf->height())) { + if (std::abs(this->prev_width - this->pixbuf->width()) > std::abs(this->prev_height - this->pixbuf->height())) { + proportion_pixbuf = this->pixbuf->width() / (double)this->pixbuf->height(); + proportion_image = this->width.computed / (double)this->height.computed; + if (proportion_pixbuf != proportion_image) { + double new_height = this->height.computed * proportion_pixbuf; + this->getRepr()->setAttributeSvgDouble("width", new_height); + } + } + else { + if (proportion_pixbuf != proportion_image) { + double new_width = this->width.computed * proportion_pixbuf; + this->getRepr()->setAttributeSvgDouble("height", new_width); + } + } + } + this->prev_width = this->pixbuf->width(); + this->prev_height = this->pixbuf->height(); +} + +void SPImage::modified(unsigned int flags) { +// SPItem::onModified(flags); + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (auto &v : views) { + auto img = cast<Inkscape::DrawingImage>(v.drawingitem.get()); + img->setStyle(style); + } + } +} + +Inkscape::XML::Node *SPImage::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags ) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:image"); + } + + Inkscape::setHrefAttribute(*repr, this->href); + + /* fixme: Reset attribute if needed (Lauris) */ + if (this->x._set) { + repr->setAttributeSvgDouble("x", this->x.computed); + } + + if (this->y._set) { + repr->setAttributeSvgDouble("y", this->y.computed); + } + + if (this->width._set) { + repr->setAttributeSvgDouble("width", this->width.computed); + } + + if (this->height._set) { + repr->setAttributeSvgDouble("height", this->height.computed); + } + repr->setAttribute("inkscape:svg-dpi", this->getRepr()->attribute("inkscape:svg-dpi")); + + this->write_preserveAspectRatio(repr); + + if (this->color_profile) { + repr->setAttribute("color-profile", this->color_profile); + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + +Geom::OptRect SPImage::bbox(Geom::Affine const &transform, SPItem::BBoxType /*type*/) const { + Geom::OptRect bbox; + + if ((this->width.computed > 0.0) && (this->height.computed > 0.0)) { + bbox = Geom::Rect::from_xywh(this->x.computed, this->y.computed, this->width.computed, this->height.computed); + *bbox *= transform; + } + + return bbox; +} + +void SPImage::print(SPPrintContext *ctx) { + if (pixbuf && width.computed > 0.0 && height.computed > 0.0) { + auto pb = *pixbuf; + pb.ensurePixelFormat(Inkscape::Pixbuf::PF_GDK); + + guchar *px = pb.pixels(); + int w = pb.width(); + int h = pb.height(); + int rs = pb.rowstride(); + + double vx = this->ox; + double vy = this->oy; + + Geom::Affine t; + Geom::Translate tp(vx, vy); + Geom::Scale s(this->sx, this->sy); + t = s * tp; + ctx->image_R8G8B8A8_N(px, w, h, rs, t, this->style); + } +} + +const char* SPImage::typeName() const { + return "image"; +} + +const char* SPImage::displayName() const { + return _("Image"); +} + +gchar* SPImage::description() const { + char *href_desc; + + if (this->href) { + href_desc = (strncmp(this->href, "data:", 5) == 0) + ? g_strdup(_("embedded")) + : xml_quote_strdup(this->href); + } else { + g_warning("Attempting to call strncmp() with a null pointer."); + href_desc = g_strdup("(null_pointer)"); // we call g_free() on href_desc + } + + char *ret = ( !pixbuf + ? g_strdup_printf(_("[bad reference]: %s"), href_desc) + : g_strdup_printf(_("%d × %d: %s"), + pixbuf->width(), + pixbuf->height(), + href_desc) ); + + if (!pixbuf && document) + { + Inkscape::Pixbuf * pb = nullptr; + double svgdpi = 96; + if (this->getRepr()->attribute("inkscape:svg-dpi")) { + svgdpi = g_ascii_strtod(this->getRepr()->attribute("inkscape:svg-dpi"), nullptr); + } + pb = readImage(Inkscape::getHrefAttribute(*this->getRepr()).second, + this->getRepr()->attribute("sodipodi:absref"), + this->document->getDocumentBase(), svgdpi); + + if (pb) { + ret = g_strdup_printf(_("%d × %d: %s"), + pb->width(), + pb->height(), + href_desc); + delete pb; + } else { + ret = g_strdup(_("{Broken Image}")); + } + } + + g_free(href_desc); + return ret; +} + +Inkscape::DrawingItem* SPImage::show(Inkscape::Drawing &drawing, unsigned int /*key*/, unsigned int /*flags*/) { + Inkscape::DrawingImage *ai = new Inkscape::DrawingImage(drawing); + + sp_image_update_arenaitem(this, ai); + + return ai; +} + + +Inkscape::Pixbuf *SPImage::readImage(gchar const *href, gchar const *absref, gchar const *base, double svgdpi) +{ + Inkscape::Pixbuf *inkpb = nullptr; + + gchar const *filename = href; + + if (filename != nullptr) { + if (g_ascii_strncasecmp(filename, "data:", 5) == 0) { + /* data URI - embedded image */ + filename += 5; + inkpb = Inkscape::Pixbuf::create_from_data_uri(filename, svgdpi); + } else { + auto url = Inkscape::URI::from_href_and_basedir(href, base); + + if (url.hasScheme("file")) { + auto native = url.toNativeFilename(); + inkpb = Inkscape::Pixbuf::create_from_file(native.c_str(), svgdpi); + } else { + try { + auto contents = url.getContents(); + inkpb = Inkscape::Pixbuf::create_from_buffer(contents, svgdpi); + } catch (const Gio::Error &e) { + g_warning("URI::getContents failed for '%.100s'", href); + } + } + } + + if (inkpb != nullptr) { + return inkpb; + } + } + + /* at last try to load from sp absolute path name */ + filename = absref; + if (filename != nullptr) { + // using absref is outside of SVG rules, so we must at least warn the user + if ( base != nullptr && href != nullptr ) { + g_warning ("<image xlink:href=\"%s\"> did not resolve to a valid image file (base dir is %s), now trying sodipodi:absref=\"%s\"", href, base, absref); + } else { + g_warning ("xlink:href did not resolve to a valid image file, now trying sodipodi:absref=\"%s\"", absref); + } + + inkpb = Inkscape::Pixbuf::create_from_file(filename, svgdpi); + if (inkpb != nullptr) { + return inkpb; + } + } + return inkpb; +} + +static std::string broken_image_svg = R"A( +<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}"> + <defs> + <symbol id="nope" style="fill:none;stroke:#ffffff;stroke-width:3" viewBox="0 0 10 10" preserveAspectRatio="{aspect}"> + <circle cx="0" cy="0" r="10" style="fill:#a40000;stroke:#cc0000" /> + <line x1="0" x2="0" y1="-5" y2="5" transform="rotate(45)" /> + <line x1="0" x2="0" y1="-5" y2="5" transform="rotate(-45)" /> + </symbol> + </defs> + <rect width="100%" height="100%" style="fill:white;stroke:#cc0000;stroke-width:6%" /> + <use xlink:href="#nope" width="30%" height="30%" x="50%" y="50%" /> +</svg> + +)A"; + +/** + * Load a standard broken image svg, used if we fail to load pixbufs from the href. + */ +Inkscape::Pixbuf *SPImage::getBrokenImage(double width, double height) +{ + // Cheap templating for size allows for dynamic sized svg + std::string copy = broken_image_svg; + copy.replace(copy.find("{width}"), std::string("{width}").size(), std::to_string(width)); + copy.replace(copy.find("{height}"), std::string("{height}").size(), std::to_string(height)); + + // Aspect attempts to make the image better for different ratios of images we might be dropped into + copy.replace(copy.find("{aspect}"), std::string("{aspect}").size(), + width > height ? "xMinYMid" : "xMidYMin"); + + auto inkpb = Inkscape::Pixbuf::create_from_buffer(copy, 0, "brokenimage.svg"); + + /* It's included here so if it still does not does load, our libraries are broken! */ + g_assert (inkpb != nullptr); + + return inkpb; +} + +/* We assert that realpixbuf is either NULL or identical size to pixbuf */ +static void +sp_image_update_arenaitem (SPImage *image, Inkscape::DrawingImage *ai) +{ + ai->setStyle(image->style); + ai->setPixbuf(image->pixbuf); + ai->setOrigin(Geom::Point(image->ox, image->oy)); + ai->setScale(image->sx, image->sy); + ai->setClipbox(image->clipbox); +} + +static void sp_image_update_canvas_image(SPImage *image) +{ + for (auto &v : image->views) { + sp_image_update_arenaitem(image, cast<Inkscape::DrawingImage>(v.drawingitem.get())); + } +} + +void SPImage::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + /* An image doesn't have any nodes to snap, but still we want to be able snap one image + to another. Therefore we will create some snappoints at the corner, similar to a rect. If + the image is rotated, then the snappoints will rotate with it. Again, just like a rect. + */ + + if (this->getClipObject()) { + //We are looking at a clipped image: do not return any snappoints, as these might be + //far far away from the visible part from the clipped image + //TODO Do return snappoints, but only when within visual bounding box + } else { + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_IMG_CORNER)) { + // The image has not been clipped: return its corners, which might be rotated for example + double const x0 = this->x.computed; + double const y0 = this->y.computed; + double const x1 = x0 + this->width.computed; + double const y1 = y0 + this->height.computed; + + Geom::Affine const i2d (this->i2dt_affine ()); + + p.emplace_back(Geom::Point(x0, y0) * i2d, Inkscape::SNAPSOURCE_IMG_CORNER, Inkscape::SNAPTARGET_IMG_CORNER); + p.emplace_back(Geom::Point(x0, y1) * i2d, Inkscape::SNAPSOURCE_IMG_CORNER, Inkscape::SNAPTARGET_IMG_CORNER); + p.emplace_back(Geom::Point(x1, y1) * i2d, Inkscape::SNAPSOURCE_IMG_CORNER, Inkscape::SNAPTARGET_IMG_CORNER); + p.emplace_back(Geom::Point(x1, y0) * i2d, Inkscape::SNAPSOURCE_IMG_CORNER, Inkscape::SNAPTARGET_IMG_CORNER); + } + } +} + +/* + * Initially we'll do: + * Transform x, y, set x, y, clear translation + */ + +Geom::Affine SPImage::set_transform(Geom::Affine const &xform) { + /* Calculate position in parent coords. */ + Geom::Point pos( Geom::Point(this->x.computed, this->y.computed) * xform ); + + /* This function takes care of translation and scaling, we return whatever parts we can't + handle. */ + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + Geom::Point const scale(hypot(ret[0], ret[1]), + hypot(ret[2], ret[3])); + + if ( scale[Geom::X] > MAGIC_EPSILON ) { + ret[0] /= scale[Geom::X]; + ret[1] /= scale[Geom::X]; + } else { + ret[0] = 1.0; + ret[1] = 0.0; + } + + if ( scale[Geom::Y] > MAGIC_EPSILON ) { + ret[2] /= scale[Geom::Y]; + ret[3] /= scale[Geom::Y]; + } else { + ret[2] = 0.0; + ret[3] = 1.0; + } + + this->width = this->width.computed * scale[Geom::X]; + this->height = this->height.computed * scale[Geom::Y]; + + /* Find position in item coords */ + pos = pos * ret.inverse(); + this->x = pos[Geom::X]; + this->y = pos[Geom::Y]; + + return ret; +} + +static void sp_image_set_curve( SPImage *image ) +{ + //create a curve at the image's boundary for snapping + if ((image->height.computed < MAGIC_EPSILON_TOO) || (image->width.computed < MAGIC_EPSILON_TOO) || (image->getClipObject())) { + } else { + Geom::OptRect rect = image->bbox(Geom::identity(), SPItem::VISUAL_BBOX); + + if (rect->isFinite()) { + image->curve.emplace(*rect, true); + } + } +} + +/** + * Return a borrowed pointer to curve (if any exists) or NULL if there is no curve + */ +SPCurve const *SPImage::get_curve() const +{ + return curve ? &*curve : nullptr; +} + +void sp_embed_image(Inkscape::XML::Node *image_node, Inkscape::Pixbuf *pb) +{ + bool free_data = false; + + // check whether the pixbuf has MIME data + guchar *data = nullptr; + gsize len = 0; + std::string data_mimetype; + + data = const_cast<guchar *>(pb->getMimeData(len, data_mimetype)); + + if (data == nullptr) { + // if there is no supported MIME data, embed as PNG + data_mimetype = "image/png"; + gdk_pixbuf_save_to_buffer(pb->getPixbufRaw(), reinterpret_cast<gchar**>(&data), &len, "png", nullptr, nullptr); + free_data = true; + } + + // Save base64 encoded data in image node + // this formula taken from Glib docs + gsize needed_size = len * 4 / 3 + len * 4 / (3 * 72) + 7; + needed_size += 5 + 8 + data_mimetype.size(); // 5 bytes for data: + 8 for ;base64, + + gchar *buffer = (gchar *) g_malloc(needed_size); + gchar *buf_work = buffer; + buf_work += g_sprintf(buffer, "data:%s;base64,", data_mimetype.c_str()); + + gint state = 0; + gint save = 0; + gsize written = 0; + written += g_base64_encode_step(data, len, TRUE, buf_work, &state, &save); + written += g_base64_encode_close(TRUE, buf_work + written, &state, &save); + buf_work[written] = 0; // null terminate + + // TODO: this is very wasteful memory-wise. + // It would be better to only keep the binary data around, + // and base64 encode on the fly when saving the XML. + Inkscape::setHrefAttribute(*image_node, buffer); + + g_free(buffer); + if (free_data) g_free(data); +} + +void sp_embed_svg(Inkscape::XML::Node *image_node, std::string const &fn) +{ + if (!g_file_test(fn.c_str(), G_FILE_TEST_EXISTS)) { + return; + } + GStatBuf stdir; + int val = g_stat(fn.c_str(), &stdir); + if (val == 0 && stdir.st_mode & S_IFDIR){ + return; + } + + // we need to load the entire file into memory, + // since we'll store it as MIME data + gchar *data = nullptr; + gsize len = 0; + GError *error = nullptr; + + if (g_file_get_contents(fn.c_str(), &data, &len, &error)) { + + if (error != nullptr) { + std::cerr << "Pixbuf::create_from_file: " << error->message << std::endl; + std::cerr << " (" << fn << ")" << std::endl; + return; + } + + std::string data_mimetype = "image/svg+xml"; + + + // Save base64 encoded data in image node + // this formula taken from Glib docs + gsize needed_size = len * 4 / 3 + len * 4 / (3 * 72) + 7; + needed_size += 5 + 8 + data_mimetype.size(); // 5 bytes for data: + 8 for ;base64, + + gchar *buffer = (gchar *) g_malloc(needed_size); + gchar *buf_work = buffer; + buf_work += g_sprintf(buffer, "data:%s;base64,", data_mimetype.c_str()); + + gint state = 0; + gint save = 0; + gsize written = 0; + written += g_base64_encode_step(reinterpret_cast<guchar *>(data), len, TRUE, buf_work, &state, &save); + written += g_base64_encode_close(TRUE, buf_work + written, &state, &save); + buf_work[written] = 0; // null terminate + + // TODO: this is very wasteful memory-wise. + // It would be better to only keep the binary data around, + // and base64 encode on the fly when saving the XML. + Inkscape::setHrefAttribute(*image_node, buffer); + + g_free(buffer); + g_free(data); + } +} + +void SPImage::refresh_if_outdated() +{ + if ( href && pixbuf && pixbuf->modificationTime()) { + // It *might* change + + GStatBuf st; + memset(&st, 0, sizeof(st)); + int val = 0; + if (g_file_test (pixbuf->originalPath().c_str(), G_FILE_TEST_EXISTS)){ + val = g_stat(pixbuf->originalPath().c_str(), &st); + } + if ( !val ) { + // stat call worked. Check time now + if ( st.st_mtime != pixbuf->modificationTime() ) { + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_IMAGE_HREF_MODIFIED_FLAG); + } + } + } +} + +/** + * Crop the image (remove pixels) based on the area rectangle + * and translate image to componsate for movement. + * + * @param area - Rectangle in document units + * + * @returns true if any pixels were removed. + */ +bool SPImage::cropToArea(Geom::Rect area) +{ + area *= i2doc_affine().inverse(); + + // Apply the image's viewbox and scal to get us image pixels + area *= Geom::Translate(-x.computed, -y.computed); + area *= Geom::Scale(pixbuf->width() / width.computed, pixbuf->height() / height.computed); + + // Any precision problems and we choose to retain more pixels (roundOut) + return cropToArea(area.roundOutwards()); +} + +/** + * Crop to the actual pixel area of the image, and adjusting the + * image's coordinates to compensate for the changes. + * + * @param area - Rectangle in image pixel units + * + * @returns true if any pixels were removed. + */ +bool SPImage::cropToArea(const Geom::IntRect &area) +{ + // Contrain requested area to the available pixels. + auto px = Geom::IntRect::from_xywh(0.0, 0.0, pixbuf->width(), pixbuf->height()); + auto px_area = area & px; + if (!px_area) + return false; + + if (auto pb = pixbuf->cropTo(*px_area)) { + // Crop ended up with bad pixels, this should rarely happen. + if (pb->width() <= 0 || pb->height() <= 0) + return false; + + // Cropping is done, now embed this image back into image tag. + sp_embed_image(getRepr(), pb); + + // Our new image has new sizes, so adjust image tag's internal viewbox + auto repr = getRepr(); + auto scale_x = px.width() / width.computed; + auto scale_y = px.height() / height.computed; + repr->setAttributeSvgDouble("x", this->x.computed + (px_area->left() / scale_x)); + repr->setAttributeSvgDouble("y", this->y.computed + (px_area->top() / scale_y)); + repr->setAttributeSvgDouble("width", px_area->width() / scale_x); + repr->setAttributeSvgDouble("height", px_area->height() / scale_y); + + return true; + } + return false; +} + +/* + 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 : diff --git a/src/object/sp-image.h b/src/object/sp-image.h new file mode 100644 index 0000000..88012c4 --- /dev/null +++ b/src/object/sp-image.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SVG <image> implementation + *//* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Edward Flick (EAF) + * + * Copyright (C) 1999-2005 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_SP_IMAGE_H +#define SEEN_INKSCAPE_SP_IMAGE_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <glibmm/ustring.h> +#include "svg/svg-length.h" +#include "sp-item.h" +#include "viewbox.h" +#include "sp-dimensions.h" +#include "display/curve.h" + +#include <memory> + +#define SP_IMAGE_HREF_MODIFIED_FLAG SP_OBJECT_USER_MODIFIED_FLAG_A + +namespace Inkscape { class Pixbuf; } +class SPImage final : public SPItem, public SPViewBox, public SPDimensions { +public: + SPImage(); + ~SPImage() override; + int tag() const override { return tag_of<decltype(*this)>; } + + Geom::Rect clipbox; + double sx, sy; + double ox, oy; + double dpi; + double prev_width, prev_height; + + std::optional<SPCurve> curve; // This curve is at the image's boundary for snapping + + char *href; + char *color_profile; + + std::shared_ptr<Inkscape::Pixbuf const> pixbuf; + bool missing = true; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void modified(unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + void print(SPPrintContext *ctx) override; + const char* typeName() const override; + const char* displayName() const override; + char* description() const override; + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + Geom::Affine set_transform(Geom::Affine const &transform) override; + + void apply_profile(Inkscape::Pixbuf *pixbuf); + + SPCurve const *get_curve() const; + void refresh_if_outdated(); + bool cropToArea(Geom::Rect area); + bool cropToArea(const Geom::IntRect &area); +private: + static Inkscape::Pixbuf *readImage(gchar const *href, gchar const *absref, gchar const *base, double svgdpi = 0); + static Inkscape::Pixbuf *getBrokenImage(double width, double height); +}; + +/* Return duplicate of curve or NULL */ +void sp_embed_image(Inkscape::XML::Node *imgnode, Inkscape::Pixbuf *pb); +void sp_embed_svg(Inkscape::XML::Node *image_node, std::string const &fn); + +#endif diff --git a/src/object/sp-item-group.cpp b/src/object/sp-item-group.cpp new file mode 100644 index 0000000..f81043c --- /dev/null +++ b/src/object/sp-item-group.cpp @@ -0,0 +1,1118 @@ +// 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 + auto item = cast<SPItem>(last_child); + if ( item ) { + /* TODO: this should be moved into SPItem somehow */ + for (auto &v : views) { + auto ac = item->invoke_show(v.drawingitem->drawing(), v.key, v.flags); + if (ac) { + v.drawingitem->appendChild(ac); + } + } + } + } else { // general case + auto item = cast<SPItem>(get_child_by_repr(child)); + if ( item ) { + /* TODO: this should be moved into SPItem somehow */ + unsigned position = item->pos_in_parent(); + + for (auto &v : views) { + auto ac = item->invoke_show (v.drawingitem->drawing(), v.key, v.flags); + if (ac) { + v.drawingitem->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); + + auto item = cast<SPItem>(get_child_by_repr(child)); + if ( item ) { + /* TODO: this should be moved into SPItem somehow */ + unsigned position = item->pos_in_parent(); + for (auto &v : item->views) { + v.drawingitem->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))) { + auto item = 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 (auto &v : views) { + auto group = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + if (parent) { + context_style = parent->context_style; + } + group->setStyle(style, 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 (auto &v : views) { + auto group = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + 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 (is<SPSwitch>(this)) { + repr = xml_doc->createElement("svg:switch"); + } else { + repr = xml_doc->createElement("svg:g"); + } + } + + for (auto& child: children) { + if (!is<SPTitle>(&child) && !is<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 (!is<SPTitle>(&child) && !is<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){ + auto item = 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; + auto item = 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 C_("Noun", "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){ + auto item = cast<SPItem>(o); + if (item) { + item->invoke_hide(key); + } + } + +// SPLPEItem::onHide(key); +} + +std::vector<SPItem*> SPGroup::item_list() +{ + std::vector<SPItem *> ret; + for (auto& child: children) { + if (auto item = cast<SPItem>(const_cast<SPObject *>(&child))) { + ret.push_back(item); + } + } + return ret; +} + +void SPGroup::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + for (auto& o: children) + { + SPItem const *item = cast<SPItem>(&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; + auto lpeitemclone = cast<SPLPEItem>(item); + + bool override = false; + if ((item_offset = cast<SPOffset>(item))) { + source = sp_offset_get_source(item_offset); + } else if ((item_text = cast<SPText>(item))) { + source = item_text->get_first_shape_dependency(); + } else if (auto textpath = cast<SPTextPath>(item)) { + item_text = cast<SPText>(textpath->parent); + if (!item_text) + return; + item = item_text; + source = sp_textpath_get_path_item(textpath); + } else if ((item_use = 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(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) { + auto shape = 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) { + auto shape = cast<SPShape>(item); + auto shape_clip = 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())) { + auto curve = shape->curve(); + auto 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) +{ + g_return_if_fail (group != nullptr); + + SPDocument *doc = group->document; + SPRoot *root = doc->getRoot(); + SPObject *defs = root->defs; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // TODO handle better ungrouping + // now is used in clipmask LPE on remove + prefs->setBool("/options/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()); + + auto pitem = cast<SPItem>(group->parent); + g_assert(pitem); + Inkscape::XML::Node *prepr = pitem->getRepr(); + + { + auto box = 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); + 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/topmost", true); + prefs->setInt("/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_NONE); + if (clip_obj) { + tmp_clip_set.unsetMask(true, false, true); + tmp_clip_set.remove(group); + tmp_clip_set.group(); + clip = tmp_clip_set.singleItem(); + } + if (mask_obj) { + tmp_mask_set.unsetMask(false, false, true); + 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 (auto citem = cast<SPItem>(&child)) { + auto lpeitem = 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 (cast<SPItem>(&child2) == linked) { + _ungroup_compensate_source_transform(citem, cast<SPItem>(linked), g); + breakparent = true; + break; + } + } + if (breakparent) { + break; + } + } + } + } + } + sp_item_group_ungroup_handle_clones(citem, g); + } + } + } + + for (auto& child: group->children) { + auto citem = 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)); + auto lpeitem = 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, false); + mask->deleteObject(true, false); + } + 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, false); + } + clip->deleteObject(true, false); + } + prefs->setBool("/options/maskobject/topmost", topmost); + prefs->setInt("/options/maskobject/grouping", grouping); + prefs->setBool("/options/onungroup", false); +} + +/* + * some API for list aspect of SPGroup + */ + +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) { + for (auto &v : views) { + if (!display_key || v.key == display_key) { + if (auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get())) { + g->setPickChildren(effectiveLayerMode(v.key) == SPGroup::LAYER); + } + } + } +} + +void SPGroup::translateChildItems(Geom::Translate const &tr) +{ + if (hasChildren()) { + for (auto &o: children) { + if (auto item = cast<SPItem>(&o)) { + 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 ( auto defs = cast<SPDefs>(&o) ) { // select symbols from defs, ignore clips, masks, patterns + for (auto& defschild: defs->children) { + auto defsgroup = cast<SPGroup>(&defschild); + if (defsgroup) + defsgroup->scaleChildItemsRec(sc, p, false); + } + } else if (auto item = cast<SPItem>(&o)) { + auto group = cast<SPGroup>(item); + if (group && !is<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 = 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 = 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; + auto text_item = cast<SPText>(item); + bool is_text_path = text_item && text_item->firstChild() && cast<SPTextPath>(text_item->firstChild()); + if (is_text_path) { + text_item->optimizeTextpathText(); + } else { + auto flowText = cast<SPFlowtext>(item); + if (flowText) { + flowText->optimizeScaledText(); + } else { + auto box = 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"); + } + } + } + + 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 (is<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 (is<SPItem>(&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){ + auto child = cast<SPItem>(o); + if (child) { + ac = child->invoke_show (drawing, key, flags); + if (ac) { + ai->appendChild(ac); + } + } + } +} + +std::vector<SPItem*> SPGroup::get_expanded(std::vector<SPItem*> const &items) +{ + std::vector<SPItem*> result; + + for (auto item : items) { + if (auto group = cast<SPGroup>(item)) { + auto sub = get_expanded(group->item_list()); + result.insert(result.end(), sub.begin(), sub.end()); + } else { + result.emplace_back(item); + } + } + + return result; +} + +void SPGroup::update_patheffect(bool write) { +#ifdef GROUP_VERBOSE + g_message("sp_group_update_patheffect: %p\n", lpeitem); +#endif + 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 + auto sub_shape = 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; + } + auto lpe_item = cast<SPLPEItem>(sub_item); + if (lpe_item) { + lpe_item->update_patheffect(write); + } + } + } + + this->resetClipPathAndMaskLPE(); + // avoid update lpe in each selection + // must be set also to non effect items (satellites or parents) + lpe_initialized = true; + 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 = group->item_list(); + for (auto sub_item : item_list) { + auto sub_group = cast<SPGroup>(sub_item); + if (sub_group) { + sp_group_perform_patheffect(sub_group, top_group, lpe, write); + } else { + auto sub_shape = cast<SPShape>(sub_item); + //auto sub_path = cast<SPPath>(sub_item); + auto clipmaskto = sub_item; + if (clipmaskto) { + top_group->applyToClipPath(clipmaskto, lpe); + top_group->applyToMask(clipmaskto, lpe); + } + if (sub_shape) { + bool success = false; + // only run LPEs when the shape has a curve defined + if (sub_shape->curve()) { + auto c = *sub_shape->curve(); + lpe->pathvector_before_effect = c.get_pathvector(); + c.transform(i2anc_affine(sub_shape, top_group)); + sub_shape->setCurveInsync(&c); + success = top_group->performOnePathEffect(&c, sub_shape, lpe); + c.transform(i2anc_affine(sub_shape, top_group).inverse()); + Inkscape::XML::Node *repr = sub_item->getRepr(); + if (success) { + sub_shape->setCurveInsync(&c); + 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")) { + sub_shape->setCurve(SPCurve(sp_svg_read_pathv(value))); + } + } + } + } + } + } + auto clipmaskto = 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 : diff --git a/src/object/sp-item-group.h b/src/object/sp-item-group.h new file mode 100644 index 0000000..f7eacfa --- /dev/null +++ b/src/object/sp-item-group.h @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_ITEM_GROUP_H +#define SEEN_SP_ITEM_GROUP_H + +/* + * SVG <g> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <map> +#include "sp-lpe-item.h" + +namespace Inkscape { + +class Drawing; +class DrawingItem; + +} // namespace Inkscape + +class SPGroup : public SPLPEItem { +public: + SPGroup(); + ~SPGroup() override; + int tag() const override { return tag_of<decltype(*this)>; } + + enum LayerMode { GROUP, LAYER, MASK_HELPER }; + + bool isLayer() const { return _layer_mode == LAYER; } + + bool _insert_bottom; + LayerMode _layer_mode; + std::map<unsigned int, LayerMode> _display_modes; + + LayerMode layerMode() const { return _layer_mode; } + void setLayerMode(LayerMode mode); + + bool insertBottom() const { return _insert_bottom; } + void setInsertBottom(bool insertbottom); + + LayerMode effectiveLayerMode(unsigned int display_key) const { + if ( _layer_mode == LAYER ) { + return LAYER; + } else { + return layerDisplayMode(display_key); + } + } + + LayerMode layerDisplayMode(unsigned int display_key) const; + void setLayerDisplayMode(unsigned int display_key, LayerMode mode); + void translateChildItems(Geom::Translate const &tr); + void scaleChildItemsRec(Geom::Scale const &sc, Geom::Point const &p, bool noRecurse); + + int getItemCount() const; + virtual void _showChildren (Inkscape::Drawing &drawing, Inkscape::DrawingItem *ai, unsigned int key, unsigned int flags); + + std::vector<SPItem*> item_list(); + +private: + void _updateLayerMode(unsigned int display_key=0); + +public: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) override; + + void update(SPCtx *ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + void set(SPAttr key, char const* value) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype) const override; + void print(SPPrintContext *ctx) override; + const char* typeName() const override; + const char* displayName() const override; + char *description() const override; + Inkscape::DrawingItem *show (Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void hide (unsigned int key) override; + + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + + void update_patheffect(bool write) override; + + guint32 highlight_color() const override; + + /** + * Return the result of recursively ungrouping all groups in \a items. + */ + static std::vector<SPItem*> get_expanded(std::vector<SPItem*> const &items); +}; + +/** + * finds clones of a child of the group going out of the group; and inverse the group transform on its clones + * Also called when moving objects between different layers + * @param group current group + * @param parent original parent + * @param clone_original lpe clone handle ungroup + * @param g transform + */ +void sp_item_group_ungroup_handle_clones(SPItem *parent, Geom::Affine const g); + +void sp_item_group_ungroup (SPGroup *group, std::vector<SPItem*> &children); + +SPObject *sp_item_group_get_child_by_name (SPGroup *group, SPObject *ref, const char *name); + + +inline bool SP_IS_LAYER(SPObject const *obj) +{ + auto group = cast<SPGroup>(obj); + return group && group->layerMode() == SPGroup::LAYER; +} + +void set_default_highlight_colors(std::vector<guint32> colors); + +#endif + +/* + 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 : diff --git a/src/object/sp-item-transform.cpp b/src/object/sp-item-transform.cpp new file mode 100644 index 0000000..a4cfc93 --- /dev/null +++ b/src/object/sp-item-transform.cpp @@ -0,0 +1,377 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Transforming single items + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@gmail.com> + * Johan Engelen <goejendaagh@zonnet.nl> + * Abhishek Sharma + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 1999-2011 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/transforms.h> +#include "sp-item.h" +#include "sp-item-transform.h" + +#include <glib.h> + +/** + * Calculate the affine transformation required to transform one visual bounding box into another, accounting for a uniform strokewidth. + * + * PS: This function will only return accurate results for the visual bounding box of a selection of one or more objects, all having + * the same strokewidth. If the stroke width varies from object to object in this selection, then the function + * get_scale_transform_for_variable_stroke() should be called instead + * + * When scaling or stretching an object using the selector, e.g. by dragging the handles or by entering a value, we will + * need to calculate the affine transformation for the old dimensions to the new dimensions. When using a geometric bounding + * box this is very straightforward, but when using a visual bounding box this become more tricky as we need to account for + * the strokewidth, which is either constant or scales width the area of the object. This function takes care of the calculation + * of the affine transformation: + * @param bbox_visual Current visual bounding box + * @param stroke_x Apparent strokewidth in horizontal direction + * @param stroke_y Apparent strokewidth in vertical direction + * @param transform_stroke If true then the stroke will be scaled proportional to the square root of the area of the geometric bounding box + * @param preserve If true then the transform element will be preserved in XML, and evaluated after stroke is applied + * @param x0 Coordinate of the target visual bounding box + * @param y0 Coordinate of the target visual bounding box + * @param x1 Coordinate of the target visual bounding box + * @param y1 Coordinate of the target visual bounding box + * PS: we have to pass each coordinate individually, to find out if we are mirroring the object; Using a Geom::Rect() instead is + * not possible here because it will only allow for a positive width and height, and therefore cannot mirror + * @return + */ +Geom::Affine get_scale_transform_for_uniform_stroke(Geom::Rect const &bbox_visual, gdouble stroke_x, gdouble stroke_y, bool transform_stroke, bool preserve, gdouble x0, gdouble y0, gdouble x1, gdouble y1) +{ + Geom::Affine p2o = Geom::Translate (-bbox_visual.min()); + Geom::Affine o2n = Geom::Translate (x0, y0); + + Geom::Affine scale = Geom::Scale (1, 1); + Geom::Affine unbudge = Geom::Translate (0, 0); // moves the object(s) to compensate for the drift caused by stroke width change + + // 1) We start with a visual bounding box (w0, h0) which we want to transfer into another visual bounding box (w1, h1) + // 2) The stroke is r0, equal for all edges, if preserve transforms is false + // 3) Given this visual bounding box we can calculate the geometric bounding box by subtracting half the stroke from each side; + // -> The width and height of the geometric bounding box will therefore be (w0 - 2*0.5*r0) and (h0 - 2*0.5*r0) + // 4) If preserve transforms is true, then stroke_x != stroke_y, since these are the apparent stroke widths, after transforming + + if ((stroke_x == Geom::infinity()) || (fabs(stroke_x) < 1e-6)) stroke_x = 0; + if ((stroke_y == Geom::infinity()) || (fabs(stroke_y) < 1e-6)) stroke_y = 0; + + gdouble w0 = bbox_visual.width(); // will return a value >= 0, as required further down the road + gdouble h0 = bbox_visual.height(); + + // We also know the width and height of the new visual bounding box + gdouble w1 = x1 - x0; // can have any sign + gdouble h1 = y1 - y0; + // The new visual bounding box will have a stroke r1 + + // Here starts the calculation you've been waiting for; first do some preparation + int flip_x = (w1 > 0) ? 1 : -1; + int flip_y = (h1 > 0) ? 1 : -1; + + // w1 and h1 will be negative when mirroring, but if so then e.g. w1-r0 won't make sense + // Therefore we will use the absolute values from this point on + w1 = fabs(w1); + h1 = fabs(h1); + // w0 and h0 will always be positive due to the definition of the width() and height() methods. + + // Check whether the stroke is negative; i.e. the geometric bounding box is larger than the visual bounding box, which + // occurs for example for clipped objects (see launchpad bug #811819) + if (stroke_x < 0 || stroke_y < 0) { + Geom::Affine direct = Geom::Scale(flip_x * w1 / w0, flip_y* h1 / h0); // Scaling of the visual bounding box + // How should we handle the stroke width scaling of clipped object? I don't know if we can/should handle this, + // so for now we simply return the direct scaling + return (p2o * direct * o2n); + } + gdouble r0 = sqrt(stroke_x*stroke_y); // r0 is redundant, used only for those cases where stroke_x = stroke_y + + // We will now try to calculate the affine transformation required to transform the first visual bounding box into + // the second one, while accounting for strokewidth + + if ((fabs(w0 - stroke_x) < 1e-6) && (fabs(h0 - stroke_y) < 1e-6)) { + return Geom::Affine(); + } + + gdouble scale_x = 1; + gdouble scale_y = 1; + gdouble r1; + + if ((fabs(w0 - stroke_x) < 1e-6) || w1 == 0) { // We have a vertical line at hand + scale_y = h1/h0; + scale_x = transform_stroke ? 1 : scale_y; + unbudge *= Geom::Translate (-flip_x * 0.5 * (scale_x - 1.0) * w0, 0); + unbudge *= Geom::Translate ( flip_x * 0.5 * (w1 - w0), 0); // compensate for the fact that this operation cannot be performed + } else if ((fabs(h0 - stroke_y) < 1e-6) || h1 == 0) { // We have a horizontal line at hand + scale_x = w1/w0; + scale_y = transform_stroke ? 1 : scale_x; + unbudge *= Geom::Translate (0, -flip_y * 0.5 * (scale_y - 1.0) * h0); + unbudge *= Geom::Translate (0, flip_y * 0.5 * (h1 - h0)); // compensate for the fact that this operation cannot be performed + } else { // We have a true 2D object at hand + if (transform_stroke && !preserve) { + /* Initial area of the geometric bounding box: A0 = (w0-r0)*(h0-r0) + * Desired area of the geometric bounding box: A1 = (w1-r1)*(h1-r1) + * This is how the stroke should scale: r1^2 / A1 = r0^2 / A0 + * So therefore we will need to solve this equation: + * + * r1^2 * (w0-r0) * (h0-r0) = r0^2 * (w1-r1) * (h1-r1) + * + * This is a quadratic equation in r1, of which the roots can be found using the ABC formula + * */ + gdouble A = -w0*h0 + r0*(w0 + h0); + gdouble B = -(w1 + h1) * r0*r0; + gdouble C = w1 * h1 * r0*r0; + if (B*B - 4*A*C < 0) { + g_message("stroke scaling error : %d, %f, %f, %f, %f, %f", preserve, r0, w0, h0, w1, h1); + } else { + r1 = -C/B; + if (!Geom::are_near(A*C/B/B, 0.0, Geom::EPSILON)) + r1 = fabs((-B - sqrt(B*B - 4*A*C))/(2*A)); + // If w1 < 0 then the scale will be wrong if we just assume that scale_x = (w1 - r1)/(w0 - r0); + // Therefore we here need the absolute values of w0, w1, h0, h1, and r0, as taken care of earlier + scale_x = (w1 - r1)/(w0 - r0); + scale_y = (h1 - r1)/(h0 - r0); + // Make sure that the lower-left corner of the visual bounding box stays where it is, even though the stroke width has changed + unbudge *= Geom::Translate (-flip_x * 0.5 * (r0 * scale_x - r1), -flip_y * 0.5 * (r0 * scale_y - r1)); + } + } else if (!transform_stroke && !preserve) { // scale the geometric bbox with constant stroke + scale_x = (w1 - r0) / (w0 - r0); + scale_y = (h1 - r0) / (h0 - r0); + unbudge *= Geom::Translate (-flip_x * 0.5 * r0 * (scale_x - 1), -flip_y * 0.5 * r0 * (scale_y - 1)); + } else if (!transform_stroke) { // 'Preserve Transforms' was chosen. + // geometric mean of stroke_x and stroke_y will be preserved + // new_stroke_x = stroke_x*sqrt(scale_x/scale_y) + // new_stroke_y = stroke_y*sqrt(scale_y/scale_x) + // scale_x = (w1 - new_stroke_x)/(w0 - stroke_x) + // scale_y = (h1 - new_stroke_y)/(h0 - stroke_y) + gdouble A = h1*(w0 - stroke_x); + gdouble B = (h0*stroke_x - w0*stroke_y); + gdouble C = -w1*(h0 - stroke_y); + gdouble Sx_div_Sy; // Sx_div_Sy = sqrt(scale_x/scale_y) + if (B*B - 4*A*C < 0) { + g_message("stroke scaling error : %d, %f, %f, %f, %f, %f, %f", preserve, stroke_x, stroke_y, w0, h0, w1, h1); + } else { + Sx_div_Sy = (-B + sqrt(B*B - 4*A*C))/2/A; + scale_x = (w1 - stroke_x*Sx_div_Sy)/(w0 - stroke_x); + scale_y = (h1 - stroke_y/Sx_div_Sy)/(h0 - stroke_y); + unbudge *= Geom::Translate (-flip_x * 0.5 * stroke_x * scale_x * (1.0 - sqrt(1.0/scale_x/scale_y)), -flip_y * 0.5 * stroke_y * scale_y * (1.0 - sqrt(1.0/scale_x/scale_y))); + } + } else { // 'Preserve Transforms' was chosen, and stroke is scaled + scale_x = w1 / w0; + scale_y = h1 / h0; + } + } + + // Now we account for mirroring by flipping if needed + scale *= Geom::Scale(flip_x * scale_x, flip_y * scale_y); + + return (p2o * scale * unbudge * o2n); +} + +/** + * Calculate the affine transformation required to transform one visual bounding box into another, accounting for a VARIABLE strokewidth. + * + * Note: Please try to understand get_scale_transform_for_uniform_stroke() first, and read all it's comments carefully. This function + * (get_scale_transform_for_variable_stroke) is a bit different because it will allow for a strokewidth that's different for each + * side of the visual bounding box. Such a situation will arise when transforming the visual bounding box of a selection of objects, + * each having a different stroke width. In fact this function is a generalized version of get_scale_transform_for_uniform_stroke(), but + * will not (yet) replace it because it has not been tested as carefully, and because the old function is can serve as an introduction to + * understand the new one. + * + * When scaling or stretching an object using the selector, e.g. by dragging the handles or by entering a value, we will + * need to calculate the affine transformation for the old dimensions to the new dimensions. When using a geometric bounding + * box this is very straightforward, but when using a visual bounding box this become more tricky as we need to account for + * the strokewidth, which is either constant or scales width the area of the object. This function takes care of the calculation + * of the affine transformation: + * + * @param bbox_visual Current visual bounding box + * @param bbox_geometric Current geometric bounding box (allows for calculating the strokewidth of each edge) + * @param transform_stroke If true then the stroke will be scaled proportional to the square root of the area of the geometric bounding box + * @param preserve If true then the transform element will be preserved in XML, and evaluated after stroke is applied + * @param x0 Coordinate of the target visual bounding box + * @param y0 Coordinate of the target visual bounding box + * @param x1 Coordinate of the target visual bounding box + * @param y1 Coordinate of the target visual bounding box + * PS: we have to pass each coordinate individually, to find out if we are mirroring the object; Using a Geom::Rect() instead is + * not possible here because it will only allow for a positive width and height, and therefore cannot mirror + * @return + */ +Geom::Affine get_scale_transform_for_variable_stroke(Geom::Rect const &bbox_visual, Geom::Rect const &bbox_geom, bool transform_stroke, bool preserve, gdouble x0, gdouble y0, gdouble x1, gdouble y1) +{ + Geom::Affine p2o = Geom::Translate (-bbox_visual.min()); + Geom::Affine o2n = Geom::Translate (x0, y0); + + Geom::Affine scale = Geom::Scale (1, 1); + Geom::Affine unbudge = Geom::Translate (0, 0); // moves the object(s) to compensate for the drift caused by stroke width change + + // 1) We start with a visual bounding box (w0, h0) which we want to transfer into another visual bounding box (w1, h1) + // 2) We will also know the geometric bounding box, which can be used to calculate the strokewidth. The strokewidth will however + // be different for each of the four sides (left/right/top/bottom: r0l, r0r, r0t, r0b) + + gdouble w0 = bbox_visual.width(); // will return a value >= 0, as required further down the road + gdouble h0 = bbox_visual.height(); + + // We also know the width and height of the new visual bounding box + gdouble w1 = x1 - x0; // can have any sign + gdouble h1 = y1 - y0; + // The new visual bounding box will have strokes r1l, r1r, r1t, and r1b + + // We will now try to calculate the affine transformation required to transform the first visual bounding box into + // the second one, while accounting for strokewidth + gdouble r0w = w0 - bbox_geom.width(); // r0w is the average strokewidth of the left and right edges, i.e. 0.5*(r0l + r0r) + gdouble r0h = h0 - bbox_geom.height(); // r0h is the average strokewidth of the top and bottom edges, i.e. 0.5*(r0t + r0b) + if ((r0w == Geom::infinity()) || (fabs(r0w) < 1e-6)) r0w = 0; + if ((r0h == Geom::infinity()) || (fabs(r0h) < 1e-6)) r0h = 0; + + int flip_x = (w1 > 0) ? 1 : -1; + int flip_y = (h1 > 0) ? 1 : -1; + + // w1 and h1 will be negative when mirroring, but if so then e.g. w1-r0 won't make sense + // Therefore we will use the absolute values from this point on + w1 = fabs(w1); + h1 = fabs(h1); + // w0 and h0 will always be positive due to the definition of the width() and height() methods. + + if ((fabs(w0 - r0w) < 1e-6) && (fabs(h0 - r0h) < 1e-6)) { + return Geom::Affine(); + } + + // Check whether the stroke is negative; i.e. the geometric bounding box is larger than the visual bounding box, which + // occurs for example for clipped objects (see launchpad bug #811819) + if (r0w < 0 || r0h < 0) { + Geom::Affine direct = Geom::Scale(flip_x * w1 / w0, flip_y* h1 / h0); // Scaling of the visual bounding box + // How should we handle the stroke width scaling of clipped object? I don't know if we can/should handle this, + // so for now we simply return the direct scaling + return (p2o * direct * o2n); + } + + // The calculation of the new strokewidth will only use the average stroke for each of the dimensions; To find the new stroke for each + // of the edges individually though, we will use the boundary condition that the ratio of the left/right strokewidth will not change due to the + // scaling. The same holds for the ratio of the top/bottom strokewidth. + gdouble stroke_ratio_w = fabs(r0w) < 1e-6 ? 1 : (bbox_geom[Geom::X].min() - bbox_visual[Geom::X].min())/r0w; + gdouble stroke_ratio_h = fabs(r0h) < 1e-6 ? 1 : (bbox_geom[Geom::Y].min() - bbox_visual[Geom::Y].min())/r0h; + + gdouble scale_x = 1; + gdouble scale_y = 1; + gdouble r1h; + gdouble r1w; + + if ((fabs(w0 - r0w) < 1e-6) || w1 == 0) { // We have a vertical line at hand + scale_y = h1/h0; + scale_x = transform_stroke ? 1 : scale_y; + unbudge *= Geom::Translate (-flip_x * 0.5 * (scale_x - 1.0) * w0, 0); + unbudge *= Geom::Translate ( flip_x * 0.5 * (w1 - w0), 0); // compensate for the fact that this operation cannot be performed + } else if ((fabs(h0 - r0h) < 1e-6) || h1 == 0) { // We have a horizontal line at hand + scale_x = w1/w0; + scale_y = transform_stroke ? 1 : scale_x; + unbudge *= Geom::Translate (0, -flip_y * 0.5 * (scale_y - 1.0) * h0); + unbudge *= Geom::Translate (0, flip_y * 0.5 * (h1 - h0)); // compensate for the fact that this operation cannot be performed + } else { // We have a true 2D object at hand + if (transform_stroke && !preserve) { + /* Initial area of the geometric bounding box: A0 = (w0-r0w)*(h0-r0h) + * Desired area of the geometric bounding box: A1 = (w1-r1w)*(h1-r1h) + * This is how the stroke should scale: r1w^2 = A1/A0 * r0w^2, AND + * r1h^2 = A1/A0 * r0h^2 + * These can be re-expressed as : r1w/r0w = r1h/r0h + * and : r1w*r1w*(w0 - r0w)*(h0 - r0h) = r0w*r0w*(w1 - r1w)*(h1 - r1h) + * This leads to a quadratic equation in r1w, solved as follows: + * */ + + gdouble A = w0*h0 - r0h*w0 - r0w*h0; + gdouble B = r0h*w1 + r0w*h1; + gdouble C = -w1*h1; + + if (B*B - 4*A*C < 0) { + g_message("variable stroke scaling error : %d, %d, %f, %f, %f, %f, %f, %f", transform_stroke, preserve, r0w, r0h, w0, h0, w1, h1); + } else { + gdouble det = -C/B; + if (!Geom::are_near(A*C/B/B, 0.0, Geom::EPSILON)) + det = (-B + sqrt(B*B - 4*A*C))/(2*A); + r1w = r0w*det; + r1h = r0h*det; + // If w1 < 0 then the scale will be wrong if we just assume that scale_x = (w1 - r1)/(w0 - r0); + // Therefore we here need the absolute values of w0, w1, h0, h1, and r0, as taken care of earlier + scale_x = (w1 - r1w)/(w0 - r0w); + scale_y = (h1 - r1h)/(h0 - r0h); + // Make sure that the lower-left corner of the visual bounding box stays where it is, even though the stroke width has changed + unbudge *= Geom::Translate (-flip_x * stroke_ratio_w * (r0w * scale_x - r1w), -flip_y * stroke_ratio_h * (r0h * scale_y - r1h)); + } + } else if (!transform_stroke && !preserve) { // scale the geometric bbox with constant stroke + scale_x = (w1 - r0w) / (w0 - r0w); + scale_y = (h1 - r0h) / (h0 - r0h); + unbudge *= Geom::Translate (-flip_x * stroke_ratio_w * r0w * (scale_x - 1), -flip_y * stroke_ratio_h * r0h * (scale_y - 1)); + } else if (!transform_stroke) { // 'Preserve Transforms' was chosen. + // geometric mean of r0w and r0h will be preserved + // new_r0w = r0w*sqrt(scale_x/scale_y) + // new_r0h = r0h*sqrt(scale_y/scale_x) + // scale_x = (w1 - new_r0w)/(w0 - r0w) + // scale_y = (h1 - new_r0h)/(h0 - r0h) + gdouble A = h1*(w0 - r0w); + gdouble B = (h0*r0w - w0*r0h); + gdouble C = -w1*(h0 - r0h); + gdouble Sx_div_Sy; // Sx_div_Sy = sqrt(scale_x/scale_y) + if (B*B - 4*A*C < 0) { + g_message("variable stroke scaling error : %d, %d, %f, %f, %f, %f, %f, %f", transform_stroke, preserve, r0w, r0h, w0, h0, w1, h1); + } else { + Sx_div_Sy = (-B + sqrt(B*B - 4*A*C))/2/A; + scale_x = (w1 - r0w*Sx_div_Sy)/(w0 - r0w); + scale_y = (h1 - r0h/Sx_div_Sy)/(h0 - r0h); + unbudge *= Geom::Translate (-flip_x * stroke_ratio_w * r0w * scale_x * (1.0 - sqrt(1.0/scale_x/scale_y)), -flip_y * stroke_ratio_h * r0h * scale_y * (1.0 - sqrt(1.0/scale_x/scale_y))); + } + } else { // 'Preserve Transforms' was chosen, and stroke is scaled + scale_x = w1 / w0; + scale_y = h1 / h0; + } + } + + // Now we account for mirroring by flipping if needed + scale *= Geom::Scale(flip_x * scale_x, flip_y * scale_y); + + return (p2o * scale * unbudge * o2n); +} + +Geom::Rect get_visual_bbox(Geom::OptRect const &initial_geom_bbox, Geom::Affine const &abs_affine, gdouble const initial_strokewidth, bool const transform_stroke) +{ + g_assert(initial_geom_bbox); + + // Find the new geometric bounding box; Do this by transforming each corner of + // the initial geometric bounding box individually and fitting a new boundingbox + // around the transformed corners + Geom::Point const p0 = Geom::Point(initial_geom_bbox->corner(0)) * abs_affine; + Geom::Rect new_geom_bbox(p0, p0); + for (unsigned i = 1 ; i < 4 ; i++) { + new_geom_bbox.expandTo(Geom::Point(initial_geom_bbox->corner(i)) * abs_affine); + } + + Geom::Rect new_visual_bbox = new_geom_bbox; + if (initial_strokewidth > 0 && initial_strokewidth < Geom::infinity()) { + if (transform_stroke) { + // scale stroke by: sqrt (((w1-r0)/(w0-r0))*((h1-r0)/(h0-r0))) (for visual bboxes, see get_scale_transform_for_stroke) + // equals scaling by: sqrt ((w1/w0)*(h1/h0)) for geometrical bboxes + // equals scaling by: sqrt (area1/area0) for geometrical bboxes + gdouble const new_strokewidth = initial_strokewidth * sqrt (new_geom_bbox.area() / initial_geom_bbox->area()); + new_visual_bbox.expandBy(0.5 * new_strokewidth); + } else { + // Do not transform the stroke + new_visual_bbox.expandBy(0.5 * initial_strokewidth); + } + } + + return new_visual_bbox; +} + +/* + 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 : diff --git a/src/object/sp-item-transform.h b/src/object/sp-item-transform.h new file mode 100644 index 0000000..f30e675 --- /dev/null +++ b/src/object/sp-item-transform.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_TRANSFORM_H +#define SEEN_SP_ITEM_TRANSFORM_H + +#include <2geom/forward.h> + +class SPItem; + +Geom::Affine get_scale_transform_for_uniform_stroke (Geom::Rect const &bbox_visual, double stroke_x, double stroke_y, bool transform_stroke, bool preserve, double x0, double y0, double x1, double y1); +Geom::Affine get_scale_transform_for_variable_stroke (Geom::Rect const &bbox_visual, Geom::Rect const &bbox_geom, bool transform_stroke, bool preserve, double x0, double y0, double x1, double y1); +Geom::Rect get_visual_bbox (Geom::OptRect const &initial_geom_bbox, Geom::Affine const &abs_affine, double const initial_strokewidth, bool const transform_stroke); + +#endif // SEEN_SP_ITEM_TRANSFORM_H + +/* + 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 : diff --git a/src/object/sp-item.cpp b/src/object/sp-item.cpp new file mode 100644 index 0000000..9d28286 --- /dev/null +++ b/src/object/sp-item.cpp @@ -0,0 +1,1880 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001-2006 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-item.h" + +#include <glibmm/i18n.h> + +#include "bad-uri-exception.h" +#include "helper/geom.h" +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "print.h" +#include "display/curve.h" +#include "display/drawing-item.h" +#include "display/drawing-pattern.h" +#include "attributes.h" +#include "document.h" + +#include "inkscape.h" +#include "desktop.h" +#include "gradient-chemistry.h" +#include "conn-avoid-ref.h" +#include "conditions.h" +#include "filter-chemistry.h" + +#include "sp-clippath.h" +#include "sp-defs.h" +#include "sp-desc.h" +#include "sp-guide.h" +#include "sp-hatch.h" +#include "sp-mask.h" +#include "sp-pattern.h" +#include "sp-rect.h" +#include "sp-root.h" +#include "sp-switch.h" +#include "sp-text.h" +#include "sp-textpath.h" +#include "sp-title.h" +#include "sp-use.h" + +#include "style.h" +#include "display/nr-filter.h" +#include "snap-preferences.h" +#include "snap-candidate.h" + +#include "extract-uri.h" +#include "live_effects/lpeobject.h" +#include "live_effects/effect.h" +#include "live_effects/lpeobject-reference.h" + +#include "util/units.h" + +#define noSP_ITEM_DEBUG_IDLE + +//#define OBJECT_TRACE + +SPItemView::SPItemView(unsigned flags, unsigned key, DrawingItemPtr<Inkscape::DrawingItem> drawingitem) + : flags(flags) + , key(key) + , drawingitem(std::move(drawingitem)) {} + +SPItem::SPItem() +{ + sensitive = TRUE; + bbox_valid = FALSE; + + _highlightColor = 0; + transform_center_x = 0; + transform_center_y = 0; + + freeze_stroke_width = false; + _is_evaluated = true; + _evaluated_status = StatusUnknown; + + transform = Geom::identity(); + // doc_bbox = Geom::OptRect(); + + clip_ref = nullptr; + mask_ref = nullptr; + + style->signal_fill_ps_changed.connect ([this] (auto old_obj, auto obj) { fill_ps_ref_changed (old_obj, obj); }); + style->signal_stroke_ps_changed.connect([this] (auto old_obj, auto obj) { stroke_ps_ref_changed(old_obj, obj); }); + style->signal_filter_changed.connect ([this] (auto old_obj, auto obj) { filter_ref_changed (old_obj, obj); }); + + avoidRef = nullptr; +} + +SPItem::~SPItem() = default; + +SPClipPath *SPItem::getClipObject() const +{ + return clip_ref ? clip_ref->getObject() : nullptr; +} + +SPMask *SPItem::getMaskObject() const +{ + return mask_ref ? mask_ref->getObject() : nullptr; +} + +SPMaskReference &SPItem::getMaskRef() +{ + if (!mask_ref) { + mask_ref = new SPMaskReference(this); + mask_ref->changedSignal().connect([this] (auto old_obj, auto obj) { mask_ref_changed(old_obj, obj); }); + } + + return *mask_ref; +} + +SPClipPathReference &SPItem::getClipRef() +{ + if (!clip_ref) { + clip_ref = new SPClipPathReference(this); + clip_ref->changedSignal().connect([this] (auto old_obj, auto obj) { clip_ref_changed(old_obj, obj); }); + } + + return *clip_ref; +} + +SPAvoidRef &SPItem::getAvoidRef() +{ + if (!avoidRef) { + avoidRef = new SPAvoidRef(this); + } + return *avoidRef; +} + +bool SPItem::isVisibleAndUnlocked() const { + return !isHidden() && !isLocked(); +} + +bool SPItem::isVisibleAndUnlocked(unsigned display_key) const { + return !isHidden(display_key) && !isLocked(); +} + +bool SPItem::isLocked() const { + for (SPObject const *o = this; o != nullptr; o = o->parent) { + SPItem const *item = cast<SPItem>(o); + if (item && !(item->sensitive)) { + return true; + } + } + return false; +} + +void SPItem::setLocked(bool locked) { + setAttribute("sodipodi:insensitive", + ( locked ? "1" : nullptr )); + updateRepr(); + document->_emitModified(); +} + +bool SPItem::isHidden() const { + if (!isEvaluated()) + return true; + return style->display.computed == SP_CSS_DISPLAY_NONE; +} + +void SPItem::setHidden(bool hide) { + style->display.set = TRUE; + style->display.value = ( hide ? SP_CSS_DISPLAY_NONE : SP_CSS_DISPLAY_INLINE ); + style->display.computed = style->display.value; + style->display.inherit = FALSE; + updateRepr(); +} + +bool SPItem::isHidden(unsigned display_key) const +{ + if (!isEvaluated()) { + return true; + } + for (auto &v : views) { + if (v.key == display_key) { + g_assert(v.drawingitem); + for (auto di = v.drawingitem.get(); di; di = di->parent()) { + if (!di->visible()) { + return true; + } + } + return false; + } + } + return true; +} + +void SPItem::setHighlight(guint32 color) { + _highlightColor = color; + updateRepr(); +} + +bool SPItem::isHighlightSet() const { + return _highlightColor != 0; +} + +guint32 SPItem::highlight_color() const { + if (isHighlightSet()) { + return _highlightColor; + } + + SPItem const *item = cast<SPItem>(parent); + if (parent && (parent != this) && item) { + return item->highlight_color(); + } else { + static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + return prefs->getInt("/tools/nodes/highlight_color", 0xaaaaaaff); + } +} + +void SPItem::setEvaluated(bool evaluated) { + _is_evaluated = evaluated; + _evaluated_status = StatusSet; +} + +void SPItem::resetEvaluated() { + if ( StatusCalculated == _evaluated_status ) { + _evaluated_status = StatusUnknown; + bool oldValue = _is_evaluated; + if ( oldValue != isEvaluated() ) { + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } if ( StatusSet == _evaluated_status ) { + auto switchItem = cast<SPSwitch>(parent); + if (switchItem) { + switchItem->resetChildEvaluated(); + } + } +} + +bool SPItem::isEvaluated() const { + if ( StatusUnknown == _evaluated_status ) { + _is_evaluated = sp_item_evaluate(this); + _evaluated_status = StatusCalculated; + } + return _is_evaluated; +} + +bool SPItem::isExplicitlyHidden() const +{ + return (style->display.set + && style->display.value == SP_CSS_DISPLAY_NONE); +} + +void SPItem::setExplicitlyHidden(bool val) { + style->display.set = val; + style->display.value = ( val ? SP_CSS_DISPLAY_NONE : SP_CSS_DISPLAY_INLINE ); + style->display.computed = style->display.value; + updateRepr(); +} + +void SPItem::setCenter(Geom::Point const &object_centre) { + document->ensureUpToDate(); + + // Copied from DocumentProperties::onDocUnitChange() + gdouble viewscale = 1.0; + Geom::Rect vb = this->document->getRoot()->viewBox; + if ( !vb.hasZeroArea() ) { + gdouble viewscale_w = this->document->getWidth().value("px") / vb.width(); + gdouble viewscale_h = this->document->getHeight().value("px")/ vb.height(); + viewscale = std::min(viewscale_h, viewscale_w); + } + + // FIXME this is seriously wrong + Geom::OptRect bbox = desktopGeometricBounds(); + if (bbox) { + // object centre is document coordinates (i.e. in pixels), so we need to consider the viewbox + // to translate to user units; transform_center_x/y is in user units + transform_center_x = (object_centre[Geom::X] - bbox->midpoint()[Geom::X])/viewscale; + if (Geom::are_near(transform_center_x, 0)) // rounding error + transform_center_x = 0; + transform_center_y = (object_centre[Geom::Y] - bbox->midpoint()[Geom::Y])/viewscale; + if (Geom::are_near(transform_center_y, 0)) // rounding error + transform_center_y = 0; + } +} + +void +SPItem::unsetCenter() { + transform_center_x = 0; + transform_center_y = 0; +} + +bool SPItem::isCenterSet() const { + return (transform_center_x != 0 || transform_center_y != 0); +} + +// Get the item's transformation center in desktop coordinates (i.e. in pixels) +Geom::Point SPItem::getCenter() const { + document->ensureUpToDate(); + + // Copied from DocumentProperties::onDocUnitChange() + gdouble viewscale = 1.0; + Geom::Rect vb = this->document->getRoot()->viewBox; + if ( !vb.hasZeroArea() ) { + gdouble viewscale_w = this->document->getWidth().value("px") / vb.width(); + gdouble viewscale_h = this->document->getHeight().value("px")/ vb.height(); + viewscale = std::min(viewscale_h, viewscale_w); + } + + // FIXME this is seriously wrong + Geom::OptRect bbox = desktopGeometricBounds(); + if (bbox) { + // transform_center_x/y are stored in user units, so we have to take the viewbox into account to translate to document coordinates + return bbox->midpoint() + Geom::Point (transform_center_x*viewscale, transform_center_y*viewscale); + + } else { + return Geom::Point(0, 0); // something's wrong! + } + +} + +void +SPItem::scaleCenter(Geom::Scale const &sc) { + transform_center_x *= sc[Geom::X]; + transform_center_y *= sc[Geom::Y]; +} + +namespace { + +bool is_item(SPObject const &object) { + return cast<SPItem>(&object) != nullptr; +} + +} + +void SPItem::raiseToTop() { + auto& list = parent->children; + auto end = SPObject::ChildrenList::reverse_iterator(list.iterator_to(*this)); + auto topmost = std::find_if(list.rbegin(), end, &is_item); + // auto topmost = find_last_if(++parent->children.iterator_to(*this), parent->children.end(), &is_item); + if (topmost != list.rend()) { + getRepr()->parent()->changeOrder(getRepr(), topmost->getRepr()); + } +} + +bool SPItem::raiseOne() { + auto next_higher = std::find_if(++parent->children.iterator_to(*this), parent->children.end(), &is_item); + if (next_higher != parent->children.end()) { + Inkscape::XML::Node *ref = next_higher->getRepr(); + getRepr()->parent()->changeOrder(getRepr(), ref); + return true; + } + return false; +} + +bool SPItem::lowerOne() { + auto& list = parent->children; + auto self = list.iterator_to(*this); + auto start = SPObject::ChildrenList::reverse_iterator(self); + auto next_lower = std::find_if(start, list.rend(), &is_item); + if (next_lower != list.rend()) { + auto next = list.iterator_to(*next_lower); + if (next == list.begin()) { + getRepr()->parent()->changeOrder(getRepr(), nullptr); + } else { + --next; + auto ref = next->getRepr(); + getRepr()->parent()->changeOrder(getRepr(), ref); + } + return true; + } + return false; +} + +void SPItem::lowerToBottom() { + auto bottom = std::find_if(parent->children.begin(), parent->children.iterator_to(*this), &is_item); + if (bottom != parent->children.iterator_to(*this)) { + Inkscape::XML::Node *ref = nullptr; + if (bottom != parent->children.begin()) { + bottom--; + ref = bottom->getRepr(); + } + parent->getRepr()->changeOrder(getRepr(), ref); + } +} + +/** + * Return the parent, only if it's a group object. + */ +SPGroup *SPItem::getParentGroup() const +{ + return cast<SPGroup>(parent); +} + +void SPItem::moveTo(SPItem *target, bool intoafter) { + + Inkscape::XML::Node *target_ref = ( target ? target->getRepr() : nullptr ); + Inkscape::XML::Node *our_ref = getRepr(); + + if (!target_ref) { + // Assume move to the "first" in the top node, find the top node + intoafter = false; + SPObject* bottom = this->document->getObjectByRepr(our_ref->root())->firstChild(); + while (!is<SPItem>(bottom->getNext())) { + bottom = bottom->getNext(); + } + target_ref = bottom->getRepr(); + } + + if (target_ref == our_ref) { + // Move to ourself ignore + return; + } + + if (intoafter) { + // Move this inside of the target at the end + our_ref->parent()->removeChild(our_ref); + target_ref->addChild(our_ref, nullptr); + } else if (target_ref->parent() != our_ref->parent()) { + // Change in parent, need to remove and add + our_ref->parent()->removeChild(our_ref); + target_ref->parent()->addChild(our_ref, target_ref); + } else { + // Same parent, just move + our_ref->parent()->changeOrder(our_ref, target_ref); + } +} + +void SPItem::build(SPDocument *document, Inkscape::XML::Node *repr) { +#ifdef OBJECT_TRACE + objectTrace( "SPItem::build"); +#endif + + SPItem* object = this; + object->readAttr(SPAttr::STYLE); + object->readAttr(SPAttr::TRANSFORM); + object->readAttr(SPAttr::CLIP_PATH); + object->readAttr(SPAttr::MASK); + object->readAttr(SPAttr::SODIPODI_INSENSITIVE); + object->readAttr(SPAttr::TRANSFORM_CENTER_X); + object->readAttr(SPAttr::TRANSFORM_CENTER_Y); + object->readAttr(SPAttr::CONNECTOR_AVOID); + object->readAttr(SPAttr::CONNECTION_POINTS); + object->readAttr(SPAttr::INKSCAPE_HIGHLIGHT_COLOR); + + SPObject::build(document, repr); +#ifdef OBJECT_TRACE + objectTrace( "SPItem::build", false); +#endif +} + +void SPItem::release() +{ + // Note: do this here before the clip_ref is deleted, since calling + // ensureUpToDate() for triggered routing may reference + // the deleted clip_ref. + delete avoidRef; + avoidRef = nullptr; + + // we do NOT disconnect from the changed signal of those before deletion. + // The destructor will call *_ref_changed with NULL as the new value, + // which will cause the hide() function to be called. + delete clip_ref; + clip_ref = nullptr; + delete mask_ref; + mask_ref = nullptr; + + // the first thing SPObject::release() does is destroy the fill/stroke/filter references. + // as above, this calls *_ref_changed() which performs the hide(). + // it is important this happens before the views are cleared. + SPObject::release(); + + views.clear(); +} + +void SPItem::set(SPAttr key, gchar const* value) { +#ifdef OBJECT_TRACE + std::stringstream temp; + temp << "SPItem::set: " << sp_attribute_name(key) << " " << (value?value:"null"); + objectTrace( temp.str() ); +#endif + SPItem *item = this; + SPItem* object = item; + + switch (key) { + case SPAttr::TRANSFORM: { + Geom::Affine t; + if (value && sp_svg_transform_read(value, &t)) { + item->set_item_transform(t); + } else { + item->set_item_transform(Geom::identity()); + } + break; + } + case SPAttr::CLIP_PATH: { + auto uri = extract_uri(value); + if (!uri.empty() || item->clip_ref) { + item->getClipRef().try_attach(uri.c_str()); + } + break; + } + case SPAttr::MASK: { + auto uri = extract_uri(value); + if (!uri.empty() || item->mask_ref) { + item->getMaskRef().try_attach(uri.c_str()); + } + break; + } + case SPAttr::SODIPODI_INSENSITIVE: + { + item->sensitive = !value; + for (auto &v : item->views) { + v.drawingitem->setSensitive(item->sensitive); + } + break; + } + case SPAttr::INKSCAPE_HIGHLIGHT_COLOR: + { + item->_highlightColor = 0; + if (value) { + item->_highlightColor = sp_svg_read_color(value, 0x0) | 0xff; + } + break; + } + case SPAttr::CONNECTOR_AVOID: + if (value || item->avoidRef) { + item->getAvoidRef().setAvoid(value); + } + break; + case SPAttr::TRANSFORM_CENTER_X: + if (value) { + item->transform_center_x = g_strtod(value, nullptr); + } else { + item->transform_center_x = 0; + } + object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::TRANSFORM_CENTER_Y: + if (value) { + item->transform_center_y = g_strtod(value, nullptr); + item->transform_center_y *= -document->yaxisdir(); + } else { + item->transform_center_y = 0; + } + object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::SYSTEM_LANGUAGE: + case SPAttr::REQUIRED_FEATURES: + case SPAttr::REQUIRED_EXTENSIONS: + { + item->resetEvaluated(); + // pass to default handler + } + default: + if (SP_ATTRIBUTE_IS_CSS(key)) { + // FIXME: See if this is really necessary. Also, check after modifying SPIPaint to preserve + // non-#abcdef color formats. + + // Propagate the property change to all clones + style->readFromObject(object); + object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } else { + SPObject::set(key, value); + } + break; + } +#ifdef OBJECT_TRACE + objectTrace( "SPItem::set", false); +#endif +} + +template <typename F> +class lazy +{ +public: + explicit lazy(F &&f): f(std::move(f)) {} + + auto operator()() + { + if (!result) result = f(); + return *result; + } + +private: + F f; + std::optional<typename std::invoke_result<F>::type> result; +}; + +void SPItem::clip_ref_changed(SPObject *old_clip, SPObject *clip) +{ + if (old_clip) { + clip_ref->modified_connection.disconnect(); + for (auto &v : views) { + auto oldPath = cast<SPClipPath>(old_clip); + g_assert(oldPath); + oldPath->hide(v.drawingitem->key() + ITEM_KEY_CLIP); + } + } + auto clipPath = cast<SPClipPath>(clip); + if (clipPath) { + Geom::OptRect bbox = geometricBounds(); + for (auto &v : views) { + auto clip_key = SPItem::ensure_key(v.drawingitem.get()) + ITEM_KEY_CLIP; + auto ai = clipPath->show(v.drawingitem->drawing(), clip_key, bbox); + v.drawingitem->setClip(ai); + } + clip_ref->modified_connection = clipPath->connectModified([this] (auto, unsigned flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG)) { + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + }); + } + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); // To update bbox. +} + +void SPItem::mask_ref_changed(SPObject *old_mask, SPObject *mask) +{ + if (old_mask) { + mask_ref->modified_connection.disconnect(); + for (auto &v : views) { + auto maskItem = cast<SPMask>(old_mask); + g_assert(maskItem); + maskItem->hide(v.drawingitem->key() + ITEM_KEY_MASK); + } + } + auto maskItem = cast<SPMask>(mask); + if (maskItem) { + Geom::OptRect bbox = geometricBounds(); + for (auto &v : views) { + auto mask_key = SPItem::ensure_key(v.drawingitem.get()) + ITEM_KEY_MASK; + auto ai = maskItem->show(v.drawingitem->drawing(), mask_key, bbox); + v.drawingitem->setMask(ai); + } + mask_ref->modified_connection = maskItem->connectModified([this] (auto, unsigned flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG)) { + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + }); + } + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); // To update bbox. +} + +void SPItem::fill_ps_ref_changed(SPObject *old_ps, SPObject *ps) +{ + auto old_fill_ps = cast<SPPaintServer>(old_ps); + if (old_fill_ps) { + for (auto &v : views) { + old_fill_ps->hide(v.drawingitem->key() + ITEM_KEY_FILL); + } + } + + auto new_fill_ps = cast<SPPaintServer>(ps); + if (new_fill_ps) { + Geom::OptRect bbox = geometricBounds(); + for (auto &v : views) { + auto fill_key = SPItem::ensure_key(v.drawingitem.get()) + ITEM_KEY_FILL; + auto pi = new_fill_ps->show(v.drawingitem->drawing(), fill_key, bbox); + v.drawingitem->setFillPattern(pi); + } + } +} + +void SPItem::stroke_ps_ref_changed(SPObject *old_ps, SPObject *ps) +{ + auto old_stroke_ps = cast<SPPaintServer>(old_ps); + if (old_stroke_ps) { + for (auto &v : views) { + old_stroke_ps->hide(v.drawingitem->key() + ITEM_KEY_STROKE); + } + } + + auto new_stroke_ps = cast<SPPaintServer>(ps); + if (new_stroke_ps) { + Geom::OptRect bbox = geometricBounds(); + for (auto &v : views) { + auto stroke_key = SPItem::ensure_key(v.drawingitem.get()) + ITEM_KEY_STROKE; + auto pi = new_stroke_ps->show(v.drawingitem->drawing(), stroke_key, bbox); + v.drawingitem->setStrokePattern(pi); + } + } +} + +void SPItem::filter_ref_changed(SPObject *old_obj, SPObject *obj) +{ + auto old_filter = cast<SPFilter>(old_obj); + if (old_filter) { + for (auto &v : views) { + old_filter->hide(v.drawingitem.get()); + } + } + + auto new_filter = cast<SPFilter>(obj); + if (new_filter) { + for (auto &v : views) { + new_filter->show(v.drawingitem.get()); + } + } +} + +void SPItem::update(SPCtx *ctx, unsigned flags) +{ + auto ictx = static_cast<SPItemCtx const*>(ctx); + + // Any of the modifications defined in sp-object.h might change bbox, + // so we invalidate it unconditionally + bbox_valid = false; + + viewport = ictx->viewport; // Cache viewport + + auto bbox = lazy([this] { + return geometricBounds(); + }); + + if (flags & (SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG)) + { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + for (auto &v : views) { + v.drawingitem->setTransform(transform); + } + } + + auto set_bboxes = [&, this] (auto obj, int type) { + if (obj) { + for (auto &v : views) { + obj->setBBox(v.drawingitem->key() + type, bbox()); + } + } + }; + + set_bboxes(getClipObject(), ITEM_KEY_CLIP); + set_bboxes(getMaskObject(), ITEM_KEY_MASK); + set_bboxes(style->getFillPaintServer(), ITEM_KEY_FILL); + set_bboxes(style->getStrokePaintServer(), ITEM_KEY_STROKE); + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (auto &v : views) { + v.drawingitem->setOpacity(SP_SCALE24_TO_FLOAT(style->opacity.value)); + v.drawingitem->setAntialiasing(style->shape_rendering.computed == SP_CSS_SHAPE_RENDERING_CRISPEDGES ? 0 : 2); + v.drawingitem->setIsolation(style->isolation.value); + v.drawingitem->setBlendMode(style->mix_blend_mode.value); + v.drawingitem->setVisible(!isHidden()); + } + } + } + + // Update bounding box in user space, used for filter and objectBoundingBox units. + if (style->filter.set) { + for (auto &v : views) { + if (v.drawingitem) { + v.drawingitem->setItemBounds(bbox()); + } + } + } + + // Update libavoid with item geometry (for connector routing). + if (avoidRef && document) { + avoidRef->handleSettingChange(); + } +} + +void SPItem::modified(unsigned int /*flags*/) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPItem::modified" ); + objectTrace( "SPItem::modified", false ); +#endif +} + +Inkscape::XML::Node* SPItem::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + SPItem *item = this; + SPItem* object = item; + + // in the case of SP_OBJECT_WRITE_BUILD, the item should always be newly created, + // so we need to add any children from the underlying object to the new repr + if (flags & SP_OBJECT_WRITE_BUILD) { + std::vector<Inkscape::XML::Node *>l; + for (auto& child: object->children) { + if (is<SPTitle>(&child) || is<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: object->children) { + if (is<SPTitle>(&child) || is<SPDesc>(&child)) { + child.updateRepr(flags); + } + } + } + + repr->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(item->transform)); + + if (flags & SP_OBJECT_WRITE_EXT) { + repr->setAttribute("sodipodi:insensitive", ( item->sensitive ? nullptr : "true" )); + if (item->transform_center_x != 0) + repr->setAttributeSvgDouble("inkscape:transform-center-x", item->transform_center_x); + else + repr->removeAttribute("inkscape:transform-center-x"); + if (item->transform_center_y != 0) { + auto y = item->transform_center_y; + y *= -document->yaxisdir(); + repr->setAttributeSvgDouble("inkscape:transform-center-y", y); + } else + repr->removeAttribute("inkscape:transform-center-y"); + } + + if (getClipObject()) { + auto value = item->clip_ref->getURI()->cssStr(); + repr->setAttributeOrRemoveIfEmpty("clip-path", value); + } + if (getMaskObject()) { + auto value = item->mask_ref->getURI()->cssStr(); + repr->setAttributeOrRemoveIfEmpty("mask", value); + } + if (item->isHighlightSet()) { + repr->setAttribute("inkscape:highlight-color", SPColor(item->_highlightColor).toString()); + } else { + repr->removeAttribute("inkscape:highlight-color"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +// CPPIFY: make pure virtual +Geom::OptRect SPItem::bbox(Geom::Affine const & /*transform*/, SPItem::BBoxType /*type*/) const { + //throw; + return Geom::OptRect(); +} + +Geom::OptRect SPItem::geometricBounds(Geom::Affine const &transform) const +{ + return bbox(transform, SPItem::GEOMETRIC_BBOX); +} + +Geom::OptRect SPItem::visualBounds(Geom::Affine const &transform, bool wfilter, bool wclip, bool wmask) const +{ + Geom::OptRect bbox; + + auto gbox = lazy([this] { + return geometricBounds(); + }); + + if (auto filter = style ? style->getFilter() : nullptr; filter && wfilter) { + // call the subclass method + bbox = gbox(); // see LP Bug 1229971 + + // default filter area per the SVG spec: + SVGLength x, y, w, h; + x.set(SVGLength::PERCENT, -0.10, 0); + y.set(SVGLength::PERCENT, -0.10, 0); + w.set(SVGLength::PERCENT, 1.20, 0); + h.set(SVGLength::PERCENT, 1.20, 0); + + // if area is explicitly set, override: + if (filter->x._set) x = filter->x; + if (filter->y._set) y = filter->y; + if (filter->width._set) w = filter->width; + if (filter->height._set) h = filter->height; + + auto const len = bbox ? bbox->dimensions() : Geom::Point(); + + x.update(12, 6, len.x()); + y.update(12, 6, len.y()); + w.update(12, 6, len.x()); + h.update(12, 6, len.y()); + + if (filter->filterUnits == SP_FILTER_UNITS_OBJECTBOUNDINGBOX && bbox) { + bbox = Geom::Rect::from_xywh( + bbox->left() + x.computed * (x.unit == SVGLength::PERCENT ? 1.0 : len.x()), + bbox->top() + y.computed * (y.unit == SVGLength::PERCENT ? 1.0 : len.y()), + w.computed * (w.unit == SVGLength::PERCENT ? 1.0 : len.x()), + h.computed * (h.unit == SVGLength::PERCENT ? 1.0 : len.y()) + ); + } else { + bbox = Geom::Rect::from_xywh(x.computed, y.computed, w.computed, h.computed); + } + + *bbox *= transform; + } else { + // call the subclass method + bbox = this->bbox(transform, SPItem::VISUAL_BBOX); + } + + auto transform_with_units = [&] (bool contentunits) { + return contentunits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && gbox() + ? Geom::Scale(gbox()->dimensions()) * Geom::Translate(gbox()->min()) * transform + : transform; + }; + + auto apply_clip_or_mask_bbox = [&] (auto const *obj, bool contentunits) { + bbox.intersectWith(obj->geometricBounds(transform_with_units(contentunits))); + }; + + if (auto clip = getClipObject(); clip && wclip) { + apply_clip_or_mask_bbox(clip, clip->clippath_units()); + } + + if (auto mask = getMaskObject(); mask && wmask) { + apply_clip_or_mask_bbox(mask, mask->mask_content_units()); + } + + return bbox; +} + +Geom::OptRect SPItem::bounds(BBoxType type, Geom::Affine const &transform) const +{ + if (type == GEOMETRIC_BBOX) { + return geometricBounds(transform); + } else { + return visualBounds(transform); + } +} + +Geom::OptRect SPItem::documentPreferredBounds() const +{ + if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) { + return documentBounds(SPItem::VISUAL_BBOX); + } else { + return documentBounds(SPItem::GEOMETRIC_BBOX); + } +} + +Geom::OptRect SPItem::documentGeometricBounds() const +{ + return geometricBounds(i2doc_affine()); +} + +Geom::OptRect SPItem::documentVisualBounds() const +{ + if (!bbox_valid) { + doc_bbox = visualBounds(i2doc_affine()); + bbox_valid = true; + } + return doc_bbox; +} +Geom::OptRect SPItem::documentBounds(BBoxType type) const +{ + if (type == GEOMETRIC_BBOX) { + return documentGeometricBounds(); + } else { + return documentVisualBounds(); + } +} + +std::optional<Geom::PathVector> SPItem::documentExactBounds() const +{ + std::optional<Geom::PathVector> result; + if (auto bounding_rect = visualBounds()) { + result = Geom::Path(*bounding_rect) * i2doc_affine(); + } + return result; +} + +Geom::OptRect SPItem::desktopGeometricBounds() const +{ + return geometricBounds(i2dt_affine()); +} + +Geom::OptRect SPItem::desktopVisualBounds() const +{ + Geom::OptRect ret = documentVisualBounds(); + if (ret) { + *ret *= document->doc2dt(); + } + return ret; +} + +Geom::OptRect SPItem::desktopPreferredBounds() const +{ + if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) { + return desktopBounds(SPItem::VISUAL_BBOX); + } else { + return desktopBounds(SPItem::GEOMETRIC_BBOX); + } +} + +Geom::OptRect SPItem::desktopBounds(BBoxType type) const +{ + if (type == GEOMETRIC_BBOX) { + return desktopGeometricBounds(); + } else { + return desktopVisualBounds(); + } +} + +unsigned int SPItem::pos_in_parent() const { + g_assert(parent != nullptr); + + unsigned int pos = 0; + + for (auto& iter: parent->children) { + if (&iter == this) { + return pos; + } + + if (is<SPItem>(&iter)) { + pos++; + } + } + + g_assert_not_reached(); + return 0; +} + +// CPPIFY: make pure virtual, see below! +void SPItem::snappoints(std::vector<Inkscape::SnapCandidatePoint> & /*p*/, Inkscape::SnapPreferences const */*snapprefs*/) const { + //throw; +} + /* This will only be called if the derived class doesn't override this. + * see for example sp_genericellipse_snappoints in sp-ellipse.cpp + * We don't know what shape we could be dealing with here, so we'll just + * do nothing + */ + +void SPItem::getSnappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const +{ + // Get the snappoints of the item + snappoints(p, snapprefs); + + // Get the snappoints at the item's center + if (snapprefs && snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_ROTATION_CENTER)) { + p.emplace_back(getCenter(), Inkscape::SNAPSOURCE_ROTATION_CENTER, Inkscape::SNAPTARGET_ROTATION_CENTER); + } + + // Get the snappoints of clipping paths and mask, if any + auto desktop = SP_ACTIVE_DESKTOP; + + auto gbox = lazy([this] { + return geometricBounds(); + }); + + auto add_clip_or_mask_points = [&, this] (SPObject const *obj, bool contentunits) { + // obj is a group object, the children are the actual clippers + for (auto &child: obj->children) { + if (auto item = cast<SPItem>(&child)) { + std::vector<Inkscape::SnapCandidatePoint> p_clip_or_mask; + // Please note the recursive call here! + item->getSnappoints(p_clip_or_mask, snapprefs); + // Take into account the transformation of the item being clipped or masked + for (auto const &p_orig : p_clip_or_mask) { + // All snappoints are in desktop coordinates, but the item's transformation is + // in document coordinates. Hence the awkward construction below + auto pt = p_orig.getPoint(); + if (contentunits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && gbox()) { + pt *= Geom::Scale(gbox()->dimensions()) * Geom::Translate(gbox()->min()); + } + pt = desktop->dt2doc(pt) * i2dt_affine(); + p.emplace_back(pt, p_orig.getSourceType(), p_orig.getTargetType()); + } + } + } + }; + + if (auto clip = getClipObject()) { + add_clip_or_mask_points(clip, clip->clippath_units()); + } + + if (auto mask = getMaskObject()) { + add_clip_or_mask_points(mask, mask->mask_content_units()); + } +} + +// CPPIFY: make pure virtual +void SPItem::print(SPPrintContext* /*ctx*/) { + //throw; +} + +void SPItem::invoke_print(SPPrintContext *ctx) +{ + if (!isHidden()) { + if (!transform.isIdentity() || style->opacity.value != SP_SCALE24_MAX) { + ctx->bind(transform, SP_SCALE24_TO_FLOAT(style->opacity.value)); + print(ctx); + ctx->release(); + } else { + print(ctx); + } + } +} + +/** + * The item's type name, not node tag name. NOT translated. + * + * @return The item's type name (default: 'item') + */ +const char* SPItem::typeName() const { + return "item"; +} + +/** + * The item's type name as a translated human string. + * + * Translated string for UI display. + */ +const char* SPItem::displayName() const { + return _("Object"); +} + +gchar* SPItem::description() const { + return g_strdup(""); +} + +gchar *SPItem::detailedDescription() const { + gchar* s = g_strdup_printf("<b>%s</b> %s", + this->displayName(), this->description()); + + if (s && getClipObject()) { + char *snew = g_strdup_printf(_("%s; <i>clipped</i>"), s); + g_free(s); + s = snew; + } + + if (s && getMaskObject()) { + char *snew = g_strdup_printf(_("%s; <i>masked</i>"), s); + g_free(s); + s = snew; + } + + if (style && style->filter.href && style->filter.href->getObject()) { + char const *label = style->filter.href->getObject()->label(); + char *snew = nullptr; + + if (label) { + snew = g_strdup_printf(_("%s; <i>filtered (%s)</i>"), s, _(label)); + } else { + snew = g_strdup_printf(_("%s; <i>filtered</i>"), s); + } + + g_free(s); + s = snew; + } + + return s; +} + +bool SPItem::isFiltered() const { + return style && style->filter.href && style->filter.href->getObject(); +} + +SPObject* SPItem::isInMask() const { + SPObject* parent = this->parent; + while (parent && !is<SPMask>(parent)) { + parent = parent->parent; + } + return parent; +} + +SPObject* SPItem::isInClipPath() const { + SPObject* parent = this->parent; + while (parent && !is<SPClipPath>(parent)) { + parent = parent->parent; + } + return parent; +} + +unsigned SPItem::display_key_new(unsigned numkeys) +{ + static unsigned dkey = 1; + + dkey += numkeys; + + return dkey - numkeys; +} + +unsigned SPItem::ensure_key(Inkscape::DrawingItem *di) +{ + if (!di->key()) { + di->setKey(SPItem::display_key_new(ITEM_KEY_SIZE)); + } + return di->key(); +} + +// CPPIFY: make pure virtual +Inkscape::DrawingItem* SPItem::show(Inkscape::Drawing& /*drawing*/, unsigned int /*key*/, unsigned int /*flags*/) { + //throw; + return nullptr; +} + +Inkscape::DrawingItem *SPItem::invoke_show(Inkscape::Drawing &drawing, unsigned key, unsigned flags) +{ + auto ai = show(drawing, key, flags); + if (!ai) { + return nullptr; + } + + auto const bbox = geometricBounds(); + + ai->setItem(this); + ai->setItemBounds(bbox); + ai->setTransform(transform); + ai->setOpacity(SP_SCALE24_TO_FLOAT(style->opacity.value)); + ai->setIsolation(style->isolation.value); + ai->setBlendMode(style->mix_blend_mode.value); + ai->setVisible(!isHidden()); + ai->setSensitive(sensitive); + views.emplace_back(flags, key, DrawingItemPtr<Inkscape::DrawingItem>(ai)); + + if (auto clip = getClipObject()) { + auto clip_key = SPItem::ensure_key(ai) + ITEM_KEY_CLIP; + auto ac = clip->show(drawing, clip_key, bbox); + ai->setClip(ac); + } + if (auto mask = getMaskObject()) { + auto mask_key = SPItem::ensure_key(ai) + ITEM_KEY_MASK; + auto ac = mask->show(drawing, mask_key, bbox); + ai->setMask(ac); + } + if (auto fill = style->getFillPaintServer()) { + auto fill_key = SPItem::ensure_key(ai) + ITEM_KEY_FILL; + auto ap = fill->show(drawing, fill_key, bbox); + ai->setFillPattern(ap); + } + if (auto stroke = style->getStrokePaintServer()) { + auto stroke_key = SPItem::ensure_key(ai) + ITEM_KEY_STROKE; + auto ap = stroke->show(drawing, stroke_key, bbox); + ai->setStrokePattern(ap); + } + if (auto filter = style->getFilter()) { + filter->show(ai); + } + + return ai; +} + +// CPPIFY: make pure virtual +void SPItem::hide(unsigned int /*key*/) { + //throw; +} + +void SPItem::invoke_hide(unsigned key) +{ + hide(key); + + for (auto it = views.begin(); it != views.end(); ) { + auto &v = *it; + if (v.key == key) { + unsigned ai_key = v.drawingitem->key(); + + if (auto clip = getClipObject()) { + clip->hide(ai_key + ITEM_KEY_CLIP); + } + if (auto mask = getMaskObject()) { + mask->hide(ai_key + ITEM_KEY_MASK); + } + if (auto fill_ps = style->getFillPaintServer()) { + fill_ps->hide(ai_key + ITEM_KEY_FILL); + } + if (auto stroke_ps = style->getStrokePaintServer()) { + stroke_ps->hide(ai_key + ITEM_KEY_STROKE); + } + if (auto filter = style->getFilter()) { + filter->hide(v.drawingitem.get()); + } + + v.drawingitem.reset(); + + *it = std::move(views.back()); + views.pop_back(); + } else { + ++it; + } + } +} + +/** + * Invoke hide on all non-group items, except for the list of items to keep. + */ +void SPItem::invoke_hide_except(unsigned key, const std::vector<SPItem *> &to_keep) +{ + // If item is not in the list of items to keep. + if (to_keep.end() == find(to_keep.begin(), to_keep.end(), this)) { + // Only hide the item if it's not a group, root or use. + if (!is<SPRoot>(this) && + !is<SPGroup>(this) && + !is<SPUse>(this) + ) { + this->invoke_hide(key); + } + // recurse + for (auto &obj : this->children) { + if (auto child = cast<SPItem>(&obj)) { + child->invoke_hide_except(key, to_keep); + } + } + } +} + +// Adjusters + +void SPItem::adjust_pattern(Geom::Affine const &postmul, bool set, PaintServerTransform pt) +{ + bool fill = (pt == TRANSFORM_FILL || pt == TRANSFORM_BOTH); + if (fill && style && (style->fill.isPaintserver())) { + SPObject *server = style->getFillPaintServer(); + auto serverPatt = cast<SPPattern>(server); + if ( serverPatt ) { + SPPattern *pattern = serverPatt->clone_if_necessary(this, "fill"); + pattern->transform_multiply(postmul, set); + } + } + + bool stroke = (pt == TRANSFORM_STROKE || pt == TRANSFORM_BOTH); + if (stroke && style && (style->stroke.isPaintserver())) { + SPObject *server = style->getStrokePaintServer(); + auto serverPatt = cast<SPPattern>(server); + if ( serverPatt ) { + SPPattern *pattern = serverPatt->clone_if_necessary(this, "stroke"); + pattern->transform_multiply(postmul, set); + } + } +} + +void SPItem::adjust_hatch(Geom::Affine const &postmul, bool set, PaintServerTransform pt) +{ + bool fill = (pt == TRANSFORM_FILL || pt == TRANSFORM_BOTH); + if (fill && style && (style->fill.isPaintserver())) { + SPObject *server = style->getFillPaintServer(); + auto serverHatch = cast<SPHatch>(server); + if (serverHatch) { + SPHatch *hatch = serverHatch->clone_if_necessary(this, "fill"); + hatch->transform_multiply(postmul, set); + } + } + + bool stroke = (pt == TRANSFORM_STROKE || pt == TRANSFORM_BOTH); + if (stroke && style && (style->stroke.isPaintserver())) { + SPObject *server = style->getStrokePaintServer(); + auto serverHatch = cast<SPHatch>(server); + if (serverHatch) { + SPHatch *hatch = serverHatch->clone_if_necessary(this, "stroke"); + hatch->transform_multiply(postmul, set); + } + } +} + +void SPItem::adjust_gradient( Geom::Affine const &postmul, bool set ) +{ + if ( style && style->fill.isPaintserver() ) { + SPPaintServer *server = style->getFillPaintServer(); + auto serverGrad = cast<SPGradient>(server); + if ( serverGrad ) { + + /** + * \note Bbox units for a gradient are generally a bad idea because + * with them, you cannot preserve the relative position of the + * object and its gradient after rotation or skew. So now we + * convert them to userspace units which are easy to keep in sync + * just by adding the object's transform to gradientTransform. + * \todo FIXME: convert back to bbox units after transforming with + * the item, so as to preserve the original units. + */ + SPGradient *gradient = sp_gradient_convert_to_userspace( serverGrad, this, "fill" ); + + sp_gradient_transform_multiply( gradient, postmul, set ); + } + } + + if ( style && style->stroke.isPaintserver() ) { + SPPaintServer *server = style->getStrokePaintServer(); + auto serverGrad = cast<SPGradient>(server); + if ( serverGrad ) { + SPGradient *gradient = sp_gradient_convert_to_userspace( serverGrad, this, "stroke"); + sp_gradient_transform_multiply( gradient, postmul, set ); + } + } +} + +void SPItem::adjust_stroke( gdouble ex ) +{ + if (freeze_stroke_width) { + return; + } + + SPStyle *style = this->style; + + if (style && !Geom::are_near(ex, 1.0, Geom::EPSILON)) { + style->stroke_width.computed *= ex; + style->stroke_width.set = TRUE; + + if ( !style->stroke_dasharray.values.empty() ) { + for (auto & value : style->stroke_dasharray.values) { + value.value *= ex; + value.computed *= ex; + } + style->stroke_dashoffset.value *= ex; + style->stroke_dashoffset.computed *= ex; + } + + updateRepr(); + } +} + +/** + * Find out the inverse of previous transform of an item (from its repr) + */ +Geom::Affine sp_item_transform_repr (SPItem *item) +{ + Geom::Affine t_old(Geom::identity()); + gchar const *t_attr = item->getRepr()->attribute("transform"); + if (t_attr) { + Geom::Affine t; + if (sp_svg_transform_read(t_attr, &t)) { + t_old = t; + } + } + + return t_old; +} + + +void SPItem::adjust_stroke_width_recursive(double expansion) +{ + adjust_stroke (expansion); + +// A clone's child is the ghost of its original - we must not touch it, skip recursion + if (!is<SPUse>(this)) { + for (auto& o: children) { + auto item = cast<SPItem>(&o); + if (item) { + item->adjust_stroke_width_recursive(expansion); + } + } + } +} + +void SPItem::freeze_stroke_width_recursive(bool freeze) +{ + freeze_stroke_width = freeze; + +// A clone's child is the ghost of its original - we must not touch it, skip recursion + if (!is<SPUse>(this)) { + for (auto& o: children) { + auto item = cast<SPItem>(&o); + if (item) { + item->freeze_stroke_width_recursive(freeze); + } + } + } +} + +/** + * Recursively adjust rx and ry of rects. + */ +static void +sp_item_adjust_rects_recursive(SPItem *item, Geom::Affine advertized_transform) +{ + auto rect = cast<SPRect>(item); + if (rect) { + rect->compensateRxRy(advertized_transform); + } + + for(auto& o: item->children) { + auto itm = cast<SPItem>(&o); + if (itm) { + sp_item_adjust_rects_recursive(itm, advertized_transform); + } + } +} + +void SPItem::adjust_paint_recursive(Geom::Affine advertized_transform, Geom::Affine t_ancestors, PaintServerType type) +{ +// _Before_ full pattern/gradient transform: t_paint * t_item * t_ancestors +// _After_ full pattern/gradient transform: t_paint_new * t_item * t_ancestors * advertised_transform +// By equating these two expressions we get t_paint_new = t_paint * paint_delta, where: + Geom::Affine t_item = sp_item_transform_repr (this); + Geom::Affine paint_delta = t_item * t_ancestors * advertized_transform * t_ancestors.inverse() * t_item.inverse(); + +// Within text, we do not fork gradients, and so must not recurse to avoid double compensation; +// also we do not recurse into clones, because a clone's child is the ghost of its original - +// we must not touch it + if (!(cast<SPText>(this) || cast<SPUse>(this))) { + for (auto& o: children) { + auto item = cast<SPItem>(&o); + if (item) { + // At the level of the transformed item, t_ancestors is identity; + // below it, it is the accumulated chain of transforms from this level to the top level + item->adjust_paint_recursive(advertized_transform, t_item * t_ancestors, type); + } + } + } + +// We recursed into children first, and are now adjusting this object second; +// this is so that adjustments in a tree are done from leaves up to the root, +// and paintservers on leaves inheriting their values from ancestors could adjust themselves properly +// before ancestors themselves are adjusted, probably differently (bug 1286535) + + switch (type) { + case PATTERN: { + adjust_pattern(paint_delta); + break; + } + case HATCH: { + adjust_hatch(paint_delta); + break; + } + default: { + adjust_gradient(paint_delta); + } + } +} + +bool SPItem::collidesWith(Geom::PathVector const &shape) const +{ + auto our_shape = documentExactBounds(); + return our_shape ? pathvs_have_nonempty_overlap(*our_shape, shape) : false; +} + +bool SPItem::collidesWith(SPItem const &other) const +{ + auto other_shape = other.documentExactBounds(); + return other_shape ? collidesWith(*other_shape) : false; +} + +// CPPIFY:: make pure virtual? +// Not all SPItems must necessarily have a set transform method! +Geom::Affine SPItem::set_transform(Geom::Affine const &transform) { +// throw; + return transform; +} + +/** + * Return true if the item is referenced by an LPE. + */ +static bool is_satellite_item(SPItem const &item) +{ + for (SPObject const *ref : item.hrefList) { + if (is<LivePathEffectObject>(ref)) { + return true; + } + } + return false; +} + +bool SPItem::unoptimized() { + if (auto path_effect = getAttribute("inkscape:path-effect")) { + assert(path_effect[0]); + return true; + } + + if (is_satellite_item(*this)) { + return true; + } + + return false; +} + +void SPItem::doWriteTransform(Geom::Affine const &transform, Geom::Affine const *adv, bool compensate) +{ + // calculate the relative transform, if not given by the adv attribute + Geom::Affine advertized_transform; + if (adv != nullptr) { + advertized_transform = *adv; + } else { + advertized_transform = sp_item_transform_repr (this).inverse() * transform; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (compensate) { + // recursively compensating for stroke scaling will not always work, because it can be scaled to zero or infinite + // from which we cannot ever recover by applying an inverse scale; therefore we temporarily block any changes + // to the strokewidth in such a case instead, and unblock these after the transformation + // (as reported in https://bugs.launchpad.net/inkscape/+bug/825840/comments/4) + if (!prefs->getBool("/options/transform/stroke", true)) { + double const expansion = 1. / advertized_transform.descrim(); + if (expansion < 1e-9 || expansion > 1e9) { + freeze_stroke_width_recursive(true); + // This will only work if the item has a set_transform method (in this method adjust_stroke() will be called) + // We will still have to apply the inverse scaling to other items, not having a set_transform method + // such as ellipses and stars + // PS: We cannot use this freeze_stroke_width_recursive() trick in all circumstances. For example, it will + // break pasting objects within their group (because in such a case the transformation of the group will affect + // the strokewidth, and has to be compensated for. See https://bugs.launchpad.net/inkscape/+bug/959223/comments/10) + } else { + adjust_stroke_width_recursive(expansion); + } + } + + // recursively compensate rx/ry of a rect if requested + if (!prefs->getBool("/options/transform/rectcorners", true)) { + sp_item_adjust_rects_recursive(this, advertized_transform); + } + + // recursively compensate pattern fill if it's not to be transformed + if (!prefs->getBool("/options/transform/pattern", true)) { + adjust_paint_recursive(advertized_transform.inverse(), Geom::identity(), PATTERN); + } + if (!prefs->getBool("/options/transform/hatch", true)) { + adjust_paint_recursive(advertized_transform.inverse(), Geom::identity(), HATCH); + } + + /// \todo FIXME: add the same else branch as for gradients below, to convert patterns to userSpaceOnUse as well + /// recursively compensate gradient fill if it's not to be transformed + if (!prefs->getBool("/options/transform/gradient", true)) { + adjust_paint_recursive(advertized_transform.inverse(), Geom::identity(), GRADIENT); + } else { + // 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 + adjust_paint_recursive(Geom::identity(), Geom::identity(), GRADIENT); + } + + } // endif(compensate) + + gint preserve = prefs->getBool("/options/preservetransform/value", false); + Geom::Affine transform_attr (transform); + + // CPPIFY: check this code. + // If onSetTransform is not overridden, CItem::onSetTransform will return the transform it was given as a parameter. + // onSetTransform cannot be pure due to the fact that not all visible Items are transformable. + auto lpeitem = cast<SPLPEItem>(this); + if (lpeitem) { + lpeitem->notifyTransform(transform); + } + bool unoptimiced = unoptimized(); + if ( // run the object's set_transform (i.e. embed transform) only if: + (cast<SPText>(this) && firstChild() && cast<SPTextPath>(firstChild())) || + (!preserve && // user did not chose to preserve all transforms + !getClipObject() && // the object does not have a clippath + !getMaskObject() && // the object does not have a mask + !(!transform.isTranslation() && style && style->getFilter()) && + !unoptimiced) + // the object does not have a filter, or the transform is translation (which is supposed to not affect filters) + ) + { + transform_attr = this->set_transform(transform); + } + if (freeze_stroke_width) { + freeze_stroke_width_recursive(false); + if (compensate) { + if (!prefs->getBool("/options/transform/stroke", true)) { + // Recursively compensate for stroke scaling, depending on user preference + // (As to why we need to do this, see the comment a few lines above near the freeze_stroke_width_recursive(true) call) + double const expansion = 1. / advertized_transform.descrim(); + adjust_stroke_width_recursive(expansion); + } + } + } + // this avoid temporary scaling issues on display when near identity + // this must be a bit grater than EPSILON * transform.descrim() + double e = 1e-5 * transform.descrim(); + if (transform_attr.isIdentity(e)) { + transform_attr = Geom::Affine(); + } + set_item_transform(transform_attr); + + // Note: updateRepr comes before emitting the transformed signal since + // it causes clone SPUse's copy of the original object to be brought up to + // date with the original. Otherwise, sp_use_bbox returns incorrect + // values if called in code handling the transformed signal. + updateRepr(); + + if (lpeitem) { + if (!lpeitem->hasPathEffectOfType(Inkscape::LivePathEffect::SLICE)) { + sp_lpe_item_update_patheffect(lpeitem, false, true); + } + } + + // send the relative transform with a _transformed_signal + _transformed_signal.emit(&advertized_transform, this); +} + +// CPPIFY: see below, do not make pure? +gint SPItem::event(SPEvent* /*event*/) { + return FALSE; +} + +gint SPItem::emitEvent(SPEvent &event) +{ + return this->event(&event); +} + +void SPItem::set_item_transform(Geom::Affine const &transform_matrix) +{ + if (!Geom::are_near(transform_matrix, transform, 1e-18)) { + transform = transform_matrix; + /* The SP_OBJECT_USER_MODIFIED_FLAG_B is used to mark the fact that it's only a + transformation. It's apparently not used anywhere else. */ + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_USER_MODIFIED_FLAG_B); + } +} + +//void SPItem::convert_to_guides() const { +// // CPPIFY: If not overridden, call SPItem::convert_to_guides() const, see below! +// this->convert_to_guides(); +//} + +Geom::Affine i2anc_affine(SPObject const *object, SPObject const *ancestor) +{ + Geom::Affine ret; + + // Stop at first non-renderable ancestor. + while (object != ancestor && is<SPItem>(object)) { + if (auto root = cast<SPRoot>(object)) { + ret *= root->c2p; + } else { + auto item = cast_unsafe<SPItem>(object); + ret *= item->transform; + } + object = object->parent; + } + + return ret; +} + +Geom::Affine +i2i_affine(SPObject const *src, SPObject const *dest) { + g_return_val_if_fail(src != nullptr && dest != nullptr, Geom::identity()); + SPObject const *ancestor = src->nearestCommonAncestor(dest); + return i2anc_affine(src, ancestor) * i2anc_affine(dest, ancestor).inverse(); +} + +Geom::Affine SPItem::getRelativeTransform(SPObject const *dest) const { + return i2i_affine(this, dest); +} + +Geom::Affine SPItem::i2doc_affine() const +{ + return i2anc_affine(this, nullptr); +} + +Geom::Affine SPItem::i2dt_affine() const +{ + return i2doc_affine() * document->doc2dt(); +} + +// TODO should be named "set_i2dt_affine" +void SPItem::set_i2d_affine(Geom::Affine const &i2dt) +{ + Geom::Affine dt2p; /* desktop to item parent transform */ + if (parent) { + dt2p = static_cast<SPItem *>(parent)->i2dt_affine().inverse(); + } else { + dt2p = document->dt2doc(); + } + + Geom::Affine const i2p( i2dt * dt2p ); + set_item_transform(i2p); +} + + +Geom::Affine SPItem::dt2i_affine() const +{ + /* fixme: Implement the right way (Lauris) */ + return i2dt_affine().inverse(); +} + +/* Item views */ + +Inkscape::DrawingItem *SPItem::get_arenaitem(unsigned key) +{ + for (auto &v : views) { + if (v.key == key) { + return v.drawingitem.get(); + } + } + return nullptr; +} + +int sp_item_repr_compare_position(SPItem const *first, SPItem const *second) +{ + return sp_repr_compare_position(first->getRepr(), + second->getRepr()); +} + +SPItem const *sp_item_first_item_child(SPObject const *obj) +{ + return sp_item_first_item_child( const_cast<SPObject *>(obj) ); +} + +SPItem *sp_item_first_item_child(SPObject *obj) +{ + SPItem *child = nullptr; + for (auto& iter: obj->children) { + auto tmp = cast<SPItem>(&iter); + if ( tmp ) { + child = tmp; + break; + } + } + return child; +} + +void SPItem::convert_to_guides() const { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prefs_bbox = prefs->getInt("/tools/bounding_box", 0); + + Geom::OptRect bbox = (prefs_bbox == 0) ? desktopVisualBounds() : desktopGeometricBounds(); + if (!bbox) { + g_warning ("Cannot determine item's bounding box during conversion to guides.\n"); + return; + } + + std::list<std::pair<Geom::Point, Geom::Point> > pts; + + Geom::Point A((*bbox).min()); + Geom::Point C((*bbox).max()); + Geom::Point B(A[Geom::X], C[Geom::Y]); + Geom::Point D(C[Geom::X], A[Geom::Y]); + + pts.emplace_back(A, B); + pts.emplace_back(B, C); + pts.emplace_back(C, D); + pts.emplace_back(D, A); + + sp_guide_pt_pairs_to_guides(document, pts); +} + +void SPItem::rotate_rel(Geom::Rotate const &rotation) +{ + Geom::Point center = getCenter(); + Geom::Translate const s(getCenter()); + Geom::Affine affine = Geom::Affine(s).inverse() * Geom::Affine(rotation) * Geom::Affine(s); + + // Rotate item. + set_i2d_affine(i2dt_affine() * (Geom::Affine)affine); + // Use each item's own transform writer, consistent with sp_selection_apply_affine() + doWriteTransform(transform); + + // Restore the center position (it's changed because the bbox center changed) + if (isCenterSet()) { + setCenter(center * affine); + updateRepr(); + } +} + +void SPItem::scale_rel(Geom::Scale const &scale) +{ + Geom::OptRect bbox = desktopVisualBounds(); + if (bbox) { + Geom::Translate const s(bbox->midpoint()); // use getCenter? + set_i2d_affine(i2dt_affine() * s.inverse() * scale * s); + doWriteTransform(transform); + } +} + +void SPItem::skew_rel(double skewX, double skewY) +{ + Geom::Point center = getCenter(); + Geom::Translate const s(getCenter()); + + Geom::Affine const skew(1, skewY, skewX, 1, 0, 0); + Geom::Affine affine = Geom::Affine(s).inverse() * skew * Geom::Affine(s); + + set_i2d_affine(i2dt_affine() * affine); + doWriteTransform(transform); + + // Restore the center position (it's changed because the bbox center changed) + if (isCenterSet()) { + setCenter(center * affine); + updateRepr(); + } +} + +void SPItem::move_rel( Geom::Translate const &tr) +{ + set_i2d_affine(i2dt_affine() * tr); + + doWriteTransform(transform); +} + +/* + 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 : diff --git a/src/object/sp-item.h b/src/object/sp-item.h new file mode 100644 index 0000000..6d93670 --- /dev/null +++ b/src/object/sp-item.h @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_ITEM_H +#define SEEN_SP_ITEM_H + +/** + * @file + * Some things pertinent to all visible shapes: SPItem, SPItemView, SPItemCtx, SPItemClass, SPEvent. + */ + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Abhishek Sharma + * + * Copyright (C) 1999-2006 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> +#include <2geom/affine.h> +#include <2geom/rect.h> +#include "live_effects/effect-enum.h" +#include <vector> + +#include "sp-object.h" +#include "sp-marker-loc.h" +#include "display/drawing-item-ptr.h" +#include "xml/repr.h" + +class SPGroup; +class SPClipPath; +class SPClipPathReference; +class SPMask; +class SPMaskReference; +class SPAvoidRef; +class SPPattern; +struct SPPrintContext; +typedef unsigned int guint32; + +namespace Inkscape { + +class Drawing; +class DrawingItem; +class URIReference; +class SnapCandidatePoint; +class SnapPreferences; + +namespace UI { +namespace View { +class SVGViewWidget; +} +} +} + +// TODO make a completely new function that transforms either the fill or +// stroke of any SPItem without adding an extra parameter to adjust_pattern. +enum PaintServerTransform { TRANSFORM_BOTH, TRANSFORM_FILL, TRANSFORM_STROKE }; + +/** + * Event structure. + * + * @todo This is just placeholder. Plan: + * We do extensible event structure, that hold applicable (ui, non-ui) + * data pointers. So it is up to given object/arena implementation + * to process correct ones in meaningful way. + * Also, this probably goes to SPObject base class. + * + * GUI Code should not be here! + */ +class SPEvent { + +public: + enum Type { + INVALID, + NONE, + ACTIVATE, + MOUSEOVER, + MOUSEOUT + }; + + Type type; + Inkscape::UI::View::SVGViewWidget* view; +}; + +struct SPItemView +{ + unsigned flags; + unsigned key; + DrawingItemPtr<Inkscape::DrawingItem> drawingitem; + SPItemView(unsigned flags, unsigned key, DrawingItemPtr<Inkscape::DrawingItem> drawingitem); +}; + +enum SPItemKey +{ + ITEM_KEY_CLIP, + ITEM_KEY_MASK, + ITEM_KEY_FILL, + ITEM_KEY_STROKE, + ITEM_KEY_MARKERS, + ITEM_KEY_SIZE = ITEM_KEY_MARKERS + SP_MARKER_LOC_QTY +}; + +/* flags */ + +#define SP_ITEM_BBOX_VISUAL 1 + +#define SP_ITEM_SHOW_DISPLAY (1 << 0) + +/** + * Flag for referenced views (i.e. markers, clippaths, masks and patterns); + * currently unused, does the same as DISPLAY + */ +#define SP_ITEM_REFERENCE_FLAGS (1 << 1) + +/** + * Contains transformations to document/viewport and the viewport size. + */ +class SPItemCtx : public SPCtx { +public: + /** Item to document transformation */ + Geom::Affine i2doc; + + /** Viewport size */ + Geom::Rect viewport; + + /** Item to viewport transformation */ + Geom::Affine i2vp; +}; + +/** + * Base class for visual SVG elements. + * SPItem is an abstract base class for all graphic (visible) SVG nodes. It + * is a subclass of SPObject, with great deal of specific functionality. + */ +class SPItem : public SPObject { +public: + enum BBoxType { + // legacy behavior: includes crude stroke, markers; excludes long miters, blur margin; is known to be wrong for caps + APPROXIMATE_BBOX, + // includes only the bare path bbox, no stroke, no nothing + GEOMETRIC_BBOX, + // includes everything: correctly done stroke (with proper miters and caps), markers, filter margins (e.g. blur) + VISUAL_BBOX + }; + + enum PaintServerType { PATTERN, HATCH, GRADIENT }; + + SPItem(); + ~SPItem() override; + int tag() const override { return tag_of<decltype(*this)>; } + + unsigned int sensitive : 1; + unsigned int stop_paint: 1; + mutable unsigned bbox_valid : 1; + double transform_center_x; + double transform_center_y; + bool freeze_stroke_width; + + // Used in the layers/objects dialog, this remembers if this item's + // children are visible in the expanded state in the tree. + bool _is_expanded = false; + + Geom::Affine transform; + mutable Geom::OptRect doc_bbox; + Geom::Rect viewport; // Cache viewport information + + SPClipPath *getClipObject() const; + SPMask *getMaskObject() const; + + SPClipPathReference &getClipRef(); + SPMaskReference &getMaskRef(); + + SPAvoidRef &getAvoidRef(); + std::vector<std::pair <Glib::ustring, Glib::ustring> > rootsatellites; + private: + SPClipPathReference *clip_ref; + SPMaskReference *mask_ref; + + // Used for object-avoiding connectors + SPAvoidRef *avoidRef; + + public: + std::vector<SPItemView> views; + + sigc::signal<void (Geom::Affine const *, SPItem *)> _transformed_signal; + + bool isLocked() const; + void setLocked(bool lock); + + bool isHidden() const; + void setHidden(bool hidden); + + // Objects dialogue + bool isSensitive() const { + return sensitive; + }; + + void setHighlight(guint32 color); + bool isHighlightSet() const; + virtual guint32 highlight_color() const; + + //==================== + + bool isEvaluated() const; + void setEvaluated(bool visible); + void resetEvaluated(); + bool unoptimized(); + bool isHidden(unsigned display_key) const; + + /** + * Returns something suitable for the `Hide' checkbox in the Object Properties dialog box. + * Corresponds to setExplicitlyHidden. + */ + bool isExplicitlyHidden() const; + + /** + * Sets the display CSS property to `hidden' if \a val is true, + * otherwise makes it unset. + */ + void setExplicitlyHidden(bool val); + + /** + * Sets the transform_center_x and transform_center_y properties to retain the rotation center + */ + void setCenter(Geom::Point const &object_centre); + + void unsetCenter(); + bool isCenterSet() const; + Geom::Point getCenter() const; + void scaleCenter(Geom::Scale const &sc); + + bool isVisibleAndUnlocked() const; + + bool isVisibleAndUnlocked(unsigned display_key) const; + + Geom::Affine getRelativeTransform(SPObject const *obj) const; + + bool raiseOne(); + bool lowerOne(); + void raiseToTop(); + void lowerToBottom(); + + SPGroup *getParentGroup() const; + + /** + * Move this SPItem into or after another SPItem in the doc. + * + * @param target the SPItem to move into or after. + * @param intoafter move to after the target (false), move inside (sublayer) of the target (true). + */ + void moveTo(SPItem *target, bool intoafter); + + sigc::connection connectTransformed(sigc::slot<void (Geom::Affine const *, SPItem *)> slot) { + return _transformed_signal.connect(slot); + } + + /** + * Get item's geometric bounding box in this item's coordinate system. + * + * The geometric bounding box includes only the path, disregarding all style attributes. + */ + Geom::OptRect geometricBounds(Geom::Affine const &transform = Geom::identity()) const; + + /** + * Get item's visual bounding box in this item's coordinate system. + * + * The visual bounding box includes the stroke and the filter region. + * @param wfilter use filter expand in bbox calculation + * @param wclip use clip data in bbox calculation + * @param wmask use mask data in bbox calculation + */ + Geom::OptRect visualBounds(Geom::Affine const &transform = Geom::identity(), bool wfilter = true, bool wclip = true, + bool wmask = true) const; + + Geom::OptRect bounds(BBoxType type, Geom::Affine const &transform = Geom::identity()) const; + + /** + * Get item's geometric bbox in document coordinate system. + * Document coordinates are the default coordinates of the root element: + * the origin is at the top left, X grows to the right and Y grows downwards. + */ + Geom::OptRect documentGeometricBounds() const; + + /** + * Get item's visual bbox in document coordinate system. + */ + Geom::OptRect documentVisualBounds() const; + + Geom::OptRect documentBounds(BBoxType type) const; + Geom::OptRect documentPreferredBounds() const; + + /** + * Get an exact geometric shape representing the visual bounds of the item in the document + * coordinates. This is different than a simple bounding rectangle aligned to the coordinate axes: + * the returned pathvector may effectively describe any shape and coincides with an appropriately + * transformed path-vector for paths. Even for rectangular items such as SPImage, the bounds may be + * a parallelogram resulting from transforming the bounding rectangle by an affine transformation. + */ + virtual std::optional<Geom::PathVector> documentExactBounds() const; + + /** + * Get item's geometric bbox in desktop coordinate system. + * Desktop coordinates should be user defined. Currently they are hardcoded: + * origin is at bottom left, X grows to the right and Y grows upwards. + */ + Geom::OptRect desktopGeometricBounds() const; + + /** + * Get item's visual bbox in desktop coordinate system. + */ + Geom::OptRect desktopVisualBounds() const; + + Geom::OptRect desktopPreferredBounds() const; + Geom::OptRect desktopBounds(BBoxType type) const; + + unsigned int pos_in_parent() const; + + /** + * Returns a string suitable for status bar, formatted in pango markup language. + * + * Must be freed by caller. + */ + char *detailedDescription() const; + + /** + * Returns true if the item is filtered, false otherwise. + * Used with groups/lists to determine how many, or if any, are filtered. + */ + bool isFiltered() const; + + SPObject* isInMask() const; + + SPObject* isInClipPath() const; + + void invoke_print(SPPrintContext *ctx); + + /** + * Allocates unique integer keys. + * + * @param numkeys Number of keys required. + * @return First allocated key; hence if the returned key is n + * you can use n, n + 1, ..., n + (numkeys - 1) + */ + static unsigned int display_key_new(unsigned numkeys); + + /** + * Ensures that a drawing item's key is the first of a block of ITEM_KEY_SIZE keys, + * assigning it such a key if necessary. + * + * @return The value of di->key() after assignment. + */ + static unsigned ensure_key(Inkscape::DrawingItem *di); + + Inkscape::DrawingItem *invoke_show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags); + + // Removed item from display tree. + void invoke_hide(unsigned int key); + void invoke_hide_except(unsigned key, const std::vector<SPItem *> &to_keep); + + void getSnappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs=nullptr) const; + void adjust_pattern(/* Geom::Affine const &premul, */ Geom::Affine const &postmul, bool set = false, + PaintServerTransform = TRANSFORM_BOTH); + void adjust_hatch(/* Geom::Affine const &premul, */ Geom::Affine const &postmul, bool set = false, + PaintServerTransform = TRANSFORM_BOTH); + void adjust_gradient(/* Geom::Affine const &premul, */ Geom::Affine const &postmul, bool set = false); + void adjust_stroke(double ex); + + /** + * Recursively scale stroke width in \a item and its children by \a expansion. + */ + void adjust_stroke_width_recursive(double ex); + + void freeze_stroke_width_recursive(bool freeze); + + /** + * Recursively compensate pattern or gradient transform. + */ + void adjust_paint_recursive(Geom::Affine advertized_transform, Geom::Affine t_ancestors, + PaintServerType type = GRADIENT); + + /** + * Checks for visual collision with another item + */ + bool collidesWith(Geom::PathVector const &shape) const; + bool collidesWith(SPItem const &other) const; + + /** + * Set a new transform on an object. + * + * Compensate for stroke scaling and gradient/pattern fill transform, if + * necessary. Call the object's set_transform method if transforms are + * stored optimized. Send _transformed_signal. Invoke _write method so that + * the repr is updated with the new transform. + */ + void doWriteTransform(Geom::Affine const &transform, Geom::Affine const *adv = nullptr, bool compensate = true); + + /** + * Sets item private transform (not propagated to repr), without compensating stroke widths, + * gradients, patterns as sp_item_write_transform does. + */ + void set_item_transform(Geom::Affine const &transform_matrix); + + int emitEvent (SPEvent &event); + + /** + * Return the arenaitem corresponding to the given item in the display + * with the given key + */ + Inkscape::DrawingItem *get_arenaitem(unsigned int key); + + /** + * Returns the accumulated transformation of the item and all its ancestors, including root's viewport. + * @pre (item != NULL) and is<SPItem>(item). + */ + Geom::Affine i2doc_affine() const; + + /** + * Returns the transformation from item to desktop coords + */ + Geom::Affine i2dt_affine() const; + + void set_i2d_affine(Geom::Affine const &transform); + + /** + * should rather be named "sp_item_d2i_affine" to match "sp_item_i2d_affine" (or vice versa). + */ + Geom::Affine dt2i_affine() const; + + guint32 _highlightColor; + + bool isExpanded() const { return _is_expanded; } + void setExpanded(bool expand) { _is_expanded = expand; } + +private: + enum EvaluatedStatus + { + StatusUnknown, + StatusCalculated, + StatusSet + }; + + mutable bool _is_evaluated; + mutable EvaluatedStatus _evaluated_status; + + void clip_ref_changed(SPObject *old_clip, SPObject *clip); + void mask_ref_changed(SPObject *old_mask, SPObject *mask); + void fill_ps_ref_changed(SPObject *old_ps, SPObject *ps); + void stroke_ps_ref_changed(SPObject *old_ps, SPObject *ps); + void filter_ref_changed(SPObject *old_obj, SPObject *obj); + +public: + void rotate_rel(Geom::Rotate const &rotation); + void scale_rel(Geom::Scale const &scale); + void skew_rel(double skewX, double skewY); + void move_rel( Geom::Translate const &tr); + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + virtual Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const; + virtual void print(SPPrintContext *ctx); + virtual const char* typeName() const; + virtual const char* displayName() const; + virtual char* description() const; + virtual Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags); + virtual void hide(unsigned int key); + virtual void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const; + virtual Geom::Affine set_transform(Geom::Affine const &transform); + + virtual void convert_to_guides() const; + + virtual int event(SPEvent *event); +}; + +// Utility + +/** + * @pre \a ancestor really is an ancestor (\>=) of \a object, or NULL. + * ("Ancestor (\>=)" here includes as far as \a object itself.) + */ +Geom::Affine i2anc_affine(SPObject const *item, SPObject const *ancestor); + +Geom::Affine i2i_affine(SPObject const *src, SPObject const *dest); + +Geom::Affine sp_item_transform_repr (SPItem *item); + +/* fixme: - these are evil, but OK */ + +int sp_item_repr_compare_position(SPItem const *first, SPItem const *second); + +inline bool sp_item_repr_compare_position_bool(SPObject const *first, SPObject const *second) +{ + return sp_repr_compare_position(first->getRepr(), + second->getRepr())<0; +} + +SPItem *sp_item_first_item_child (SPObject *obj); +SPItem const *sp_item_first_item_child (SPObject const *obj); + +#endif // SEEN_SP_ITEM_H + +/* + 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 : diff --git a/src/object/sp-line.cpp b/src/object/sp-line.cpp new file mode 100644 index 0000000..d12f1a2 --- /dev/null +++ b/src/object/sp-line.cpp @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <line> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "style.h" +#include "sp-line.h" +#include "sp-guide.h" +#include "display/curve.h" +#include <glibmm/i18n.h> +#include "document.h" +#include "inkscape.h" + +SPLine::SPLine() : SPShape() { + this->x1.unset(); + this->y1.unset(); + this->x2.unset(); + this->y2.unset(); +} + +SPLine::~SPLine() = default; + +void SPLine::build(SPDocument * document, Inkscape::XML::Node * repr) { + SPShape::build(document, repr); + + this->readAttr(SPAttr::X1); + this->readAttr(SPAttr::Y1); + this->readAttr(SPAttr::X2); + this->readAttr(SPAttr::Y2); +} + +void SPLine::set(SPAttr key, const gchar* value) { + /* fixme: we should really collect updates */ + + switch (key) { + case SPAttr::X1: + this->x1.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::Y1: + this->y1.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::X2: + this->x2.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::Y2: + this->y2.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPShape::set(key, value); + break; + } +} + +void SPLine::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + SPStyle const *style = this->style; + SPItemCtx const *ictx = (SPItemCtx const *) ctx; + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = em * 0.5; // fixme: get from pango or libnrtype. + + this->x1.update(em, ex, w); + this->x2.update(em, ex, w); + this->y1.update(em, ex, h); + this->y2.update(em, ex, h); + + this->set_shape(); + } + + SPShape::update(ctx, flags); +} + +Inkscape::XML::Node* SPLine::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:line"); + } + + if (repr != this->getRepr()) { + repr->mergeFrom(this->getRepr(), "id"); + } + + repr->setAttributeSvgDouble("x1", this->x1.computed); + repr->setAttributeSvgDouble("y1", this->y1.computed); + repr->setAttributeSvgDouble("x2", this->x2.computed); + repr->setAttributeSvgDouble("y2", this->y2.computed); + + SPShape::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPLine::typeName() const { + return "path"; +} + +const char* SPLine::displayName() const { + return _("Line"); +} + +void SPLine::convert_to_guides() const { + Geom::Point points[2]; + Geom::Affine const i2dt(this->i2dt_affine()); + + points[0] = Geom::Point(this->x1.computed, this->y1.computed)*i2dt; + points[1] = Geom::Point(this->x2.computed, this->y2.computed)*i2dt; + + SPGuide::createSPGuide(this->document, points[0], points[1]); +} + + +Geom::Affine SPLine::set_transform(Geom::Affine const &transform) { + Geom::Point points[2]; + + points[0] = Geom::Point(this->x1.computed, this->y1.computed); + points[1] = Geom::Point(this->x2.computed, this->y2.computed); + + points[0] *= transform; + points[1] *= transform; + + this->x1.computed = points[0][Geom::X]; + this->y1.computed = points[0][Geom::Y]; + this->x2.computed = points[1][Geom::X]; + this->y2.computed = points[1][Geom::Y]; + + this->adjust_stroke(transform.descrim()); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + + return Geom::identity(); +} + +void SPLine::set_shape() { + SPCurve c; + + c.moveto(this->x1.computed, this->y1.computed); + c.lineto(this->x2.computed, this->y2.computed); + + // *_insync does not call update, avoiding infinite recursion when set_shape is called by update + setCurveInsync(std::move(c)); + setCurveBeforeLPE(curve()); + + // LPE's cannot be applied to lines. (the result can (generally) not be represented as SPLine) +} + +/* + 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 : diff --git a/src/object/sp-line.h b/src/object/sp-line.h new file mode 100644 index 0000000..6bfe63f --- /dev/null +++ b/src/object/sp-line.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_LINE_H +#define SEEN_SP_LINE_H + +/* + * SVG <line> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "svg/svg-length.h" +#include "sp-shape.h" + +class SPLine final : public SPShape { +public: + SPLine(); + ~SPLine() override; + int tag() const override { return tag_of<decltype(*this)>; } + + SVGLength x1; + SVGLength y1; + SVGLength x2; + SVGLength y2; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void set(SPAttr key, char const* value) override; + + const char* typeName() const override; + const char* displayName() const override; + Geom::Affine set_transform(Geom::Affine const &transform) override; + void convert_to_guides() const override; + void update(SPCtx* ctx, unsigned int flags) override; + + void set_shape() override; +}; + +#endif // SEEN_SP_LINE_H +/* + 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 : diff --git a/src/object/sp-linear-gradient.cpp b/src/object/sp-linear-gradient.cpp new file mode 100644 index 0000000..829c8f6 --- /dev/null +++ b/src/object/sp-linear-gradient.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cairo.h> + +#include "sp-linear-gradient.h" + +#include "attributes.h" +#include "style.h" +#include "xml/repr.h" + +#include "display/drawing-paintserver.h" + +/* + * Linear Gradient + */ +SPLinearGradient::SPLinearGradient() : SPGradient() { + this->x1.unset(SVGLength::PERCENT, 0.0, 0.0); + this->y1.unset(SVGLength::PERCENT, 0.0, 0.0); + this->x2.unset(SVGLength::PERCENT, 1.0, 1.0); + this->y2.unset(SVGLength::PERCENT, 0.0, 0.0); +} + +SPLinearGradient::~SPLinearGradient() = default; + +void SPLinearGradient::build(SPDocument *document, Inkscape::XML::Node *repr) { + this->readAttr(SPAttr::X1); + this->readAttr(SPAttr::Y1); + this->readAttr(SPAttr::X2); + this->readAttr(SPAttr::Y2); + + SPGradient::build(document, repr); +} + +/** + * Callback: set attribute. + */ +void SPLinearGradient::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::X1: + this->x1.readOrUnset(value, SVGLength::PERCENT, 0.0, 0.0); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::Y1: + this->y1.readOrUnset(value, SVGLength::PERCENT, 0.0, 0.0); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::X2: + this->x2.readOrUnset(value, SVGLength::PERCENT, 1.0, 1.0); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::Y2: + this->y2.readOrUnset(value, SVGLength::PERCENT, 0.0, 0.0); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPGradient::set(key, value); + break; + } +} + +void +SPLinearGradient::update(SPCtx *ctx, guint flags) +{ + // To do: Verify flags. + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx); + + if (getUnits() == SP_GRADIENT_UNITS_USERSPACEONUSE) { + double w = ictx->viewport.width(); + double h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + this->x1.update(em, ex, w); + this->y1.update(em, ex, h); + this->x2.update(em, ex, w); + this->y2.update(em, ex, h); + } + } +} + +/** + * Callback: write attributes to associated repr. + */ +Inkscape::XML::Node* SPLinearGradient::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:linearGradient"); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->x1._set) { + repr->setAttributeSvgDouble("x1", this->x1.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->y1._set) { + repr->setAttributeSvgDouble("y1", this->y1.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->x2._set) { + repr->setAttributeSvgDouble("x2", this->x2.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->y2._set) { + repr->setAttributeSvgDouble("y2", this->y2.computed); + } + + SPGradient::write(xml_doc, repr, flags); + + return repr; +} + +std::unique_ptr<Inkscape::DrawingPaintServer> SPLinearGradient::create_drawing_paintserver() +{ + ensureVector(); + return std::make_unique<Inkscape::DrawingLinearGradient>(getSpread(), getUnits(), gradientTransform, + x1.computed, y1.computed, x2.computed, y2.computed, vector.stops); +} + +/* + 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 : diff --git a/src/object/sp-linear-gradient.h b/src/object/sp-linear-gradient.h new file mode 100644 index 0000000..b01d999 --- /dev/null +++ b/src/object/sp-linear-gradient.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_LINEAR_GRADIENT_H +#define SP_LINEAR_GRADIENT_H + +/** \file + * SPLinearGradient: SVG <lineargradient> implementation + */ + +#include "sp-gradient.h" +#include "svg/svg-length.h" + +/** Linear gradient. */ +class SPLinearGradient final + : public SPGradient +{ +public: + SPLinearGradient(); + ~SPLinearGradient() override; + int tag() const override { return tag_of<decltype(*this)>; } + + SVGLength x1; + SVGLength y1; + SVGLength x2; + SVGLength y2; + + std::unique_ptr<Inkscape::DrawingPaintServer> create_drawing_paintserver() override; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + void update(SPCtx *ctx, guint flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif /* !SP_LINEAR_GRADIENT_H */ + +/* + 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 : diff --git a/src/object/sp-lpe-item.cpp b/src/object/sp-lpe-item.cpp new file mode 100755 index 0000000..7a8f9d4 --- /dev/null +++ b/src/object/sp-lpe-item.cpp @@ -0,0 +1,1793 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Base class for live path effect items + */ +/* + * Authors: + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Bastien Bouclet <bgkweb@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +#endif + +#include <glibmm/i18n.h> + +#include "bad-uri-exception.h" + +#include "attributes.h" +#include "desktop.h" +#include "display/curve.h" +#include "inkscape.h" +#include "live_effects/effect.h" +#include "live_effects/lpe-bool.h" +#include "live_effects/lpe-clone-original.h" +#include "live_effects/lpe-copy_rotate.h" +#include "live_effects/lpe-lattice2.h" +#include "live_effects/lpe-measure-segments.h" +#include "live_effects/lpe-slice.h" +#include "live_effects/lpe-mirror_symmetry.h" +#include "live_effects/lpe-tiling.h" +#include "message-stack.h" +#include "path-chemistry.h" +#include "sp-clippath.h" +#include "sp-ellipse.h" +#include "sp-spiral.h" +#include "sp-star.h" +#include "sp-item-group.h" +#include "sp-mask.h" +#include "sp-path.h" +#include "sp-rect.h" +#include "sp-root.h" +#include "sp-symbol.h" +#include "svg/svg.h" +#include "ui/shape-editor.h" +#include "uri.h" + +/* LPEItem base class */ + +static void lpeobject_ref_modified(SPObject *href, guint flags, SPLPEItem *lpeitem); +static void sp_lpe_item_create_original_path_recursive(SPLPEItem *lpeitem); +static SPLPEItem * sp_lpe_item_cleanup_original_path_recursive(SPLPEItem *lpeitem, bool keep_paths, bool force = false, bool is_clip_mask = false); + +typedef std::list<std::string> HRefList; +static std::string patheffectlist_svg_string(PathEffectList const & list); +static std::string hreflist_svg_string(HRefList const & list); + +namespace { + void clear_path_effect_list(PathEffectList* const l) { + PathEffectList::iterator it = l->begin(); + while ( it != l->end()) { + (*it)->unlink(); + //delete *it; + it = l->erase(it); + } + } +} + +SPLPEItem::SPLPEItem() + : SPItem() + , path_effects_enabled(1) + , path_effect_list(new PathEffectList()) + , lpe_modified_connection_list(new std::list<sigc::connection>()) + , current_path_effect(nullptr) + , lpe_helperpaths() +{ +} + +SPLPEItem::~SPLPEItem() = default; + +void SPLPEItem::build(SPDocument *document, Inkscape::XML::Node *repr) { + this->readAttr(SPAttr::INKSCAPE_PATH_EFFECT); + onsymbol = isOnSymbol(); + SPItem::build(document, repr); +} + +void SPLPEItem::release() { + // disconnect all modified listeners: + + for (auto & mod_it : *this->lpe_modified_connection_list) + { + mod_it.disconnect(); + } + + delete this->lpe_modified_connection_list; + this->lpe_modified_connection_list = nullptr; + + clear_path_effect_list(this->path_effect_list); + // delete the list itself + delete this->path_effect_list; + this->path_effect_list = nullptr; + + SPItem::release(); +} + +void SPLPEItem::set(SPAttr key, gchar const* value) { + switch (key) { + case SPAttr::INKSCAPE_PATH_EFFECT: + { + this->current_path_effect = nullptr; + + // Disable the path effects while populating the LPE list + sp_lpe_item_enable_path_effects(this, false); + + // disconnect all modified listeners: + for (auto & mod_it : *this->lpe_modified_connection_list) + { + mod_it.disconnect(); + } + + this->lpe_modified_connection_list->clear(); + clear_path_effect_list(this->path_effect_list); + + // Parse the contents of "value" to rebuild the path effect reference list + if ( value ) { + std::istringstream iss(value); + std::string href; + + while (std::getline(iss, href, ';')) + { + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference>path_effect_ref = std::make_shared<Inkscape::LivePathEffect::LPEObjectReference>(this); + + try { + path_effect_ref->link(href.c_str()); + } catch (Inkscape::BadURIException &e) { + g_warning("BadURIException when trying to find LPE: %s", e.what()); + path_effect_ref->unlink(); + //delete path_effect_ref; + path_effect_ref = nullptr; + } + + this->path_effect_list->push_back(path_effect_ref); + + if ( path_effect_ref->lpeobject && path_effect_ref->lpeobject->get_lpe() ) { + // connect modified-listener + this->lpe_modified_connection_list->push_back( + path_effect_ref->lpeobject->connectModified(sigc::bind(sigc::ptr_fun(&lpeobject_ref_modified), this)) ); + } else { + // on clipboard we fix refs so in middle time of the operation, in LPE with multiples path + // effects can result middle updata and fire a warning, so we silent it + if (!isOnClipboard()) { + // something has gone wrong in finding the right patheffect. + g_warning("Unknown LPE type specified, LPE stack effectively disabled"); + // keep the effect in the lpestack, so the whole stack is effectively disabled but + // maintained + } + } + } + } + + sp_lpe_item_enable_path_effects(this, true); + } + break; + + default: + SPItem::set(key, value); + break; + } +} + +void SPLPEItem::update(SPCtx* ctx, unsigned int flags) { + SPItem::update(ctx, flags); + + // update the helperpaths of all LPEs applied to the item + // TODO: re-add for the new node tool +} + +void SPLPEItem::modified(unsigned int flags) { + //stop update when modified and make the effect update on the LPE transform method if the effect require it + //if (is<SPGroup>(this) && (flags & SP_OBJECT_MODIFIED_FLAG) && (flags & SP_OBJECT_USER_MODIFIED_FLAG_B)) { + // sp_lpe_item_update_patheffect(this, true, false); + //} + if (document->isSeeking()) { + auto lpes = this->getPathEffects(); + if (!lpes.empty()) { + lpes[0]->on_undo = true; + for (auto lpe :lpes) { + lpe->setLPEAction(Inkscape::LivePathEffect::LPE_UPDATE); + } + } + } +} + +Inkscape::XML::Node* SPLPEItem::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_EXT) { + if ( hasPathEffect() ) { + repr->setAttributeOrRemoveIfEmpty("inkscape:path-effect", patheffectlist_svg_string(*this->path_effect_list)); + } else { + repr->removeAttribute("inkscape:path-effect"); + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + +/** + * The lpeitem is on clipboard + */ +bool SPLPEItem::isOnClipboard() +{ + Inkscape::XML::Node *root = document->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + return clipnode != nullptr; +} + +bool SPLPEItem::isOnSymbol() const { + auto p = cast<SPLPEItem>(parent); + return (p && p->onsymbol) || is<SPSymbol>(this); +} +/** + * returns true when LPE was successful. + */ +bool SPLPEItem::performPathEffect(SPCurve *curve, SPShape *current, bool is_clip_or_mask) { + + if (!curve) { + return false; + } + + if (this->hasPathEffect() && this->pathEffectsEnabled()) { + PathEffectList path_effect_list(*this->path_effect_list); + size_t path_effect_list_size = path_effect_list.size(); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj) { + /** \todo Investigate the cause of this. + * For example, this happens when copy pasting an object with LPE applied. Probably because the object is pasted while the effect is not yet pasted to defs, and cannot be found. + */ + g_warning("SPLPEItem::performPathEffect - NULL lpeobj in list!"); + return false; + } + + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (!lpe || !performOnePathEffect(curve, current, lpe, is_clip_or_mask)) { + return false; + } + auto hreflist = lpeobj->hrefList; + if (hreflist.size()) { // lpe can be removed on perform (eg: clone lpe on copy) + if (path_effect_list_size != this->path_effect_list->size()) { + break; + } + } + } + } + return true; +} + +/** + * returns true when LPE was successful. + */ +bool SPLPEItem::performOnePathEffect(SPCurve *curve, SPShape *current, Inkscape::LivePathEffect::Effect *lpe, bool is_clip_or_mask) { + if (!lpe) { + /** \todo Investigate the cause of this. + * Not sure, but I think this can happen when an unknown effect type is specified... + */ + g_warning("SPLPEItem::performPathEffect - lpeobj with invalid lpe in the stack!"); + return false; + } + if (document->isSeeking()) { + lpe->refresh_widgets = true; + } + if (lpe->isVisible()) { + if (lpe->acceptsNumClicks() > 0 && !lpe->isReady()) { + // if the effect expects mouse input before being applied and the input is not finished + // yet, we don't alter the path + return false; + } + //if is not clip or mask or LPE apply to clip and mask + if (!is_clip_or_mask || lpe->apply_to_clippath_and_mask) { + // Uncomment to get updates + // g_debug("LPE running:: %s",Inkscape::LivePathEffect::LPETypeConverter.get_key(lpe->effectType()).c_str()); + lpe->setCurrentShape(current); + if (!is<SPGroup>(this)) { + lpe->pathvector_before_effect = curve->get_pathvector(); + } + // To Calculate BBox on shapes and nested LPE + current->setCurveInsync(curve); + // Groups have their doBeforeEffect called elsewhere + if (lpe->lpeversion.param_getSVGValue() != "0") { // we are on 1 or up + current->bbox_vis_cache_is_valid = false; + current->bbox_geom_cache_is_valid = false; + } + auto group = cast<SPGroup>(this); + if (!group && !is_clip_or_mask) { + lpe->doBeforeEffect_impl(this); + } + + try { + lpe->doEffect(curve); + lpe->has_exception = false; + } + + catch (std::exception & e) { + g_warning("Exception during LPE %s execution. \n %s", lpe->getName().c_str(), e.what()); + if (SP_ACTIVE_DESKTOP && SP_ACTIVE_DESKTOP->messageStack()) { + SP_ACTIVE_DESKTOP->messageStack()->flash( Inkscape::WARNING_MESSAGE, + _("An exception occurred during execution of the Path Effect.") ); + } + lpe->doOnException(this); + return false; + } + + if (!group) { + // To have processed the shape to doAfterEffect + current->setCurveInsync(curve); + if (curve) { + lpe->pathvector_after_effect = curve->get_pathvector(); + } + lpe->doAfterEffect_impl(this, curve); + } + // we need this on slice LPE to calculate effects correctly + if (dynamic_cast<Inkscape::LivePathEffect::LPESlice*>(lpe)) { // we are on 1 or up + current->bbox_vis_cache_is_valid = false; + current->bbox_geom_cache_is_valid = false; + } + } + } + return true; +} + +/** + * returns false when LPE write unoptimiced + */ +bool SPLPEItem::optimizeTransforms() +{ + if (is<SPGroup>(this)) { + return false; + } + + if (is<SPSpiral>(this) && !this->transform.isUniformScale()) { + return false; + } + if (is<SPStar>(this) && !this->transform.isUniformScale()) { + return false; + } + auto* mask_path = this->getMaskObject(); + if(mask_path) { + return false; + } + auto* clip_path = this->getClipObject(); + if(clip_path) { + return false; + } + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + if (!lperef) { + continue; + } + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (lpe) { + if (dynamic_cast<Inkscape::LivePathEffect::LPEMeasureSegments*>(lpe) || + dynamic_cast<Inkscape::LivePathEffect::LPELattice2*>(lpe)) + { + return false; + } + } + } + } + + if (unoptimized()) { + return false; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + return !prefs->getBool("/options/preservetransform/value", false); +} + +/** + * notify tranbsform applied to a LPE + */ +void SPLPEItem::notifyTransform(Geom::Affine const &postmul) +{ + if (!pathEffectsEnabled()) + return; + + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + if (!lperef) { + continue; + } + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (lpe && !lpe->is_load) { + lpe->transform_multiply_impl(postmul, this); + } + } + } +} + +// CPPIFY: make pure virtual +void SPLPEItem::update_patheffect(bool /*write*/) { + //throw; +} + +/** + * Calls any registered handlers for the update_patheffect action + */ +void +sp_lpe_item_update_patheffect (SPLPEItem *lpeitem, bool wholetree, bool write, bool with_satellites) +{ +#ifdef SHAPE_VERBOSE + g_message("sp_lpe_item_update_patheffect: %p\n", lpeitem); +#endif + g_return_if_fail (lpeitem != nullptr); + + // Do not check for LPE item to allow LPE work on clips/mask + if (!lpeitem->pathEffectsEnabled()) + return; + + SPLPEItem *top = nullptr; + + if (wholetree) { + SPLPEItem *prev_parent = lpeitem; + auto parent = cast<SPLPEItem>(prev_parent->parent); + while (parent && parent->hasPathEffectRecursive()) { + prev_parent = parent; + parent = cast<SPLPEItem>(prev_parent->parent); + } + top = prev_parent; + } + else { + top = lpeitem; + } + top->update_patheffect(write); + if (with_satellites) { + top->update_satellites(); + } +} + +/** + * Gets called when any of the lpestack's lpeobject repr contents change: i.e. parameter change in any of the stacked LPEs + */ +static void +lpeobject_ref_modified(SPObject */*href*/, guint flags, SPLPEItem *lpeitem) +{ +#ifdef SHAPE_VERBOSE + g_message("lpeobject_ref_modified"); +#endif + if (flags != 29 && flags != 253 && !(flags & SP_OBJECT_STYLESHEET_MODIFIED_FLAG)) + { + sp_lpe_item_update_patheffect(lpeitem, true, true); + } +} + +static void +sp_lpe_item_create_original_path_recursive(SPLPEItem *lpeitem) +{ + g_return_if_fail(lpeitem != nullptr); + + SPClipPath *clip_path = lpeitem->getClipObject(); + if(clip_path) { + std::vector<SPObject*> clip_path_list = clip_path->childList(true); + for (auto iter : clip_path_list) { + auto clip_data = cast<SPLPEItem>(iter); + sp_lpe_item_create_original_path_recursive(clip_data); + sp_object_unref(iter); + } + } + + SPMask *mask_path = lpeitem->getMaskObject(); + if(mask_path) { + std::vector<SPObject*> mask_path_list = mask_path->childList(true); + for (auto iter : mask_path_list) { + auto mask_data = cast<SPLPEItem>(iter); + sp_lpe_item_create_original_path_recursive(mask_data); + sp_object_unref(iter); + } + } + if (is<SPGroup>(lpeitem)) { + std::vector<SPItem*> item_list = cast<SPGroup>(lpeitem)->item_list(); + for (auto subitem : item_list) { + if (is<SPLPEItem>(subitem)) { + sp_lpe_item_create_original_path_recursive(cast<SPLPEItem>(subitem)); + } + } + } else if (auto path = cast<SPPath>(lpeitem)) { + if (!path->getAttribute("inkscape:original-d") ) { + if (gchar const * value = path->getAttribute("d")) { + path->setAttribute("inkscape:original-d", value); + } + } + } else if (auto shape = cast<SPShape>(lpeitem)) { + if (!shape->curveBeforeLPE()) { + shape->setCurveBeforeLPE(shape->curve()); + } + } +} + +static SPLPEItem * +sp_lpe_item_cleanup_original_path_recursive(SPLPEItem *lpeitem, bool keep_paths, bool force, bool is_clip_mask) +{ + if (!lpeitem) { + return nullptr; + } + auto group = cast<SPGroup>(lpeitem); + auto shape = cast<SPShape>(lpeitem); + auto path = cast<SPPath>(lpeitem); + SPClipPath *clip_path = lpeitem->getClipObject(); + if(clip_path) { + std::vector<SPObject*> clip_path_list = clip_path->childList(true); + for (auto iter : clip_path_list) { + auto clip_data = cast<SPLPEItem>(iter); + if (clip_data) { + sp_lpe_item_cleanup_original_path_recursive(clip_data, keep_paths, lpeitem && !lpeitem->hasPathEffectRecursive(), true); + } + sp_object_unref(iter); + } + } + + SPMask *mask_path = lpeitem->getMaskObject(); + if(mask_path) { + std::vector<SPObject*> mask_path_list = mask_path->childList(true); + for (auto iter : mask_path_list) { + auto mask_data = cast<SPLPEItem>(iter); + if (mask_data) { + sp_lpe_item_cleanup_original_path_recursive(mask_data, keep_paths, lpeitem && !lpeitem->hasPathEffectRecursive(), true); + } + sp_object_unref(iter); + } + } + + if (group) { + std::vector<SPItem*> item_list = cast<SPGroup>(lpeitem)->item_list(); + for (auto iter : item_list) { + auto subitem = cast<SPLPEItem>(iter); + if (subitem) { + sp_lpe_item_cleanup_original_path_recursive(subitem, keep_paths); + } + } + } else if (path) { + Inkscape::XML::Node *repr = lpeitem->getRepr(); + if (repr->attribute("inkscape:original-d") && + !lpeitem->hasPathEffectRecursive() && + (!is_clip_mask || + ( is_clip_mask && force))) + { + if (!keep_paths) { + repr->setAttribute("d", repr->attribute("inkscape:original-d")); + } + repr->removeAttribute("inkscape:original-d"); + path->setCurveBeforeLPE(nullptr); + if (!(shape->curve()->get_segment_count())) { + repr->parent()->removeChild(repr); + } + } else { + if (!keep_paths) { + sp_lpe_item_update_patheffect(lpeitem, true, true); + } + } + } else if (shape) { + Inkscape::XML::Node *repr = lpeitem->getRepr(); + SPCurve const *c_lpe = shape->curve(); + Glib::ustring d_str; + if (c_lpe) { + d_str = sp_svg_write_path(c_lpe->get_pathvector()); + } else if (shape->getAttribute("d")) { + d_str = shape->getAttribute("d"); + } else { + return lpeitem; + } + if (!lpeitem->hasPathEffectRecursive() && + (!is_clip_mask || + ( is_clip_mask && force))) + { + if (!keep_paths) { + repr->removeAttribute("d"); + shape->setCurveBeforeLPE(nullptr); + } else { + const char * id = repr->attribute("id"); + const char * style = repr->attribute("style"); + // remember the position of the item + gint pos = shape->getRepr()->position(); + // remember parent + Inkscape::XML::Node *parent = shape->getRepr()->parent(); + // remember class + char const *class_attr = shape->getRepr()->attribute("class"); + // remember title + gchar *title = shape->title(); + // remember description + gchar *desc = shape->desc(); + // remember transformation + gchar const *transform_str = shape->getRepr()->attribute("transform"); + // Mask + gchar const *mask_str = (gchar *) shape->getRepr()->attribute("mask"); + // Clip path + gchar const *clip_str = (gchar *) shape->getRepr()->attribute("clip-path"); + + /* Rotation center */ + gchar const *transform_center_x = shape->getRepr()->attribute("inkscape:transform-center-x"); + gchar const *transform_center_y = shape->getRepr()->attribute("inkscape:transform-center-y"); + + // It's going to resurrect, so we delete without notifying listeners. + SPDocument * doc = shape->document; + shape->deleteObject(false); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + // restore id + repr->setAttribute("id", id); + // restore class + repr->setAttribute("class", class_attr); + // restore transform + repr->setAttribute("transform", transform_str); + // restore clip + repr->setAttribute("clip-path", clip_str); + // restore mask + repr->setAttribute("mask", mask_str); + // restore transform_center_x + repr->setAttribute("inkscape:transform-center-x", transform_center_x); + // restore transform_center_y + repr->setAttribute("inkscape:transform-center-y", transform_center_y); + //restore d + repr->setAttribute("d", d_str); + //restore style + repr->setAttribute("style", style); + // add the new repr to the parent + parent->appendChild(repr); + SPObject* newObj = doc->getObjectByRepr(repr); + if (title && newObj) { + newObj->setTitle(title); + g_free(title); + } + if (desc && newObj) { + newObj->setDesc(desc); + g_free(desc); + } + // move to the saved position + repr->setPosition(pos > 0 ? pos : 0); + Inkscape::GC::release(repr); + lpeitem = cast<SPLPEItem>(newObj); + } + } else { + if (!keep_paths) { + sp_lpe_item_update_patheffect(lpeitem, true, true); + } + } + } + if (lpeitem->getRepr() && !lpeitem->getAttribute("inkscape:path-effect") && lpeitem->path_effect_list) { + clear_path_effect_list(lpeitem->path_effect_list); + } + return lpeitem; +} + + + +void SPLPEItem::addPathEffect(std::string value, bool reset) +{ + if (!value.empty()) { + // Apply the path effects here because in the casse of a group, lpe->resetDefaults + // needs that all the subitems have their effects applied + auto group = cast<SPGroup>(this); + if (group) { + sp_lpe_item_update_patheffect(this, false, true); + } + // Disable the path effects while preparing the new lpe + sp_lpe_item_enable_path_effects(this, false); + + // Add the new reference to the list of LPE references + HRefList hreflist; + for (PathEffectList::const_iterator it = this->path_effect_list->begin(); it != this->path_effect_list->end(); ++it) + { + hreflist.push_back( std::string((*it)->lpeobject_href) ); + } + hreflist.push_back(value); // C++11: should be emplace_back std::move'd (also the reason why passed by value to addPathEffect) + + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", hreflist_svg_string(hreflist)); + // Make sure that ellipse is stored as <svg:path> + if( is<SPGenericEllipse>(this)) { + cast<SPGenericEllipse>(this)->write( this->getRepr()->document(), this->getRepr(), SP_OBJECT_WRITE_EXT ); + } + + + LivePathEffectObject *lpeobj = this->path_effect_list->back()->lpeobject; + if (lpeobj && lpeobj->get_lpe()) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + // Ask the path effect to reset itself if it doesn't have parameters yet + if (reset) { + // has to be called when all the subitems have their lpes applied + lpe->resetDefaults(this); + } + // Moved here to fix #1299461, we can call previous function twice after + // if anyone find necessary + // make sure there is an original-d for paths!!! + sp_lpe_item_create_original_path_recursive(this); + // perform this once when the effect is applied + lpe->doOnApply_impl(this); + } + + //Enable the path effects now that everything is ready to apply the new path effect + sp_lpe_item_enable_path_effects(this, true); + + // Apply the path effect + sp_lpe_item_update_patheffect(this, true, true); + } +} + +void SPLPEItem::addPathEffect(LivePathEffectObject * new_lpeobj) +{ + const gchar * repr_id = new_lpeobj->getRepr()->attribute("id"); + gchar *hrefstr = g_strdup_printf("#%s", repr_id); + this->addPathEffect(hrefstr, false); + g_free(hrefstr); +} + +/** + * If keep_path is true, the item should not be updated, effectively 'flattening' the LPE. + */ +SPLPEItem * SPLPEItem::removeCurrentPathEffect(bool keep_paths) +{ + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef = this->getCurrentLPEReference(); + if (!lperef) { + return nullptr; + } + if (Inkscape::LivePathEffect::Effect* effect_ = this->getCurrentLPE()) { + effect_->keep_paths = keep_paths; + effect_->on_remove_all = false; + effect_->doOnRemove_impl(this); + } + this->path_effect_list->remove(lperef); //current lpe ref is always our 'own' pointer from the path_effect_list + //effect_->getLPEObj()->hrefList.clear(); + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", patheffectlist_svg_string(*this->path_effect_list)); + if (!keep_paths) { + // Make sure that ellipse is stored as <svg:circle> or <svg:ellipse> if possible. + if (auto ell = cast<SPGenericEllipse>(this)) { + ell->write(getRepr()->document(), getRepr(), SP_OBJECT_WRITE_EXT); + } + } + return sp_lpe_item_cleanup_original_path_recursive(this, keep_paths); +} + +/** + * If keep_path is true, the item should not be updated, effectively 'flattening' the LPE. + */ +SPLPEItem * SPLPEItem::removeAllPathEffects(bool keep_paths, bool recursive) +{ + if (recursive) { + auto grp = cast<SPGroup>(this); + if (grp) { + std::vector<SPItem *> item_list = grp->item_list(); + for (auto iter : item_list) { + auto subitem = cast<SPLPEItem>(iter); + if (subitem) { + subitem->removeAllPathEffects(keep_paths, recursive); + } + } + } + } + if (!hasPathEffect()) { + return nullptr; + } + if (keep_paths) { + if (path_effect_list->empty()) { + return nullptr; + } + } + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + if (!lperef) { + continue; + } + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect * lpe = lpeobj->get_lpe(); + if (lpe) { + lpe->keep_paths = keep_paths; + lpe->on_remove_all = true; + lpe->doOnRemove_impl(this); + } + lpeobj->hrefList.clear(); + } + } + clear_path_effect_list(this->path_effect_list); + this->removeAttribute("inkscape:path-effect"); + if (!keep_paths) { + // Make sure that ellipse is stored as <svg:circle> or <svg:ellipse> if possible. + if (auto ell = cast<SPGenericEllipse>(this)) { + ell->write(getRepr()->document(), getRepr(), SP_OBJECT_WRITE_EXT); + } + } + // SPItem can be changed on remove all LPE items (Shape to Path) We return generated item + return sp_lpe_item_cleanup_original_path_recursive(this, keep_paths); +} + +void SPLPEItem::downCurrentPathEffect() +{ + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef = getCurrentLPEReference(); + if (!lperef) + return; + PathEffectList new_list = *this->path_effect_list; + PathEffectList::iterator cur_it = find( new_list.begin(), new_list.end(), lperef ); + if (cur_it != new_list.end()) { + PathEffectList::iterator down_it = cur_it; + ++down_it; + if (down_it != new_list.end()) { // perhaps current effect is already last effect + std::iter_swap(cur_it, down_it); + } + } + + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", patheffectlist_svg_string(new_list)); + + sp_lpe_item_cleanup_original_path_recursive(this, false); +} + +void SPLPEItem::duplicateCurrentPathEffect() +{ + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef = getCurrentLPEReference(); + if (!lperef) + return; + + HRefList hreflist; + PathEffectList::const_iterator cur_it = find( this->path_effect_list->begin(), this->path_effect_list->end(), lperef ); + PathEffectList path_effect_list(*this->path_effect_list); + for (PathEffectList::const_iterator it = this->path_effect_list->begin(); it != this->path_effect_list->end(); ++it) { + hreflist.push_back(std::string((*it)->lpeobject_href) ); + LivePathEffectObject *lpeobj = (*it)->lpeobject; + if (it == cur_it) { + auto *duple = lpeobj->fork_private_if_necessary(0); + hreflist.push_back(std::string("#") + std::string(duple->getId())); + } + } + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", hreflist_svg_string(hreflist)); + + sp_lpe_item_cleanup_original_path_recursive(this, false); + update_satellites(true); +} + +SPLPEItem *SPLPEItem::flattenCurrentPathEffect() +{ + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef = getCurrentLPEReference(); + if (!lperef) + return nullptr; + HRefList hreflist; + HRefList hreflist2; + PathEffectList::const_iterator cur_it = find( this->path_effect_list->begin(), this->path_effect_list->end(), lperef ); + PathEffectList path_effect_list(*this->path_effect_list); + bool done = false; + for (PathEffectList::const_iterator it = this->path_effect_list->begin(); it != this->path_effect_list->end(); ++it) { + if (done) { + hreflist2.push_back(std::string((*it)->lpeobject_href) ); + } else { + hreflist.push_back(std::string((*it)->lpeobject_href) ); + } + if (it == cur_it) { + done = true; + } + } + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", hreflist_svg_string(hreflist)); + sp_lpe_item_cleanup_original_path_recursive(this, false); + sp_lpe_item_update_patheffect(this, true, true); + auto lpeitem = removeAllPathEffects(true); + if ( hreflist2.size()) { + sp_lpe_item_enable_path_effects(lpeitem, false); + lpeitem->setAttributeOrRemoveIfEmpty("inkscape:path-effect", hreflist_svg_string(hreflist2)); + sp_lpe_item_create_original_path_recursive(lpeitem); + sp_lpe_item_enable_path_effects(lpeitem, true); + sp_lpe_item_update_patheffect(lpeitem, true, true); + lpeitem->update_satellites(true); + } + return lpeitem; +} + +void SPLPEItem::removePathEffect(Inkscape::LivePathEffect::Effect *lpe, bool keep_paths) +{ + PathEffectList path_effect_list(*this->path_effect_list); + bool exist = false; + if (!lpe) + return; + for (auto &lperef : path_effect_list) { + if (lperef->lpeobject == lpe->getLPEObj()) { + setCurrentPathEffect(lperef); + exist = true; + break; + } + } + if (exist) { + removeCurrentPathEffect(keep_paths); + } else { + g_warning("LPE dont exist to remove"); + } +} + +void SPLPEItem::movePathEffect(gint origin, gint dest, bool select_moved) +{ + PathEffectList new_list = *this->path_effect_list; + auto lpe = getCurrentLPE(); + if (!lpe) + return; + + LivePathEffectObject *lpeobj = lpe->getLPEObj(); + if (lpeobj) { + size_t nlpe = new_list.size(); + if (!nlpe || + origin == dest || + origin > nlpe -1 || + dest > nlpe -1) + { + return; + } + gint selectme = 0; + std::list<std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference>>::iterator insertme = new_list.begin(); + std::list<std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference>>::iterator insertto = new_list.begin(); + std::advance(insertme, origin); + if (origin > dest) { + std::advance(insertto, dest); + selectme = dest; + } else { + std::advance(insertto, dest + 1); + selectme = dest + 1; + } + new_list.insert(insertto, *insertme); + std::list<std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference>>::iterator removeme = new_list.begin(); + if (origin > dest) { + std::advance(removeme, origin + 1); + } else { + std::advance(removeme, origin); + selectme = dest; + } + new_list.erase(removeme); + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", patheffectlist_svg_string(new_list)); + sp_lpe_item_cleanup_original_path_recursive(this, false); + std::list<std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference>>::iterator select = this->path_effect_list->begin(); + std::advance(select, selectme); + if (select_moved) { + setCurrentPathEffect(*select); + } else { + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + if (lperef->lpeobject == lpeobj) { + setCurrentPathEffect(lperef); + break; + } + } + } + } +} + + +void SPLPEItem::upCurrentPathEffect() +{ + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef = getCurrentLPEReference(); + if (!lperef) + return; + + PathEffectList new_list = *this->path_effect_list; + PathEffectList::iterator cur_it = find( new_list.begin(), new_list.end(), lperef ); + if (cur_it != new_list.end() && cur_it != new_list.begin()) { + PathEffectList::iterator up_it = cur_it; + --up_it; + std::iter_swap(cur_it, up_it); + } + + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", patheffectlist_svg_string(new_list)); + + sp_lpe_item_cleanup_original_path_recursive(this, false); +} + +void +SPLPEItem::update_satellites(bool recursive) { + if (path_effect_list->empty()) { + return; + } + auto grp = cast<SPGroup>(this); + if (recursive && grp) { + std::vector<SPItem *> item_list = grp->item_list(); + for (auto iter : item_list) { + auto subitem = cast<SPLPEItem>(iter); + if (subitem) { + subitem->update_satellites(recursive); + } + } + } + + // go through the list; if some are unknown or invalid, return true + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + if (auto *lpe = lpeobj->get_lpe()) { + lpe->update_satellites(); + } + } + } +} + +/** used for shapes so they can see if they should also disable shape calculation and read from d= */ +bool SPLPEItem::hasBrokenPathEffect() const +{ + if (path_effect_list->empty()) { + return false; + } + + // go through the list; if some are unknown or invalid, return true + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj || !lpeobj->get_lpe()) { + return true; + } + } + + return false; +} + +bool SPLPEItem::hasPathEffectOfTypeRecursive(int const type, bool is_ready) const +{ + auto parent_lpe_item = cast<SPLPEItem>(parent); + if (parent_lpe_item) { + return hasPathEffectOfType(type, is_ready) || parent_lpe_item->hasPathEffectOfTypeRecursive(type, is_ready); + } else { + return hasPathEffectOfType(type, is_ready); + } +} + +bool SPLPEItem::hasPathEffectOfType(int const type, bool is_ready) const +{ + if (path_effect_list->empty()) { + return false; + } + + for (PathEffectList::const_iterator it = path_effect_list->begin(); it != path_effect_list->end(); ++it) + { + LivePathEffectObject const *lpeobj = (*it)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect const* lpe = lpeobj->get_lpe(); + if (lpe && (lpe->effectType() == type)) { + if (is_ready || lpe->isReady()) { + return true; + } + } + } + } + + return false; +} + +/** + * returns true when any LPE apply to clip or mask. + */ +bool SPLPEItem::hasPathEffectOnClipOrMask(SPLPEItem * shape) const +{ + if (shape->hasPathEffectRecursive()) { + return true; + } + if (!path_effect_list || path_effect_list->empty()) { + return false; + } + + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj) { + continue; + } + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (lpe->apply_to_clippath_and_mask) { + return true; + } + } + return false; +} + +/** + * returns true when any LPE apply to clip or mask. recursive mode + */ +bool SPLPEItem::hasPathEffectOnClipOrMaskRecursive(SPLPEItem * shape) const +{ + auto parent_lpe_item = cast<SPLPEItem>(parent); + if (parent_lpe_item) { + return hasPathEffectOnClipOrMask(shape) || parent_lpe_item->hasPathEffectOnClipOrMaskRecursive(shape); + } + else { + return hasPathEffectOnClipOrMask(shape); + } +} + +bool SPLPEItem::hasPathEffect() const +{ + if (!path_effect_list || path_effect_list->empty()) { + return false; + } + + // go through the list; if some are unknown or invalid, we are not an LPE item! + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj || !lpeobj->get_lpe()) { + return false; + } + } + + return true; +} + +bool SPLPEItem::hasPathEffectRecursive() const +{ + auto parent_lpe_item = cast<SPLPEItem>(parent); + if (parent_lpe_item) { + return hasPathEffect() || parent_lpe_item->hasPathEffectRecursive(); + } + else { + return hasPathEffect(); + } +} + +/** + * returns top most LPE item with LPE + */ +SPLPEItem const * SPLPEItem::getTopPathEffect() const +{ + auto parent_lpe_item = cast<SPLPEItem>(parent); + if (parent_lpe_item && !hasPathEffectRecursive()) { + return hasPathEffect() ? parent_lpe_item : this; + } else { + return parent_lpe_item ? parent_lpe_item->getTopPathEffect() : this; + } +} + +void +SPLPEItem::resetClipPathAndMaskLPE(bool fromrecurse) +{ + if (fromrecurse) { + auto group = cast<SPGroup>(this); + auto shape = cast<SPShape>(this); + if (group) { + std::vector<SPItem*> item_list = group->item_list(); + for (auto iter2 : item_list) { + auto subitem = cast<SPLPEItem>(iter2); + if (subitem) { + subitem->resetClipPathAndMaskLPE(true); + } + } + } else if (shape) { + shape->setCurveInsync(shape->curveForEdit()); + if (!hasPathEffectOnClipOrMaskRecursive(shape)) { + shape->removeAttribute("inkscape:original-d"); + shape->setCurveBeforeLPE(nullptr); + } else { + // make sure there is an original-d for paths!!! + sp_lpe_item_create_original_path_recursive(shape); + } + } + return; + } + SPClipPath *clip_path = this->getClipObject(); + if(clip_path) { + std::vector<SPObject*> clip_path_list = clip_path->childList(true); + for (auto iter : clip_path_list) { + auto group = cast<SPGroup>(iter); + auto shape = cast<SPShape>(iter); + if (group) { + std::vector<SPItem*> item_list = group->item_list(); + for (auto iter2 : item_list) { + auto subitem = cast<SPLPEItem>(iter2); + if (subitem) { + subitem->resetClipPathAndMaskLPE(true); + } + } + } else if (shape) { + shape->setCurveInsync(shape->curveForEdit()); + if (!hasPathEffectOnClipOrMaskRecursive(shape)) { + shape->removeAttribute("inkscape:original-d"); + shape->setCurveBeforeLPE(nullptr); + } else { + // make sure there is an original-d for paths!!! + sp_lpe_item_create_original_path_recursive(shape); + } + } + sp_object_unref(iter); + } + } + SPMask *mask = this->getMaskObject(); + if(mask) { + std::vector<SPObject*> mask_list = mask->childList(true); + for (auto iter : mask_list) { + auto group = cast<SPGroup>(iter); + auto shape = cast<SPShape>(iter); + if (group) { + std::vector<SPItem*> item_list = group->item_list(); + for (auto iter2 : item_list) { + auto subitem = cast<SPLPEItem>(iter2); + if (subitem) { + subitem->resetClipPathAndMaskLPE(true); + } + } + } else if (shape) { + shape->setCurveInsync(shape->curveForEdit()); + if (!hasPathEffectOnClipOrMaskRecursive(shape)) { + shape->removeAttribute("inkscape:original-d"); + shape->setCurveBeforeLPE(nullptr); + } else { + // make sure there is an original-d for paths!!! + sp_lpe_item_create_original_path_recursive(shape); + } + } + sp_object_unref(iter); + } + } +} + +void +SPLPEItem::applyToClipPath(SPItem* to, Inkscape::LivePathEffect::Effect *lpe) +{ + if (lpe && !lpe->apply_to_clippath_and_mask) { + return; + } + SPClipPath *clip_path = to->getClipObject(); + if(clip_path) { + std::vector<SPObject*> clip_path_list = clip_path->childList(true); + for (auto clip_data : clip_path_list) { + applyToClipPathOrMask(cast<SPItem>(clip_data), to, lpe); + sp_object_unref(clip_data); + } + } +} + +void +SPLPEItem::applyToMask(SPItem* to, Inkscape::LivePathEffect::Effect *lpe) +{ + if (lpe && !lpe->apply_to_clippath_and_mask) { + return; + } + SPMask *mask = to->getMaskObject(); + if(mask) { + std::vector<SPObject*> mask_list = mask->childList(true); + for (auto mask_data : mask_list) { + applyToClipPathOrMask(cast<SPItem>(mask_data), to, lpe); + sp_object_unref(mask_data); + } + } +} + +void +SPLPEItem::applyToClipPathOrMask(SPItem *clip_mask, SPItem* to, Inkscape::LivePathEffect::Effect *lpe) +{ + auto group = cast<SPGroup>(clip_mask); + auto shape = cast<SPShape>(clip_mask); + SPRoot *root = this->document->getRoot(); + if (group) { + std::vector<SPItem*> item_list = group->item_list(); + for (auto subitem : item_list) { + applyToClipPathOrMask(subitem, to, lpe); + } + } else if (shape) { + if (sp_version_inside_range(root->version.inkscape, 0, 1, 0, 92)) { + shape->removeAttribute("inkscape:original-d"); + } else { + if (shape->curve()) { + auto c = *shape->curve(); + bool success = false; + try { + if (lpe) { + success = this->performOnePathEffect(&c, shape, lpe, true); + } else { + success = this->performPathEffect(&c, shape, true); + } + } catch (std::exception & e) { + g_warning("Exception during LPE execution. \n %s", e.what()); + if (SP_ACTIVE_DESKTOP && SP_ACTIVE_DESKTOP->messageStack()) { + SP_ACTIVE_DESKTOP->messageStack()->flash( Inkscape::WARNING_MESSAGE, + _("An exception occurred during execution of the Path Effect.") ); + } + success = false; + } + if (success) { + auto str = sp_svg_write_path(c.get_pathvector()); + shape->setCurveInsync(std::move(c)); + shape->setAttribute("d", str); + } else { + // LPE was unsuccessful or doeffect stack return null.. Read the old 'd'-attribute. + if (gchar const * value = shape->getAttribute("d")) { + shape->setCurve(SPCurve(sp_svg_read_pathv(value))); + } + } + shape->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + } +} + +Inkscape::LivePathEffect::Effect *SPLPEItem::getFirstPathEffectOfType(int type) +{ + 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->effectType() == type)) { + return lpe; + } + } + } + return nullptr; +} + +Inkscape::LivePathEffect::Effect const *SPLPEItem::getFirstPathEffectOfType(int type) const +{ + std::list<std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference>>::const_iterator i; + for (i = path_effect_list->begin(); i != path_effect_list->end(); ++i) { + LivePathEffectObject const *lpeobj = (*i)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect const *lpe = lpeobj->get_lpe(); + if (lpe && (lpe->effectType() == type)) { + return lpe; + } + } + } + return nullptr; +} + +std::vector<Inkscape::LivePathEffect::Effect *> SPLPEItem::getPathEffectsOfType(int type) +{ + std::vector<Inkscape::LivePathEffect::Effect *> effects; + 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->effectType() == type)) { + effects.push_back(lpe); + } + } + } + return effects; +} + +std::vector<Inkscape::LivePathEffect::Effect const *> SPLPEItem::getPathEffectsOfType(int type) const +{ + std::vector<Inkscape::LivePathEffect::Effect const *> effects; + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect const *lpe = lpeobj->get_lpe(); + if (lpe && (lpe->effectType() == type)) { + effects.push_back(lpe); + } + } + } + return effects; +} + +std::vector<Inkscape::LivePathEffect::Effect *> SPLPEItem::getPathEffects() +{ + std::vector<Inkscape::LivePathEffect::Effect *> effects; + 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) { + effects.push_back(lpe); + } + } + } + return effects; +} + +std::vector<Inkscape::LivePathEffect::Effect const *> SPLPEItem::getPathEffects() const +{ + std::vector<Inkscape::LivePathEffect::Effect const *> effects; + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect const *lpe = lpeobj->get_lpe(); + if (lpe) { + effects.push_back(lpe); + } + } + } + return effects; +} + +void SPLPEItem::editNextParamOncanvas(SPDesktop *dt) +{ + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference>lperef = this->getCurrentLPEReference(); + if (lperef && lperef->lpeobject && lperef->lpeobject->get_lpe()) { + lperef->lpeobject->get_lpe()->editNextParamOncanvas(this, dt); + } +} + +void SPLPEItem::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPItem::child_added(child, ref); + + if (this->hasPathEffectRecursive()) { + SPObject *ochild = this->get_child_by_repr(child); + + if ( ochild && is<SPLPEItem>(ochild) ) { + sp_lpe_item_create_original_path_recursive(cast<SPLPEItem>(ochild)); + } + } +} +void SPLPEItem::remove_child(Inkscape::XML::Node * child) { + SPObject *ochild = this->get_child_by_repr(child); + if (ochild && is<SPLPEItem>(ochild) && cast<SPLPEItem>(ochild)->hasPathEffectRecursive()) { + // we not need to update item because keep paths is false + sp_lpe_item_cleanup_original_path_recursive(cast<SPLPEItem>(ochild), false); + } + + SPItem::remove_child(child); +} + +static std::string patheffectlist_svg_string(PathEffectList const & list) +{ + HRefList hreflist; + + for (auto it : list) + { + hreflist.push_back( std::string(it->lpeobject_href) ); // C++11: use emplace_back + } + + return hreflist_svg_string(hreflist); +} + +/** + * THE function that should be used to generate any patheffectlist string. + * one of the methods to change the effect list: + * - create temporary href list + * - populate the templist with the effects from the old list that you want to have and their order + * - call this function with temp list as param + */ +static std::string hreflist_svg_string(HRefList const & list) +{ + std::string r; + bool semicolon_first = false; + + for (const auto & it : list) + { + if (semicolon_first) { + r += ';'; + } + + semicolon_first = true; + + r += it; + } + + return r; +} + +// Return a copy of the effect list +PathEffectList SPLPEItem::getEffectList() +{ + return *path_effect_list; +} + +// Return a copy of the effect list +PathEffectList const SPLPEItem::getEffectList() const +{ + return *path_effect_list; +} + +std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> +SPLPEItem::getPrevLPEReference(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef) +{ + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> prev= nullptr; + for (auto & it : *path_effect_list) { + if (it->lpeobject_repr == lperef->lpeobject_repr) { + break; + } + prev = it; + } + return prev; +} + +std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> +SPLPEItem::getNextLPEReference(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef) +{ + bool match = false; + for (auto & it : *path_effect_list) { + if (match) { + return it; + } + if (it->lpeobject_repr == lperef->lpeobject_repr) { + match = true; + } + } + return nullptr; +} + +std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> +SPLPEItem::getLastLPEReference() +{ + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> last = nullptr; + for (auto & it : *path_effect_list) { + last = it; + } + return last; +} + +size_t +SPLPEItem::getLPEReferenceIndex(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef) const +{ + size_t counter = 0; + for (auto & it : *path_effect_list) { + if (it->lpeobject_repr == lperef->lpeobject_repr) { + return counter; + } + counter++; + } + return Glib::ustring::npos; +} + +std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> SPLPEItem::getCurrentLPEReference() +{ + if (!this->current_path_effect && !this->path_effect_list->empty()) { + setCurrentPathEffect(this->path_effect_list->back()); + } + + return this->current_path_effect; +} + +Inkscape::LivePathEffect::Effect* SPLPEItem::getCurrentLPE() +{ + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef = getCurrentLPEReference(); + + if (lperef && lperef->lpeobject) + return lperef->lpeobject->get_lpe(); + else + return nullptr; +} + +Inkscape::LivePathEffect::Effect* SPLPEItem::getPrevLPE(Inkscape::LivePathEffect::Effect* lpe) +{ + Inkscape::LivePathEffect::Effect* prev = nullptr; + for (auto & it : *path_effect_list) { + if (it->lpeobject == lpe->getLPEObj()) { + break; + } + prev = it->lpeobject->get_lpe(); + } + return prev; +} + +Inkscape::LivePathEffect::Effect* SPLPEItem::getNextLPE(Inkscape::LivePathEffect::Effect* lpe) +{ + bool match = false; + for (auto & it : *path_effect_list) { + if (match) { + return it->lpeobject->get_lpe(); + } + if (it->lpeobject == lpe->getLPEObj()) { + match = true; + } + } + return nullptr; +} + +Inkscape::LivePathEffect::Effect* SPLPEItem::getLastLPE() +{ + Inkscape::LivePathEffect::Effect* last = nullptr; + for (auto & it : *path_effect_list) { + last = it->lpeobject->get_lpe(); + } + return last; +} + +size_t SPLPEItem::countLPEOfType(int const type, bool inc_hidden, bool is_ready) const +{ + size_t counter = 0; + if (path_effect_list->empty()) { + return counter; + } + + for (PathEffectList::const_iterator it = path_effect_list->begin(); it != path_effect_list->end(); ++it) + { + LivePathEffectObject const *lpeobj = (*it)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect const* lpe = lpeobj->get_lpe(); + if (lpe && (lpe->effectType() == type) && (lpe->is_visible || inc_hidden)) { + if (is_ready || lpe->isReady()) { + counter++; + } + } + } + } + + return counter; +} + +size_t +SPLPEItem::getLPEIndex(Inkscape::LivePathEffect::Effect* lpe) const +{ + size_t counter = 0; + for (auto & it : *path_effect_list) { + if (it->lpeobject == lpe->getLPEObj()) { + return counter; + } + counter++; + } + return Glib::ustring::npos; +} + +bool SPLPEItem::setCurrentPathEffect(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef) +{ + for (auto & it : *path_effect_list) { + if (it->lpeobject_repr == lperef->lpeobject_repr) { + this->current_path_effect = it; // current_path_effect should always be a pointer from the path_effect_list ! + return true; + } + } + + return false; +} + +bool SPLPEItem::setCurrentPathEffect(LivePathEffectObject const * lopeobj) +{ + for (auto & it : *path_effect_list) { + if (it->lpeobject_repr == lopeobj->getRepr()) { + this->current_path_effect = it; // current_path_effect should always be a pointer from the path_effect_list ! + return true; + } + } + + return false; +} + +std::vector<SPObject *> SPLPEItem::get_satellites(bool force, bool recursive, bool onchilds) +{ + std::vector<SPObject *> satellites; + if (onchilds) { + auto group = cast<SPGroup>(this); + if (group) { + std::vector<SPItem*> item_list = group->item_list(); + for (auto child:item_list) { + auto lpechild = cast<SPLPEItem>(child); + if (lpechild) { + std::vector<SPObject *> tmp = lpechild->get_satellites(force, recursive); + satellites.insert( satellites.end(), tmp.begin(), tmp.end() ); + } + } + } + } + for (auto &it : *path_effect_list) { + LivePathEffectObject *lpeobj = it->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (lpe) { + std::vector<SPObject *> tmp = lpe->effect_get_satellites(force); + satellites.insert(satellites.begin(), tmp.begin(), tmp.end()); + } + } + } + if (recursive) { + std::vector<SPObject *> allsatellites; + for (auto satellite : satellites) { + SPLPEItem *lpeitem = nullptr; + if ( satellite && ( lpeitem = cast<SPLPEItem>(satellite) )) { + std::vector<SPObject *> tmp = lpeitem->get_satellites(force, recursive); + allsatellites.insert(allsatellites.begin(), tmp.begin(), tmp.end()); + } + } + satellites.insert(satellites.begin(), allsatellites.begin(), allsatellites.end()); + } + return satellites; +} + +/** + * Writes a new "inkscape:path-effect" string to xml, where the old_lpeobjects are substituted by the new ones. + * Note that this method messes up the item's \c PathEffectList. + */ +void SPLPEItem::replacePathEffects( std::vector<LivePathEffectObject const *> const &old_lpeobjs, + std::vector<LivePathEffectObject const *> const &new_lpeobjs ) +{ + HRefList hreflist; + for (PathEffectList::const_iterator it = this->path_effect_list->begin(); it != this->path_effect_list->end(); ++it) + { + LivePathEffectObject const * current_lpeobj = (*it)->lpeobject; + std::vector<LivePathEffectObject const *>::const_iterator found_it(std::find(old_lpeobjs.begin(), old_lpeobjs.end(), current_lpeobj)); + + if ( found_it != old_lpeobjs.end() ) { + std::vector<LivePathEffectObject const *>::difference_type found_index = std::distance (old_lpeobjs.begin(), found_it); + const gchar * repr_id = new_lpeobjs[found_index]->getRepr()->attribute("id"); + gchar *hrefstr = g_strdup_printf("#%s", repr_id); + hreflist.push_back( std::string(hrefstr) ); + g_free(hrefstr); + } + else { + hreflist.push_back( std::string((*it)->lpeobject_href) ); + } + } + + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", hreflist_svg_string(hreflist)); +} + +/** + * Check all effects in the stack if they are used by other items, and fork them if so. + * It is not recommended to fork the effects by yourself calling LivePathEffectObject::fork_private_if_necessary, + * use this method instead. + * Returns true if one or more effects were forked; returns false if nothing was done. + */ +bool SPLPEItem::forkPathEffectsIfNecessary(unsigned int nr_of_allowed_users, bool recursive, bool force) +{ + bool forked = false; + auto group = cast<SPGroup>(this); + if (group && recursive) { + std::vector<SPItem*> item_list = group->item_list(); + for (auto child:item_list) { + auto lpeitem = cast<SPLPEItem>(child); + if (lpeitem && lpeitem->forkPathEffectsIfNecessary(nr_of_allowed_users, recursive)) { + forked = true; + } + } + } + + if ( this->hasPathEffect() ) { + // If one of the path effects is used by 2 or more items, fork it + // so that each object has its own independent copy of the effect. + // Note: replacing path effects messes up the path effect list + + // Clones of the LPEItem will increase the refcount of the lpeobjects. + // Therefore, nr_of_allowed_users should be increased with the number of clones (i.e. refs to the lpeitem) + // is not well handled forker because is based in hrefcount + // to handle clones and this can be wrong with other references + // for this I add a new parameter to allow force fork + nr_of_allowed_users += this->hrefcount; + if (force) { + nr_of_allowed_users = 1; + } + std::vector<LivePathEffectObject const*> old_lpeobjs, new_lpeobjs; + std::vector<LivePathEffectObject *> upd_lpeobjs; + PathEffectList effect_list = this->getEffectList(); + for (auto & it : effect_list) + { + LivePathEffectObject *lpeobj = it->lpeobject; + if (lpeobj) { + LivePathEffectObject *forked_lpeobj = lpeobj->fork_private_if_necessary(nr_of_allowed_users); + if (forked_lpeobj && forked_lpeobj != lpeobj) { + forked = true; + forked_lpeobj->get_lpe()->is_load = true; + forked_lpeobj->get_lpe()->sp_lpe_item = this; + old_lpeobjs.push_back(lpeobj); + new_lpeobjs.push_back(forked_lpeobj); + upd_lpeobjs.push_back(forked_lpeobj); + } + } + } + + if (forked) { + this->replacePathEffects(old_lpeobjs, new_lpeobjs); + for (auto &forked_lpeobj : upd_lpeobjs) { + forked_lpeobj->get_lpe()->read_from_SVG(); + } + } + } + + return forked; +} + +// Enable or disable the path effects of the item. +void sp_lpe_item_enable_path_effects(SPLPEItem *lpeitem, bool enable) +{ + if (enable) { + lpeitem->path_effects_enabled++; + } + else { + lpeitem->path_effects_enabled--; + } +} + +// Are the path effects enabled on this item ? +bool SPLPEItem::pathEffectsEnabled() const +{ + return !onsymbol && path_effects_enabled > 0; +} + +/* + 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 : diff --git a/src/object/sp-lpe-item.h b/src/object/sp-lpe-item.h new file mode 100644 index 0000000..f24726d --- /dev/null +++ b/src/object/sp-lpe-item.h @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_LPE_ITEM_H_SEEN +#define SP_LPE_ITEM_H_SEEN + +/** \file + * Base class for live path effect items + */ +/* + * Authors: + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Bastien Bouclet <bgkweb@gmail.com> + * + * Copyright (C) 2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <list> +#include <string> +#include <memory> +#include "sp-item.h" + +class LivePathEffectObject; +class SPCurve; +class SPShape; +class SPDesktop; + +namespace Inkscape{ + namespace Display { + class TemporaryItem; + } + namespace LivePathEffect{ + class LPEObjectReference; + class Effect; + } +} + +typedef std::list<std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference>> PathEffectList; + +class SPLPEItem : public SPItem { +public: + SPLPEItem(); + ~SPLPEItem() override; + int tag() const override { return tag_of<decltype(*this)>; } + + int path_effects_enabled; + + PathEffectList* path_effect_list; + std::list<sigc::connection> *lpe_modified_connection_list; // this list contains the connections for listening to lpeobject parameter changes + + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> current_path_effect; + std::vector<Inkscape::Display::TemporaryItem*> lpe_helperpaths; + + void replacePathEffects( std::vector<LivePathEffectObject const *> const &old_lpeobjs, + std::vector<LivePathEffectObject const *> const &new_lpeobjs ); + + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttr key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + virtual void update_patheffect(bool write); + bool optimizeTransforms(); + void notifyTransform(Geom::Affine const &postmul); + bool performPathEffect(SPCurve *curve, SPShape *current, bool is_clip_or_mask = false); + bool performOnePathEffect(SPCurve *curve, SPShape *current, Inkscape::LivePathEffect::Effect *lpe, bool is_clip_or_mask = false); + bool pathEffectsEnabled() const; + bool hasPathEffect() const; + bool hasPathEffectOfType(int const type, bool is_ready = true) const; + bool hasPathEffectOfTypeRecursive(int const type, bool is_ready = true) const; + bool hasPathEffectRecursive() const; + SPLPEItem const * getTopPathEffect() const; + bool hasPathEffectOnClipOrMask(SPLPEItem * shape) const; + bool hasPathEffectOnClipOrMaskRecursive(SPLPEItem * shape) const; + size_t getLPEIndex(Inkscape::LivePathEffect::Effect* lpe) const; + size_t countLPEOfType(int const type, bool inc_hidden = true, bool is_ready = true) const; + size_t getLPEReferenceIndex(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef) const; + Inkscape::LivePathEffect::Effect *getFirstPathEffectOfType(int type); + Inkscape::LivePathEffect::Effect const *getFirstPathEffectOfType(int type) const; + std::vector<Inkscape::LivePathEffect::Effect *> getPathEffectsOfType(int type); + std::vector<Inkscape::LivePathEffect::Effect const *> getPathEffectsOfType(int type) const; + std::vector<Inkscape::LivePathEffect::Effect *> getPathEffects(); + std::vector<Inkscape::LivePathEffect::Effect const *> getPathEffects() const; + std::vector<SPObject *> get_satellites(bool force = true, bool recursive = false, bool onchilds = false); + bool isOnClipboard(); + bool isOnSymbol() const; + bool onsymbol = false; + bool hasBrokenPathEffect() const; + bool lpe_initialized = false; + PathEffectList getEffectList(); + PathEffectList const getEffectList() const; + + void duplicateCurrentPathEffect(); + void downCurrentPathEffect(); + void upCurrentPathEffect(); + void removePathEffect(Inkscape::LivePathEffect::Effect* lpe, bool keep_paths); + void movePathEffect(gint origin, gint dest, bool select_moved = false); + SPLPEItem * flattenCurrentPathEffect(); + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> getCurrentLPEReference(); + Inkscape::LivePathEffect::Effect* getCurrentLPE(); + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> getPrevLPEReference(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef); + Inkscape::LivePathEffect::Effect* getPrevLPE(Inkscape::LivePathEffect::Effect* lpe); + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> getNextLPEReference(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference>); + Inkscape::LivePathEffect::Effect* getNextLPE(Inkscape::LivePathEffect::Effect* lpe); + std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> getLastLPEReference(); + Inkscape::LivePathEffect::Effect* getLastLPE(); + bool setCurrentPathEffect(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef); + bool setCurrentPathEffect(LivePathEffectObject const * lopeobj); + SPLPEItem * removeCurrentPathEffect(bool keep_paths); + SPLPEItem * removeAllPathEffects(bool keep_paths, bool recursive = false); + void addPathEffect(std::string value, bool reset); + void addPathEffect(LivePathEffectObject * new_lpeobj); + void resetClipPathAndMaskLPE(bool fromrecurse = false); + void applyToMask(SPItem* to, Inkscape::LivePathEffect::Effect *lpe = nullptr); + void applyToClipPath(SPItem* to, Inkscape::LivePathEffect::Effect *lpe = nullptr); + void applyToClipPathOrMask(SPItem * clip_mask, SPItem* to, Inkscape::LivePathEffect::Effect *lpe = nullptr); + bool forkPathEffectsIfNecessary(unsigned int nr_of_allowed_users = 1, bool recursive = true, bool force = false); + void editNextParamOncanvas(SPDesktop *dt); + void update_satellites(bool recursive = true); +}; +void sp_lpe_item_update_patheffect (SPLPEItem *lpeitem, bool wholetree, bool write, bool with_satellites = false); // careful, class already has method with *very* similar name! +void sp_lpe_item_enable_path_effects(SPLPEItem *lpeitem, bool enable); + +#endif /* !SP_LPE_ITEM_H_SEEN */ + +/* + 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 : diff --git a/src/object/sp-marker-loc.h b/src/object/sp-marker-loc.h new file mode 100644 index 0000000..db4b365 --- /dev/null +++ b/src/object/sp-marker-loc.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_MARKER_LOC_H +#define SEEN_SP_MARKER_LOC_H + +/** + * These enums are to allow us to have 4-element arrays that represent a set of marker locations + * (all, start, mid, and end). This allows us to iterate through the array in places where we need + * to do a process across all of the markers, instead of separate code stanzas for each. + * + * IMPORTANT: the code assumes that the locations have the values as written below! so don't change the values!!! + */ +enum SPMarkerLoc { + SP_MARKER_LOC = 0, + SP_MARKER_LOC_START = 1, + SP_MARKER_LOC_MID = 2, + SP_MARKER_LOC_END = 3, + SP_MARKER_LOC_QTY = 4 +}; + +#endif /* !SEEN_SP_MARKER_LOC_H */ + +/* + 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 : diff --git a/src/object/sp-marker.cpp b/src/object/sp-marker.cpp new file mode 100644 index 0000000..37b9128 --- /dev/null +++ b/src/object/sp-marker.cpp @@ -0,0 +1,653 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <marker> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Bryce Harrington <bryce@bryceharrington.org> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2003 Lauris Kaplinski + * 2004-2006 Bryce Harrington + * 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-marker.h" + +#include <cstring> +#include <string> +#include <glib/gi18n.h> + +#include <2geom/affine.h> +#include <2geom/transforms.h> + +#include "attributes.h" +#include "document-undo.h" +#include "document.h" +#include "preferences.h" +#include "sp-defs.h" + +#include "display/drawing-group.h" +#include "display/drawing-item-ptr.h" +#include "object/object-set.h" +#include "svg/css-ostringstream.h" +#include "svg/svg.h" +#include "ui/icon-names.h" +#include "xml/repr.h" + +using Inkscape::DocumentUndo; +using Inkscape::ObjectSet; + +struct SPMarkerView +{ + std::vector<DrawingItemPtr<Inkscape::DrawingItem>> items; +}; + +SPMarker::SPMarker() : SPGroup(), SPViewBox(), + markerUnits_set(0), + markerUnits(0), + refX(), + refY(), + markerWidth(), + markerHeight(), + orient_set(0), + orient_mode(MARKER_ORIENT_ANGLE) +{ + // cppcheck-suppress useInitializationList + orient = 0; +} + +/** + * Initializes an SPMarker object. This notes the marker's viewBox is + * not set and initializes the marker's c2p identity matrix. + */ + +SPMarker::~SPMarker() = default; + +/** + * Virtual build callback for SPMarker. + * + * This is to be invoked immediately after creation of an SPMarker. This + * method fills an SPMarker object with its SVG attributes, and calls the + * parent class' build routine to attach the object to its document and + * repr. The result will be creation of the whole document tree. + * + * \see SPObject::build() + */ +void SPMarker::build(SPDocument *document, Inkscape::XML::Node *repr) { + this->readAttr(SPAttr::MARKERUNITS); + this->readAttr(SPAttr::REFX); + this->readAttr(SPAttr::REFY); + this->readAttr(SPAttr::MARKERWIDTH); + this->readAttr(SPAttr::MARKERHEIGHT); + this->readAttr(SPAttr::ORIENT); + this->readAttr(SPAttr::VIEWBOX); + this->readAttr(SPAttr::PRESERVEASPECTRATIO); + this->readAttr(SPAttr::STYLE); + + SPGroup::build(document, repr); +} + + +/** + * Removes, releases and unrefs all children of object + * + * This is the inverse of sp_marker_build(). It must be invoked as soon + * as the marker is removed from the tree, even if it is still referenced + * by other objects. It hides and removes any views of the marker, then + * calls the parent classes' release function to deregister the object + * and release its SPRepr bindings. The result will be the destruction + * of the entire document tree. + * + * \see SPObject::release() + */ +void SPMarker::release() { + + for (auto &it : views_map) { + SPGroup::hide(it.first); + } + views_map.clear(); + + SPGroup::release(); +} + +void SPMarker::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::MARKERUNITS: + this->markerUnits_set = FALSE; + this->markerUnits = SP_MARKER_UNITS_STROKEWIDTH; + + if (value) { + if (!strcmp (value, "strokeWidth")) { + this->markerUnits_set = TRUE; + } else if (!strcmp (value, "userSpaceOnUse")) { + this->markerUnits = SP_MARKER_UNITS_USERSPACEONUSE; + this->markerUnits_set = TRUE; + } + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::REFX: + this->refX.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::REFY: + this->refY.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::MARKERWIDTH: + this->markerWidth.readOrUnset(value, SVGLength::NONE, 3.0, 3.0); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::MARKERHEIGHT: + this->markerHeight.readOrUnset(value, SVGLength::NONE, 3.0, 3.0); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::ORIENT: + this->orient_set = FALSE; + this->orient_mode = MARKER_ORIENT_ANGLE; + this->orient = 0.0; + + if (value) { + if (!strcmp (value, "auto")) { + this->orient_mode = MARKER_ORIENT_AUTO; + this->orient_set = TRUE; + } else if (!strcmp (value, "auto-start-reverse")) { + this->orient_mode = MARKER_ORIENT_AUTO_START_REVERSE; + this->orient_set = TRUE; + } else { + orient.readOrUnset(value); + if (orient._set) { + this->orient_mode = MARKER_ORIENT_ANGLE; + this->orient_set = orient._set; + } + } + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::VIEWBOX: + set_viewBox( value ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::PRESERVEASPECTRATIO: + set_preserveAspectRatio( value ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + default: + SPGroup::set(key, value); + break; + } +} + +void SPMarker::update(SPCtx *ctx, guint flags) { + + SPItemCtx ictx; + + // Copy parent context + ictx.flags = ctx->flags; + + // Initialize transformations + ictx.i2doc = Geom::identity(); + ictx.i2vp = Geom::identity(); + + // Set up viewport + ictx.viewport = Geom::Rect::from_xywh(0, 0, this->markerWidth.computed, this->markerHeight.computed); + + SPItemCtx rctx = get_rctx( &ictx ); + + // Shift according to refX, refY + Geom::Point ref( this->refX.computed, this->refY.computed ); + ref *= c2p; + this->c2p = this->c2p * Geom::Translate( -ref ); + + // And invoke parent method + SPGroup::update((SPCtx *) &rctx, flags); + + // As last step set additional transform of drawing group + for (auto &it : views_map) { + for (auto &item : it.second.items) { + if (item) { + auto g = cast<Inkscape::DrawingGroup>(item.get()); + g->setChildTransform(c2p); + } + } + } +} + +Inkscape::XML::Node* SPMarker::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:marker"); + } + + if (this->markerUnits_set) { + if (this->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + repr->setAttribute("markerUnits", "strokeWidth"); + } else { + repr->setAttribute("markerUnits", "userSpaceOnUse"); + } + } else { + repr->removeAttribute("markerUnits"); + } + + if (this->refX._set) { + repr->setAttributeSvgDouble("refX", this->refX.computed); + } else { + repr->removeAttribute("refX"); + } + + if (this->refY._set) { + repr->setAttributeSvgDouble("refY", this->refY.computed); + } else { + repr->removeAttribute("refY"); + } + + if (this->markerWidth._set) { + repr->setAttributeSvgDouble("markerWidth", this->markerWidth.computed); + } else { + repr->removeAttribute("markerWidth"); + } + + if (this->markerHeight._set) { + repr->setAttributeSvgDouble("markerHeight", this->markerHeight.computed); + } else { + repr->removeAttribute("markerHeight"); + } + + if (this->orient_set) { + if (this->orient_mode == MARKER_ORIENT_AUTO) { + repr->setAttribute("orient", "auto"); + } else if (this->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) { + repr->setAttribute("orient", "auto-start-reverse"); + } else { + repr->setAttributeCssDouble("orient", this->orient.computed); + } + } else { + repr->removeAttribute("orient"); + } + + this->write_viewBox(repr); + this->write_preserveAspectRatio(repr); + + SPGroup::write(xml_doc, repr, flags); + + return repr; +} + +Inkscape::DrawingItem* SPMarker::show(Inkscape::Drawing &/*drawing*/, unsigned int /*key*/, unsigned int /*flags*/) { + // Markers in tree are never shown directly even if outside of <defs>. + return nullptr; +} + +Inkscape::DrawingItem* SPMarker::private_show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) { + return SPGroup::show(drawing, key, flags); +} + +void SPMarker::hide(unsigned int key) { + // CPPIFY: correct? + SPGroup::hide(key); +} + +/** + * Calculate the transformation for this marker. + */ +Geom::Affine SPMarker::get_marker_transform(const Geom::Affine &base, double linewidth, bool start_marker) +{ + // Default is MARKER_ORIENT_AUTO + Geom::Affine result = base; + + if (this->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) { + if (start_marker) { + result = Geom::Rotate::from_degrees( 180.0 ) * base; + } + } else if (this->orient_mode != MARKER_ORIENT_AUTO) { + /* fixme: Orient units (Lauris) */ + result = Geom::Rotate::from_degrees(this->orient.computed); + result *= Geom::Translate(base.translation()); + } + + if (this->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + result = Geom::Scale(linewidth) * result; + } + return result; +} + +/* +- used to validate the marker item before passing it into the shape editor from the marker-tool. +- sets any missing properties that are needed before editing starts. +*/ +void sp_validate_marker(SPMarker *sp_marker, SPDocument *doc) { + + if (!doc || !sp_marker) return; + + doc->ensureUpToDate(); + + // calculate the marker bounds to set any missing viewBox information + std::vector<SPObject*> items = const_cast<SPMarker*>(sp_marker)->childList(false, SPObject::ActionBBox); + + Geom::OptRect r; + for (auto *i : items) { + auto item = cast<SPItem>(i); + r.unionWith(item->desktopVisualBounds()); + } + + Geom::Rect bounds(r->min() * doc->dt2doc(), r->max() * doc->dt2doc()); + + if(!sp_marker->refX._set) { + sp_marker->setAttribute("refX", "0.0"); + } + + if(!sp_marker->refY._set) { + sp_marker->setAttribute("refY", "0.0"); + } + + if(!sp_marker->orient._set) { + sp_marker->setAttribute("orient", "0.0"); + } + + double xScale = 1; + double yScale = 1; + + if(sp_marker->viewBox_set) { + // check if the X direction has any existing scale factor + if(sp_marker->viewBox.width() > 0) { + double existingXScale = sp_marker->markerWidth.computed/sp_marker->viewBox.width(); + xScale = (existingXScale >= 0? existingXScale: 1); + } + + // check if the Y direction has any existing scale factor + if(sp_marker->viewBox.height() > 0) { + double existingYScale = sp_marker->markerHeight.computed/sp_marker->viewBox.height(); + yScale = (existingYScale >= 0? existingYScale: 1); + } + + // only enforce uniform scale if the preserveAspectRatio is not set yet or if it does not equal "none" + if((!sp_marker->aspect_set) || (sp_marker->aspect_align != SP_ASPECT_NONE)) { + // set the scale to the smaller option if both xScale and yScale exist + if(xScale > yScale) { + xScale = yScale; + } else { + yScale = xScale; + } + } + } else { + Inkscape::CSSOStringStream os; + os << "0 0 " << bounds.dimensions()[Geom::X] << " " << bounds.dimensions()[Geom::Y]; + sp_marker->setAttribute("viewBox", os.str().c_str()); + } + + sp_marker->setAttributeDouble("markerWidth", sp_marker->viewBox.width() * xScale); + sp_marker->setAttributeDouble("markerHeight", sp_marker->viewBox.height() * yScale); + + if(!sp_marker->aspect_set) { + // feedback from UX expert indicates that uniform scaling should be used by default; + // marker tool should respect aspect ratio setting too (without Ctrl key modifier?) + sp_marker->setAttribute("preserveAspectRatio", "xMidYMid"); + } +} + +Geom::OptRect SPMarker::bbox(Geom::Affine const &/*transform*/, SPItem::BBoxType /*type*/) const { + return Geom::OptRect(); +} + +void SPMarker::print(SPPrintContext* /*ctx*/) { +} + +/* fixme: Remove link if zero-sized (Lauris) */ + +/** + * Removes any SPMarkerViews that a marker has with a specific key. + * Set up the DrawingItem array's size in the specified SPMarker's SPMarkerView. + * This is called from sp_shape_update() for shapes that have markers. It + * removes the old view of the marker and establishes a new one, registering + * it with the marker's list of views for future updates. + * + * \param marker Marker to create views in. + * \param key Key to give each SPMarkerView. + * \param size Number of DrawingItems to put in the SPMarkerView. + */ +// If marker views are always created in order, then this function could be eliminated +// by doing the push_back in sp_marker_show_instance. +void +sp_marker_show_dimension (SPMarker *marker, unsigned int key, unsigned int size) +{ + auto it = marker->views_map.find(key); + if (it != marker->views_map.end()) { + if (it->second.items.size() != size ) { + // Need to change size of vector! (We should not really need to do this.) + marker->hide(key); + it->second.items.clear(); + for (unsigned int i = 0; i < size; ++i) { + it->second.items.push_back(nullptr); + } + } + } else { + marker->views_map[key] = SPMarkerView(); + for (unsigned int i = 0; i < size; ++i) { + marker->views_map[key].items.push_back(nullptr); + } + } +} + +/** + * Shows an instance of a marker. This is called during sp_shape_update_marker_view() + * show and transform a child item in the drawing for all views with the given key. + */ +Inkscape::DrawingItem * +sp_marker_show_instance ( SPMarker *marker, Inkscape::DrawingItem *parent, + unsigned int key, unsigned int pos, + Geom::Affine const &base, float linewidth) +{ + // Do not show marker if linewidth == 0 and markerUnits == strokeWidth + // otherwise Cairo will fail to render anything on the tile + // that contains the "degenerate" marker. + if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH && linewidth == 0) { + return nullptr; + } + + auto it = marker->views_map.find(key); + if (it == marker->views_map.end()) { + // Key not found + return nullptr; + } + + SPMarkerView *view = &it->second; + if (pos >= view->items.size() ) { + // Position index too large, doesn't exist. + return nullptr; + } + + // If not already created + if (!view->items[pos]) { + + /* Parent class ::show method */ + view->items[pos].reset(marker->private_show(parent->drawing(), key, SP_ITEM_REFERENCE_FLAGS)); + + if (view->items[pos]) { + /* fixme: Position (Lauris) */ + parent->prependChild(view->items[pos].get()); + if (auto g = cast<Inkscape::DrawingGroup>(view->items[pos].get())) { + g->setChildTransform(marker->c2p); + } + } + } + + if (view->items[pos]) { + // Rotating for reversed-marker option is done at rendering time if necessary + // so always pass in start_marker is false. + view->items[pos]->setTransform(marker->get_marker_transform(base, linewidth, false)); + } + + return view->items[pos].get(); +} + +/** + * Hides/removes all views of the given marker that have key 'key'. + * This replaces SPItem implementation because we have our own views + * \param key SPMarkerView key to hide. + */ +void +sp_marker_hide (SPMarker *marker, unsigned int key) +{ + marker->hide(key); + marker->views_map.erase(key); +} + + +const gchar *generate_marker(std::vector<Inkscape::XML::Node*> &reprs, Geom::Rect bounds, SPDocument *document, Geom::Point center, Geom::Affine move) +{ + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:marker"); + + // Uncommenting this will make the marker fixed-size independent of stroke width. + // Commented out for consistency with standard markers which scale when you change + // stroke width: + //repr->setAttribute("markerUnits", "userSpaceOnUse"); + + repr->setAttributeSvgDouble("markerWidth", bounds.dimensions()[Geom::X]); + repr->setAttributeSvgDouble("markerHeight", bounds.dimensions()[Geom::Y]); + repr->setAttributeSvgDouble("refX", center[Geom::X]); + repr->setAttributeSvgDouble("refY", center[Geom::Y]); + + repr->setAttribute("orient", "auto"); + + defsrepr->appendChild(repr); + const gchar *mark_id = repr->attribute("id"); + SPObject *mark_object = document->getObjectById(mark_id); + + for (auto node : reprs){ + auto copy = cast<SPItem>(mark_object->appendChildRepr(node)); + + Geom::Affine dup_transform; + if (!sp_svg_transform_read (node->attribute("transform"), &dup_transform)) + dup_transform = Geom::identity(); + dup_transform *= move; + + copy->doWriteTransform(dup_transform); + } + + Inkscape::GC::release(repr); + return mark_id; +} + +SPObject *sp_marker_fork_if_necessary(SPObject *marker) +{ + if (marker->hrefcount < 2) { + return marker; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gboolean colorStock = prefs->getBool("/options/markers/colorStockMarkers", true); + gboolean colorCustom = prefs->getBool("/options/markers/colorCustomMarkers", false); + const gchar *stock = marker->getRepr()->attribute("inkscape:isstock"); + gboolean isStock = (!stock || !strcmp(stock,"true")); + + if (isStock ? !colorStock : !colorCustom) { + return marker; + } + + SPDocument *doc = marker->document; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + // Turn off garbage-collectable or it might be collected before we can use it + marker->removeAttribute("inkscape:collect"); + Inkscape::XML::Node *mark_repr = marker->getRepr()->duplicate(xml_doc); + doc->getDefs()->getRepr()->addChild(mark_repr, nullptr); + if (!mark_repr->attribute("inkscape:stockid")) { + mark_repr->setAttribute("inkscape:stockid", mark_repr->attribute("id")); + } + marker->setAttribute("inkscape:collect", "always"); + + SPObject *marker_new = static_cast<SPObject *>(doc->getObjectByRepr(mark_repr)); + Inkscape::GC::release(mark_repr); + return marker_new; +} + +void sp_marker_set_orient(SPMarker* marker, const char* value) { + if (!marker || !value) return; + + marker->setAttribute("orient", value); + + if (marker->document) { + DocumentUndo::maybeDone(marker->document, "marker", _("Set marker orientation"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } +} + +void sp_marker_set_size(SPMarker* marker, double sx, double sy) { + if (!marker) return; + + marker->setAttributeDouble("markerWidth", sx); + marker->setAttributeDouble("markerHeight", sy); + + if (marker->document) { + DocumentUndo::maybeDone(marker->document, "marker", _("Set marker size"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } +} + +void sp_marker_scale_with_stroke(SPMarker* marker, bool scale_with_stroke) { + if (!marker) return; + + marker->setAttribute("markerUnits", scale_with_stroke ? "strokeWidth" : "userSpaceOnUse"); + + if (marker->document) { + DocumentUndo::maybeDone(marker->document, "marker", _("Set marker scale with stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } +} + +void sp_marker_set_offset(SPMarker* marker, double dx, double dy) { + if (!marker) return; + + marker->setAttributeDouble("refX", dx); + marker->setAttributeDouble("refY", dy); + + if (marker->document) { + DocumentUndo::maybeDone(marker->document, "marker", _("Set marker offset"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } +} + +void sp_marker_set_uniform_scale(SPMarker* marker, bool uniform) { + if (!marker) return; + + marker->setAttribute("preserveAspectRatio", uniform ? "xMidYMid" : "none"); + + if (marker->document) { + DocumentUndo::maybeDone(marker->document, "marker", _("Set marker uniform scaling"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } +} + +void sp_marker_flip_horizontally(SPMarker* marker) { + if (!marker) return; + + ObjectSet set(marker->document); + set.addList(marker->item_list()); + Geom::OptRect bbox = set.visualBounds(); + if (bbox) { + set.setScaleRelative(bbox->midpoint(), Geom::Scale(-1.0, 1.0)); + if (marker->document) { + DocumentUndo::maybeDone(marker->document, "marker", _("Flip marker horizontally"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } + } +} + +/* + 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 : diff --git a/src/object/sp-marker.h b/src/object/sp-marker.h new file mode 100644 index 0000000..5ca2494 --- /dev/null +++ b/src/object/sp-marker.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MARKER_H +#define SEEN_SP_MARKER_H + +/* + * SVG <marker> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2003 Lauris Kaplinski + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * This is quite similar in logic to <svg> + * Maybe we should merge them somehow (Lauris) + */ + +class SPMarkerView; + +#include <map> + +#include <2geom/rect.h> +#include <2geom/affine.h> + +#include "enums.h" +#include "svg/svg-length.h" +#include "svg/svg-angle.h" +#include "sp-item-group.h" +#include "uri-references.h" +#include "viewbox.h" + +enum markerOrient { + MARKER_ORIENT_ANGLE, + MARKER_ORIENT_AUTO, + MARKER_ORIENT_AUTO_START_REVERSE +}; + +class SPMarker final : public SPGroup, public SPViewBox { +public: + SPMarker(); + ~SPMarker() override; + int tag() const override { return tag_of<decltype(*this)>; } + + /* units */ + unsigned int markerUnits_set : 1; + unsigned int markerUnits : 1; + + /* reference point */ + SVGLength refX; + SVGLength refY; + + /* dimensions */ + SVGLength markerWidth; + SVGLength markerHeight; + + /* orient */ + unsigned int orient_set : 1; + markerOrient orient_mode : 2; + SVGAngle orient; + + Geom::Affine get_marker_transform(const Geom::Affine &base, double linewidth, bool for_display = false); + + /* Private views indexed by key that corresponds to a + * particular marker type (start, mid, end) on a particular + * path. SPMarkerView is a wrapper for a vector of pointers to + * Inkscape::DrawingItem instances, one pointer for each + * rendered marker. + */ + std::map<unsigned int, SPMarkerView> views_map; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, gchar const* value) override; + void update(SPCtx *ctx, guint flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) override; + + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + virtual Inkscape::DrawingItem* private_show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags); + void hide(unsigned int key) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + void print(SPPrintContext *ctx) override; +}; + + +class SPMarkerReference : public Inkscape::URIReference { + SPMarkerReference(SPObject *obj) : URIReference(obj) {} + SPMarker *getObject() const { + return static_cast<SPMarker *>(URIReference::getObject()); + } +protected: + bool _acceptObject(SPObject *obj) const override { + return is<SPMarker>(obj) && URIReference::_acceptObject(obj); + } +}; + +void sp_validate_marker(SPMarker *sp_marker, SPDocument *doc); +void sp_marker_show_dimension (SPMarker *marker, unsigned int key, unsigned int size); +Inkscape::DrawingItem *sp_marker_show_instance (SPMarker *marker, Inkscape::DrawingItem *parent, + unsigned int key, unsigned int pos, + Geom::Affine const &base, float linewidth); +void sp_marker_hide (SPMarker *marker, unsigned int key); +const char *generate_marker (std::vector<Inkscape::XML::Node*> &reprs, Geom::Rect bounds, SPDocument *document, Geom::Point center, Geom::Affine move); +SPObject *sp_marker_fork_if_necessary(SPObject *marker); + +void sp_marker_set_orient(SPMarker* marker, const char* value); +void sp_marker_set_size(SPMarker* marker, double sx, double sy); +void sp_marker_scale_with_stroke(SPMarker* marker, bool scale_with_stroke); +void sp_marker_set_offset(SPMarker* marker, double dx, double dy); +void sp_marker_set_uniform_scale(SPMarker* marker, bool uniform); +void sp_marker_flip_horizontally(SPMarker* marker); + +#endif diff --git a/src/object/sp-mask.cpp b/src/object/sp-mask.cpp new file mode 100644 index 0000000..c0c2640 --- /dev/null +++ b/src/object/sp-mask.cpp @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <mask> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> +#include <2geom/transforms.h> + +#include "display/drawing.h" +#include "display/drawing-group.h" +#include "xml/repr.h" + +#include "enums.h" +#include "attributes.h" +#include "document.h" +#include "style.h" +#include "attributes.h" + +#include "sp-defs.h" +#include "sp-item.h" +#include "sp-mask.h" + +SPMask::SPMask() +{ + maskUnits_set = false; + maskUnits = SP_CONTENT_UNITS_OBJECTBOUNDINGBOX; + + maskContentUnits_set = false; + maskContentUnits = SP_CONTENT_UNITS_USERSPACEONUSE; +} + +SPMask::~SPMask() = default; + +void SPMask::build(SPDocument *doc, Inkscape::XML::Node *repr) +{ + SPObjectGroup::build(doc, repr); + + readAttr(SPAttr::MASKUNITS); + readAttr(SPAttr::MASKCONTENTUNITS); + readAttr(SPAttr::STYLE); + + doc->addResource("mask", this); +} + +void SPMask::release() +{ + if (document) { + document->removeResource("mask", this); + } + + views.clear(); + + SPObjectGroup::release(); +} + +void SPMask::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::MASKUNITS: + maskUnits = SP_CONTENT_UNITS_OBJECTBOUNDINGBOX; + maskUnits_set = false; + + if (value) { + if (!std::strcmp(value, "userSpaceOnUse")) { + maskUnits = SP_CONTENT_UNITS_USERSPACEONUSE; + maskUnits_set = true; + } else if (!std::strcmp(value, "objectBoundingBox")) { + maskUnits_set = true; + } + } + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::MASKCONTENTUNITS: + maskContentUnits = SP_CONTENT_UNITS_USERSPACEONUSE; + maskContentUnits_set = false; + + if (value) { + if (!std::strcmp(value, "userSpaceOnUse")) { + maskContentUnits_set = true; + } else if (!std::strcmp(value, "objectBoundingBox")) { + maskContentUnits = SP_CONTENT_UNITS_OBJECTBOUNDINGBOX; + maskContentUnits_set = true; + } + } + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPObjectGroup::set(key, value); + break; + } +} + +Geom::OptRect SPMask::geometricBounds(Geom::Affine const &transform) const +{ + Geom::OptRect bbox; + + for (auto &c : children) { + if (auto item = cast<SPItem>(&c)) { + bbox.unionWith(item->geometricBounds(item->transform * transform)); + } + } + + return bbox; +} + +Geom::OptRect SPMask::visualBounds(Geom::Affine const &transform) const +{ + Geom::OptRect bbox; + + for (auto &c : children) { + if (auto item = cast<SPItem>(&c)) { + bbox.unionWith(item->visualBounds(item->transform * transform)); + } + } + + return bbox; +} + +void SPMask::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + SPObjectGroup::child_added(child, ref); + + if (auto item = cast<SPItem>(document->getObjectByRepr(child))) { + for (auto &v : views) { + auto ac = item->invoke_show(v.drawingitem->drawing(), v.key, SP_ITEM_REFERENCE_FLAGS); + if (ac) { + // Fixme: Must take position into account. + v.drawingitem->prependChild(ac); + } + } + } +} + +void SPMask::update(SPCtx *ctx, unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + for (auto child : childList(true)) { + if (cflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->updateDisplay(ctx, cflags); + } + sp_object_unref(child); + } + + for (auto &v : views) { + update_view(v); + } +} + +void SPMask::update_view(View &v) +{ + if (maskContentUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && v.bbox) { + v.drawingitem->setChildTransform(Geom::Scale(v.bbox->dimensions()) * Geom::Translate(v.bbox->min())); + } else { + v.drawingitem->setChildTransform(Geom::identity()); + } +} + +void SPMask::modified(unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + for (auto child : childList(true)) { + if (cflags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(cflags); + } + sp_object_unref(child); + } +} + +Inkscape::XML::Node *SPMask::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:mask"); + } + + SPObjectGroup::write(xml_doc, repr, flags); + + return repr; +} + +// Create a mask element (using passed elements), add it to <defs> +char const *SPMask::create(std::vector<Inkscape::XML::Node*> &reprs, SPDocument *document) +{ + auto defsrepr = document->getDefs()->getRepr(); + + auto xml_doc = document->getReprDoc(); + auto repr = xml_doc->createElement("svg:mask"); + repr->setAttribute("maskUnits", "userSpaceOnUse"); + + defsrepr->appendChild(repr); + char const *mask_id = repr->attribute("id"); + auto mask_object = document->getObjectById(mask_id); + + for (auto node : reprs) { + mask_object->appendChildRepr(node); + } + + if (repr != defsrepr->lastChild()) { + defsrepr->changeOrder(repr, defsrepr->lastChild()); // workaround for bug 989084 + } + + Inkscape::GC::release(repr); + return mask_id; +} + +Inkscape::DrawingItem *SPMask::show(Inkscape::Drawing &drawing, unsigned key, Geom::OptRect const &bbox) +{ + views.emplace_back(make_drawingitem<Inkscape::DrawingGroup>(drawing), bbox, key); + auto &v = views.back(); + auto root = v.drawingitem.get(); + + for (auto &child : children) { + if (auto item = cast<SPItem>(&child)) { + auto ac = item->invoke_show(drawing, key, SP_ITEM_REFERENCE_FLAGS); + if (ac) { + root->appendChild(ac); + } + } + } + + update_view(v); + + return root; +} + +void SPMask::hide(unsigned key) +{ + for (auto &child : children) { + if (auto item = cast<SPItem>(&child)) { + item->invoke_hide(key); + } + } + + auto it = std::find_if(views.begin(), views.end(), [=] (auto &v) { + return v.key == key; + }); + assert(it != views.end()); + + views.erase(it); +} + +void SPMask::setBBox(unsigned key, Geom::OptRect const &bbox) +{ + auto it = std::find_if(views.begin(), views.end(), [=] (auto &v) { + return v.key == key; + }); + assert(it != views.end()); + auto &v = *it; + + v.bbox = bbox; + update_view(v); +} + +SPMask::View::View(DrawingItemPtr<Inkscape::DrawingGroup> drawingitem, Geom::OptRect const &bbox, unsigned key) + : drawingitem(std::move(drawingitem)) + , bbox(bbox) + , key(key) {} + +bool SPMaskReference::_acceptObject(SPObject *obj) const +{ + if (!is<SPMask>(obj)) { + return false; + } + + if (URIReference::_acceptObject(obj)) { + return true; + } + + auto const owner = getOwner(); + //XML Tree being used directly here while it shouldn't be... + auto const owner_repr = owner->getRepr(); + //XML Tree being used directly here while it shouldn't be... + auto const obj_repr = obj->getRepr(); + char const *owner_name = ""; + char const *owner_mask = ""; + char const *obj_name = ""; + char const *obj_id = ""; + if (owner_repr) { + owner_name = owner_repr->name(); + owner_mask = owner_repr->attribute("mask"); + } + if (obj_repr) { + obj_name = obj_repr->name(); + obj_id = obj_repr->attribute("id"); + } + std::printf("WARNING: Ignoring recursive mask reference " + "<%s mask=\"%s\"> in <%s id=\"%s\">", + owner_name, owner_mask, + obj_name, obj_id); + + return false; +} + +/* + 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 : diff --git a/src/object/sp-mask.h b/src/object/sp-mask.h new file mode 100644 index 0000000..96502f9 --- /dev/null +++ b/src/object/sp-mask.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MASK_H +#define SEEN_SP_MASK_H + +/* + * SVG <mask> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <vector> +#include <2geom/rect.h> +#include "sp-object-group.h" +#include "uri-references.h" +#include "display/drawing-item-ptr.h" +#include "xml/node.h" + +namespace Inkscape { +class Drawing; +class DrawingItem; +class DrawingGroup; +} // namespace Inkscape + +class SPMask final + : public SPObjectGroup +{ +public: + SPMask(); + ~SPMask() override; + int tag() const override { return tag_of<decltype(*this)>; } + + bool mask_content_units() const { return maskContentUnits; } + + // Fixme: Hack used by cairo-renderer. + Geom::OptRect get_last_bbox() const { return views.back().bbox; } + + static char const *create(std::vector<Inkscape::XML::Node*> &reprs, SPDocument *document); + + Inkscape::DrawingItem *show(Inkscape::Drawing &drawing, unsigned key, Geom::OptRect const &bbox); + void hide(unsigned key); + void setBBox(unsigned key, Geom::OptRect const &bbox); + + Geom::OptRect geometricBounds(Geom::Affine const &transform) const; + Geom::OptRect visualBounds(Geom::Affine const &transform) const; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const *value) override; + void update(SPCtx *ctx, unsigned flags) override; + void modified(unsigned flags) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned flags) override; + + void child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) override; + +private: + bool maskUnits_set : 1; + bool maskUnits : 1; + + bool maskContentUnits_set : 1; + bool maskContentUnits : 1; + + struct View + { + DrawingItemPtr<Inkscape::DrawingGroup> drawingitem; + Geom::OptRect bbox; + unsigned key; + View(DrawingItemPtr<Inkscape::DrawingGroup> drawingitem, Geom::OptRect const &bbox, unsigned key); + }; + std::vector<View> views; + void update_view(View &v); +}; + +class SPMaskReference + : public Inkscape::URIReference +{ +public: + SPMaskReference(SPObject *obj) + : URIReference(obj) {} + + SPMask *getObject() const + { + return static_cast<SPMask*>(URIReference::getObject()); + } + + sigc::connection modified_connection; + +protected: + /** + * If the owner element of this reference (the element with <... mask="...">) + * is a child of the mask it refers to, return false. + * \return false if obj is not a mask or if obj is a parent of this + * reference's owner element. True otherwise. + */ + bool _acceptObject(SPObject *obj) const override; +}; + +#endif // SEEN_SP_MASK_H diff --git a/src/object/sp-mesh-array.cpp b/src/object/sp-mesh-array.cpp new file mode 100644 index 0000000..33f2a40 --- /dev/null +++ b/src/object/sp-mesh-array.cpp @@ -0,0 +1,3096 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + A group of classes and functions for manipulating mesh gradients. + + A mesh is made up of an array of patches. Each patch has four sides and four corners. The sides can + be shared between two patches and the corners between up to four. + + The order of the points for each side always goes from left to right or top to bottom. + For sides 2 and 3 the points must be reversed when used (as in calls to cairo functions). + + Two patches: (C=corner, S=side, H=handle, T=tensor) + + C0 H1 H2 C1 C0 H1 H2 C1 + + ---------- + ---------- + + | S0 | S0 | + H1 | T0 T1 |H1 T0 T1 | H1 + |S3 S1|S3 S1| + H2 | T3 T2 |H2 T3 T2 | H2 + | S2 | S2 | + + ---------- + ---------- + + C3 H1 H2 C2 C3 H1 H2 C2 + + The mesh is stored internally as an array of nodes that includes the tensor nodes. + + Note: This code uses tensor points which are not part of the SVG2 plan at the moment. + Including tensor points was motivated by a desire to experiment with their usefulness + in smoothing color transitions. There doesn't seem to be much advantage for that + purpose. However including them internally allows for storing all the points in + an array which simplifies things like inserting new rows or columns. +*/ + +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2012, 2015 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm.h> +#include <set> + +// For color picking +#include "display/drawing.h" +#include "display/drawing-context.h" +#include "display/cairo-utils.h" +#include "document.h" +#include "sp-root.h" + +#include "sp-mesh-gradient.h" +#include "sp-mesh-array.h" +#include "sp-mesh-row.h" +#include "sp-mesh-patch.h" +#include "sp-stop.h" +#include "display/curve.h" + +// For new mesh creation +#include "preferences.h" +#include "sp-ellipse.h" +#include "sp-star.h" + +// For writing color/opacity to style +#include "svg/css-ostringstream.h" + +// For default color +#include "style.h" +#include "svg/svg-color.h" + + +// Includes bezier-curve.h, ray.h, crossing.h +#include "2geom/line.h" + +#include "xml/repr.h" +#include <cmath> +#include <algorithm> + +enum { ROW, COL }; + +SPMeshPatchI::SPMeshPatchI( std::vector<std::vector< SPMeshNode* > > * n, int r, int c ) { + + nodes = n; + row = r*3; // Convert from patch array to node array + col = c*3; + + guint i = 0; + if( row != 0 ) i = 1; + for( ; i < 4; ++i ) { + if( nodes->size() < row+i+1 ) { + std::vector< SPMeshNode* > row; + nodes->push_back( row ); + } + + guint j = 0; + if( col != 0 ) j = 1; + for( ; j < 4; ++j ) { + if( (*nodes)[row+i].size() < col+j+1 ){ + SPMeshNode* node = new SPMeshNode; + // Ensure all nodes know their type. + node->node_type = MG_NODE_TYPE_HANDLE; + if( (i == 0 || i == 3) && (j == 0 || j == 3 ) ) node->node_type = MG_NODE_TYPE_CORNER; + if( (i == 1 || i == 2) && (j == 1 || j == 2 ) ) node->node_type = MG_NODE_TYPE_TENSOR; + (*nodes)[row+i].push_back( node ); + } + } + } +} + +/** + Returns point for side in proper order for patch +*/ +Geom::Point SPMeshPatchI::getPoint( guint s, guint pt ) { + + assert( s < 4 ); + assert( pt < 4 ); + + Geom::Point p; + switch ( s ) { + case 0: + p = (*nodes)[ row ][ col+pt ]->p; + break; + case 1: + p = (*nodes)[ row+pt ][ col+3 ]->p; + break; + case 2: + p = (*nodes)[ row+3 ][ col+3-pt ]->p; + break; + case 3: + p = (*nodes)[ row+3-pt ][ col ]->p; + break; + } + return p; + +}; + +/** + Returns vector of points for a side in proper order for a patch (clockwise order). +*/ +std::vector< Geom::Point > SPMeshPatchI::getPointsForSide( guint i ) { + + assert( i < 4 ); + + std::vector< Geom::Point> points; + points.push_back( getPoint( i, 0 ) ); + points.push_back( getPoint( i, 1 ) ); + points.push_back( getPoint( i, 2 ) ); + points.push_back( getPoint( i, 3 ) ); + return points; +}; + + +/** + Set point for side in proper order for patch +*/ +void SPMeshPatchI::setPoint( guint s, guint pt, Geom::Point p, bool set ) { + + assert( s < 4 ); + assert( pt < 4 ); + + NodeType node_type = MG_NODE_TYPE_CORNER; + if( pt == 1 || pt == 2 ) node_type = MG_NODE_TYPE_HANDLE; + + // std::cout << "SPMeshPatchI::setPoint: s: " << s + // << " pt: " << pt + // << " p: " << p + // << " node_type: " << node_type + // << " set: " << set + // << " row: " << row + // << " col: " << col << std::endl; + switch ( s ) { + case 0: + (*nodes)[ row ][ col+pt ]->p = p; + (*nodes)[ row ][ col+pt ]->set = set; + (*nodes)[ row ][ col+pt ]->node_type = node_type; + break; + case 1: + (*nodes)[ row+pt ][ col+3 ]->p = p; + (*nodes)[ row+pt ][ col+3 ]->set = set; + (*nodes)[ row+pt ][ col+3 ]->node_type = node_type; + break; + case 2: + (*nodes)[ row+3 ][ col+3-pt ]->p = p; + (*nodes)[ row+3 ][ col+3-pt ]->set = set; + (*nodes)[ row+3 ][ col+3-pt ]->node_type = node_type; + break; + case 3: + (*nodes)[ row+3-pt ][ col ]->p = p; + (*nodes)[ row+3-pt ][ col ]->set = set; + (*nodes)[ row+3-pt ][ col ]->node_type = node_type; + break; + } + +}; + +/** + Get path type for side (stored in handle nodes). +*/ +gchar SPMeshPatchI::getPathType( guint s ) { + + assert( s < 4 ); + + gchar type = 'x'; + + switch ( s ) { + case 0: + type = (*nodes)[ row ][ col+1 ]->path_type; + break; + case 1: + type = (*nodes)[ row+1 ][ col+3 ]->path_type; + break; + case 2: + type = (*nodes)[ row+3 ][ col+2 ]->path_type; + break; + case 3: + type = (*nodes)[ row+2 ][ col ]->path_type; + break; + } + + return type; +}; + +/** + Set path type for side (stored in handle nodes). +*/ +void SPMeshPatchI::setPathType( guint s, gchar t ) { + + assert( s < 4 ); + + switch ( s ) { + case 0: + (*nodes)[ row ][ col+1 ]->path_type = t; + (*nodes)[ row ][ col+2 ]->path_type = t; + break; + case 1: + (*nodes)[ row+1 ][ col+3 ]->path_type = t; + (*nodes)[ row+2 ][ col+3 ]->path_type = t; + break; + case 2: + (*nodes)[ row+3 ][ col+1 ]->path_type = t; + (*nodes)[ row+3 ][ col+2 ]->path_type = t; + break; + case 3: + (*nodes)[ row+1 ][ col ]->path_type = t; + (*nodes)[ row+2 ][ col ]->path_type = t; + break; + } + +}; + +/** + Set tensor control point for "corner" i. + */ +void SPMeshPatchI::setTensorPoint( guint i, Geom::Point p ) { + + assert( i < 4 ); + switch ( i ) { + case 0: + (*nodes)[ row + 1 ][ col + 1 ]->p = p; + (*nodes)[ row + 1 ][ col + 1 ]->set = true; + (*nodes)[ row + 1 ][ col + 1 ]->node_type = MG_NODE_TYPE_TENSOR; + break; + case 1: + (*nodes)[ row + 1 ][ col + 2 ]->p = p; + (*nodes)[ row + 1 ][ col + 2 ]->set = true; + (*nodes)[ row + 1 ][ col + 2 ]->node_type = MG_NODE_TYPE_TENSOR; + break; + case 2: + (*nodes)[ row + 2 ][ col + 2 ]->p = p; + (*nodes)[ row + 2 ][ col + 2 ]->set = true; + (*nodes)[ row + 2 ][ col + 2 ]->node_type = MG_NODE_TYPE_TENSOR; + break; + case 3: + (*nodes)[ row + 2 ][ col + 1 ]->p = p; + (*nodes)[ row + 2 ][ col + 1 ]->set = true; + (*nodes)[ row + 2 ][ col + 1 ]->node_type = MG_NODE_TYPE_TENSOR; + break; + } +} + +/** + Return if any tensor control point is set. + */ +bool SPMeshPatchI::tensorIsSet() { + for( guint i = 0; i < 4; ++i ) { + if( tensorIsSet( i ) ) { + return true; + } + } + return false; +} + +/** + Return if tensor control point for "corner" i is set. + */ +bool SPMeshPatchI::tensorIsSet( unsigned int i ) { + + assert( i < 4 ); + + bool set = false; + switch ( i ) { + case 0: + set = (*nodes)[ row + 1 ][ col + 1 ]->set; + break; + case 1: + set = (*nodes)[ row + 1 ][ col + 2 ]->set; + break; + case 2: + set = (*nodes)[ row + 2 ][ col + 2 ]->set; + break; + case 3: + set = (*nodes)[ row + 2 ][ col + 1 ]->set; + break; + } + return set; +} + +/** + Return tensor control point for "corner" i. + If not set, returns calculated (Coons) point. + */ +Geom::Point SPMeshPatchI::getTensorPoint( guint k ) { + + assert( k < 4 ); + + guint i = 0; + guint j = 0; + + + switch ( k ) { + case 0: + i = 1; + j = 1; + break; + case 1: + i = 1; + j = 2; + break; + case 2: + i = 2; + j = 2; + break; + case 3: + i = 2; + j = 1; + break; + } + + Geom::Point p; + if( (*nodes)[ row + i ][ col + j ]->set ) { + p = (*nodes)[ row + i ][ col + j ]->p; + } else { + p = coonsTensorPoint( k ); + } + return p; +} + +/** + Find default tensor point (equivalent point to Coons Patch). + Formulas defined in PDF spec. + Equivalent to 1/3 of side length from corner for square patch. + */ +Geom::Point SPMeshPatchI::coonsTensorPoint( guint i ) { + + Geom::Point t; + Geom::Point p[4][4]; // Points in PDF notation + + p[0][0] = getPoint( 0, 0 ); + p[0][1] = getPoint( 0, 1 ); + p[0][2] = getPoint( 0, 2 ); + p[0][3] = getPoint( 0, 3 ); + p[1][0] = getPoint( 3, 2 ); + p[1][3] = getPoint( 1, 1 ); + p[2][0] = getPoint( 3, 1 ); + p[2][3] = getPoint( 1, 2 ); + p[3][0] = getPoint( 2, 3 ); + p[3][1] = getPoint( 2, 2 ); + p[3][2] = getPoint( 2, 1 ); + p[3][3] = getPoint( 2, 0 ); + + switch ( i ) { + case 0: + t = ( -4.0 * p[0][0] + + 6.0 * ( p[0][1] + p[1][0] ) + + -2.0 * ( p[0][3] + p[3][0] ) + + 3.0 * ( p[3][1] + p[1][3] ) + + -1.0 * p[3][3] ) / 9.0; + break; + + case 1: + t = ( -4.0 * p[0][3] + + 6.0 * ( p[0][2] + p[1][3] ) + + -2.0 * ( p[0][0] + p[3][3] ) + + 3.0 * ( p[3][2] + p[1][0] ) + + -1.0 * p[3][0] ) / 9.0; + break; + + case 2: + t = ( -4.0 * p[3][3] + + 6.0 * ( p[3][2] + p[2][3] ) + + -2.0 * ( p[3][0] + p[0][3] ) + + 3.0 * ( p[0][2] + p[2][0] ) + + -1.0 * p[0][0] ) / 9.0; + break; + + case 3: + t = ( -4.0 * p[3][0] + + 6.0 * ( p[3][1] + p[2][0] ) + + -2.0 * ( p[3][3] + p[0][0] ) + + 3.0 * ( p[0][1] + p[2][3] ) + + -1.0 * p[0][3] ) / 9.0; + break; + + default: + + g_warning( "Impossible!" ); + + } + return t; +} + +/** + Update default values for handle and tensor nodes. +*/ +void SPMeshPatchI::updateNodes() { + + // std::cout << "SPMeshPatchI::updateNodes: " << row << "," << col << std::endl; + // Handles first (tensors require update handles). + for( guint i = 0; i < 4; ++i ) { + for( guint j = 0; j < 4; ++j ) { + if( (*nodes)[ row + i ][ col + j ]->set == false ) { + + if( (*nodes)[ row + i ][ col + j ]->node_type == MG_NODE_TYPE_HANDLE ) { + + // If a handle is not set it is because the side is a line. + // Set node points 1/3 of the way between corners. + + if( i == 0 || i == 3 ) { + Geom::Point p0 = ( (*nodes)[ row + i ][ col ]->p ); + Geom::Point p3 = ( (*nodes)[ row + i ][ col + 3 ]->p ); + Geom::Point dp = (p3 - p0)/3.0; + if( j == 2 ) dp *= 2.0; + (*nodes)[ row + i ][ col + j ]->p = p0 + dp; + } + + if( j == 0 || j == 3 ) { + Geom::Point p0 = ( (*nodes)[ row ][ col + j ]->p ); + Geom::Point p3 = ( (*nodes)[ row + 3 ][ col + j ]->p ); + Geom::Point dp = (p3 - p0)/3.0; + if( i == 2 ) dp *= 2.0; + (*nodes)[ row + i ][ col + j ]->p = p0 + dp; + } + } + } + } + } + + // Update tensor nodes + for( guint i = 1; i < 3; ++i ) { + for( guint j = 1; j < 3; ++j ) { + if( (*nodes)[ row + i ][ col + j ]->set == false ) { + + (*nodes)[ row + i ][ col + j ]->node_type = MG_NODE_TYPE_TENSOR; + + guint t = 0; + if( i == 1 && j == 2 ) t = 1; + if( i == 2 && j == 2 ) t = 2; + if( i == 2 && j == 1 ) t = 3; + (*nodes)[ row + i ][ col + j ]->p = coonsTensorPoint( t ); + // std::cout << "Update node: " << i << ", " << j << " " << coonsTensorPoint( t ) << std::endl; + + } + } + } +} + +/** + Return color for corner of patch. +*/ +SPColor SPMeshPatchI::getColor( guint i ) { + + assert( i < 4 ); + + SPColor color; + switch ( i ) { + case 0: + color = (*nodes)[ row ][ col ]->color; + break; + case 1: + color = (*nodes)[ row ][ col+3 ]->color; + break; + case 2: + color = (*nodes)[ row+3 ][ col+3 ]->color; + break; + case 3: + color = (*nodes)[ row+3 ][ col ]->color; + break; + + } + + return color; + +}; + +/** + Set color for corner of patch. +*/ +void SPMeshPatchI::setColor( guint i, SPColor color ) { + + assert( i < 4 ); + + switch ( i ) { + case 0: + (*nodes)[ row ][ col ]->color = color; + break; + case 1: + (*nodes)[ row ][ col+3 ]->color = color; + break; + case 2: + (*nodes)[ row+3 ][ col+3 ]->color = color; + break; + case 3: + (*nodes)[ row+3 ][ col ]->color = color; + break; + } +}; + +/** + Return opacity for corner of patch. +*/ +gdouble SPMeshPatchI::getOpacity( guint i ) { + + assert( i < 4 ); + + gdouble opacity = 0.0; + switch ( i ) { + case 0: + opacity = (*nodes)[ row ][ col ]->opacity; + break; + case 1: + opacity = (*nodes)[ row ][ col+3 ]->opacity; + break; + case 2: + opacity = (*nodes)[ row+3 ][ col+3 ]->opacity; + break; + case 3: + opacity = (*nodes)[ row+3 ][ col ]->opacity; + break; + } + + return opacity; +}; + + +/** + Set opacity for corner of patch. +*/ +void SPMeshPatchI::setOpacity( guint i, gdouble opacity ) { + + assert( i < 4 ); + + switch ( i ) { + case 0: + (*nodes)[ row ][ col ]->opacity = opacity; + break; + case 1: + (*nodes)[ row ][ col+3 ]->opacity = opacity; + break; + case 2: + (*nodes)[ row+3 ][ col+3 ]->opacity = opacity; + break; + case 3: + (*nodes)[ row+3 ][ col ]->opacity = opacity; + break; + + } + +}; + + +/** + Return stop pointer for corner of patch. +*/ +SPStop* SPMeshPatchI::getStopPtr( guint i ) { + + assert( i < 4 ); + + SPStop* stop = nullptr; + switch ( i ) { + case 0: + stop = (*nodes)[ row ][ col ]->stop; + break; + case 1: + stop = (*nodes)[ row ][ col+3 ]->stop; + break; + case 2: + stop = (*nodes)[ row+3 ][ col+3 ]->stop; + break; + case 3: + stop = (*nodes)[ row+3 ][ col ]->stop; + break; + } + + return stop; +}; + + +/** + Set stop pointer for corner of patch. +*/ +void SPMeshPatchI::setStopPtr( guint i, SPStop* stop ) { + + assert( i < 4 ); + + switch ( i ) { + case 0: + (*nodes)[ row ][ col ]->stop = stop; + break; + case 1: + (*nodes)[ row ][ col+3 ]->stop = stop; + break; + case 2: + (*nodes)[ row+3 ][ col+3 ]->stop = stop; + break; + case 3: + (*nodes)[ row+3 ][ col ]->stop = stop; + break; + + } + +}; + + +SPMeshNodeArray::SPMeshNodeArray( SPMeshGradient *mg ) { + + read( mg ); + +}; + + +// Copy constructor +SPMeshNodeArray::SPMeshNodeArray( const SPMeshNodeArray& rhs ) : + nodes(rhs.nodes) // This only copies the pointers but it does size the vector of vectors. +{ + + built = false; + mg = nullptr; + draggers_valid = false; + + for( unsigned i=0; i < nodes.size(); ++i ) { + for( unsigned j=0; j < nodes[i].size(); ++j ) { + nodes[i][j] = new SPMeshNode( *rhs.nodes[i][j] ); // Copy data. + } + } +}; + + +// Copy assignment operator +SPMeshNodeArray& SPMeshNodeArray::operator=( const SPMeshNodeArray& rhs ) { + + if( this == &rhs ) return *this; + + clear(); // Clear any existing array. + + built = false; + mg = nullptr; + draggers_valid = false; + + nodes = rhs.nodes; // This only copies the pointers but it does size the vector of vectors. + + for( unsigned i=0; i < nodes.size(); ++i ) { + for( unsigned j=0; j < nodes[i].size(); ++j ) { + nodes[i][j] = new SPMeshNode( *rhs.nodes[i][j] ); // Copy data. + } + } + + return *this; +}; + +// Fill array with data from mesh objects. +// Returns true of array's dimensions unchanged. +bool SPMeshNodeArray::read( SPMeshGradient *mg_in ) { + + mg = mg_in; + auto mg_array = cast<SPMeshGradient>(mg->getArray()); + if (!mg_array) { + std::cerr << "SPMeshNodeArray::read: No mesh array!" << std::endl; + return false; + } + // std::cout << "SPMeshNodeArray::read: " << mg_in << " array: " << mg_array << std::endl; + + // Count rows and columns, if unchanged reuse array to keep draggers valid. + unsigned cols = 0; + unsigned rows = 0; + for (auto& ro: mg_array->children) { + if (is<SPMeshrow>(&ro)) { + ++rows; + if (rows == 1 ) { + for (auto& po: ro.children) { + if (is<SPMeshpatch>(&po)) { + ++cols; + } + } + } + } + } + bool same_size = true; + if (cols != patch_columns() || rows != patch_rows() ) { + // Draggers will be invalidated. + same_size = false; + clear(); + draggers_valid = false; + } + + Geom::Point current_p( mg->x.computed, mg->y.computed ); + // std::cout << "SPMeshNodeArray::read: p: " << current_p << std::endl; + + guint max_column = 0; + guint irow = 0; // Corresponds to top of patch being read in. + for (auto& ro: mg_array->children) { + + if (is<SPMeshrow>(&ro)) { + + guint icolumn = 0; // Corresponds to left of patch being read in. + for (auto& po: ro.children) { + + if (is<SPMeshpatch>(&po)) { + + auto patch = cast<SPMeshpatch>(&po); + + // std::cout << "SPMeshNodeArray::read: row size: " << nodes.size() << std::endl; + SPMeshPatchI new_patch( &nodes, irow, icolumn ); // Adds new nodes. + // std::cout << " after: " << nodes.size() << std::endl; + + gint istop = 0; + + // Only 'top' side defined for first row. + if( irow != 0 ) ++istop; + + for (auto& so: po.children) { + if (is<SPStop>(&so)) { + + if( istop > 3 ) { + // std::cout << " Mesh Gradient: Too many stops: " << istop << std::endl; + break; + } + + auto stop = cast<SPStop>(&so); + + // Handle top of first row. + if( istop == 0 && icolumn == 0 ) { + // First patch in mesh. + new_patch.setPoint( 0, 0, current_p ); + } + // First point is always already defined by previous side (stop). + current_p = new_patch.getPoint( istop, 0 ); + + // If side closes patch, then we read one less point. + bool closed = false; + if( icolumn == 0 && istop == 3 ) closed = true; + if( icolumn > 0 && istop == 2 ) closed = true; + + + // Copy path and then replace commas by spaces so we can use stringstream to parse + std::string path_string = stop->path_string.raw(); + std::replace(path_string.begin(),path_string.end(),',',' '); + + // std::cout << " path_string: " << path_string << std::endl; + // std::cout << " current_p: " << current_p << std::endl; + + std::stringstream os( path_string ); + + // Determine type of path + char path_type; + os >> path_type; + new_patch.setPathType( istop, path_type ); + + gdouble x, y; + Geom::Point p, dp; + guint max; + switch ( path_type ) { + case 'l': + if( !closed ) { + os >> x >> y; + if( !os.fail() ) { + dp = Geom::Point( x, y ); + new_patch.setPoint( istop, 3, current_p + dp ); + } else { + std::cerr << "Failed to read l" << std::endl; + } + } + // To facilitate some side operations, set handles to 1/3 and + // 2/3 distance between corner points but flag as unset. + p = new_patch.getPoint( istop, 3 ); + dp = (p - current_p)/3.0; // Calculate since may not be set if closed. + // std::cout << " istop: " << istop + // << " dp: " << dp + // << " p: " << p + // << " current_p: " << current_p + // << std::endl; + new_patch.setPoint( istop, 1, current_p + dp, false ); + new_patch.setPoint( istop, 2, current_p + 2.0 * dp, false ); + break; + case 'L': + if( !closed ) { + os >> x >> y; + if( !os.fail() ) { + p = Geom::Point( x, y ); + new_patch.setPoint( istop, 3, p ); + } else { + std::cerr << "Failed to read L" << std::endl; + } + } + // To facilitate some side operations, set handles to 1/3 and + // 2/3 distance between corner points but flag as unset. + p = new_patch.getPoint( istop, 3 ); + dp = (p - current_p)/3.0; + new_patch.setPoint( istop, 1, current_p + dp, false ); + new_patch.setPoint( istop, 2, current_p + 2.0 * dp, false ); + break; + case 'c': + max = 4; + if( closed ) max = 3; + for( guint i = 1; i < max; ++i ) { + os >> x >> y; + if( !os.fail() ) { + p = Geom::Point( x, y ); + p += current_p; + new_patch.setPoint( istop, i, p ); + } else { + std::cerr << "Failed to read c: " << i << std::endl; + } + } + break; + case 'C': + max = 4; + if( closed ) max = 3; + for( guint i = 1; i < max; ++i ) { + os >> x >> y; + if( !os.fail() ) { + p = Geom::Point( x, y ); + new_patch.setPoint( istop, i, p ); + } else { + std::cerr << "Failed to read C: " << i << std::endl; + } + } + break; + default: + // should not reach + std::cerr << "Path Error: unhandled path type: " << path_type << std::endl; + } + current_p = new_patch.getPoint( istop, 3 ); + + // Color + if( (istop == 0 && irow == 0 && icolumn > 0) || (istop == 1 && irow > 0 ) ) { + // skip + } else { + SPColor color = stop->getColor(); + double opacity = stop->getOpacity(); + new_patch.setColor( istop, color ); + new_patch.setOpacity( istop, opacity ); + new_patch.setStopPtr( istop, stop ); + } + ++istop; + } + } // Loop over stops + + // Read in tensor string after stops since tensor nodes defined relative to corner nodes. + + // Copy string and then replace commas by spaces so we can use stringstream to parse XXXX + if( patch->tensor_string ) { + std::string tensor_string = patch->tensor_string->raw(); + std::replace(tensor_string.begin(),tensor_string.end(),',',' '); + + // std::cout << " tensor_string: " << tensor_string << std::endl; + + std::stringstream os( tensor_string ); + for( guint i = 0; i < 4; ++i ) { + double x = 0.0; + double y = 0.0; + os >> x >> y; + if( !os.fail() ) { + new_patch.setTensorPoint( i, new_patch.getPoint( i, 0 ) + Geom::Point( x, y ) ); + } else { + std::cerr << "Failed to read p: " << i << std::endl; + break; + } + } + } + ++icolumn; + if( max_column < icolumn ) max_column = icolumn; + } + } + ++irow; + } + } + + // Insure we have a true array. + for(auto & node : nodes) { + node.resize( max_column * 3 + 1 ); + } + + // Set node edge. + for( guint i = 0; i < nodes.size(); ++i ) { + for( guint j = 0; j < nodes[i].size(); ++j ) { + nodes[i][j]->node_edge = MG_NODE_EDGE_NONE; + if( i == 0 ) nodes[i][j]->node_edge |= MG_NODE_EDGE_TOP; + if( i == nodes.size() - 1 ) nodes[i][j]->node_edge |= MG_NODE_EDGE_BOTTOM; + if( j == 0 ) nodes[i][j]->node_edge |= MG_NODE_EDGE_RIGHT; + if( j == nodes[i].size() - 1 ) nodes[i][j]->node_edge |= MG_NODE_EDGE_LEFT; + } + } + + // std::cout << "SPMeshNodeArray::Read: result:" << std::endl; + // print(); + + built = true; + + return same_size; +}; + +/** + Write repr using our array. +*/ +void SPMeshNodeArray::write( SPMeshGradient *mg ) { + + // std::cout << "SPMeshNodeArray::write: entrance:" << std::endl; + // print(); + using Geom::X; + using Geom::Y; + + auto mg_array = cast<SPMeshGradient>(mg->getArray()); + if (!mg_array) { + // std::cerr << "SPMeshNodeArray::write: missing patches!" << std::endl; + mg_array = mg; + } + + // First we must delete reprs for old mesh rows and patches. We only need to call the + // deleteObject() method, which in turn calls sp_repr_unparent. Since iterators do not play + // well with boost::intrusive::list (which ChildrenList derive from) we need to iterate over a + // copy of the pointers to the objects. + std::vector<SPObject*> children_pointers; + for (auto& row : mg_array->children) { + children_pointers.push_back(&row); + } + + for (auto i : children_pointers) { + i->deleteObject(); + } + + // Now we build new reprs + Inkscape::XML::Node *mesh = mg->getRepr(); + Inkscape::XML::Node *mesh_array = mg_array->getRepr(); + + SPMeshNodeArray* array = &(mg_array->array); + SPMeshPatchI patch0( &(array->nodes), 0, 0 ); + Geom::Point current_p = patch0.getPoint( 0, 0 ); // Side 0, point 0 + + mesh->setAttributeSvgDouble("x", current_p[X] ); + mesh->setAttributeSvgDouble("y", current_p[Y] ); + + Geom::Point current_p2( mg->x.computed, mg->y.computed ); + + Inkscape::XML::Document *xml_doc = mesh->document(); + guint rows = array->patch_rows(); + for( guint i = 0; i < rows; ++i ) { + + // Write row + Inkscape::XML::Node *row = xml_doc->createElement("svg:meshrow"); + mesh_array->appendChild( row ); // No attributes + + guint columns = array->patch_columns(); + for( guint j = 0; j < columns; ++j ) { + + // Write patch + Inkscape::XML::Node *patch = xml_doc->createElement("svg:meshpatch"); + + SPMeshPatchI patchi( &(array->nodes), i, j ); + + // Add tensor + if( patchi.tensorIsSet() ) { + + std::stringstream is; + + for( guint k = 0; k < 4; ++k ) { + Geom::Point p = patchi.getTensorPoint( k ) - patchi.getPoint( k, 0 ); + is << p[X] << "," << p[Y]; + if( k < 3 ) is << " "; + } + + patch->setAttribute("tensor", is.str()); + // std::cout << " SPMeshNodeArray::write: tensor: " << is.str() << std::endl; + } + + row->appendChild( patch ); + + // Write sides + for( guint k = 0; k < 4; ++k ) { + + // Only first row has top stop + if( k == 0 && i != 0 ) continue; + + // Only first column has left stop + if( k == 3 && j != 0 ) continue; + + Inkscape::XML::Node *stop = xml_doc->createElement("svg:stop"); + + // Add path + std::stringstream is; + char path_type = patchi.getPathType( k ); + is << path_type; + + std::vector< Geom::Point> p = patchi.getPointsForSide( k ); + current_p = patchi.getPoint( k, 0 ); + + switch ( path_type ) { + case 'l': + is << " " + << ( p[3][X] - current_p[X] ) << "," + << ( p[3][Y] - current_p[Y] ); + break; + case 'L': + is << " " + << p[3][X] << "," + << p[3][Y]; + break; + case 'c': + is << " " + << ( p[1][X] - current_p[X] ) << "," + << ( p[1][Y] - current_p[Y] ) << " " + << ( p[2][X] - current_p[X] ) << "," + << ( p[2][Y] - current_p[Y] ) << " " + << ( p[3][X] - current_p[X] ) << "," + << ( p[3][Y] - current_p[Y] ); + break; + case 'C': + is << " " + << p[1][X] << "," + << p[1][Y] << " " + << p[2][X] << "," + << p[2][Y] << " " + << p[3][X] << "," + << p[3][Y]; + break; + case 'z': + case 'Z': + std::cerr << "SPMeshNodeArray::write(): bad path type" << path_type << std::endl; + break; + default: + std::cerr << "SPMeshNodeArray::write(): unhandled path type" << path_type << std::endl; + } + stop->setAttribute("path", is.str()); + // std::cout << "SPMeshNodeArray::write: path: " << is.str().c_str() << std::endl; + // Add stop-color + if( ( k == 0 && i == 0 && j == 0 ) || + ( k == 1 && i == 0 ) || + ( k == 2 ) || + ( k == 3 && j == 0 ) ) { + + // Why are we setting attribute and not style? + //stop->setAttribute("stop-color", patchi.getColor(k).toString() ); + //stop->setAttribute("stop-opacity", patchi.getOpacity(k) ); + + Inkscape::CSSOStringStream os; + os << "stop-color:" << patchi.getColor(k).toString() << ";stop-opacity:" << patchi.getOpacity(k); + stop->setAttribute("style", os.str()); + } + patch->appendChild( stop ); + } + } + } +} + +/** + * Find default color based on colors in existing fill. + */ +static SPColor default_color( SPItem *item ) { + + SPColor color( 0.5, 0.0, 0.5 ); + + if ( item->style ) { + SPIPaint const &paint = ( item->style->fill ); // Could pick between style.fill/style.stroke + if ( paint.isColor() ) { + color = paint.value.color; + } else if ( paint.isPaintserver() ) { + auto *server = item->style->getFillPaintServer(); + auto gradient = cast<SPGradient>(server); + if (gradient && gradient->getVector()) { + SPStop *firstStop = gradient->getVector()->getFirstStop(); + if ( firstStop ) { + color = firstStop->getColor(); + } + } + } + } else { + std::cerr << " SPMeshNodeArray: default_color(): No style" << std::endl; + } + + return color; +} + +/** + Create a default mesh. +*/ +void SPMeshNodeArray::create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bbox ) { + + // std::cout << "SPMeshNodeArray::create: Entrance" << std::endl; + + if( !bbox ) { + // Set default size to bounding box if size not given. + std::cerr << "SPMeshNodeArray::create(): bbox empty" << std::endl; + bbox = item->geometricBounds(); + + if( !bbox ) { + std::cerr << "SPMeshNodeArray::create: ERROR: No bounding box!" << std::endl; + return; + } + } + + Geom::Coord const width = bbox->dimensions()[Geom::X]; + Geom::Coord const height = bbox->dimensions()[Geom::Y]; + Geom::Point center = bbox->midpoint(); + + // Must keep repr and array in sync. We have two choices: + // Build the repr first and then "read" it. + // Construct the array and then "write" it. + // We'll do the second. + + // Remove any existing mesh. We could choose to simply scale an existing mesh... + //clear(); + + // We get called twice when a new mesh is created...WHY? + // return if we've already constructed the mesh. + if( !nodes.empty() ) return; + + // Set 'gradientUnits'. Our calculations assume "userSpaceOnUse". + Inkscape::XML::Node *repr = mg->getRepr(); + repr->setAttribute("gradientUnits", "userSpaceOnUse"); + + // Get default color + SPColor color = default_color( item ); + + // Set some corners to white so we can see the mesh. + SPColor white( 1.0, 1.0, 1.0 ); + if (color == white) { + // If default color is white, set other color to black. + white = SPColor( 0.0, 0.0, 0.0 ); + } + + // Get preferences + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint prows = prefs->getInt("/tools/mesh/mesh_rows", 1); + guint pcols = prefs->getInt("/tools/mesh/mesh_cols", 1); + + SPMeshGeometry mesh_type = + (SPMeshGeometry) prefs->getInt("/tools/mesh/mesh_geometry", SP_MESH_GEOMETRY_NORMAL); + + if( mesh_type == SP_MESH_GEOMETRY_CONICAL ) { + + // Conical gradient.. for any shape/path using geometric bounding box. + + gdouble rx = width/2.0; + gdouble ry = height/2.0; + + // Start and end angles + gdouble start = 0.0; + gdouble end = 2.0 * M_PI; + + if ( is<SPStar>( item ) ) { + // But if it is a star... use star parameters! + auto star = cast<SPStar>( item ); + center = star->center; + rx = star->r[0]; + ry = star->r[0]; + start = star->arg[0]; + end = start + 2.0 * M_PI; + } + + if ( is<SPGenericEllipse>( item ) ) { + // For arcs use set start/stop + auto arc = cast<SPGenericEllipse>( item ); + center[Geom::X] = arc->cx.computed; + center[Geom::Y] = arc->cy.computed; + rx = arc->rx.computed; + ry = arc->ry.computed; + start = arc->start; + end = arc->end; + if( end <= start ) { + end += 2.0 * M_PI; + } + } + + // std::cout << " start: " << start << " end: " << end << std::endl; + + // IS THIS NECESSARY? + repr->setAttributeSvgDouble("x", center[Geom::X] + rx * cos(start) ); + repr->setAttributeSvgDouble("y", center[Geom::Y] + ry * sin(start) ); + + guint sections = pcols; + + // If less sections, arc approximation error too great. (Check!) + if( sections < 4 ) sections = 4; + + double arc = (end - start) / (double)sections; + + // See: http://en.wikipedia.org/wiki/B%C3%A9zier_curve + gdouble kappa = 4.0/3.0 * tan(arc/4.0); + gdouble lenx = rx * kappa; + gdouble leny = ry * kappa; + + gdouble s = start; + for( guint i = 0; i < sections; ++i ) { + + SPMeshPatchI patch( &nodes, 0, i ); + + gdouble x0 = center[Geom::X] + rx * cos(s); + gdouble y0 = center[Geom::Y] + ry * sin(s); + gdouble x1 = x0 - lenx * sin(s); + gdouble y1 = y0 + leny * cos(s); + + s += arc; + gdouble x3 = center[Geom::X] + rx * cos(s); + gdouble y3 = center[Geom::Y] + ry * sin(s); + gdouble x2 = x3 + lenx * sin(s); + gdouble y2 = y3 - leny * cos(s); + + patch.setPoint( 0, 0, Geom::Point( x0, y0 ) ); + patch.setPoint( 0, 1, Geom::Point( x1, y1 ) ); + patch.setPoint( 0, 2, Geom::Point( x2, y2 ) ); + patch.setPoint( 0, 3, Geom::Point( x3, y3 ) ); + + patch.setPoint( 2, 0, center ); + patch.setPoint( 3, 0, center ); + + for( guint k = 0; k < 4; ++k ) { + patch.setPathType( k, 'l' ); + patch.setColor( k, (i+k)%2 ? color : white ); + patch.setOpacity( k, 1.0 ); + } + patch.setPathType( 0, 'c' ); + + // Set handle and tensor nodes. + patch.updateNodes(); + + } + + split_row( 0, prows ); + + } else { + + // Normal grid meshes + + if( is<SPGenericEllipse>( item ) ) { + + // std::cout << "We've got ourselves an arc!" << std::endl; + + auto arc = cast<SPGenericEllipse>( item ); + center[Geom::X] = arc->cx.computed; + center[Geom::Y] = arc->cy.computed; + gdouble rx = arc->rx.computed; + gdouble ry = arc->ry.computed; + + gdouble s = -3.0/2.0 * M_PI_2; + + repr->setAttributeSvgDouble("x", center[Geom::X] + rx * cos(s) ); + repr->setAttributeSvgDouble("y", center[Geom::Y] + ry * sin(s) ); + + gdouble lenx = rx * 4*tan(M_PI_2/4)/3; + gdouble leny = ry * 4*tan(M_PI_2/4)/3; + + SPMeshPatchI patch( &nodes, 0, 0 ); + for( guint i = 0; i < 4; ++i ) { + + gdouble x0 = center[Geom::X] + rx * cos(s); + gdouble y0 = center[Geom::Y] + ry * sin(s); + gdouble x1 = x0 + lenx * cos(s + M_PI_2); + gdouble y1 = y0 + leny * sin(s + M_PI_2); + + s += M_PI_2; + gdouble x3 = center[Geom::X] + rx * cos(s); + gdouble y3 = center[Geom::Y] + ry * sin(s); + gdouble x2 = x3 + lenx * cos(s - M_PI_2); + gdouble y2 = y3 + leny * sin(s - M_PI_2); + + Geom::Point p1( x1, y1 ); + Geom::Point p2( x2, y2 ); + Geom::Point p3( x3, y3 ); + patch.setPoint( i, 1, p1 ); + patch.setPoint( i, 2, p2 ); + patch.setPoint( i, 3, p3 ); + + patch.setPathType( i, 'c' ); + + patch.setColor( i, i%2 ? color : white ); + patch.setOpacity( i, 1.0 ); + } + + // Fill out tensor points + patch.updateNodes(); + + split_row( 0, prows ); + split_column( 0, pcols ); + + // END Arc + + } else if ( is<SPStar>( item ) ) { + + // Do simplest thing... assume star is not rounded or randomized. + // (It should be easy to handle the rounded/randomized cases by making + // the appropriate star class function public.) + auto star = cast<SPStar>( item ); + guint sides = star->sides; + + // std::cout << "We've got ourselves an star! Sides: " << sides << std::endl; + + Geom::Point p0 = sp_star_get_xy( star, SP_STAR_POINT_KNOT1, 0 ); + repr->setAttributeSvgDouble("x", p0[Geom::X] ); + repr->setAttributeSvgDouble("y", p0[Geom::Y] ); + + for( guint i = 0; i < sides; ++i ) { + + if( star->flatsided ) { + + SPMeshPatchI patch( &nodes, 0, i ); + + patch.setPoint( 0, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT1, i ) ); + guint ii = i+1; + if( ii == sides ) ii = 0; + patch.setPoint( 1, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT1, ii ) ); + patch.setPoint( 2, 0, star->center ); + patch.setPoint( 3, 0, star->center ); + + for( guint s = 0; s < 4; ++s ) { + patch.setPathType( s, 'l' ); + patch.setColor( s, (i+s)%2 ? color : white ); + patch.setOpacity( s, 1.0 ); + } + + // Set handle and tensor nodes. + patch.updateNodes(); + + } else { + + SPMeshPatchI patch0( &nodes, 0, 2*i ); + + patch0.setPoint( 0, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT1, i ) ); + patch0.setPoint( 1, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT2, i ) ); + patch0.setPoint( 2, 0, star->center ); + patch0.setPoint( 3, 0, star->center ); + + guint ii = i+1; + if( ii == sides ) ii = 0; + + SPMeshPatchI patch1( &nodes, 0, 2*i+1 ); + + patch1.setPoint( 0, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT2, i ) ); + patch1.setPoint( 1, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT1, ii ) ); + patch1.setPoint( 2, 0, star->center ); + patch1.setPoint( 3, 0, star->center ); + + for( guint s = 0; s < 4; ++s ) { + patch0.setPathType( s, 'l' ); + patch0.setColor( s, s%2 ? color : white ); + patch0.setOpacity( s, 1.0 ); + patch1.setPathType( s, 'l' ); + patch1.setColor( s, s%2 ? white : color ); + patch1.setOpacity( s, 1.0 ); + } + + // Set handle and tensor nodes. + patch0.updateNodes(); + patch1.updateNodes(); + + } + } + + //print(); + + split_row( 0, prows ); + //split_column( 0, pcols ); + + } else { + + // Generic + + repr->setAttributeSvgDouble("x", bbox->min()[Geom::X]); + repr->setAttributeSvgDouble("y", bbox->min()[Geom::Y]); + + // Get node array size + guint nrows = prows * 3 + 1; + guint ncols = pcols * 3 + 1; + + gdouble dx = width / (gdouble)(ncols-1.0); + gdouble dy = height / (gdouble)(nrows-1.0); + + Geom::Point p0( mg->x.computed, mg->y.computed ); + + for( guint i = 0; i < nrows; ++i ) { + std::vector< SPMeshNode* > row; + for( guint j = 0; j < ncols; ++j ) { + SPMeshNode* node = new SPMeshNode; + node->p = p0 + Geom::Point( j * dx, i * dy ); + + node->node_edge = MG_NODE_EDGE_NONE; + if( i == 0 ) node->node_edge |= MG_NODE_EDGE_TOP; + if( i == nrows -1 ) node->node_edge |= MG_NODE_EDGE_BOTTOM; + if( j == 0 ) node->node_edge |= MG_NODE_EDGE_LEFT; + if( j == ncols -1 ) node->node_edge |= MG_NODE_EDGE_RIGHT; + + if( i%3 == 0 ) { + + if( j%3 == 0) { + // Corner + node->node_type = MG_NODE_TYPE_CORNER; + node->set = true; + node->color = (i+j)%2 ? color : white; + node->opacity = 1.0; + + } else { + // Side + node->node_type = MG_NODE_TYPE_HANDLE; + node->set = true; + node->path_type = 'c'; + } + + } else { + + if( j%3 == 0) { + // Side + node->node_type = MG_NODE_TYPE_HANDLE; + node->set = true; + node->path_type = 'c'; + } else { + // Tensor + node->node_type = MG_NODE_TYPE_TENSOR; + node->set = false; + } + + } + + row.push_back( node ); + } + nodes.push_back( row ); + } + // End normal + } + + } // If conical + + //print(); + + // Write repr + write( mg ); +} + + +/** + Clear mesh gradient. +*/ +void SPMeshNodeArray::clear() { + + for(auto & node : nodes) { + for(auto & j : node) { + if( j ) { + delete j; + } + } + } + nodes.clear(); +}; + + +/** + Print mesh gradient (for debugging). +*/ +void SPMeshNodeArray::print() { + for( guint i = 0; i < nodes.size(); ++i ) { + std::cout << "New node row:" << std::endl; + for( guint j = 0; j < nodes[i].size(); ++j ) { + if( nodes[i][j] ) { + std::cout.width(4); + std::cout << " Node: " << i << "," << j << ": " + << nodes[i][j]->p + << " Node type: " << nodes[i][j]->node_type + << " Node edge: " << nodes[i][j]->node_edge + << " Set: " << nodes[i][j]->set + << " Path type: " << nodes[i][j]->path_type + << " Stop: " << nodes[i][j]->stop + << std::endl; + } else { + std::cout << "Error: missing mesh node." << std::endl; + } + } // Loop over patches + } // Loop over rows +}; + + + +/* +double hermite( const double p0, const double p1, const double m0, const double m1, const double t ) { + double t2 = t*t; + double t3 = t2*t; + + double result = (2.0*t3 - 3.0*t2 +1.0) * p0 + + (t3 - 2.0*t2 + t) * m0 + + (-2.0*t3 + 3.0*t2) * p1 + + (t3 -t2) * m1; + + return result; +} +*/ + +class SPMeshSmoothCorner { + +public: + SPMeshSmoothCorner() { + for(auto & i : g) { + for( unsigned j = 0; j < 4; ++j ) { + i[j] = 0; + } + } + } + + double g[3][8]; // 3 colors, 8 parameters: see enum. + Geom::Point p; // Location of point +}; + +// Find slope at point 1 given values at previous and next points +// Return value is slope in user space +double find_slope1( const double &p0, const double &p1, const double &p2, + const double &d01, const double &d12 ) { + + double slope = 0; + + if( d01 > 0 && d12 > 0 ) { + slope = 0.5 * ( (p1 - p0)/d01 + (p2 - p1)/d12 ); + + if( ( p0 > p1 && p1 < p2 ) || + ( p0 < p1 && p1 > p2 ) ) { + // At minimum or maximum, use slope of zero + slope = 0; + } else { + // Ensure we don't overshoot + if( fabs(slope) > fabs(3*(p1-p0)/d01) ) { + slope = 3*(p1-p0)/d01; + } + if( fabs(slope) > fabs(3*(p2-p1)/d12) ) { + slope = 3*(p2-p1)/d12; + } + } + } else { + // Do something clever + } + return slope; +}; + + +/* +// Find slope at point 0 given values at previous and next points +// TO DO: TAKE DISTANCE BETWEEN POINTS INTO ACCOUNT +double find_slope2( double pmm, double ppm, double pmp, double ppp, double p0 ) { + + // pmm == d[i-1][j-1], ... 'm' is minus, 'p' is plus + double slope = (ppp - ppm - pmp + pmm)/2.0; + if( (ppp > p0 && ppm > p0 && pmp > p0 && pmm > 0) || + (ppp < p0 && ppm < p0 && pmp < p0 && pmm < 0) ) { + // At minimum or maximum, use slope of zero + slope = 0; + } else { + // Don't really know what to do here + if( fabs(slope) > fabs(3*(ppp-p0)) ) { + slope = 3*(ppp-p0); + } + if( fabs(slope) > fabs(3*(pmp-p0)) ) { + slope = 3*(pmp-p0); + } + if( fabs(slope) > fabs(3*(ppm-p0)) ) { + slope = 3*(ppm-p0); + } + if( fabs(slope) > fabs(3*(pmm-p0)) ) { + slope = 3*(pmm-p0); + } + } + return slope; +} +*/ + +// https://en.wikipedia.org/wiki/Bicubic_interpolation +void invert( const double v[16], double alpha[16] ) { + + const double A[16][16] = { + + { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + {-3, 3, 0, 0, -2,-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 2,-2, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, -3, 3, 0, 0, -2,-1, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 2,-2, 0, 0, 1, 1, 0, 0 }, + {-3, 0, 3, 0, 0, 0, 0, 0, -2, 0,-1, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, -3, 0, 3, 0, 0, 0, 0, 0, -2, 0,-1, 0 }, + { 9,-9,-9, 9, 6, 3,-6,-3, 6,-6, 3,-3, 4, 2, 2, 1 }, + {-6, 6, 6,-6, -3,-3, 3, 3, -4, 4,-2, 2, -2,-2,-1,-1 }, + { 2, 0,-2, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 2, 0,-2, 0, 0, 0, 0, 0, 1, 0, 1, 0 }, + {-6, 6, 6,-6, -4,-2, 4, 2, -3, 3,-3, 3, -2,-1,-2,-1 }, + { 4,-4,-4, 4, 2, 2,-2,-2, 2,-2, 2,-2, 1, 1, 1, 1 } + }; + + for( unsigned i = 0; i < 16; ++i ) { + alpha[i] = 0; + for( unsigned j = 0; j < 16; ++j ) { + alpha[i] += A[i][j]*v[j]; + } + } +} + +double sum( const double alpha[16], const double& x, const double& y ) { + + double result = 0; + + double xx = x*x; + double xxx = xx * x; + double yy = y*y; + double yyy = yy * y; + + result += alpha[ 0 ]; + result += alpha[ 1 ] * x; + result += alpha[ 2 ] * xx; + result += alpha[ 3 ] * xxx; + result += alpha[ 4 ] * y; + result += alpha[ 5 ] * y * x; + result += alpha[ 6 ] * y * xx; + result += alpha[ 7 ] * y * xxx; + result += alpha[ 8 ] * yy; + result += alpha[ 9 ] * yy * x; + result += alpha[ 10 ] * yy * xx; + result += alpha[ 11 ] * yy * xxx; + result += alpha[ 12 ] * yyy; + result += alpha[ 13 ] * yyy * x; + result += alpha[ 14 ] * yyy * xx; + result += alpha[ 15 ] * yyy * xxx; + + return result; +} + +/** + Fill 'smooth' with a smoothed version of the array by subdividing each patch into smaller patches. +*/ +void SPMeshNodeArray::bicubic( SPMeshNodeArray* smooth, SPMeshType type ) { + + + *smooth = *this; // Deep copy via copy assignment constructor, smooth cleared before copy + // std::cout << "SPMeshNodeArray::smooth2(): " << this->patch_rows() << " " << smooth->patch_columns() << std::endl; + // std::cout << " " << smooth << " " << this << std::endl; + + // Find derivatives at corners + + // Create array of corner points + std::vector< std::vector <SPMeshSmoothCorner> > d; + d.resize( smooth->patch_rows() + 1 ); + for( unsigned i = 0; i < d.size(); ++i ) { + d[i].resize( smooth->patch_columns() + 1 ); + for( unsigned j = 0; j < d[i].size(); ++j ) { + float rgb_color[3]; + this->nodes[ i*3 ][ j*3 ]->color.get_rgb_floatv(rgb_color); + d[i][j].g[0][0] = rgb_color[ 0 ]; + d[i][j].g[1][0] = rgb_color[ 1 ]; + d[i][j].g[2][0] = rgb_color[ 2 ]; + d[i][j].p = this->nodes[ i*3 ][ j*3 ]->p; + } + } + + // Calculate interior derivatives + for( unsigned i = 0; i < d.size(); ++i ) { + for( unsigned j = 0; j < d[i].size(); ++j ) { + for( unsigned k = 0; k < 3; ++k ) { // Loop over colors + + // dx + + if( i != 0 && i != d.size()-1 ) { + double lm = Geom::distance( d[i-1][j].p, d[i][j].p ); + double lp = Geom::distance( d[i+1][j].p, d[i][j].p ); + d[i][j].g[k][1] = find_slope1( d[i-1][j].g[k][0], d[i][j].g[k][0], d[i+1][j].g[k][0], lm, lp ); + } + + // dy + if( j != 0 && j != d[i].size()-1 ) { + double lm = Geom::distance( d[i][j-1].p, d[i][j].p ); + double lp = Geom::distance( d[i][j+1].p, d[i][j].p ); + d[i][j].g[k][2] = find_slope1( d[i][j-1].g[k][0], d[i][j].g[k][0], d[i][j+1].g[k][0], lm, lp ); + } + + // dxdy if needed, need to take lengths into account + // if( i != 0 && i != d.size()-1 && j != 0 && j != d[i].size()-1 ) { + // d[i][j].g[k][3] = find_slope2( d[i-1][j-1].g[k][0], d[i+1][j-1].g[k][0], + // d[i-1][j+1].g[k][0], d[i-1][j-1].g[k][0], + // d[i][j].g[k][0] ); + // } + + } + } + } + + // Calculate exterior derivatives + // We need to do this after calculating interior derivatives as we need to already + // have the non-exterior derivative calculated for finding the parabola. + for( unsigned j = 0; j< d[0].size(); ++j ) { + for( unsigned k = 0; k < 3; ++k ) { // Loop over colors + + // Parabolic + double d0 = Geom::distance( d[1][j].p, d[0 ][j].p ); + if( d0 > 0 ) { + d[0][j].g[k][1] = 2.0*(d[1][j].g[k][0] - d[0 ][j].g[k][0])/d0 - d[1][j].g[k][1]; + } else { + d[0][j].g[k][1] = 0; + } + + unsigned z = d.size()-1; + double dz = Geom::distance( d[z][j].p, d[z-1][j].p ); + if( dz > 0 ) { + d[z][j].g[k][1] = 2.0*(d[z][j].g[k][0] - d[z-1][j].g[k][0])/dz - d[z-1][j].g[k][1]; + } else { + d[z][j].g[k][1] = 0; + } + } + } + + for( unsigned i = 0; i< d.size(); ++i ) { + for( unsigned k = 0; k < 3; ++k ) { // Loop over colors + + // Parabolic + double d0 = Geom::distance( d[i][1].p, d[i][0 ].p ); + if( d0 > 0 ) { + d[i][0].g[k][2] = 2.0*(d[i][1].g[k][0] - d[i][0 ].g[k][0])/d0 - d[i][1].g[k][2]; + } else { + d[i][0].g[k][2] = 0; + } + + unsigned z = d[0].size()-1; + double dz = Geom::distance( d[i][z].p, d[i][z-1].p ); + if( dz > 0 ) { + d[i][z].g[k][2] = 2.0*(d[i][z].g[k][0] - d[i][z-1].g[k][0])/dz - d[i][z-1].g[k][2]; + } else { + d[i][z].g[k][2] = 0; + } + } + } + + // Leave outside corner cross-derivatives at zero. + + // Next split each patch into 8x8 smaller patches. + + // Split each row into eight rows. + // Must do it from end so inserted rows don't mess up indexing + for( int i = smooth->patch_rows() - 1; i >= 0; --i ) { + smooth->split_row( i, unsigned(8) ); + } + + // Split each column into eight columns. + // Must do it from end so inserted columns don't mess up indexing + for( int i = smooth->patch_columns() - 1; i >= 0; --i ) { + smooth->split_column( i, (unsigned)8 ); + } + + // Fill new patches + for( unsigned i = 0; i < this->patch_rows(); ++i ) { + for( unsigned j = 0; j < this->patch_columns(); ++j ) { + + double dx0 = Geom::distance( d[i ][j ].p, d[i+1][j ].p ); + double dx1 = Geom::distance( d[i ][j+1].p, d[i+1][j+1].p ); + double dy0 = Geom::distance( d[i ][j ].p, d[i ][j+1].p ); + double dy1 = Geom::distance( d[i+1][j ].p, d[i+1][j+1].p ); + + // Temp loop over 0..8 to get last column/row edges + float r[3][9][9]; // result + for( unsigned m = 0; m < 3; ++m ) { + + double v[16]; + v[ 0] = d[i ][j ].g[m][0]; + v[ 1] = d[i+1][j ].g[m][0]; + v[ 2] = d[i ][j+1].g[m][0]; + v[ 3] = d[i+1][j+1].g[m][0]; + v[ 4] = d[i ][j ].g[m][1]*dx0; + v[ 5] = d[i+1][j ].g[m][1]*dx0; + v[ 6] = d[i ][j+1].g[m][1]*dx1; + v[ 7] = d[i+1][j+1].g[m][1]*dx1; + v[ 8] = d[i ][j ].g[m][2]*dy0; + v[ 9] = d[i+1][j ].g[m][2]*dy1; + v[10] = d[i ][j+1].g[m][2]*dy0; + v[11] = d[i+1][j+1].g[m][2]*dy1; + v[12] = d[i ][j ].g[m][3]; + v[13] = d[i+1][j ].g[m][3]; + v[14] = d[i ][j+1].g[m][3]; + v[15] = d[i+1][j+1].g[m][3]; + + double alpha[16]; + invert( v, alpha ); + + for( unsigned k = 0; k < 9; ++k ) { + for( unsigned l = 0; l < 9; ++l ) { + double x = k/8.0; + double y = l/8.0; + r[m][k][l] = sum( alpha, x, y ); + // Clamp to allowed values + if( r[m][k][l] > 1.0 ) + r[m][k][l] = 1.0; + if( r[m][k][l] < 0.0 ) + r[m][k][l] = 0.0; + } + } + + } // Loop over colors + + for( unsigned k = 0; k < 9; ++k ) { + for( unsigned l = 0; l < 9; ++l ) { + // Every third node is a corner node + smooth->nodes[ (i*8+k)*3 ][(j*8+l)*3 ]->color.set( r[0][k][l], r[1][k][l], r[2][k][l] ); + } + } + } + } +} + +/** + Number of patch rows. +*/ +guint SPMeshNodeArray::patch_rows() { + + return nodes.size()/3; +} + +/** + Number of patch columns. +*/ +guint SPMeshNodeArray::patch_columns() { + if (nodes.empty()) { + return 0; + } + return nodes[0].size()/3; +} + +/** + Inputs: + i, j: Corner draggable indices. + Returns: + true if corners adjacent. + n[] is array of nodes in top/bottom or left/right order. +*/ +bool SPMeshNodeArray::adjacent_corners( guint i, guint j, SPMeshNode* n[4] ) { + + // This works as all corners have indices and they + // are numbered in order by row and column (and + // the node array is rectangular). + + bool adjacent = false; + + guint c1 = i; + guint c2 = j; + if( j < i ) { + c1 = j; + c2 = i; + } + + // Number of corners in a row of patches. + guint ncorners = patch_columns() + 1; + + guint crow1 = c1 / ncorners; + guint crow2 = c2 / ncorners; + guint ccol1 = c1 % ncorners; + guint ccol2 = c2 % ncorners; + + guint nrow = crow1 * 3; + guint ncol = ccol1 * 3; + + // std::cout << " i: " << i + // << " j: " << j + // << " ncorners: " << ncorners + // << " c1: " << c1 + // << " crow1: " << crow1 + // << " ccol1: " << ccol1 + // << " c2: " << c2 + // << " crow2: " << crow2 + // << " ccol2: " << ccol2 + // << " nrow: " << nrow + // << " ncol: " << ncol + // << std::endl; + + // Check for horizontal neighbors + if ( crow1 == crow2 && (ccol2 - ccol1) == 1 ) { + adjacent = true; + for( guint k = 0; k < 4; ++k ) { + n[k] = nodes[nrow][ncol+k]; + } + } + + // Check for vertical neighbors + if ( ccol1 == ccol2 && (crow2 - crow1) == 1 ) { + adjacent = true; + for( guint k = 0; k < 4; ++k ) { + n[k] = nodes[nrow+k][ncol]; + } + } + + return adjacent; +} + +/** + Toggle sides between lineto and curve to if both corners selected. + Input is a list of selected corner draggable indices. +*/ +guint SPMeshNodeArray::side_toggle( std::vector<guint> corners ) { + + guint toggled = 0; + + if( corners.size() < 2 ) return 0; + + for( guint i = 0; i < corners.size()-1; ++i ) { + for( guint j = i+1; j < corners.size(); ++j ) { + + SPMeshNode* n[4]; + if( adjacent_corners( corners[i], corners[j], n ) ) { + + gchar path_type = n[1]->path_type; + switch (path_type) + { + case 'L': + n[1]->path_type = 'C'; + n[2]->path_type = 'C'; + n[1]->set = true; + n[2]->set = true; + break; + + case 'l': + n[1]->path_type = 'c'; + n[2]->path_type = 'c'; + n[1]->set = true; + n[2]->set = true; + break; + + case 'C': { + n[1]->path_type = 'L'; + n[2]->path_type = 'L'; + n[1]->set = false; + n[2]->set = false; + // 'L' acts as if handles are 1/3 of path length from corners. + Geom::Point dp = (n[3]->p - n[0]->p)/3.0; + n[1]->p = n[0]->p + dp; + n[2]->p = n[3]->p - dp; + break; + } + case 'c': { + n[1]->path_type = 'l'; + n[2]->path_type = 'l'; + n[1]->set = false; + n[2]->set = false; + // 'l' acts as if handles are 1/3 of path length from corners. + Geom::Point dp = (n[3]->p - n[0]->p)/3.0; + n[1]->p = n[0]->p + dp; + n[2]->p = n[3]->p - dp; + // std::cout << "Toggle sides: " + // << n[0]->p << " " + // << n[1]->p << " " + // << n[2]->p << " " + // << n[3]->p << " " + // << dp << std::endl; + break; + } + default: + std::cerr << "Toggle sides: Invalid path type: " << path_type << std::endl; + } + ++toggled; + } + } + } + if( toggled > 0 ) built = false; + return toggled; +} + +/** + * Converts generic Beziers to Beziers approximating elliptical arcs, preserving handle direction. + * There are infinite possible solutions. The solution chosen here is to generate a section of an + * ellipse that is centered on the intersection of the two lines passing through the two nodes but + * parallel to the other node's handle direction. This is the section of an ellipse that + * corresponds to a quarter of a circle squished and then skewed. + */ +guint SPMeshNodeArray::side_arc( std::vector<guint> corners ) { + + if( corners.size() < 2 ) return 0; + + guint arced = 0; + for( guint i = 0; i < corners.size()-1; ++i ) { + for( guint j = i+1; j < corners.size(); ++j ) { + + SPMeshNode* n[4]; + if( adjacent_corners( corners[i], corners[j], n ) ) { + + gchar path_type = n[1]->path_type; + switch (path_type) + { + case 'L': + case 'l': + std::cerr << "SPMeshNodeArray::side_arc: Can't convert straight lines to arcs." << std::endl; + break; + + case 'C': + case 'c': { + + Geom::Ray ray1( n[0]->p, n[1]->p ); + Geom::Ray ray2( n[3]->p, n[2]->p ); + if( !are_parallel( (Geom::Line)ray1, (Geom::Line)ray2 ) ) { + + Geom::OptCrossing crossing = intersection( ray1, ray2 ); + + if( crossing ) { + + Geom::Point intersection = ray1.pointAt( (*crossing).ta ); + + const double f = 4.0/3.0 * tan( M_PI/2.0/4.0 ); + + Geom::Point h1 = intersection - n[0]->p; + Geom::Point h2 = intersection - n[3]->p; + + n[1]->p = n[0]->p + f*h1; + n[2]->p = n[3]->p + f*h2; + ++arced; + + } else { + std::cerr << "SPMeshNodeArray::side_arc: No crossing, can't turn into arc." << std::endl; + } + } else { + std::cerr << "SPMeshNodeArray::side_arc: Handles parallel, can't turn into arc." << std::endl; + } + break; + } + default: + std::cerr << "SPMeshNodeArray::side_arc: Invalid path type: " << n[1]->path_type << std::endl; + } + } + } + } + if( arced > 0 ) built = false; + return arced; +} + +/** + Toggle sides between lineto and curve to if both corners selected. + Input is a list of selected corner draggable indices. +*/ +guint SPMeshNodeArray::tensor_toggle( std::vector<guint> corners ) { + + // std::cout << "SPMeshNodeArray::tensor_toggle" << std::endl; + + if( corners.size() < 4 ) return 0; + + guint toggled = 0; + + // Number of corners in a row of patches. + guint ncorners = patch_columns() + 1; + + for( guint i = 0; i < corners.size()-3; ++i ) { + for( guint j = i+1; j < corners.size()-2; ++j ) { + for( guint k = j+1; k < corners.size()-1; ++k ) { + for( guint l = k+1; l < corners.size(); ++l ) { + + guint c[4]; + c[0] = corners[i]; + c[1] = corners[j]; + c[2] = corners[k]; + c[3] = corners[l]; + std::sort( c, c+4 ); + + // Check we have four corners of one patch selected + if( c[1]-c[0] == 1 && + c[3]-c[2] == 1 && + c[2]-c[0] == ncorners && + c[3]-c[1] == ncorners && + c[0] % ncorners < ncorners - 1 ) { + + // Patch + guint prow = c[0] / ncorners; + guint pcol = c[0] % ncorners; + + // Upper left node of patch + guint irow = prow * 3; + guint jcol = pcol * 3; + + // std::cout << "tensor::toggle: " + // << c[0] << ", " + // << c[1] << ", " + // << c[2] << ", " + // << c[3] << std::endl; + + // std::cout << "tensor::toggle: " + // << " irow: " << irow + // << " jcol: " << jcol + // << " prow: " << prow + // << " pcol: " << pcol + // << std::endl; + + SPMeshPatchI patch( &nodes, prow, pcol ); + patch.updateNodes(); + + if( patch.tensorIsSet() ) { + // Unset tensor points + nodes[irow+1][jcol+1]->set = false; + nodes[irow+1][jcol+2]->set = false; + nodes[irow+2][jcol+1]->set = false; + nodes[irow+2][jcol+2]->set = false; + } else { + // Set tensor points + nodes[irow+1][jcol+1]->set = true; + nodes[irow+1][jcol+2]->set = true; + nodes[irow+2][jcol+1]->set = true; + nodes[irow+2][jcol+2]->set = true; + } + + ++toggled; + } + } + } + } + } + if( toggled > 0 ) built = false; + return toggled; +} + +/** + Attempts to smooth color transitions across corners. + Input is a list of selected corner draggable indices. +*/ +guint SPMeshNodeArray::color_smooth( std::vector<guint> corners ) { + + // std::cout << "SPMeshNodeArray::color_smooth" << std::endl; + + guint smoothed = 0; + + // Number of corners in a row of patches. + guint ncorners = patch_columns() + 1; + + // Number of node rows and columns + guint ncols = patch_columns() * 3 + 1; + guint nrows = patch_rows() * 3 + 1; + + for(unsigned int corner : corners) { + + // std::cout << "SPMeshNodeArray::color_smooth: " << i << " " << corner << std::endl; + + // Node row & col + guint nrow = (corner / ncorners) * 3; + guint ncol = (corner % ncorners) * 3; + + SPMeshNode* n[7]; + for( guint s = 0; s < 2; ++s ) { + + bool smooth = false; + + // Find neighboring nodes + if( s == 0 ) { + + // Horizontal + if( ncol > 2 && ncol+3 < ncols) { + for( guint j = 0; j < 7; ++j ) { + n[j] = nodes[ nrow ][ ncol - 3 + j ]; + } + smooth = true; + } + + } else { + + // Vertical + if( nrow > 2 && nrow+3 < nrows) { + for( guint j = 0; j < 7; ++j ) { + n[j] = nodes[ nrow - 3 + j ][ ncol ]; + } + smooth = true; + } + } + + if( smooth ) { + + // Let the smoothing begin + // std::cout << " checking: " << ncol << " " << nrow << std::endl; + + // Get initial slopes using closest handles. + double slope[2][3]; + double slope_ave[3]; + double slope_diff[3]; + + // Color of corners + SPColor color0 = n[0]->color; + SPColor color3 = n[3]->color; + SPColor color6 = n[6]->color; + + // Distance nodes from selected corner + Geom::Point d[7]; + for( guint k = 0; k < 7; ++k ) { + d[k]= n[k]->p - n[3]->p; + // std::cout << " d[" << k << "]: " << d[k].length() << std::endl; + } + + double sdm = -1.0; // Slope Diff Max + guint cdm = 0; // Color Diff Max (Which color has the maximum difference in slopes) + for( guint c = 0; c < 3; ++c ) { + if( d[2].length() != 0.0 ) { + slope[0][c] = (color3.v.c[c] - color0.v.c[c]) / d[2].length(); + } + if( d[4].length() != 0.0 ) { + slope[1][c] = (color6.v.c[c] - color3.v.c[c]) / d[4].length(); + } + slope_ave[c] = (slope[0][c]+slope[1][c]) / 2.0; + slope_diff[c] = (slope[0][c]-slope[1][c]); + // std::cout << " color: " << c << " :" + // << color0.v.c[c] << " " + // << color3.v.c[c] << " " + // << color6.v.c[c] + // << " slope: " + // << slope[0][c] << " " + // << slope[1][c] + // << " slope_ave: " << slope_ave[c] + // << " slope_diff: " << slope_diff[c] + // << std::endl; + + // Find color with maximum difference + if( std::abs( slope_diff[c] ) > sdm ) { + sdm = std::abs( slope_diff[c] ); + cdm = c; + } + } + // std::cout << " cdm: " << cdm << std::endl; + + // Find new handle positions: + double length_left = d[0].length(); + double length_right = d[6].length(); + if( slope_ave[ cdm ] != 0.0 ) { + length_left = std::abs( (color3.v.c[cdm] - color0.v.c[cdm]) / slope_ave[ cdm ] ); + length_right = std::abs( (color6.v.c[cdm] - color3.v.c[cdm]) / slope_ave[ cdm ] ); + } + + // Move closest handle a maximum of mid point... but don't shorten + double max = 0.8; + if( length_left > max * d[0].length() && length_left > d[2].length() ) { + std::cerr << " Can't smooth left side" << std::endl; + length_left = std::max( max * d[0].length(), d[2].length() ); + } + if( length_right > max * d[6].length() && length_right > d[4].length() ) { + std::cerr << " Can't smooth right side" << std::endl; + length_right = std::max( max * d[6].length(), d[4].length() ); + } + + if( d[2].length() != 0.0 ) d[2] *= length_left/d[2].length(); + if( d[4].length() != 0.0 ) d[4] *= length_right/d[4].length(); + + // std::cout << " length_left: " << length_left + // << " d[0]: " << d[0].length() + // << " length_right: " << length_right + // << " d[6]: " << d[6].length() + // << std::endl; + + n[2]->p = n[3]->p + d[2]; + n[4]->p = n[3]->p + d[4]; + + ++smoothed; + } + } + + } + + if( smoothed > 0 ) built = false; + return smoothed; +} + +/** + Pick color from background for selected corners. +*/ +guint SPMeshNodeArray::color_pick( std::vector<guint> icorners, SPItem* item ) { + + // std::cout << "SPMeshNodeArray::color_pick" << std::endl; + + guint picked = 0; + + // Code inspired from clone tracing + + // Setup... + + // We need a copy of the drawing so we can hide the mesh. + Inkscape::Drawing *pick_drawing = new Inkscape::Drawing(); + unsigned pick_visionkey = SPItem::display_key_new(1); + + SPDocument *pick_doc = mg->document; + + pick_drawing->setRoot(pick_doc->getRoot()->invoke_show(*pick_drawing, pick_visionkey, SP_ITEM_SHOW_DISPLAY)); + + item->invoke_hide(pick_visionkey); + + pick_doc->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + pick_doc->ensureUpToDate(); + + //gdouble pick_zoom = 1.0; // zoom; + //pick_drawing->root()->setTransform(Geom::Scale(pick_zoom)); + pick_drawing->update(); + + // std::cout << " transform: " << std::endl; + // std::cout << item->transform << std::endl; + // std::cout << " i2doc: " << std::endl; + // std::cout << item->i2doc_affine() << std::endl; + // std::cout << " i2dt: " << std::endl; + // std::cout << item->i2dt_affine() << std::endl; + // std::cout << " dt2i: " << std::endl; + // std::cout << item->dt2i_affine() << std::endl; + SPGradient* gr = mg; + // if( gr->gradientTransform_set ) { + // std::cout << " gradient transform set: " << std::endl; + // std::cout << gr->gradientTransform << std::endl; + // } else { + // std::cout << " gradient transform not set! " << std::endl; + // } + + // Do picking + for(unsigned int corner : icorners) { + + SPMeshNode* n = corners[ corner ]; + + // Region to average over + Geom::Point p = n->p; + // std::cout << " before transform: p: " << p << std::endl; + p *= gr->gradientTransform; + // std::cout << " after transform: p: " << p << std::endl; + p *= item->i2doc_affine(); + // std::cout << " after transform: p: " << p << std::endl; + + // If on edge, move inward + guint cols = patch_columns()+1; + guint rows = patch_rows()+1; + guint col = corner % cols; + guint row = corner / cols; + guint ncol = col * 3; + guint nrow = row * 3; + + const double size = 3.0; + + // Top edge + if( row == 0 ) { + Geom::Point dp = nodes[nrow+1][ncol]->p - p; + p += unit_vector( dp ) * size; + } + // Right edge + if( col == cols-1 ) { + Geom::Point dp = nodes[nrow][ncol-1]->p - p; + p += unit_vector( dp ) * size; + } + // Bottom edge + if( row == rows-1 ) { + Geom::Point dp = nodes[nrow-1][ncol]->p - p; + p += unit_vector( dp ) * size; + } + // Left edge + if( col == 0 ) { + Geom::Point dp = nodes[nrow][ncol+1]->p - p; + p += unit_vector( dp ) * size; + } + + Geom::Rect box( p[Geom::X]-size/2.0, p[Geom::Y]-size/2.0, + p[Geom::X]+size/2.0, p[Geom::Y]+size/2.0 ); + + /* Item integer bbox in points */ + Geom::IntRect ibox = box.roundOutwards(); + + /* Find visible area */ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ibox.width(), ibox.height()); + Inkscape::DrawingContext dc(s, ibox.min()); + + /* Render copy and pick color */ + pick_drawing->render(dc, ibox); + double R = 0, G = 0, B = 0, A = 0; + ink_cairo_surface_average_color(s, R, G, B, A); + cairo_surface_destroy(s); + + // std::cout << " p: " << p + // << " box: " << ibox + // << " R: " << R + // << " G: " << G + // << " B: " << B + // << std::endl; + n->color.set( R, G, B ); + } + + pick_doc->getRoot()->invoke_hide(pick_visionkey); + delete pick_drawing; + + picked = 1; // Picking always happens + if( picked > 0 ) built = false; + return picked; +} + +/** + Splits selected rows and/or columns in half (according to the path 't' parameter). + Input is a list of selected corner draggable indices. +*/ +guint SPMeshNodeArray::insert( std::vector<guint> corners ) { + + guint inserted = 0; + + if( corners.size() < 2 ) return 0; + + std::set<guint> columns; + std::set<guint> rows; + + for( guint i = 0; i < corners.size()-1; ++i ) { + for( guint j = i+1; j < corners.size(); ++j ) { + + // This works as all corners have indices and they + // are numbered in order by row and column (and + // the node array is rectangular). + + guint c1 = corners[i]; + guint c2 = corners[j]; + if (c2 < c1) { + c1 = corners[j]; + c2 = corners[i]; + } + + // Number of corners in a row of patches. + guint ncorners = patch_columns() + 1; + + guint crow1 = c1 / ncorners; + guint crow2 = c2 / ncorners; + guint ccol1 = c1 % ncorners; + guint ccol2 = c2 % ncorners; + + // Check for horizontal neighbors + if ( crow1 == crow2 && (ccol2 - ccol1) == 1 ) { + columns.insert( ccol1 ); + } + + // Check for vertical neighbors + if ( ccol1 == ccol2 && (crow2 - crow1) == 1 ) { + rows.insert( crow1 ); + } + } + } + + // Iterate backwards so column/row numbers are not invalidated. + std::set<guint>::reverse_iterator rit; + for (rit=columns.rbegin(); rit != columns.rend(); ++rit) { + split_column( *rit, 0.5); + ++inserted; + } + for (rit=rows.rbegin(); rit != rows.rend(); ++rit) { + split_row( *rit, 0.5); + ++inserted; + } + + if( inserted > 0 ) built = false; + return inserted; +} + +/** + Moves handles in response to a corner node move. + p_old: original position of moved corner node. + corner: the corner node moved (draggable index, i.e. point_i). + selected: list of all corners selected (draggable indices). + op: how other corners should be moved. + Corner node must already have been moved! +*/ +void SPMeshNodeArray::update_handles( guint corner, std::vector< guint > /*selected*/, Geom::Point p_old, MeshNodeOperation /*op*/ ) +{ + if (!draggers_valid) { + std::cerr << "SPMeshNodeArray::update_handles: Draggers not valid!" << std::endl; + return; + } + // assert( draggers_valid ); + + // std::cout << "SPMeshNodeArray::update_handles: " + // << " corner: " << corner + // << " op: " << op + // << std::endl; + + // Find number of patch rows and columns + guint mrow = patch_rows(); + guint mcol = patch_columns(); + + // Number of corners in a row of patches. + guint ncorners = mcol + 1; + + // Find corner row/column + guint crow = corner / ncorners; + guint ccol = corner % ncorners; + + // Find node row/column + guint nrow = crow * 3; + guint ncol = ccol * 3; + + // std::cout << " mrow: " << mrow + // << " mcol: " << mcol + // << " crow: " << crow + // << " ccol: " << ccol + // << " ncorners: " << ncorners + // << " nrow: " << nrow + // << " ncol: " << ncol + // << std::endl; + + // New corner mesh coordinate. + Geom::Point p_new = nodes[nrow][ncol]->p; + + // Corner point move dpg in mesh coordinate system. + Geom::Point dp = p_new - p_old; + + // std::cout << " p_old: " << p_old << std::endl; + // std::cout << " p_new: " << p_new << std::endl; + // std::cout << " dp: " << dp << std::endl; + + // STEP 1: ONLY DO DIRECT MOVE + bool patch[4]; + patch[0] = patch[1] = patch[2] = patch[3] = false; + if( ccol > 0 && crow > 0 ) patch[0] = true; + if( ccol < mcol && crow > 0 ) patch[1] = true; + if( ccol < mcol && crow < mrow ) patch[2] = true; + if( ccol > 0 && crow < mrow ) patch[3] = true; + + // std::cout << patch[0] << " " + // << patch[1] << " " + // << patch[2] << " " + // << patch[3] << std::endl; + + // Move handles + if( patch[0] || patch[1] ) { + if( nodes[nrow-1][ncol]->path_type == 'l' || + nodes[nrow-1][ncol]->path_type == 'L' ) { + Geom::Point s = (nodes[nrow-3][ncol]->p - nodes[nrow][ncol]->p)/3.0; + nodes[nrow-1][ncol ]->p = nodes[nrow][ncol]->p + s; + nodes[nrow-2][ncol ]->p = nodes[nrow-3][ncol]->p - s; + } else { + nodes[nrow-1][ncol ]->p += dp; + } + } + + if( patch[1] || patch[2] ) { + if( nodes[nrow ][ncol+1]->path_type == 'l' || + nodes[nrow ][ncol+1]->path_type == 'L' ) { + Geom::Point s = (nodes[nrow][ncol+3]->p - nodes[nrow][ncol]->p)/3.0; + nodes[nrow ][ncol+1]->p = nodes[nrow][ncol]->p + s; + nodes[nrow ][ncol+2]->p = nodes[nrow][ncol+3]->p - s; + } else { + nodes[nrow ][ncol+1]->p += dp; + } + } + + if( patch[2] || patch[3] ) { + if( nodes[nrow+1][ncol ]->path_type == 'l' || + nodes[nrow+1][ncol ]->path_type == 'L' ) { + Geom::Point s = (nodes[nrow+3][ncol]->p - nodes[nrow][ncol]->p)/3.0; + nodes[nrow+1][ncol ]->p = nodes[nrow][ncol]->p + s; + nodes[nrow+2][ncol ]->p = nodes[nrow+3][ncol]->p - s; + } else { + nodes[nrow+1][ncol ]->p += dp; + } + } + + if( patch[3] || patch[0] ) { + if( nodes[nrow ][ncol-1]->path_type == 'l' || + nodes[nrow ][ncol-1]->path_type == 'L' ) { + Geom::Point s = (nodes[nrow][ncol-3]->p - nodes[nrow][ncol]->p)/3.0; + nodes[nrow ][ncol-1]->p = nodes[nrow][ncol]->p + s; + nodes[nrow ][ncol-2]->p = nodes[nrow][ncol-3]->p - s; + } else { + nodes[nrow ][ncol-1]->p += dp; + } + } + + + // Move tensors + if( patch[0] ) nodes[nrow-1][ncol-1]->p += dp; + if( patch[1] ) nodes[nrow-1][ncol+1]->p += dp; + if( patch[2] ) nodes[nrow+1][ncol+1]->p += dp; + if( patch[3] ) nodes[nrow+1][ncol-1]->p += dp; + + // // Check if neighboring corners are selected. + + // bool do_scale = false; + + // bool do_scale_xp = do_scale; + // bool do_scale_xn = do_scale; + // bool do_scale_yp = do_scale; + // bool do_scale_yn = do_scale; + + // if( ccol < mcol+1 ) { + // if( std::find( sc.begin(), sc.end(), point_i + 1 ) != sc.end() ) { + // do_scale_xp = false; + // std::cout << " Not scaling x+" << std::endl; + // } + // } + + // if( ccol > 0 ) { + // if( std::find( sc.begin(), sc.end(), point_i - 1 ) != sc.end() ) { + // do_scale_xn = false; + // std::cout << " Not scaling x-" << std::endl; + // } + // } + + // if( crow < mrow+1 ) { + // if( std::find( sc.begin(), sc.end(), point_i + ncorners ) != sc.end() ) { + // do_scale_yp = false; + // std::cout << " Not scaling y+" << std::endl; + // } + // } + + // if( crow > 0 ) { + // if( std::find( sc.begin(), sc.end(), point_i - ncorners ) != sc.end() ) { + // do_scale_yn = false; + // std::cout << " Not scaling y-" << std::endl; + // } + // } + + // // We have four patches to adjust... + // for ( guint k = 0; k < 4; ++k ) { + + // bool do_scale_x = do_scale; + // bool do_scale_y = do_scale; + + // SPMeshNode* pnodes[4][4]; + + // // Load up matrix + // switch (k) { + + // case 0: + // if( crow < mrow+1 && ccol < mcol+1 ) { + // // Bottom right patch + + // do_scale_x = do_scale_xp; + // do_scale_y = do_scale_yp; + + // for( guint i = 0; i < 4; ++i ) { + // for( guint j = 0; j< 4; ++j ) { + // pnodes[i][j] = mg->array.nodes[nrow+i][nrow+j]; + // } + // } + // } + // break; + + // case 1: + // if( crow < mrow+1 && ccol > 0 ) { + // // Bottom left patch (note x, y swapped) + + // do_scale_y = do_scale_xn; + // do_scale_x = do_scale_yp; + + // for( guint i = 0; i < 4; ++i ) { + // for( guint j = 0; j< 4; ++j ) { + // pnodes[j][i] = mg->array.nodes[nrow+i][nrow-j]; + // } + // } + // } + // break; + + // case 2: + // if( crow > 0 && ccol > 0 ) { + // // Top left patch + + // do_scale_x = do_scale_xn; + // do_scale_y = do_scale_yn; + + // for( guint i = 0; i < 4; ++i ) { + // for( guint j = 0; j< 4; ++j ) { + // pnodes[i][j] = mg->array.nodes[nrow-i][nrow-j]; + // } + // } + // } + // break; + + // case 3: + // if( crow > 0 && ccol < mcol+1 ) { + // // Top right patch (note x, y swapped) + + // do_scale_y = do_scale_xp; + // do_scale_x = do_scale_yn; + + // for( guint i = 0; i < 4; ++i ) { + // for( guint j = 0; j< 4; ++j ) { + // pnodes[j][i] = mg->array.nodes[nrow-i][nrow+j]; + // } + // } + // } + // break; + // } + + // // Now we must move points in both x and y. + // // There are upto six points to move: P01, P02, P11, P12, P21, P22. + // // (The points P10, P20 will be moved in another branch of the loop. + // // The points P03, P13, P23, P33, P32, P31, P30 are not moved.) + // // + // // P00 P01 P02 P03 + // // P10 P11 P12 P13 + // // P20 P21 P22 P23 + // // P30 P31 P32 P33 + // // + // // The goal is to preserve the direction of the handle! + + + // Geom::Point dsx_new = pnodes[0][3]->p - pnodes[0][0]->p; // New side x + // Geom::Point dsy_new = pnodes[3][0]->p - pnodes[0][0]->p; // New side y + // Geom::Point dsx_old = pnodes[0][3]->p - pcg_old; // Old side x + // Geom::Point dsy_old = pnodes[3][0]->p - pcg_old; // Old side y + + + // double scale_factor_x = 1.0; + // if( dsx_old.length() != 0.0 ) scale_factor_x = dsx_new.length()/dsx_old.length(); + + // double scale_factor_y = 1.0; + // if( dsy_old.length() != 0.0 ) scale_factor_y = dsy_new.length()/dsy_old.length(); + + + // if( do_scalex && do_scaley ) { + + // // We have six point to move. + + // // P01 + // Geom::Point dp01 = pnodes[0][1] - pcg_old; + // dp01 *= scale_factor_x; + // pnodes[0][1] = pnodes[0][0] + dp01; + + // // P02 + // Geom::Point dp02 = pnodes[0][2] - pnodes[0][3]; + // dp02 *= scale_factor_x; + // pnodes[0][2] = pnodes[0][3] + dp02; + + // // P11 + // Geom::Point dp11 = pnodes[1][1] - pcg_old; + // dp11 *= scale_factor_x; + // pnodes[1][1] = pnodes[0][0] + dp11; + + + + // // P21 + // Geom::Point dp21 = pnodes[2][1] - pnodes[3][0]; + // dp21 *= scale_factor_x; + // dp21 *= scale_factor_y; + // pnodes[2][1] = pnodes[3][0] + dp21; + + + // Geom::Point dsx1 = pnodes[0][1]->p - +} + +SPCurve SPMeshNodeArray::outline_path() const +{ + SPCurve outline; + + if (nodes.empty() ) { + std::cerr << "SPMeshNodeArray::outline_path: empty array!" << std::endl; + return outline; + } + + outline.moveto( nodes[0][0]->p ); + + int ncol = nodes[0].size(); + int nrow = nodes.size(); + + // Top + for (int i = 1; i < ncol; i += 3 ) { + outline.curveto( nodes[0][i]->p, nodes[0][i+1]->p, nodes[0][i+2]->p); + } + + // Right + for (int i = 1; i < nrow; i += 3 ) { + outline.curveto( nodes[i][ncol-1]->p, nodes[i+1][ncol-1]->p, nodes[i+2][ncol-1]->p); + } + + // Bottom (right to left) + for (int i = 1; i < ncol; i += 3 ) { + outline.curveto( nodes[nrow-1][ncol-i-1]->p, nodes[nrow-1][ncol-i-2]->p, nodes[nrow-1][ncol-i-3]->p); + } + + // Left (bottom to top) + for (int i = 1; i < nrow; i += 3 ) { + outline.curveto( nodes[nrow-i-1][0]->p, nodes[nrow-i-2][0]->p, nodes[nrow-i-3][0]->p); + } + + outline.closepath(); + + return outline; +} + +void SPMeshNodeArray::transform(Geom::Affine const &m) { + + for (int i = 0; i < nodes[0].size(); ++i) { + for (auto & node : nodes) { + node[i]->p *= m; + } + } +} + +// Transform mesh to fill box. Return true if mesh transformed. +bool SPMeshNodeArray::fill_box(Geom::OptRect &box) { + + // If gradientTransfor is set (as happens when an object is transformed + // with the "optimized" preferences set true), we need to remove it. + if (mg->gradientTransform_set) { + Geom::Affine gt = mg->gradientTransform; + transform( gt ); + mg->gradientTransform_set = false; + mg->gradientTransform.setIdentity(); + } + + auto mesh_bbox = outline_path().get_pathvector().boundsExact(); + + if (mesh_bbox->width() == 0 || mesh_bbox->height() == 0) { + return false; + } + + double scale_x = (*box).width() /(*mesh_bbox).width() ; + double scale_y = (*box).height()/(*mesh_bbox).height(); + + Geom::Translate t1(-(*mesh_bbox).min()); + Geom::Scale scale(scale_x,scale_y); + Geom::Translate t2((*box).min()); + Geom::Affine trans = t1 * scale * t2; + if (!trans.isIdentity() ) { + transform(trans); + write( mg ); + mg->requestModified(SP_OBJECT_MODIFIED_FLAG); + return true; + } + + return false; +} + +// Defined in gradient-chemistry.cpp +guint32 average_color(guint32 c1, guint32 c2, gdouble p); + +/** + Split a row into n equal parts. +*/ +void SPMeshNodeArray::split_row( unsigned int row, unsigned int n ) { + + double nn = n; + if( n > 1 ) split_row( row, (nn-1)/nn ); + if( n > 2 ) split_row( row, n-1 ); +} + +/** + Split a column into n equal parts. +*/ +void SPMeshNodeArray::split_column( unsigned int col, unsigned int n ) { + + double nn = n; + if( n > 1 ) split_column( col, (nn-1)/nn ); + if( n > 2 ) split_column( col, n-1 ); +} + +/** + Split a row into two rows at coord (fraction of row height). +*/ +void SPMeshNodeArray::split_row( unsigned int row, double coord ) { + + // std::cout << "Splitting row: " << row << " at " << coord << std::endl; + // print(); + assert( coord >= 0.0 && coord <= 1.0 ); + assert( row < patch_rows() ); + + built = false; + + // First step is to ensure that handle and tensor points are up-to-date if they are not set. + // (We can't do this on the fly as we overwrite the necessary points to do the calculation + // during the update.) + for( guint j = 0; j < patch_columns(); ++ j ) { + SPMeshPatchI patch( &nodes, row, j ); + patch.updateNodes(); + } + + // Add three new rows of empty nodes + for( guint i = 0; i < 3; ++i ) { + std::vector< SPMeshNode* > new_row; + for( guint j = 0; j < nodes[0].size(); ++j ) { + SPMeshNode* new_node = new SPMeshNode; + new_row.push_back( new_node ); + } + nodes.insert( nodes.begin()+3*(row+1), new_row ); + } + + guint i = 3 * row; // Convert from patch row to node row + for( guint j = 0; j < nodes[i].size(); ++j ) { + + // std::cout << "Splitting row: column: " << j << std::endl; + + Geom::Point p[4]; + for( guint k = 0; k < 4; ++k ) { + guint n = k; + if( k == 3 ) n = 6; // Bottom patch row has been shifted by new rows + p[k] = nodes[i+n][j]->p; + // std::cout << p[k] << std::endl; + } + + Geom::BezierCurveN<3> b( p[0], p[1], p[2], p[3] ); + + std::pair<Geom::BezierCurveN<3>, Geom::BezierCurveN<3> > b_new = + b.subdivide( coord ); + + // Update points + for( guint n = 0; n < 4; ++n ) { + nodes[i+n ][j]->p = b_new.first[n]; + nodes[i+n+3][j]->p = b_new.second[n]; + // std::cout << b_new.first[n] << " " << b_new.second[n] << std::endl; + } + + if( nodes[i][j]->node_type == MG_NODE_TYPE_CORNER ) { + // We are splitting a side + + // Path type stored in handles. + gchar path_type = nodes[i+1][j]->path_type; + nodes[i+4][j]->path_type = path_type; + nodes[i+5][j]->path_type = path_type; + bool set = nodes[i+1][j]->set; + nodes[i+4][j]->set = set; + nodes[i+5][j]->set = set; + nodes[i+4][j]->node_type = MG_NODE_TYPE_HANDLE; + nodes[i+5][j]->node_type = MG_NODE_TYPE_HANDLE; + + // Color stored in corners + guint c0 = nodes[i ][j]->color.toRGBA32( 1.0 ); + guint c1 = nodes[i+6][j]->color.toRGBA32( 1.0 ); + gdouble o0 = nodes[i ][j]->opacity; + gdouble o1 = nodes[i+6][j]->opacity; + guint cnew = average_color( c0, c1, coord ); + gdouble onew = o0 * (1.0 - coord) + o1 * coord; + nodes[i+3][j]->color.set( cnew ); + nodes[i+3][j]->opacity = onew; + nodes[i+3][j]->node_type = MG_NODE_TYPE_CORNER; + nodes[i+3][j]->set = true; + + } else { + // We are splitting a middle + + bool set = nodes[i+1][j]->set || nodes[i+2][j]->set; + nodes[i+4][j]->set = set; + nodes[i+5][j]->set = set; + nodes[i+4][j]->node_type = MG_NODE_TYPE_TENSOR; + nodes[i+5][j]->node_type = MG_NODE_TYPE_TENSOR; + + // Path type, if different, choose l -> L -> c -> C. + gchar path_type0 = nodes[i ][j]->path_type; + gchar path_type1 = nodes[i+6][j]->path_type; + gchar path_type = 'l'; + if( path_type0 == 'L' || path_type1 == 'L') path_type = 'L'; + if( path_type0 == 'c' || path_type1 == 'c') path_type = 'c'; + if( path_type0 == 'C' || path_type1 == 'C') path_type = 'C'; + nodes[i+3][j]->path_type = path_type; + nodes[i+3][j]->node_type = MG_NODE_TYPE_HANDLE; + if( path_type == 'c' || path_type == 'C' ) nodes[i+3][j]->set = true; + + } + + nodes[i+3][j]->node_edge = MG_NODE_EDGE_NONE; + nodes[i+4][j]->node_edge = MG_NODE_EDGE_NONE; + nodes[i+5][j]->node_edge = MG_NODE_EDGE_NONE;; + if( j == 0 ) { + nodes[i+3][j]->node_edge |= MG_NODE_EDGE_LEFT; + nodes[i+4][j]->node_edge |= MG_NODE_EDGE_LEFT; + nodes[i+5][j]->node_edge |= MG_NODE_EDGE_LEFT; + } + if( j == nodes[i].size() - 1 ) { + nodes[i+3][j]->node_edge |= MG_NODE_EDGE_RIGHT; + nodes[i+4][j]->node_edge |= MG_NODE_EDGE_RIGHT; + nodes[i+5][j]->node_edge |= MG_NODE_EDGE_RIGHT; + } + } + + // std::cout << "Splitting row: result:" << std::endl; + // print(); +} + + + +/** + Split a column into two columns at coord (fraction of column width). +*/ +void SPMeshNodeArray::split_column( unsigned int col, double coord ) { + + // std::cout << "Splitting column: " << col << " at " << coord << std::endl; + // print(); + assert( coord >= 0.0 && coord <= 1.0 ); + assert( col < patch_columns() ); + + built = false; + + // First step is to ensure that handle and tensor points are up-to-date if they are not set. + // (We can't do this on the fly as we overwrite the necessary points to do the calculation + // during the update.) + for( guint i = 0; i < patch_rows(); ++ i ) { + SPMeshPatchI patch( &nodes, i, col ); + patch.updateNodes(); + } + + guint j = 3 * col; // Convert from patch column to node column + for( guint i = 0; i < nodes.size(); ++i ) { + + // std::cout << "Splitting column: row: " << i << std::endl; + + Geom::Point p[4]; + for( guint k = 0; k < 4; ++k ) { + p[k] = nodes[i][j+k]->p; + } + + Geom::BezierCurveN<3> b( p[0], p[1], p[2], p[3] ); + + std::pair<Geom::BezierCurveN<3>, Geom::BezierCurveN<3> > b_new = + b.subdivide( coord ); + + // Add three new nodes + for( guint n = 0; n < 3; ++n ) { + SPMeshNode* new_node = new SPMeshNode; + nodes[i].insert( nodes[i].begin()+j+3, new_node ); + } + + // Update points + for( guint n = 0; n < 4; ++n ) { + nodes[i][j+n]->p = b_new.first[n]; + nodes[i][j+n+3]->p = b_new.second[n]; + } + + if( nodes[i][j]->node_type == MG_NODE_TYPE_CORNER ) { + // We are splitting a side + + // Path type stored in handles. + gchar path_type = nodes[i][j+1]->path_type; + nodes[i][j+4]->path_type = path_type; + nodes[i][j+5]->path_type = path_type; + bool set = nodes[i][j+1]->set; + nodes[i][j+4]->set = set; + nodes[i][j+5]->set = set; + nodes[i][j+4]->node_type = MG_NODE_TYPE_HANDLE; + nodes[i][j+5]->node_type = MG_NODE_TYPE_HANDLE; + + // Color stored in corners + guint c0 = nodes[i][j ]->color.toRGBA32( 1.0 ); + guint c1 = nodes[i][j+6]->color.toRGBA32( 1.0 ); + gdouble o0 = nodes[i][j ]->opacity; + gdouble o1 = nodes[i][j+6]->opacity; + guint cnew = average_color( c0, c1, coord ); + gdouble onew = o0 * (1.0 - coord) + o1 * coord; + nodes[i][j+3]->color.set( cnew ); + nodes[i][j+3]->opacity = onew; + nodes[i][j+3]->node_type = MG_NODE_TYPE_CORNER; + nodes[i][j+3]->set = true; + + } else { + // We are splitting a middle + + bool set = nodes[i][j+1]->set || nodes[i][j+2]->set; + nodes[i][j+4]->set = set; + nodes[i][j+5]->set = set; + nodes[i][j+4]->node_type = MG_NODE_TYPE_TENSOR; + nodes[i][j+5]->node_type = MG_NODE_TYPE_TENSOR; + + // Path type, if different, choose l -> L -> c -> C. + gchar path_type0 = nodes[i][j ]->path_type; + gchar path_type1 = nodes[i][j+6]->path_type; + gchar path_type = 'l'; + if( path_type0 == 'L' || path_type1 == 'L') path_type = 'L'; + if( path_type0 == 'c' || path_type1 == 'c') path_type = 'c'; + if( path_type0 == 'C' || path_type1 == 'C') path_type = 'C'; + nodes[i][j+3]->path_type = path_type; + nodes[i][j+3]->node_type = MG_NODE_TYPE_HANDLE; + if( path_type == 'c' || path_type == 'C' ) nodes[i][j+3]->set = true; + + } + + nodes[i][j+3]->node_edge = MG_NODE_EDGE_NONE; + nodes[i][j+4]->node_edge = MG_NODE_EDGE_NONE; + nodes[i][j+5]->node_edge = MG_NODE_EDGE_NONE;; + if( i == 0 ) { + nodes[i][j+3]->node_edge |= MG_NODE_EDGE_TOP; + nodes[i][j+4]->node_edge |= MG_NODE_EDGE_TOP; + nodes[i][j+5]->node_edge |= MG_NODE_EDGE_TOP; + } + if( i == nodes.size() - 1 ) { + nodes[i][j+3]->node_edge |= MG_NODE_EDGE_BOTTOM; + nodes[i][j+4]->node_edge |= MG_NODE_EDGE_BOTTOM; + nodes[i][j+5]->node_edge |= MG_NODE_EDGE_BOTTOM; + } + + } + + // std::cout << "Splitting col: result:" << std::endl; + // print(); +} + +/* + 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 : diff --git a/src/object/sp-mesh-array.h b/src/object/sp-mesh-array.h new file mode 100644 index 0000000..0f740d2 --- /dev/null +++ b/src/object/sp-mesh-array.h @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MESH_ARRAY_H +#define SEEN_SP_MESH_ARRAY_H +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyrigt (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/** + A group of classes and functions for manipulating mesh gradients. + + A mesh is made up of an array of patches. Each patch has four sides and four corners. The sides can + be shared between two patches and the corners between up to four. + + The order of the points for each side always goes from left to right or top to bottom. + For sides 2 and 3 the points must be reversed when used (as in calls to cairo functions). + + Two patches: (C=corner, S=side, H=handle, T=tensor) + + C0 H1 H2 C1 C0 H1 H2 C1 + + ---------- + ---------- + + | S0 | S0 | + H1 | T0 T1 |H1 T0 T1 | H1 + |S3 S1|S3 S1| + H2 | T3 T2 |H2 T3 T2 | H2 + | S2 | S2 | + + ---------- + ---------- + + C3 H1 H2 C2 C3 H1 H2 C2 + + The mesh is stored internally as an array of nodes that includes the tensor nodes. + + Note: This code uses tensor points which are not part of the SVG2 plan at the moment. + Including tensor points was motivated by a desire to experiment with their usefulness + in smoothing color transitions. There doesn't seem to be much advantage for that + purpose. However including them internally allows for storing all the points in + an array which simplifies things like inserting new rows or columns. +*/ + +#include <2geom/point.h> +#include "color.h" + +// For color picking +#include "sp-item.h" + +#include <memory> + +enum SPMeshType { + SP_MESH_TYPE_COONS, + SP_MESH_TYPE_BICUBIC +}; + +enum SPMeshGeometry { + SP_MESH_GEOMETRY_NORMAL, + SP_MESH_GEOMETRY_CONICAL +}; + +enum NodeType { + MG_NODE_TYPE_UNKNOWN, + MG_NODE_TYPE_CORNER, + MG_NODE_TYPE_HANDLE, + MG_NODE_TYPE_TENSOR +}; + +// Is a node along an edge? +enum NodeEdge { + MG_NODE_EDGE_NONE, + MG_NODE_EDGE_TOP = 1, + MG_NODE_EDGE_LEFT = 2, + MG_NODE_EDGE_BOTTOM = 4, + MG_NODE_EDGE_RIGHT = 8 +}; + +enum MeshCornerOperation { + MG_CORNER_SIDE_TOGGLE, + MG_CORNER_SIDE_ARC, + MG_CORNER_TENSOR_TOGGLE, + MG_CORNER_COLOR_SMOOTH, + MG_CORNER_COLOR_PICK, + MG_CORNER_INSERT +}; + +enum MeshNodeOperation { + MG_NODE_NO_SCALE, + MG_NODE_SCALE, + MG_NODE_SCALE_HANDLE +}; + +class SPStop; + +class SPMeshNode { +public: + SPMeshNode() { + node_type = MG_NODE_TYPE_UNKNOWN; + node_edge = MG_NODE_EDGE_NONE; + set = false; + draggable = -1; + path_type = 'u'; + opacity = 0.0; + stop = nullptr; + } + NodeType node_type; + unsigned int node_edge; + bool set; + Geom::Point p; + unsigned int draggable; // index of on-screen node + char path_type; + SPColor color; + double opacity; + SPStop *stop; // Stop corresponding to node. +}; + + +// I for Internal to distinguish it from the Object class +// This is a convenience class... +class SPMeshPatchI { + +private: + std::vector<std::vector< SPMeshNode* > > *nodes; + int row; + int col; + +public: + SPMeshPatchI( std::vector<std::vector< SPMeshNode* > > *n, int r, int c ); + Geom::Point getPoint( unsigned int side, unsigned int point ); + std::vector< Geom::Point > getPointsForSide( unsigned int i ); + void setPoint( unsigned int side, unsigned int point, Geom::Point p, bool set = true ); + char getPathType( unsigned int i ); + void setPathType( unsigned int, char t ); + Geom::Point getTensorPoint( unsigned int i ); + void setTensorPoint( unsigned int i, Geom::Point p ); + bool tensorIsSet(); + bool tensorIsSet( unsigned int i ); + Geom::Point coonsTensorPoint( unsigned int i ); + void updateNodes(); + SPColor getColor( unsigned int i ); + void setColor( unsigned int i, SPColor c ); + double getOpacity( unsigned int i ); + void setOpacity( unsigned int i, double o ); + SPStop* getStopPtr( unsigned int i ); + void setStopPtr( unsigned int i, SPStop* ); +}; + +class SPMeshGradient; +class SPCurve; + +// An array of mesh nodes. +class SPMeshNodeArray { + +// Should be private +public: + SPMeshGradient *mg; + std::vector< std::vector< SPMeshNode* > > nodes; + +public: + // Draggables to nodes + bool draggers_valid; + std::vector< SPMeshNode* > corners; + std::vector< SPMeshNode* > handles; + std::vector< SPMeshNode* > tensors; + +public: + + friend class SPMeshPatchI; + + SPMeshNodeArray() { built = false; mg = nullptr; draggers_valid = false; }; + SPMeshNodeArray( SPMeshGradient *mg ); + SPMeshNodeArray( const SPMeshNodeArray& rhs ); + SPMeshNodeArray& operator=(const SPMeshNodeArray& rhs); + + ~SPMeshNodeArray() { clear(); }; + bool built; + + bool read( SPMeshGradient *mg ); + void write( SPMeshGradient *mg ); + void create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bbox ); + void clear(); + void print(); + + // Fill 'smooth' with a smoothed version by subdividing each patch. + void bicubic( SPMeshNodeArray* smooth, SPMeshType type); + + // Get size of patch + unsigned int patch_rows(); + unsigned int patch_columns(); + + SPMeshNode * node( unsigned int i, unsigned int j ) { return nodes[i][j]; } + + // Operations on corners + bool adjacent_corners( unsigned int i, unsigned int j, SPMeshNode* n[4] ); + unsigned int side_toggle( std::vector< unsigned int > ); + unsigned int side_arc( std::vector< unsigned int > ); + unsigned int tensor_toggle( std::vector< unsigned int > ); + unsigned int color_smooth( std::vector< unsigned int > ); + unsigned int color_pick( std::vector< unsigned int >, SPItem* ); + unsigned int insert( std::vector< unsigned int > ); + + // Update other nodes in response to a node move. + void update_handles( unsigned int corner, std::vector< unsigned int > selected_corners, Geom::Point old_p, MeshNodeOperation op ); + + // Return outline path + SPCurve outline_path() const; + + // Transform array + void transform(Geom::Affine const &m); + + // Transform mesh to fill box. Return true if not identity transform. + bool fill_box(Geom::OptRect &box); + + // Find bounding box + // Geom::OptRect findBoundingBox(); + + void split_row( unsigned int i, unsigned int n ); + void split_column( unsigned int j, unsigned int n ); + void split_row( unsigned int i, double coord ); + void split_column( unsigned int j, double coord ); +}; + +#endif /* !SEEN_SP_MESH_ARRAY_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + c-basic-offset:2 + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-mesh-gradient.cpp b/src/object/sp-mesh-gradient.cpp new file mode 100644 index 0000000..4bbc455 --- /dev/null +++ b/src/object/sp-mesh-gradient.cpp @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <glibmm.h> + +#include "attributes.h" +#include "display/cairo-utils.h" +#include "display/drawing-paintserver.h" + +#include "sp-mesh-gradient.h" + +/* + * Mesh Gradient + */ +//#define MESH_DEBUG +//#define OBJECT_TRACE + +SPMeshGradient::SPMeshGradient() : SPGradient(), type( SP_MESH_TYPE_COONS ), type_set(false) { +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::SPMeshGradient" ); +#endif + + // Start coordinate of mesh + this->x.unset(SVGLength::NONE, 0.0, 0.0); + this->y.unset(SVGLength::NONE, 0.0, 0.0); + +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::SPMeshGradient", false ); +#endif +} + +SPMeshGradient::~SPMeshGradient() { +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::~SPMeshGradient (empty function)" ); + objectTrace( "SPMeshGradient::~SPMeshGradient", false ); +#endif +} + +void SPMeshGradient::build(SPDocument *document, Inkscape::XML::Node *repr) { +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::build" ); +#endif + + SPGradient::build(document, repr); + + // Start coordinate of meshgradient + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + + this->readAttr(SPAttr::TYPE); + +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::build", false ); +#endif +} + + +void SPMeshGradient::set(SPAttr key, gchar const *value) { +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::set" ); +#endif + + switch (key) { + case SPAttr::X: + if (!this->x.read(value)) { + this->x.unset(SVGLength::NONE, 0.0, 0.0); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::Y: + if (!this->y.read(value)) { + this->y.unset(SVGLength::NONE, 0.0, 0.0); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::TYPE: + if (value) { + if (!strcmp(value, "coons")) { + this->type = SP_MESH_TYPE_COONS; + } else if (!strcmp(value, "bicubic")) { + this->type = SP_MESH_TYPE_BICUBIC; + } else { + std::cerr << "SPMeshGradient::set(): invalid value " << value << std::endl; + } + this->type_set = TRUE; + } else { + // std::cout << "SPMeshGradient::set() No value " << std::endl; + this->type = SP_MESH_TYPE_COONS; + this->type_set = FALSE; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPGradient::set(key, value); + break; + } + +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::set", false ); +#endif +} + +/** + * Write mesh gradient attributes to associated repr. + */ +Inkscape::XML::Node* SPMeshGradient::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::write", false ); +#endif + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:meshgradient"); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->x._set) { + repr->setAttributeSvgDouble("x", this->x.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->y._set) { + repr->setAttributeSvgDouble("y", this->y.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->type_set) { + switch (this->type) { + case SP_MESH_TYPE_COONS: + repr->setAttribute("type", "coons"); + break; + case SP_MESH_TYPE_BICUBIC: + repr->setAttribute("type", "bicubic"); + break; + default: + // Do nothing + break; + } + } + + SPGradient::write(xml_doc, repr, flags); + +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::write", false ); +#endif + return repr; +} + +std::unique_ptr<Inkscape::DrawingPaintServer> SPMeshGradient::create_drawing_paintserver() +{ + ensureArray(); + + SPMeshNodeArray* my_array = &array; + + if (type_set) { + switch (type) { + case SP_MESH_TYPE_COONS: + // std::cout << "SPMeshGradient::pattern_new: Coons" << std::endl; + break; + case SP_MESH_TYPE_BICUBIC: + array.bicubic(&array_smoothed, type); + my_array = &array_smoothed; + break; + } + } + + int rows = my_array->patch_rows(); + int cols = my_array->patch_columns(); + + std::vector<std::vector<Inkscape::DrawingMeshGradient::PatchData>> patchdata; + patchdata.resize(rows); + for (auto &row : patchdata) { + row.resize(cols); + } + + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + auto patch = SPMeshPatchI(&my_array->nodes, i, j); + auto &data = patchdata[i][j]; + + for (int x = 0; x < 4; x++) { + for (int y = 0; y < 4; y++) { + data.points[x][y] = patch.getPoint(x, y); + } + } + + for (int k = 0; k < 4; k++) { + #ifdef DEBUG_MESH + std::cout << i << " " << j << " " << patch.getPathType(k) << " ("; + for (int p = 0; p < 4; p++) { + std::cout << patch.getPoint(k, p); + } + std::cout << ") " << patch.getColor(k).toString() << std::endl; + #endif + + data.pathtype[k] = patch.getPathType(k); + + if (patch.tensorIsSet(k)) { + data.tensorIsSet[k] = true; + data.tensorpoints[k] = patch.getTensorPoint(k); + //auto t = patch.getTensorPoint(k); + //std::cout << " sp_mesh_create_pattern: tensor " << k + // << " set to " << t << "." << std::endl; + } else { + data.tensorIsSet[k] = false; + //auto t = patch.coonsTensorPoint(k); + //std::cout << " sp_mesh_create_pattern: tensor " << k + // << " calculated as " << t << "." << std::endl; + } + + auto color = patch.getColor(k); + for (int r = 0; r < 3; r++) { + data.color[k][r] = color.v.c[r]; + } + + data.opacity[k] = patch.getOpacity(k); + } + } + } + + return std::make_unique<Inkscape::DrawingMeshGradient>(getSpread(), getUnits(), gradientTransform, + rows, cols, std::move(patchdata)); +} diff --git a/src/object/sp-mesh-gradient.h b/src/object/sp-mesh-gradient.h new file mode 100644 index 0000000..96b8298 --- /dev/null +++ b/src/object/sp-mesh-gradient.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_MESH_GRADIENT_H +#define SP_MESH_GRADIENT_H + +/** \file + * SPMeshGradient: SVG <meshgradient> implementation. + */ + +#include "svg/svg-length.h" +#include "sp-gradient.h" + +/** Mesh gradient. */ +class SPMeshGradient final : public SPGradient { +public: + SPMeshGradient(); + ~SPMeshGradient() override; + int tag() const override { return tag_of<decltype(*this)>; } + + SVGLength x; // Upper left corner of meshgradient + SVGLength y; // Upper right corner of mesh + SPMeshType type; + bool type_set; + + std::unique_ptr<Inkscape::DrawingPaintServer> create_drawing_paintserver() override; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif /* !SP_MESH_GRADIENT_H */ + +/* + 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 : diff --git a/src/object/sp-mesh-patch.cpp b/src/object/sp-mesh-patch.cpp new file mode 100644 index 0000000..7fa2024 --- /dev/null +++ b/src/object/sp-mesh-patch.cpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @gradient meshpatch class. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org> + * Tavmjong Bah <tavjong@free.fr> + * + * Copyright (C) 1999,2005 authors + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-mesh-patch.h" +#include "style.h" + +#include "attributes.h" + +SPMeshpatch* SPMeshpatch::getNextMeshpatch() +{ + SPMeshpatch *result = nullptr; + + for (SPObject* obj = getNext(); obj && !result; obj = obj->getNext()) { + if (is<SPMeshpatch>(obj)) { + result = cast<SPMeshpatch>(obj); + } + } + + return result; +} + +SPMeshpatch* SPMeshpatch::getPrevMeshpatch() +{ + SPMeshpatch *result = nullptr; + + for (SPObject* obj = getPrev(); obj; obj = obj->getPrev()) { + // The closest previous SPObject that is an SPMeshpatch *should* be ourself. + if (is<SPMeshpatch>(obj)) { + auto meshpatch = cast<SPMeshpatch>(obj); + // Sanity check to ensure we have a proper sibling structure. + if (meshpatch->getNextMeshpatch() == this) { + result = meshpatch; + } else { + g_warning("SPMeshpatch previous/next relationship broken"); + } + break; + } + } + + return result; +} + + +/* + * Mesh Patch + */ +SPMeshpatch::SPMeshpatch() : SPObject() { + this->tensor_string = nullptr; +} + +SPMeshpatch::~SPMeshpatch() = default; + +void SPMeshpatch::build(SPDocument* doc, Inkscape::XML::Node* repr) { + SPObject::build(doc, repr); + + this->readAttr(SPAttr::TENSOR); +} + +/** + * Virtual build: set meshpatch attributes from its associated XML node. + */ +void SPMeshpatch::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::TENSOR: { + if (value) { + this->tensor_string = new Glib::ustring( value ); + // std::cout << "sp_meshpatch_set: Tensor string: " << patch->tensor_string->c_str() << std::endl; + } + break; + } + default: { + // Do nothing + } + } +} + +/** + * modified + */ +void SPMeshpatch::modified(unsigned int flags) { + + flags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector<SPObject *> l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + + +/** + * Virtual set: set attribute to value. + */ +Inkscape::XML::Node* SPMeshpatch::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:meshpatch"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/** + * Virtual write: write object attributes to repr. + */ + +/* + 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 : diff --git a/src/object/sp-mesh-patch.h b/src/object/sp-mesh-patch.h new file mode 100644 index 0000000..36208f0 --- /dev/null +++ b/src/object/sp-mesh-patch.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MESHPATCH_H +#define SEEN_SP_MESHPATCH_H + +/** \file + * SPMeshpatch: SVG <meshpatch> implementation. + */ +/* + * Authors: Tavmjong Bah + * + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/ustring.h> +#include "sp-object.h" + +/** Gradient Meshpatch. */ +class SPMeshpatch final : public SPObject { +public: + SPMeshpatch(); + ~SPMeshpatch() override; + int tag() const override { return tag_of<decltype(*this)>; } + + SPMeshpatch* getNextMeshpatch(); + SPMeshpatch* getPrevMeshpatch(); + Glib::ustring * tensor_string; + //SVGLength tx[4]; // Tensor points + //SVGLength ty[4]; // Tensor points + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttr key, const char* value) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif /* !SEEN_SP_MESHPATCH_H */ + +/* + 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 : diff --git a/src/object/sp-mesh-row.cpp b/src/object/sp-mesh-row.cpp new file mode 100644 index 0000000..da9b026 --- /dev/null +++ b/src/object/sp-mesh-row.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @gradient meshrow class. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org> + * Tavmjong Bah <tavjong@free.fr> + * + * Copyright (C) 1999,2005 authors + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-mesh-row.h" +#include "style.h" + +SPMeshrow* SPMeshrow::getNextMeshrow() +{ + SPMeshrow *result = nullptr; + + for (SPObject* obj = getNext(); obj && !result; obj = obj->getNext()) { + if (is<SPMeshrow>(obj)) { + result = cast<SPMeshrow>(obj); + } + } + + return result; +} + +SPMeshrow* SPMeshrow::getPrevMeshrow() +{ + SPMeshrow *result = nullptr; + + for (SPObject* obj = getPrev(); obj; obj = obj->getPrev()) { + // The closest previous SPObject that is an SPMeshrow *should* be ourself. + if (is<SPMeshrow>(obj)) { + auto meshrow = cast<SPMeshrow>(obj); + // Sanity check to ensure we have a proper sibling structure. + if (meshrow->getNextMeshrow() == this) { + result = meshrow; + } else { + g_warning("SPMeshrow previous/next relationship broken"); + } + break; + } + } + + return result; +} + + +/* + * Mesh Row + */ +SPMeshrow::SPMeshrow() : SPObject() { +} + +SPMeshrow::~SPMeshrow() = default; + +void SPMeshrow::build(SPDocument* doc, Inkscape::XML::Node* repr) { + SPObject::build(doc, repr); +} + + +/** + * Virtual build: set meshrow attributes from its associated XML node. + */ +void SPMeshrow::set(SPAttr /*key*/, const gchar* /*value*/) { +} + +/** + * modified + */ +void SPMeshrow::modified(unsigned int flags) { + + flags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector<SPObject *> l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + + +/** + * Virtual set: set attribute to value. + */ +Inkscape::XML::Node* SPMeshrow::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:meshrow"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/** + * Virtual write: write object attributes to repr. + */ + +/* + 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 : diff --git a/src/object/sp-mesh-row.h b/src/object/sp-mesh-row.h new file mode 100644 index 0000000..2b1a2d7 --- /dev/null +++ b/src/object/sp-mesh-row.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MESHROW_H +#define SEEN_SP_MESHROW_H + +/** \file + * SPMeshrow: SVG <meshrow> implementation. + */ +/* + * Authors: Tavmjong Bah + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +/** Gradient Meshrow. */ +class SPMeshrow final : public SPObject { +public: + SPMeshrow(); + ~SPMeshrow() override; + int tag() const override { return tag_of<decltype(*this)>; } + + SPMeshrow* getNextMeshrow(); + SPMeshrow* getPrevMeshrow(); + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttr key, const char* value) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif /* !SEEN_SP_MESHROW_H */ + +/* + 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 : diff --git a/src/object/sp-metadata.cpp b/src/object/sp-metadata.cpp new file mode 100644 index 0000000..391f2dc --- /dev/null +++ b/src/object/sp-metadata.cpp @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <metadata> implementation + * + * Authors: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2004 Kees Cook + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-metadata.h" + +#include <regex> + +#include "xml/node-iterators.h" +#include "document.h" + +#include "sp-item-group.h" +#include "sp-root.h" + +#define noDEBUG_METADATA +#ifdef DEBUG_METADATA +# define debug(f, a...) { g_print("%s(%d) %s:", \ + __FILE__,__LINE__,__FUNCTION__); \ + g_print(f, ## a); \ + g_print("\n"); \ + } +#else +# define debug(f, a...) /**/ +#endif + +/* Metadata base class */ + +SPMetadata::SPMetadata() : SPObject() { +} + +SPMetadata::~SPMetadata() = default; + +namespace { + +void strip_ids_recursively(Inkscape::XML::Node *node) { + using Inkscape::XML::NodeSiblingIterator; + if ( node->type() == Inkscape::XML::NodeType::ELEMENT_NODE ) { + node->removeAttribute("id"); + } + for ( NodeSiblingIterator iter=node->firstChild() ; iter ; ++iter ) { + strip_ids_recursively(iter); + } +} + +/** + * Return true if the given metadata belongs to a CorelDraw layer. + */ +bool is_corel_layer_metadata(SPMetadata const &metadata) +{ + char const *id = metadata.getId(); + return id && // + g_str_has_prefix(id, "CorelCorpID") && // + g_str_has_suffix(id, "Corel-Layer"); +} + +/** + * Get the label of a CorelDraw layer. + */ +std::string corel_layer_get_label(SPGroup const &layer) +{ + char const *id = layer.getId(); + if (id) { + return std::regex_replace(id, std::regex("_x0020_"), " "); + } + return "<unnamed-corel-layer>"; +} +} + + +void SPMetadata::build(SPDocument* doc, Inkscape::XML::Node* repr) { + using Inkscape::XML::NodeSiblingIterator; + + debug("0x%08x",(unsigned int)this); + + /* clean up our mess from earlier versions; elements under rdf:RDF should not + * have id= attributes... */ + static GQuark const rdf_root_name = g_quark_from_static_string("rdf:RDF"); + + for ( NodeSiblingIterator iter=repr->firstChild() ; iter ; ++iter ) { + if ( (GQuark)iter->code() == rdf_root_name ) { + strip_ids_recursively(iter); + } + } + + SPObject::build(doc, repr); +} + +void SPMetadata::release() { + debug("0x%08x",(unsigned int)this); + + // handle ourself + + SPObject::release(); +} + +void SPMetadata::set(SPAttr key, const gchar* value) { + debug("0x%08x %s(%u): '%s'",(unsigned int)this, + sp_attribute_name(key),key,value); + + // see if any parents need this value + SPObject::set(key, value); +} + +void SPMetadata::update(SPCtx* /*ctx*/, unsigned int flags) { + debug("0x%08x",(unsigned int)this); + //auto metadata = cast<SPMetadata>(object); + + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something? */ + + // Detect CorelDraw layers + if (is_corel_layer_metadata(*this)) { + auto layer = cast<SPGroup>(parent); + if (layer && layer->layerMode() == SPGroup::GROUP) { + layer->setLayerMode(SPGroup::LAYER); + if (!layer->label()) { + layer->setLabel(corel_layer_get_label(*layer).c_str()); + } + } + } + } + +// SPObject::onUpdate(ctx, flags); +} + +Inkscape::XML::Node* SPMetadata::write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) { + debug("0x%08x",(unsigned int)this); + + if ( repr != this->getRepr() ) { + if (repr) { + repr->mergeFrom(this->getRepr(), "id"); + } else { + repr = this->getRepr()->duplicate(doc); + } + } + + SPObject::write(doc, repr, flags); + + return repr; +} + +/** + * Retrieves the metadata object associated with a document. + */ +SPMetadata *sp_document_metadata(SPDocument *document) +{ + SPObject *nv; + + g_return_val_if_fail (document != nullptr, NULL); + + nv = sp_item_group_get_child_by_name( document->getRoot(), nullptr, + "metadata"); + g_assert (nv != nullptr); + + return (SPMetadata *)nv; +} + + +/* + 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 : diff --git a/src/object/sp-metadata.h b/src/object/sp-metadata.h new file mode 100644 index 0000000..d19784e --- /dev/null +++ b/src/object/sp-metadata.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_METADATA_H +#define SEEN_SP_METADATA_H + +/* + * SVG <metadata> implementation + * + * Authors: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2004 Kees Cook + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +/* Metadata base class */ + +class SPMetadata final : public SPObject { +public: + SPMetadata(); + ~SPMetadata() override; + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttr key, const char* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +SPMetadata * sp_document_metadata (SPDocument *document); + +#endif +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-missing-glyph.cpp b/src/object/sp-missing-glyph.cpp new file mode 100644 index 0000000..ff32a5f --- /dev/null +++ b/src/object/sp-missing-glyph.cpp @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <missing-glyph> element implementation + * + * Author: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Abhishek Sharma + * + * Copyright (C) 2008, Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/repr.h" +#include "attributes.h" +#include "sp-missing-glyph.h" +#include "document.h" + +SPMissingGlyph::SPMissingGlyph() : SPObject() { +//TODO: correct these values: + this->d = nullptr; + this->horiz_adv_x = 0; + this->vert_origin_x = 0; + this->vert_origin_y = 0; + this->vert_adv_y = 0; +} + +SPMissingGlyph::~SPMissingGlyph() = default; + +void SPMissingGlyph::build(SPDocument* doc, Inkscape::XML::Node* repr) { + SPObject::build(doc, repr); + + this->readAttr(SPAttr::D); + this->readAttr(SPAttr::HORIZ_ADV_X); + this->readAttr(SPAttr::VERT_ORIGIN_X); + this->readAttr(SPAttr::VERT_ORIGIN_Y); + this->readAttr(SPAttr::VERT_ADV_Y); +} + +void SPMissingGlyph::release() { + SPObject::release(); +} + + +void SPMissingGlyph::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::D: + { + if (this->d) { + g_free(this->d); + } + this->d = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SPAttr::HORIZ_ADV_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + if (number != this->horiz_adv_x){ + this->horiz_adv_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::VERT_ORIGIN_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + if (number != this->vert_origin_x){ + this->vert_origin_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::VERT_ORIGIN_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + if (number != this->vert_origin_y){ + this->vert_origin_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SPAttr::VERT_ADV_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + if (number != this->vert_adv_y){ + this->vert_adv_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + { + SPObject::set(key, value); + break; + } + } +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPMissingGlyph::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:glyph"); + } + + /* I am commenting out this part because I am not certain how does it work. I will have to study it later. Juca + repr->setAttribute("d", glyph->d); + repr->setAttributeSvgDouble("horiz-adv-x", glyph->horiz_adv_x); + repr->setAttributeSvgDouble("vert-origin-x", glyph->vert_origin_x); + repr->setAttributeSvgDouble("vert-origin-y", glyph->vert_origin_y); + repr->setAttributeSvgDouble("vert-adv-y", glyph->vert_adv_y); + */ + if (repr != this->getRepr()) { + + // TODO + // All the COPY_ATTR functions below use + // XML Tree directly while they shouldn't. + COPY_ATTR(repr, this->getRepr(), "d"); + COPY_ATTR(repr, this->getRepr(), "horiz-adv-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-y"); + COPY_ATTR(repr, this->getRepr(), "vert-adv-y"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* + 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 : diff --git a/src/object/sp-missing-glyph.h b/src/object/sp-missing-glyph.h new file mode 100644 index 0000000..71f59b4 --- /dev/null +++ b/src/object/sp-missing-glyph.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MISSING_GLYPH_H +#define SEEN_SP_MISSING_GLYPH_H + +/* + * SVG <missing-glyph> element implementation + * + * Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +class SPMissingGlyph final : public SPObject { +public: + SPMissingGlyph(); + ~SPMissingGlyph() override; + int tag() const override { return tag_of<decltype(*this)>; } + + char* d; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttr key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + +private: + double horiz_adv_x; + double vert_origin_x; + double vert_origin_y; + double vert_adv_y; +}; + +#endif //#ifndef __SP_MISSING_GLYPH_H__ diff --git a/src/object/sp-namedview.cpp b/src/object/sp-namedview.cpp new file mode 100644 index 0000000..6da51b0 --- /dev/null +++ b/src/object/sp-namedview.cpp @@ -0,0 +1,1010 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * <sodipodi:namedview> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 1999-2013 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-namedview.h" + +#include <cstring> +#include <string> + +#include <2geom/transforms.h> + +#include <gtkmm/window.h> + +#include "attributes.h" +#include "conn-avoid-ref.h" // for defaultConnSpacing. +#include "desktop-events.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "enums.h" +#include "event-log.h" +#include "layer-manager.h" +#include "page-manager.h" +#include "preferences.h" +#include "sp-guide.h" +#include "sp-grid.h" +#include "sp-page.h" +#include "sp-item-group.h" +#include "sp-root.h" + +#include "actions/actions-canvas-snapping.h" +#include "display/control/canvas-page.h" +#include "svg/svg-color.h" +#include "ui/monitor.h" +#include "ui/widget/canvas.h" +#include "ui/widget/canvas-grid.h" +#include "util/units.h" +#include "widgets/desktop-widget.h" +#include "xml/repr.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +#define DEFAULTGUIDECOLOR 0x0086e599 +#define DEFAULTGUIDEHICOLOR 0xff00007f +#define DEFAULTDESKCOLOR 0xd1d1d1ff + +SPNamedView::SPNamedView() + : SPObjectGroup() + , snap_manager(this, get_snapping_preferences()) + , showguides(true) + , lockguides(false) + , clip_to_page(false) + , grids_visible(false) + , desk_checkerboard(false) +{ + this->zoom = 0; + this->guidecolor = 0; + this->guidehicolor = 0; + this->views.clear(); + // this->page_size_units = nullptr; + this->window_x = 0; + this->cy = 0; + this->window_y = 0; + this->display_units = nullptr; + // this->page_size_units = nullptr; + this->cx = 0; + this->rotation = 0; + this->window_width = 0; + this->window_height = 0; + this->window_maximized = 0; + this->desk_color = DEFAULTDESKCOLOR; + + this->editable = TRUE; + this->viewcount = 0; + + this->default_layer_id = 0; + + this->connector_spacing = defaultConnSpacing; + + this->_viewport = new Inkscape::CanvasPage(); + this->_viewport->hide(); +} + +SPNamedView::~SPNamedView() +{ + delete _viewport; +} + +void SPNamedView::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObjectGroup::build(document, repr); + + this->readAttr(SPAttr::INKSCAPE_DOCUMENT_UNITS); + this->readAttr(SPAttr::UNITS); + this->readAttr(SPAttr::VIEWONLY); + this->readAttr(SPAttr::SHOWGUIDES); + this->readAttr(SPAttr::SHOWGRIDS); + this->readAttr(SPAttr::GRIDTOLERANCE); + this->readAttr(SPAttr::GUIDETOLERANCE); + this->readAttr(SPAttr::OBJECTTOLERANCE); + this->readAttr(SPAttr::ALIGNMENTTOLERANCE); + this->readAttr(SPAttr::DISTRIBUTIONTOLERANCE); + this->readAttr(SPAttr::GUIDECOLOR); + this->readAttr(SPAttr::GUIDEOPACITY); + this->readAttr(SPAttr::GUIDEHICOLOR); + this->readAttr(SPAttr::GUIDEHIOPACITY); + this->readAttr(SPAttr::SHOWBORDER); + this->readAttr(SPAttr::SHOWPAGESHADOW); + this->readAttr(SPAttr::BORDERLAYER); + this->readAttr(SPAttr::BORDERCOLOR); + this->readAttr(SPAttr::BORDEROPACITY); + this->readAttr(SPAttr::PAGECOLOR); + this->readAttr(SPAttr::PAGELABELSTYLE); + this->readAttr(SPAttr::INKSCAPE_DESK_COLOR); + this->readAttr(SPAttr::INKSCAPE_DESK_CHECKERBOARD); + this->readAttr(SPAttr::INKSCAPE_PAGESHADOW); + this->readAttr(SPAttr::INKSCAPE_ZOOM); + this->readAttr(SPAttr::INKSCAPE_ROTATION); + this->readAttr(SPAttr::INKSCAPE_CX); + this->readAttr(SPAttr::INKSCAPE_CY); + this->readAttr(SPAttr::INKSCAPE_WINDOW_WIDTH); + this->readAttr(SPAttr::INKSCAPE_WINDOW_HEIGHT); + this->readAttr(SPAttr::INKSCAPE_WINDOW_X); + this->readAttr(SPAttr::INKSCAPE_WINDOW_Y); + this->readAttr(SPAttr::INKSCAPE_WINDOW_MAXIMIZED); + this->readAttr(SPAttr::INKSCAPE_CURRENT_LAYER); + this->readAttr(SPAttr::INKSCAPE_CONNECTOR_SPACING); + this->readAttr(SPAttr::INKSCAPE_LOCKGUIDES); + readAttr(SPAttr::INKSCAPE_CLIP_TO_PAGE_RENDERING); + + /* Construct guideline and pages list */ + for (auto &child : children) { + if (auto guide = cast<SPGuide>(&child)) { + this->guides.push_back(guide); + //g_object_set(G_OBJECT(g), "color", nv->guidecolor, "hicolor", nv->guidehicolor, NULL); + guide->setColor(this->guidecolor); + guide->setHiColor(this->guidehicolor); + guide->readAttr(SPAttr::INKSCAPE_COLOR); + } + if (auto page = cast<SPPage>(&child)) { + document->getPageManager().addPage(page); + } + if (auto grid = cast<SPGrid>(&child)) { + grids.emplace_back(grid); + } + } +} + +void SPNamedView::release() { + this->guides.clear(); + this->grids.clear(); + + SPObjectGroup::release(); +} + +void SPNamedView::set_clip_to_page(SPDesktop* desktop, bool enable) { + if (desktop) { + desktop->getCanvas()->set_clip_to_page_mode(enable); + } +} + +void SPNamedView::set_desk_color(SPDesktop* desktop) { + if (desktop) { + if (desk_checkerboard) { + desktop->getCanvas()->set_desk(desk_color); + } else { + desktop->getCanvas()->set_desk(desk_color | 0xff); + } + // Update pages, who's colours sometimes change whe the desk color changes. + document->getPageManager().setDefaultAttributes(_viewport); + } +} + +void SPNamedView::modified(unsigned int flags) +{ + // Copy the page style for the default viewport attributes + auto &page_manager = document->getPageManager(); + if (flags & SP_OBJECT_MODIFIED_FLAG) { + page_manager.setDefaultAttributes(_viewport); + updateViewPort(); + // Pass modifications to the page manager to update the page items. + for (auto &page : page_manager.getPages()) { + page->setDefaultAttributes(); + } + // Update unit action group + auto action = document->getActionGroup()->lookup_action("set-display-unit"); + if (auto saction = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(action)) { + Glib::VariantType String(Glib::VARIANT_TYPE_STRING); + saction->change_state(getDisplayUnit()->abbr); + } + + updateGuides(); + updateGrids(); + } + // Add desk color and checkerboard pattern to desk view + for (auto desktop : views) { + set_desk_color(desktop); + set_clip_to_page(desktop, clip_to_page); + } + + for (auto child : this->childList(false)) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags & SP_OBJECT_MODIFIED_CASCADE); + } + } +} + +/** + * Propergate the update to the child nodes so they can be updated correctly. + */ +void SPNamedView::update(SPCtx *ctx, guint flags) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + for (auto child : this->childList(false)) { + if (flags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->updateDisplay(ctx, flags); + } + } +} + +const Inkscape::Util::Unit* sp_parse_document_units(const char* value) { + /* The default display unit if the document doesn't override this: e.g. for files saved as + * `plain SVG', or non-inkscape files, or files created by an inkscape 0.40 & + * earlier. + * + * Note that these units are not the same as the units used for the values in SVG! + * + * We default to `px'. + */ + static Inkscape::Util::Unit const *px = unit_table.getUnit("px"); + Inkscape::Util::Unit const *new_unit = px; + + if (value) { + Inkscape::Util::Unit const *const req_unit = unit_table.getUnit(value); + if ( !unit_table.hasUnit(value) ) { + g_warning("Unrecognized unit `%s'", value); + /* fixme: Document errors should be reported in the status bar or + * the like (e.g. as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing); g_log + * should be only for programmer errors. */ + } else if ( req_unit->isAbsolute() ) { + new_unit = req_unit; + } else { + g_warning("Document units must be absolute like `mm', `pt' or `px', but found `%s'", value); + /* fixme: Don't use g_log (see above). */ + } + } + + return new_unit; +} + +void SPNamedView::set(SPAttr key, const gchar* value) { + // Send page attributes to the page manager. + if (document->getPageManager().subset(key, value)) { + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + return; + } + + switch (key) { + case SPAttr::VIEWONLY: + this->editable = (!value); + break; + case SPAttr::SHOWGUIDES: + this->showguides.readOrUnset(value); + break; + case SPAttr::INKSCAPE_LOCKGUIDES: + this->lockguides.readOrUnset(value); + break; + case SPAttr::SHOWGRIDS: + this->grids_visible.readOrUnset(value); + break; + case SPAttr::GRIDTOLERANCE: + this->snap_manager.snapprefs.setGridTolerance(value ? g_ascii_strtod(value, nullptr) : 10); + break; + case SPAttr::GUIDETOLERANCE: + this->snap_manager.snapprefs.setGuideTolerance(value ? g_ascii_strtod(value, nullptr) : 20); + break; + case SPAttr::OBJECTTOLERANCE: + this->snap_manager.snapprefs.setObjectTolerance(value ? g_ascii_strtod(value, nullptr) : 20); + break; + case SPAttr::ALIGNMENTTOLERANCE: + this->snap_manager.snapprefs.setAlignementTolerance(value ? g_ascii_strtod(value, nullptr) : 5); + break; + case SPAttr::DISTRIBUTIONTOLERANCE: + this->snap_manager.snapprefs.setDistributionTolerance(value ? g_ascii_strtod(value, nullptr) : 5); + break; + case SPAttr::GUIDECOLOR: + this->guidecolor = (this->guidecolor & 0xff) | (DEFAULTGUIDECOLOR & 0xffffff00); + if (value) { + this->guidecolor = (this->guidecolor & 0xff) | sp_svg_read_color(value, this->guidecolor); + } + for(auto guide : this->guides) { + guide->setColor(this->guidecolor); + guide->readAttr(SPAttr::INKSCAPE_COLOR); + } + break; + case SPAttr::GUIDEOPACITY: + sp_ink_read_opacity(value, &this->guidecolor, DEFAULTGUIDECOLOR); + for (auto guide : this->guides) { + guide->setColor(this->guidecolor); + guide->readAttr(SPAttr::INKSCAPE_COLOR); + } + break; + case SPAttr::GUIDEHICOLOR: + this->guidehicolor = (this->guidehicolor & 0xff) | (DEFAULTGUIDEHICOLOR & 0xffffff00); + if (value) { + this->guidehicolor = (this->guidehicolor & 0xff) | sp_svg_read_color(value, this->guidehicolor); + } + for(auto guide : this->guides) { + guide->setHiColor(this->guidehicolor); + } + break; + case SPAttr::GUIDEHIOPACITY: + sp_ink_read_opacity(value, &this->guidehicolor, DEFAULTGUIDEHICOLOR); + for (auto guide : this->guides) { + guide->setHiColor(this->guidehicolor); + } + break; + case SPAttr::INKSCAPE_DESK_COLOR: + if (value) { + desk_color = sp_svg_read_color(value, desk_color); + } + break; + case SPAttr::INKSCAPE_DESK_CHECKERBOARD: + this->desk_checkerboard.readOrUnset(value); + break; + case SPAttr::INKSCAPE_ZOOM: + this->zoom = value ? g_ascii_strtod(value, nullptr) : 0; // zero means not set + break; + case SPAttr::INKSCAPE_ROTATION: + this->rotation = value ? g_ascii_strtod(value, nullptr) : 0; // zero means not set + break; + case SPAttr::INKSCAPE_CX: + this->cx = value ? g_ascii_strtod(value, nullptr) : HUGE_VAL; // HUGE_VAL means not set + break; + case SPAttr::INKSCAPE_CY: + this->cy = value ? g_ascii_strtod(value, nullptr) : HUGE_VAL; // HUGE_VAL means not set + break; + case SPAttr::INKSCAPE_WINDOW_WIDTH: + this->window_width = value? atoi(value) : -1; // -1 means not set + break; + case SPAttr::INKSCAPE_WINDOW_HEIGHT: + this->window_height = value ? atoi(value) : -1; // -1 means not set + break; + case SPAttr::INKSCAPE_WINDOW_X: + this->window_x = value ? atoi(value) : 0; + break; + case SPAttr::INKSCAPE_WINDOW_Y: + this->window_y = value ? atoi(value) : 0; + break; + case SPAttr::INKSCAPE_WINDOW_MAXIMIZED: + this->window_maximized = value ? atoi(value) : 0; + break; + case SPAttr::INKSCAPE_CURRENT_LAYER: + this->default_layer_id = value ? g_quark_from_string(value) : 0; + break; + case SPAttr::INKSCAPE_CONNECTOR_SPACING: + this->connector_spacing = value ? g_ascii_strtod(value, nullptr) : defaultConnSpacing; + break; + case SPAttr::INKSCAPE_DOCUMENT_UNITS: + display_units = sp_parse_document_units(value); + break; + case SPAttr::INKSCAPE_CLIP_TO_PAGE_RENDERING: + clip_to_page.readOrUnset(value); + break; + /* + case SPAttr::UNITS: { + // Only used in "Custom size" section of Document Properties dialog + Inkscape::Util::Unit const *new_unit = nullptr; + + if (value) { + Inkscape::Util::Unit const *const req_unit = unit_table.getUnit(value); + if ( !unit_table.hasUnit(value) ) { + g_warning("Unrecognized unit `%s'", value); + / * fixme: Document errors should be reported in the status bar or + * the like (e.g. as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing); g_log + * should be only for programmer errors. * / + } else if ( req_unit->isAbsolute() ) { + new_unit = req_unit; + } else { + g_warning("Document units must be absolute like `mm', `pt' or `px', but found `%s'", + value); + / * fixme: Don't use g_log (see above). * / + } + } + this->page_size_units = new_unit; + break; + } */ + default: + SPObjectGroup::set(key, value); + return; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Update the visibility of the viewport space. This can look like a page + * if there's no multi-pages, or invisible if it shadows the first page. + */ +void SPNamedView::updateViewPort() +{ + auto box = document->preferredBounds(); + if (auto page = document->getPageManager().getPageAt(box->corner(0))) { + // An existing page is set as the main page, so hide th viewport canvas item. + _viewport->hide(); + page->setDesktopRect(*box); + } else { + // Otherwise we are showing the viewport item. + _viewport->show(); + _viewport->update(*box, {}, {}, nullptr, document->getPageManager().hasPages()); + } +} + +void SPNamedView::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObjectGroup::child_added(child, ref); + + SPObject *no = this->document->getObjectByRepr(child); + if (!no) + return; + + if (auto grid = cast<SPGrid>(no)) { + grids.emplace_back(grid); + for (auto view : views) { + grid->show(view); + } + } else if (!strcmp(child->name(), "inkscape:page")) { + if (auto page = cast<SPPage>(no)) { + document->getPageManager().addPage(page); + for (auto view : this->views) { + page->showPage(view->getCanvasPagesBg(), view->getCanvasPagesFg()); + } + } + } else { + if (auto g = cast<SPGuide>(no)) { + this->guides.push_back(g); + + //g_object_set(G_OBJECT(g), "color", this->guidecolor, "hicolor", this->guidehicolor, NULL); + g->setColor(this->guidecolor); + g->setHiColor(this->guidehicolor); + g->readAttr(SPAttr::INKSCAPE_COLOR); + + if (this->editable) { + for(auto view : this->views) { + g->SPGuide::showSPGuide(view->getCanvasGuides()); + + if (view->guides_active) { + g->sensitize(view->getCanvas(), TRUE); + } + + this->setShowGuideSingle(g); + } + } + } + } +} + +void SPNamedView::remove_child(Inkscape::XML::Node *child) { + if (!strcmp(child->name(), "inkscape:page")) { + document->getPageManager().removePage(child); + } else if (!strcmp(child->name(), "inkscape:grid")) { + for (auto it = grids.begin(); it != grids.end(); ++it) { + auto grid = *it; + if (grid->getRepr() == child) { + for (auto view : views) { + grid->hide(view); + } + grids.erase(it); + break; + } + } + } else { + for(std::vector<SPGuide *>::iterator it=this->guides.begin();it!=this->guides.end();++it ) { + if ( (*it)->getRepr() == child ) { + this->guides.erase(it); + break; + } + } + } + + SPObjectGroup::remove_child(child); +} + +void SPNamedView::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_repr, + Inkscape::XML::Node *new_repr) +{ + SPObjectGroup::order_changed(child, old_repr, new_repr); + if (!strcmp(child->name(), "inkscape:page")) { + document->getPageManager().reorderPage(child); + } +} + +Inkscape::XML::Node* SPNamedView::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( ( flags & SP_OBJECT_WRITE_EXT ) && + repr != this->getRepr() ) + { + if (repr) { + repr->mergeFrom(this->getRepr(), "id"); + } else { + repr = this->getRepr()->duplicate(xml_doc); + } + } + + return repr; +} + +void SPNamedView::show(SPDesktop *desktop) +{ + + for (auto guide : this->guides) { + guide->showSPGuide( desktop->getCanvasGuides() ); + + if (desktop->guides_active) { + guide->sensitize(desktop->getCanvas(), TRUE); + } + this->setShowGuideSingle(guide); + } + + for (auto grid : grids) { + grid->show(desktop); + } + + auto box = document->preferredBounds(); + _viewport->add(*box, desktop->getCanvasPagesBg(), desktop->getCanvasPagesFg()); + document->getPageManager().setDefaultAttributes(_viewport); + updateViewPort(); + + for (auto page : document->getPageManager().getPages()) { + page->showPage(desktop->getCanvasPagesBg(), desktop->getCanvasPagesFg()); + } + + views.push_back(desktop); +} + +/* + * Restores window geometry from the document settings or defaults in prefs + */ +void sp_namedview_window_from_document(SPDesktop *desktop) +{ + SPNamedView *nv = desktop->namedview; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int window_geometry = prefs->getInt("/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_NONE); + int default_size = prefs->getInt("/options/defaultwindowsize/value", PREFS_WINDOW_SIZE_NATURAL); + bool new_document = (nv->window_width <= 0) || (nv->window_height <= 0); + + // restore window size and position stored with the document + Gtk::Window *win = desktop->getToplevel(); + g_assert(win); + + if (window_geometry == PREFS_WINDOW_GEOMETRY_LAST) { + gint pw = prefs->getInt("/desktop/geometry/width", -1); + gint ph = prefs->getInt("/desktop/geometry/height", -1); + gint px = prefs->getInt("/desktop/geometry/x", -1); + gint py = prefs->getInt("/desktop/geometry/y", -1); + gint full = prefs->getBool("/desktop/geometry/fullscreen"); + gint maxed = prefs->getBool("/desktop/geometry/maximized"); + if (pw>0 && ph>0) { + + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_at_point(px, py); + pw = std::min(pw, monitor_geometry.get_width()); + ph = std::min(ph, monitor_geometry.get_height()); + desktop->setWindowSize(pw, ph); + desktop->setWindowPosition(Geom::Point(px, py)); + } + if (maxed) { + win->maximize(); + } + if (full) { + win->fullscreen(); + } + } else if ((window_geometry == PREFS_WINDOW_GEOMETRY_FILE && nv->window_maximized) || + ((new_document || window_geometry == PREFS_WINDOW_GEOMETRY_NONE) && + default_size == PREFS_WINDOW_SIZE_MAXIMIZED)) { + win->maximize(); + } else { + const int MIN_WINDOW_SIZE = 600; + + int w = prefs->getInt("/template/base/inkscape:window-width", 0); + int h = prefs->getInt("/template/base/inkscape:window-height", 0); + bool move_to_screen = false; + if (window_geometry == PREFS_WINDOW_GEOMETRY_FILE && !new_document) { + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_at_point(nv->window_x, nv->window_y); + w = MIN(monitor_geometry.get_width(), nv->window_width); + h = MIN(monitor_geometry.get_height(), nv->window_height); + move_to_screen = true; + } else if (default_size == PREFS_WINDOW_SIZE_LARGE) { + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_at_window(win->get_window()); + w = MAX(0.75 * monitor_geometry.get_width(), MIN_WINDOW_SIZE); + h = MAX(0.75 * monitor_geometry.get_height(), MIN_WINDOW_SIZE); + } else if (default_size == PREFS_WINDOW_SIZE_SMALL) { + w = h = MIN_WINDOW_SIZE; + } else if (default_size == PREFS_WINDOW_SIZE_NATURAL) { + // don't set size (i.e. keep the gtk+ default, which will be the natural size) + // unless gtk+ decided it would be a good idea to show a window that is larger than the screen + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_at_window(win->get_window()); + int monitor_width = monitor_geometry.get_width(); + int monitor_height = monitor_geometry.get_height(); + int window_width, window_height; + win->get_size(window_width, window_height); + if (window_width > monitor_width || window_height > monitor_height) { + w = std::min(monitor_width, window_width); + h = std::min(monitor_height, window_height); + } + } + if ((w > 0) && (h > 0)) { + desktop->setWindowSize(w, h); + if (move_to_screen) { + desktop->setWindowPosition(Geom::Point(nv->window_x, nv->window_y)); + } + } + } + + // Cancel any history of transforms up to this point (must be before call to zoom). + desktop->clear_transform_history(); +} + +/* + * Restores zoom and view from the document settings + */ +void sp_namedview_zoom_and_view_from_document(SPDesktop *desktop) +{ + SPNamedView *nv = desktop->namedview; + if (nv->zoom != 0 && nv->zoom != HUGE_VAL && !std::isnan(nv->zoom) + && nv->cx != HUGE_VAL && !std::isnan(nv->cx) + && nv->cy != HUGE_VAL && !std::isnan(nv->cy)) { + desktop->zoom_absolute( Geom::Point(nv->cx, nv->cy), nv->zoom, false ); + } else if (auto document = desktop->getDocument()) { + // document without saved zoom, zoom to its page + document->getPageManager().zoomToSelectedPage(desktop); + } + if (nv->rotation != 0 && nv->rotation != HUGE_VAL && !std::isnan(nv->rotation)) { + Geom::Point p; + if (nv->cx != HUGE_VAL && !std::isnan(nv->cx) && nv->cy != HUGE_VAL && !std::isnan(nv->cy)) { + p = Geom::Point(nv->cx, nv->cy); + }else{ + p = desktop->current_center(); + } + desktop->rotate_absolute_keep_point(p, nv->rotation * M_PI / 180.0); + } +} + +void sp_namedview_update_layers_from_document (SPDesktop *desktop) +{ + SPObject *layer = nullptr; + SPDocument *document = desktop->doc(); + SPNamedView *nv = desktop->namedview; + if ( nv->default_layer_id != 0 ) { + layer = document->getObjectById(g_quark_to_string(nv->default_layer_id)); + } + // don't use that object if it's not at least group + if ( !layer || !is<SPGroup>(layer) ) { + layer = nullptr; + } + // if that didn't work out, look for the topmost layer + if (!layer) { + for (auto& iter: document->getRoot()->children) { + if (desktop->layerManager().isLayer(&iter)) { + layer = &iter; + } + } + } + if (layer) { + desktop->layerManager().setCurrentLayer(layer); + } + + // FIXME: find a better place to do this + document->get_event_log()->updateUndoVerbs(); +} + +void sp_namedview_document_from_window(SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int window_geometry = prefs->getInt("/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_NONE); + bool save_geometry_in_file = window_geometry == PREFS_WINDOW_GEOMETRY_FILE; + bool save_viewport_in_file = prefs->getBool("/options/savedocviewport/value", true); + Inkscape::XML::Node *view = desktop->namedview->getRepr(); + + // saving window geometry is not undoable + DocumentUndo::ScopedInsensitive _no_undo(desktop->getDocument()); + + if (save_viewport_in_file) { + view->setAttributeSvgDouble("inkscape:zoom", desktop->current_zoom()); + double rotation = ::round(desktop->current_rotation() * 180.0 / M_PI); + view->setAttributeSvgNonDefaultDouble("inkscape:rotation", rotation, 0.0); + Geom::Point center = desktop->current_center(); + view->setAttributeSvgDouble("inkscape:cx", center.x()); + view->setAttributeSvgDouble("inkscape:cy", center.y()); + } + + if (save_geometry_in_file) { + gint w, h, x, y; + desktop->getWindowGeometry(x, y, w, h); + view->setAttributeInt("inkscape:window-width", w); + view->setAttributeInt("inkscape:window-height", h); + view->setAttributeInt("inkscape:window-x", x); + view->setAttributeInt("inkscape:window-y", y); + view->setAttributeInt("inkscape:window-maximized", desktop->is_maximized()); + } + + view->setAttribute("inkscape:current-layer", desktop->layerManager().currentLayer()->getId()); +} + +void SPNamedView::hide(SPDesktop const *desktop) +{ + g_assert(desktop != nullptr); + g_assert(std::find(views.begin(),views.end(),desktop)!=views.end()); + for (auto guide : guides) { + guide->hideSPGuide(desktop->getCanvas()); + } + for (auto grid : grids) { + grid->hide(desktop); + } + _viewport->remove(desktop->getCanvas()); + for (auto page : document->getPageManager().getPages()) { + page->hidePage(desktop->getCanvas()); + } + views.erase(std::remove(views.begin(),views.end(),desktop),views.end()); +} + +/** + * Set an attribute in the named view to the value in this preference, or use the fallback. + * + * @param attribute - The svg namedview attribute to set. + * @param preference - The preference to find the value from (optional) + * @param fallback - The fallback to use if preference not set or not found. (optional) + */ +void SPNamedView::setDefaultAttribute(std::string attribute, std::string preference, std::string fallback) +{ + if (!getAttribute(attribute.c_str())) { + std::string value = ""; + if (!preference.empty()) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + value = prefs->getString(preference); + } + if (value.empty() && !fallback.empty()) { + value = fallback; + } + if (!value.empty()) { + setAttribute(attribute, value); + } + } +} + +void SPNamedView::activateGuides(void* desktop, bool active) +{ + g_assert(desktop != nullptr); + g_assert(std::find(views.begin(),views.end(),desktop)!=views.end()); + + SPDesktop *dt = static_cast<SPDesktop*>(desktop); + for(auto & guide : this->guides) { + guide->sensitize(dt->getCanvas(), active); + } +} + +gchar const *SPNamedView::getName() const +{ + return this->getAttribute("id"); +} + +std::vector<SPDesktop *> const SPNamedView::getViewList() const +{ + return views; +} + +void SPNamedView::toggleShowGuides() +{ + setShowGuides(!getShowGuides()); +} + +void SPNamedView::toggleLockGuides() +{ + setLockGuides(!getLockGuides()); +} + +void SPNamedView::toggleShowGrids() +{ + setShowGrids(!getShowGrids()); +} + +void SPNamedView::setShowGrids(bool v) +{ + { + DocumentUndo::ScopedInsensitive ice(document); + + if (v && grids.empty()) + SPGrid::create_new(document, this->getRepr(), GridType::RECTANGULAR); + + getRepr()->setAttributeBoolean("showgrid", v); + } + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +bool SPNamedView::getShowGrids() +{ + return grids_visible; +} + +void SPNamedView::setShowGuides(bool v) +{ + if (auto repr = getRepr()) { + { + DocumentUndo::ScopedInsensitive _no_undo(document); + repr->setAttributeBoolean("showguides", v); + } + requestModified(SP_OBJECT_MODIFIED_FLAG); + } +} + +void SPNamedView::setLockGuides(bool v) +{ + if (auto repr = getRepr()) { + { + DocumentUndo::ScopedInsensitive _no_undo(document); + repr->setAttributeBoolean("inkscape:lockguides", v); + } + requestModified(SP_OBJECT_MODIFIED_FLAG); + } +} + +void SPNamedView::setShowGuideSingle(SPGuide *guide) +{ + if (getShowGuides()) + guide->showSPGuide(); + else + guide->hideSPGuide(); +} + +bool SPNamedView::getShowGuides() +{ + if (auto repr = getRepr()) { + // show guides if not specified, for backwards compatibility + return repr->getAttributeBoolean("showguides", true); + } + + return false; +} + +bool SPNamedView::getLockGuides() +{ + if (auto repr = getRepr()) { + return repr->getAttributeBoolean("inkscape:lockguides"); + } + + return false; +} + +void SPNamedView::updateGrids() +{ + if (auto saction = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic( + document->getActionGroup()->lookup_action("show-grids"))) { + + saction->change_state(getShowGrids()); + } + { + DocumentUndo::ScopedInsensitive ice(document); + for (auto grid : grids) { + grid->setVisible(getShowGrids()); + } + } +} + +void SPNamedView::updateGuides() +{ + if (auto saction = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic( + document->getActionGroup()->lookup_action("show-all-guides"))) { + + saction->change_state(getShowGuides()); + } + + if (auto saction = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic( + document->getActionGroup()->lookup_action("lock-all-guides"))) { + + bool is_locked = getLockGuides(); + saction->change_state(is_locked); + + for (auto desktop : views) { + auto dt_widget = desktop->getDesktopWidget(); + dt_widget->get_canvas_grid()->GetGuideLock()->set_active(is_locked); + } + } + + for (SPGuide *guide : guides) { + setShowGuideSingle(guide); + guide->set_locked(this->getLockGuides(), true); + } +} + +/** + * Returns namedview's default unit. + * If no default unit is set, "px" is returned + */ +Inkscape::Util::Unit const * SPNamedView::getDisplayUnit() const +{ + return display_units ? display_units : unit_table.getUnit("px"); +} + +/** + * Set the display unit to the given value. + */ +void SPNamedView::setDisplayUnit(std::string unit) +{ + setDisplayUnit(unit_table.getUnit(unit)); +} + +void SPNamedView::setDisplayUnit(Inkscape::Util::Unit const *unit) +{ + // If this is unset, it will be returned as px by getDisplayUnit + display_units = unit; + getRepr()->setAttributeOrRemoveIfEmpty("inkscape:document-units", + unit ? unit->abbr.c_str() : nullptr); +} + +/** + * Returns the first grid it could find that isEnabled(). Returns NULL, if none is enabled + */ +SPGrid *SPNamedView::getFirstEnabledGrid() +{ + for (auto grid : grids) { + if (grid->isEnabled()) + return grid; + } + + return nullptr; +} + +void SPNamedView::translateGuides(Geom::Translate const &tr) { + for(auto & it : this->guides) { + SPGuide &guide = *it; + Geom::Point point_on_line = guide.getPoint(); + point_on_line *= tr; + guide.moveto(point_on_line, true); + } +} + +void SPNamedView::translateGrids(Geom::Translate const &tr) { + for (auto grid : grids) { + grid->setOrigin( grid->getOrigin() * tr ); + } +} + +void SPNamedView::scrollAllDesktops(double dx, double dy) { + for(auto & view : this->views) { + view->scroll_relative_in_svg_coords(dx, dy); + } +} + +void SPNamedView::change_color(unsigned int rgba, SPAttr color_key, SPAttr opacity_key /*= SPAttr::INVALID*/) { + gchar buf[32]; + sp_svg_write_color(buf, sizeof(buf), rgba); + getRepr()->setAttribute(sp_attribute_name(color_key), buf); + + if (opacity_key != SPAttr::INVALID) { + getRepr()->setAttributeCssDouble(sp_attribute_name(opacity_key), (rgba & 0xff) / 255.0); + } +} + +void SPNamedView::change_bool_setting(SPAttr key, bool value) { + const char* str_value = nullptr; + if (key == SPAttr::SHAPE_RENDERING) { + str_value = value ? "auto" : "crispEdges"; + } else if (key == SPAttr::PAGELABELSTYLE) { + str_value = value ? "below" : "default"; + } else { + str_value = value ? "true" : "false"; + } + getRepr()->setAttribute(sp_attribute_name(key), str_value); +} + +// show/hide guide lines without modifying view; used to quickly and temporarily hide them and restore them +void SPNamedView::temporarily_show_guides(bool show) { + // hide grid and guides + for (auto guide : guides) { + show ? guide->showSPGuide() : guide->hideSPGuide(); + } + + // hide page margin and bleed lines + for (auto page : document->getPageManager().getPages()) { + page->set_guides_visible(show); + } +} + +/* + 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 : diff --git a/src/object/sp-namedview.h b/src/object/sp-namedview.h new file mode 100644 index 0000000..a177a49 --- /dev/null +++ b/src/object/sp-namedview.h @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_SP_NAMEDVIEW_H +#define INKSCAPE_SP_NAMEDVIEW_H + +/* + * <sodipodi:namedview> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) Lauris Kaplinski 2000-2002 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "sp-object-group.h" +#include "snap.h" +#include "document.h" +#include "util/units.h" +#include "svg/svg-bool.h" +#include <vector> + +namespace Inkscape { + class CanvasPage; + namespace Util { + class Unit; + } +} + +class SPGrid; + +typedef unsigned int guint32; +typedef guint32 GQuark; + +enum { + SP_BORDER_LAYER_BOTTOM, + SP_BORDER_LAYER_TOP +}; + +class SPNamedView final : public SPObjectGroup { +public: + SPNamedView(); + ~SPNamedView() override; + int tag() const override { return tag_of<decltype(*this)>; } + + unsigned int editable : 1; + + SVGBool showguides; + SVGBool lockguides; + SVGBool grids_visible; + SVGBool clip_to_page; // if true, clip rendered content to pages' boundaries + guint32 desk_color; + SVGBool desk_checkerboard; + + double zoom; + double rotation; // Document rotation in degrees (positive is clockwise) + double cx; + double cy; + int window_width; + int window_height; + int window_x; + int window_y; + int window_maximized; + + SnapManager snap_manager; + + Inkscape::Util::Unit const *display_units; // Units used for the UI (*not* the same as units of SVG coordinates) + // Inkscape::Util::Unit const *page_size_units; // Only used in "Custom size" part of Document Properties dialog + + GQuark default_layer_id; + + double connector_spacing; + + guint32 guidecolor; + guint32 guidehicolor; + + std::vector<SPGuide *> guides; + std::vector<SPGrid *> grids; + std::vector<SPDesktop *> views; + + int viewcount; + + void show(SPDesktop *desktop); + void hide(SPDesktop const *desktop); + void setDefaultAttribute(std::string attribute, std::string preference, std::string fallback); + void activateGuides(void* desktop, bool active); + char const *getName() const; + std::vector<SPDesktop *> const getViewList() const; + Inkscape::Util::Unit const * getDisplayUnit() const; + void setDisplayUnit(std::string unit); + void setDisplayUnit(Inkscape::Util::Unit const *unit); + + void translateGuides(Geom::Translate const &translation); + void translateGrids(Geom::Translate const &translation); + void scrollAllDesktops(double dx, double dy); + + bool getShowGrids(); + void setShowGrids(bool v); + + void toggleShowGuides(); + void toggleLockGuides(); + void toggleShowGrids(); + + bool getLockGuides(); + void setLockGuides(bool v); + + void setShowGuides(bool v); + bool getShowGuides(); + + void updateViewPort(); + + // page background, border, desk colors + void change_color(unsigned int rgba, SPAttr color_key, SPAttr opacity_key = SPAttr::INVALID); + // show border, border on top, anti-aliasing, ... + void change_bool_setting(SPAttr key, bool value); + // sync desk colors + void set_desk_color(SPDesktop* desktop); + // turn clip to page mode on/off + void set_clip_to_page(SPDesktop* desktop, bool enable); + // immediate show/hide guides request, not recorded in a named view + void temporarily_show_guides(bool show); + + SPGrid *getFirstEnabledGrid(); + +private: + void updateGuides(); + void updateGrids(); + + void setShowGuideSingle(SPGuide *guide); + + friend class SPDocument; + + Inkscape::CanvasPage *_viewport = nullptr; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void modified(unsigned int flags) override; + void update(SPCtx *ctx, unsigned int flags) override; + void set(SPAttr key, char const* value) override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + void order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_repr, + Inkscape::XML::Node *new_repr) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + + +void sp_namedview_window_from_document(SPDesktop *desktop); +void sp_namedview_zoom_and_view_from_document(SPDesktop *desktop); +void sp_namedview_document_from_window(SPDesktop *desktop); +void sp_namedview_update_layers_from_document (SPDesktop *desktop); + +const Inkscape::Util::Unit* sp_parse_document_units(const char* unit); + + +#endif /* !INKSCAPE_SP_NAMEDVIEW_H */ + +/* + 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 : diff --git a/src/object/sp-object-group.cpp b/src/object/sp-object-group.cpp new file mode 100644 index 0000000..0977287 --- /dev/null +++ b/src/object/sp-object-group.cpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Abstract base class for non-item groups + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2003 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object-group.h" +#include "xml/repr.h" +#include "document.h" + +SPObjectGroup::SPObjectGroup() : SPObject() { +} + +SPObjectGroup::~SPObjectGroup() = default; + +void SPObjectGroup::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObject::child_added(child, ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +void SPObjectGroup::remove_child(Inkscape::XML::Node *child) { + SPObject::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +void SPObjectGroup::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) { + SPObject::order_changed(child, old_ref, new_ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +Inkscape::XML::Node *SPObjectGroup::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + if (!repr) { + repr = xml_doc->createElement("svg:g"); + } + + std::vector<Inkscape::XML::Node *> l; + for (auto& child: children) { + 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) { + child.updateRepr(flags); + } + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* + 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 : diff --git a/src/object/sp-object-group.h b/src/object/sp-object-group.h new file mode 100644 index 0000000..d1aef4d --- /dev/null +++ b/src/object/sp-object-group.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_OBJECTGROUP_H +#define SEEN_SP_OBJECTGROUP_H + +/* + * Abstract base class for non-item groups + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 1999-2003 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +class SPObjectGroup : public SPObject { +public: + SPObjectGroup(); + ~SPObjectGroup() override; + int tag() const override { return tag_of<decltype(*this)>; } + +protected: + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void order_changed(Inkscape::XML::Node* child, Inkscape::XML::Node* old, Inkscape::XML::Node* new_repr) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif // SEEN_SP_OBJECTGROUP_H +/* + 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 : diff --git a/src/object/sp-object.cpp b/src/object/sp-object.cpp new file mode 100644 index 0000000..4fa3912 --- /dev/null +++ b/src/object/sp-object.cpp @@ -0,0 +1,1891 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SPObject implementation. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Stephen Silver <sasilver@users.sourceforge.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Adrian Boguszewski + * + * Copyright (C) 1999-2016 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> +#include <vector> +#include <limits> +#include <glibmm.h> + +#include <boost/range/adaptor/transformed.hpp> + +#include "helper/sp-marshal.h" +#include "attributes.h" +#include "attribute-rel-util.h" +#include "color-profile.h" +#include "document.h" +#include "io/fix-broken-links.h" +#include "preferences.h" +#include "style.h" +#include "live_effects/lpeobject.h" +#include "sp-factory.h" +#include "sp-font.h" +#include "sp-paint-server.h" +#include "sp-root.h" +#include "sp-use.h" +#include "sp-use-reference.h" +#include "sp-style-elem.h" +#include "sp-script.h" +#include "streq.h" +#include "strneq.h" +#include "xml/node-fns.h" +#include "xml/href-attribute-helper.h" +#include "debug/event-tracker.h" +#include "debug/simple-event.h" +#include "debug/demangle.h" +#include "svg/css-ostringstream.h" +#include "util/format.h" +#include "util/longest-common-suffix.h" + +#define noSP_OBJECT_DEBUG_CASCADE + +#define noSP_OBJECT_DEBUG + +#ifdef SP_OBJECT_DEBUG +# define debug(f, a...) { g_print("%s(%d) %s:", \ + __FILE__,__LINE__,__FUNCTION__); \ + g_print(f, ## a); \ + g_print("\n"); \ + } +#else +# define debug(f, a...) /* */ +#endif + +// Define to enable indented tracing of SPObject. +//#define OBJECT_TRACE +static unsigned indent_level = 0; + +/** + * A friend class used to set internal members on SPObject so as to not expose settors in SPObject's public API + */ +class SPObjectImpl +{ +public: + +/** + * Null's the id member of an SPObject without attempting to free prior contents. + * + * @param[inout] obj Pointer to the object which's id shall be nulled. + */ + static void setIdNull( SPObject* obj ) { + if (obj) { + obj->id = nullptr; + } + } + +/** + * Sets the id member of an object, freeing any prior content. + * + * @param[inout] obj Pointer to the object which's id shall be set. + * @param[in] id New id + */ + static void setId( SPObject* obj, gchar const* id ) { + if (obj && (id != obj->id) ) { + if (obj->id) { + g_free(obj->id); + obj->id = nullptr; + } + if (id) { + obj->id = g_strdup(id); + } + } + } +}; + +/** + * Constructor, sets all attributes to default values. + */ +SPObject::SPObject() + : cloned{0} + , uflags{0} + , mflags{0} +{ + debug("id=%p, typename=%s", this, g_type_name_from_instance((GTypeInstance *)this)); + + SPObjectImpl::setIdNull(this); + + // FIXME: now we create style for all objects, but per SVG, only the following can have style attribute: + // vg, g, defs, desc, title, symbol, use, image, switch, path, rect, circle, ellipse, line, polyline, + // polygon, text, tspan, tref, textPath, altGlyph, glyphRef, marker, linearGradient, radialGradient, + // stop, pattern, clipPath, mask, filter, feImage, a, font, glyph, missing-glyph, foreignObject + style = new SPStyle(nullptr, this); + context_style = nullptr; +} + +/** + * Destructor, frees the used memory and unreferences a potential successor of the object. + */ +SPObject::~SPObject() +{ + g_free(this->_label); + g_free(this->_default_label); + + if (this->_successor) { + sp_object_unref(this->_successor, nullptr); + this->_successor = nullptr; + } + if (this->_tmpsuccessor) { + sp_object_unref(this->_tmpsuccessor, nullptr); + this->_tmpsuccessor = nullptr; + } + if (parent) { + parent->children.erase(parent->children.iterator_to(*this)); + } + + delete style; + this->document = nullptr; + this->repr = nullptr; +} + +// CPPIFY: make pure virtual +void SPObject::read_content() { + //throw; +} + +void SPObject::update(SPCtx* /*ctx*/, unsigned int /*flags*/) { + //throw; +} + +void SPObject::modified(unsigned int /*flags*/) { +#ifdef OBJECT_TRACE + objectTrace( "SPObject::modified (default) (empty function)" ); + objectTrace( "SPObject::modified (default)", false ); +#endif + //throw; +} + +namespace { + +namespace Debug = Inkscape::Debug; +namespace Util = Inkscape::Util; + +typedef Debug::SimpleEvent<Debug::Event::REFCOUNT> BaseRefCountEvent; + +class RefCountEvent : public BaseRefCountEvent { +public: + RefCountEvent(SPObject *object, int bias, char const *name) + : BaseRefCountEvent(name) + { + _addProperty("object", Util::format("%p", object).pointer()); + _addProperty("class", Debug::demangle(typeid(*object).name())); + _addProperty("new-refcount", Util::format("%d", object->refCount + bias).pointer()); + } +}; + +class RefEvent : public RefCountEvent { +public: + RefEvent(SPObject *object) + : RefCountEvent(object, 1, "sp-object-ref") + {} +}; + +class UnrefEvent : public RefCountEvent { +public: + UnrefEvent(SPObject *object) + : RefCountEvent(object, -1, "sp-object-unref") + {} +}; + +} // namespace + +gchar const* SPObject::getId() const { + return id; +} + +/** + * Accumulate this id and all it's descendants ids + */ +void SPObject::getIds(std::set<std::string> &ret) const { + if (id) { + ret.insert(std::string(id)); + } + for (auto &child : children) { + child.getIds(ret); + } +} + +/** + * Returns the id as a url param, in the form 'url(#{id})' + */ +std::string SPObject::getUrl() const { + if (id) { + return std::string("url(#") + id + ")"; + } + return ""; +} + +Inkscape::XML::Node * SPObject::getRepr() { + return repr; +} + +Inkscape::XML::Node const* SPObject::getRepr() const{ + return repr; +} + + +SPObject *sp_object_ref(SPObject *object, SPObject *owner) +{ + g_return_val_if_fail(object != nullptr, NULL); + + Inkscape::Debug::EventTracker<RefEvent> tracker(object); + + object->refCount++; + + return object; +} + +SPObject *sp_object_unref(SPObject *object, SPObject *owner) +{ + g_return_val_if_fail(object != nullptr, NULL); + + Inkscape::Debug::EventTracker<UnrefEvent> tracker(object); + + object->refCount--; + + if (object->refCount <= 0) { + delete object; + } + + return nullptr; +} + +void SPObject::hrefObject(SPObject* owner) +{ + // if (owner) std::cout << " owner: " << *owner << std::endl; + + // If owner is a clone, do not increase hrefcount, it's already href'ed by original. + if (!owner || !owner->cloned) { + hrefcount++; + _updateTotalHRefCount(1); + } + + if(owner) + hrefList.push_front(owner); +} + +void SPObject::unhrefObject(SPObject* owner) +{ + if (!owner || !owner->cloned) { + g_return_if_fail(hrefcount > 0); + + hrefcount--; + _updateTotalHRefCount(-1); + } + + if(owner) + hrefList.remove(owner); +} + +void SPObject::_updateTotalHRefCount(int increment) { + SPObject *topmost_collectable = nullptr; + for ( SPObject *iter = this ; iter ; iter = iter->parent ) { + iter->_total_hrefcount += increment; + if ( iter->_total_hrefcount < iter->hrefcount ) { + g_critical("HRefs overcounted"); + } + if ( iter->_total_hrefcount == 0 && + iter->_collection_policy != COLLECT_WITH_PARENT ) + { + topmost_collectable = iter; + } + } + if (topmost_collectable) { + topmost_collectable->requestOrphanCollection(); + } +} + +void SPObject::getLinked(std::vector<SPObject *> &objects, bool ignore_clones) const +{ + for (auto linked : hrefList) { + if (auto link = cast<SPUse>(linked)) { + if (ignore_clones && link->ref && link->ref->getObject() == this) { + continue; + } + } + objects.push_back(linked); + } +} + +bool SPObject::isAncestorOf(SPObject const *object) const +{ + g_return_val_if_fail(object != nullptr, false); + object = object->parent; + while (object) { + if ( object == this ) { + return true; + } + object = object->parent; + } + return false; +} + +SPObject const *SPObject::nearestCommonAncestor(SPObject const *object) const { + g_return_val_if_fail(object != nullptr, NULL); + + using Inkscape::Algorithms::nearest_common_ancestor; + return nearest_common_ancestor<SPObject::ConstParentIterator>(this, object, nullptr); +} + +static SPObject const *AncestorSon(SPObject const *obj, SPObject const *ancestor) { + SPObject const *result = nullptr; + if ( obj && ancestor ) { + if (obj->parent == ancestor) { + result = obj; + } else { + result = AncestorSon(obj->parent, ancestor); + } + } + return result; +} + +int sp_object_compare_position(SPObject const *first, SPObject const *second) +{ + int result = 0; + if (first != second) { + SPObject const *ancestor = first->nearestCommonAncestor(second); + // Need a common ancestor to be able to compare + if ( ancestor ) { + // we have an object and its ancestor (should not happen when sorting selection) + if (ancestor == first) { + result = 1; + } else if (ancestor == second) { + result = -1; + } else { + SPObject const *to_first = AncestorSon(first, ancestor); + SPObject const *to_second = AncestorSon(second, ancestor); + + g_assert(to_second->parent == to_first->parent); + + result = sp_repr_compare_position(to_first->getRepr(), to_second->getRepr()); + } + } + } + return result; +} + +bool sp_object_compare_position_bool(SPObject const *first, SPObject const *second){ + return sp_object_compare_position(first,second)<0; +} + +SPObject *SPObject::appendChildRepr(Inkscape::XML::Node *repr) { + if ( !cloned ) { + getRepr()->appendChild(repr); + return document->getObjectByRepr(repr); + } else { + g_critical("Attempt to append repr as child of cloned object"); + return nullptr; + } +} + +void SPObject::setCSS(SPCSSAttr *css, gchar const *attr) +{ + g_assert(this->getRepr() != nullptr); + sp_repr_css_set(this->getRepr(), css, attr); +} + +void SPObject::changeCSS(SPCSSAttr *css, gchar const *attr) +{ + g_assert(this->getRepr() != nullptr); + sp_repr_css_change(this->getRepr(), css, attr); +} + +std::vector<SPObject*> SPObject::childList(bool add_ref, Action) { + std::vector<SPObject*> l; + for (auto& child: children) { + if (add_ref) { + sp_object_ref(&child); + } + l.push_back(&child); + } + return l; +} + +std::vector<SPObject*> SPObject::ancestorList(bool root_to_tip) +{ + std::vector<SPObject *> ancestors; + for (SPObject::ParentIterator iter=parent ; iter ; ++iter) { + ancestors.push_back(iter); + } + if (root_to_tip) { + std::reverse(ancestors.begin(), ancestors.end()); + } + return ancestors; +} + +gchar const *SPObject::label() const { + return _label; +} + +gchar const *SPObject::defaultLabel() const { + if (_label) { + return _label; + } else { + if (!_default_label) { + if (getId()) { + _default_label = g_strdup_printf("#%s", getId()); + } else if (getRepr()) { + _default_label = g_strdup_printf("<%s>", getRepr()->name()); + } else { + _default_label = g_strdup("Default label"); + } + } + return _default_label; + } +} + +void SPObject::setLabel(gchar const *label) +{ + getRepr()->setAttribute("inkscape:label", label); + // Update anything that's watching the object's label + _modified_signal.emit(this, SP_OBJECT_MODIFIED_FLAG); +} + + +void SPObject::requestOrphanCollection() { + g_return_if_fail(document != nullptr); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // do not remove style or script elements (Bug #276244) + if (is<SPStyleElem>(this)) { + // leave it + } else if (is<SPScript>(this)) { + // leave it + } else if (is<SPFont>(this)) { + // leave it + } else if (!prefs->getBool("/options/cleanupswatches/value", false) && is<SPPaintServer>(this) && static_cast<SPPaintServer*>(this)->isSwatch()) { + // leave it + } else if (is<Inkscape::ColorProfile>(this)) { + // leave it + } else if (is<LivePathEffectObject>(this)) { + document->queueForOrphanCollection(this); + } else { + document->queueForOrphanCollection(this); + + /** \todo + * This is a temporary hack added to make fill&stroke rebuild its + * gradient list when the defs are vacuumed. gradient-vector.cpp + * listens to the modified signal on defs, and now we give it that + * signal. Mental says that this should be made automatic by + * merging SPObjectGroup with SPObject; SPObjectGroup would issue + * this signal automatically. Or maybe just derive SPDefs from + * SPObjectGroup? + */ + + this->requestModified(SP_OBJECT_CHILD_MODIFIED_FLAG); + } +} + +void SPObject::_sendDeleteSignalRecursive() { + for (auto& child: children) { + child._delete_signal.emit(&child); + child._sendDeleteSignalRecursive(); + } +} + +void SPObject::deleteObject(bool propagate, bool propagate_descendants) +{ + sp_object_ref(this, nullptr); + if (is<SPLPEItem>(this)) { + cast<SPLPEItem>(this)->removeAllPathEffects(false, propagate_descendants); + } + if (propagate) { + _delete_signal.emit(this); + } + if (propagate_descendants) { + this->_sendDeleteSignalRecursive(); + } + + Inkscape::XML::Node *repr = getRepr(); + if (repr && repr->parent()) { + sp_repr_unparent(repr); + } + + if (_successor) { + _successor->deleteObject(propagate, propagate_descendants); + } + sp_object_unref(this, nullptr); +} + +void SPObject::cropToObject(SPObject *except) +{ + std::vector<SPObject *> toDelete; + for (auto &child : children) { + if (is<SPItem>(&child)) { + if (child.isAncestorOf(except)) { + child.cropToObject(except); + } else if (&child != except) { + sp_object_ref(&child, nullptr); + toDelete.push_back(&child); + } + } + } + for (auto &i : toDelete) { + i->deleteObject(true, true); + sp_object_unref(i, nullptr); + } +} + +/** + * Removes objects which are not related to given list of objects. + * + * Use Case: Group[MyRect1 , MyRect2] , MyRect3 + * List Provided: MyRect1, MyRect3 + * Output doc: Group[MyRect1], MyRect3 + * List Provided: MyRect1, Group + * Output doc: Group[MyRect1, MyRect2] (notice MyRect2 is not deleted as it is related to Group) + */ +void SPObject::cropToObjects(std::vector<SPObject *> except_objects) +{ + if (except_objects.empty()) { + return; + } + std::vector<SPObject *> toDelete; + + // Make sure we have all related objects so we don't delete + // things which will later cause a crash. + getLinkedObjects(except_objects, true); + + // Collect a list of objects we expect to delete. + getObjectsExcept(toDelete, except_objects); + + for (auto &i : toDelete) { + i->deleteObject(true, true); + } +} + +void SPObject::getObjectsExcept(std::vector<SPObject *> &objects, const std::vector<SPObject *> &excepts) +{ + for (auto &child : children) { + if (is<SPItem>(&child)) { + int child_flag = 1; + for (auto except : excepts) { + if (&child == except) { + child_flag = 0; + break; + } + if (child.isAncestorOf(except)) { + child_flag = 2; + } + } + if (child_flag == 1) { + objects.push_back(&child); + } else if (child_flag == 2) { + child.getObjectsExcept(objects, excepts); + } + } + } +} + +void SPObject::getLinkedObjects(std::vector<SPObject *> &objects, bool ignore_clones) const +{ + getLinked(objects, ignore_clones); + for (auto &child : children) { + if (is<SPItem>(&child)) { + child.getLinkedObjects(objects, ignore_clones); + } + } +} + +void SPObject::attach(SPObject *object, SPObject *prev) +{ + g_return_if_fail(object != nullptr); + g_return_if_fail(!prev || prev->parent == this); + g_return_if_fail(!object->parent); + + sp_object_ref(object, this); + object->parent = this; + this->_updateTotalHRefCount(object->_total_hrefcount); + + auto it = children.begin(); + if (prev != nullptr) { + it = ++children.iterator_to(*prev); + } + children.insert(it, *object); + + if (!object->xml_space.set) + object->xml_space.value = this->xml_space.value; +} + +void SPObject::reorder(SPObject* obj, SPObject* prev) { + g_return_if_fail(obj != nullptr); + g_return_if_fail(obj->parent); + g_return_if_fail(obj->parent == this); + g_return_if_fail(obj != prev); + g_return_if_fail(!prev || prev->parent == obj->parent); + + auto it = children.begin(); + if (prev != nullptr) { + it = ++children.iterator_to(*prev); + } + + children.splice(it, children, children.iterator_to(*obj)); +} + +void SPObject::detach(SPObject *object) +{ + g_return_if_fail(object != nullptr); + g_return_if_fail(object->parent == this); + + children.erase(children.iterator_to(*object)); + object->releaseReferences(); + + object->parent = nullptr; + + this->_updateTotalHRefCount(-object->_total_hrefcount); + sp_object_unref(object, this); +} + +SPObject *SPObject::get_child_by_repr(Inkscape::XML::Node *repr) +{ + g_return_val_if_fail(repr != nullptr, NULL); + SPObject *result = nullptr; + + if (children.size() > 0 && children.back().getRepr() == repr) { + result = &children.back(); // optimization for common scenario + } else { + for (auto& child: children) { + if (child.getRepr() == repr) { + result = &child; + break; + } + } + } + return result; +} + +/** + * Get closest child to a reference representation. May traverse backwards + * until it finds a child SPObject node. + * + * @param obj Parent object + * @param ref Reference node, may be NULL + * @return Child, or NULL if not found + */ +static SPObject *get_closest_child_by_repr(SPObject &obj, Inkscape::XML::Node *ref) +{ + for (; ref; ref = ref->prev()) { + // The most likely situation is that `ref` is indeed a child of `obj`, + // so try that first, before checking getObjectByRepr. + if (auto result = obj.get_child_by_repr(ref)) { + return result; + } + + // Only continue if `ref` is not an SPObject, but e.g. an XML comment + if (obj.document->getObjectByRepr(ref)) { + break; + } + } + + return nullptr; +} + +void SPObject::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObject* object = this; + + const std::string type_string = NodeTraits::get_type_string(*child); + + SPObject* ochild = SPFactory::createObject(type_string); + if (ochild == nullptr) { + // Currently, there are many node types that do not have + // corresponding classes in the SPObject tree. + // (rdf:RDF, inkscape:clipboard, ...) + // Thus, simply ignore this case for now. + return; + } + + SPObject *prev = get_closest_child_by_repr(*object, ref); + object->attach(ochild, prev); + sp_object_unref(ochild, nullptr); + + ochild->invoke_build(object->document, child, object->cloned); +} + +void SPObject::release() { + SPObject* object = this; + debug("id=%p, typename=%s", object, g_type_name_from_instance((GTypeInstance*)object)); + + style->filter.clear(); + style->fill.value.href.reset(); + style->stroke.value.href.reset(); + style->shape_inside.clear(); + style->shape_subtract.clear(); + + auto tmp = children | boost::adaptors::transformed([](SPObject& obj){return &obj;}); + std::vector<SPObject *> toRelease(tmp.begin(), tmp.end()); + + for (auto& p: toRelease) { + object->detach(p); + } +} + +void SPObject::remove_child(Inkscape::XML::Node* child) { + debug("id=%p, typename=%s", this, g_type_name_from_instance((GTypeInstance*)this)); + + SPObject *ochild = this->get_child_by_repr(child); + + // If the xml node has got a corresponding child in the object tree + if (ochild) { + this->detach(ochild); + } +} + +void SPObject::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node * /*old_ref*/, Inkscape::XML::Node *new_ref) { + SPObject* object = this; + + SPObject *ochild = object->get_child_by_repr(child); + g_return_if_fail(ochild != nullptr); + SPObject *prev = get_closest_child_by_repr(*object, new_ref); + object->reorder(ochild, prev); + ochild->_position_changed_signal.emit(ochild); +} + +void SPObject::tag_name_changed(gchar const* oldname, gchar const* newname) { + g_warning("XML Element renamed from %s to %s!", oldname, newname); +} + +void SPObject::build(SPDocument *document, Inkscape::XML::Node *repr) { + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::build" ); +#endif + SPObject* object = this; + + /* Nothing specific here */ + debug("id=%p, typename=%s", object, g_type_name_from_instance((GTypeInstance*)object)); + + object->readAttr(SPAttr::XML_SPACE); + object->readAttr(SPAttr::LANG); + object->readAttr(SPAttr::XML_LANG); // "xml:lang" overrides "lang" per spec, read it last. + object->readAttr(SPAttr::INKSCAPE_LABEL); + object->readAttr(SPAttr::INKSCAPE_COLLECT); + + // Inherit if not set + if (lang.empty() && object->parent) { + lang = object->parent->lang; + } + + if(object->cloned && (repr->attribute("id")) ) // The cases where this happens are when the "original" has no id. This happens + // if it is a SPString (a TextNode, e.g. in a <title>), or when importing + // stuff externally modified to have no id. + object->clone_original = document->getObjectById(repr->attribute("id")); + + for (Inkscape::XML::Node *rchild = repr->firstChild() ; rchild != nullptr; rchild = rchild->next()) { + const std::string typeString = NodeTraits::get_type_string(*rchild); + + SPObject* child = SPFactory::createObject(typeString); + if (child == nullptr) { + // Currently, there are many node types that do not have + // corresponding classes in the SPObject tree. + // (rdf:RDF, inkscape:clipboard, ...) + // Thus, simply ignore this case for now. + continue; + } + + object->attach(child, object->lastChild()); + sp_object_unref(child, nullptr); + child->invoke_build(document, rchild, object->cloned); + } + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::build", false ); +#endif +} + +void SPObject::invoke_build(SPDocument *document, Inkscape::XML::Node *repr, unsigned int cloned) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPObject::invoke_build" ); +#endif + debug("id=%p, typename=%s", this, g_type_name_from_instance((GTypeInstance*)this)); + + g_assert(document != nullptr); + g_assert(repr != nullptr); + + g_assert(this->document == nullptr); + g_assert(this->repr == nullptr); + g_assert(this->getId() == nullptr); + + /* Bookkeeping */ + + this->document = document; + this->repr = repr; + if (!cloned) { + Inkscape::GC::anchor(repr); + } + this->cloned = cloned; + + /* Invoke derived methods, if any */ + this->build(document, repr); + + if ( !cloned ) { + this->document->bindObjectToRepr(this->repr, this); + + if (Inkscape::XML::id_permitted(this->repr)) { + /* If we are not cloned, and not seeking, force unique id */ + gchar const *id = this->repr->attribute("id"); + if (!document->isSeeking()) { + auto realid = generate_unique_id(id); + this->document->bindObjectToId(realid.c_str(), this); + SPObjectImpl::setId(this, realid.c_str()); + + /* Redefine ID, if required */ + if (!id || std::strcmp(id, getId()) != 0) { + this->repr->setAttribute("id", getId()); + } + } else if (id) { + // bind if id, but no conflict -- otherwise, we can expect + // a subsequent setting of the id attribute + if (!this->document->getObjectById(id)) { + this->document->bindObjectToId(id, this); + SPObjectImpl::setId(this, id); + } + } + } + } else { + g_assert(this->getId() == nullptr); + } + + this->document->process_pending_resource_changes(); + + /* Signalling (should be connected AFTER processing derived methods */ + repr->addObserver(*this); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::invoke_build", false ); +#endif +} + +int SPObject::getIntAttribute(char const *key, int def) +{ + return getRepr()->getAttributeInt(key, def); +} + +unsigned SPObject::getPosition(){ + g_assert(this->repr); + + return repr->position(); +} + +void SPObject::appendChild(Inkscape::XML::Node *child) { + g_assert(this->repr); + + repr->appendChild(child); +} + +SPObject* SPObject::nthChild(unsigned index) { + g_assert(this->repr); + if (hasChildren()) { + std::vector<SPObject*> l; + unsigned counter = 0; + for (auto& child: children) { + if (counter == index) { + return &child; + } + counter++; + } + } + return nullptr; +} + +void SPObject::addChild(Inkscape::XML::Node *child, Inkscape::XML::Node * prev) +{ + g_assert(this->repr); + + repr->addChild(child,prev); +} + +void SPObject::releaseReferences() { + g_assert(this->document); + g_assert(this->repr); + g_assert(cloned || repr->_anchored_refcount() > 0); + + repr->removeObserver(*this); + + this->_release_signal.emit(this); + + this->release(); + + /* all hrefs should be released by the "release" handlers */ + g_assert(this->hrefcount == 0); + + if (!cloned) { + if (this->id) { + this->document->bindObjectToId(this->id, nullptr); + } + g_free(this->id); + this->id = nullptr; + + g_free(this->_default_label); + this->_default_label = nullptr; + + this->document->bindObjectToRepr(this->repr, nullptr); + + Inkscape::GC::release(this->repr); + } else { + g_assert(!this->id); + } + + this->document = nullptr; + this->repr = nullptr; +} + +SPObject *SPObject::getPrev() +{ + SPObject *prev = nullptr; + if (parent && !parent->children.empty() && &parent->children.front() != this) { + prev = &*(--parent->children.iterator_to(*this)); + } + return prev; +} + +SPObject* SPObject::getNext() +{ + SPObject *next = nullptr; + if (parent && !parent->children.empty() && &parent->children.back() != this) { + next = &*(++parent->children.iterator_to(*this)); + } + return next; +} + +void SPObject::notifyChildAdded(Inkscape::XML::Node &node, Inkscape::XML::Node &child, Inkscape::XML::Node *ref) +{ + child_added(&child, ref); +} + +void SPObject::notifyChildRemoved(Inkscape::XML::Node &, Inkscape::XML::Node &child, Inkscape::XML::Node *) +{ + remove_child(&child); +} + +void SPObject::notifyChildOrderChanged(Inkscape::XML::Node &, Inkscape::XML::Node &child, Inkscape::XML::Node *old_prev, + Inkscape::XML::Node *new_prev) +{ + order_changed(&child, old_prev, new_prev); +} + +void SPObject::notifyElementNameChanged(Inkscape::XML::Node &node, GQuark old_name, GQuark new_name) +{ + auto const oldname = g_quark_to_string(old_name); + auto const newname = g_quark_to_string(new_name); + + tag_name_changed(oldname, newname); +} + +void SPObject::set(SPAttr key, gchar const* value) { + +#ifdef OBJECT_TRACE + std::stringstream temp; + temp << "SPObject::set: " << sp_attribute_name(key) << " " << (value?value:"null"); + objectTrace( temp.str() ); +#endif + + g_assert(key != SPAttr::INVALID); + + SPObject* object = this; + + switch (key) { + + case SPAttr::ID: + + //XML Tree being used here. + if ( !object->cloned && object->getRepr()->type() == Inkscape::XML::NodeType::ELEMENT_NODE ) { + SPDocument *document=object->document; + SPObject *conflict=nullptr; + + gchar const *new_id = value; + + if (new_id) { + conflict = document->getObjectById((char const *)new_id); + } + + if ( conflict && conflict != object ) { + if (!document->isSeeking()) { + sp_object_ref(conflict, nullptr); + // give the conflicting object a new ID + auto new_conflict_id = conflict->generate_unique_id(); + conflict->setAttribute("id", new_conflict_id); + sp_object_unref(conflict, nullptr); + } else { + new_id = nullptr; + } + } + + if (object->getId()) { + document->bindObjectToId(object->getId(), nullptr); + SPObjectImpl::setId(object, nullptr); + } + + if (new_id) { + SPObjectImpl::setId(object, new_id); + document->bindObjectToId(object->getId(), object); + } + + g_free(object->_default_label); + object->_default_label = nullptr; + } + break; + + case SPAttr::INKSCAPE_LABEL: + g_free(object->_label); + if (value) { + object->_label = g_strdup(value); + } else { + object->_label = nullptr; + } + g_free(object->_default_label); + object->_default_label = nullptr; + break; + + case SPAttr::INKSCAPE_COLLECT: + if ( value && !std::strcmp(value, "always") ) { + object->setCollectionPolicy(SPObject::ALWAYS_COLLECT); + } else { + object->setCollectionPolicy(SPObject::COLLECT_WITH_PARENT); + } + break; + + case SPAttr::XML_SPACE: + if (value && !std::strcmp(value, "preserve")) { + object->xml_space.value = SP_XML_SPACE_PRESERVE; + object->xml_space.set = TRUE; + } else if (value && !std::strcmp(value, "default")) { + object->xml_space.value = SP_XML_SPACE_DEFAULT; + object->xml_space.set = TRUE; + } else if (object->parent) { + SPObject *parent; + parent = object->parent; + object->xml_space.value = parent->xml_space.value; + } + object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + break; + + case SPAttr::LANG: + if (value) { + lang = value; + // To do: sanity check + } + break; + + case SPAttr::XML_LANG: + if (value) { + lang = value; + // To do: sanity check + } + break; + + case SPAttr::STYLE: + object->style->readFromObject( object ); + object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + break; + + default: + break; + } +#ifdef OBJECT_TRACE + objectTrace( "SPObject::set", false ); +#endif +} + +void SPObject::setKeyValue(SPAttr key, gchar const *value) +{ + this->set(key, value); +} + +void SPObject::readAttr(SPAttr keyid) +{ + if (keyid == SPAttr::XLINK_HREF) { + auto value = Inkscape::getHrefAttribute(*getRepr()).second; + setKeyValue(keyid, value); + return; + } + + char const *key = sp_attribute_name(keyid); + + assert(key != nullptr); + assert(getRepr() != nullptr); + + char const *value = getRepr()->attribute(key); + + setKeyValue(keyid, value); +} + +void SPObject::readAttr(gchar const *key) +{ + g_assert(key != nullptr); + + //XML Tree being used here. + g_assert(this->getRepr() != nullptr); + + auto keyid = sp_attribute_lookup(key); + if (keyid != SPAttr::INVALID) { + /* Retrieve the 'key' attribute from the object's XML representation */ + gchar const *value = getRepr()->attribute(key); + + setKeyValue(keyid, value); + } +} + +void SPObject::notifyAttributeChanged(Inkscape::XML::Node &, GQuark key_, Util::ptr_shared, Util::ptr_shared) +{ + auto const key = g_quark_to_string(key_); + readAttr(key); + + auto lpeitem = cast<SPLPEItem>(this); + if (lpeitem && lpeitem->document->isSeeking()) { + lpeitem->modified(SP_OBJECT_MODIFIED_FLAG); + } +} + +void SPObject::notifyContentChanged(Inkscape::XML::Node &, Util::ptr_shared, Util::ptr_shared) +{ + read_content(); +} + +/** + * Return string representation of space value. + */ +static gchar const *sp_xml_get_space_string(unsigned int space) +{ + switch (space) { + case SP_XML_SPACE_DEFAULT: + return "default"; + case SP_XML_SPACE_PRESERVE: + return "preserve"; + default: + return nullptr; + } +} + +Inkscape::XML::Node* SPObject::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { +#ifdef OBJECT_TRACE + objectTrace( "SPObject::write" ); +#endif + + if (!repr && (flags & SP_OBJECT_WRITE_BUILD)) { + repr = this->getRepr()->duplicate(doc); + if (!( flags & SP_OBJECT_WRITE_EXT )) { + repr->removeAttribute("inkscape:collect"); + } + } else if (repr) { + repr->setAttribute("id", this->getId()); + + if (this->xml_space.set) { + char const *xml_space; + xml_space = sp_xml_get_space_string(this->xml_space.value); + repr->setAttribute("xml:space", xml_space); + } + + if ( flags & SP_OBJECT_WRITE_EXT && + this->collectionPolicy() == SPObject::ALWAYS_COLLECT ) + { + repr->setAttribute("inkscape:collect", "always"); + } else { + repr->removeAttribute("inkscape:collect"); + } + + if (style) { + // Write if property set by style attribute in this object + Glib::ustring style_prop = style->write(SPStyleSrc::STYLE_PROP); + + // Write style attributes (SPStyleSrc::ATTRIBUTE) back to xml object + bool any_written = false; + auto properties = style->properties(); + for (auto * prop : properties) { + if (prop->shall_write(SP_STYLE_FLAG_IFSET | SP_STYLE_FLAG_IFSRC, SPStyleSrc::ATTRIBUTE)) { + // WARNING: We don't know for sure if the css names are the same as the attribute names + auto val = repr->attribute(prop->name().c_str()); + auto new_val = prop->get_value(); + if (new_val.empty() && !val || new_val != val) { + repr->setAttributeOrRemoveIfEmpty(prop->name(), new_val); + any_written = true; + } + } + } + if(any_written) { + // We need to ask the object to update the style and keep things in sync + // see `case SPAttr::STYLE` above for how the style attr itself does this. + style->readFromObject(this); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + + // Check for valid attributes. This may be time consuming. + // It is useful, though, for debugging Inkscape code. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if( prefs->getBool("/options/svgoutput/check_on_editing") ) { + + unsigned int flags = sp_attribute_clean_get_prefs(); + style_prop = sp_attribute_clean_style(repr, style_prop.c_str(), flags); + } + + repr->setAttributeOrRemoveIfEmpty("style", style_prop); + } else { + /** \todo I'm not sure what to do in this case. Bug #1165868 + * suggests that it can arise, but the submitter doesn't know + * how to do so reliably. The main two options are either + * leave repr's style attribute unchanged, or explicitly clear it. + * Must also consider what to do with property attributes for + * the element; see below. + */ + char const *style_str = repr->attribute("style"); + if (!style_str) { + style_str = "NULL"; + } + g_warning("Item's style is NULL; repr style attribute is %s", style_str); + } + } + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::write", false ); +#endif + return repr; +} + +/** +* Indicates that another object supercedes this one. +* Used by duple and stamp to keep references of LPE +*/ +void +SPObject::setTmpSuccessor(SPObject *tmpsuccessor) { + assert(tmpsuccessor != NULL); + assert(_tmpsuccessor == NULL); + assert(tmpsuccessor->_tmpsuccessor == NULL); + sp_object_ref(tmpsuccessor, nullptr); + _tmpsuccessor = tmpsuccessor; + if (repr) { + char const *linked_fill_id = getAttribute("inkscape:linked-fill"); + if (linked_fill_id && document) { + SPObject *lfill = document->getObjectById(linked_fill_id); + if (lfill && lfill->_tmpsuccessor) { + lfill->_tmpsuccessor->setAttribute("inkscape:linked-fill",lfill->_tmpsuccessor->getId()); + } + } + + if (children.size() == _tmpsuccessor->children.size()) { + for (auto &obj : children) { + auto tmpsuccessorchild = _tmpsuccessor->nthChild(obj.getPosition()); + if (tmpsuccessorchild && !obj._tmpsuccessor) { + obj.setTmpSuccessor(tmpsuccessorchild); + } + } + } + } +} + +/** +* Fix temporary successors in duple stamp. +*/ +void +SPObject::fixTmpSuccessors() { + for (auto &obj : children) { + obj.fixTmpSuccessors(); + } + if (_tmpsuccessor) { + char const *linked_fill_id = getAttribute("inkscape:linked-fill"); + if (linked_fill_id && document) { + SPObject *lfill = document->getObjectById(linked_fill_id); + if (lfill && lfill->_tmpsuccessor) { + _tmpsuccessor->setAttribute("inkscape:linked-fill", lfill->_tmpsuccessor->getId()); + } + } + } +} + +void +SPObject::unsetTmpSuccessor() { + for (auto &object : children) { + object.unsetTmpSuccessor(); + } + if (_tmpsuccessor) { + sp_object_unref(_tmpsuccessor, nullptr); + _tmpsuccessor = nullptr; + } +} + +/** +* Returns ancestor non layer. +*/ +SPObject const * SPObject::getTopAncestorNonLayer() const { + auto group = cast<SPGroup>(parent); + if (group && group->layerMode() != SPGroup::LAYER) { + return group->getTopAncestorNonLayer(); + } else { + return this; + } +}; + + +Inkscape::XML::Node * SPObject::updateRepr(unsigned int flags) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 1" ); +#endif + + if ( !cloned ) { + Inkscape::XML::Node *repr = getRepr(); + if (repr) { +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 1", false ); +#endif + return updateRepr(repr->document(), repr, flags); + } else { + g_critical("Attempt to update non-existent repr"); +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 1", false ); +#endif + return nullptr; + } + } else { + /* cloned objects have no repr */ +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 1", false ); +#endif + return nullptr; + } +} + +Inkscape::XML::Node * SPObject::updateRepr(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned int flags) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 2" ); +#endif + + g_assert(doc != nullptr); + + if (cloned) { + /* cloned objects have no repr */ +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 2", false ); +#endif + return nullptr; + } + + if (!(flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = getRepr(); + } + +#ifdef OBJECT_TRACE + Inkscape::XML::Node *node = write(doc, repr, flags); + objectTrace( "SPObject::updateRepr 2", false ); + return node; +#else + return this->write(doc, repr, flags); +#endif + +} + +/* Modification */ + +void SPObject::requestDisplayUpdate(unsigned int flags) +{ + g_return_if_fail( this->document != nullptr ); + +#ifndef NDEBUG + // expect no nested update calls + if (document->update_in_progress) { + // observed with LPE on <rect> + g_warning("WARNING: Requested update while update in progress, counter = %d", document->update_in_progress); + } +#endif + + /* requestModified must be used only to set one of SP_OBJECT_MODIFIED_FLAG or + * SP_OBJECT_CHILD_MODIFIED_FLAG */ + g_return_if_fail(!(flags & SP_OBJECT_PARENT_MODIFIED_FLAG)); + g_return_if_fail((flags & SP_OBJECT_MODIFIED_FLAG) || (flags & SP_OBJECT_CHILD_MODIFIED_FLAG)); + g_return_if_fail(!((flags & SP_OBJECT_MODIFIED_FLAG) && (flags & SP_OBJECT_CHILD_MODIFIED_FLAG))); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::requestDisplayUpdate" ); +#endif + + bool already_propagated = (!(this->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))); + //https://stackoverflow.com/a/7841333 + if ((this->uflags & flags) != flags ) { + this->uflags |= flags; + } + /* If requestModified has already been called on this object or one of its children, then we + * don't need to set CHILD_MODIFIED on our ancestors because it's already been done. + */ + if (already_propagated) { + if(this->document) { + if (parent) { + parent->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG); + } else { + this->document->requestModified(); + } + } + } + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::requestDisplayUpdate", false ); +#endif + +} + +void SPObject::updateDisplay(SPCtx *ctx, unsigned int flags) +{ + g_return_if_fail(!(flags & ~SP_OBJECT_MODIFIED_CASCADE)); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateDisplay" ); +#endif + + assert(++(document->update_in_progress)); + +#ifdef SP_OBJECT_DEBUG_CASCADE + g_print("Update %s:%s %x %x %x\n", g_type_name_from_instance((GTypeInstance *) this), getId(), flags, this->uflags, this->mflags); +#endif + + /* Get this flags */ + flags |= this->uflags; + /* Copy flags to modified cascade for later processing */ + this->mflags |= this->uflags; + /* We have to clear flags here to allow rescheduling update */ + this->uflags = 0; + + // Merge style if we have good reasons to think that parent style is changed */ + /** \todo + * I am not sure whether we should check only propagated + * flag. We are currently assuming that style parsing is + * done immediately. I think this is correct (Lauris). + */ + if (style) { + style->block_filter_bbox_updates = true; + if ((flags & SP_OBJECT_STYLESHEET_MODIFIED_FLAG)) { + style->readFromObject(this); + } else if (parent && (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) && (flags & SP_OBJECT_PARENT_MODIFIED_FLAG)) { + style->cascade( this->parent->style ); + } + style->block_filter_bbox_updates = false; + } + + try + { + this->update(ctx, flags); + } + catch(...) + { + /** \todo + * in case of catching an exception we need to inform the user somehow that the document is corrupted + * maybe by implementing an document flag documentOk + * or by a modal error dialog + */ + g_warning("SPObject::updateDisplay(SPCtx *ctx, unsigned int flags) : throw in ((SPObjectClass *) G_OBJECT_GET_CLASS(this))->update(this, ctx, flags);"); + } + + assert((document->update_in_progress)--); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateDisplay", false ); +#endif +} + +void SPObject::requestModified(unsigned int flags) +{ + g_return_if_fail( this->document != nullptr ); + + /* requestModified must be used only to set one of SP_OBJECT_MODIFIED_FLAG or + * SP_OBJECT_CHILD_MODIFIED_FLAG */ + g_return_if_fail(!(flags & SP_OBJECT_PARENT_MODIFIED_FLAG)); + g_return_if_fail((flags & SP_OBJECT_MODIFIED_FLAG) || (flags & SP_OBJECT_CHILD_MODIFIED_FLAG)); + g_return_if_fail(!((flags & SP_OBJECT_MODIFIED_FLAG) && (flags & SP_OBJECT_CHILD_MODIFIED_FLAG))); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::requestModified" ); +#endif + + bool already_propagated = (!(this->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))); + + this->mflags |= flags; + + /* If requestModified has already been called on this object or one of its children, then we + * don't need to set CHILD_MODIFIED on our ancestors because it's already been done. + */ + if (already_propagated) { + if (parent) { + parent->requestModified(SP_OBJECT_CHILD_MODIFIED_FLAG); + } else { + document->requestModified(); + } + } +#ifdef OBJECT_TRACE + objectTrace( "SPObject::requestModified", false ); +#endif +} + +void SPObject::emitModified(unsigned int flags) +{ + /* only the MODIFIED_CASCADE flag is legal here */ + g_return_if_fail(!(flags & ~SP_OBJECT_MODIFIED_CASCADE)); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::emitModified", true, flags ); +#endif + +#ifdef SP_OBJECT_DEBUG_CASCADE + g_print("Modified %s:%s %x %x %x\n", g_type_name_from_instance((GTypeInstance *) this), getId(), flags, this->uflags, this->mflags); +#endif + + flags |= this->mflags; + /* We have to clear mflags beforehand, as signal handlers may + * make changes and therefore queue new modification notifications + * themselves. */ + this->mflags = 0; + + sp_object_ref(this); + + this->modified(flags); + + _modified_signal.emit(this, flags); + sp_object_unref(this); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::emitModified", false ); +#endif +} + +gchar const *SPObject::getTagName() const +{ + g_assert(repr != nullptr); + + /// \todo fixme: Exception if object is NULL? */ + //XML Tree being used here. + return getRepr()->name(); +} + +gchar const *SPObject::getAttribute(gchar const *key) const +{ + g_assert(this->repr != nullptr); + + /// \todo fixme: Exception if object is NULL? */ + //XML Tree being used here. + return (gchar const *) getRepr()->attribute(key); +} + +void SPObject::setAttribute(Inkscape::Util::const_char_ptr key, + Inkscape::Util::const_char_ptr value) +{ + g_assert(this->repr != nullptr); + + /// \todo fixme: Exception if object is NULL? */ + //XML Tree being used here. + getRepr()->setAttribute(key, value); +} + +void SPObject::setAttributeDouble(Inkscape::Util::const_char_ptr key, double value) { + Inkscape::CSSOStringStream os; + os << value; + setAttribute(key, os.str()); +} + +void SPObject::removeAttribute(gchar const *key) +{ + /// \todo fixme: Exception if object is NULL? */ + //XML Tree being used here. + getRepr()->removeAttribute(key); +} + +bool SPObject::storeAsDouble( gchar const *key, double *val ) const +{ + g_assert(this->getRepr()!= nullptr); + double nan = std::numeric_limits<double>::quiet_NaN(); + double temp_val = ((Inkscape::XML::Node *)(this->getRepr()))->getAttributeDouble(key, nan); + if (std::isnan(temp_val)) { + return false; + } + *val = temp_val; + return true; +} + +std::string SPObject::generate_unique_id(char const *default_id) const +{ + if (default_id && !document->getObjectById(default_id)) { + return default_id; + } + + //XML Tree being used here. + auto name = repr->name(); + g_assert(name); + + if (auto local = std::strchr(name, ':')) { + name = local + 1; + } + + return document->generate_unique_id(name); +} + +void SPObject::_requireSVGVersion(Inkscape::Version version) { + for ( SPObject::ParentIterator iter=this ; iter ; ++iter ) { + SPObject *object = iter; + if (is<SPRoot>(object)) { + auto root = cast<SPRoot>(object); + if ( root->version.svg < version ) { + root->version.svg = version; + } + } + } +} + +// Titles and descriptions + +/* Note: + Titles and descriptions are stored in 'title' and 'desc' child elements + (see section 5.4 of the SVG 1.0 and 1.1 specifications). The spec allows + an element to have more than one 'title' child element, but strongly + recommends against this and requires using the first one if a choice must + be made. The same applies to 'desc' elements. Therefore, these functions + ignore all but the first 'title' child element and first 'desc' child + element, except when deleting a title or description. + + This will change in SVG 2, where multiple 'title' and 'desc' elements will + be allowed with different localized strings. +*/ + +gchar * SPObject::title() const +{ + return getTitleOrDesc("svg:title"); +} + +bool SPObject::setTitle(gchar const *title, bool verbatim) +{ + return setTitleOrDesc(title, "svg:title", verbatim); +} + +gchar * SPObject::desc() const +{ + return getTitleOrDesc("svg:desc"); +} + +bool SPObject::setDesc(gchar const *desc, bool verbatim) +{ + return setTitleOrDesc(desc, "svg:desc", verbatim); +} + +char * SPObject::getTitleOrDesc(gchar const *svg_tagname) const +{ + char *result = nullptr; + SPObject *elem = findFirstChild(svg_tagname); + if ( elem ) { + //This string copy could be avoided by changing + //the return type of SPObject::getTitleOrDesc + //to std::unique_ptr<Glib::ustring> + result = g_strdup(elem->textualContent().c_str()); + } + return result; +} + +bool SPObject::setTitleOrDesc(gchar const *value, gchar const *svg_tagname, bool verbatim) +{ + if (!verbatim) { + // If the new title/description is just whitespace, + // treat it as though it were NULL. + if (value) { + bool just_whitespace = true; + for (const gchar *cp = value; *cp; ++cp) { + if (!std::strchr("\r\n \t", *cp)) { + just_whitespace = false; + break; + } + } + if (just_whitespace) { + value = nullptr; + } + } + // Don't stomp on mark-up if there is no real change. + if (value) { + gchar *current_value = getTitleOrDesc(svg_tagname); + if (current_value) { + bool different = std::strcmp(current_value, value); + g_free(current_value); + if (!different) { + return false; + } + } + } + } + + SPObject *elem = findFirstChild(svg_tagname); + + if (value == nullptr) { + if (elem == nullptr) { + return false; + } + // delete the title/description(s) + while (elem) { + elem->deleteObject(); + elem = findFirstChild(svg_tagname); + } + return true; + } + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + if (elem == nullptr) { + // create a new 'title' or 'desc' element, putting it at the + // beginning (in accordance with the spec's recommendations) + Inkscape::XML::Node *xml_elem = xml_doc->createElement(svg_tagname); + repr->addChild(xml_elem, nullptr); + elem = document->getObjectByRepr(xml_elem); + Inkscape::GC::release(xml_elem); + } + else { + // remove the current content of the 'text' or 'desc' element + auto tmp = elem->children | boost::adaptors::transformed([](SPObject& obj) { return &obj; }); + std::vector<SPObject*> vec(tmp.begin(), tmp.end()); + for (auto &child: vec) { + child->deleteObject(); + } + } + + // add the new content + elem->appendChildRepr(xml_doc->createTextNode(value)); + return true; +} + +SPObject* SPObject::findFirstChild(gchar const *tagname) const +{ + for (auto& child: const_cast<SPObject*>(this)->children) + { + if (child.repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE && + !std::strcmp(child.repr->name(), tagname)) { + return &child; + } + } + return nullptr; +} + +Glib::ustring SPObject::textualContent() const +{ + Glib::ustring text; + + for (auto& child: children) + { + Inkscape::XML::NodeType child_type = child.repr->type(); + + if (child_type == Inkscape::XML::NodeType::ELEMENT_NODE) { + text += child.textualContent(); + } + else if (child_type == Inkscape::XML::NodeType::TEXT_NODE) { + text += child.repr->content(); + } + } + return text; +} + +Glib::ustring SPObject::getExportFilename() const +{ + if (auto filename = repr->attribute("inkscape:export-filename")) { + return Glib::ustring(filename); + } + return ""; +} + +void SPObject::setExportFilename(Glib::ustring filename) +{ + // Is this svg has been saved before. + const char *doc_filename = document->getDocumentFilename(); + std::string base = Glib::path_get_dirname(doc_filename ? doc_filename : filename); + + filename = Inkscape::convertPathToRelative(filename, base); + repr->setAttributeOrRemoveIfEmpty("inkscape:export-filename", filename.c_str()); +} + +Geom::Point SPObject::getExportDpi() const +{ + return Geom::Point( + repr->getAttributeDouble("inkscape:export-xdpi", 0.0), + repr->getAttributeDouble("inkscape:export-ydpi", 0.0)); +} + +void SPObject::setExportDpi(Geom::Point dpi) +{ + if (!dpi.x() || !dpi.y()) { + repr->removeAttribute("inkscape:export-xdpi"); + repr->removeAttribute("inkscape:export-ydpi"); + } else { + repr->setAttributeSvgDouble("inkscape:export-xdpi", dpi.x()); + repr->setAttributeSvgDouble("inkscape:export-ydpi", dpi.y()); + } +} + +// For debugging: Print SP tree structure. +void SPObject::recursivePrintTree( unsigned level ) +{ + if (level == 0) { + std::cout << "SP Object Tree" << std::endl; + } + std::cout << "SP: "; + for (unsigned i = 0; i < level; ++i) { + std::cout << " "; + } + std::cout << (getId()?getId():"No object id") + << " clone: " << std::boolalpha << (bool)cloned + << " hrefcount: " << hrefcount << std::endl; + for (auto& child: children) { + child.recursivePrintTree(level + 1); + } +} + +// Function to allow tracing of program flow through SPObject and derived classes. +// To trace function, add at entrance ('in' = true) and exit of function ('in' = false). +void SPObject::objectTrace( std::string const &text, bool in, unsigned flags ) { + if( in ) { + for (unsigned i = 0; i < indent_level; ++i) { + std::cout << " "; + } + std::cout << text << ":" + << " entrance: " + << (id?id:"null") + // << " uflags: " << uflags + // << " mflags: " << mflags + // << " flags: " << flags + << std::endl; + ++indent_level; + } else { + --indent_level; + for (unsigned i = 0; i < indent_level; ++i) { + std::cout << " "; + } + std::cout << text << ":" + << " exit: " + << (id?id:"null") + // << " uflags: " << uflags + // << " mflags: " << mflags + // << " flags: " << flags + << std::endl; + } +} + +std::ostream &operator<<(std::ostream &out, const SPObject &o) +{ + out << (o.getId()?o.getId():"No ID") + << " cloned: " << std::boolalpha << (bool)o.cloned + << " ref: " << o.refCount + << " href: " << o.hrefcount + << " total href: " << o._total_hrefcount; + return out; +} +/* + 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 : diff --git a/src/object/sp-object.h b/src/object/sp-object.h new file mode 100644 index 0000000..e936dd9 --- /dev/null +++ b/src/object/sp-object.h @@ -0,0 +1,916 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_OBJECT_H_SEEN +#define SP_OBJECT_H_SEEN + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Adrian Boguszewski + * + * Copyright (C) 1999-2016 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <set> +#include <glibmm/ustring.h> +#include "util/const_char_ptr.h" +#include "xml/node-observer.h" +/* SPObject flags */ + +class SPObject; + +/* Async modification flags */ +#define SP_OBJECT_MODIFIED_FLAG (1 << 0) +#define SP_OBJECT_CHILD_MODIFIED_FLAG (1 << 1) +#define SP_OBJECT_PARENT_MODIFIED_FLAG (1 << 2) +#define SP_OBJECT_STYLE_MODIFIED_FLAG (1 << 3) +#define SP_OBJECT_VIEWPORT_MODIFIED_FLAG (1 << 4) +#define SP_OBJECT_USER_MODIFIED_FLAG_A (1 << 5) +#define SP_OBJECT_USER_MODIFIED_FLAG_B (1 << 6) +#define SP_OBJECT_STYLESHEET_MODIFIED_FLAG (1 << 7) + +/* Convenience */ +#define SP_OBJECT_FLAGS_ALL 0xff + +/* Flags that mark object as modified */ +/* Object, Child, Style, Viewport, User */ +#define SP_OBJECT_MODIFIED_STATE (SP_OBJECT_FLAGS_ALL & ~(SP_OBJECT_PARENT_MODIFIED_FLAG)) + +/* Flags that will propagate downstreams */ +/* Parent, Style, Viewport, User */ +#define SP_OBJECT_MODIFIED_CASCADE (SP_OBJECT_FLAGS_ALL & ~(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG)) +inline unsigned cascade_flags(unsigned flags) +{ + // Unset object-modified and child-modified, set parent-modified if object-modified. + static_assert(SP_OBJECT_PARENT_MODIFIED_FLAG == SP_OBJECT_MODIFIED_FLAG << 2); + return (flags & SP_OBJECT_MODIFIED_CASCADE) | (flags & SP_OBJECT_MODIFIED_FLAG) << 2; +} + +/* Write flags */ +#define SP_OBJECT_WRITE_BUILD (1 << 0) +#define SP_OBJECT_WRITE_EXT (1 << 1) +#define SP_OBJECT_WRITE_ALL (1 << 2) +#define SP_OBJECT_WRITE_NO_CHILDREN (1 << 3) + +#include <vector> +#include <cassert> +#include <cstddef> +#include <boost/intrusive/list.hpp> +#include <2geom/point.h> // Used for dpi only +#include <sigc++/connection.h> +#include <sigc++/functors/slot.h> +#include <sigc++/signal.h> +#include "util/forward-pointer-iterator.h" +#include "tags.h" +#include "version.h" + +enum class SPAttr; + +class SPCSSAttr; +class SPStyle; + +namespace Inkscape::XML { class Node; struct Document; } + +/// Unused +struct SPCtx +{ + unsigned int flags; +}; + +enum +{ + SP_XML_SPACE_DEFAULT, + SP_XML_SPACE_PRESERVE +}; + +class SPDocument; + +/// Internal class consisting of two bits. +class SPIXmlSpace { +public: + SPIXmlSpace(): set(0), value(SP_XML_SPACE_DEFAULT) {}; + unsigned int set : 1; + unsigned int value : 1; +}; + +/* + * Refcounting + * + * Owner is here for debug reasons, you can set it to NULL safely + * Ref should return object, NULL is error, unref return always NULL + */ + +/** + * Increase reference count of object, with possible debugging. + * + * @param owner If non-NULL, make debug log entry. + * @return object, NULL is error. + * \pre object points to real object + * @todo need to move this to be a member of SPObject. + */ +SPObject *sp_object_ref(SPObject *object, SPObject *owner=nullptr); + +/** + * Decrease reference count of object, with possible debugging and + * finalization. + * + * @param owner If non-NULL, make debug log entry. + * @return always NULL + * \pre object points to real object + * @todo need to move this to be a member of SPObject. + */ +SPObject *sp_object_unref(SPObject *object, SPObject *owner=nullptr); + +/** + * SPObject is an abstract base class of all of the document nodes at the + * SVG document level. Each SPObject subclass implements a certain SVG + * element node type, or is an abstract base class for different node + * types. The SPObject layer is bound to the SPRepr layer, closely + * following the SPRepr mutations via callbacks. During creation, + * SPObject parses and interprets all textual attributes and CSS style + * strings of the SPRepr, and later updates the internal state whenever + * it receives a signal about a change. The opposite is not true - there + * are methods manipulating SPObjects directly and such changes do not + * propagate to the SPRepr layer. This is important for implementation of + * the undo stack, animations and other features. + * + * SPObjects are bound to the higher-level container SPDocument, which + * provides document level functionality such as the undo stack, + * dictionary and so on. Source: doc/architecture.txt + */ +class SPObject : private Inkscape::XML::NodeObserver +{ +public: + enum CollectionPolicy + { + COLLECT_WITH_PARENT, + ALWAYS_COLLECT + }; + + SPObject(); + SPObject(SPObject const &) = delete; + SPObject &operator=(SPObject const &) = delete; + ~SPObject() override; + virtual int tag() const { return tag_of<decltype(*this)>; } + + unsigned int cloned : 1; + SPObject *clone_original{nullptr}; + unsigned int uflags : 8; + unsigned int mflags : 8; + SPIXmlSpace xml_space; + Glib::ustring lang; + unsigned int hrefcount{0}; /* number of xlink:href references */ + unsigned int _total_hrefcount{0}; /* our hrefcount + total descendants */ + SPDocument *document{nullptr}; /* Document we are part of */ + SPObject *parent{nullptr}; /* Our parent (only one allowed) */ + +private: + char *id{nullptr}; /* Our very own unique id */ + Inkscape::XML::Node *repr{nullptr}; /* Our xml representation */ + +public: + int refCount{1}; + std::list<SPObject *> hrefList; + + /** + * Returns the objects current ID string. + */ + char const* getId() const; + + void getIds(std::set<std::string> &ret) const; + + /** + * Get the id in a URL format. + */ + std::string getUrl() const; + + /** + * Returns the XML representation of tree + */ +//protected: + Inkscape::XML::Node * getRepr(); + + /** + * Returns the XML representation of tree + */ + Inkscape::XML::Node const* getRepr() const; + +public: + + /** + * Cleans up an SPObject, releasing its references and + * requesting that references to it be released + */ + void releaseReferences(); + + /** + * Connects to the release request signal + * + * @param slot the slot to connect + * + * @return the sigc::connection formed + */ + sigc::connection connectRelease(sigc::slot<void (SPObject *)> slot) { + return _release_signal.connect(slot); + } + + /** + * Represents the style properties, whether from presentation attributes, the <tt>style</tt> + * attribute, or inherited. + * + * private_set() doesn't handle SPAttr::STYLE or any presentation attributes at the + * time of writing, so this is probably NULL for all SPObject's that aren't an SPItem. + * + * However, this gives rise to the bugs mentioned in sp_object_get_style_property. + * Note that some non-SPItem SPObject's, such as SPStop, do need styling information, + * and need to inherit properties even through other non-SPItem parents like \<defs\>. + */ + SPStyle *style; + + /** + * Represents the style that should be used to resolve 'context-fill' and 'context-stroke' + */ + SPStyle *context_style; + + /// Switch containing next() method. + struct ParentIteratorStrategy { + static SPObject const *next(SPObject const *object) { + return object->parent; + } + }; + + typedef Inkscape::Util::ForwardPointerIterator<SPObject, ParentIteratorStrategy> ParentIterator; + typedef Inkscape::Util::ForwardPointerIterator<SPObject const, ParentIteratorStrategy> ConstParentIterator; + + bool isSiblingOf(SPObject const *object) const { + if (object == nullptr) return false; + return this->parent && this->parent == object->parent; + } + + /** + * True if object is linked to us + */ + virtual void getLinked(std::vector<SPObject *> &objects, bool ignore_clones) const; + + /** + * True if object is non-NULL and this is some in/direct parent of object. + */ + bool isAncestorOf(SPObject const *object) const; + + /** + * Returns youngest object being parent to this and object. + */ + SPObject const *nearestCommonAncestor(SPObject const *object) const; + + /** + * Returns ancestor non layer. + */ + SPObject const * getTopAncestorNonLayer() const; + + /* Returns next object in sibling list or NULL. */ + SPObject *getNext(); + + /** + * Returns previous object in sibling list or NULL. + */ + SPObject *getPrev(); + + bool hasChildren() const { return ( children.size() > 0 ); } + + SPObject *firstChild() { return children.empty() ? nullptr : &children.front(); } + SPObject const *firstChild() const { return children.empty() ? nullptr : &children.front(); } + + SPObject *lastChild() { return children.empty() ? nullptr : &children.back(); } + SPObject const *lastChild() const { return children.empty() ? nullptr : &children.back(); } + + SPObject *nthChild(unsigned index); + SPObject const *nthChild(unsigned index) const; + + enum Action { ActionGeneral, ActionBBox, ActionUpdate, ActionShow }; + + /** + * Retrieves the children as a std vector object, optionally ref'ing the children + * in the process, if add_ref is specified. + */ + std::vector<SPObject*> childList(bool add_ref, Action action = ActionGeneral); + + + /** + * Retrieves a list of ancestors of the object, as an easy to use vector + * @param root_to_tip - If set, orders the list from the svg root to the tip. + */ + std::vector<SPObject*> ancestorList(bool root_to_tip); + + /** + * Append repr as child of this object. + * \pre this is not a cloned object + */ + SPObject *appendChildRepr(Inkscape::XML::Node *repr); + + /** + * Gets the author-visible label property for the object or a default if + * no label is defined. + */ + char const *label() const; + + /** + * Returns a default label property for this object. + */ + char const *defaultLabel() const; + + /** + * Sets the author-visible label for this object. + * + * @param label the new label. + */ + void setLabel(char const *label); + + /** + * Returns the title of this object, or NULL if there is none. + * The caller must free the returned string using g_free() - see comment + * for getTitleOrDesc() below. + */ + char *title() const; + + /** + * Sets the title of this object. + * A NULL first argument is interpreted as meaning that the existing title + * (if any) should be deleted. + * The second argument is optional - @see setTitleOrDesc() below for details. + */ + bool setTitle(char const *title, bool verbatim = false); + + /** + * Returns the description of this object, or NULL if there is none. + * The caller must free the returned string using g_free() - see comment + * for getTitleOrDesc() below. + */ + char *desc() const; + + /** + * Sets the description of this object. + * A NULL first argument is interpreted as meaning that the existing + * description (if any) should be deleted. + * The second argument is optional - @see setTitleOrDesc() below for details. + */ + bool setDesc(char const *desc, bool verbatim=false); + + /** + * Get and set the exportable filename on this object. Usually sp-item or sp-page + */ + Glib::ustring getExportFilename() const; + void setExportFilename(Glib::ustring filename); + + /** + * Get and set the exported DPI for this objet, if available. + */ + Geom::Point getExportDpi() const; + void setExportDpi(Geom::Point dpi); + + /** + * Set the policy under which this object will be orphan-collected. + * + * Orphan-collection is the process of deleting all objects which no longer have + * hyper-references pointing to them. The policy determines when this happens. Many objects + * should not be deleted simply because they are no longer referred to; other objects (like + * "intermediate" gradients) are more or less throw-away and should always be collected when no + * longer in use. + * + * Along these lines, there are currently two orphan-collection policies: + * + * COLLECT_WITH_PARENT - don't worry about the object's hrefcount; + * if its parent is collected, this object + * will be too + * + * COLLECT_ALWAYS - always collect the object as soon as its + * hrefcount reaches zero + * + * @return the current collection policy in effect for this object + */ + CollectionPolicy collectionPolicy() const { return _collection_policy; } + + /** + * Sets the orphan-collection policy in effect for this object. + * + * @param policy the new policy to adopt + * + * @see SPObject::collectionPolicy + */ + void setCollectionPolicy(CollectionPolicy policy) { + _collection_policy = policy; + } + + /** + * Requests a later automatic call to collectOrphan(). + * + * This method requests that collectOrphan() be called during the document update cycle, + * deleting the object if it is no longer used. + * + * If the current collection policy is COLLECT_WITH_PARENT, this function has no effect. + * + * @see SPObject::collectOrphan + */ + void requestOrphanCollection(); + + /** + * Unconditionally delete the object if it is not referenced. + * + * Unconditionally delete the object if there are no outstanding hyper-references to it. + * Observers are not notified of the object's deletion (at the SPObject level; XML tree + * notifications still fire). + * + * @see SPObject::deleteObject + */ + void collectOrphan() { + if ( _total_hrefcount == 0 ) { + deleteObject(false); + } + } + + /** + * Increase weak refcount. + * + * Hrefcount is used for weak references, for example, to + * determine whether any graphical element references a certain gradient + * node. + * It keeps a list of "owners". + * @param owner Used to track who uses this object. + */ + void hrefObject(SPObject* owner = nullptr); + + /** + * Decrease weak refcount. + * + * Hrefcount is used for weak references, for example, to determine whether + * any graphical element references a certain gradient node. + * @param owner Used to track who uses this object. + * \pre hrefcount>0 + */ + void unhrefObject(SPObject* owner = nullptr); + + /** + * Check if object is referenced by any other object. + */ + bool isReferenced() { return ( _total_hrefcount > 0 ); } + + /** + * Deletes an object, unparenting it from its parent. + * + * Detaches the object's repr, and optionally sends notification that the object has been + * deleted. + * + * @param propagate If it is set to true, it emits a delete signal. + * + * @param propagate_descendants If it is true, it recursively sends the delete signal to children. + */ + void deleteObject(bool propagate, bool propagate_descendants); + + /** + * Deletes on object. + * + * @param propagate Notify observers of this object and its children that they have been + * deleted? + */ + void deleteObject(bool propagate = true) + { + deleteObject(propagate, propagate); + } + + /** + * Removes all children except for the given object, it's children and it's ancesstors. + */ + void cropToObject(SPObject *except); + void cropToObjects(std::vector<SPObject *> except_objects); + + /** + * Get all child objects except for any in the list. + */ + void getObjectsExcept(std::vector<SPObject *> &objects, const std::vector<SPObject *> &except); + + /** + * Grows the input list with any and all linked items. + * + * @param ignore_clones - Links between objects and their child clones are not counted + */ + void getLinkedObjects(std::vector<SPObject *> &objects, bool ignore_clones) const; + + /** + * Connects a slot to be called when an object is deleted. + * + * This connects a slot to an object's internal delete signal, which is invoked when the object + * is deleted + * + * The signal is mainly useful for e.g. knowing when to break hrefs or dissociate clones. + * + * @param slot the slot to connect + * + * @see SPObject::deleteObject + */ + sigc::connection connectDelete(sigc::slot<void (SPObject *)> slot) { + return _delete_signal.connect(slot); + } + + sigc::connection connectPositionChanged(sigc::slot<void (SPObject *)> slot) { + return _position_changed_signal.connect(slot); + } + + /** + * Returns the object which supercedes this one (if any). + * + * This is mainly useful for ensuring we can correctly perform a series of moves or deletes, + * even if the objects in question have been replaced in the middle of the sequence. + */ + SPObject *successor() { return _successor; } + + /** + * Indicates that another object supercedes this one. + */ + void setSuccessor(SPObject *successor) { + assert(successor != NULL); + assert(_successor == NULL); + assert(successor->_successor == NULL); + sp_object_ref(successor, nullptr); + _successor = successor; + } + + /** + * Indicates that another object supercedes temporaty this one. + */ + void setTmpSuccessor(SPObject *tmpsuccessor); + + /** + * Unset object supercedes. + */ + void unsetTmpSuccessor(); + + /** + * Fix temporary successors in duple stamp. + */ + void fixTmpSuccessors(); + + /* modifications; all three sets of methods should probably ultimately be protected, as they + * are not really part of its public interface. However, other parts of the code to + * occasionally use them at present. */ + + /* the no-argument version of updateRepr() is intended to be a bit more public, however -- it + * essentially just flushes any changes back to the backing store (the repr layer); maybe it + * should be called something else and made public at that point. */ + + /** + * Updates the object's repr based on the object's state. + * + * This method updates the repr attached to the object to reflect the object's current + * state; see the three-argument version for details. + * + * @param flags object write flags that apply to this update + * + * @return the updated repr + */ + Inkscape::XML::Node *updateRepr(unsigned int flags = SP_OBJECT_WRITE_EXT); + + /** + * Updates the given repr based on the object's state. + * + * Used both to create reprs in the original document, and to create reprs + * in another document (e.g. a temporary document used when saving as "Plain SVG". + * + * This method updates the given repr to reflect the object's current state. There are + * several flags that affect this: + * + * SP_OBJECT_WRITE_BUILD - create new reprs + * + * SP_OBJECT_WRITE_EXT - write elements and attributes + * which are not part of pure SVG + * (i.e. the Inkscape and Sodipodi + * namespaces) + * + * SP_OBJECT_WRITE_ALL - create all nodes and attributes, + * even those which might be redundant + * + * @param repr the repr to update + * @param flags object write flags that apply to this update + * + * @return the updated repr + */ + Inkscape::XML::Node *updateRepr(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned int flags); + + /** + * Queues an deferred update of this object's display. + * + * This method sets flags to indicate updates to be performed later, during the idle loop. + * + * There are several flags permitted here: + * + * SP_OBJECT_MODIFIED_FLAG - the object has been modified + * + * SP_OBJECT_CHILD_MODIFIED_FLAG - a child of the object has been + * modified + * + * SP_OBJECT_STYLE_MODIFIED_FLAG - the object's style has been + * modified + * + * There are also some subclass-specific modified flags which are hardly ever used. + * + * One of either MODIFIED or CHILD_MODIFIED is required. + * + * @param flags flags indicating what to update + */ + void requestDisplayUpdate(unsigned int flags); + + /** + * Updates the object's display immediately + * + * This method is called during the idle loop by SPDocument in order to update the object's + * display. + * + * One additional flag is legal here: + * + * SP_OBJECT_PARENT_MODIFIED_FLAG - the parent has been + * modified + * + * @param ctx an SPCtx which accumulates various state + * during the recursive update -- beware! some + * subclasses try to cast this to an SPItemCtx * + * + * @param flags flags indicating what to update (in addition + * to any already set flags) + */ + void updateDisplay(SPCtx *ctx, unsigned int flags); + + /** + * Requests that a modification notification signal + * be emitted later (e.g. during the idle loop) + * + * Request modified always bubbles *up* the tree, as opposed to + * request display update, which trickles down and relies on the + * flags set during this pass... + * + * @param flags flags indicating what has been modified + */ + void requestModified(unsigned int flags); + + /** + * Emits the MODIFIED signal with the object's flags. + * The object's mflags are the original set aside during the update pass for + * later delivery here. Once emitModified() is called, those flags don't + * need to be stored any longer. + * + * @param flags indicating what has been modified. + */ + void emitModified(unsigned int flags); + + /** + * Connects to the modification notification signal + * + * @param slot the slot to connect + * + * @return the connection formed thereby + */ + sigc::connection connectModified( + sigc::slot<void (SPObject *, unsigned int)> slot + ) { + return _modified_signal.connect(slot); + } + + /** Sends the delete signal to all children of this object recursively */ + void _sendDeleteSignalRecursive(); + + /** + * Adds increment to _total_hrefcount of object and its parents. + */ + void _updateTotalHRefCount(int increment); + + void _requireSVGVersion(unsigned major, unsigned minor) { _requireSVGVersion(Inkscape::Version(major, minor)); } + + /** + * Lifts SVG version of all root objects to version. + */ + void _requireSVGVersion(Inkscape::Version version); + + sigc::signal<void (SPObject *)> _release_signal; + sigc::signal<void (SPObject *)> _delete_signal; + sigc::signal<void (SPObject *)> _position_changed_signal; + sigc::signal<void (SPObject *, unsigned int)> _modified_signal; + SPObject *_successor{nullptr}; + SPObject *_tmpsuccessor{nullptr}; + CollectionPolicy _collection_policy{SPObject::COLLECT_WITH_PARENT}; + char *_label{nullptr}; + mutable char *_default_label{nullptr}; + + // WARNING: + // Methods below should not be used outside of the SP tree, + // as they operate directly on the XML representation. + // In future, they will be made protected. + + /** + * Put object into object tree, under parent, and behind prev; + * also update object's XML space. + */ + void attach(SPObject *object, SPObject *prev); + + /** + * In list of object's children, move object behind prev. + */ + void reorder(SPObject* obj, SPObject *prev); + + /** + * Remove object from parent's children, release and unref it. + */ + void detach(SPObject *object); + + /** + * Return object's child whose node pointer equals repr. + */ + SPObject *get_child_by_repr(Inkscape::XML::Node *repr); + + void invoke_build(SPDocument *document, Inkscape::XML::Node *repr, unsigned int cloned); + + int getIntAttribute(char const *key, int def); + + unsigned getPosition(); + + char const * getAttribute(char const *name) const; + + void appendChild(Inkscape::XML::Node *child); + + void addChild(Inkscape::XML::Node *child,Inkscape::XML::Node *prev=nullptr); + + /** + * Call virtual set() function of object. + */ + void setKeyValue(SPAttr key, char const *value); + + + void setAttribute(Inkscape::Util::const_char_ptr key, + Inkscape::Util::const_char_ptr value); + + void setAttributeDouble(Inkscape::Util::const_char_ptr key, double value); + + void setAttributeOrRemoveIfEmpty(Inkscape::Util::const_char_ptr key, + Inkscape::Util::const_char_ptr value) { + this->setAttribute(key.data(), + (value.data() == nullptr || value.data()[0]=='\0') ? nullptr : value.data()); + } + + /** + * Read value of key attribute from XML node into object. + */ + void readAttr(char const *key); + void readAttr(SPAttr keyid); + + char const *getTagName() const; + + void removeAttribute(char const *key); + + void setCSS(SPCSSAttr *css, char const *attr); + + void changeCSS(SPCSSAttr *css, char const *attr); + + bool storeAsDouble( char const *key, double *val ) const; + +private: + // Private member functions used in the definitions of setTitle(), + // setDesc(), title() and desc(). + + /** + * Sets or deletes the title or description of this object. + * A NULL 'value' argument causes the title or description to be deleted. + * + * 'verbatim' parameter: + * If verbatim==true, then the title or description is set to exactly the + * specified value. If verbatim==false then two exceptions are made: + * (1) If the specified value is just whitespace, then the title/description + * is deleted. + * (2) If the specified value is the same as the current value except for + * mark-up, then the current value is left unchanged. + * This is usually the desired behaviour, so 'verbatim' defaults to false for + * setTitle() and setDesc(). + * + * The return value is true if a change was made to the title/description, + * and usually false otherwise. + */ + bool setTitleOrDesc(char const *value, char const *svg_tagname, bool verbatim); + + /** + * Returns the title or description of this object, or NULL if there is none. + * + * The SVG spec allows 'title' and 'desc' elements to contain text marked up + * using elements from other namespaces. Therefore, this function cannot + * in general just return a pointer to an existing string - it must instead + * construct a string containing the title or description without the mark-up. + * Consequently, the return value is a newly allocated string (or NULL), and + * must be freed (using g_free()) by the caller. + */ + char * getTitleOrDesc(char const *svg_tagname) const; + + /** + * Find the first child of this object with a given tag name, + * and return it. Returns NULL if there is no matching child. + */ + SPObject * findFirstChild(char const *tagname) const; + + /** + * Return the full textual content of an element (typically all the + * content except the tags). + * Must not be used on anything except elements. + */ + Glib::ustring textualContent() const; + + /* Real handlers of repr signals */ + +private: + // XML::NodeObserver functions + void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark key, Inkscape::Util::ptr_shared oldval, + Inkscape::Util::ptr_shared newval) final; + + void notifyContentChanged(Inkscape::XML::Node &node, Inkscape::Util::ptr_shared oldcontent, + Inkscape::Util::ptr_shared newcontent) final; + + void notifyChildAdded(Inkscape::XML::Node &node, Inkscape::XML::Node &child, + Inkscape::XML::Node *prev) final; + + void notifyChildRemoved(Inkscape::XML::Node &node, Inkscape::XML::Node &child, + Inkscape::XML::Node *prev) final; + + void notifyChildOrderChanged(Inkscape::XML::Node &node, Inkscape::XML::Node &child, Inkscape::XML::Node *old_prev, + Inkscape::XML::Node *new_prev) final; + + void notifyElementNameChanged(Inkscape::XML::Node &node, GQuark old_name, GQuark new_name) final; + + friend class SPObjectImpl; + +protected: + virtual void build(SPDocument *doc, Inkscape::XML::Node *repr); + virtual void release(); + + virtual void child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref); + virtual void remove_child(Inkscape::XML::Node *child); + + virtual void order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_repr, + Inkscape::XML::Node *new_repr); + virtual void tag_name_changed(gchar const *oldname, gchar const *newname); + + virtual void set(SPAttr key, const char *value); + + virtual void update(SPCtx *ctx, unsigned int flags); + virtual void modified(unsigned int flags); + + virtual Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned int flags); + + typedef boost::intrusive::list_member_hook<> ListHook; + ListHook _child_hook; + +public: + using ChildrenList = boost::intrusive::list< + SPObject, + boost::intrusive::member_hook< + SPObject, + ListHook, + &SPObject::_child_hook + >>; + ChildrenList children; + virtual void read_content(); + + void recursivePrintTree(unsigned level = 0); // For debugging + void objectTrace(std::string const &, bool in = true, unsigned flags = 0); + + /** + * @brief Generate a document-wide unique id for this object. + * + * Returns an id string not in use by any object within the object's document. + * If default_id is specified, it will be returned if possible. + * Otherwise, an id will be generated based on the object's name. + */ + std::string generate_unique_id(char const *default_id = nullptr) const; +}; + +std::ostream &operator<<(std::ostream &out, const SPObject &o); + +/** + * Compares height of objects in tree. + * + * Works for different-parent objects, so long as they have a common ancestor. + * \return \verbatim + * 0 positions are equivalent + * 1 first object's position is greater than the second + * -1 first object's position is less than the second \endverbatim + */ +int sp_object_compare_position(SPObject const *first, SPObject const *second); +bool sp_object_compare_position_bool(SPObject const *first, SPObject const *second); + +#endif // SP_OBJECT_H_SEEN + +/* + 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 : diff --git a/src/object/sp-offset.cpp b/src/object/sp-offset.cpp new file mode 100644 index 0000000..8ef1c76 --- /dev/null +++ b/src/object/sp-offset.cpp @@ -0,0 +1,1173 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Implementation of <path sodipodi:type="inkscape:offset">. + */ + +/* + * Authors: (of the sp-spiral.c upon which this file was constructed): + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-offset.h" + +#include <cstring> +#include <string> + +#include <glibmm/i18n.h> + +#include "bad-uri-exception.h" +#include "svg/svg.h" +#include "attributes.h" +#include "display/curve.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "enums.h" +#include "preferences.h" +#include "sp-text.h" +#include "sp-use-reference.h" +#include "uri.h" + +class SPDocument; + +#define noOFFSET_VERBOSE + +/** \note + * SPOffset is a derivative of SPShape, much like the SPSpiral or SPRect. + * The goal is to have a source shape (= originalPath), an offset (= radius) + * and compute the offset of the source by the radius. To get it to work, + * one needs to know what the source is and what the radius is, and how it's + * stored in the xml representation. The object itself is a "path" element, + * to get lots of shape functionality for free. The source is the easy part: + * it's stored in a "inkscape:original" attribute in the path. In case of + * "linked" offset, as they've been dubbed, there is an additional + * "inkscape:href" that contains the id of an element of the svg. + * When built, the object will attach a listener vector to that object and + * rebuild the "inkscape:original" whenever the href'd object changes. This + * is of course grossly inefficient, and also does not react to changes + * to the href'd during context stuff (like changing the shape of a star by + * dragging control points) unless the path of that object is changed during + * the context (seems to be the case for SPEllipse). The computation of the + * offset is done in sp_offset_set_shape(), a function that is called whenever + * a change occurs to the offset (change of source or change of radius). + * just like the sp-star and other, this path derivative can make control + * points, or more precisely one control point, that's enough to define the + * radius (look in shape-editor-knotholders). + */ + +static void refresh_offset_source(SPOffset* offset); + +static void sp_offset_start_listening(SPOffset *offset,SPObject* to); +static void sp_offset_quit_listening(SPOffset *offset); +static void sp_offset_href_changed(SPObject *old_ref, SPObject *ref, SPOffset *offset); +static void sp_offset_move_compensate(Geom::Affine const *mp, SPItem *original, SPOffset *self); +static void sp_offset_delete_self(SPObject *deleted, SPOffset *self); +static void sp_offset_source_modified (SPObject *iSource, guint flags, SPItem *item); + + +// slow= source path->polygon->offset of polygon->polygon->path +// fast= source path->offset of source path->polygon->path +// fast is not mathematically correct, because computing the offset of a single +// cubic bezier patch is not trivial; in particular, there are problems with holes +// reappearing in offset when the radius becomes too large +//TODO: need fix for bug: #384688 with fix released in r.14156 +//but reverted because bug #1507049 seems has more priority. +static bool use_slow_but_correct_offset_method = false; + +SPOffset::SPOffset() : SPShape() { + this->rad = 1.0; + this->original = nullptr; + this->originalPath = nullptr; + this->knotSet = false; + this->sourceDirty=false; + this->isUpdating=false; + // init various connections + this->sourceHref = nullptr; + this->sourceRepr = nullptr; + this->sourceObject = nullptr; + + // set up the uri reference + this->sourceRef = new SPUseReference(this); + this->_changed_connection = this->sourceRef->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_offset_href_changed), this)); +} + +SPOffset::~SPOffset() { + delete this->sourceRef; + + this->_modified_connection.disconnect(); + this->_delete_connection.disconnect(); + this->_changed_connection.disconnect(); + this->_transformed_connection.disconnect(); +} + +void SPOffset::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPShape::build(document, repr); + + //XML Tree being used directly here while it shouldn't be. + if (this->getRepr()->attribute("inkscape:radius")) { + this->readAttr(SPAttr::INKSCAPE_RADIUS); + } else { + //XML Tree being used directly here (as object->getRepr) + //in all the below lines in the block while it shouldn't be. + gchar const *oldA = this->getRepr()->attribute("sodipodi:radius"); + this->setAttribute("inkscape:radius", oldA); + this->removeAttribute("sodipodi:radius"); + + this->readAttr(SPAttr::INKSCAPE_RADIUS); + } + + if (this->getRepr()->attribute("inkscape:original")) { + this->readAttr(SPAttr::INKSCAPE_ORIGINAL); + } else { + gchar const *oldA = this->getRepr()->attribute("sodipodi:original"); + this->setAttribute("inkscape:original", oldA); + this->removeAttribute("sodipodi:original"); + + this->readAttr(SPAttr::INKSCAPE_ORIGINAL); + } + + if (this->getRepr()->attribute("xlink:href")) { + this->readAttr(SPAttr::XLINK_HREF); + } else { + gchar const *oldA = this->getRepr()->attribute("inkscape:href"); + + if (oldA) { + size_t lA = strlen(oldA); + char *nA=(char*)malloc((1+lA+1)*sizeof(char)); + + memcpy(nA+1,oldA,lA*sizeof(char)); + + nA[0]='#'; + nA[lA+1]=0; + + this->setAttribute("xlink:href", nA); + + free(nA); + + this->removeAttribute("inkscape:href"); + } + + this->readAttr(SPAttr::XLINK_HREF); + } +} + +Inkscape::XML::Node* SPOffset::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:path"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + /** \todo + * Fixme: we may replace these attributes by + * inkscape:offset="cx cy exp revo rad arg t0" + */ + repr->setAttribute("sodipodi:type", "inkscape:offset"); + repr->setAttributeSvgDouble("inkscape:radius", this->rad); + repr->setAttribute("inkscape:original", this->original); + repr->setAttribute("inkscape:href", this->sourceHref); + } + + + // Make sure the offset has curve + if (!_curve) { + set_shape(); + } + + // write that curve to "d" + repr->setAttribute("d", sp_svg_write_path(_curve->get_pathvector())); + + SPShape::write(xml_doc, repr, flags | SP_SHAPE_WRITE_PATH); + + return repr; +} + +void SPOffset::release() { + if (this->original) { + free (this->original); + } + + if (this->originalPath) { + delete ((Path *) this->originalPath); + } + + this->original = nullptr; + this->originalPath = nullptr; + + sp_offset_quit_listening(this); + + this->_changed_connection.disconnect(); + + g_free(this->sourceHref); + + this->sourceHref = nullptr; + this->sourceRef->detach(); + + SPShape::release(); +} + +void SPOffset::set(SPAttr key, const gchar* value) { + if ( this->sourceDirty ) { + refresh_offset_source(this); + } + + /* fixme: we should really collect updates */ + switch (key) + { + case SPAttr::INKSCAPE_ORIGINAL: + case SPAttr::SODIPODI_ORIGINAL: + if (value == nullptr) { + } else { + if (this->original) { + free (this->original); + delete ((Path *) this->originalPath); + + this->original = nullptr; + this->originalPath = nullptr; + } + + this->original = strdup (value); + + Geom::PathVector pv = sp_svg_read_pathv(this->original); + + this->originalPath = new Path; + reinterpret_cast<Path *>(this->originalPath)->LoadPathVector(pv); + + this->knotSet = false; + + if ( this->isUpdating == false ) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + break; + + case SPAttr::INKSCAPE_RADIUS: + case SPAttr::SODIPODI_RADIUS: + if (!sp_svg_length_read_computed_absolute (value, &this->rad)) { + if (fabs (this->rad) < 0.01) { + this->rad = (this->rad < 0) ? -0.01 : 0.01; + } + + this->knotSet = false; // knotset=false because it's not set from the context + } + + if ( this->isUpdating == false ) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + break; + + case SPAttr::INKSCAPE_HREF: + case SPAttr::XLINK_HREF: + if ( value == nullptr ) { + sp_offset_quit_listening(this); + if ( this->sourceHref ) { + g_free(this->sourceHref); + } + + this->sourceHref = nullptr; + this->sourceRef->detach(); + } else { + if ( this->sourceHref && ( strcmp(value, this->sourceHref) == 0 ) ) { + } else { + if ( this->sourceHref ) { + g_free(this->sourceHref); + } + + this->sourceHref = g_strdup(value); + + try { + this->sourceRef->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + this->sourceRef->detach(); + } + } + } + break; + + default: + SPShape::set(key, value); + break; + } +} + +void SPOffset::update(SPCtx *ctx, guint flags) { + this->isUpdating=true; // prevent sp_offset_set from requesting updates + + if ( this->sourceDirty ) { + refresh_offset_source(this); + } + + if (flags & + (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + this->set_shape(); + } + + this->isUpdating=false; + + SPShape::update(ctx, flags); +} + +const char* SPOffset::displayName() const { + if ( this->sourceHref ) { + return _("Linked Offset"); + } else { + return _("Dynamic Offset"); + } +} + +const char* SPOffset::typeName() const { + return "offset"; +} + +gchar* SPOffset::description() const { + // TRANSLATORS COMMENT: %s is either "outset" or "inset" depending on sign + return g_strdup_printf(_("%s by %f pt"), (this->rad >= 0) ? + _("outset") : _("inset"), fabs (this->rad)); +} + +void SPOffset::set_shape() { + if ( this->originalPath == nullptr ) { + // oops : no path?! (the offset object should do harakiri) + return; + } +#ifdef OFFSET_VERBOSE + g_print ("rad=%g\n", offset->rad); +#endif + // au boulot + + if ( fabs(this->rad) < 0.01 ) { + // grosso modo: 0 + // just put the source of this (almost-non-offsetted) object as being the actual offset, + // no one will notice. it's also useless to compute the offset with a 0 radius + + //XML Tree being used directly here while it shouldn't be. + const char *res_d = this->getRepr()->attribute("inkscape:original"); + + if ( res_d ) { + setCurveInsync(SPCurve(sp_svg_read_pathv(res_d))); + setCurveBeforeLPE(curve()); + } + + return; + } + + // extra paranoiac careful check. the preceding if () should take care of this case + if (fabs (this->rad) < 0.01) { + this->rad = (this->rad < 0) ? -0.01 : 0.01; + } + + Path *orig = new Path; + orig->Copy ((Path *)this->originalPath); + + if ( use_slow_but_correct_offset_method == false ) { + // version par outline + Shape *theShape = new Shape; + Shape *theRes = new Shape; + Path *originaux[1]; + Path *res = new Path; + res->SetBackData (false); + + // and now: offset + float o_width; + if (this->rad >= 0) + { + o_width = this->rad; + orig->OutsideOutline (res, o_width, join_round, butt_straight, 20.0); + } + else + { + o_width = -this->rad; + orig->OutsideOutline (res, -o_width, join_round, butt_straight, 20.0); + } + + if (o_width >= 1.0) + { + // res->ConvertForOffset (1.0, orig, offset->rad); + res->ConvertWithBackData (1.0); + } + else + { + // res->ConvertForOffset (o_width, orig, offset->rad); + res->ConvertWithBackData (o_width); + } + res->Fill (theShape, 0); + theRes->ConvertToShape (theShape, fill_positive); + originaux[0] = res; + + theRes->ConvertToForme (orig, 1, originaux); + + Geom::OptRect bbox = this->documentVisualBounds(); + + if ( bbox ) { + gdouble size = L2(bbox->dimensions()); + gdouble const exp = this->transform.descrim(); + + if (exp != 0) { + size /= exp; + } + + orig->Coalesce (size * 0.001); + //g_print ("coa %g exp %g item %p\n", size * 0.001, exp, item); + } + + + // if (o_width >= 1.0) + // { + // orig->Coalesce (0.1); // small threshold, since we only want to get rid of small segments + // the curve should already be computed by the Outline() function + // orig->ConvertEvenLines (1.0); + // orig->Simplify (0.5); + // } + // else + // { + // orig->Coalesce (0.1*o_width); + // orig->ConvertEvenLines (o_width); + // orig->Simplify (0.5 * o_width); + // } + + delete theShape; + delete theRes; + delete res; + } else { + // version par makeoffset + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + + // and now: offset + float o_width; + if (this->rad >= 0) + { + o_width = this->rad; + } + else + { + o_width = -this->rad; + } + + // one has to have a measure of the details + if (o_width >= 1.0) + { + orig->ConvertWithBackData (0.5); + } + else + { + orig->ConvertWithBackData (0.5*o_width); + } + + orig->Fill (theShape, 0); + theRes->ConvertToShape (theShape, fill_positive); + + Path *originaux[1]; + originaux[0]=orig; + + Path *res = new Path; + theRes->ConvertToForme (res, 1, originaux); + + int nbPart=0; + Path** parts=res->SubPaths(nbPart,true); + char *holes=(char*)malloc(nbPart*sizeof(char)); + + // we offset contours separately, because we can. + // this way, we avoid doing a unique big ConvertToShape when dealing with big shapes with lots of holes + { + Shape* onePart=new Shape; + Shape* oneCleanPart=new Shape; + + theShape->Reset(); + + for (int i=0;i<nbPart;i++) { + double partSurf=parts[i]->Surface(); + parts[i]->Convert(1.0); + + { + // raffiner si besoin + double bL,bT,bR,bB; + parts[i]->PolylineBoundingBox(bL,bT,bR,bB); + double measure=((bR-bL)+(bB-bT))*0.5; + if ( measure < 10.0 ) { + parts[i]->Convert(0.02*measure); + } + } + + if ( partSurf < 0 ) { // inverse par rapport a la realite + // plein + holes[i]=0; + parts[i]->Fill(oneCleanPart,0); + onePart->ConvertToShape(oneCleanPart,fill_positive); // there aren't intersections in that one, but maybe duplicate points and null edges + oneCleanPart->MakeOffset(onePart,this->rad,join_round,20.0); + onePart->ConvertToShape(oneCleanPart,fill_positive); + + onePart->CalcBBox(); + double typicalSize=0.5*((onePart->rightX-onePart->leftX)+(onePart->bottomY-onePart->topY)); + + if ( typicalSize < 0.05 ) { + typicalSize=0.05; + } + + typicalSize*=0.01; + + if ( typicalSize > 1.0 ) { + typicalSize=1.0; + } + + onePart->ConvertToForme (parts[i]); + parts[i]->ConvertEvenLines (typicalSize); + parts[i]->Simplify (typicalSize); + + double nPartSurf=parts[i]->Surface(); + + if ( nPartSurf >= 0 ) { + // inversion de la surface -> disparait + delete parts[i]; + parts[i]=nullptr; + } else { + } + +/* int firstP=theShape->nbPt; + for (int j=0;j<onePart->nbPt;j++) theShape->AddPoint(onePart->pts[j].x); + for (int j=0;j<onePart->nbAr;j++) theShape->AddEdge(firstP+onePart->aretes[j].st,firstP+onePart->aretes[j].en);*/ + } else { + // trou + holes[i]=1; + parts[i]->Fill(oneCleanPart,0,false,true,true); + onePart->ConvertToShape(oneCleanPart,fill_positive); + oneCleanPart->MakeOffset(onePart,-this->rad,join_round,20.0); + onePart->ConvertToShape(oneCleanPart,fill_positive); +// for (int j=0;j<onePart->nbAr;j++) onePart->Inverse(j); // pas oublier de reinverser + + onePart->CalcBBox(); + double typicalSize=0.5*((onePart->rightX-onePart->leftX)+(onePart->bottomY-onePart->topY)); + + if ( typicalSize < 0.05 ) { + typicalSize=0.05; + } + + typicalSize*=0.01; + + if ( typicalSize > 1.0 ) { + typicalSize=1.0; + } + + onePart->ConvertToForme (parts[i]); + parts[i]->ConvertEvenLines (typicalSize); + parts[i]->Simplify (typicalSize); + double nPartSurf=parts[i]->Surface(); + + if ( nPartSurf >= 0 ) { + // inversion de la surface -> disparait + delete parts[i]; + parts[i]=nullptr; + } else { + } + + /* int firstP=theShape->nbPt; + for (int j=0;j<onePart->nbPt;j++) theShape->AddPoint(onePart->pts[j].x); + for (int j=0;j<onePart->nbAr;j++) theShape->AddEdge(firstP+onePart->aretes[j].en,firstP+onePart->aretes[j].st);*/ + } +// delete parts[i]; + } +// theShape->MakeOffset(theRes,offset->rad,join_round,20.0); + delete onePart; + delete oneCleanPart; + } + + if ( nbPart > 1 ) { + theShape->Reset(); + + for (int i=0;i<nbPart;i++) { + if ( parts[i] ) { + parts[i]->ConvertWithBackData(1.0); + + if ( holes[i] ) { + parts[i]->Fill(theShape,i,true,true,true); + } else { + parts[i]->Fill(theShape,i,true,true,false); + } + } + } + + theRes->ConvertToShape (theShape, fill_positive); + theRes->ConvertToForme (orig,nbPart,parts); + + for (int i=0;i<nbPart;i++) { + if ( parts[i] ) { + delete parts[i]; + } + } + } else if ( nbPart == 1 ) { + orig->Copy(parts[0]); + + for (int i=0;i<nbPart;i++) { + if ( parts[i] ) { + delete parts[i]; + } + } + } else { + orig->Reset(); + } +// theRes->ConvertToShape (theShape, fill_positive); +// theRes->ConvertToForme (orig); + +/* if (o_width >= 1.0) { + orig->ConvertEvenLines (1.0); + orig->Simplify (1.0); + } else { + orig->ConvertEvenLines (1.0*o_width); + orig->Simplify (1.0 * o_width); + }*/ + + if ( parts ) { + free(parts); + } + + if ( holes ) { + free(holes); + } + + delete res; + delete theShape; + delete theRes; + } + { + Geom::PathVector res_pv; + + if (orig->descr_cmd.size() <= 1) { + // Aie.... nothing left. + res_pv = sp_svg_read_pathv("M 0 0 L 0 0 z"); + } else { + res_pv = orig->MakePathVector(); + } + + delete orig; + + setCurveInsync(SPCurve(std::move(res_pv))); + setCurveBeforeLPE(curve()); + } +} + +void SPOffset::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + SPShape::snappoints(p, snapprefs); +} + + +// utilitaires pour les poignees +// used to get the distance to the shape: distance to polygon give the fabs(radius), we still need +// the sign. for edges, it's easy to determine which side the point is on, for points of the polygon +// it's trickier: we need to identify which angle the point is in; to that effect, we take each +// successive clockwise angle (A,C) and check if the vector B given by the point is in the angle or +// outside. +// another method would be to use the Winding() function to test whether the point is inside or outside +// the polygon (it would be wiser to do so, in fact, but i like being stupid) + +/** + * + * \todo + * FIXME: This can be done using linear operations, more stably and + * faster. method: transform A and C into B's space, A should be + * negative and B should be positive in the orthogonal component. I + * think this is equivalent to + * dot(A, rot90(B))*dot(C, rot90(B)) == -1. + * -- njh + */ +static bool +vectors_are_clockwise (Geom::Point A, Geom::Point B, Geom::Point C) +{ + using Geom::rot90; + double ab_s = dot(A, rot90(B)); + double ab_c = dot(A, B); + double ca_s = dot(C, rot90(A)); + double ca_c = dot(C, A); + + double ab_a = acos (ab_c); + + if (ab_c <= -1.0) { + ab_a = M_PI; + } + + if (ab_c >= 1.0) { + ab_a = 0; + } + + if (ab_s < 0) { + ab_a = 2 * M_PI - ab_a; + } + + double ca_a = acos (ca_c); + + if (ca_c <= -1.0) { + ca_a = M_PI; + } + + if (ca_c >= 1.0) { + ca_a = 0; + } + + if (ca_s < 0) { + ca_a = 2 * M_PI - ca_a; + } + + double lim = 2 * M_PI - ca_a; + + if (ab_a < lim) { + return true; + } + + return false; +} + +/** + * Distance to the original path; that function is called from shape-editor-knotholders + * to set the radius when the control knot moves. + * + * The sign of the result is the radius we're going to offset the shape with, + * so result > 0 ==outset and result < 0 ==inset. thus result<0 means + * 'px inside source'. + */ +double +sp_offset_distance_to_original (SPOffset * offset, Geom::Point px) +{ + if (offset == nullptr || offset->originalPath == nullptr || ((Path *) offset->originalPath)->descr_cmd.size() <= 1) { + return 1.0; + } + + double dist = 1.0; + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + /** \todo + * Awfully damn stupid method: uncross the source path EACH TIME you + * need to compute the distance. The good way to do this would be to + * store the uncrossed source path somewhere, and delete it when the + * context is finished. Hopefully this part is much faster than actually + * computing the offset (which happen just after), so the time spent in + * this function should end up being negligible with respect to the + * delay of one context. + */ + // move + ((Path *) offset->originalPath)->Convert (1.0); + ((Path *) offset->originalPath)->Fill (theShape, 0); + theRes->ConvertToShape (theShape, fill_oddEven); + + if (theRes->numberOfEdges() <= 1) + { + + } + else + { + double ptDist = -1.0; + bool ptSet = false; + double arDist = -1.0; + bool arSet = false; + + // first get the minimum distance to the points + for (int i = 0; i < theRes->numberOfPoints(); i++) + { + if (theRes->getPoint(i).totalDegree() > 0) + { + Geom::Point nx = theRes->getPoint(i).x; + Geom::Point nxpx = px-nx; + double ndist = sqrt (dot(nxpx,nxpx)); + + if (ptSet == false || fabs (ndist) < fabs (ptDist)) + { + // we have a new minimum distance + // now we need to wheck if px is inside or outside (for the sign) + nx = px - theRes->getPoint(i).x; + double nlen = sqrt (dot(nx , nx)); + nx /= nlen; + int pb, cb, fb; + fb = theRes->getPoint(i).incidentEdge[LAST]; + pb = theRes->getPoint(i).incidentEdge[LAST]; + cb = theRes->getPoint(i).incidentEdge[FIRST]; + + do + { + // one angle + Geom::Point prx, nex; + prx = theRes->getEdge(pb).dx; + nlen = sqrt (dot(prx, prx)); + prx /= nlen; + nex = theRes->getEdge(cb).dx; + nlen = sqrt (dot(nex , nex)); + nex /= nlen; + + if (theRes->getEdge(pb).en == i) + { + prx = -prx; + } + + if (theRes->getEdge(cb).en == i) + { + nex = -nex; + } + + if (vectors_are_clockwise (nex, nx, prx)) + { + // we're in that angle. set the sign, and exit that loop + if (theRes->getEdge(cb).st == i) + { + ptDist = -ndist; + ptSet = true; + } + else + { + ptDist = ndist; + ptSet = true; + } + break; + } + + pb = cb; + cb = theRes->NextAt (i, cb); + } + + while (cb >= 0 && pb >= 0 && pb != fb); + } + } + } + + // loop over the edges to try to improve the distance + for (int i = 0; i < theRes->numberOfEdges(); i++) + { + Geom::Point sx = theRes->getPoint(theRes->getEdge(i).st).x; + Geom::Point ex = theRes->getPoint(theRes->getEdge(i).en).x; + Geom::Point nx = ex - sx; + double len = sqrt (dot(nx,nx)); + + if (len > 0.0001) + { + Geom::Point pxsx=px-sx; + double ab = dot(nx,pxsx); + + if (ab > 0 && ab < len * len) + { + // we're in the zone of influence of the segment + double ndist = (cross(nx, pxsx)) / len; + + if (arSet == false || fabs (ndist) < fabs (arDist)) + { + arDist = ndist; + arSet = true; + } + } + } + } + + if (arSet || ptSet) + { + if (arSet == false) { + arDist = ptDist; + } + + if (ptSet == false) { + ptDist = arDist; + } + + if (fabs (ptDist) < fabs (arDist)) { + dist = ptDist; + } else { + dist = arDist; + } + } + } + + delete theShape; + delete theRes; + + return dist; +} + +/** + * Computes a point on the offset; used to set a "seed" position for + * the control knot. + * + * \return the topmost point on the offset. + */ +void +sp_offset_top_point (SPOffset const * offset, Geom::Point *px) +{ + (*px) = Geom::Point(0, 0); + + if (offset == nullptr) { + return; + } + + if (offset->knotSet) + { + (*px) = offset->knot; + return; + } + + SPCurve const *curve = offset->curve(); + + if (curve == nullptr) + { + const_cast<SPOffset*>(offset)->set_shape(); + + curve = offset->curve(); + + if (curve == nullptr) + return; + } + + if (curve->is_empty()) + { + return; + } + + Path *finalPath = new Path; + finalPath->LoadPathVector(curve->get_pathvector()); + + Shape *theShape = new Shape; + + finalPath->Convert (1.0); + finalPath->Fill (theShape, 0); + + if (theShape->hasPoints()) + { + theShape->SortPoints (); + *px = theShape->getPoint(0).x; + } + + delete theShape; + delete finalPath; +} + +// the listening functions +static void sp_offset_start_listening(SPOffset *offset,SPObject* to) +{ + if ( to == nullptr ) { + return; + } + + offset->sourceObject = to; + offset->sourceRepr = to->getRepr(); + + offset->_delete_connection = to->connectDelete(sigc::bind(sigc::ptr_fun(&sp_offset_delete_self), offset)); + offset->_transformed_connection = cast<SPItem>(to)->connectTransformed(sigc::bind(sigc::ptr_fun(&sp_offset_move_compensate), offset)); + offset->_modified_connection = to->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_offset_source_modified), offset)); +} + +static void sp_offset_quit_listening(SPOffset *offset) +{ + if ( offset->sourceObject == nullptr ) { + return; + } + + offset->_modified_connection.disconnect(); + offset->_delete_connection.disconnect(); + offset->_transformed_connection.disconnect(); + + offset->sourceRepr = nullptr; + offset->sourceObject = nullptr; +} + +static void +sp_offset_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, SPOffset *offset) +{ + sp_offset_quit_listening(offset); + + if (offset->sourceRef) { + SPItem *refobj = offset->sourceRef->getObject(); + + if (refobj) { + sp_offset_start_listening(offset,refobj); + } + + offset->sourceDirty=true; + offset->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +static void sp_offset_move_compensate(Geom::Affine const *mp, SPItem */*original*/, SPOffset *self) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_PARALLEL); + + Geom::Affine m(*mp); + + if (!(m.isTranslation()) || mode == SP_CLONE_COMPENSATION_NONE) { + self->sourceDirty=true; + self->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + return; + } + + // calculate the compensation matrix and the advertized movement matrix + self->readAttr(SPAttr::TRANSFORM); + + Geom::Affine t = self->transform; + Geom::Affine offset_move = t.inverse() * m * t; + + Geom::Affine advertized_move; + if (mode == SP_CLONE_COMPENSATION_PARALLEL) { + offset_move = offset_move.inverse() * m; + advertized_move = m; + } else if (mode == SP_CLONE_COMPENSATION_UNMOVED) { + offset_move = offset_move.inverse(); + advertized_move.setIdentity(); + } else { + g_assert_not_reached(); + } + + self->sourceDirty=true; + + // commit the compensation + self->transform *= offset_move; + self->doWriteTransform(self->transform, &advertized_move); + self->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +static void +sp_offset_delete_self(SPObject */*deleted*/, SPOffset *offset) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint const mode = prefs->getInt("/options/cloneorphans/value", SP_CLONE_ORPHANS_UNLINK); + + if (mode == SP_CLONE_ORPHANS_UNLINK) { + // leave it be. just forget about the source + sp_offset_quit_listening(offset); + + if ( offset->sourceHref ) { + g_free(offset->sourceHref); + } + + offset->sourceHref = nullptr; + offset->sourceRef->detach(); + } else if (mode == SP_CLONE_ORPHANS_DELETE) { + offset->deleteObject(); + } +} + +static void +sp_offset_source_modified (SPObject */*iSource*/, guint flags, SPItem *item) +{ + auto offset = cast<SPOffset>(item); + offset->sourceDirty=true; + + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG)) { + offset->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +static void +refresh_offset_source(SPOffset* offset) +{ + if ( offset == nullptr ) { + return; + } + + offset->sourceDirty=false; + + // le mauvais cas: pas d'attribut d => il faut verifier que c'est une SPShape puis prendre le contour + // The bad case: no d attribute. Must check that it's an SPShape and then take the outline. + SPObject *refobj=offset->sourceObject; + + if ( refobj == nullptr ) { + return; + } + + auto item = cast<SPItem>(refobj); + SPCurve curve; + + if (auto shape = cast<SPShape>(item)) { + if (!shape->curve()) { + return; + } + curve = *shape->curve(); + } else if (auto text = cast<SPText>(item)) { + curve = text->getNormalizedBpath(); + } else { + return; + } + + Path *orig = new Path; + orig->LoadPathVector(curve.get_pathvector()); + + if (!item->transform.isIdentity()) { + gchar const *t_attr = item->getRepr()->attribute("transform"); + + if (t_attr) { + Geom::Affine t; + + if (sp_svg_transform_read(t_attr, &t)) { + orig->Transform(t); + } + } + } + + // Finish up. + { + SPCSSAttr *css; + const gchar *val; + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + orig->ConvertWithBackData (1.0); + orig->Fill (theShape, 0); + + css = sp_repr_css_attr (offset->sourceRepr , "style"); + val = sp_repr_css_property (css, "fill-rule", nullptr); + + if (val && strcmp (val, "nonzero") == 0) + { + theRes->ConvertToShape (theShape, fill_nonZero); + } + else if (val && strcmp (val, "evenodd") == 0) + { + theRes->ConvertToShape (theShape, fill_oddEven); + } + else + { + theRes->ConvertToShape (theShape, fill_nonZero); + } + + Path *originaux[1]; + originaux[0] = orig; + Path *res = new Path; + theRes->ConvertToForme (res, 1, originaux); + + delete theShape; + delete theRes; + + char *res_d = res->svg_dump_path (); + delete res; + delete orig; + + // TODO fix: + //XML Tree being used directly here while it shouldn't be. + offset->setAttribute("inkscape:original", res_d); + + free (res_d); + } +} + +SPItem *sp_offset_get_source(SPOffset *offset) +{ + if (offset && offset->sourceRef) { + return offset->sourceRef->getObject(); + } + + return nullptr; +} + +/* + 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 : diff --git a/src/object/sp-offset.h b/src/object/sp-offset.h new file mode 100644 index 0000000..2263358 --- /dev/null +++ b/src/object/sp-offset.h @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_OFFSET_H +#define SEEN_SP_OFFSET_H +/* + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * (of the sp-spiral.h upon which this file was created) + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "sp-shape.h" + +class SPUseReference; + +/** + * SPOffset class. + * + * An offset is defined by curve and radius. The original curve is kept as + * a path in a sodipodi:original attribute. It's not possible to change + * the original curve. + * + * SPOffset is a derivative of SPShape, much like the SPSpiral or SPRect. + * The goal is to have a source shape (= originalPath), an offset (= radius) + * and compute the offset of the source by the radius. To get it to work, + * one needs to know what the source is and what the radius is, and how it's + * stored in the xml representation. The object itself is a "path" element, + * to get lots of shape functionality for free. The source is the easy part: + * it's stored in a "inkscape:original" attribute in the path. In case of + * "linked" offset, as they've been dubbed, there is an additional + * "inkscape:href" that contains the id of an element of the svg. + * When built, the object will attach a listener vector to that object and + * rebuild the "inkscape:original" whenever the href'd object changes. This + * is of course grossly inefficient, and also does not react to changes + * to the href'd during context stuff (like changing the shape of a star by + * dragging control points) unless the path of that object is changed during + * the context (seems to be the case for SPEllipse). The computation of the + * offset is done in sp_offset_set_shape(), a function that is called whenever + * a change occurs to the offset (change of source or change of radius). + * just like the sp-star and other, this path derivative can make control + * points, or more precisely one control point, that's enough to define the + * radius (look in shape-editor-knotholders). + */ +class SPOffset final : public SPShape { +public: + SPOffset(); + ~SPOffset() override; + int tag() const override { return tag_of<decltype(*this)>; } + + void *originalPath; ///< will be a livarot Path, just don't declare it here to please the gcc linker FIXME what? + char *original; ///< SVG description of the source path + float rad; ///< offset radius + + /// for interactive setting of the radius + bool knotSet; + Geom::Point knot; + + bool sourceDirty; + bool isUpdating; + + char *sourceHref; + SPUseReference *sourceRef; + Inkscape::XML::Node *sourceRepr; ///< the repr associated with that id + SPObject *sourceObject; + + sigc::connection _modified_connection; + sigc::connection _delete_connection; + sigc::connection _changed_connection; + sigc::connection _transformed_connection; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned flags) override; + void release() override; + + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + const char* displayName() const override; + const char* typeName() const override; + char* description() const override; + + void set_shape() override; +}; + +double sp_offset_distance_to_original (SPOffset * offset, Geom::Point px); +void sp_offset_top_point (SPOffset const *offset, Geom::Point *px); + +SPItem *sp_offset_get_source (SPOffset *offset); + +#endif + +/* + 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 : diff --git a/src/object/sp-page.cpp b/src/object/sp-page.cpp new file mode 100644 index 0000000..cc5c41f --- /dev/null +++ b/src/object/sp-page.cpp @@ -0,0 +1,649 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape pages implementation + * + * Authors: + * Martin Owens <doctormo@geek-2.com> + * + * Copyright (C) 2021 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include "sp-page.h" + +#include "attributes.h" +#include "desktop.h" +#include "display/control/canvas-page.h" +#include "inkscape.h" +#include "object/object-set.h" +#include "sp-namedview.h" +#include "sp-root.h" +#include "util/numeric/converters.h" + +using Inkscape::DocumentUndo; + +SPPage::SPPage() + : SPObject() +{ + _canvas_item = new Inkscape::CanvasPage(); +} + +SPPage::~SPPage() +{ + delete _canvas_item; + _canvas_item = nullptr; +} + +void SPPage::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + this->readAttr(SPAttr::INKSCAPE_LABEL); + this->readAttr(SPAttr::PAGE_SIZE); + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + this->readAttr(SPAttr::WIDTH); + this->readAttr(SPAttr::HEIGHT); + this->readAttr(SPAttr::PAGE_MARGIN); + this->readAttr(SPAttr::PAGE_BLEED); + + /* Register */ + document->addResource("page", this); +} + +void SPPage::release() +{ + if (this->document) { + // Unregister ourselves + this->document->removeResource("page", this); + } + + SPObject::release(); +} + +void SPPage::set(SPAttr key, const gchar *value) +{ + switch (key) { + case SPAttr::X: + this->x.readOrUnset(value); + break; + case SPAttr::Y: + this->y.readOrUnset(value); + break; + case SPAttr::WIDTH: + this->width.readOrUnset(value); + break; + case SPAttr::HEIGHT: + this->height.readOrUnset(value); + break; + case SPAttr::PAGE_MARGIN: + this->margin.readOrUnset(value); + break; + case SPAttr::PAGE_BLEED: + this->bleed.readOrUnset(value); + break; + case SPAttr::PAGE_SIZE: + this->_size_label = value ? std::string(value) : ""; + break; + default: + SPObject::set(key, value); + break; + } + update_relatives(); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Update the percentage values of the svg boxes + */ +void SPPage::update_relatives() +{ + if (this->width && this->height) { + if (this->margin) + this->margin.update(12, 6, this->width.computed, this->height.computed); + if (this->bleed) + this->bleed.update(12, 6, this->width.computed, this->height.computed); + } +} + +/** + * Returns true if the only aspect to this page is its size + */ +bool SPPage::isBarePage() const +{ + if (margin || bleed) { + return false; + } + return true; +} + +/** + * Gets the rectangle in document units + */ +Geom::Rect SPPage::getRect() const +{ + return Geom::Rect::from_xywh(x.computed, y.computed, width.computed, height.computed); +} + +/** + * Get the rectangle of the page, in desktop units + */ +Geom::Rect SPPage::getDesktopRect() const +{ + return getDocumentRect() * document->doc2dt(); +} + +/** + * Gets the page's position as a translation in desktop units. + */ +Geom::Translate SPPage::getDesktopAffine() const +{ + auto box = getDesktopRect(); + return Geom::Translate(box.left(), box.top()); +} + +/** + * Get document rect, minus the margin amounts. + */ +Geom::Rect SPPage::getDocumentMargin() const +{ + auto rect = getDocumentRect(); + rect.setTop(rect.top() + margin.top().computed); + rect.setLeft(rect.left() + margin.left().computed); + rect.setBottom(rect.bottom() - margin.bottom().computed); + rect.setRight(rect.right() - margin.right().computed); + if (rect.hasZeroArea()) + return getDocumentRect(); // Cancel! + return rect; +} + +Geom::Rect SPPage::getDesktopMargin() const +{ + return getDocumentMargin() * document->doc2dt(); +} + +/** + * Get document rect, plus the bleed amounts. + */ +Geom::Rect SPPage::getDocumentBleed() const +{ + auto rect = getDocumentRect(); + rect.setTop(rect.top() - bleed.top().computed); + rect.setLeft(rect.left() - bleed.left().computed); + rect.setBottom(rect.bottom() + bleed.bottom().computed); + rect.setRight(rect.right() + bleed.right().computed); + if (rect.hasZeroArea()) + return getDocumentRect(); // Cancel! + return rect; +} + +Geom::Rect SPPage::getDesktopBleed() const +{ + return getDocumentBleed() * document->doc2dt(); +} + +/** + * Get the rectangle of the page, scaled to the document. + */ +Geom::Rect SPPage::getDocumentRect() const +{ + return getRect() * document->getDocumentScale(); +} + +/** + * Like getDesktopRect but returns a slightly shrunken rectangle + * so interactions don't confuse the border with the object. + */ +Geom::Rect SPPage::getSensitiveRect() const +{ + auto rect = getDesktopRect(); + rect.expandBy(-0.1); + return rect; +} + +/** + * Set the page rectangle in its native units. + */ +void SPPage::setRect(Geom::Rect rect) +{ + this->x = rect.left(); + this->y = rect.top(); + this->width = rect.width(); + this->height = rect.height(); + + // always clear size label, toolbar is responsible for putting it back if needed. + this->_size_label = ""; + + // This is needed to update the xml + this->updateRepr(); + + // This eventually calls the ::update below while idle + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Set the page rectangle in document coordinates. + */ +void SPPage::setDocumentRect(Geom::Rect rect, bool add_margins) +{ + if (add_margins) { + // Add margins to rectangle. + rect.setTop(rect.top() - margin.top().computed); + rect.setLeft(rect.left() - margin.left().computed); + rect.setBottom(rect.bottom() + margin.bottom().computed); + rect.setRight(rect.right() + margin.right().computed); + } + setRect(rect * document->getDocumentScale().inverse()); +} + +/** + * Set the page rectangle in desktop coordinates. + */ +void SPPage::setDesktopRect(Geom::Rect rect) +{ + setDocumentRect(rect * document->dt2doc()); +} + +/** @brief + * Set just the height and width from a predefined size. + * These dimensions are in document units, which happen to be the same + * as desktop units, since pages are aligned to the coordinate axes. + * + * @param width The desired width in document/desktop units. + * @param height The desired height in document/desktop units. + */ +void SPPage::setSize(double width, double height) +{ + auto rect = getDocumentRect(); + rect.setMax(rect.corner(0) + Geom::Point(width, height)); + setDocumentRect(rect); +} + +/** + * Set the page's margin + */ +void SPPage::setMargin(const std::string &value) +{ + this->margin.fromString(value, document->getDisplayUnit()->abbr); + this->updateRepr(); +} + +/** + * Set the page's bleed + */ +void SPPage::setBleed(const std::string &value) +{ + this->bleed.fromString(value, document->getDisplayUnit()->abbr); + this->updateRepr(); +} + +/** + * Get the margin side of the box. + */ +double SPPage::getMarginSide(int side) +{ + return this->margin.get((BoxSide)side); +} + +/** + * Set the margin at this side of the box. + */ +void SPPage::setMarginSide(int side, double value, bool confine) +{ + if (confine && !margin) { + this->margin.set(value, value, value, value); + } else { + this->margin.set((BoxSide)side, value, confine); + } + this->updateRepr(); +} +void SPPage::setMarginSide(int side, const std::string &value, bool confine) +{ + auto unit = document->getDisplayUnit()->abbr; + if (confine && !margin) { + this->margin.fromString(value, unit); + } else { + this->margin.fromString((BoxSide)side, value, unit); + } + this->updateRepr(); +} + +std::string SPPage::getMarginLabel() const +{ + if (!margin || margin.isZero()) + return ""; + auto unit = document->getDisplayUnit()->abbr; + return margin.toString(unit, 2); +} + +std::string SPPage::getBleedLabel() const +{ + if (!bleed || bleed.isZero()) + return ""; + auto unit = document->getDisplayUnit()->abbr; + return bleed.toString(unit, 2); +} + +/** + * Get the items which are ONLY on this page and don't overlap. + * + * This ignores layers so items in the same layer which are shared + * between pages are not moved around or exported into pages they + * shouldn't be. + * + * @param hidden - Return hidden items (default: true) + * @param in_bleed - Use the bleed box instead of the page box + * @param in_layers - Should layers be traversed to find items (default: true) + */ +std::vector<SPItem *> SPPage::getExclusiveItems(bool hidden, bool in_bleed, bool in_layers) const +{ + return document->getItemsInBox(0, in_bleed ? getDocumentBleed() : getDocumentRect(), hidden, true, true, false, in_layers); +} + +/** + * Like ExcludiveItems above but get all the items which are inside or overlapping. + * + * @param hidden - Return hidden items (default: true) + * @param in_bleed - Use the bleed box instead of the page box + * @param in_layers - Should layers be traversed to find items (default: true) + */ +std::vector<SPItem *> SPPage::getOverlappingItems(bool hidden, bool in_bleed, bool in_layers) const +{ + return document->getItemsPartiallyInBox(0, in_bleed ? getDocumentBleed() : getDocumentRect(), hidden, true, true, false, in_layers); +} + +/** + * Return true if this item is contained within the page boundary. + */ +bool SPPage::itemOnPage(SPItem *item, bool contains) const +{ + if (auto box = item->desktopGeometricBounds()) { + if (contains) { + return getDesktopRect().contains(*box); + } + return getDesktopRect().intersects(*box); + } + return false; +} + +/** + * Returns true if this page is the same as the viewport. + */ +bool SPPage::isViewportPage() const +{ + auto rect = document->preferredBounds(); + return getDocumentRect().corner(0) == rect->corner(0); +} + +/** + * Shows the page in the given canvas item group. + */ +void SPPage::showPage(Inkscape::CanvasItemGroup *fg, Inkscape::CanvasItemGroup *bg) +{ + _canvas_item->add(getDesktopRect(), fg, bg); + // The final steps are completed in an update cycle + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Sets the default attributes from the namedview. + */ +bool SPPage::setDefaultAttributes() +{ + if (document->getPageManager().setDefaultAttributes(_canvas_item)) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + return true; + } + return false; +} + +/** + * Set the selected high-light for this page. + */ +void SPPage::setSelected(bool sel) +{ + this->_canvas_item->is_selected = sel; + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Returns the page number (order of pages) starting at 1 + */ +int SPPage::getPageIndex() const +{ + return document->getPageManager().getPageIndex(this); +} + +/** + * Set this page to a new order in the page stack. + * + * @param index - Placement of page in the stack, starting at '0' + * @param swap_page - Swap the rectangle position + * + * @returns true if page has been moved. + */ +bool SPPage::setPageIndex(int index, bool swap_page) +{ + int current = getPageIndex(); + + if (current != index) { + auto &page_manager = document->getPageManager(); + + // The page we're going to be shifting to + auto sibling = page_manager.getPage(index); + + // Insertions are done to the right of the sibling + if (index < current) { + index -= 1; + } + auto insert_after = page_manager.getPage(index); + + // We may have selected an index off the end, so attach it after the last page. + if (!insert_after && index > 0) { + insert_after = page_manager.getLastPage(); + sibling = nullptr; // disable swap + } + + if (insert_after) { + if (this == insert_after) { + g_warning("Page is already at this index. Not moving."); + return false; + } + // Attach after the given page + getRepr()->parent()->changeOrder(getRepr(), insert_after->getRepr()); + } else { + // Attach to before any existing page + sibling = page_manager.getFirstPage(); + getRepr()->parent()->changeOrder(getRepr(), nullptr); + } + if (sibling && swap_page) { + swapPage(sibling, true); + } + return true; + } + return false; +} + +/** + * Returns the sibling page next to this one in the stack order. + */ +SPPage *SPPage::getNextPage() +{ + SPObject *item = this; + while ((item = item->getNext())) { + if (auto next = cast<SPPage>(item)) { + return next; + } + } + return nullptr; +} + +/** + * Returns the sibling page previous to this one in the stack order. + */ +SPPage *SPPage::getPreviousPage() +{ + SPObject *item = this; + while ((item = item->getPrev())) { + if (auto prev = cast<SPPage>(item)) { + return prev; + } + } + return nullptr; +} + +/** + * Move the page by the given affine, in desktop units. + * + * @param translate - The positional translation to apply. + * @param with_objects - Flag to request that connected objects also move. + */ +void SPPage::movePage(Geom::Affine translate, bool with_objects) +{ + if (translate.isTranslation()) { + if (with_objects) { + // Move each item that is overlapping this page too + moveItems(translate, getOverlappingItems()); + } + setDesktopRect(getDesktopRect() * translate); + } +} + +/** + * Move the given items by the given translation in document units. + * + * @param translate - The movement to be applied + * @param objects - a vector of SPItems to move + */ +void SPPage::moveItems(Geom::Affine translate, std::vector<SPItem *> const &items) +{ + if (items.empty()) { + return; + } + Inkscape::ObjectSet set(items[0]->document); + for (auto &item : items) { + if (item->isLocked()) { + continue; + } + set.add(item); + } + set.applyAffine(translate, true, false, true); +} + +/** + * Swap the locations of this page with another page (see movePage) + * + * @param other - The other page to swap with + * @param with_objects - Should the page objects move too. + */ +void SPPage::swapPage(SPPage *other, bool with_objects) +{ + // Swapping with the viewport page must be handled gracefully. + if (this->isViewportPage()) { + auto other_rect = other->getDesktopRect(); + auto new_rect = Geom::Rect(Geom::Point(0, 0), + Geom::Point(other_rect.width(), other_rect.height())); + this->document->fitToRect(new_rect, false); + } else if (other->isViewportPage()) { + other->swapPage(this, with_objects); + return; + } + + auto this_affine = Geom::Translate(getDesktopRect().corner(0)); + auto other_affine = Geom::Translate(other->getDesktopRect().corner(0)); + movePage(this_affine.inverse() * other_affine, with_objects); + other->movePage(other_affine.inverse() * this_affine, with_objects); +} + +void SPPage::update(SPCtx * /*ctx*/, unsigned int /*flags*/) +{ + // This is manual because this is not an SPItem, but it's own visual identity. + auto lbl = label(); + char *alt = nullptr; + if (document->getPageManager().showDefaultLabel()) { + alt = g_strdup_printf("%d", getPagePosition()); + } + _canvas_item->update(getDesktopRect(), getDesktopMargin(), getDesktopBleed(), lbl ? lbl : alt); + g_free(alt); +} + +/** + * Write out the page's data into its xml structure. + */ +Inkscape::XML::Node *SPPage::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("inkscape:page"); + } + + repr->setAttributeSvgDouble("x", this->x.computed); + repr->setAttributeSvgDouble("y", this->y.computed); + repr->setAttributeSvgDouble("width", this->width.computed); + repr->setAttributeSvgDouble("height", this->height.computed); + repr->setAttributeOrRemoveIfEmpty("margin", this->margin.write()); + repr->setAttributeOrRemoveIfEmpty("bleed", this->bleed.write()); + repr->setAttributeOrRemoveIfEmpty("page-size", this->_size_label); + + return SPObject::write(xml_doc, repr, flags); +} + +void SPPage::setSizeLabel(std::string label) +{ + _size_label = label; + // This is needed to update the xml + this->updateRepr(); +} + +std::string SPPage::getDefaultLabel() const +{ + gchar *format = g_strdup_printf(_("Page %d"), getPagePosition()); + auto ret = std::string(format); + g_free(format); + return ret; +} + +std::string SPPage::getLabel() const +{ + auto ret = label(); + if (!ret) { + return getDefaultLabel(); + } + return std::string(ret); +} + +std::string SPPage::getSizeLabel() const +{ + return _size_label; +} + +/** + * Copy non-size attributes from the given page. + */ +void SPPage::copyFrom(SPPage *page) +{ + this->_size_label = page->_size_label; + if (auto margin = page->getMargin()) { + this->margin.read(margin.write()); + } + if (auto bleed = page->getBleed()) { + this->bleed.read(bleed.write()); + } +} + +void SPPage::set_guides_visible(bool show) { + _canvas_item->set_guides_visible(show); +} + +/* + 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 : diff --git a/src/object/sp-page.h b/src/object/sp-page.h new file mode 100644 index 0000000..4f19821 --- /dev/null +++ b/src/object/sp-page.h @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SPPage -- a page object. + *//* + * Authors: + * Martin Owens 2021 + * + * Copyright (C) 2021 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_PAGE_H +#define SEEN_SP_PAGE_H + +#include <2geom/rect.h> +#include <vector> + +#include "display/control/canvas-page.h" +#include "page-manager.h" +#include "sp-object.h" +#include "svg/svg-length.h" +#include "svg/svg-box.h" + +class SPDesktop; +class SPItem; +namespace Inkscape { + class ObjectSet; +} + +class SPPage final : public SPObject +{ +public: + SPPage(); + ~SPPage() override; + int tag() const override { return tag_of<decltype(*this)>; } + + void movePage(Geom::Affine translate, bool with_objects); + void swapPage(SPPage *other, bool with_objects); + static void moveItems(Geom::Affine translate, std::vector<SPItem *> const &objects); + + // Canvas visualisation + void showPage(Inkscape::CanvasItemGroup *fg, Inkscape::CanvasItemGroup *bg); + void hidePage(Inkscape::UI::Widget::Canvas *canvas) { _canvas_item->remove(canvas); } + void showPage() { _canvas_item->show(); } + void hidePage() { _canvas_item->hide(); } + void set_guides_visible(bool show); + + double getMarginSide(int side); + const SVGBox &getMargin() const { return margin; } + void setMargin(const std::string &value); + void setMarginSide(int pos, double value, bool confine = false); + void setMarginSide(int side, const std::string &value, bool confine = false); + std::string getMarginLabel() const; + + const SVGBox &getBleed() const { return bleed; } + void setBleed(const std::string &value); + std::string getBleedLabel() const; + + void copyFrom(SPPage *page); + void setSelected(bool selected); + bool setDefaultAttributes(); + void setSizeLabel(std::string label); + int getPageIndex() const; + int getPagePosition() const { return getPageIndex() + 1; } + bool setPageIndex(int index, bool swap_page); + bool setPagePosition(int position, bool swap_page) { return setPageIndex(position - 1, swap_page); } + bool isBarePage() const; + + // To sort the pages in the set by index/page number + struct PageIndexOrder + { + bool operator()(const SPPage* Page1, const SPPage* Page2) const + { + return (Page1->getPageIndex() < Page2->getPageIndex()); + } + }; + + SPPage *getNextPage(); + SPPage *getPreviousPage(); + + Geom::Rect getRect() const; + Geom::Rect getDesktopRect() const; + Geom::Rect getDesktopMargin() const; + Geom::Rect getDesktopBleed() const; + Geom::Rect getDocumentRect() const; + Geom::Rect getDocumentMargin() const; + Geom::Rect getDocumentBleed() const; + Geom::Rect getSensitiveRect() const; + void setRect(Geom::Rect rect); + void setDocumentRect(Geom::Rect rect, bool add_margins = false); + void setDesktopRect(Geom::Rect rect); + void setSize(double width, double height); + std::vector<SPItem *> getExclusiveItems(bool hidden = true, bool in_bleed = false, bool in_layers = true) const; + std::vector<SPItem *> getOverlappingItems(bool hidden = true, bool in_bleed = false, bool in_layers = true) const; + bool itemOnPage(SPItem *item, bool contains = false) const; + bool isViewportPage() const; + std::string getDefaultLabel() const; + std::string getLabel() const; + std::string getSizeLabel() const; + + Geom::Translate getDesktopAffine() const; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void update(SPCtx *ctx, unsigned int flags) override; + void set(SPAttr key, const char *value) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) override; + + void update_relatives(); +private: + Inkscape::CanvasPage *_canvas_item = nullptr; + + SVGLength x; + SVGLength y; + SVGLength width; + SVGLength height; + SVGBox margin; + SVGBox bleed; + std::string _size_label; +}; + +#endif // SEEN_SP_PAGE_H + +/* + 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 : diff --git a/src/object/sp-paint-server-reference.h b/src/object/sp-paint-server-reference.h new file mode 100644 index 0000000..4f496ba --- /dev/null +++ b/src/object/sp-paint-server-reference.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_PAINT_SERVER_REFERENCE_H +#define SEEN_SP_PAINT_SERVER_REFERENCE_H + +/* + * Reference class for gradients and patterns. + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "uri-references.h" + +class SPDocument; +class SPObject; +class SPPaintServer; + +class SPPaintServerReference : public Inkscape::URIReference { +public: + SPPaintServerReference (SPObject *obj) : URIReference(obj) {} + SPPaintServerReference (SPDocument *doc) : URIReference(doc) {} + SPPaintServer *getObject() const; + +protected: + bool _acceptObject(SPObject *obj) const override; +}; + +#endif // SEEN_SP_PAINT_SERVER_REFERENCE_H +/* + 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 : diff --git a/src/object/sp-paint-server.cpp b/src/object/sp-paint-server.cpp new file mode 100644 index 0000000..b7a245f --- /dev/null +++ b/src/object/sp-paint-server.cpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Base class for gradients and patterns + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-paint-server-reference.h" +#include "sp-paint-server.h" +#include "display/drawing-paintserver.h" + +#include "sp-gradient.h" +#include "xml/node.h" + +SPPaintServer *SPPaintServerReference::getObject() const +{ + return static_cast<SPPaintServer *>(URIReference::getObject()); +} + +bool SPPaintServerReference::_acceptObject(SPObject *obj) const +{ + return is<SPPaintServer>(obj) && URIReference::_acceptObject(obj); +} + +SPPaintServer::SPPaintServer() = default; + +SPPaintServer::~SPPaintServer() = default; + +bool SPPaintServer::isSwatch() const +{ + return swatch; +} + +bool SPPaintServer::isValid() const +{ + return true; +} + +Inkscape::DrawingPattern *SPPaintServer::show(Inkscape::Drawing &/*drawing*/, unsigned /*key*/, Geom::OptRect const &/*bbox*/) +{ + return nullptr; +} + +void SPPaintServer::hide(unsigned key) +{ +} + +void SPPaintServer::setBBox(unsigned key, Geom::OptRect const &bbox) +{ +} + +std::unique_ptr<Inkscape::DrawingPaintServer> SPPaintServer::create_drawing_paintserver() +{ + return {}; +} + +/* + 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 : diff --git a/src/object/sp-paint-server.h b/src/object/sp-paint-server.h new file mode 100644 index 0000000..741ef94 --- /dev/null +++ b/src/object/sp-paint-server.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_PAINT_SERVER_H +#define SEEN_SP_PAINT_SERVER_H + +/* + * Base class for gradients and patterns + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <cairo.h> +#include <2geom/rect.h> +#include <sigc++/slot.h> +#include "sp-object.h" + +namespace Inkscape { +class Drawing; +class DrawingPattern; +class DrawingPaintServer; +} // namespace Inkscape + +class SPPaintServer + : public SPObject +{ +public: + SPPaintServer(); + ~SPPaintServer() override; + int tag() const override { return tag_of<decltype(*this)>; } + + bool isSwatch() const; + virtual bool isValid() const; + + /* + * There are two ways to implement a paint server: + * + * 1. Simple paint servers (solid colors and gradients) implement the create_drawing_paintserver() method. + * This returns a DrawingPaintServer instance holding a copy of the paint server's resources which is + * used to produce a pattern on-demand using create_pattern(). + * + * 2. The other paint servers (patterns and hatches) implement show(), hide() and setBBox(). + * The drawing item subtree returned by show() is attached as a fill/stroke child of the + * drawing item the paint server is applied to, and used directly when rendering. + * + * Paint servers only need to implement one method. If both are implemented, then option 2 is used. + */ + + virtual std::unique_ptr<Inkscape::DrawingPaintServer> create_drawing_paintserver(); + + virtual Inkscape::DrawingPattern *show(Inkscape::Drawing &drawing, unsigned key, Geom::OptRect const &bbox); + virtual void hide(unsigned key); + virtual void setBBox(unsigned key, Geom::OptRect const &bbox); + +protected: + bool swatch = false; +}; + +/** + * Returns the first of {src, src-\>ref-\>getObject(), + * src-\>ref-\>getObject()-\>ref-\>getObject(),...} + * for which \a match is true, or NULL if none found. + * + * The raison d'être of this routine is that it correctly handles cycles in the href chain (e.g., if + * a gradient gives itself as its href, or if each of two gradients gives the other as its href). + * + * \pre is<SPGradient>(src). + */ +template <class PaintServer> +PaintServer *chase_hrefs(PaintServer *src, sigc::slot<bool (PaintServer const *)> match) { + /* Use a pair of pointers for detecting loops: p1 advances half as fast as p2. If there is a + loop, then once p1 has entered the loop, we'll detect it the next time the distance between + p1 and p2 is a multiple of the loop size. */ + PaintServer *p1 = src, *p2 = src; + bool do1 = false; + for (;;) { + if (match(p2)) { + return p2; + } + + p2 = p2->ref->getObject(); + if (!p2) { + return p2; + } + if (do1) { + p1 = p1->ref->getObject(); + } + do1 = !do1; + + if ( p2 == p1 ) { + /* We've been here before, so return NULL to indicate that no matching gradient found + * in the chain. */ + return nullptr; + } + } +} + +#endif // SEEN_SP_PAINT_SERVER_H +/* + 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 : diff --git a/src/object/sp-path.cpp b/src/object/sp-path.cpp new file mode 100644 index 0000000..6c44576 --- /dev/null +++ b/src/object/sp-path.cpp @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <path> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * David Turner <novalis@gnu.org> + * Abhishek Sharma + * Johan Engelen + * + * Copyright (C) 2004 David Turner + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 1999-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "sp-lpe-item.h" + +#include "display/curve.h" +#include <2geom/curves.h> +#include "helper/geom-curves.h" + +#include "svg/svg.h" +#include "xml/repr.h" +#include "attributes.h" + +#include "sp-path.h" +#include "sp-guide.h" + +#include "document.h" +#include "desktop.h" + +#include "desktop-style.h" +#include "ui/tools/tool-base.h" +#include "inkscape.h" +#include "style.h" + +#define noPATH_VERBOSE + +gint SPPath::nodesInPath() const +{ + return _curve ? _curve->nodes_in_path() : 0; +} + +const char* SPPath::typeName() const { + return "path"; +} + +const char* SPPath::displayName() const { + return _("Path"); +} + +gchar* SPPath::description() const { + int count = this->nodesInPath(); + char *lpe_desc = g_strdup(""); + + if (hasPathEffect()) { + Glib::ustring s; + PathEffectList effect_list = this->getEffectList(); + + for (auto & it : effect_list) + { + LivePathEffectObject *lpeobj = it->lpeobject; + + if (!lpeobj || !lpeobj->get_lpe()) { + break; + } + + if (s.empty()) { + s = lpeobj->get_lpe()->getName(); + } else { + s = s + ", " + lpeobj->get_lpe()->getName(); + } + } + lpe_desc = g_strdup_printf(_(", path effect: %s"), s.c_str()); + } + char *ret = g_strdup_printf(ngettext( + "%i node%s", "%i nodes%s", count), count, lpe_desc); + g_free(lpe_desc); + return ret; +} + +void SPPath::convert_to_guides() const { + if (!this->_curve) { + return; + } + + std::list<std::pair<Geom::Point, Geom::Point> > pts; + + Geom::Affine const i2dt(this->i2dt_affine()); + Geom::PathVector const & pv = this->_curve->get_pathvector(); + + for(const auto & pit : pv) { + for(Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_default(); ++cit) { + // only add curves for straight line segments + if( is_straight_curve(*cit) ) + { + pts.emplace_back(cit->initialPoint() * i2dt, cit->finalPoint() * i2dt); + } + } + } + + sp_guide_pt_pairs_to_guides(this->document, pts); +} + +SPPath::SPPath() : SPShape(), connEndPair(this) { +} + +SPPath::~SPPath() = default; + +void SPPath::build(SPDocument *document, Inkscape::XML::Node *repr) { + /* Are these calls actually necessary? */ + this->readAttr(SPAttr::MARKER); + this->readAttr(SPAttr::MARKER_START); + this->readAttr(SPAttr::MARKER_MID); + this->readAttr(SPAttr::MARKER_END); + + sp_conn_end_pair_build(this); + + SPShape::build(document, repr); + // Our code depends on 'd' being an attribute (LPE's, etc.). To support 'd' as a property, we + // check it here (after the style property has been evaluated, this allows us to properly + // handled precedence of property vs attribute). If we read in a 'd' set by styling, convert it + // to an attribute. We'll convert it back on output. + + d_source = style->d.style_src; + + if (style->d.set && + + (d_source == SPStyleSrc::STYLE_PROP || d_source == SPStyleSrc::STYLE_SHEET) ) { + + if (char const *d_val = style->d.value()) { + // Chrome shipped with a different syntax for property vs attribute. + // The SVG Working group decided to follow the Chrome syntax (which may + // allow future extensions of the 'd' property). The property syntax + // wraps the path data with "path(...)". We must strip that! + + // Must be Glib::ustring or we get conversion errors! + Glib::ustring input = d_val; + Glib::ustring expression = R"A(path\("(.*)"\))A"; + Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create(expression); + Glib::MatchInfo matchInfo; + regex->match(input, matchInfo); + + if (matchInfo.matches()) { + Glib::ustring value = matchInfo.fetch(1); + + // Update curve + setCurveInsync(SPCurve(sp_svg_read_pathv(value.c_str()))); + + // Convert from property to attribute (convert back on write) + setAttributeOrRemoveIfEmpty("d", value); + + SPCSSAttr *css = sp_repr_css_attr( getRepr(), "style"); + sp_repr_css_unset_property ( css, "d"); + sp_repr_css_set ( getRepr(), css, "style" ); + sp_repr_css_attr_unref ( css ); + + style->d.style_src = SPStyleSrc::ATTRIBUTE; + } + } + // If any if statement is false, do nothing... don't overwrite 'd' from attribute + } + + this->readAttr(SPAttr::INKSCAPE_ORIGINAL_D); + this->readAttr(SPAttr::D); + + /* d is a required attribute */ + char const *d = this->getAttribute("d"); + + if (d == nullptr) { + // First see if calculating the path effect will generate "d": + this->update_patheffect(true); + d = this->getAttribute("d"); + + // I guess that didn't work, now we have nothing useful to write ("") + if (d == nullptr) { + this->setKeyValue( sp_attribute_lookup("d"), ""); + } + } +} + +void SPPath::release() { + this->connEndPair.release(); + + SPShape::release(); +} + +void SPPath::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::INKSCAPE_ORIGINAL_D: + if (value) { + setCurveBeforeLPE(SPCurve(sp_svg_read_pathv(value))); + } else { + setCurveBeforeLPE(nullptr); + } + break; + + case SPAttr::D: + if (value) { + setCurve(SPCurve(sp_svg_read_pathv(value))); + } else { + setCurve(nullptr); + } + break; + + case SPAttr::MARKER: + set_marker(SP_MARKER_LOC, value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::MARKER_START: + set_marker(SP_MARKER_LOC_START, value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::MARKER_MID: + set_marker(SP_MARKER_LOC_MID, value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::MARKER_END: + set_marker(SP_MARKER_LOC_END, value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::CONNECTOR_TYPE: + case SPAttr::CONNECTOR_CURVATURE: + case SPAttr::CONNECTION_START: + case SPAttr::CONNECTION_END: + case SPAttr::CONNECTION_START_POINT: + case SPAttr::CONNECTION_END_POINT: + this->connEndPair.setAttr(key, value); + break; + + default: + SPShape::set(key, value); + break; + } +} + +Inkscape::XML::Node* SPPath::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:path"); + } + +#ifdef PATH_VERBOSE +g_message("sp_path_write writes 'd' attribute"); +#endif + + if (this->_curve) { + repr->setAttribute("d", sp_svg_write_path(this->_curve->get_pathvector())); + } else { + repr->removeAttribute("d"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + if (_curve_before_lpe) { + repr->setAttribute("inkscape:original-d", sp_svg_write_path(_curve_before_lpe->get_pathvector())); + } else { + repr->removeAttribute("inkscape:original-d"); + } + } + + this->connEndPair.writeRepr(repr); + + SPShape::write(xml_doc, repr, flags); + + return repr; +} + +void SPPath::update_patheffect(bool write) { + SPShape::update_patheffect(write); +} + +void SPPath::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + flags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; // since we change the description, it's not a "just translation" anymore + } + + SPShape::update(ctx, flags); + this->connEndPair.update(); +} + +Geom::Affine SPPath::set_transform(Geom::Affine const &transform) { + if (!_curve) { // 0 nodes, nothing to transform + return Geom::identity(); + } + if (pathEffectsEnabled() && !optimizeTransforms()) { + return transform; + } + if (hasPathEffectRecursive() && pathEffectsEnabled()) { + if (!_curve_before_lpe) { + // we are inside a LPE group creating a new element + // and the original-d curve is not defined, + // This fix a issue with calligrapic tool that make a transform just when draw + setCurveBeforeLPE(_curve.get()); + } + _curve_before_lpe->transform(transform); + // fix issue https://gitlab.com/inkscape/inbox/-/issues/5460 + sp_lpe_item_update_patheffect(this, false, false); + } else { + setCurve(_curve->transformed(transform)); + } + // Adjust stroke + this->adjust_stroke(transform.descrim()); + + // Adjust pattern fill + this->adjust_pattern(transform); + + // Adjust gradient fill + this->adjust_gradient(transform); + + // nothing remains - we've written all of the transform, so return identity + return Geom::identity(); +} + +/* + 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 : diff --git a/src/object/sp-path.h b/src/object/sp-path.h new file mode 100644 index 0000000..2a37286 --- /dev/null +++ b/src/object/sp-path.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_PATH_H +#define SEEN_SP_PATH_H + +/* + * SVG <path> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ximian, Inc. + * Johan Engelen + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 1999-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-shape.h" +#include "sp-conn-end-pair.h" +#include "style-internal.h" // For SPStyleSrc + +class SPCurve; + +/** + * SVG <path> implementation + */ +class SPPath final : public SPShape { +public: + SPPath(); + ~SPPath() override; + int tag() const override { return tag_of<decltype(*this)>; } + + int nodesInPath() const; + friend class SPConnEndPair; + SPConnEndPair connEndPair; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + + void set(SPAttr key, char const* value) override; + void update_patheffect(bool write) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + const char* typeName() const override; + const char* displayName() const override; + char* description() const override; + Geom::Affine set_transform(Geom::Affine const &transform) override; + void convert_to_guides() const override; +private: + SPStyleSrc d_source; // Source of 'd' value, saved for output. +}; + +#endif // SEEN_SP_PATH_H + +/* + 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 : diff --git a/src/object/sp-pattern.cpp b/src/object/sp-pattern.cpp new file mode 100644 index 0000000..b5417aa --- /dev/null +++ b/src/object/sp-pattern.cpp @@ -0,0 +1,742 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <pattern> implementation + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-pattern.h" + +#include <string> +#include <cstring> + +#include <2geom/transforms.h> + +#include <glibmm.h> + +#include "attributes.h" +#include "bad-uri-exception.h" +#include "document.h" + +#include "sp-defs.h" +#include "sp-factory.h" +#include "sp-item.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-surface.h" +#include "display/drawing.h" +#include "display/drawing-group.h" +#include "display/drawing-pattern.h" + +#include "svg/svg.h" +#include "xml/href-attribute-helper.h" + +SPPatternReference::SPPatternReference(SPPattern *owner) + : URIReference(owner) +{ +} + +SPPattern *SPPatternReference::getObject() const +{ + return static_cast<SPPattern*>(URIReference::getObject()); +} + +bool SPPatternReference::_acceptObject(SPObject *obj) const +{ + return is<SPPattern>(obj) && URIReference::_acceptObject(obj); +} + +/* + * + */ + +SPPattern::SPPattern() + : ref(this) + , _pattern_units(UNITS_OBJECTBOUNDINGBOX) + , _pattern_units_set(false) + , _pattern_content_units(UNITS_USERSPACEONUSE) + , _pattern_content_units_set(false) + , _pattern_transform_set(false) + , shown(nullptr) +{ + ref.changedSignal().connect(sigc::mem_fun(*this, &SPPattern::_onRefChanged)); +} + +SPPattern::~SPPattern() = default; + +void SPPattern::build(SPDocument *doc, Inkscape::XML::Node *repr) +{ + SPPaintServer::build(doc, repr); + + readAttr(SPAttr::PATTERNUNITS); + readAttr(SPAttr::PATTERNCONTENTUNITS); + readAttr(SPAttr::PATTERNTRANSFORM); + readAttr(SPAttr::X); + readAttr(SPAttr::Y); + readAttr(SPAttr::WIDTH); + readAttr(SPAttr::HEIGHT); + readAttr(SPAttr::VIEWBOX); + readAttr(SPAttr::PRESERVEASPECTRATIO); + readAttr(SPAttr::XLINK_HREF); + readAttr(SPAttr::STYLE); + + doc->addResource("pattern", this); +} + +void SPPattern::release() +{ + if (document) { + document->removeResource("pattern", this); + } + + // Should have been unattached by their owners on the release signal. + assert(attached_views.empty()); + + set_shown(nullptr); + views.clear(); + + _modified_connection.disconnect(); + ref.detach(); + + SPPaintServer::release(); +} + +void SPPattern::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::PATTERNUNITS: + _pattern_units = UNITS_OBJECTBOUNDINGBOX; + _pattern_units_set = false; + + if (value) { + if (!std::strcmp(value, "userSpaceOnUse")) { + _pattern_units = UNITS_USERSPACEONUSE; + _pattern_units_set = true; + } else if (!std::strcmp(value, "objectBoundingBox")) { + _pattern_units_set = true; + } + } + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::PATTERNCONTENTUNITS: + _pattern_content_units = UNITS_USERSPACEONUSE; + _pattern_content_units_set = false; + + if (value) { + if (!std::strcmp(value, "userSpaceOnUse")) { + _pattern_content_units_set = true; + } else if (!std::strcmp(value, "objectBoundingBox")) { + _pattern_content_units = UNITS_OBJECTBOUNDINGBOX; + _pattern_content_units_set = true; + } + } + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::PATTERNTRANSFORM: { + _pattern_transform = Geom::identity(); + _pattern_transform_set = false; + + if (value) { + Geom::Affine t; + if (sp_svg_transform_read(value, &t)) { + _pattern_transform = t; + _pattern_transform_set = true; + } + } + + 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::WIDTH: + _width.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::HEIGHT: + _height.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::VIEWBOX: + set_viewBox(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::PRESERVEASPECTRATIO: + set_preserveAspectRatio(value); + requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::XLINK_HREF: + if (!value) { + if (href.empty()) { + break; + } + href.clear(); + ref.detach(); + } else { + if (href == value) { + break; + } + href = value; + + // Attempt to attach ref, which emits the changed signal. + try { + ref.attach(Inkscape::URI(href.data())); + } catch (Inkscape::BadURIException const &e) { + g_warning("%s", e.what()); + href.clear(); + ref.detach(); + } + } + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPPaintServer::set(key, value); + break; + } +} + +void SPPattern::update(SPCtx *ctx, unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + for (auto c : childList(true)) { + if (cflags || (c->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + c->updateDisplay(ctx, cflags); + } + sp_object_unref(c, nullptr); + } + + for (auto &v : views) { + update_view(v); + } +} + +void SPPattern::update_view(View &v) +{ + // * "width" and "height" determine tile size. + // * "viewBox" (if defined) or "patternContentUnits" determines placement of content inside tile. + // * "x", "y", and "patternTransform" transform tile to user space after tile is generated. + + // These functions recursively search up the tree to find the values. + double tile_x = x(); + double tile_y = y(); + double tile_width = width(); + double tile_height = height(); + if (v.bbox && patternUnits() == UNITS_OBJECTBOUNDINGBOX) { + tile_x *= v.bbox->width(); + tile_y *= v.bbox->height(); + tile_width *= v.bbox->width(); + tile_height *= v.bbox->height(); + } + + // Pattern size in pattern space + auto pattern_tile = Geom::Rect::from_xywh(0, 0, tile_width, tile_height); + + // Content to tile (pattern space) + Geom::Affine content2ps; + if (auto effective_view_box = viewbox()) { + // viewBox to pattern server (using SPViewBox) + viewBox = *effective_view_box; + c2p.setIdentity(); + apply_viewbox(pattern_tile); + content2ps = c2p; + } + else { + // Content to bbox + if (v.bbox && patternContentUnits() == UNITS_OBJECTBOUNDINGBOX) { + content2ps = Geom::Affine(v.bbox->width(), 0.0, 0.0, v.bbox->height(), 0, 0); + } + } + + // Tile (pattern space) to user. + Geom::Affine ps2user = Geom::Translate(tile_x, tile_y) * getTransform(); + + v.drawingitem->setTileRect(pattern_tile); + v.drawingitem->setChildTransform(content2ps); + v.drawingitem->setPatternToUserTransform(ps2user); +} + +void SPPattern::modified(unsigned flags) +{ + auto const cflags = cascade_flags(flags); + + for (auto c : childList(true)) { + if (cflags || (c->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + c->emitModified(cflags); + } + sp_object_unref(c); + } + + set_shown(rootPattern()); +} + +// The following three functions are based on SPGroup. + +void SPPattern::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + SPPaintServer::child_added(child, ref); + + auto last_child = lastChild(); + if (last_child && last_child->getRepr() == child) { + if (auto item = cast<SPItem>(last_child)) { + for (auto &v : attached_views) { + auto ac = item->invoke_show(v.drawingitem->drawing(), v.key, SP_ITEM_SHOW_DISPLAY); + if (ac) { + v.drawingitem->appendChild(ac); + } + } + } + } else { + if (auto item = cast<SPItem>(get_child_by_repr(child))) { + unsigned position = item->pos_in_parent(); + for (auto &v : attached_views) { + auto ac = item->invoke_show(v.drawingitem->drawing(), v.key, SP_ITEM_SHOW_DISPLAY); + if (ac) { + v.drawingitem->prependChild(ac); + ac->setZOrder(position); + } + } + } + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPPattern::remove_child(Inkscape::XML::Node *child) +{ + SPPaintServer::remove_child(child); + // no need to do anything as child will automatically remove itself + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPPattern::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_prev, Inkscape::XML::Node *new_prev) +{ + SPPaintServer::order_changed(child, old_prev, new_prev); + + if (auto item = cast<SPItem>(get_child_by_repr(child))) { + unsigned position = item->pos_in_parent(); + for (auto &v : attached_views) { + auto ac = item->get_arenaitem(v.key); + ac->setZOrder(position); + } + } + + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPPattern::_onRefChanged(SPObject *old_ref, SPObject *ref) +{ + if (old_ref) { + _modified_connection.disconnect(); + } + + if (is<SPPattern>(ref)) { + _modified_connection = ref->connectModified(sigc::mem_fun(*this, &SPPattern::_onRefModified)); + } + + _onRefModified(ref, 0); +} + +void SPPattern::_onRefModified(SPObject */*ref*/, unsigned /*flags*/) +{ + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPPattern::set_shown(SPPattern *new_shown) +{ + if (shown == new_shown) { + return; + } + + if (shown) { + for (auto &v : views) { + shown->unattach_view(v.drawingitem.get()); + } + + shown_released_connection.disconnect(); + } + + shown = new_shown; + + if (shown) { + for (auto &v : views) { + shown->attach_view(v.drawingitem.get(), v.key); + } + + shown_released_connection = shown->connectRelease([this] (auto) { + set_shown(nullptr); + }); + } +} + +void SPPattern::attach_view(Inkscape::DrawingPattern *di, unsigned key) +{ + attached_views.push_back({di, key}); + + for (auto &c : children) { + if (auto child = cast<SPItem>(&c)) { + auto item = child->invoke_show(di->drawing(), key, SP_ITEM_SHOW_DISPLAY); + di->appendChild(item); + } + } +} + +void SPPattern::unattach_view(Inkscape::DrawingPattern *di) +{ + auto it = std::find_if(attached_views.begin(), attached_views.end(), [di] (auto const &v) { + return v.drawingitem == di; + }); + assert(it != attached_views.end()); + + for (auto &c : children) { + if (auto child = cast<SPItem>(&c)) { + child->invoke_hide(it->key); + } + } + + attached_views.erase(it); +} + +unsigned SPPattern::_countHrefs(SPObject *o) const +{ + if (!o) + return 1; + + guint i = 0; + + SPStyle *style = o->style; + if (style && style->fill.isPaintserver() && is<SPPattern>(SP_STYLE_FILL_SERVER(style)) && + cast<SPPattern>(SP_STYLE_FILL_SERVER(style)) == this) { + i++; + } + if (style && style->stroke.isPaintserver() && is<SPPattern>(SP_STYLE_STROKE_SERVER(style)) && + cast<SPPattern>(SP_STYLE_STROKE_SERVER(style)) == this) { + i++; + } + + for (auto& child: o->children) { + i += _countHrefs(&child); + } + + return i; +} + +SPPattern *SPPattern::_chain() const +{ + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:pattern"); + repr->setAttribute("inkscape:collect", "always"); + Glib::ustring parent_ref = Glib::ustring::compose("#%1", getRepr()->attribute("id")); + Inkscape::setHrefAttribute(*repr, parent_ref); + // this attribute is used to express uniform pattern scaling in pattern editor, so keep it + repr->setAttribute("preserveAspectRatio", getRepr()->attribute("preserveAspectRatio")); + + defsrepr->addChild(repr, nullptr); + SPObject *child = document->getObjectByRepr(repr); + assert(child == document->getObjectById(repr->attribute("id"))); + g_assert(is<SPPattern>(child)); + + return cast<SPPattern>(child); +} + +SPPattern *SPPattern::clone_if_necessary(SPItem *item, const gchar *property) +{ + SPPattern *pattern = this; + if (pattern->href.empty() || pattern->hrefcount > _countHrefs(item)) { + pattern = _chain(); + Glib::ustring href = Glib::ustring::compose("url(#%1)", pattern->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 pattern; +} + +// do not remove identity transform in pattern elements; when patterns are referenced then linking +// pattern transform overrides root/referenced pattern transform; if it disappears then root transform +// takes over and that's not what we want +static std::string write_transform(const Geom::Affine& transform) { + if (transform.isIdentity()) { + return "scale(1)"; + } + return sp_svg_transform_write(transform); +} + +void SPPattern::transform_multiply(Geom::Affine postmul, bool set) +{ + // this formula is for a different interpretation of pattern transforms as described in (*) in sp-pattern.cpp + // for it to work, we also need sp_object_read_attr( item, "transform"); + // pattern->patternTransform = premul * item->transform * pattern->patternTransform * item->transform.inverse() * + // postmul; + + // otherwise the formula is much simpler + if (set) { + _pattern_transform = postmul; + } + else { + _pattern_transform = getTransform() * postmul; + } + _pattern_transform_set = true; + + setAttributeOrRemoveIfEmpty("patternTransform", write_transform(_pattern_transform)); +} + +char const *SPPattern::produce(std::vector<Inkscape::XML::Node*> const &reprs, Geom::Rect const &bounds, + SPDocument *document, Geom::Affine const &transform, Geom::Affine const &move) +{ + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:pattern"); + repr->setAttribute("patternUnits", "userSpaceOnUse"); + repr->setAttributeSvgDouble("width", bounds.dimensions()[Geom::X]); + repr->setAttributeSvgDouble("height", bounds.dimensions()[Geom::Y]); + repr->setAttributeOrRemoveIfEmpty("patternTransform", write_transform(transform)); + // by default use uniform scaling + repr->setAttribute("preserveAspectRatio", "xMidYMid"); + defsrepr->appendChild(repr); + const gchar *pd = repr->attribute("id"); + SPObject *pat_object = document->getObjectById(pd); + bool can_colorize = false; + + for (auto node : reprs) { + auto copy = cast<SPItem>(pat_object->appendChildRepr(node)); + + if (!repr->attribute("inkscape:label") && node->attribute("inkscape:label")) { + repr->setAttribute("inkscape:label", node->attribute("inkscape:label")); + } + + // if some elements have undefined color or solid black, then their fill color is customizable + if (copy->style && copy->style->isSet(SPAttr::FILL)) { + if (auto paint = copy->style->getFillOrStroke(true)) { + if (paint->isColor() && paint->value.color.toRGBA32(255) == 255) { // black color set? + can_colorize = true; + // remove black fill, it will be inherited from pattern + paint->clear(); + } + } + } + else { + // no color - it will be inherited + can_colorize = true; + } + + Geom::Affine dup_transform; + if (!sp_svg_transform_read(node->attribute("transform"), &dup_transform)) + dup_transform = Geom::identity(); + dup_transform *= move; + + copy->doWriteTransform(dup_transform, nullptr, false); + } + + if (can_colorize && pat_object->style) { + // add black fill style to the pattern object - it will tell pattern editor to enable color selector + pat_object->style->readIfUnset(SPAttr::FILL, "black"); + } + + Inkscape::GC::release(repr); + return pd; +} + +SPPattern const *SPPattern::rootPattern() const +{ + for (auto p = this; p; p = p->ref.getObject()) { + if (p->firstChild()) { // find the first one with children + return p; + } + } + return this; // document is broken, we can't get to root; but at least we can return ourself which is supposedly a valid pattern +} + +SPPattern *SPPattern::rootPattern() +{ + return const_cast<SPPattern*>(std::as_const(*this).rootPattern()); +} + +// Access functions that look up fields up the chain of referenced patterns and return the first one which is set + +SPPattern::PatternUnits SPPattern::patternUnits() const +{ + for (auto p = this; p; p = p->ref.getObject()) { + if (p->_pattern_units_set) + return p->_pattern_units; + } + return _pattern_units; +} + +SPPattern::PatternUnits SPPattern::patternContentUnits() const +{ + for (auto p = this; p; p = p->ref.getObject()) { + if (p->_pattern_content_units_set) + return p->_pattern_content_units; + } + return _pattern_content_units; +} + +Geom::Affine const &SPPattern::getTransform() const +{ + for (auto p = this; p; p = p->ref.getObject()) { + if (p->_pattern_transform_set) + return p->_pattern_transform; + } + return _pattern_transform; +} + +const Geom::Affine& SPPattern::get_this_transform() const { + return _pattern_transform; +} + +double SPPattern::x() const +{ + for (auto p = this; p; p = p->ref.getObject()) { + if (p->_x._set) + return p->_x.computed; + } + return 0; +} + +double SPPattern::y() const +{ + for (auto p = this; p; p = p->ref.getObject()) { + if (p->_y._set) + return p->_y.computed; + } + return 0; +} + +double SPPattern::width() const +{ + for (auto p = this; p; p = p->ref.getObject()) { + if (p->_width._set) + return p->_width.computed; + } + return 0; +} + +double SPPattern::height() const +{ + for (auto p = this; p; p = p->ref.getObject()) { + if (p->_height._set) + return p->_height.computed; + } + return 0; +} + +Geom::OptRect SPPattern::viewbox() const +{ + Geom::OptRect viewbox; + for (auto p = this; p; p = p->ref.getObject()) { + if (p->viewBox_set) { + viewbox = p->viewBox; + break; + } + } + return viewbox; +} + +bool SPPattern::_hasItemChildren() const +{ + for (auto &child : children) { + if (is<SPItem>(&child)) { + return true; + } + } + + return false; +} + +bool SPPattern::isValid() const +{ + return width() > 0 && height() > 0; +} + +Inkscape::DrawingPattern *SPPattern::show(Inkscape::Drawing &drawing, unsigned key, Geom::OptRect const &bbox) +{ + views.emplace_back(make_drawingitem<Inkscape::DrawingPattern>(drawing), bbox, key); + auto &v = views.back(); + auto root = v.drawingitem.get(); + + if (shown) { + shown->attach_view(root, key); + } + + root->setStyle(style); + + update_view(v); + + return root; +} + +void SPPattern::hide(unsigned key) +{ + auto it = std::find_if(views.begin(), views.end(), [=] (auto &v) { + return v.key == key; + }); + + if (it == views.end()) { + return; + } + + if (shown) { + shown->unattach_view(it->drawingitem.get()); + } + + views.erase(it); +} + +void SPPattern::setBBox(unsigned key, Geom::OptRect const &bbox) +{ + auto it = std::find_if(views.begin(), views.end(), [=] (auto &v) { + return v.key == key; + }); + assert(it != views.end()); + auto &v = *it; + + v.bbox = bbox; + update_view(v); +} + +SPPattern::View::View(DrawingItemPtr<Inkscape::DrawingPattern> 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 : diff --git a/src/object/sp-pattern.h b/src/object/sp-pattern.h new file mode 100644 index 0000000..e7b36a0 --- /dev/null +++ b/src/object/sp-pattern.h @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SVG <pattern> implementation + *//* + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_PATTERN_H +#define SEEN_SP_PATTERN_H + +#include <memory> +#include <vector> +#include <cstddef> +#include <glibmm/ustring.h> +#include <sigc++/connection.h> + +#include "svg/svg-length.h" +#include "sp-paint-server.h" +#include "uri-references.h" +#include "viewbox.h" +#include "display/drawing-item-ptr.h" + +class SPPattern; +class SPItem; + +namespace Inkscape { +namespace XML { +class Node; +} // namespace XML +} // namespace Inkscape + +class SPPatternReference + : public Inkscape::URIReference +{ +public: + SPPatternReference(SPPattern *owner); + SPPattern *getObject() const; + +protected: + bool _acceptObject(SPObject *obj) const override; +}; + +class SPPattern final + : public SPPaintServer + , public SPViewBox +{ +public: + enum PatternUnits + { + UNITS_USERSPACEONUSE, + UNITS_OBJECTBOUNDINGBOX + }; + + SPPattern(); + ~SPPattern() override; + int tag() const override { return tag_of<decltype(*this)>; } + + // Reference (href) + Glib::ustring href; + SPPatternReference ref; + + double x() const; + double y() const; + double width() const; + double height() const; + Geom::OptRect viewbox() const; + SPPattern::PatternUnits patternUnits() const; + SPPattern::PatternUnits patternContentUnits() const; + Geom::Affine const &getTransform() const; + SPPattern const *rootPattern() const; + SPPattern *rootPattern(); + const Geom::Affine& get_this_transform() const; + + SPPattern *clone_if_necessary(SPItem *item, char const *property); + void transform_multiply(Geom::Affine postmul, bool set); + + /** + * @brief create a new pattern in XML tree + * @return created pattern id + */ + static char const *produce(std::vector<Inkscape::XML::Node*> const &reprs, Geom::Rect const &bounds, + SPDocument *document, Geom::Affine const &transform, Geom::Affine const &move); + + bool isValid() const override; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const *value) override; + void update(SPCtx *ctx, unsigned flags) override; + void modified(unsigned flags) override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + void order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) override; + + Inkscape::DrawingPattern *show(Inkscape::Drawing &drawing, unsigned key, Geom::OptRect const &bbox) override; + void hide(unsigned key) override; + void setBBox(unsigned key, Geom::OptRect const &bbox) override; + +private: + bool _hasItemChildren() const; + + SPPattern *_chain() const; + + /** + * Count how many times pattern is used by the styles of o and its descendants + */ + unsigned _countHrefs(SPObject *o) const; + + /** + * Gets called when the pattern is reattached to another <pattern> + */ + void _onRefChanged(SPObject *old_ref, SPObject *ref); + + /** + * Gets called when the referenced <pattern> is changed + */ + void _onRefModified(SPObject *ref, unsigned flags); + + /* patternUnits and patternContentUnits attribute */ + PatternUnits _pattern_units : 1; + bool _pattern_units_set : 1; + PatternUnits _pattern_content_units : 1; + bool _pattern_content_units_set : 1; + /* patternTransform attribute */ + Geom::Affine _pattern_transform; + bool _pattern_transform_set : 1; + /* Tile rectangle */ + SVGLength _x; + SVGLength _y; + SVGLength _width; + SVGLength _height; + + sigc::connection _modified_connection; + + /** + * The pattern at the end of the href chain, currently tasked with keeping our DrawingPattern + * up to date. When 'shown' is deleted, our DrawingPattern will be unattached from it and 'shown' + * will be nulled. Later (asynchronously), 'shown' will be re-resolved to another Pattern and our + * DrawingPattern will be re-attached to that. + */ + SPPattern *shown; + sigc::connection shown_released_connection; + void set_shown(SPPattern *new_shown); + + /** + * Drawing items belonging to other patterns with this pattern at the end of their href chain. + * They will be updated in sync with this pattern's children. + */ + struct AttachedView + { + Inkscape::DrawingPattern *drawingitem; + unsigned key; + }; + std::vector<AttachedView> attached_views; + void attach_view(Inkscape::DrawingPattern *di, unsigned key); + void unattach_view(Inkscape::DrawingPattern *di); + + struct View + { + DrawingItemPtr<Inkscape::DrawingPattern> drawingitem; + Geom::OptRect bbox; + unsigned key; + View(DrawingItemPtr<Inkscape::DrawingPattern> drawingitem, Geom::OptRect const &bbox, unsigned key); + }; + std::vector<View> views; + void update_view(View &v); +}; + +#endif // SEEN_SP_PATTERN_H + +/* + 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 : diff --git a/src/object/sp-polygon.cpp b/src/object/sp-polygon.cpp new file mode 100644 index 0000000..3f07bbc --- /dev/null +++ b/src/object/sp-polygon.cpp @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <polygon> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "sp-polygon.h" +#include "display/curve.h" +#include <glibmm/i18n.h> +#include <2geom/curves.h> +#include "helper/geom-curves.h" +#include "svg/stringstream.h" +#include "xml/repr.h" +#include "document.h" + +SPPolygon::SPPolygon() : SPShape() { +} + +SPPolygon::~SPPolygon() = default; + +void SPPolygon::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPPolygon* object = this; + + SPShape::build(document, repr); + + object->readAttr(SPAttr::POINTS); +} + +/* + * sp_svg_write_polygon: Write points attribute for polygon tag. + * pathv may only contain paths with only straight line segments + * Return value: points attribute string. + */ +static gchar *sp_svg_write_polygon(Geom::PathVector const & pathv) +{ + Inkscape::SVGOStringStream os; + + for (const auto & pit : pathv) { + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_default(); ++cit) { + if ( is_straight_curve(*cit) ) + { + os << cit->finalPoint()[0] << "," << cit->finalPoint()[1] << " "; + } else { + g_error("sp_svg_write_polygon: polygon path contains non-straight line segments"); + } + } + } + + return g_strdup(os.str().c_str()); +} + +Inkscape::XML::Node* SPPolygon::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + // Tolerable workaround: we need to update the object's curve before we set points= + // because it's out of sync when e.g. some extension attrs of the polygon or star are changed in XML editor + this->set_shape(); + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:polygon"); + } + + /* We can safely write points here, because all subclasses require it too (Lauris) */ + /* While saving polygon element without points attribute _curve is NULL (see bug 1202753) */ + if (this->_curve != nullptr) { + gchar *str = sp_svg_write_polygon(this->_curve->get_pathvector()); + repr->setAttribute("points", str); + g_free(str); + } + + SPShape::write(xml_doc, repr, flags); + + return repr; +} + +/** + * @brief Parse a double from the string passed by pointer and advance the string start. + * + * @param[in,out] p A pointer to a string (representing a piece of the `points` attribute). + * @param[out] v The parsed value. + * @return Parse status. + */ +SPPolyParseError sp_poly_get_value(char const **p, double *v) +{ + while (**p != '\0' && (**p == ',' || **p == '\x20' || **p == '\x9' || **p == '\xD' || **p == '\xA')) { + (*p)++; + } + + if (**p == '\0') { + return POLY_END_OF_STRING; + } + + gchar *e = nullptr; + double value = g_ascii_strtod(*p, &e); + if (e == *p) { + return POLY_INVALID_NUMBER; + } + if (std::isnan(value)) { + return POLY_NOT_A_NUMBER; + } + if (std::isinf(value)) { + return POLY_INFINITE_VALUE; + } + + *p = e; + *v = value; + return POLY_OK; +} + +/** + * @brief Print a warning message related to the parsing of a 'points' attribute. + */ +static void sp_poly_print_warning(char const *points, char const *error_location, SPPolyParseError error) +{ + switch (error) { + case POLY_END_OF_STRING: // Unexpected end of string! + { + size_t constexpr MAX_DISPLAY_SIZE = 64; + Glib::ustring s{points}; + if (s.size() > MAX_DISPLAY_SIZE) { + s = "... " + s.substr(s.size() - MAX_DISPLAY_SIZE); + } + g_warning("Error parsing a 'points' attribute: string ended unexpectedly!\n\t\"%s\"", s.c_str()); + break; + } + case POLY_INVALID_NUMBER: + g_warning("Invalid number in the 'points' attribute:\n\t\"(...) %s\"", error_location); + break; + + case POLY_INFINITE_VALUE: + g_warning("Infinity is not allowed in the 'points' attribute:\n\t\"(...) %s\"", error_location); + break; + + case POLY_NOT_A_NUMBER: + g_warning("NaN-value is not allowed in the 'points' attribute:\n\t\"(...) %s\"", error_location); + break; + + case POLY_OK: + default: + break; + } +} + +/** + * @brief Parse a 'points' attribute, printing a warning when an error occurs. + * + * @param points The points attribute. + * @return The corresponding polyline curve (open). + */ +SPCurve sp_poly_parse_curve(char const *points) +{ + SPCurve result; + char const *cptr = points; + bool has_pt = false; + + while (true) { + double x, y; + + if (auto error = sp_poly_get_value(&cptr, &x)) { + // If the error is something other than end of input, we must report it. + // End of input is allowed when scanning for the next x coordinate: it + // simply means that we have reached the end of the coordinate list. + if (error != POLY_END_OF_STRING) { + sp_poly_print_warning(points, cptr, error); + } + break; + } + if (auto error = sp_poly_get_value(&cptr, &y)) { + // End of input is not allowed when scanning for y. + sp_poly_print_warning(points, cptr, error); + break; + } + + if (has_pt) { + result.lineto(x, y); + } else { + result.moveto(x, y); + has_pt = true; + } + } + return result; +} + +void SPPolygon::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::POINTS: { + if (!value) { + /* fixme: The points attribute is required. We should handle its absence as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. */ + break; + } + + auto curve = sp_poly_parse_curve(value); + curve.closepath(); + setCurve(std::move(curve)); + break; + } + default: + SPShape::set(key, value); + break; + } +} + +const char* SPPolygon::typeName() const { + return "path"; +} + +gchar* SPPolygon::description() const { + return g_strdup(_("<b>Polygon</b>")); +} + +/* + 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 : diff --git a/src/object/sp-polygon.h b/src/object/sp-polygon.h new file mode 100644 index 0000000..640e05c --- /dev/null +++ b/src/object/sp-polygon.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_POLYGON_H +#define SEEN_SP_POLYGON_H + +/* + * SVG <polygon> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-shape.h" + +class SPPolygon : public SPShape +{ +public: + SPPolygon(); + ~SPPolygon() override; + int tag() const override { return tag_of<decltype(*this)>; } + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + Inkscape::XML::Node *write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void set(SPAttr key, char const *value) override; + char const *typeName() const override; + char *description() const override; +}; + +// Functionality shared with SPPolyline +enum SPPolyParseError : uint8_t +{ + POLY_OK = 0, + POLY_END_OF_STRING, + POLY_INVALID_NUMBER, + POLY_INFINITE_VALUE, + POLY_NOT_A_NUMBER +}; +SPPolyParseError sp_poly_get_value(char const **p, double *v); +SPCurve sp_poly_parse_curve(char const *points); + +#endif diff --git a/src/object/sp-polyline.cpp b/src/object/sp-polyline.cpp new file mode 100644 index 0000000..75ac206 --- /dev/null +++ b/src/object/sp-polyline.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <polyline> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "sp-polygon.h" +#include "sp-polyline.h" +#include "display/curve.h" +#include <glibmm/i18n.h> +#include "xml/repr.h" +#include "document.h" + +SPPolyLine::SPPolyLine() : SPShape() { +} + +SPPolyLine::~SPPolyLine() = default; + +void SPPolyLine::build(SPDocument * document, Inkscape::XML::Node * repr) { + SPShape::build(document, repr); + + this->readAttr(SPAttr::POINTS); +} + +void SPPolyLine::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::POINTS: + if (value) { + setCurve(sp_poly_parse_curve(value)); + } + break; + default: + SPShape::set(key, value); + break; + } +} + +Inkscape::XML::Node* SPPolyLine::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:polyline"); + } + + if (repr != this->getRepr()) { + repr->mergeFrom(this->getRepr(), "id"); + } + + SPShape::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPPolyLine::typeName() const { + return "path"; +} + +gchar* SPPolyLine::description() const { + return g_strdup(_("<b>Polyline</b>")); +} + + +/* + 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 : diff --git a/src/object/sp-polyline.h b/src/object/sp-polyline.h new file mode 100644 index 0000000..3d8604c --- /dev/null +++ b/src/object/sp-polyline.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_POLYLINE_H +#define SEEN_SP_POLYLINE_H + +#include "sp-shape.h" + +class SPPolyLine final : public SPShape { +public: + SPPolyLine(); + ~SPPolyLine() override; + int tag() const override { return tag_of<decltype(*this)>; } + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttr key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + const char* typeName() const override; + char* description() const override; +}; + +#endif // SEEN_SP_POLYLINE_H + +/* + 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 : diff --git a/src/object/sp-radial-gradient.cpp b/src/object/sp-radial-gradient.cpp new file mode 100644 index 0000000..b8b4885 --- /dev/null +++ b/src/object/sp-radial-gradient.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cairo.h> +#include <2geom/transforms.h> + +#include "sp-radial-gradient.h" + +#include "attributes.h" +#include "style.h" +#include "xml/repr.h" + +#include "display/drawing-paintserver.h" + +/* + * Radial Gradient + */ +SPRadialGradient::SPRadialGradient() : SPGradient() { + this->cx.unset(SVGLength::PERCENT, 0.5, 0.5); + this->cy.unset(SVGLength::PERCENT, 0.5, 0.5); + this->r.unset(SVGLength::PERCENT, 0.5, 0.5); + this->fx.unset(SVGLength::PERCENT, 0.5, 0.5); + this->fy.unset(SVGLength::PERCENT, 0.5, 0.5); + this->fr.unset(SVGLength::PERCENT, 0.5, 0.5); +} + +SPRadialGradient::~SPRadialGradient() = default; + +/** + * Set radial gradient attributes from associated repr. + */ +void SPRadialGradient::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPGradient::build(document, repr); + + this->readAttr(SPAttr::CX); + this->readAttr(SPAttr::CY); + this->readAttr(SPAttr::R); + this->readAttr(SPAttr::FX); + this->readAttr(SPAttr::FY); + this->readAttr(SPAttr::FR); +} + +/** + * Set radial gradient attribute. + */ +void SPRadialGradient::set(SPAttr key, gchar const *value) { + + switch (key) { + case SPAttr::CX: + if (!this->cx.read(value)) { + this->cx.unset(SVGLength::PERCENT, 0.5, 0.5); + } + + if (!this->fx._set) { + this->fx.value = this->cx.value; + this->fx.computed = this->cx.computed; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::CY: + if (!this->cy.read(value)) { + this->cy.unset(SVGLength::PERCENT, 0.5, 0.5); + } + + if (!this->fy._set) { + this->fy.value = this->cy.value; + this->fy.computed = this->cy.computed; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::R: + if (!this->r.read(value)) { + this->r.unset(SVGLength::PERCENT, 0.5, 0.5); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::FX: + if (!this->fx.read(value)) { + this->fx.unset(this->cx.unit, this->cx.value, this->cx.computed); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::FY: + if (!this->fy.read(value)) { + this->fy.unset(this->cy.unit, this->cy.value, this->cy.computed); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::FR: + if (!this->fr.read(value)) { + this->fr.unset(SVGLength::PERCENT, 0.0, 0.0); + } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPGradient::set(key, value); + break; + } +} + +void +SPRadialGradient::update(SPCtx *ctx, guint flags) +{ + // To do: Verify flags. + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx); + + if (getUnits() == SP_GRADIENT_UNITS_USERSPACEONUSE) { + double w = ictx->viewport.width(); + double h = ictx->viewport.height(); + double d = sqrt ((w*w + h*h)/2.0); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + this->cx.update(em, ex, w); + this->cy.update(em, ex, h); + this->r.update(em, ex, d); + this->fx.update(em, ex, w); + this->fy.update(em, ex, h); + this->fr.update(em, ex, d); + } + } +} + +/** + * Write radial gradient attributes to associated repr. + */ +Inkscape::XML::Node* SPRadialGradient::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:radialGradient"); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->cx._set) { + repr->setAttributeSvgDouble("cx", this->cx.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->cy._set) { + repr->setAttributeSvgDouble("cy", this->cy.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->r._set) { + repr->setAttributeSvgDouble("r", this->r.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->fx._set) { + repr->setAttributeSvgDouble("fx", this->fx.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->fy._set) { + repr->setAttributeSvgDouble("fy", this->fy.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->fr._set) { + repr->setAttributeSvgDouble("fr", this->fr.computed); + } + + SPGradient::write(xml_doc, repr, flags); + + return repr; +} + +std::unique_ptr<Inkscape::DrawingPaintServer> SPRadialGradient::create_drawing_paintserver() +{ + ensureVector(); + return std::make_unique<Inkscape::DrawingRadialGradient>(getSpread(), getUnits(), gradientTransform, + fx.computed, fy.computed, cx.computed, cy.computed, r.computed, fr.computed, vector.stops); +} + +/* + 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 : diff --git a/src/object/sp-radial-gradient.h b/src/object/sp-radial-gradient.h new file mode 100644 index 0000000..16e007f --- /dev/null +++ b/src/object/sp-radial-gradient.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_RADIAL_GRADIENT_H +#define SP_RADIAL_GRADIENT_H + +/** \file + * SPRadialGradient: SVG <radialgradient> implementtion. + */ + +#include "sp-gradient.h" +#include "svg/svg-length.h" + +typedef struct _cairo cairo_t; +typedef struct _cairo_pattern cairo_pattern_t; + +/** Radial gradient. */ +class SPRadialGradient final : public SPGradient { +public: + SPRadialGradient(); + ~SPRadialGradient() override; + int tag() const override { return tag_of<decltype(*this)>; } + + SVGLength cx; + SVGLength cy; + SVGLength r; + SVGLength fx; + SVGLength fy; + SVGLength fr; // Focus radius. Added in SVG 2 + + std::unique_ptr<Inkscape::DrawingPaintServer> create_drawing_paintserver() override; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + void update(SPCtx *ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif /* !SP_RADIAL_GRADIENT_H */ + +/* + 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 : diff --git a/src/object/sp-rect.cpp b/src/object/sp-rect.cpp new file mode 100644 index 0000000..86cc77e --- /dev/null +++ b/src/object/sp-rect.cpp @@ -0,0 +1,658 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <rect> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/curve.h" + +#include "inkscape.h" +#include "document.h" +#include "attributes.h" +#include "style.h" +#include "sp-rect.h" +#include "sp-guide.h" +#include "preferences.h" +#include "svg/svg.h" +#include "snap-candidate.h" +#include "snap-preferences.h" +#include <glibmm/i18n.h> + +#define noRECT_VERBOSE + +//#define OBJECT_TRACE + +SPRect::SPRect() : SPShape() + ,type(SP_GENERIC_RECT_UNDEFINED) +{ +} + +SPRect::~SPRect() = default; + +/* +* Ellipse and rects are the only SP object who's repr element tag name changes +* during it's lifetime. During undo and redo these changes can cause +* the SP object to become unstuck from the repr's true state. +*/ +void SPRect::tag_name_changed(gchar const* oldname, gchar const* newname) +{ + const std::string typeString = newname; + if (typeString == "svg:rect") { + type = SP_GENERIC_RECT; + } else if (typeString == "svg:path") { + type = SP_GENERIC_PATH; + } +} + +void SPRect::build(SPDocument* doc, Inkscape::XML::Node* repr) { +#ifdef OBJECT_TRACE + objectTrace( "SPRect::build" ); +#endif + + SPShape::build(doc, repr); + + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + this->readAttr(SPAttr::WIDTH); + this->readAttr(SPAttr::HEIGHT); + this->readAttr(SPAttr::RX); + this->readAttr(SPAttr::RY); + +#ifdef OBJECT_TRACE + objectTrace( "SPRect::build", false ); +#endif +} + +void SPRect::set(SPAttr key, gchar const *value) { + +#ifdef OBJECT_TRACE + std::stringstream temp; + temp << "SPRect::set: " << sp_attribute_name(key) << " " << (value?value:"null"); + objectTrace( temp.str() ); +#endif + + /* fixme: We need real error processing some time */ + + // We must update the SVGLengths immediately or nodes may be misplaced after they are moved. + double const w = viewport.width(); + double const h = viewport.height(); + double const em = style->font_size.computed; + double const ex = em * 0.5; + + switch (key) { + case SPAttr::X: + this->x.readOrUnset(value); + this->x.update( em, ex, w ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::Y: + this->y.readOrUnset(value); + this->y.update( em, ex, h ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::WIDTH: + if (!this->width.read(value) || this->width.value < 0.0) { + this->width.unset(); + } + this->width.update( em, ex, w ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::HEIGHT: + if (!this->height.read(value) || this->height.value < 0.0) { + this->height.unset(); + } + this->height.update( em, ex, h ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::RX: + if (!this->rx.read(value) || this->rx.value <= 0.0) { + this->rx.unset(); + } + this->rx.update( em, ex, w ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::RY: + if (!this->ry.read(value) || this->ry.value <= 0.0) { + this->ry.unset(); + } + this->ry.update( em, ex, h ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPShape::set(key, value); + break; + } +#ifdef OBJECT_TRACE + objectTrace( "SPRect::set", false ); +#endif +} + +void SPRect::update(SPCtx* ctx, unsigned int flags) { + +#ifdef OBJECT_TRACE + objectTrace( "SPRect::update", true, flags ); +#endif + + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx); + + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + this->x.update(em, ex, w); + this->y.update(em, ex, h); + this->width.update(em, ex, w); + this->height.update(em, ex, h); + this->rx.update(em, ex, w); + this->ry.update(em, ex, h); + this->set_shape(); + + flags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; // since we change the description, it's not a "just translation" anymore + } + + SPShape::update(ctx, flags); +#ifdef OBJECT_TRACE + objectTrace( "SPRect::update", false, flags ); +#endif +} + +Inkscape::XML::Node * SPRect::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + +#ifdef OBJECT_TRACE + objectTrace( "SPRect::write", true, flags ); +#endif + GenericRectType new_type = SP_GENERIC_RECT; + if (hasPathEffectOnClipOrMaskRecursive(this)) { + new_type = SP_GENERIC_PATH; + } + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + + switch ( new_type ) { + + case SP_GENERIC_RECT: + repr = xml_doc->createElement("svg:rect"); + break; + case SP_GENERIC_PATH: + repr = xml_doc->createElement("svg:path"); + break; + default: + std::cerr << "SPGenericRect::write(): unknown type." << std::endl; + } + } + if (type != new_type) { + switch (new_type) { + case SP_GENERIC_RECT: + repr->setCodeUnsafe(g_quark_from_string("svg:rect")); + break; + case SP_GENERIC_PATH: + repr->setCodeUnsafe(g_quark_from_string("svg:path")); + repr->setAttribute("sodipodi:type", "rect"); + break; + default: + std::cerr << "SPGenericRect::write(): unknown type." << std::endl; + } + type = new_type; + } + repr->setAttributeSvgLength("width", this->width); + repr->setAttributeSvgLength("height", this->height); + + if (this->rx._set) { + repr->setAttributeSvgLength("rx", this->rx); + } + + if (this->ry._set) { + repr->setAttributeSvgLength("ry", this->ry); + } + + repr->setAttributeSvgLength("x", this->x); + repr->setAttributeSvgLength("y", this->y); + // write d= + if (type == SP_GENERIC_PATH) { + set_rect_path_attribute(repr); // include set_shape() + } else { + this->set_shape(); // evaluate SPCurve + } + SPShape::write(xml_doc, repr, flags); + +#ifdef OBJECT_TRACE + objectTrace( "SPRect::write", false, flags ); +#endif + + return repr; +} + +const char* SPRect::typeName() const { + return "rect"; +} + +const char* SPRect::displayName() const { + return _("Rectangle"); +} + +#define C1 0.554 + +void SPRect::set_shape() { + if (checkBrokenPathEffect()) { + return; + } + if ((this->height.computed < 1e-18) || (this->width.computed < 1e-18)) { + this->setCurveInsync(nullptr); + this->setCurveBeforeLPE(nullptr); + return; + } + + SPCurve c; + + double const x = this->x.computed; + double const y = this->y.computed; + double const w = this->width.computed; + double const h = this->height.computed; + double const w2 = w / 2; + double const h2 = h / 2; + double const rx = std::min(( this->rx._set + ? this->rx.computed + : ( this->ry._set + ? this->ry.computed + : 0.0 ) ), + .5 * this->width.computed); + double const ry = std::min(( this->ry._set + ? this->ry.computed + : ( this->rx._set + ? this->rx.computed + : 0.0 ) ), + .5 * this->height.computed); + /* TODO: Handle negative rx or ry as per + * http://www.w3.org/TR/SVG11/shapes.html#RectElementRXAttribute once Inkscape has proper error + * handling (see http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing). + */ + + /* We don't use proper circular/elliptical arcs, but bezier curves can approximate a 90-degree + * arc fairly well. + */ + if ((rx > 1e-18) && (ry > 1e-18)) { + c.moveto(x + rx, y); + + if (rx < w2) { + c.lineto(x + w - rx, y); + } + + c.curveto(x + w - rx * (1 - C1), y, x + w, y + ry * (1 - C1), x + w, y + ry); + + if (ry < h2) { + c.lineto(x + w, y + h - ry); + } + + c.curveto(x + w, y + h - ry * (1 - C1), x + w - rx * (1 - C1), y + h, x + w - rx, y + h); + + if (rx < w2) { + c.lineto(x + rx, y + h); + } + + c.curveto(x + rx * (1 - C1), y + h, x, y + h - ry * (1 - C1), x, y + h - ry); + + if (ry < h2) { + c.lineto(x, y + ry); + } + + c.curveto(x, y + ry * (1 - C1), x + rx * (1 - C1), y, x + rx, y); + } else { + c.moveto(x + 0.0, y + 0.0); + c.lineto(x + w, y + 0.0); + c.lineto(x + w, y + h); + c.lineto(x + 0.0, y + h); + } + + c.closepath(); + + prepareShapeForLPE(&c); +} + +bool SPRect::set_rect_path_attribute(Inkscape::XML::Node *repr) +{ + // Make sure our pathvector is up to date. + this->set_shape(); + + if (_curve) { + repr->setAttribute("d", sp_svg_write_path(_curve->get_pathvector())); + } else { + repr->removeAttribute("d"); + } + + return true; +} + +void SPRect::modified(guint flags) +{ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + this->set_shape(); + } + + SPShape::modified(flags); +} + +/* fixme: Think (Lauris) */ + +void SPRect::setPosition(gdouble x, gdouble y, gdouble width, gdouble height) { + this->x = x; + this->y = y; + this->width = width; + this->height = height; + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPRect::setRx(bool set, gdouble value) { + this->rx._set = set; + + if (set) { + this->rx = value; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPRect::setRy(bool set, gdouble value) { + this->ry._set = set; + + if (set) { + this->ry = value; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPRect::update_patheffect(bool write) { + if (type != SP_GENERIC_PATH && hasPathEffectOnClipOrMaskRecursive(this)) { + SPRect::write(document->getReprDoc(), getRepr(), SP_OBJECT_MODIFIED_FLAG); + } + SPShape::update_patheffect(write); +} + +Geom::Affine SPRect::set_transform(Geom::Affine const& xform) { + if (pathEffectsEnabled() && !optimizeTransforms()) { + return xform; + } + /* Calculate rect start in parent coords. */ + Geom::Point pos(Geom::Point(this->x.computed, this->y.computed) * xform); + + /* This function takes care of translation and scaling, we return whatever parts we can't + handle. */ + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + gdouble const sw = hypot(ret[0], ret[1]); + gdouble const sh = hypot(ret[2], ret[3]); + + if (sw > 1e-9) { + ret[0] /= sw; + ret[1] /= sw; + } else { + ret[0] = 1.0; + ret[1] = 0.0; + } + + if (sh > 1e-9) { + ret[2] /= sh; + ret[3] /= sh; + } else { + ret[2] = 0.0; + ret[3] = 1.0; + } + + /* Preserve units */ + this->width.scale( sw ); + this->height.scale( sh ); + + if (this->rx._set) { + this->rx.scale( sw ); + } + + if (this->ry._set) { + this->ry.scale( sh ); + } + + /* Find start in item coords */ + pos = pos * ret.inverse(); + this->x = pos[Geom::X]; + this->y = pos[Geom::Y]; + + this->set_shape(); + + // Adjust stroke width + this->adjust_stroke(sqrt(fabs(sw * sh))); + + // Adjust pattern fill + this->adjust_pattern(xform * ret.inverse()); + + // Adjust gradient fill + this->adjust_gradient(xform * ret.inverse()); + + return ret; +} + + +/** +Returns the ratio in which the vector from p0 to p1 is stretched by transform + */ +gdouble SPRect::vectorStretch(Geom::Point p0, Geom::Point p1, Geom::Affine xform) { + if (p0 == p1) { + return 0; + } + + return (Geom::distance(p0 * xform, p1 * xform) / Geom::distance(p0, p1)); +} + +void SPRect::setVisibleRx(gdouble rx) { + if (rx == 0) { + this->rx.unset(); + } else { + this->rx = rx / SPRect::vectorStretch( + Geom::Point(this->x.computed + 1, this->y.computed), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); + } + + this->updateRepr(); +} + +void SPRect::setVisibleRy(gdouble ry) { + if (ry == 0) { + this->ry.unset(); + } else { + this->ry = ry / SPRect::vectorStretch( + Geom::Point(this->x.computed, this->y.computed + 1), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); + } + + this->updateRepr(); +} + +gdouble SPRect::getVisibleRx() const { + if (!this->rx._set) { + return 0; + } + + return this->rx.computed * SPRect::vectorStretch( + Geom::Point(this->x.computed + 1, this->y.computed), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); +} + +gdouble SPRect::getVisibleRy() const { + if (!this->ry._set) { + return 0; + } + + return this->ry.computed * SPRect::vectorStretch( + Geom::Point(this->x.computed, this->y.computed + 1), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); +} + +Geom::Rect SPRect::getRect() const { + Geom::Point p0 = Geom::Point(this->x.computed, this->y.computed); + Geom::Point p2 = Geom::Point(this->x.computed + this->width.computed, this->y.computed + this->height.computed); + + return Geom::Rect(p0, p2); +} + +void SPRect::compensateRxRy(Geom::Affine xform) { + if (this->rx.computed == 0 && this->ry.computed == 0) { + return; // nothing to compensate + } + + // test unit vectors to find out compensation: + Geom::Point c(this->x.computed, this->y.computed); + Geom::Point cx = c + Geom::Point(1, 0); + Geom::Point cy = c + Geom::Point(0, 1); + + // apply previous transform if any + c *= this->transform; + cx *= this->transform; + cy *= this->transform; + + // find out stretches that we need to compensate + gdouble eX = SPRect::vectorStretch(cx, c, xform); + gdouble eY = SPRect::vectorStretch(cy, c, xform); + + // If only one of the radii is set, set both radii so they have the same visible length + // This is needed because if we just set them the same length in SVG, they might end up unequal because of transform + if ((this->rx._set && !this->ry._set) || (this->ry._set && !this->rx._set)) { + gdouble r = MAX(this->rx.computed, this->ry.computed); + this->rx = r / eX; + this->ry = r / eY; + } else { + this->rx = this->rx.computed / eX; + this->ry = this->ry.computed / eY; + } + + // Note that a radius may end up larger than half-side if the rect is scaled down; + // that's ok because this preserves the intended radii in case the rect is enlarged again, + // and set_shape will take care of trimming too large radii when generating d= +} + +void SPRect::setVisibleWidth(gdouble width) { + this->width = width / SPRect::vectorStretch( + Geom::Point(this->x.computed + 1, this->y.computed), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); + + this->updateRepr(); +} + +void SPRect::setVisibleHeight(gdouble height) { + this->height = height / SPRect::vectorStretch( + Geom::Point(this->x.computed, this->y.computed + 1), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); + + this->updateRepr(); +} + +gdouble SPRect::getVisibleWidth() const { + if (!this->width._set) { + return 0; + } + + return this->width.computed * SPRect::vectorStretch( + Geom::Point(this->x.computed + 1, this->y.computed), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); +} + +gdouble SPRect::getVisibleHeight() const { + if (!this->height._set) { + return 0; + } + + return this->height.computed * SPRect::vectorStretch( + Geom::Point(this->x.computed, this->y.computed + 1), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); +} + +void SPRect::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + /* This method overrides sp_shape_snappoints, which is the default for any shape. The default method + returns all eight points along the path of a rounded rectangle, but not the real corners. Snapping + the startpoint and endpoint of each rounded corner is not very useful and really confusing. Instead + we could snap either the real corners, or not snap at all. Bulia Byak opted to snap the real corners, + but it should be noted that this might be confusing in some cases with relatively large radii. With + small radii though the user will easily understand which point is snapping. */ + + Geom::Affine const i2dt (this->i2dt_affine ()); + + Geom::Point p0 = Geom::Point(this->x.computed, this->y.computed) * i2dt; + Geom::Point p1 = Geom::Point(this->x.computed, this->y.computed + this->height.computed) * i2dt; + Geom::Point p2 = Geom::Point(this->x.computed + this->width.computed, this->y.computed + this->height.computed) * i2dt; + Geom::Point p3 = Geom::Point(this->x.computed + this->width.computed, this->y.computed) * i2dt; + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_RECT_CORNER)) { + p.emplace_back(p0, Inkscape::SNAPSOURCE_RECT_CORNER, Inkscape::SNAPTARGET_RECT_CORNER); + p.emplace_back(p1, Inkscape::SNAPSOURCE_RECT_CORNER, Inkscape::SNAPTARGET_RECT_CORNER); + p.emplace_back(p2, Inkscape::SNAPSOURCE_RECT_CORNER, Inkscape::SNAPTARGET_RECT_CORNER); + p.emplace_back(p3, Inkscape::SNAPSOURCE_RECT_CORNER, Inkscape::SNAPTARGET_RECT_CORNER); + } + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_LINE_MIDPOINT)) { + p.emplace_back((p0 + p1)/2, Inkscape::SNAPSOURCE_LINE_MIDPOINT, Inkscape::SNAPTARGET_LINE_MIDPOINT); + p.emplace_back((p1 + p2)/2, Inkscape::SNAPSOURCE_LINE_MIDPOINT, Inkscape::SNAPTARGET_LINE_MIDPOINT); + p.emplace_back((p2 + p3)/2, Inkscape::SNAPSOURCE_LINE_MIDPOINT, Inkscape::SNAPTARGET_LINE_MIDPOINT); + p.emplace_back((p3 + p0)/2, Inkscape::SNAPSOURCE_LINE_MIDPOINT, Inkscape::SNAPTARGET_LINE_MIDPOINT); + } + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT)) { + p.emplace_back((p0 + p2)/2, Inkscape::SNAPSOURCE_OBJECT_MIDPOINT, Inkscape::SNAPTARGET_OBJECT_MIDPOINT); + } +} + +void SPRect::convert_to_guides() const { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (!prefs->getBool("/tools/shapes/rect/convertguides", true)) { + // Use bounding box instead of edges + SPShape::convert_to_guides(); + return; + } + + std::list<std::pair<Geom::Point, Geom::Point> > pts; + + Geom::Affine const i2dt(this->i2dt_affine()); + + Geom::Point A1(Geom::Point(this->x.computed, this->y.computed) * i2dt); + Geom::Point A2(Geom::Point(this->x.computed, this->y.computed + this->height.computed) * i2dt); + Geom::Point A3(Geom::Point(this->x.computed + this->width.computed, this->y.computed + this->height.computed) * i2dt); + Geom::Point A4(Geom::Point(this->x.computed + this->width.computed, this->y.computed) * i2dt); + + pts.emplace_back(A1, A2); + pts.emplace_back(A2, A3); + pts.emplace_back(A3, A4); + pts.emplace_back(A4, A1); + + sp_guide_pt_pairs_to_guides(this->document, pts); +} + +/* + 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 : diff --git a/src/object/sp-rect.h b/src/object/sp-rect.h new file mode 100644 index 0000000..f157e81 --- /dev/null +++ b/src/object/sp-rect.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_RECT_H +#define SEEN_SP_RECT_H + +/* + * SVG <rect> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> + +#include "svg/svg-length.h" +#include "sp-shape.h" + +enum GenericRectType { + SP_GENERIC_RECT_UNDEFINED, // FIXME shouldn't exist + SP_GENERIC_RECT, // Default + SP_GENERIC_PATH // LPE +}; + +class SPRect final : public SPShape { +public: + SPRect(); + ~SPRect() override; + int tag() const override { return tag_of<decltype(*this)>; } + + void tag_name_changed(gchar const* oldname, gchar const* newname) override; + + void setPosition(double x, double y, double width, double height); + + /* If SET if FALSE, VALUE is just ignored */ + void setRx(bool set, double value); + void setRy(bool set, double value); + + double getVisibleRx() const; + void setVisibleRx(double rx); + + double getVisibleRy() const; + void setVisibleRy(double ry); + + Geom::Rect getRect() const; + + double getVisibleWidth() const; + void setVisibleWidth(double rx); + + double getVisibleHeight() const; + void setVisibleHeight(double ry); + + void compensateRxRy(Geom::Affine xform); + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + + void set(SPAttr key, char const *value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + bool set_rect_path_attribute(Inkscape::XML::Node *repr); + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + const char* typeName() const override; + const char* displayName() const override; + void update_patheffect(bool write) override; + void set_shape() override; + Geom::Affine set_transform(Geom::Affine const& xform) override; + + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + void convert_to_guides() const override; + GenericRectType type; + SVGLength x; + SVGLength y; + SVGLength width; + SVGLength height; + SVGLength rx; + SVGLength ry; + +private: + static double vectorStretch(Geom::Point p0, Geom::Point p1, Geom::Affine xform); +}; + +#endif // SEEN_SP_RECT_H + +/* + 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 : diff --git a/src/object/sp-root.cpp b/src/object/sp-root.cpp new file mode 100644 index 0000000..47f0e93 --- /dev/null +++ b/src/object/sp-root.cpp @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG \<svg\> implementation. + */ +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <string> +#include <2geom/transforms.h> + +#include "attributes.h" +#include "print.h" +#include "document.h" +#include "inkscape-version.h" +#include "sp-defs.h" +#include "sp-namedview.h" +#include "sp-root.h" +#include "sp-use.h" +#include "display/drawing-group.h" +#include "svg/svg.h" +#include "xml/repr.h" +#include "util/units.h" + +SPRoot::SPRoot() : SPGroup(), SPViewBox() +{ + this->onload = nullptr; + + static Inkscape::Version const zero_version(0, 0); + + sp_version_from_string(SVG_VERSION, &this->original.svg); + this->version.svg = zero_version; + this->original.svg = zero_version; + this->version.inkscape = zero_version; + this->original.inkscape = zero_version; + + this->unset_x_and_y(); + this->width.unset(SVGLength::PERCENT, 1.0, 1.0); + this->height.unset(SVGLength::PERCENT, 1.0, 1.0); + + this->defs = nullptr; +} + +SPRoot::~SPRoot() += default; + +void SPRoot::unset_x_and_y() +{ + this->x.unset(SVGLength::PERCENT, 0.0, 0.0); // Ignored for root SVG element + this->y.unset(SVGLength::PERCENT, 0.0, 0.0); +} + +void SPRoot::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + //XML Tree being used directly here while it shouldn't be. + if (!this->getRepr()->attribute("version")) { + repr->setAttribute("version", SVG_VERSION); + } + + this->readAttr(SPAttr::VERSION); + this->readAttr(SPAttr::INKSCAPE_VERSION); + /* It is important to parse these here, so objects will have viewport build-time */ + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + this->readAttr(SPAttr::WIDTH); + this->readAttr(SPAttr::HEIGHT); + this->readAttr(SPAttr::VIEWBOX); + this->readAttr(SPAttr::PRESERVEASPECTRATIO); + this->readAttr(SPAttr::ONLOAD); + + SPGroup::build(document, repr); + + // Search for first <defs> node + for (auto& o: children) { + if (is<SPDefs>(&o)) { + this->defs = cast<SPDefs>(&o); + break; + } + } + + // clear transform, if any was read in - SVG does not allow transform= on <svg> + this->transform = Geom::identity(); +} + +void SPRoot::release() +{ + this->defs = nullptr; + + SPGroup::release(); +} + + +void SPRoot::set(SPAttr key, const gchar *value) +{ + switch (key) { + case SPAttr::VERSION: + if (!sp_version_from_string(value, &this->version.svg)) { + this->version.svg = this->original.svg; + } + break; + + case SPAttr::INKSCAPE_VERSION: + if (!sp_version_from_string(value, &this->version.inkscape)) { + this->version.inkscape = this->original.inkscape; + } + break; + + case SPAttr::X: + /* Valid for non-root SVG elements; ex, em not handled correctly. */ + if (!this->x.read(value)) { + this->x.unset(SVGLength::PERCENT, 0.0, 0.0); + } + + /* fixme: I am almost sure these do not require viewport flag (Lauris) */ + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::Y: + /* Valid for non-root SVG elements; ex, em not handled correctly. */ + if (!this->y.read(value)) { + this->y.unset(SVGLength::PERCENT, 0.0, 0.0); + } + + /* fixme: I am almost sure these do not require viewport flag (Lauris) */ + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::WIDTH: + if (!this->width.read(value) || !(this->width.computed > 0.0)) { + this->width.unset(SVGLength::PERCENT, 1.0, 1.0); + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::HEIGHT: + if (!this->height.read(value) || !(this->height.computed > 0.0)) { + this->height.unset(SVGLength::PERCENT, 1.0, 1.0); + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::VIEWBOX: + set_viewBox( value ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::PRESERVEASPECTRATIO: + set_preserveAspectRatio( value ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::ONLOAD: + this->onload = (char *) value; + break; + + default: + /* Pass the set event to the parent */ + SPGroup::set(key, value); + break; + } +} + +void SPRoot::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + SPGroup::child_added(child, ref); + + SPObject *co = this->document->getObjectByRepr(child); + // NOTE: some XML nodes do not have corresponding SP objects, + // for instance inkscape:clipboard used in the clipboard code. + // See LP bug #1227827 + //g_assert (co != NULL || !strcmp("comment", child->name())); // comment repr node has no object + + if (co && is<SPDefs>(co)) { + // We search for first <defs> node - it is not beautiful, but works + for (auto& c: children) { + if (is<SPDefs>(&c)) { + this->defs = cast<SPDefs>(&c); + break; + } + } + } +} + +void SPRoot::remove_child(Inkscape::XML::Node *child) +{ + if (this->defs && (this->defs->getRepr() == child)) { + SPObject *iter = nullptr; + + // We search for first remaining <defs> node - it is not beautiful, but works + for (auto& child: children) { + iter = &child; + if (is<SPDefs>(iter) && (SPDefs *)iter != this->defs) { + this->defs = (SPDefs *)iter; + break; + } + } + + if (!iter) { + /* we should probably create a new <defs> here? */ + this->defs = nullptr; + } + } + + SPGroup::remove_child(child); +} + +void SPRoot::setRootDimensions() +{ + /* + * This is the root SVG element: + * + * x, y, width, and height apply to positioning the SVG element inside a parent. + * For the root SVG in Inkscape there is no parent, thus special rules apply: + * If width, height not set, width = 100%, height = 100% (as always). + * If width and height are in percent, they are percent of viewBox width/height. + * If width, height, and viewBox are not set... pick "random" width/height. + * x, y are ignored. + * initial viewport = (0 0 width height) + */ + if( this->viewBox_set ) { + + if( this->width._set ) { + // Check if this is necessary + if (this->width.unit == SVGLength::PERCENT) { + this->width.computed = this->width.value * this->viewBox.width(); + } + } else { + this->width.set( SVGLength::PX, this->viewBox.width(), this->viewBox.width() ); + } + + if( this->height._set ) { + if (this->height.unit == SVGLength::PERCENT) { + this->height.computed = this->height.value * this->viewBox.height(); + } + } else { + this->height.set(SVGLength::PX, this->viewBox.height(), this->viewBox.height() ); + } + + } else { + + if( !this->width._set || this->width.unit == SVGLength::PERCENT) { + this->width.set( SVGLength::PX, 300, 300 ); // CSS/SVG default + } + + if( !this->height._set || this->height.unit == SVGLength::PERCENT) { + this->height.set( SVGLength::PX, 150, 150 ); // CSS/SVG default + } + } + + // Ignore x, y values for root element + this->unset_x_and_y(); +} + +void SPRoot::update(SPCtx *ctx, guint flags) +{ + SPItemCtx const *ictx = (SPItemCtx const *) ctx; + + if( !this->parent ) { + this->setRootDimensions(); + } + + // Calculate x, y, width, height from parent/initial viewport + this->calcDimsFromParentViewport(ictx, false, cloned ? cast<SPUse>(parent) : nullptr); + + // std::cout << "SPRoot::update: final:" + // << " x: " << x.computed + // << " y: " << y.computed + // << " width: " << width.computed + // << " height: " << height.computed << std::endl; + + // Calculate new viewport + SPItemCtx rctx = *ictx; + rctx.viewport = Geom::Rect::from_xywh( this->x.computed, this->y.computed, + this->width.computed, this->height.computed ); + rctx = get_rctx( &rctx, Inkscape::Util::Quantity::convert(1, this->document->getDisplayUnit(), "px") ); + + /* And invoke parent method */ + SPGroup::update((SPCtx *) &rctx, flags); + + /* As last step set additional transform of drawing group */ + for (auto &v : views) { + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + g->setChildTransform(c2p); + } +} + +void SPRoot::modified(unsigned int flags) +{ + SPGroup::modified(flags); + + if (!this->parent && (flags & SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + // Size of viewport has changed. + document->getNamedView()->updateViewPort(); + } +} + + +Inkscape::XML::Node *SPRoot::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:svg"); + } + + /* Only update version string on successful write to file. This is handled by 'file_save()'. + * if (flags & SP_OBJECT_WRITE_EXT) { + * repr->setAttribute("inkscape:version", Inkscape::version_string); + * } + */ + + if (!repr->attribute("version")) { + gchar *myversion = sp_version_to_string(this->version.svg); + repr->setAttribute("version", myversion); + g_free(myversion); + } + + if (fabs(this->x.computed) > 1e-9) { + repr->setAttributeSvgDouble("x", this->x.computed); + } + + if (fabs(this->y.computed) > 1e-9) { + repr->setAttributeSvgDouble("y", this->y.computed); + } + + /* Unlike all other SPObject, here we want to preserve absolute units too (and only here, + * according to the recommendation in http://www.w3.org/TR/SVG11/coords.html#Units). + */ + repr->setAttribute("width", sp_svg_length_write_with_units(this->width)); + repr->setAttribute("height", sp_svg_length_write_with_units(this->height)); + + this->write_viewBox(repr); + this->write_preserveAspectRatio(repr); + + SPGroup::write(xml_doc, repr, flags); + + return repr; +} + +Inkscape::DrawingItem *SPRoot::show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) +{ + Inkscape::DrawingItem *ai = SPGroup::show(drawing, key, flags); + + if (ai) { + auto g = cast<Inkscape::DrawingGroup>(ai); + g->setChildTransform(this->c2p); + } + + // Uncomment to print out XML tree + // getRepr()->recursivePrintTree(0); + + // Uncomment to print out SP Object tree + // recursivePrintTree(0); + + // Uncomment to print out Display Item tree + // ai->recursivePrintTree(0); + + return ai; +} + +void SPRoot::print(SPPrintContext *ctx) +{ + ctx->bind(this->c2p, 1.0); + + SPGroup::print(ctx); + + ctx->release(); +} + +const char *SPRoot::typeName() const { + return "image"; +} + +const char *SPRoot::displayName() const { + return "SVG"; // Do not translate +} + +/* + 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 : diff --git a/src/object/sp-root.h b/src/object/sp-root.h new file mode 100644 index 0000000..152d681 --- /dev/null +++ b/src/object/sp-root.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SPRoot: SVG \<svg\> implementation. + */ +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_ROOT_H_SEEN +#define SP_ROOT_H_SEEN + +#include "version.h" +#include "svg/svg-length.h" +#include "sp-item-group.h" +#include "viewbox.h" +#include "sp-dimensions.h" + +class SPDefs; + +/** \<svg\> element */ +class SPRoot final : public SPGroup, public SPViewBox, public SPDimensions { +public: + SPRoot(); + ~SPRoot() override; + int tag() const override { return tag_of<decltype(*this)>; } + + struct { + Inkscape::Version svg; + Inkscape::Version inkscape; + } version, original; + + char *onload; + + /** + * Primary \<defs\> element where we put new defs (patterns, gradients etc.). + * + * At the time of writing, this is chosen as the first \<defs\> child of + * this \<svg\> element: see writers of this member in sp-root.cpp. + */ + SPDefs *defs; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + void modified(unsigned int flags) override; + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void print(SPPrintContext *ctx) override; + const char* typeName() const override; + const char* displayName() const override; +private: + void unset_x_and_y(); + void setRootDimensions(); +}; + +#endif /* !SP_ROOT_H_SEEN */ + +/* + 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 : diff --git a/src/object/sp-script.cpp b/src/object/sp-script.cpp new file mode 100644 index 0000000..7daaa5c --- /dev/null +++ b/src/object/sp-script.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <script> implementation + * + * Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-script.h" +#include "attributes.h" + +SPScript::SPScript() : SPObject() { + this->xlinkhref = nullptr; +} + +SPScript::~SPScript() = default; + +void SPScript::build(SPDocument* doc, Inkscape::XML::Node* repr) { + SPObject::build(doc, repr); + + //Read values of key attributes from XML nodes into object. + this->readAttr(SPAttr::XLINK_HREF); + + doc->addResource("script", this); +} + +/** + * Reads the Inkscape::XML::Node, and initializes SPScript variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ + +void SPScript::release() { + if (this->document) { + // Unregister ourselves + this->document->removeResource("script", this); + } + + SPObject::release(); +} + +void SPScript::update(SPCtx* /*ctx*/, unsigned int /*flags*/) { +} + + +void SPScript::modified(unsigned int /*flags*/) { +} + + +void SPScript::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::XLINK_HREF: + if (this->xlinkhref) { + g_free(this->xlinkhref); + } + + this->xlinkhref = g_strdup(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPObject::set(key, value); + break; + } +} + +Inkscape::XML::Node* SPScript::write(Inkscape::XML::Document* /*doc*/, Inkscape::XML::Node* repr, guint /*flags*/) { + return repr; +} + +/* + 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 : diff --git a/src/object/sp-script.h b/src/object/sp-script.h new file mode 100644 index 0000000..c8f6712 --- /dev/null +++ b/src/object/sp-script.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SCRIPT_H +#define SEEN_SP_SCRIPT_H + +/* + * SVG <script> implementation + * + * Author: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2008 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" +#include "document.h" + +/* SPScript */ +class SPScript final : public SPObject { +public: + SPScript(); + ~SPScript() override; + int tag() const override { return tag_of<decltype(*this)>; } + + char *xlinkhref; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttr key, char const* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif + +/* + 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 : diff --git a/src/object/sp-shape-reference.cpp b/src/object/sp-shape-reference.cpp new file mode 100644 index 0000000..26ede2e --- /dev/null +++ b/src/object/sp-shape-reference.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Reference class for shapes (SVG 2 text). + * + * Copyright (C) 2020 Authors + */ + +#include "sp-shape-reference.h" +#include "object/sp-text.h" + +SPShapeReference::SPShapeReference(SPObject *obj) + : URIReference(obj) +{ + // The text object can be detached from the document but still be + // referenced, and its style (who's the owner of the SPShapeReference) + // can also still be referenced even after the object got destroyed. + _owner_release_connection = obj->connectRelease([this](SPObject *text_object) { + assert(text_object == this->getOwner()); + + // Fully detach to prevent reconnecting with a shape's modified signal + this->detach(); + + this->_owner_release_connection.disconnect(); + }); + + // https://www.w3.org/TR/SVG/text.html#TextShapeInside + // Applies to: 'text' elements + // Inherited: no + if (!is<SPText>(obj)) { + g_warning("shape reference on non-text object: %s", typeid(*obj).name()); + return; + } + + // Listen to the shape's modified event to keep the text layout updated + changedSignal().connect([this](SPObject *, SPObject *shape_object) { + this->_shape_modified_connection.disconnect(); + + if (shape_object) { + this->_shape_modified_connection = + shape_object->connectModified(sigc::mem_fun(*this, &SPShapeReference::on_shape_modified)); + } + }); +} + +SPShapeReference::~SPShapeReference() +{ // + _shape_modified_connection.disconnect(); + _owner_release_connection.disconnect(); +} + +/** + * Slot to connect to the shape's modified signal. Requests display update of the text object. + */ +void SPShapeReference::on_shape_modified(SPObject *shape_object, unsigned flags) +{ + auto *text_object = getOwner(); + + assert(text_object); + assert(shape_object == getObject()); + + if ((flags & SP_OBJECT_MODIFIED_FLAG)) { + text_object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); + } +} + +// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-shape-reference.h b/src/object/sp-shape-reference.h new file mode 100644 index 0000000..876ed5f --- /dev/null +++ b/src/object/sp-shape-reference.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SHAPE_REFERENCE_H +#define SEEN_SP_SHAPE_REFERENCE_H + +/* + * Reference class for shapes (SVG 2 text). + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "uri-references.h" + +#include "sp-object.h" +#include "sp-shape.h" + +class SPDocument; + +class SPShapeReference : public Inkscape::URIReference { +public: + ~SPShapeReference() override; + SPShapeReference(SPObject *obj); + SPShape *getObject() const { + return static_cast<SPShape *>(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject *obj) const override { + return is<SPShape>(obj) && URIReference::_acceptObject(obj); + }; + + private: + void on_shape_modified(SPObject *, unsigned flags); + + sigc::connection _shape_modified_connection; + sigc::connection _owner_release_connection; +}; + +#endif // SEEN_SP_SHAPE_REFERENCE_H +/* + 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 : diff --git a/src/object/sp-shape.cpp b/src/object/sp-shape.cpp new file mode 100644 index 0000000..b3c74de --- /dev/null +++ b/src/object/sp-shape.cpp @@ -0,0 +1,1294 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Base class for shapes, including <path> element + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2004 John Cliff + * Copyright (C) 2007-2008 Johan Engelen + * Copyright (C) 2010 Jon A. Cruz <jon@joncruz.org> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/rect.h> +#include <2geom/transforms.h> +#include <2geom/pathvector.h> +#include <2geom/path-intersection.h> +#include "helper/geom.h" +#include "helper/geom-nodetype.h" + +#include <sigc++/functors/ptr_fun.h> +#include <sigc++/adaptors/bind.h> + +#include "display/drawing-shape.h" +#include "display/curve.h" +#include "print.h" +#include "document.h" +#include "style.h" +#include "sp-marker.h" +#include "sp-root.h" +#include "sp-path.h" +#include "preferences.h" +#include "attributes.h" +#include "path/path-outline.h" // For bound box calculation + +#include "svg/svg.h" +#include "svg/path-string.h" +#include "snap-candidate.h" +#include "snap-preferences.h" +#include "live_effects/lpeobject.h" + +#define noSHAPE_VERBOSE + +static void sp_shape_update_marker_view (SPShape *shape, Inkscape::DrawingItem *ai); + +SPShape::SPShape() : SPLPEItem() { + for (auto & i : this->_marker) { + i = nullptr; + } +} + +SPShape::~SPShape() { + for ( int i = 0 ; i < SP_MARKER_LOC_QTY ; i++ ) { + this->_release_connect[i].disconnect(); + this->_modified_connect[i].disconnect(); + } +} + +void SPShape::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPLPEItem::build(document, repr); + + for (int i = 0 ; i < SP_MARKER_LOC_QTY ; i++) { + set_marker(i, style->marker_ptrs[i]->value()); + } +} + +/** + * Removes, releases and unrefs all children of object + * + * This is the inverse of sp_shape_build(). It must be invoked as soon + * as the shape is removed from the tree, even if it is still referenced + * by other objects. This routine also disconnects/unrefs markers and + * curves attached to it. + * + * \see SPObject::release() + */ +void SPShape::release() +{ + for (int i = 0; i < SP_MARKER_LOC_QTY; i++) { + if (_marker[i]) { + + for (auto &v : views) { + sp_marker_hide(_marker[i], v.drawingitem->key() + ITEM_KEY_MARKERS + i); + } + + _release_connect[i].disconnect(); + _modified_connect[i].disconnect(); + _marker[i]->unhrefObject(this); + _marker[i] = nullptr; + } + } + + _curve.reset(); + + _curve_before_lpe.reset(); + + SPLPEItem::release(); +} + +void SPShape::set(SPAttr key, const gchar* value) { + SPLPEItem::set(key, value); +} + + +Inkscape::XML::Node* SPShape::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + SPLPEItem::write(xml_doc, repr, flags); + return repr; +} + +void SPShape::update(SPCtx* ctx, guint flags) { + // Any update can change the bounding box, + // so the cached version can no longer be used. + // But the idle checker usually is just moving the objects around. + bbox_vis_cache_is_valid = false; + bbox_geom_cache_is_valid = false; + + // std::cout << "SPShape::update(): " << (getId()?getId():"null") << std::endl; + SPLPEItem::update(ctx, flags); + + /* This stanza checks that an object's marker style agrees with + * the marker objects it has allocated. sp_shape_set_marker ensures + * that the appropriate marker objects are present (or absent) to + * match the style. + */ + for (int i = 0 ; i < SP_MARKER_LOC_QTY ; i++) { + set_marker(i, style->marker_ptrs[i]->value()); + } + + if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + if (this->style->stroke_width.unit == SP_CSS_UNIT_PERCENT) { + SPItemCtx *ictx = (SPItemCtx *) ctx; + double const aw = 1.0 / ictx->i2vp.descrim(); + this->style->stroke_width.computed = this->style->stroke_width.value * aw; + + for (auto &v : views) { + auto sh = cast<Inkscape::DrawingShape>(v.drawingitem.get()); + if (hasMarkers()) { + context_style = style; + sh->setStyle(style, context_style); + // Done at end: + // sh->setChildrenStyle(this->context_style); //Resolve 'context-xxx' in children. + } else if (parent) { + context_style = parent->context_style; + sh->setStyle(style, context_style); + } + } + } + } + + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG)) { + /* This is suboptimal, because changing parent style schedules recalculation */ + /* But on the other hand - how can we know that parent does not tie style and transform */ + for (auto &v : views) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + auto sh = static_cast<Inkscape::DrawingShape*>(v.drawingitem.get()); + sh->setPath(_curve); + } + } + } + + if (this->hasMarkers ()) { + + /* Dimension marker views */ + for (auto &v : views) { + SPItem::ensure_key(v.drawingitem.get()); + for (int i = 0; i < SP_MARKER_LOC_QTY; i++) { + if (_marker[i]) { + sp_marker_show_dimension(_marker[i], v.drawingitem->key() + ITEM_KEY_MARKERS + i, numberOfMarkers(i)); + } + } + } + + /* Update marker views */ + for (auto &v : views) { + sp_shape_update_marker_view (this, v.drawingitem.get()); + } + + // Marker selector needs this here or marker previews are not rendered. + for (auto &v : views) { + auto sh = static_cast<Inkscape::DrawingShape*>(v.drawingitem.get()); + sh->setChildrenStyle(this->context_style); // Resolve 'context-xxx' in children. + } + } + + /* Update stroke/dashes for relative units. */ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx); + + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const d = sqrt(w*w + h*h) * M_SQRT1_2; // diagonal per SVG spec + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + if (style->stroke_width.unit == SP_CSS_UNIT_EM) { + style->stroke_width.computed = style->stroke_width.value * em; + } + else if (style->stroke_width.unit == SP_CSS_UNIT_EX) { + style->stroke_width.computed = style->stroke_width.value * ex; + } + else if (style->stroke_width.unit == SP_CSS_UNIT_PERCENT) { + style->stroke_width.computed = style->stroke_width.value * d; + } + + if (style->stroke_dasharray.values.size() != 0) { + for (auto&& i: style->stroke_dasharray.values) { + if (i.unit == SP_CSS_UNIT_EM) i.computed = i.value * em; + else if (i.unit == SP_CSS_UNIT_EX) i.computed = i.value * ex; + else if (i.unit == SP_CSS_UNIT_PERCENT) i.computed = i.value * d; + } + } + + if (style->stroke_dashoffset.unit == SP_CSS_UNIT_EM) { + style->stroke_dashoffset.computed = style->stroke_dashoffset.value * em; + } + else if (style->stroke_dashoffset.unit == SP_CSS_UNIT_EX) { + style->stroke_dashoffset.computed = style->stroke_dashoffset.value * ex; + } + else if (style->stroke_dashoffset.unit == SP_CSS_UNIT_PERCENT) { + style->stroke_dashoffset.computed = style->stroke_dashoffset.value * d; + } + } +} + +/** + * Calculate the transform required to get a marker's path object in the + * right place for particular path segment on a shape. + * + * \see sp_shape_marker_update_marker_view. + * + * From SVG spec: + * The axes of the temporary new user coordinate system are aligned according to the orient attribute on the 'marker' + * element and the slope of the curve at the given vertex. (Note: if there is a discontinuity at a vertex, the slope + * is the average of the slopes of the two segments of the curve that join at the given vertex. If a slope cannot be + * determined, the slope is assumed to be zero.) + * + * Reference: http://www.w3.org/TR/SVG11/painting.html#MarkerElement, the `orient' attribute. + * Reference for behaviour of zero-length segments: + * http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes + */ +Geom::Affine sp_shape_marker_get_transform(Geom::Curve const & c1, Geom::Curve const & c2) +{ + Geom::Point p = c1.pointAt(1); + Geom::Curve * c1_reverse = c1.reverse(); + Geom::Point tang1 = - c1_reverse->unitTangentAt(0); + delete c1_reverse; + Geom::Point tang2 = c2.unitTangentAt(0); + + double const angle1 = Geom::atan2(tang1); + double const angle2 = Geom::atan2(tang2); + + double ret_angle = .5 * (angle1 + angle2); + + if ( fabs( angle2 - angle1 ) > M_PI ) { + /* ret_angle is in the middle of the larger of the two sectors between angle1 and + * angle2, so flip it by 180degrees to force it to the middle of the smaller sector. + * + * (Imagine a circle with rays drawn at angle1 and angle2 from the centre of the + * circle. Those two rays divide the circle into two sectors.) + */ + ret_angle += M_PI; + } + + return Geom::Rotate(ret_angle) * Geom::Translate(p); +} + +Geom::Affine sp_shape_marker_get_transform_at_start(Geom::Curve const & c) +{ + Geom::Point p = c.pointAt(0); + Geom::Affine ret = Geom::Translate(p); + + if ( !c.isDegenerate() ) { + Geom::Point tang = c.unitTangentAt(0); + double const angle = Geom::atan2(tang); + ret = Geom::Rotate(angle) * Geom::Translate(p); + } else { + /* FIXME: the svg spec says to search for a better alternative than zero angle directionality: + * http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes */ + } + + return ret; +} + +Geom::Affine sp_shape_marker_get_transform_at_end(Geom::Curve const & c) +{ + Geom::Point p = c.pointAt(1); + Geom::Affine ret = Geom::Translate(p); + + if ( !c.isDegenerate() ) { + Geom::Curve * c_reverse = c.reverse(); + Geom::Point tang = - c_reverse->unitTangentAt(0); + delete c_reverse; + double const angle = Geom::atan2(tang); + ret = Geom::Rotate(angle) * Geom::Translate(p); + } else { + /* FIXME: the svg spec says to search for a better alternative than zero angle directionality: + * http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes */ + } + + return ret; +} + +/** + * Updates the instances (views) of a given marker in a shape. + * Marker views have to be scaled already. The transformation + * is retrieved and then shown by calling sp_marker_show_instance. + * + * @todo figure out what to do when both 'marker' and for instance 'marker-end' are set. + */ +static void +sp_shape_update_marker_view(SPShape *shape, Inkscape::DrawingItem *ai) +{ + // position arguments to sp_marker_show_instance, basically counts the amount of markers. + int counter[4] = {0}; + + if (!shape->curve()) + return; + + Geom::PathVector const &pathv = shape->curve()->get_pathvector(); + if (pathv.empty()) return; + + // the first vertex should get a start marker, the last an end marker, and all the others a mid marker + // see bug 456148 + + // START marker + { + Geom::Affine const m (sp_shape_marker_get_transform_at_start(pathv.begin()->front())); + for (int i = 0; i < 2; i++) { // SP_MARKER_LOC and SP_MARKER_LOC_START + if ( shape->_marker[i] ) { + Geom::Affine m_auto = m; + // Reverse start marker if necessary. + if (shape->_marker[i]->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) { + m_auto = Geom::Rotate::from_degrees( 180.0 ) * m; + } + sp_marker_show_instance(shape->_marker[i], ai, + ai->key() + ITEM_KEY_MARKERS + i, counter[i], m_auto, + shape->style->stroke_width.computed); + counter[i]++; + } + } + } + + // MID marker + if (shape->_marker[SP_MARKER_LOC_MID] || shape->_marker[SP_MARKER_LOC]) { + for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) { + // START position + if ( path_it != pathv.begin() + && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, don't draw mid marker there + { + Geom::Affine const m (sp_shape_marker_get_transform_at_start(path_it->front())); + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + if ( shape->_marker[i] ) { + sp_marker_show_instance(shape->_marker[i], ai, + ai->key() + ITEM_KEY_MARKERS + i, counter[i], m, + shape->style->stroke_width.computed); + counter[i]++; + } + } + } + // MID position + if ( path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); // outgoing curve + while (curve_it2 != path_it->end_default()) + { + /* Put marker between curve_it1 and curve_it2. + * Loop to end_default (so including closing segment), because when a path is closed, + * there should be a midpoint marker between last segment and closing straight line segment + */ + Geom::Affine const m (sp_shape_marker_get_transform(*curve_it1, *curve_it2)); + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + if (shape->_marker[i]) { + sp_marker_show_instance(shape->_marker[i], ai, + ai->key() + ITEM_KEY_MARKERS + i, counter[i], m, + shape->style->stroke_width.computed); + counter[i]++; + } + } + + ++curve_it1; + ++curve_it2; + } + } + // END position + if ( path_it != (pathv.end()-1) && !path_it->empty()) { + Geom::Curve const &lastcurve = path_it->back_default(); + Geom::Affine const m = sp_shape_marker_get_transform_at_end(lastcurve); + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + if (shape->_marker[i]) { + sp_marker_show_instance(shape->_marker[i], ai, + ai->key() + ITEM_KEY_MARKERS + i, counter[i], m, + shape->style->stroke_width.computed); + counter[i]++; + } + } + } + } + } + + // END marker + if ( shape->_marker[SP_MARKER_LOC_END] || shape->_marker[SP_MARKER_LOC] ) { + /* Get reference to last curve in the path. + * For moveto-only path, this returns the "closing line segment". */ + Geom::Path const &path_last = pathv.back(); + unsigned int index = path_last.size_default(); + if (index > 0) { + index--; + } + Geom::Curve const &lastcurve = path_last[index]; + Geom::Affine const m = sp_shape_marker_get_transform_at_end(lastcurve); + + for (int i = 0; i < 4; i += 3) { // SP_MARKER_LOC and SP_MARKER_LOC_END + if (shape->_marker[i]) { + sp_marker_show_instance(shape->_marker[i], ai, + ai->key() + ITEM_KEY_MARKERS + i, counter[i], m, + shape->style->stroke_width.computed); + counter[i]++; + } + } + } +} + +void SPShape::modified(unsigned int flags) { + // std::cout << "SPShape::modified(): " << (getId()?getId():"null") << std::endl; + SPLPEItem::modified(flags); + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (auto &v : views) { + auto sh = cast<Inkscape::DrawingShape>(v.drawingitem.get()); + if (hasMarkers()) { + this->context_style = this->style; + sh->setStyle(this->style, this->context_style); + // Note: marker selector preview does not trigger SP_OBJECT_STYLE_MODIFIED_FLAG so + // this is not called when marker previews are generated, however there is code in + // SPShape::update() that calls this routine so we don't worry about it here. + sh->setChildrenStyle(this->context_style); // Resolve 'context-xxx' in children. + } else if (this->parent) { + this->context_style = this->parent->context_style; + sh->setStyle(this->style, this->context_style); + } + } + } + + if (flags & SP_OBJECT_MODIFIED_FLAG && style->filter.set) { + if (auto filter = style->getFilter()) { + filter->update_filter_all_regions(); + } + } + + if (!_curve) { + sp_lpe_item_update_patheffect(this, true, false); + } +} + +bool SPShape::checkBrokenPathEffect() +{ + if (hasBrokenPathEffect()) { + g_warning("The shape has unknown LPE on it. Convert to path to make it editable preserving the appearance; " + "editing it will remove the bad LPE"); + + if (this->getRepr()->attribute("d")) { + // unconditionally read the curve from d, if any, to preserve appearance + setCurveInsync(SPCurve(sp_svg_read_pathv(getAttribute("d")))); + setCurveBeforeLPE(curve()); + } + + return true; + } + return false; +} + +/* Reset the shape's curve to the "original_curve" + * This is very important for LPEs to work properly! (the bbox might be recalculated depending on the curve in shape)*/ + +bool SPShape::prepareShapeForLPE(SPCurve const *c) +{ + auto const before = curveBeforeLPE(); + if (before && before->get_pathvector() != c->get_pathvector()) { + setCurveBeforeLPE(c); + sp_lpe_item_update_patheffect(this, true, false); + return true; + } + + if (hasPathEffectOnClipOrMaskRecursive(this)) { + if (!before && this->getRepr()->attribute("d")) { + setCurveInsync(SPCurve(sp_svg_read_pathv(getAttribute("d")))); + } + setCurveBeforeLPE(c); + return true; + } + setCurveInsync(c); + return false; +} + +Geom::OptRect SPShape::bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype) const { + // If the object is clipped, the update function that invalidates + // the cache doesn't get called if the object is moved, so we need + // to compare the transformations as well. + + if (bboxtype == SPItem::VISUAL_BBOX) { + bbox_vis_cache = + either_bbox(transform, bboxtype, bbox_vis_cache_is_valid, bbox_vis_cache, bbox_vis_cache_transform); + if (bbox_vis_cache) { + bbox_vis_cache_transform = transform; + bbox_vis_cache_is_valid = true; + } + return bbox_vis_cache; + } else { + bbox_geom_cache = + either_bbox(transform, bboxtype, bbox_geom_cache_is_valid, bbox_geom_cache, bbox_geom_cache_transform); + if (bbox_geom_cache) { + bbox_geom_cache_transform = transform; + bbox_geom_cache_is_valid = true; + } + return bbox_geom_cache; + } +} + +Geom::OptRect SPShape::either_bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype, bool cache_is_valid, + Geom::OptRect bbox_cache, Geom::Affine const &transform_cache) const +{ + + Geom::OptRect bbox; + + // Return the cache if possible. + auto delta = transform_cache.inverse() * transform; + if (cache_is_valid && bbox_cache && delta.isTranslation()) { + + // Don't re-adjust the cache if we haven't moved + if (!delta.isNonzeroTranslation()) { + return bbox_cache; + } + // delta is pure translation so it's safe to use it as is + return *bbox_cache * delta; + } + + if (!this->_curve || this->_curve->get_pathvector().empty()) { + return bbox; + } + + bbox = bounds_exact_transformed(this->_curve->get_pathvector(), transform); + + if (!bbox) { + return bbox; + } + + if (bboxtype == SPItem::VISUAL_BBOX) { + // convert the stroke to a path and calculate that path's geometric bbox + + if (!this->style->stroke.isNone() && !this->style->stroke_extensions.hairline) { + Geom::PathVector *pathv = item_to_outline(this, true); // calculate bbox_only + + if (pathv) { + bbox |= bounds_exact_transformed(*pathv, transform); + delete pathv; + } + } + + // Union with bboxes of the markers, if any + if ( this->hasMarkers() && !this->_curve->get_pathvector().empty() ) { + /** \todo make code prettier! */ + Geom::PathVector const & pathv = this->_curve->get_pathvector(); + // START marker + for (unsigned i = 0; i < 2; i++) { // SP_MARKER_LOC and SP_MARKER_LOC_START + if ( this->_marker[i] ) { + SPItem* marker_item = sp_item_first_item_child( _marker[i] ); + + if (marker_item) { + Geom::Affine tr(sp_shape_marker_get_transform_at_start(pathv.begin()->front())); + tr = _marker[i]->get_marker_transform(tr, this->style->stroke_width.computed, true); + + // total marker transform + tr = marker_item->transform * _marker[i]->c2p * tr * transform; + + // get bbox of the marker with that transform + bbox |= marker_item->visualBounds(tr); + } + } + } + + // MID marker + for (unsigned i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + if ( !this->_marker[i] ) { + continue; + } + + SPMarker* marker = _marker[i]; + SPItem* marker_item = sp_item_first_item_child( marker ); + + if ( !marker_item ) { + continue; + } + + for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) { + // START position + if ( path_it != pathv.begin() + && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, there is no mid marker there + { + Geom::Affine tr(sp_shape_marker_get_transform_at_start(path_it->front())); + tr = marker->get_marker_transform(tr, this->style->stroke_width.computed, false); + tr = marker_item->transform * marker->c2p * tr * transform; + bbox |= marker_item->visualBounds(tr); + } + + // MID position + if ( path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); // outgoing curve + + while (curve_it2 != path_it->end_default()) + { + /* Put marker between curve_it1 and curve_it2. + * Loop to end_default (so including closing segment), because when a path is closed, + * there should be a midpoint marker between last segment and closing straight line segment */ + + SPMarker* marker = _marker[i]; + SPItem* marker_item = sp_item_first_item_child( marker ); + + if (marker_item) { + Geom::Affine tr(sp_shape_marker_get_transform(*curve_it1, *curve_it2)); + tr = marker->get_marker_transform(tr, this->style->stroke_width.computed, false); + tr = marker_item->transform * marker->c2p * tr * transform; + bbox |= marker_item->visualBounds(tr); + } + + ++curve_it1; + ++curve_it2; + } + } + + // END position + if ( path_it != (pathv.end()-1) && !path_it->empty()) { + Geom::Curve const &lastcurve = path_it->back_default(); + Geom::Affine tr = sp_shape_marker_get_transform_at_end(lastcurve); + tr = marker->get_marker_transform(tr, this->style->stroke_width.computed, false); + tr = marker_item->transform * marker->c2p * tr * transform; + bbox |= marker_item->visualBounds(tr); + } + } + } + + // END marker + for (unsigned i = 0; i < 4; i += 3) { // SP_MARKER_LOC and SP_MARKER_LOC_END + if ( _marker[i] ) { + SPMarker* marker = _marker[i]; + SPItem* marker_item = sp_item_first_item_child( marker ); + + if (marker_item) { + /* Get reference to last curve in the path. + * For moveto-only path, this returns the "closing line segment". */ + Geom::Path const &path_last = pathv.back(); + unsigned int index = path_last.size_default(); + + if (index > 0) { + index--; + } + + Geom::Curve const &lastcurve = path_last[index]; + + Geom::Affine tr = sp_shape_marker_get_transform_at_end(lastcurve); + tr = marker->get_marker_transform(tr, this->style->stroke_width.computed, false); + tr = marker_item->transform * marker->c2p * tr * transform; + + // get bbox of the marker with that transform + bbox |= marker_item->visualBounds(tr); + } + } + } + } + } + + + return bbox; +} + +static void +sp_shape_print_invoke_marker_printing(SPObject *obj, Geom::Affine tr, SPPrintContext *ctx) +{ + auto marker = cast<SPMarker>(obj); + SPItem* marker_item = sp_item_first_item_child( marker ); + if (marker_item) { + tr = marker_item->transform * marker->c2p * tr; + + Geom::Affine old_tr = marker_item->transform; + marker_item->transform = tr; + marker_item->invoke_print (ctx); + marker_item->transform = old_tr; + } +} + +void SPShape::print(SPPrintContext* ctx) { + if (!this->_curve) { + return; + } + + Geom::PathVector const & pathv = this->_curve->get_pathvector(); + + if (pathv.empty()) { + return; + } + + /* fixme: Think (Lauris) */ + Geom::OptRect pbox, dbox, bbox; + pbox = this->geometricBounds(); + bbox = this->desktopVisualBounds(); + dbox = Geom::Rect::from_xywh(Geom::Point(0,0), this->document->getDimensions()); + + Geom::Affine const i2dt(this->i2dt_affine()); + + // Copy the style for this printable item + SPStyle *style = this->style; + SPStyle *new_style = nullptr; + + if (ctx->context_item) { + new_style = new SPStyle(document, this); + new_style->merge(style); + // Set style contexts for print here + if (style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE) + new_style->fill.overwrite(ctx->context_item->style->stroke.upcast()); + if (style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL) + new_style->fill.overwrite(ctx->context_item->style->fill.upcast()); + if (style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE) + new_style->stroke.overwrite(ctx->context_item->style->stroke.upcast()); + if (style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL) + new_style->stroke.overwrite(ctx->context_item->style->fill.upcast()); + style = new_style; + } + + if (!style->fill.isNone()) { + ctx->fill (pathv, i2dt, style, pbox, dbox, bbox); + } + + if (!style->stroke.isNone()) { + ctx->stroke (pathv, i2dt, style, pbox, dbox, bbox); + } + + auto linewidth = this->style->stroke_width.computed; + + if (new_style) { + // Clean up temporary context style copy + delete new_style; + } + + /** \todo make code prettier */ + // START marker + for (int i = 0; i < 2; i++) { // SP_MARKER_LOC and SP_MARKER_LOC_START + if (auto marker = this->_marker[i]) { + Geom::Affine tr(sp_shape_marker_get_transform_at_start(pathv.begin()->front())); + tr = marker->get_marker_transform(tr, linewidth, true); + ctx->context_item = this; + sp_shape_print_invoke_marker_printing(marker, tr, ctx); + } + } + + // MID marker + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + if (auto marker = this->_marker[i]) { + for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) { + // START position + if ( path_it != pathv.begin() + && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, there is no mid marker there + { + Geom::Affine tr(sp_shape_marker_get_transform_at_start(path_it->front())); + tr = marker->get_marker_transform(tr, linewidth, false); + ctx->context_item = this; + sp_shape_print_invoke_marker_printing(marker, tr, ctx); + } + + // MID position + if ( path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); // outgoing curve + + while (curve_it2 != path_it->end_default()) + { + /* Put marker between curve_it1 and curve_it2. + * Loop to end_default (so including closing segment), because when a path is closed, + * there should be a midpoint marker between last segment and closing straight line segment */ + Geom::Affine tr(sp_shape_marker_get_transform(*curve_it1, *curve_it2)); + tr = marker->get_marker_transform(tr, linewidth, false); + + ctx->context_item = this; + sp_shape_print_invoke_marker_printing(marker, tr, ctx); + + ++curve_it1; + ++curve_it2; + } + } + + if ( path_it != (pathv.end()-1) && !path_it->empty()) { + Geom::Curve const &lastcurve = path_it->back_default(); + Geom::Affine tr = sp_shape_marker_get_transform_at_end(lastcurve); + tr = marker->get_marker_transform(tr, linewidth, false); + + ctx->context_item = this; + sp_shape_print_invoke_marker_printing(marker, tr, ctx); + } + } + } + } + + // END marker + if ( this->_marker[SP_MARKER_LOC_END] || this->_marker[SP_MARKER_LOC]) { + /* Get reference to last curve in the path. + * For moveto-only path, this returns the "closing line segment". */ + Geom::Path const &path_last = pathv.back(); + unsigned int index = path_last.size_default(); + + if (index > 0) { + index--; + } + + Geom::Curve const &lastcurve = path_last[index]; + + for (int i = 0; i < 4; i += 3) { // SP_MARKER_LOC and SP_MARKER_LOC_END + if (auto marker = this->_marker[i]) { + Geom::Affine tr = sp_shape_marker_get_transform_at_end(lastcurve); + tr = marker->get_marker_transform(tr, linewidth, false); + + ctx->context_item = this; + sp_shape_print_invoke_marker_printing(marker, tr, ctx); + } + } + } + + // Clear any context item used in the above markers. + ctx->context_item = nullptr; +} + +std::optional<Geom::PathVector> SPShape::documentExactBounds() const +{ + std::optional<Geom::PathVector> result; + if (auto const *c = curve()) { + result = c->get_pathvector() * i2doc_affine(); + } + return result; +} + +void SPShape::update_patheffect(bool write) +{ + if (!curveForEdit()) { + set_shape(); + } + if (curveForEdit()) { + auto c_lpe = *curveForEdit(); + /* if a path has an lpeitem applied, then reset the curve to the _curve_before_lpe. + * This is very important for LPEs to work properly! (the bbox might be recalculated depending on the curve in shape)*/ + setCurveInsync(&c_lpe); + SPRoot *root = document->getRoot(); + if (!sp_version_inside_range(root->version.inkscape, 0, 1, 0, 92)) { + resetClipPathAndMaskLPE(); + } + + bool success = false; + // avoid update lpe in each selection + // must be set also to non effect items (satellites or parents) + lpe_initialized = true; + if (hasPathEffect() && pathEffectsEnabled()) { + success = this->performPathEffect(&c_lpe, this); + if (success) { + setCurveInsync(&c_lpe); + applyToClipPath(this); + applyToMask(this); + } + } + if (write && success) { + if (auto repr = getRepr()) { + repr->setAttribute("d", sp_svg_write_path(c_lpe.get_pathvector())); + } + } + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +Inkscape::DrawingItem* SPShape::show(Inkscape::Drawing &drawing, unsigned int /*key*/, unsigned int /*flags*/) { + // std::cout << "SPShape::show(): " << (getId()?getId():"null") << std::endl; + Inkscape::DrawingShape *s = new Inkscape::DrawingShape(drawing); + + bool has_markers = this->hasMarkers(); + + s->setPath(_curve); + + /* This stanza checks that an object's marker style agrees with + * the marker objects it has allocated. sp_shape_set_marker ensures + * that the appropriate marker objects are present (or absent) to + * match the style. + */ + for (int i = 0; i < SP_MARKER_LOC_QTY; i++) { + set_marker(i, style->marker_ptrs[i]->value()); + } + + if (has_markers) { + /* provide key and dimension the marker views */ + SPItem::ensure_key(s); + for (int i = 0; i < SP_MARKER_LOC_QTY; i++) { + if (_marker[i]) { + sp_marker_show_dimension(_marker[i], s->key() + ITEM_KEY_MARKERS + i, numberOfMarkers(i)); + } + } + + /* Update marker views */ + sp_shape_update_marker_view(this, s); + + this->context_style = this->style; + s->setStyle(this->style, this->context_style); + s->setChildrenStyle(this->context_style); // Resolve 'context-xxx' in children. + } else if (this->parent) { + this->context_style = this->parent->context_style; + s->setStyle(this->style, this->context_style); + } + return s; +} + +/** + * Sets style, path, and paintbox. Updates marker views, including dimensions. + */ +void SPShape::hide(unsigned key) +{ + for (int i = 0; i < SP_MARKER_LOC_QTY; ++i) { + if (_marker[i]) { + for (auto &v : views) { + if (key == v.key) { + sp_marker_hide(_marker[i], v.drawingitem->key() + ITEM_KEY_MARKERS + i); + } + } + } + } + + //SPLPEItem::onHide(key); +} + +/** +* \param shape Shape. +* \return TRUE if the shape has any markers, or FALSE if not. +*/ +int SPShape::hasMarkers() const +{ + /* Note, we're ignoring 'marker' settings, which technically should apply for + all three settings. This should be fixed later such that if 'marker' is + specified, then all three should appear. */ + + // Ignore markers for objects which are inside markers themselves. + for (SPObject *parent = this->parent; parent != nullptr; parent = parent->parent) { + if (is<SPMarker>(parent)) { + return 0; + } + } + + return ( + this->_curve && + (this->_marker[SP_MARKER_LOC] || + this->_marker[SP_MARKER_LOC_START] || + this->_marker[SP_MARKER_LOC_MID] || + this->_marker[SP_MARKER_LOC_END]) + ); +} + + +/** +* \param shape Shape. +* \param type Marker type (e.g. SP_MARKER_LOC_START) +* \return Number of markers that the shape has of this type. +*/ +int SPShape::numberOfMarkers(int type) const { + Geom::PathVector const & pathv = this->_curve->get_pathvector(); + + if (pathv.size() == 0) { + return 0; + } + switch(type) { + + case SP_MARKER_LOC: + { + if ( this->_marker[SP_MARKER_LOC] ) { + guint n = 0; + for(const auto & path_it : pathv) { + n += path_it.size_default() + 1; + } + return n; + } else { + return 0; + } + } + case SP_MARKER_LOC_START: + // there is only a start marker on the first path of a pathvector + return this->_marker[SP_MARKER_LOC_START] ? 1 : 0; + + case SP_MARKER_LOC_MID: + { + if ( this->_marker[SP_MARKER_LOC_MID] ) { + guint n = 0; + for(const auto & path_it : pathv) { + n += path_it.size_default() + 1; + } + n = (n > 1) ? (n - 2) : 0; // Minus the start and end marker, but never negative. + // A path or polyline may have only one point. + return n; + } else { + return 0; + } + } + + case SP_MARKER_LOC_END: + { + // there is only an end marker on the last path of a pathvector + return this->_marker[SP_MARKER_LOC_END] ? 1 : 0; + } + + default: + return 0; + } +} + +/** + * Checks if the given marker is used in the shape, and if so, it + * releases it by calling sp_marker_hide. Also detaches signals + * and unrefs the marker from the shape. + */ +static void +sp_shape_marker_release(SPObject *marker, SPShape *shape) +{ + auto item = shape; + g_return_if_fail(item != nullptr); + + for (int i = 0; i < SP_MARKER_LOC_QTY; i++) { + if (marker == shape->_marker[i]) { + /* Hide marker */ + for (auto &v : item->views) { + sp_marker_hide(shape->_marker[i], v.drawingitem->key() + ITEM_KEY_MARKERS + i); + } + /* Detach marker */ + shape->_release_connect[i].disconnect(); + shape->_modified_connect[i].disconnect(); + shape->_marker[i]->unhrefObject(item); + shape->_marker[i] = nullptr; + } + } +} + +/** + * No-op. Exists for handling 'modified' messages + */ +static void sp_shape_marker_modified (SPObject* marker, guint flags, SPItem* item) { + if ((flags & SP_OBJECT_MODIFIED_FLAG) && item && marker) { + // changing marker can impact object's visual bounding box, so request update on this object itself + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +/** + * Adds a new marker to shape object at the location indicated by key. value + * must be a valid URI reference resolvable from the shape object (i.e., present + * in the document <defs>). If the shape object already has a marker + * registered at the given position, it is removed first. Then the + * new marker is hrefed and its signals connected. + */ +void SPShape::set_marker(unsigned key, char const *value) +{ + if (key > SP_MARKER_LOC_END) { + return; + } + + auto mrk = sp_css_uri_reference_resolve(document, value); + auto marker = cast<SPMarker>(mrk); + + if (marker != _marker[key]) { + if (_marker[key]) { + /* Detach marker */ + _release_connect[key].disconnect(); + _modified_connect[key].disconnect(); + + /* Hide marker */ + for (auto &v : views) { + sp_marker_hide(_marker[key], v.drawingitem->key() + ITEM_KEY_MARKERS + key); + } + + /* Unref marker */ + _marker[key]->unhrefObject(this); + _marker[key] = nullptr; + } + if (marker) { + _marker[key] = marker; + _marker[key]->hrefObject(this); + _release_connect[key] = marker->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_shape_marker_release), this)); + _modified_connect[key] = marker->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_shape_marker_modified), this)); + } + } +} + +// CPPIFY: make pure virtual +void SPShape::set_shape() { + //throw; +} + +/* Shape section */ + +/** + * Adds a curve to the shape. + * Any existing curve in the shape will be unreferenced first. + * This routine also triggers a request to update the display. + */ +void SPShape::setCurve(SPCurve new_curve) +{ + _curve = std::make_shared<SPCurve>(std::move(new_curve)); + if (document) { + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} +void SPShape::setCurve(SPCurve const *new_curve) +{ + if (new_curve) { + setCurve(*new_curve); + } else { + _curve.reset(); + } +} + +/** + * Sets _curve_before_lpe to a copy of `new_curve` + */ +void SPShape::setCurveBeforeLPE(SPCurve new_curve) +{ + _curve_before_lpe = std::move(new_curve); +} +void SPShape::setCurveBeforeLPE(SPCurve const *new_curve) +{ + if (new_curve) { + setCurveBeforeLPE(*new_curve); + } else { + _curve_before_lpe.reset(); + } +} + +/** + * Same as setCurve() but without updating the display + */ +void SPShape::setCurveInsync(SPCurve new_curve) +{ + _curve = std::make_shared<SPCurve>(std::move(new_curve)); +} +void SPShape::setCurveInsync(SPCurve const *new_curve) +{ + if (new_curve) { + setCurveInsync(*new_curve); + } else { + _curve.reset(); + } +} + +/** + * Return a borrowed pointer to the curve (if any exists) or NULL if there is no curve + */ +SPCurve const *SPShape::curve() const +{ + return _curve.get(); +} + +/** + * Return a borrowed pointer of the curve *before* LPE (if any exists) or NULL if there is no curve + */ +SPCurve const *SPShape::curveBeforeLPE() const +{ + return _curve_before_lpe ? &*_curve_before_lpe : nullptr; +} + +/** + * Return a borrowed pointer of the curve for edit + */ +SPCurve const *SPShape::curveForEdit() const +{ + return _curve_before_lpe ? &*_curve_before_lpe : curve(); +} + +void SPShape::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + if (this->_curve == nullptr) { + return; + } + + Geom::PathVector const &pathv = this->_curve->get_pathvector(); + + if (pathv.empty()) { + return; + } + + Geom::Affine const i2dt (this->i2dt_affine ()); + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT)) { + Geom::OptRect bbox = this->desktopVisualBounds(); + + if (bbox) { + p.emplace_back(bbox->midpoint(), Inkscape::SNAPSOURCE_OBJECT_MIDPOINT, Inkscape::SNAPTARGET_OBJECT_MIDPOINT); + } + } + + for(const auto & path_it : pathv) { + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_NODE_CUSP)) { + // Add the first point of the path + p.emplace_back(path_it.initialPoint() * i2dt, Inkscape::SNAPSOURCE_NODE_CUSP, Inkscape::SNAPTARGET_NODE_CUSP); + } + + Geom::Path::const_iterator curve_it1 = path_it.begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); // outgoing curve + + while (curve_it1 != path_it.end_default()) + { + // For each path: consider midpoints of line segments for snapping + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_LINE_MIDPOINT)) { + if (Geom::LineSegment const* line_segment = dynamic_cast<Geom::LineSegment const*>(&(*curve_it1))) { + p.emplace_back(Geom::middle_point(*line_segment) * i2dt, Inkscape::SNAPSOURCE_LINE_MIDPOINT, Inkscape::SNAPTARGET_LINE_MIDPOINT); + } + } + + if (curve_it2 == path_it.end_default()) { // Test will only pass for the last iteration of the while loop + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_NODE_CUSP) && !path_it.closed()) { + // Add the last point of the path, but only for open paths + // (for closed paths the first and last point will coincide) + p.emplace_back((*curve_it1).finalPoint() * i2dt, Inkscape::SNAPSOURCE_NODE_CUSP, Inkscape::SNAPTARGET_NODE_CUSP); + } + } else { + /* Test whether to add the node between curve_it1 and curve_it2. + * Loop to end_default (so only iterating through the stroked part); */ + + Geom::NodeType nodetype = Geom::get_nodetype(*curve_it1, *curve_it2); + + bool c1 = snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_NODE_CUSP) && (nodetype == Geom::NODE_CUSP || nodetype == Geom::NODE_NONE); + bool c2 = snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_NODE_SMOOTH) && (nodetype == Geom::NODE_SMOOTH || nodetype == Geom::NODE_SYMM); + + if (c1 || c2) { + Inkscape::SnapSourceType sst; + Inkscape::SnapTargetType stt; + + switch (nodetype) { + case Geom::NODE_CUSP: + sst = Inkscape::SNAPSOURCE_NODE_CUSP; + stt = Inkscape::SNAPTARGET_NODE_CUSP; + break; + case Geom::NODE_SMOOTH: + case Geom::NODE_SYMM: + sst = Inkscape::SNAPSOURCE_NODE_SMOOTH; + stt = Inkscape::SNAPTARGET_NODE_SMOOTH; + break; + default: + sst = Inkscape::SNAPSOURCE_UNDEFINED; + stt = Inkscape::SNAPTARGET_UNDEFINED; + break; + } + + p.emplace_back(curve_it1->finalPoint() * i2dt, sst, stt); + } + } + + ++curve_it1; + ++curve_it2; + } + + // Find the internal intersections of each path and consider these for snapping + // (using "Method 1" as described in Inkscape::ObjectSnapper::_collectNodes()) + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_PATH_INTERSECTION) || snapprefs->isSourceSnappable(Inkscape::SNAPSOURCE_PATH_INTERSECTION)) { + Geom::Crossings cs; + + try { + cs = self_crossings(path_it); // This can be slow! + + if (!cs.empty()) { // There might be multiple intersections... + for (const auto & c : cs) { + Geom::Point p_ix = path_it.pointAt(c.ta); + p.emplace_back(p_ix * i2dt, Inkscape::SNAPSOURCE_PATH_INTERSECTION, Inkscape::SNAPTARGET_PATH_INTERSECTION); + } + } + } catch (Geom::RangeError &e) { + // do nothing + // The exception could be Geom::InfiniteSolutions: then no snappoints should be added + } + + } + } +} + +/* + 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 : diff --git a/src/object/sp-shape.h b/src/object/sp-shape.h new file mode 100644 index 0000000..9d25729 --- /dev/null +++ b/src/object/sp-shape.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SHAPE_H +#define SEEN_SP_SHAPE_H + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * Johan Engelen + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 1999-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> +#include <cstddef> +#include <sigc++/connection.h> + +#include "sp-lpe-item.h" +#include "sp-marker-loc.h" +#include "display/curve.h" + +#include <memory> + +#define SP_SHAPE_WRITE_PATH (1 << 2) + +class SPDesktop; +class SPMarker; +namespace Inkscape { class DrawingItem; } + +/** + * Base class for shapes, including <path> element + */ +class SPShape : public SPLPEItem { +public: + SPShape(); + ~SPShape() override; + int tag() const override { return tag_of<decltype(*this)>; } + + SPCurve const *curve() const; + SPCurve const *curveBeforeLPE() const; + SPCurve const *curveForEdit() const; + +public: + void setCurve(SPCurve const *); + void setCurve(SPCurve); + void setCurveInsync(SPCurve const *); + void setCurveInsync(SPCurve); + void setCurveBeforeLPE(SPCurve const *); + void setCurveBeforeLPE(SPCurve); + bool checkBrokenPathEffect(); + bool prepareShapeForLPE(SPCurve const *c); + int hasMarkers () const; + int numberOfMarkers (int type) const; + + // bbox cache + mutable bool bbox_geom_cache_is_valid = false; + mutable bool bbox_vis_cache_is_valid = false; + mutable Geom::Affine bbox_geom_cache_transform; + mutable Geom::Affine bbox_vis_cache_transform; + mutable Geom::OptRect bbox_geom_cache; + mutable Geom::OptRect bbox_vis_cache; + +protected: + std::optional<SPCurve> _curve_before_lpe; + std::shared_ptr<SPCurve const> _curve; + +public: + SPMarker *_marker[SP_MARKER_LOC_QTY]; + sigc::connection _release_connect [SP_MARKER_LOC_QTY]; + sigc::connection _modified_connect [SP_MARKER_LOC_QTY]; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + void set(SPAttr key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype) const override; + Geom::OptRect either_bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype, bool cache_is_valid, + Geom::OptRect bbox_cache, Geom::Affine const &transform_cache) const; + void print(SPPrintContext* ctx) override; + std::optional<Geom::PathVector> documentExactBounds() const override; + + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void hide(unsigned int key) override; + + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + + virtual void set_shape(); + void update_patheffect(bool write) override; + + void set_marker(unsigned key, char const *value); +}; + +Geom::Affine sp_shape_marker_get_transform(Geom::Curve const & c1, Geom::Curve const & c2); +Geom::Affine sp_shape_marker_get_transform_at_start(Geom::Curve const & c); +Geom::Affine sp_shape_marker_get_transform_at_end(Geom::Curve const & c); + +#endif // SEEN_SP_SHAPE_H + +/* + 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 : diff --git a/src/object/sp-solid-color.cpp b/src/object/sp-solid-color.cpp new file mode 100644 index 0000000..b7d520a --- /dev/null +++ b/src/object/sp-solid-color.cpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @solid color class. + */ +/* Authors: + * Tavmjong Bah <tavjong@free.fr> + * + * Copyright (C) 2014 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cairo.h> + +#include "sp-solid-color.h" + +#include "attributes.h" +#include "style.h" +#include "display/drawing-paintserver.h" + +/* + * Solid Color + */ +SPSolidColor::SPSolidColor() : SPPaintServer() { +} + +SPSolidColor::~SPSolidColor() = default; + +void SPSolidColor::build(SPDocument* doc, Inkscape::XML::Node* repr) { + SPPaintServer::build(doc, repr); + + this->readAttr(SPAttr::STYLE); + this->readAttr(SPAttr::SOLID_COLOR); + this->readAttr(SPAttr::SOLID_OPACITY); +} + +/** + * Virtual build: set solidcolor attributes from its associated XML node. + */ + +void SPSolidColor::set(SPAttr key, const gchar* value) { + + if (SP_ATTRIBUTE_IS_CSS(key)) { + style->clear(key); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } else { + SPPaintServer::set(key, value); + } +} + +/** + * Virtual set: set attribute to value. + */ + +Inkscape::XML::Node* SPSolidColor::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:solidColor"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +std::unique_ptr<Inkscape::DrawingPaintServer> SPSolidColor::create_drawing_paintserver() +{ + return std::make_unique<Inkscape::DrawingSolidColor>(style->solid_color.value.color.v.c, SP_SCALE24_TO_FLOAT(style->solid_opacity.value)); +} + +/* + 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 : diff --git a/src/object/sp-solid-color.h b/src/object/sp-solid-color.h new file mode 100644 index 0000000..6b5b472 --- /dev/null +++ b/src/object/sp-solid-color.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SOLIDCOLOR_H +#define SEEN_SP_SOLIDCOLOR_H + +/** \file + * SPSolidColor: SVG <solidColor> implementation. + */ +/* + * Authors: Tavmjong Bah + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "color.h" +#include "sp-paint-server.h" + +typedef struct _cairo cairo_t; +typedef struct _cairo_pattern cairo_pattern_t; + +/** Gradient SolidColor. */ +class SPSolidColor final + : public SPPaintServer +{ +public: + SPSolidColor(); + ~SPSolidColor() override; + int tag() const override { return tag_of<decltype(*this)>; } + + std::unique_ptr<Inkscape::DrawingPaintServer> create_drawing_paintserver() override; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttr key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif /* !SEEN_SP_SOLIDCOLOR_H */ + +/* + 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 : diff --git a/src/object/sp-spiral.cpp b/src/object/sp-spiral.cpp new file mode 100644 index 0000000..7be01df --- /dev/null +++ b/src/object/sp-spiral.cpp @@ -0,0 +1,565 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * <sodipodi:spiral> implementation + */ +/* + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "svg/svg.h" +#include "attributes.h" +#include <2geom/bezier-utils.h> +#include <2geom/pathvector.h> +#include "display/curve.h" +#include <glibmm/i18n.h> +#include "xml/repr.h" +#include "document.h" + +#include "sp-spiral.h" + +SPSpiral::SPSpiral() + : SPShape() + , cx(0) + , cy(0) + , exp(1) + , revo(3) + , rad(1) + , arg(0) + , t0(0) +{ +} + +SPSpiral::~SPSpiral() = default; + +void SPSpiral::build(SPDocument * document, Inkscape::XML::Node * repr) { + SPShape::build(document, repr); + + this->readAttr(SPAttr::SODIPODI_CX); + this->readAttr(SPAttr::SODIPODI_CY); + this->readAttr(SPAttr::SODIPODI_EXPANSION); + this->readAttr(SPAttr::SODIPODI_REVOLUTION); + this->readAttr(SPAttr::SODIPODI_RADIUS); + this->readAttr(SPAttr::SODIPODI_ARGUMENT); + this->readAttr(SPAttr::SODIPODI_T0); +} + +Inkscape::XML::Node* SPSpiral::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:path"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + /* Fixme: we may replace these attributes by + * sodipodi:spiral="cx cy exp revo rad arg t0" + */ + repr->setAttribute("sodipodi:type", "spiral"); + repr->setAttributeSvgDouble("sodipodi:cx", this->cx); + repr->setAttributeSvgDouble("sodipodi:cy", this->cy); + repr->setAttributeSvgDouble("sodipodi:expansion", this->exp); + repr->setAttributeSvgDouble("sodipodi:revolution", this->revo); + repr->setAttributeSvgDouble("sodipodi:radius", this->rad); + repr->setAttributeSvgDouble("sodipodi:argument", this->arg); + repr->setAttributeSvgDouble("sodipodi:t0", this->t0); + } + + // make sure the curve is rebuilt with all up-to-date parameters + this->set_shape(); + + // Nulls might be possible if this called iteratively + if (!this->_curve) { + //g_warning("sp_spiral_write(): No path to copy"); + return nullptr; + } + + repr->setAttribute("d", sp_svg_write_path(this->_curve->get_pathvector())); + + SPShape::write(xml_doc, repr, flags | SP_SHAPE_WRITE_PATH); + + return repr; +} + +void SPSpiral::set(SPAttr key, gchar const* value) { + /// \todo fixme: we should really collect updates + switch (key) { + case SPAttr::SODIPODI_CX: + if (!sp_svg_length_read_computed_absolute (value, &this->cx)) { + this->cx = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_CY: + if (!sp_svg_length_read_computed_absolute (value, &this->cy)) { + this->cy = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_EXPANSION: + if (value) { + /** \todo + * FIXME: check that value looks like a (finite) + * number. Create a routine that uses strtod, and + * accepts a default value (if strtod finds an error). + * N.B. atof/sscanf/strtod consider "nan" and "inf" + * to be valid numbers. + */ + this->exp = g_ascii_strtod (value, nullptr); + this->exp = CLAMP (this->exp, 0.0, 1000.0); + } else { + this->exp = 1.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_REVOLUTION: + if (value) { + this->revo = g_ascii_strtod (value, nullptr); + this->revo = CLAMP (this->revo, 0.05, 1024.0); + } else { + this->revo = 3.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_RADIUS: + if (!sp_svg_length_read_computed_absolute (value, &this->rad)) { + this->rad = MAX (this->rad, 0.001); + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_ARGUMENT: + if (value) { + this->arg = g_ascii_strtod (value, nullptr); + /** \todo + * FIXME: We still need some bounds on arg, for + * numerical reasons. E.g., we don't want inf or NaN, + * nor near-infinite numbers. I'm inclined to take + * modulo 2*pi. If so, then change the knot editors, + * which use atan2 - revo*2*pi, which typically + * results in very negative arg. + */ + } else { + this->arg = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_T0: + if (value) { + this->t0 = g_ascii_strtod (value, nullptr); + this->t0 = CLAMP (this->t0, 0.0, 0.999); + /** \todo + * Have shared constants for the allowable bounds for + * attributes. There was a bug here where we used -1.0 + * as the minimum (which leads to NaN via, e.g., + * pow(-1.0, 0.5); see sp_spiral_get_xy for + * requirements. + */ + } else { + this->t0 = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPShape::set(key, value); + break; + } +} + +void SPSpiral::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + this->set_shape(); + } + + SPShape::update(ctx, flags); +} + +const char* SPSpiral::typeName() const { + return "spiral"; +} + +const char* SPSpiral::displayName() const { + return _("Spiral"); +} + +gchar* SPSpiral::description() const { + // TRANSLATORS: since turn count isn't an integer, please adjust the + // string as needed to deal with an localized plural forms. + return g_strdup_printf (_("with %3f turns"), this->revo); +} + +/** + * Fit beziers together to spiral and draw it. + * + * \pre dstep \> 0. + * \pre is_unit_vector(*hat1). + * \post is_unit_vector(*hat2). + **/ +void SPSpiral::fitAndDraw(SPCurve* c, double dstep, Geom::Point darray[], Geom::Point const& hat1, Geom::Point& hat2, double* t) const { +#define BEZIER_SIZE 4 +#define FITTING_MAX_BEZIERS 4 +#define BEZIER_LENGTH (BEZIER_SIZE * FITTING_MAX_BEZIERS) + + g_assert (dstep > 0); + g_assert (is_unit_vector (hat1)); + + Geom::Point bezier[BEZIER_LENGTH]; + double d; + int depth, i; + + for (d = *t, i = 0; i <= SAMPLE_SIZE; d += dstep, i++) { + darray[i] = this->getXY(d); + + /* Avoid useless adjacent dups. (Otherwise we can have all of darray filled with + the same value, which upsets chord_length_parameterize.) */ + if ((i != 0) && (darray[i] == darray[i - 1]) && (d < 1.0)) { + i--; + d += dstep; + /** We mustn't increase dstep for subsequent values of + * i: for large spiral.exp values, rate of growth + * increases very rapidly. + */ + /** \todo + * Get the function itself to decide what value of d + * to use next: ensure that we move at least 0.25 * + * stroke width, for example. The derivative (as used + * for get_tangent before normalization) would be + * useful for estimating the appropriate d value. Or + * perhaps just start with a small dstep and scale by + * some small number until we move >= 0.25 * + * stroke_width. Must revert to the original dstep + * value for next iteration to avoid the problem + * mentioned above. + */ + } + } + + double const next_t = d - 2 * dstep; + /* == t + (SAMPLE_SIZE - 1) * dstep, in absence of dups. */ + + hat2 = -this->getTangent(next_t); + + /** \todo + * We should use better algorithm to specify maximum error. + */ + depth = Geom::bezier_fit_cubic_full (bezier, nullptr, darray, SAMPLE_SIZE, + hat1, hat2, + SPIRAL_TOLERANCE*SPIRAL_TOLERANCE, + FITTING_MAX_BEZIERS); + + g_assert(depth * BEZIER_SIZE <= gint(G_N_ELEMENTS(bezier))); + +#ifdef SPIRAL_DEBUG + if (*t == spiral->t0 || *t == 1.0) + g_print ("[%s] depth=%d, dstep=%g, t0=%g, t=%g, arg=%g\n", + debug_state, depth, dstep, spiral->t0, *t, spiral->arg); +#endif + + if (depth != -1) { + for (i = 0; i < 4*depth; i += 4) { + c->curveto(bezier[i + 1], + bezier[i + 2], + bezier[i + 3]); + } + } else { +#ifdef SPIRAL_VERBOSE + g_print ("cant_fit_cubic: t=%g\n", *t); +#endif + for (i = 1; i < SAMPLE_SIZE; i++) + c->lineto(darray[i]); + } + + *t = next_t; + + g_assert (is_unit_vector (hat2)); +} + +void SPSpiral::set_shape() { + if (checkBrokenPathEffect()) { + return; + } + + Geom::Point darray[SAMPLE_SIZE + 1]; + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + + SPCurve c; + +#ifdef SPIRAL_VERBOSE + g_print ("cx=%g, cy=%g, exp=%g, revo=%g, rad=%g, arg=%g, t0=%g\n", + this->cx, + this->cy, + this->exp, + this->revo, + this->rad, + this->arg, + this->t0); +#endif + + /* Initial moveto. */ + c.moveto(this->getXY(this->t0)); + + double const tstep = SAMPLE_STEP / this->revo; + double const dstep = tstep / (SAMPLE_SIZE - 1); + + Geom::Point hat1 = this->getTangent(this->t0); + Geom::Point hat2; + + double t; + for (t = this->t0; t < (1.0 - tstep);) { + this->fitAndDraw(&c, dstep, darray, hat1, hat2, &t); + + hat1 = -hat2; + } + + if ((1.0 - t) > SP_EPSILON) { + this->fitAndDraw(&c, (1.0 - t) / (SAMPLE_SIZE - 1.0), darray, hat1, hat2, &t); + } + + prepareShapeForLPE(&c); + +} + +/** + * Set spiral properties and update display. + */ +void SPSpiral::setPosition(gdouble cx, gdouble cy, gdouble exp, gdouble revo, gdouble rad, gdouble arg, gdouble t0) { + /** \todo + * Consider applying CLAMP or adding in-bounds assertions for + * some of these parameters. + */ + this->cx = cx; + this->cy = cy; + this->exp = exp; + this->revo = revo; + this->rad = MAX (rad, 0.0); + this->arg = arg; + this->t0 = CLAMP(t0, 0.0, 0.999); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPSpiral::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + // We will determine the spiral's midpoint ourselves, instead of trusting on the base class + // Therefore snapping to object midpoints is temporarily disabled + Inkscape::SnapPreferences local_snapprefs = *snapprefs; + local_snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT, false); + + SPShape::snappoints(p, &local_snapprefs); + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT)) { + Geom::Affine const i2dt (this->i2dt_affine ()); + + p.emplace_back(Geom::Point(this->cx, this->cy) * i2dt, Inkscape::SNAPSOURCE_OBJECT_MIDPOINT, Inkscape::SNAPTARGET_OBJECT_MIDPOINT); + // This point is the start-point of the spiral, which is also returned when _snap_to_itemnode has been set + // in the object snapper. In that case we will get a duplicate! + } +} + +/** + * Set spiral transform + */ +Geom::Affine SPSpiral::set_transform(Geom::Affine const &xform) +{ + if (pathEffectsEnabled() && !optimizeTransforms()) { + return xform; + } + // Only set transform with proportional scaling + if (!xform.withoutTranslation().isUniformScale()) { + return xform; + } + /* Calculate spiral start in parent coords. */ + Geom::Point pos( Geom::Point(this->cx, this->cy) * xform ); + + /* This function takes care of translation and scaling, we return whatever parts we can't + handle. */ + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + gdouble const s = hypot(ret[0], ret[1]); + if (s > 1e-9) { + ret[0] /= s; + ret[1] /= s; + ret[2] /= s; + ret[3] /= s; + } else { + ret[0] = 1.0; + ret[1] = 0.0; + ret[2] = 0.0; + ret[3] = 1.0; + } + + this->rad *= s; + + /* Find start in item coords */ + pos = pos * ret.inverse(); + this->cx = pos[Geom::X]; + this->cy = pos[Geom::Y]; + + this->set_shape(); + + // Adjust stroke width + this->adjust_stroke(s); + + // Adjust pattern fill + this->adjust_pattern(xform * ret.inverse()); + + // Adjust gradient fill + this->adjust_gradient(xform * ret.inverse()); + + return ret; +} + +void SPSpiral::update_patheffect(bool write) { + SPShape::update_patheffect(write); +} + +/** + * Return one of the points on the spiral. + * + * \param t specifies how far along the spiral. + * \pre \a t in [0.0, 2.03]. (It doesn't make sense for t to be much more + * than 1.0, though some callers go slightly beyond 1.0 for curve-fitting + * purposes.) + */ +Geom::Point SPSpiral::getXY(gdouble t) const { + g_assert (this->exp >= 0.0); + /* Otherwise we get NaN for t==0. */ + g_assert (this->exp <= 1000.0); + /* Anything much more results in infinities. Even allowing 1000 is somewhat overkill. */ + g_assert (t >= 0.0); + /* Any callers passing -ve t will have a bug for non-integral values of exp. */ + + double const rad = this->rad * pow(t, (double)this->exp); + double const arg = 2.0 * M_PI * this->revo * t + this->arg; + + return Geom::Point(rad * cos(arg) + this->cx, rad * sin(arg) + this->cy); +} + + +/** + * Returns the derivative of sp_spiral_get_xy with respect to t, + * scaled to a unit vector. + * + * \pre spiral != 0. + * \pre 0 \<= t. + * \pre p != NULL. + * \post is_unit_vector(*p). + */ +Geom::Point SPSpiral::getTangent(gdouble t) const { + Geom::Point ret(1.0, 0.0); + + g_assert (t >= 0.0); + g_assert (this->exp >= 0.0); + /* See above for comments on these assertions. */ + + double const t_scaled = 2.0 * M_PI * this->revo * t; + double const arg = t_scaled + this->arg; + double const s = sin(arg); + double const c = cos(arg); + + if (this->exp == 0.0) { + ret = Geom::Point(-s, c); + } else if (t_scaled == 0.0) { + ret = Geom::Point(c, s); + } else { + Geom::Point unrotated(this->exp, t_scaled); + double const s_len = L2 (unrotated); + g_assert (s_len != 0); + /** \todo + * Check that this isn't being too hopeful of the hypot + * function. E.g. test with numbers around 2**-1070 + * (denormalized numbers), preferably on a few different + * platforms. However, njh says that the usual implementation + * does handle both very big and very small numbers. + */ + unrotated /= s_len; + + /* ret = spiral->exp * (c, s) + t_scaled * (-s, c); + alternatively ret = (spiral->exp, t_scaled) * (( c, s), + (-s, c)).*/ + ret = Geom::Point(dot(unrotated, Geom::Point(c, -s)), + dot(unrotated, Geom::Point(s, c))); + /* ret should already be approximately normalized: the + matrix ((c, -s), (s, c)) is orthogonal (it just + rotates by arg), and unrotated has been normalized, + so ret is already of unit length other than numerical + error in the above matrix multiplication. */ + + /** \todo + * I haven't checked how important it is for ret to be very + * near unit length; we could get rid of the below. + */ + + ret.normalize(); + /* Proof that ret length is non-zero: see above. (Should be near 1.) */ + } + + g_assert (is_unit_vector(ret)); + return ret; +} + +/** + * Compute rad and/or arg for point on spiral. + */ +void SPSpiral::getPolar(gdouble t, gdouble* rad, gdouble* arg) const { + if (rad) { + *rad = this->rad * pow(t, (double)this->exp); + } + + if (arg) { + *arg = 2.0 * M_PI * this->revo * t + this->arg; + } +} + +/** + * Return true if spiral has properties that make it invalid. + */ +bool SPSpiral::isInvalid() const { + gdouble rad; + + this->getPolar(0.0, &rad, nullptr); + + if (rad < 0.0 || rad > SP_HUGE) { + g_warning("rad(t=0)=%g", rad); + return true; + } + + this->getPolar(1.0, &rad, nullptr); + + if (rad < 0.0 || rad > SP_HUGE) { + g_warning("rad(t=1)=%g", rad); + return true; + } + + return false; +} + +/* + 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 : diff --git a/src/object/sp-spiral.h b/src/object/sp-spiral.h new file mode 100644 index 0000000..e220dc6 --- /dev/null +++ b/src/object/sp-spiral.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SPIRAL_H +#define SEEN_SP_SPIRAL_H +/* + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-shape.h" + +#define noSPIRAL_VERBOSE + +#define SP_EPSILON 1e-5 +#define SP_EPSILON_2 (SP_EPSILON * SP_EPSILON) +#define SP_HUGE 1e5 + +#define SPIRAL_TOLERANCE 3.0 +#define SAMPLE_STEP (1.0/4.0) ///< step per 2PI +#define SAMPLE_SIZE 8 ///< sample size per one bezier + +/** + * A spiral Shape. + * + * The Spiral shape is defined as: + * \verbatim + x(t) = rad * t^exp cos(2 * Pi * revo*t + arg) + cx + y(t) = rad * t^exp sin(2 * Pi * revo*t + arg) + cy \endverbatim + * where spiral curve is drawn for {t | t0 <= t <= 1}. The rad and arg + * parameters can also be represented by transformation. + * + * \todo Should I remove these attributes? + */ +class SPSpiral final : public SPShape { +public: + SPSpiral(); + ~SPSpiral() override; + int tag() const override { return tag_of<decltype(*this)>; } + + float cx, cy; + float exp; ///< Spiral expansion factor + float revo; ///< Spiral revolution factor + float rad; ///< Spiral radius + float arg; ///< Spiral argument + float t0; + + /* Lowlevel interface */ + void setPosition(double cx, double cy, double exp, double revo, double rad, double arg, double t0); + Geom::Affine set_transform(Geom::Affine const& xform) override; + + Geom::Point getXY(double t) const; + + void getPolar(double t, double* rad, double* arg) const; + + bool isInvalid() const; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void update(SPCtx *ctx, unsigned int flags) override; + void set(SPAttr key, char const* value) override; + + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + const char* typeName() const override; + const char* displayName() const override; + char* description() const override; + void update_patheffect(bool write) override; + void set_shape() override; + +private: + Geom::Point getTangent(double t) const; + void fitAndDraw(SPCurve* c, double dstep, Geom::Point darray[], Geom::Point const& hat1, Geom::Point& hat2, double* t) const; +}; + +#endif // SEEN_SP_SPIRAL_H diff --git a/src/object/sp-star.cpp b/src/object/sp-star.cpp new file mode 100644 index 0000000..9fac561 --- /dev/null +++ b/src/object/sp-star.cpp @@ -0,0 +1,564 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * <sodipodi:star> implementation + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> +#include <glib.h> +#include "live_effects/effect.h" + +#include "svg/svg.h" +#include "attributes.h" +#include "display/curve.h" +#include "xml/repr.h" +#include "document.h" + +#include "sp-star.h" +#include <glibmm/i18n.h> + +SPStar::SPStar() : SPShape() , + sides(5), + center(0, 0), + flatsided(false), + rounded(0.0), + randomized(0.0) +{ + this->r[0] = 1.0; + this->r[1] = 0.001; + this->arg[0] = this->arg[1] = 0.0; +} + +SPStar::~SPStar() = default; + +void SPStar::build(SPDocument * document, Inkscape::XML::Node * repr) { + // CPPIFY: see header file + SPShape::build(document, repr); + + this->readAttr(SPAttr::SODIPODI_CX); + this->readAttr(SPAttr::SODIPODI_CY); + this->readAttr(SPAttr::INKSCAPE_FLATSIDED); + this->readAttr(SPAttr::SODIPODI_SIDES); + this->readAttr(SPAttr::SODIPODI_R1); + this->readAttr(SPAttr::SODIPODI_R2); + this->readAttr(SPAttr::SODIPODI_ARG1); + this->readAttr(SPAttr::SODIPODI_ARG2); + this->readAttr(SPAttr::INKSCAPE_ROUNDED); + this->readAttr(SPAttr::INKSCAPE_RANDOMIZED); +} + +Inkscape::XML::Node* SPStar::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:path"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + repr->setAttribute("sodipodi:type", "star"); + repr->setAttributeBoolean("inkscape:flatsided", this->flatsided); + repr->setAttributeInt("sodipodi:sides", this->sides); + repr->setAttributeSvgDouble("sodipodi:cx", this->center[Geom::X]); + repr->setAttributeSvgDouble("sodipodi:cy", this->center[Geom::Y]); + repr->setAttributeSvgDouble("sodipodi:r1", this->r[0]); + repr->setAttributeSvgDouble("sodipodi:r2", this->r[1]); + repr->setAttributeSvgDouble("sodipodi:arg1", this->arg[0]); + repr->setAttributeSvgDouble("sodipodi:arg2", this->arg[1]); + repr->setAttributeSvgDouble("inkscape:rounded", this->rounded); + repr->setAttributeSvgDouble("inkscape:randomized", this->randomized); + } + + this->set_shape(); + if (this->_curve) { + repr->setAttribute("d", sp_svg_write_path(this->_curve->get_pathvector())); + } else { + repr->removeAttribute("d"); + } + // CPPIFY: see header file + SPShape::write(xml_doc, repr, flags); + + return repr; +} + +void SPStar::set(SPAttr key, const gchar* value) { + SVGLength::Unit unit; + + /* fixme: we should really collect updates */ + switch (key) { + case SPAttr::SODIPODI_SIDES: + if (value) { + this->sides = atoi (value); + this->sides = CLAMP(this->sides, this->flatsided ? 3 : 2, 1024); + } else { + this->sides = 5; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_CX: + if (!sp_svg_length_read_ldd (value, &unit, nullptr, &this->center[Geom::X]) || + (unit == SVGLength::EM) || + (unit == SVGLength::EX) || + (unit == SVGLength::PERCENT)) { + this->center[Geom::X] = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_CY: + if (!sp_svg_length_read_ldd (value, &unit, nullptr, &this->center[Geom::Y]) || + (unit == SVGLength::EM) || + (unit == SVGLength::EX) || + (unit == SVGLength::PERCENT)) { + this->center[Geom::Y] = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_R1: + if (!sp_svg_length_read_ldd (value, &unit, nullptr, &this->r[0]) || + (unit == SVGLength::EM) || + (unit == SVGLength::EX) || + (unit == SVGLength::PERCENT)) { + this->r[0] = 1.0; + } + + /* fixme: Need CLAMP (Lauris) */ + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_R2: + if (!sp_svg_length_read_ldd (value, &unit, nullptr, &this->r[1]) || + (unit == SVGLength::EM) || + (unit == SVGLength::EX) || + (unit == SVGLength::PERCENT)) { + this->r[1] = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + return; + + case SPAttr::SODIPODI_ARG1: + if (value) { + this->arg[0] = g_ascii_strtod (value, nullptr); + } else { + this->arg[0] = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::SODIPODI_ARG2: + if (value) { + this->arg[1] = g_ascii_strtod (value, nullptr); + } else { + this->arg[1] = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::INKSCAPE_FLATSIDED: + if (value && !strcmp(value, "true")) { + this->flatsided = true; + this->sides = MAX(this->sides, 3); + } else { + this->flatsided = false; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::INKSCAPE_ROUNDED: + if (value) { + this->rounded = g_ascii_strtod (value, nullptr); + } else { + this->rounded = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::INKSCAPE_RANDOMIZED: + if (value) { + this->randomized = g_ascii_strtod (value, nullptr); + } else { + this->randomized = 0.0; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + // CPPIFY: see header file + SPShape::set(key, value); + break; + } +} + +void SPStar::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + this->set_shape(); + } + + // CPPIFY: see header file + SPShape::update(ctx, flags); +} + +const char* SPStar::typeName() const { + if (this->flatsided == false) + return "star"; + return "polygon"; +} + +const char* SPStar::displayName() const { + if (this->flatsided == false) + return _("Star"); + return _("Polygon"); +} + +gchar* SPStar::description() const { + // while there will never be less than 2 or 3 vertices, we still need to + // make calls to ngettext because the pluralization may be different + // for various numbers >=3. The singular form is used as the index. + return g_strdup_printf (ngettext("with %d vertex", "with %d vertices", + this->sides), this->sides); +} + +/** +Returns a unit-length vector at 90 degrees to the direction from o to n + */ +static Geom::Point +rot90_rel (Geom::Point o, Geom::Point n) +{ + return ((1/Geom::L2(n - o)) * Geom::Point ((n - o)[Geom::Y], (o - n)[Geom::X])); +} + +/** +Returns a unique 32 bit int for a given point. +Obvious (but acceptable for my purposes) limits to uniqueness: +- returned value for x,y repeats for x+n*1024,y+n*1024 +- returned value is unchanged when the point is moved by less than 1/1024 of px +*/ +static guint32 +point_unique_int (Geom::Point o) +{ + return ((guint32) + 65536 * + (((int) floor (o[Geom::X] * 64)) % 1024 + ((int) floor (o[Geom::X] * 1024)) % 64) + + + (((int) floor (o[Geom::Y] * 64)) % 1024 + ((int) floor (o[Geom::Y] * 1024)) % 64) + ); +} + +/** +Returns the next pseudorandom value using the Linear Congruential Generator algorithm (LCG) +with the parameters (m = 2^32, a = 69069, b = 1). These parameters give a full-period generator, +i.e. it is guaranteed to go through all integers < 2^32 (see http://random.mat.sbg.ac.at/~charly/server/server.html) +*/ +static inline guint32 +lcg_next(guint32 const prev) +{ + return (guint32) ( 69069 * prev + 1 ); +} + +/** +Returns a random number in the range [-0.5, 0.5) from the given seed, stepping the given number of steps from the seed. +*/ +static double +rnd (guint32 const seed, unsigned steps) { + guint32 lcg = seed; + for (; steps > 0; steps --) + lcg = lcg_next (lcg); + + return ( lcg / 4294967296. ) - 0.5; +} + +static Geom::Point +sp_star_get_curvepoint (SPStar *star, SPStarPoint point, gint index, bool previ) +{ + // the point whose neighboring curve handle we're calculating + Geom::Point o = sp_star_get_xy (star, point, index); + + // indices of previous and next points + gint pi = (index > 0)? (index - 1) : (star->sides - 1); + gint ni = (index < star->sides - 1)? (index + 1) : 0; + + // the other point type + SPStarPoint other = (point == SP_STAR_POINT_KNOT2? SP_STAR_POINT_KNOT1 : SP_STAR_POINT_KNOT2); + + // the neighbors of o; depending on flatsided, they're either the same type (polygon) or the other type (star) + Geom::Point prev = (star->flatsided? sp_star_get_xy (star, point, pi) : sp_star_get_xy (star, other, point == SP_STAR_POINT_KNOT2? index : pi)); + Geom::Point next = (star->flatsided? sp_star_get_xy (star, point, ni) : sp_star_get_xy (star, other, point == SP_STAR_POINT_KNOT1? index : ni)); + + // prev-next midpoint + Geom::Point mid = 0.5 * (prev + next); + + // point to which we direct the bissector of the curve handles; + // it's far enough outside the star on the perpendicular to prev-next through mid + Geom::Point biss = mid + 100000 * rot90_rel (mid, next); + + // lengths of vectors to prev and next + gdouble prev_len = Geom::L2 (prev - o); + gdouble next_len = Geom::L2 (next - o); + + // unit-length vector perpendicular to o-biss + Geom::Point rot = rot90_rel (o, biss); + + // multiply rot by star->rounded coefficient and the distance to the star point; flip for next + Geom::Point ret; + if (previ) { + ret = (star->rounded * prev_len) * rot; + } else { + ret = (star->rounded * next_len * -1) * rot; + } + + if (star->randomized == 0) { + // add the vector to o to get the final curvepoint + return o + ret; + } else { + // the seed corresponding to the exact point + guint32 seed = point_unique_int (o); + + // randomly rotate (by step 3 from the seed) and scale (by step 4) the vector + ret = ret * Geom::Affine (Geom::Rotate (star->randomized * M_PI * rnd (seed, 3))); + ret *= ( 1 + star->randomized * rnd (seed, 4)); + + // the randomized corner point + Geom::Point o_randomized = sp_star_get_xy (star, point, index, true); + + return o_randomized + ret; + } +} + +#define NEXT false +#define PREV true + +void SPStar::set_shape() { + // perhaps we should convert all our shapes into LPEs without source path + // and with knotholders for parameters, then this situation will be handled automatically + // by disabling the entire stack (including the shape LPE) + if (checkBrokenPathEffect()) { + return; + } + + SPCurve c; + + bool not_rounded = (fabs (this->rounded) < 1e-4); + + // note that we pass randomized=true to sp_star_get_xy, because the curve must be randomized; + // other places that call that function (e.g. the knotholder) need the exact point + + // draw 1st segment + c.moveto(sp_star_get_xy (this, SP_STAR_POINT_KNOT1, 0, true)); + + if (this->flatsided == false) { + if (not_rounded) { + c.lineto(sp_star_get_xy (this, SP_STAR_POINT_KNOT2, 0, true)); + } else { + c.curveto(sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT1, 0, NEXT), + sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT2, 0, PREV), + sp_star_get_xy (this, SP_STAR_POINT_KNOT2, 0, true)); + } + } + + // draw all middle segments + for (gint i = 1; i < sides; i++) { + if (not_rounded) { + c.lineto(sp_star_get_xy (this, SP_STAR_POINT_KNOT1, i, true)); + } else { + if (this->flatsided == false) { + c.curveto(sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT2, i - 1, NEXT), + sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT1, i, PREV), + sp_star_get_xy (this, SP_STAR_POINT_KNOT1, i, true)); + } else { + c.curveto(sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT1, i - 1, NEXT), + sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT1, i, PREV), + sp_star_get_xy (this, SP_STAR_POINT_KNOT1, i, true)); + } + } + + if (this->flatsided == false) { + if (not_rounded) { + c.lineto(sp_star_get_xy (this, SP_STAR_POINT_KNOT2, i, true)); + } else { + c.curveto(sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT1, i, NEXT), + sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT2, i, PREV), + sp_star_get_xy (this, SP_STAR_POINT_KNOT2, i, true)); + } + } + } + + // draw last segment + if (!not_rounded) { + if (this->flatsided == false) { + c.curveto(sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT2, sides - 1, NEXT), + sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT1, 0, PREV), + sp_star_get_xy (this, SP_STAR_POINT_KNOT1, 0, true)); + } else { + c.curveto(sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT1, sides - 1, NEXT), + sp_star_get_curvepoint (this, SP_STAR_POINT_KNOT1, 0, PREV), + sp_star_get_xy (this, SP_STAR_POINT_KNOT1, 0, true)); + } + } + + c.closepath(); + + prepareShapeForLPE(&c); + +} + +void +sp_star_position_set (SPStar *star, gint sides, Geom::Point center, gdouble r1, gdouble r2, gdouble arg1, gdouble arg2, bool isflat, double rounded, double randomized) +{ + g_return_if_fail (star != nullptr); + + star->flatsided = isflat; + star->center = center; + star->r[0] = MAX (r1, 0.001); + + if (isflat == false) { + star->sides = CLAMP(sides, 2, 1024); + star->r[1] = CLAMP(r2, 0.0, star->r[0]); + } else { + star->sides = CLAMP(sides, 3, 1024); + star->r[1] = CLAMP( r1*cos(M_PI/sides) ,0.0, star->r[0] ); + } + + star->arg[0] = arg1; + star->arg[1] = arg2; + star->rounded = rounded; + star->randomized = randomized; + star->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPStar::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + // We will determine the star's midpoint ourselves, instead of trusting on the base class + // Therefore snapping to object midpoints is temporarily disabled + Inkscape::SnapPreferences local_snapprefs = *snapprefs; + local_snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT, false); + + // CPPIFY: see header file + SPShape::snappoints(p, &local_snapprefs); + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT)) { + Geom::Affine const i2dt (this->i2dt_affine ()); + p.emplace_back(this->center * i2dt,Inkscape::SNAPSOURCE_OBJECT_MIDPOINT, Inkscape::SNAPTARGET_OBJECT_MIDPOINT); + } +} + +Geom::Affine SPStar::set_transform(Geom::Affine const &xform) +{ + bool opt_trans = (randomized == 0); + if (pathEffectsEnabled() && !optimizeTransforms()) { + return xform; + } + // Only set transform with proportional scaling + if (!xform.withoutTranslation().isUniformScale()) { + return xform; + } + + /* Calculate star start in parent coords. */ + Geom::Point pos( this->center * xform ); + + /* This function takes care of translation and scaling, we return whatever parts we can't + handle. */ + Geom::Affine ret(opt_trans ? xform.withoutTranslation() : xform); + gdouble const s = hypot(ret[0], ret[1]); + if (s > 1e-9) { + ret[0] /= s; + ret[1] /= s; + ret[2] /= s; + ret[3] /= s; + } else { + ret[0] = 1.0; + ret[1] = 0.0; + ret[2] = 0.0; + ret[3] = 1.0; + } + + this->r[0] *= s; + this->r[1] *= s; + + /* Find start in item coords */ + pos = pos * ret.inverse(); + this->center = pos; + + this->set_shape(); + + // Adjust stroke width + this->adjust_stroke(s); + + // Adjust pattern fill + this->adjust_pattern(xform * ret.inverse()); + + // Adjust gradient fill + this->adjust_gradient(xform * ret.inverse()); + + return ret; +} + +void SPStar::update_patheffect(bool write) { + SPShape::update_patheffect(write); +} + +/** + * sp_star_get_xy: Get X-Y value as item coordinate system + * @star: star item + * @point: point type to obtain X-Y value + * @index: index of vertex + * @p: pointer to store X-Y value + * @randomized: false (default) if you want to get exact, not randomized point + * + * Initial item coordinate system is same as document coordinate system. + */ +Geom::Point +sp_star_get_xy (SPStar const *star, SPStarPoint point, gint index, bool randomized) +{ + gdouble darg = 2.0 * M_PI / (double) star->sides; + + double arg = star->arg[point]; + arg += index * darg; + + Geom::Point xy = star->r[point] * Geom::Point(cos(arg), sin(arg)) + star->center; + + if (!randomized || star->randomized == 0) { + // return the exact point + return xy; + } else { // randomize the point + // find out the seed, unique for this point so that randomization is the same so long as the original point is stationary + guint32 seed = point_unique_int (xy); + // the full range (corresponding to star->randomized == 1.0) is equal to the star's diameter + double range = 2 * MAX (star->r[0], star->r[1]); + // find out the random displacement; x is controlled by step 1 from the seed, y by the step 2 + Geom::Point shift (star->randomized * range * rnd (seed, 1), star->randomized * range * rnd (seed, 2)); + // add the shift to the exact point + return xy + shift; + } +} + +/* + 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 : diff --git a/src/object/sp-star.h b/src/object/sp-star.h new file mode 100644 index 0000000..47abebf --- /dev/null +++ b/src/object/sp-star.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_STAR_H +#define SEEN_SP_STAR_H + +/* + * <sodipodi:star> implementation + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-polygon.h" + +enum SPStarPoint { + SP_STAR_POINT_KNOT1, + SP_STAR_POINT_KNOT2 +}; + +class SPStar final : public SPShape { +public: + SPStar(); + ~SPStar() override; + int tag() const override { return tag_of<decltype(*this)>; } + + int sides; + + Geom::Point center; + double r[2]; + double arg[2]; + bool flatsided; + + double rounded; + double randomized; + +// CPPIFY: This derivation is a bit weird. +// parent_class = reinterpret_cast<SPShapeClass *>(g_type_class_ref(SP_TYPE_SHAPE)); +// So shouldn't star be derived from shape instead of polygon? +// What does polygon have that shape doesn't? + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void set(SPAttr key, char const* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + + const char* typeName() const override; + const char* displayName() const override; + char* description() const override; + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + void update_patheffect(bool write) override; + void set_shape() override; + Geom::Affine set_transform(Geom::Affine const& xform) override; +}; + +void sp_star_position_set (SPStar *star, int sides, Geom::Point center, double r1, double r2, double arg1, double arg2, bool isflat, double rounded, double randomized); + +Geom::Point sp_star_get_xy (SPStar const *star, SPStarPoint point, int index, bool randomized = false); + +#endif diff --git a/src/object/sp-stop.cpp b/src/object/sp-stop.cpp new file mode 100644 index 0000000..fd18898 --- /dev/null +++ b/src/object/sp-stop.cpp @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @gradient stop class. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999,2005 authors + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "sp-stop.h" +#include "style.h" + +#include "attributes.h" +#include "streq.h" +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "svg/css-ostringstream.h" + +SPStop::SPStop() : SPObject() { + this->offset = 0.0; +} + +SPStop::~SPStop() = default; + +void SPStop::build(SPDocument* doc, Inkscape::XML::Node* repr) { + this->readAttr(SPAttr::STYLE); + this->readAttr(SPAttr::OFFSET); + this->readAttr(SPAttr::STOP_PATH); // For mesh + + SPObject::build(doc, repr); +} + +/** + * Virtual build: set stop attributes from its associated XML node. + */ + +void SPStop::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::OFFSET: { + this->offset = sp_svg_read_percentage(value, 0.0); + this->requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + break; + } + case SPAttr::STOP_PATH: { + if (value) { + this->path_string = Glib::ustring(value); + //Geom::PathVector pv = sp_svg_read_pathv(value); + //SPCurve *curve = new SPCurve(pv); + //if( curve ) { + // std::cout << "Got Curve" << std::endl; + //curve->unref(); + //} + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: { + if (SP_ATTRIBUTE_IS_CSS(key)) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } else { + SPObject::set(key, value); + } + // This makes sure that the parent sp-gradient is updated. + this->requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + break; + } + } +} + +void SPStop::modified(guint flags) +{ + if (parent && !(flags & SP_OBJECT_PARENT_MODIFIED_FLAG)) { + parent->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } +} + + +/** + * Virtual set: set attribute to value. + */ + +Inkscape::XML::Node* SPStop::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:stop"); + } + + SPObject::write(xml_doc, repr, flags); + repr->setAttributeCssDouble("offset", this->offset); + /* strictly speaking, offset an SVG <number> rather than a CSS one, but exponents make no sense + * for offset proportions. */ + + return repr; +} + +/** + * Virtual write: write object attributes to repr. + */ + +// A stop might have some non-stop siblings +SPStop* SPStop::getNextStop() { + SPStop *result = nullptr; + + for (SPObject* obj = getNext(); obj && !result; obj = obj->getNext()) { + if (is<SPStop>(obj)) { + result = cast<SPStop>(obj); + } + } + + return result; +} + +SPStop* SPStop::getPrevStop() { + SPStop *result = nullptr; + + for (SPObject* obj = getPrev(); obj; obj = obj->getPrev()) { + // The closest previous SPObject that is an SPStop *should* be ourself. + if (is<SPStop>(obj)) { + auto stop = cast<SPStop>(obj); + // Sanity check to ensure we have a proper sibling structure. + if (stop->getNextStop() == this) { + result = stop; + } else { + g_warning("SPStop previous/next relationship broken"); + } + break; + } + } + + return result; +} + +SPColor SPStop::getColor() const +{ + if (style->stop_color.currentcolor) { + return style->color.value.color; + } + return style->stop_color.value.color; +} + +gfloat SPStop::getOpacity() const +{ + return SP_SCALE24_TO_FLOAT(style->stop_opacity.value); +} + +/** + * Sets the stop color and stop opacity in the style attribute. + */ +void SPStop::setColor(SPColor color, double opacity) +{ + setColorRepr(getRepr(), color, opacity); +} + +/** + * Set the color and opacity directly into the given xml repr. + */ +void SPStop::setColorRepr(Inkscape::XML::Node *node, SPColor color, double opacity) +{ + Inkscape::CSSOStringStream os; + os << "stop-color:" << color.toString() << ";stop-opacity:" << opacity <<";"; + node->setAttribute("style", os.str()); +} + +/** + * Return stop's color as 32bit value. + */ +guint32 SPStop::get_rgba32() const +{ + return getColor().toRGBA32(getOpacity()); +} + +/* + 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 : diff --git a/src/object/sp-stop.h b/src/object/sp-stop.h new file mode 100644 index 0000000..90fdc30 --- /dev/null +++ b/src/object/sp-stop.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_STOP_H +#define SEEN_SP_STOP_H + +/** \file + * SPStop: SVG <stop> implementation. + */ +/* + * Authors: + */ + +#include <glibmm/ustring.h> + +#include "color.h" +#include "sp-object.h" + +typedef unsigned int guint32; + +/** Gradient stop. */ +class SPStop final : public SPObject { +public: + SPStop(); + ~SPStop() override; + int tag() const override { return tag_of<decltype(*this)>; } + + /// \todo fixme: Should be SPSVGPercentage + float offset; + + Glib::ustring path_string; + //SPCurve path; + + SPStop* getNextStop(); + SPStop* getPrevStop(); + + SPColor getColor() const; + gfloat getOpacity() const; + guint32 get_rgba32() const; + void setColor(SPColor color, double opacity); + + static void setColorRepr(Inkscape::XML::Node *node, SPColor color, double opacity); + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttr key, const char* value) override; + void modified(guint flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif /* !SEEN_SP_STOP_H */ + +/* + 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 : diff --git a/src/object/sp-string.cpp b/src/object/sp-string.cpp new file mode 100644 index 0000000..ee38bfd --- /dev/null +++ b/src/object/sp-string.cpp @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <text> and <tspan> implementation + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * fixme: + * + * These subcomponents should not be items, or alternately + * we have to invent set of flags to mark, whether standard + * attributes are applicable to given item (I even like this + * idea somewhat - Lauris) + * + */ + +#include "sp-string.h" +#include "style.h" + +/*##################################################### +# SPSTRING +#####################################################*/ + +SPString::SPString() : SPObject() { + + // This is dangerous but strings shouldn't have style. + // delete (style); + // style = nullptr; +} + +SPString::~SPString() = default; + +void SPString::build(SPDocument *doc, Inkscape::XML::Node *repr) { + + read_content(); + + SPObject::build(doc, repr); +} + +void SPString::release() { + SPObject::release(); +} + + +void SPString::read_content() { + + string.clear(); + + //XML Tree being used directly here while it shouldn't be. + gchar const *xml_string = getRepr()->content(); + + // std::cout << ">" << (xml_string?xml_string:"Null") << "<" << std::endl; + + // SVG2/CSS Text Level 3 'white-space' has five values. + // See: http://dev.w3.org/csswg/css-text/#white-space + // | New Lines | Spaces/Tabs | Text Wrapping + // ---------|------------|--------------|-------------- + // normal | Collapse | Collapse | Wrap + // pre | Preserve | Preserve | No Wrap + // nowrap | Collapse | Collapse | No Wrap + // pre-wrap | Preserve | Preserve | Wrap + // pre-line | Preserve | Collapse | Wrap + + // 'xml:space' has two values: + // 'default' which corresponds to 'normal' (without wrapping). + // 'preserve' which corresponds to 'pre' except new lines are converted to spaces. + // See algorithms described in svg 1.1 section 10.15 + + bool collapse_space = true; + bool collapse_line = true; + bool is_css = false; + + // Strings don't have style, check parent for style + if( parent && parent->style ) { + if( parent->style->white_space.computed == SP_CSS_WHITE_SPACE_PRE || + parent->style->white_space.computed == SP_CSS_WHITE_SPACE_PREWRAP || + parent->style->white_space.computed == SP_CSS_WHITE_SPACE_PRELINE ) { + collapse_line = false; + } + if( parent->style->white_space.computed == SP_CSS_WHITE_SPACE_PRE || + parent->style->white_space.computed == SP_CSS_WHITE_SPACE_PREWRAP ) { + collapse_space = false; + } + if( parent->style->white_space.computed != SP_CSS_WHITE_SPACE_NORMAL ) { + is_css = true; // If white-space not normal, we assume white-space is set. + } + } + if( !is_css ) { + // SVG 2: Use 'xml:space' only if 'white-space' not 'normal'. + if (xml_space.value == SP_XML_SPACE_PRESERVE) { + collapse_space = false; + } + } + + bool white_space = false; + for ( ; *xml_string ; xml_string = g_utf8_next_char(xml_string) ) { + + gunichar c = g_utf8_get_char(xml_string); + switch (c) { + case 0xd: // Carriage return + // XML Parsers convert 0xa, 0xd, 0xD 0xA to 0xA. CSS also follows this rule so we + // should never see 0xd. + std::cerr << "SPString: Carriage Return found! Argh!" << std::endl; + continue; + break; + case 0xa: // Line feed + if( collapse_line ) { + if( !is_css && collapse_space ) continue; // xml:space == 'default' strips LFs. + white_space = true; // Convert to space and collapse + } else { + string += c; // Preserve line feed + continue; + } + break; + case '\t': // Tab + if( collapse_space ) { + white_space = true; // Convert to space and collapse + } else { + string += c; // Preserve tab + continue; + } + break; + case ' ': // Space + if( collapse_space ) { + white_space = true; // Collapse white space + } else { + string += c; // Preserve space + continue; + } + break; + default: + if( white_space && (!string.empty() || (getPrev() != nullptr))) { + string += ' '; + } + string += c; + white_space = false; + + } // End switch + } // End loop + + // Insert white space at end if more text follows + if (white_space && getRepr()->next() != nullptr) { // can't use SPObject::getNext() when the SPObject tree is still being built + string += ' '; + } + + // std::cout << ">" << string << "<" << std::endl; + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPString::update(SPCtx * /*ctx*/, unsigned /*flags*/) { + + // if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_MODIFIED_FLAG)) { + // /* Parent style or we ourselves changed, so recalculate */ + // flags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; // won't be "just a transformation" anymore, we're going to recompute "x" and "y" attributes + // } +} + + +/* + 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 : diff --git a/src/object/sp-string.h b/src/object/sp-string.h new file mode 100644 index 0000000..05bde35 --- /dev/null +++ b/src/object/sp-string.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_STRING_H +#define SEEN_SP_STRING_H + +/* + * string elements + * extracted from sp-text + */ + +#include <glibmm/ustring.h> + +#include "sp-object.h" + +class SPString final : public SPObject { +public: + SPString(); + ~SPString() override; + int tag() const override { return tag_of<decltype(*this)>; } + + Glib::ustring string; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void read_content() override; + + void update(SPCtx* ctx, unsigned int flags) override; +}; + +#endif diff --git a/src/object/sp-style-elem.cpp b/src/object/sp-style-elem.cpp new file mode 100644 index 0000000..b91d53e --- /dev/null +++ b/src/object/sp-style-elem.cpp @@ -0,0 +1,549 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-style-elem.h" + +#include "3rdparty/libcroco/src/cr-parser.h" + +#include "attributes.h" +#include "document.h" +#include "sp-root.h" +#include "style.h" +#include "xml/repr.h" + +// For external style sheets +#include <fstream> +#include <iostream> + +#include "io/resource.h" + +// For font-rule +#include "libnrtype/font-factory.h" + +void SPStyleElemTextNodeObserver::notifyContentChanged(Inkscape::XML::Node &, Inkscape::Util::ptr_shared, + Inkscape::Util::ptr_shared) +{ + auto styleelem = static_cast<SPStyleElem *>(this); + styleelem->read_content(); + styleelem->document->getRoot()->emitModified(SP_OBJECT_MODIFIED_CASCADE); +} + +void SPStyleElemNodeObserver::notifyChildAdded(Inkscape::XML::Node &, Inkscape::XML::Node &child, Inkscape::XML::Node *) +{ + auto styleelem = static_cast<SPStyleElem *>(this); + + if (child.type() == Inkscape::XML::NodeType::TEXT_NODE) { + child.addObserver(styleelem->textNodeObserver()); + } + + styleelem->read_content(); +} + +void SPStyleElemNodeObserver::notifyChildRemoved(Inkscape::XML::Node &, Inkscape::XML::Node &child, Inkscape::XML::Node *) +{ + auto styleelem = static_cast<SPStyleElem *>(this); + + child.removeObserver(styleelem->textNodeObserver()); + styleelem->read_content(); +} + +void SPStyleElemNodeObserver::notifyChildOrderChanged(Inkscape::XML::Node &, Inkscape::XML::Node &, Inkscape::XML::Node *, + Inkscape::XML::Node *) +{ + auto stylelem = static_cast<SPStyleElem *>(this); + stylelem->read_content(); +} + +SPStyleElem::SPStyleElem() +{ + media_set_all(this->media); +} + +SPStyleElem::~SPStyleElem() = default; + +void SPStyleElem::set(SPAttr key, char const *value) +{ + switch (key) { + case SPAttr::TYPE: { + if (!value) { + /* TODO: `type' attribute is required. Give error message as per + http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. */ + is_css = false; + } else { + /* fixme: determine what whitespace is allowed. Will probably need to ask on SVG + list; though the relevant RFC may give info on its lexer. */ + is_css = ( g_ascii_strncasecmp(value, "text/css", 8) == 0 + && ( value[8] == '\0' || + value[8] == ';' ) ); + } + break; + } + +#if 0 /* unfinished */ + case SPAttr::MEDIA: { + parse_media(style_elem, value); + break; + } +#endif + + /* title is ignored. */ + default: { + SPObject::set(key, value); + break; + } + } +} + +Inkscape::XML::Node* SPStyleElem::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:style"); + } + + if (flags & SP_OBJECT_WRITE_BUILD) { + g_warning("nyi: Forming <style> content for SP_OBJECT_WRITE_BUILD."); + /* fixme: Consider having the CRStyleSheet be a member of SPStyleElem, and then + pretty-print to a string s, then repr->addChild(xml_doc->createTextNode(s), NULL). */ + } + if (is_css) { + repr->setAttribute("type", "text/css"); + } + /* todo: media */ + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + + +/** Returns the concatenation of the content of the text children of the specified object. */ +static Glib::ustring +concat_children(Inkscape::XML::Node const &repr) +{ + Glib::ustring ret; + // effic: Initialising ret to a reasonable starting size could speed things up. + for (Inkscape::XML::Node const *rch = repr.firstChild(); rch != nullptr; rch = rch->next()) { + if ( rch->type() == Inkscape::XML::NodeType::TEXT_NODE ) { + ret += rch->content(); + } + } + return ret; +} + + + +/* Callbacks for SAC-style libcroco parser. */ + +enum StmtType { NO_STMT, FONT_FACE_STMT, NORMAL_RULESET_STMT }; + +/** + * Helper class which owns the parser and tracks the current statement + */ +struct ParseTmp +{ +private: + static constexpr unsigned ParseTmp_magic = 0x23474397; // from /dev/urandom + unsigned const magic = ParseTmp_magic; + +public: + CRParser *const parser; + CRStyleSheet *const stylesheet; + SPDocument *const document; // Need file location for '@import' + + // Current statement + StmtType stmtType = NO_STMT; + CRStatement *currStmt = nullptr; + + ParseTmp() = delete; + ParseTmp(ParseTmp const &) = delete; + ParseTmp(CRStyleSheet *const stylesheet, SPDocument *const document); + ~ParseTmp() { cr_parser_destroy(parser); } + + /** + * @pre a_handler->app_data points to a ParseTmp instance + */ + static ParseTmp *cast(CRDocHandler *a_handler) + { + assert(a_handler && a_handler->app_data); + auto self = static_cast<ParseTmp *>(a_handler->app_data); + assert(self->magic == ParseTmp_magic); + return self; + } +}; + +static void +import_style_cb (CRDocHandler *a_handler, + GList *a_media_list, + CRString *a_uri, + CRString *a_uri_default_ns, + CRParsingLocation *a_location) +{ + /* a_uri_default_ns is set to NULL and is unused by libcroco */ + + // Get document + g_return_if_fail(a_handler && a_uri); + auto &parse_tmp = *ParseTmp::cast(a_handler); + + SPDocument* document = parse_tmp.document; + if (!document) { + std::cerr << "import_style_cb: No document!" << std::endl; + return; + } + if (!document->getDocumentFilename()) { + std::cerr << "import_style_cb: Document filename is NULL" << std::endl; + return; + } + + // Get file + auto import_file = + Inkscape::IO::Resource::get_filename (document->getDocumentFilename(), a_uri->stryng->str); + + // Parse file + CRStyleSheet *stylesheet = cr_stylesheet_new (nullptr); + ParseTmp parse_new(stylesheet, document); + CRStatus const parse_status = + cr_parser_parse_file(parse_new.parser, reinterpret_cast<const guchar *>(import_file.c_str()), CR_UTF_8); + if (parse_status == CR_OK) { + g_assert(parse_tmp.stylesheet); + g_assert(parse_tmp.stylesheet != stylesheet); + stylesheet->origin = ORIGIN_AUTHOR; + // Append import statement + CRStatement *ruleset = + cr_statement_new_at_import_rule(parse_tmp.stylesheet, cr_string_dup(a_uri), nullptr, stylesheet); + parse_tmp.stylesheet->statements = cr_statement_append(parse_tmp.stylesheet->statements, ruleset); + } else { + std::cerr << "import_style_cb: Could not parse: " << import_file << std::endl; + cr_stylesheet_destroy (stylesheet); + } +}; + +static void +start_selector_cb(CRDocHandler *a_handler, + CRSelector *a_sel_list) +{ + g_return_if_fail(a_handler && a_sel_list); + auto &parse_tmp = *ParseTmp::cast(a_handler); + + if ( (parse_tmp.currStmt != nullptr) + || (parse_tmp.stmtType != NO_STMT) ) { + g_warning("Expecting currStmt==NULL and stmtType==0 (NO_STMT) at start of ruleset, but found currStmt=%p, stmtType=%u", + static_cast<void *>(parse_tmp.currStmt), unsigned(parse_tmp.stmtType)); + // fixme: Check whether we need to unref currStmt if non-NULL. + } + CRStatement *ruleset = cr_statement_new_ruleset(parse_tmp.stylesheet, a_sel_list, nullptr, nullptr); + g_return_if_fail(ruleset && ruleset->type == RULESET_STMT); + parse_tmp.stmtType = NORMAL_RULESET_STMT; + parse_tmp.currStmt = ruleset; +} + +static void +end_selector_cb(CRDocHandler *a_handler, + CRSelector *a_sel_list) +{ + g_return_if_fail(a_handler && a_sel_list); + auto &parse_tmp = *ParseTmp::cast(a_handler); + + CRStatement *const ruleset = parse_tmp.currStmt; + if (parse_tmp.stmtType == NORMAL_RULESET_STMT + && ruleset + && ruleset->type == RULESET_STMT + && ruleset->kind.ruleset->sel_list == a_sel_list) + { + parse_tmp.stylesheet->statements = cr_statement_append(parse_tmp.stylesheet->statements, + ruleset); + } else { + g_warning("Found stmtType=%u, stmt=%p, stmt.type=%u, ruleset.sel_list=%p, a_sel_list=%p.", + unsigned(parse_tmp.stmtType), + ruleset, + unsigned(ruleset->type), + ruleset->kind.ruleset->sel_list, + a_sel_list); + } + parse_tmp.currStmt = nullptr; + parse_tmp.stmtType = NO_STMT; +} + +static void +start_font_face_cb(CRDocHandler *a_handler, + CRParsingLocation *) +{ + auto &parse_tmp = *ParseTmp::cast(a_handler); + + if (parse_tmp.stmtType != NO_STMT || parse_tmp.currStmt != nullptr) { + g_warning("Expecting currStmt==NULL and stmtType==0 (NO_STMT) at start of @font-face, but found currStmt=%p, stmtType=%u", + static_cast<void *>(parse_tmp.currStmt), unsigned(parse_tmp.stmtType)); + // fixme: Check whether we need to unref currStmt if non-NULL. + } + CRStatement *font_face_rule = cr_statement_new_at_font_face_rule (parse_tmp.stylesheet, nullptr); + g_return_if_fail(font_face_rule && font_face_rule->type == AT_FONT_FACE_RULE_STMT); + parse_tmp.stmtType = FONT_FACE_STMT; + parse_tmp.currStmt = font_face_rule; +} + +static void +end_font_face_cb(CRDocHandler *a_handler) +{ + auto &parse_tmp = *ParseTmp::cast(a_handler); + + CRStatement *const font_face_rule = parse_tmp.currStmt; + if (parse_tmp.stmtType == FONT_FACE_STMT + && font_face_rule + && font_face_rule->type == AT_FONT_FACE_RULE_STMT) + { + parse_tmp.stylesheet->statements = cr_statement_append(parse_tmp.stylesheet->statements, + font_face_rule); + } else { + g_warning("Found stmtType=%u, stmt=%p, stmt.type=%u.", + unsigned(parse_tmp.stmtType), + font_face_rule, + unsigned(font_face_rule->type)); + } + + g_warning("end_font_face_cb: font face rule limited support."); + cr_declaration_dump(font_face_rule->kind.font_face_rule->decl_list, stderr, 2, TRUE); + std::cerr << std::endl; + + // Get document + SPDocument* document = parse_tmp.document; + if (!document) { + std::cerr << "end_font_face_cb: No document!" << std::endl; + return; + } + if (!document->getDocumentFilename()) { + std::cerr << "end_font_face_cb: Document filename is NULL" << std::endl; + return; + } + + // Add ttf or otf fonts. + CRDeclaration const *cur = nullptr; + for (cur = font_face_rule->kind.font_face_rule->decl_list; cur; cur = cur->next) { + if (cur->property && + cur->property->stryng && + cur->property->stryng->str && + strcmp(cur->property->stryng->str, "src") == 0 ) { + + if (cur->value && + cur->value->content.str && + cur->value->content.str->stryng && + cur->value->content.str->stryng->str) { + + Glib::ustring value = cur->value->content.str->stryng->str; + + if (value.rfind("ttf") == (value.length() - 3) || + value.rfind("otf") == (value.length() - 3)) { + + // Get file + Glib::ustring ttf_file = + Inkscape::IO::Resource::get_filename (document->getDocumentFilename(), value); + + if (!ttf_file.empty()) { + FontFactory::get().AddFontFile(ttf_file.c_str()); + g_info("end_font_face_cb: Added font: %s", ttf_file.c_str()); + + // FIX ME: Need to refresh font list. + } else { + g_warning("end_font_face_cb: Failed to add: %s", value.c_str()); + } + } + } + } + } + + parse_tmp.currStmt = nullptr; + parse_tmp.stmtType = NO_STMT; + +} + +static void +property_cb(CRDocHandler *const a_handler, + CRString *const a_name, + CRTerm *const a_value, gboolean const a_important) +{ + // std::cerr << "property_cb: Entrance: " << a_name->stryng->str << ": " << cr_term_to_string(a_value) << std::endl; + g_return_if_fail(a_handler && a_name); + auto &parse_tmp = *ParseTmp::cast(a_handler); + + CRStatement *const ruleset = parse_tmp.currStmt; + g_return_if_fail(ruleset); + + CRDeclaration *const decl = cr_declaration_new (ruleset, cr_string_dup(a_name), a_value); + g_return_if_fail(decl); + decl->important = a_important; + + switch (parse_tmp.stmtType) { + + case NORMAL_RULESET_STMT: { + g_return_if_fail (ruleset->type == RULESET_STMT); + CRStatus const append_status = cr_statement_ruleset_append_decl (ruleset, decl); + g_return_if_fail (append_status == CR_OK); + break; + } + case FONT_FACE_STMT: { + g_return_if_fail (ruleset->type == AT_FONT_FACE_RULE_STMT); + CRDeclaration *new_decls = cr_declaration_append (ruleset->kind.font_face_rule->decl_list, decl); + g_return_if_fail (new_decls); + ruleset->kind.font_face_rule->decl_list = new_decls; + break; + } + default: + g_warning ("property_cb: Unhandled stmtType: %u", parse_tmp.stmtType); + return; + } +} + +ParseTmp::ParseTmp(CRStyleSheet *const stylesheet, SPDocument *const document) + : parser(cr_parser_new(nullptr)) + , stylesheet(stylesheet) + , document(document) +{ + CRDocHandler *sac_handler = cr_doc_handler_new(); + sac_handler->app_data = this; + sac_handler->import_style = import_style_cb; + sac_handler->start_selector = start_selector_cb; + sac_handler->end_selector = end_selector_cb; + sac_handler->start_font_face = start_font_face_cb; + sac_handler->end_font_face = end_font_face_cb; + sac_handler->property = property_cb; + cr_parser_set_sac_handler(parser, sac_handler); + cr_doc_handler_unref(sac_handler); +} + +/** + * Get the list of styles. + * Currently only used for testing. + */ +std::vector<std::unique_ptr<SPStyle>> SPStyleElem::get_styles() const +{ + std::vector<std::unique_ptr<SPStyle>> styles; + + if (style_sheet) { + auto count = cr_stylesheet_nr_rules(style_sheet); + for (int x = 0; x < count; ++x) { + CRStatement *statement = cr_stylesheet_statement_get_from_list(style_sheet, x); + styles.emplace_back(new SPStyle(document)); + styles.back()->mergeStatement(statement); + } + } + + return styles; +} + +/** + * Remove `style_sheet` from the document style cascade and destroy it. + * @post style_sheet is NULL + */ +static void clear_style_sheet(SPStyleElem &self) +{ + if (!self.style_sheet) { + return; + } + + auto *next = self.style_sheet->next; + auto *cascade = self.document->getStyleCascade(); + auto *topsheet = cr_cascade_get_sheet(cascade, ORIGIN_AUTHOR); + + cr_stylesheet_unlink(self.style_sheet); + + if (topsheet == self.style_sheet) { + // will unref style_sheet + cr_cascade_set_sheet(cascade, next, ORIGIN_AUTHOR); + } else if (!topsheet) { + cr_stylesheet_unref(self.style_sheet); + } + + self.style_sheet = nullptr; +} + +void SPStyleElem::read_content() { + // TODO On modification (observer callbacks), clearing and re-appending to + // the cascade can change the position of a stylesheet relative to other + // sheets in the document. We need a better way to update a style sheet + // which preserves the position. + clear_style_sheet(*this); + + // First, create the style-sheet object and track it in this + // element so that it can be edited. It'll be combined with + // the document's style sheet later. + style_sheet = cr_stylesheet_new (nullptr); + + ParseTmp parse_tmp(style_sheet, document); + + //XML Tree being used directly here while it shouldn't be. + Glib::ustring const text = concat_children(*getRepr()); + if (!(text.find_first_not_of(" \t\r\n") != std::string::npos)) { + return; + } + CRStatus const parse_status = + cr_parser_parse_buf(parse_tmp.parser, reinterpret_cast<const guchar *>(text.c_str()), text.bytes(), CR_UTF_8); + + if (parse_status == CR_OK) { + auto *cascade = document->getStyleCascade(); + auto *topsheet = cr_cascade_get_sheet(cascade, ORIGIN_AUTHOR); + if (!topsheet) { + // if the style is the first style sheet that we've seen, set the document's + // first style sheet to this style and create a cascade object with it. + cr_cascade_set_sheet(cascade, style_sheet, ORIGIN_AUTHOR); + // unref style sheet; it's been refed by adding to cascade + cr_stylesheet_unref(style_sheet); + } else { + // If not the first, then chain up this style_sheet + cr_stylesheet_append_stylesheet(topsheet, style_sheet); + } + } else { + cr_stylesheet_destroy (style_sheet); + style_sheet = nullptr; + if (parse_status != CR_PARSING_ERROR) { + g_printerr("parsing error code=%u\n", unsigned(parse_status)); + } + } + // If style sheet has changed, we need to cascade the entire object tree, top down + // Get root, read style, loop through children + document->getRoot()->requestDisplayUpdate(SP_OBJECT_STYLESHEET_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_MODIFIED_FLAG); +} + +void SPStyleElem::build(SPDocument *document, Inkscape::XML::Node *repr) { + read_content(); + + readAttr(SPAttr::TYPE); +#if 0 + readAttr( "media" ); +#endif + + repr->addObserver(nodeObserver()); + for (auto child = repr->firstChild(); child; child = child->next()) { + if (child->type() == Inkscape::XML::NodeType::TEXT_NODE) { + child->addObserver(textNodeObserver()); + } + } + + SPObject::build(document, repr); +} + +void SPStyleElem::release() { + getRepr()->removeObserver(nodeObserver()); + for (auto child = getRepr()->firstChild(); child; child = child->next()) { + child->removeObserver(textNodeObserver()); + } + + clear_style_sheet(*this); + + SPObject::release(); +} + + +/* + 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 : diff --git a/src/object/sp-style-elem.h b/src/object/sp-style-elem.h new file mode 100644 index 0000000..11ba99c --- /dev/null +++ b/src/object/sp-style-elem.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SP_STYLE_ELEM_H +#define INKSCAPE_SP_STYLE_ELEM_H + +#include <memory> +#include <vector> +#include "3rdparty/libcroco/src/cr-statement.h" + +#include "media.h" +#include "sp-object.h" + +#include "xml/node-observer.h" + +class SPStyleElem; + +class SPStyleElemNodeObserver : public Inkscape::XML::NodeObserver +{ + friend class SPStyleElem; + ~SPStyleElemNodeObserver() override = default; // can only exist as a direct base of SPStyleElem + + void notifyChildAdded(Inkscape::XML::Node &, Inkscape::XML::Node &, Inkscape::XML::Node *) final; + void notifyChildRemoved(Inkscape::XML::Node &, Inkscape::XML::Node &, Inkscape::XML::Node *) final; + void notifyChildOrderChanged(Inkscape::XML::Node &, Inkscape::XML::Node &, Inkscape::XML::Node *, Inkscape::XML::Node *) final; +}; + +class SPStyleElemTextNodeObserver : public Inkscape::XML::NodeObserver +{ + friend class SPStyleElem; + ~SPStyleElemTextNodeObserver() override = default; // can only exist as a direct base of SPStyleElem + + void notifyContentChanged(Inkscape::XML::Node &, Inkscape::Util::ptr_shared, Inkscape::Util::ptr_shared) final; +}; + +class SPStyleElem final + : public SPObject + , private SPStyleElemNodeObserver + , private SPStyleElemTextNodeObserver +{ +public: + SPStyleElem(); + ~SPStyleElem() override; + int tag() const override { return tag_of<decltype(*this)>; } + + // Container for the libcroco style sheet instance created on load. + CRStyleSheet *style_sheet{nullptr}; + + Media media; + bool is_css{false}; + + std::vector<std::unique_ptr<SPStyle>> get_styles() const; + + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, char const *value) override; + void read_content() override; + void release() override; + + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned int flags) override; + +private: + SPStyleElemNodeObserver &nodeObserver() { return *this; } + SPStyleElemTextNodeObserver &textNodeObserver() { return *this; } + + // for static_casts + friend SPStyleElemNodeObserver; + friend SPStyleElemTextNodeObserver; +}; + +#endif /* !INKSCAPE_SP_STYLE_ELEM_H */ + +/* + 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 : diff --git a/src/object/sp-switch.cpp b/src/object/sp-switch.cpp new file mode 100644 index 0000000..01a5114 --- /dev/null +++ b/src/object/sp-switch.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <switch> implementation + * + * Authors: + * Andrius R. <knutux@gmail.com> + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include "sp-switch.h" +#include "display/drawing-group.h" +#include "conditions.h" + +#include <sigc++/adaptors/bind.h> + +SPSwitch::SPSwitch() : SPGroup() { + this->_cached_item = nullptr; +} + +SPSwitch::~SPSwitch() { + _releaseLastItem(_cached_item); +} + +SPObject *SPSwitch::_evaluateFirst() { + SPObject *first = nullptr; + + for (auto& child: children) { + if (is<SPItem>(&child) && sp_item_evaluate(cast<SPItem>(&child))) { + first = &child; + break; + } + } + + return first; +} + +std::vector<SPObject*> SPSwitch::_childList(bool add_ref, SPObject::Action action) { + if ( action != SPObject::ActionGeneral ) { + return this->childList(add_ref, action); + } + + SPObject *child = _evaluateFirst(); + std::vector<SPObject*> x; + if (nullptr == child) + return x; + + if (add_ref) { + //g_object_ref (G_OBJECT (child)); + sp_object_ref(child); + } + x.push_back(child); + return x; +} + +const char *SPSwitch::typeName() const { + return "switch"; +} + +const char *SPSwitch::displayName() const { + return _("Conditional Group"); +} + +gchar *SPSwitch::description() const { + gint len = this->getItemCount(); + return g_strdup_printf( + ngettext("of <b>%d</b> object", "of <b>%d</b> objects", len), len); +} + +void SPSwitch::child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) { + SPGroup::child_added(child, ref); + + this->_reevaluate(true); +} + +void SPSwitch::remove_child(Inkscape::XML::Node *child) { + SPGroup::remove_child(child); + + this->_reevaluate(); +} + +void SPSwitch::order_changed (Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) +{ + SPGroup::order_changed(child, old_ref, new_ref); + + this->_reevaluate(); +} + +void SPSwitch::_reevaluate(bool /*add_to_drawing*/) { + SPObject *evaluated_child = _evaluateFirst(); + if (!evaluated_child || _cached_item == evaluated_child) { + return; + } + + _releaseLastItem(_cached_item); + + std::vector<SPObject*> item_list = _childList(false, SPObject::ActionShow); + for ( std::vector<SPObject*>::const_reverse_iterator iter=item_list.rbegin();iter!=item_list.rend();++iter) { + SPObject *o = *iter; + if ( !is<SPItem>(o) ) { + continue; + } + + auto child = cast<SPItem>(o); + child->setEvaluated(o == evaluated_child); + } + + _cached_item = evaluated_child; + _release_connection = evaluated_child->connectRelease(sigc::bind(sigc::ptr_fun(&SPSwitch::_releaseItem), this)); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); +} + +void SPSwitch::_releaseItem(SPObject *obj, SPSwitch *selection) +{ + selection->_releaseLastItem(obj); +} + +void SPSwitch::_releaseLastItem(SPObject *obj) +{ + if (nullptr == this->_cached_item || this->_cached_item != obj) + return; + + this->_release_connection.disconnect(); + this->_cached_item = nullptr; +} + +void SPSwitch::_showChildren (Inkscape::Drawing &drawing, Inkscape::DrawingItem *ai, unsigned int key, unsigned int flags) { + SPObject *evaluated_child = this->_evaluateFirst(); + + std::vector<SPObject*> l = this->_childList(false, SPObject::ActionShow); + + for ( std::vector<SPObject*>::const_reverse_iterator iter=l.rbegin();iter!=l.rend();++iter) { + SPObject *o = *iter; + + if (is<SPItem>(o)) { + auto child = cast<SPItem>(o); + child->setEvaluated(o == evaluated_child); + Inkscape::DrawingItem *ac = child->invoke_show (drawing, key, flags); + + if (ac) { + ai->appendChild(ac); + } + } + } +} + +/* + 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 : diff --git a/src/object/sp-switch.h b/src/object/sp-switch.h new file mode 100644 index 0000000..8b8d060 --- /dev/null +++ b/src/object/sp-switch.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SWITCH_H +#define SEEN_SP_SWITCH_H + +/* + * SVG <switch> implementation + * + * Authors: + * Andrius R. <knutux@gmail.com> + * + * Copyright (C) 2006 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/connection.h> + +#include "sp-item-group.h" + +class SPSwitch final : public SPGroup { +public: + SPSwitch(); + ~SPSwitch() override; + int tag() const override { return tag_of<decltype(*this)>; } + + void resetChildEvaluated() { _reevaluate(); } + + std::vector<SPObject*> _childList(bool add_ref, SPObject::Action action); + void _showChildren (Inkscape::Drawing &drawing, Inkscape::DrawingItem *ai, unsigned int key, unsigned int flags) override; + + SPObject *_evaluateFirst(); + void _reevaluate(bool add_to_arena = false); + static void _releaseItem(SPObject *obj, SPSwitch *selection); + void _releaseLastItem(SPObject *obj); + + SPObject *_cached_item; + sigc::connection _release_connection; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) override; + const char* typeName() const override; + const char* displayName() const override; + gchar *description() const override; +}; + +#endif diff --git a/src/object/sp-symbol.cpp b/src/object/sp-symbol.cpp new file mode 100644 index 0000000..cb5893d --- /dev/null +++ b/src/object/sp-symbol.cpp @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <symbol> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2003 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <string> +#include <glibmm/i18n.h> +#include <2geom/transforms.h> +#include <2geom/pathvector.h> + +#include "display/drawing-group.h" +#include "xml/repr.h" +#include "attributes.h" +#include "print.h" +#include "sp-symbol.h" +#include "sp-use.h" +#include "svg/svg.h" +#include "document.h" +#include "inkscape.h" +#include "desktop.h" +#include "layer-manager.h" + +SPSymbol::SPSymbol() : SPGroup(), SPViewBox() { +} + +SPSymbol::~SPSymbol() = default; + +void SPSymbol::build(SPDocument *document, Inkscape::XML::Node *repr) { + this->readAttr(SPAttr::REFX); + this->readAttr(SPAttr::REFY); + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + this->readAttr(SPAttr::WIDTH); + this->readAttr(SPAttr::HEIGHT); + this->readAttr(SPAttr::VIEWBOX); + this->readAttr(SPAttr::PRESERVEASPECTRATIO); + + SPGroup::build(document, repr); + + document->addResource("symbol", this); +} + +void SPSymbol::release() { + if (document) { + document->removeResource("symbol", this); + } + + SPGroup::release(); +} + +void SPSymbol::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::REFX: + value = Inkscape::refX_named_to_percent(value); + this->refX.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::REFY: + value = Inkscape::refY_named_to_percent(value); + this->refY.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::X: + this->x.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::Y: + this->y.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::WIDTH: + this->width.readOrUnset(value, SVGLength::PERCENT, 1.0, 1.0); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::HEIGHT: + this->height.readOrUnset(value, SVGLength::PERCENT, 1.0, 1.0); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::VIEWBOX: + set_viewBox( value ); + // std::cout << "Symbol: ViewBox: " << viewBox << std::endl; + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SPAttr::PRESERVEASPECTRATIO: + set_preserveAspectRatio( value ); + // std::cout << "Symbol: Preserve aspect ratio: " << aspect_align << ", " << aspect_clip << std::endl; + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + default: + SPGroup::set(key, value); + break; + } +} + +void SPSymbol::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPGroup::child_added(child, ref); +} + +void SPSymbol::unSymbol() +{ + SPDocument *doc = this->document; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + // Check if something is selected. + + doc->ensureUpToDate(); + + // Create new <g> and insert in current layer + Inkscape::XML::Node *group = xml_doc->createElement("svg:g"); + //TODO: Better handle if no desktop, currently go to defs without it + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(desktop && desktop->doc() == doc) { + desktop->layerManager().currentLayer()->getRepr()->appendChild(group); + } else { + parent->getRepr()->appendChild(group); + } + + // Move all children of symbol to group + std::vector<SPObject*> children = childList(false); + + // Converting a group to a symbol inserts a group for non-translational transform. + // In converting a symbol back to a group we strip out the inserted group (or any other + // group that only adds a transform to the symbol content). + if( children.size() == 1 ) { + SPObject *object = children[0]; + if (is<SPGroup>( object ) ) { + if( object->getAttribute("style") == nullptr || + object->getAttribute("class") == nullptr ) { + + group->setAttribute("transform", object->getAttribute("transform")); + children = object->childList(false); + } + } + } + for (std::vector<SPObject*>::const_reverse_iterator i=children.rbegin();i!=children.rend();++i){ + Inkscape::XML::Node *repr = (*i)->getRepr(); + repr->parent()->removeChild(repr); + group->addChild(repr,nullptr); + } + + // Copy relevant attributes + group->setAttribute("style", getAttribute("style")); + group->setAttribute("class", getAttribute("class")); + group->setAttribute("title", getAttribute("title")); + group->setAttribute("inkscape:transform-center-x", + getAttribute("inkscape:transform-center-x")); + group->setAttribute("inkscape:transform-center-y", + getAttribute("inkscape:transform-center-y")); + + + // Need to delete <symbol>; all <use> elements that referenced <symbol> should + // auto-magically reference <g> (if <symbol> deleted after setting <g> 'id'). + Glib::ustring id = getAttribute("id"); + group->setAttribute("id", id); + + deleteObject(true); + + // Clean up + Inkscape::GC::release(group); +} + +std::optional<Geom::PathVector> SPSymbol::documentExactBounds() const +{ + Geom::PathVector shape; + bool is_empty = true; + for (auto &child : children) { + if (auto const item = cast<SPItem>(&child)) { + if (auto bounds = item->documentExactBounds()) { + shape.insert(shape.end(), bounds->begin(), bounds->end()); + is_empty = false; + } + } + } + std::optional<Geom::PathVector> result; + if (!is_empty) { + result = shape * i2doc_affine(); + } + return result; +} + +void SPSymbol::update(SPCtx *ctx, guint flags) { + if (this->cloned) { + + SPItemCtx *ictx = (SPItemCtx *) ctx; + + // Calculate x, y, width, height from parent/initial viewport + this->calcDimsFromParentViewport(ictx, false, cast<SPUse>(parent)); + + SPItemCtx rctx = *ictx; + rctx.viewport = Geom::Rect::from_xywh(x.computed, y.computed, width.computed, height.computed); + rctx = get_rctx(&rctx); + + // Shift according to refX, refY + if (refX._set && refY._set) { + refX.update(1, 1, viewBox.width()); + refY.update(1, 1, viewBox.height()); + auto ref = Geom::Point(refX.computed, refY.computed) * c2p; + c2p *= Geom::Translate(-ref); + } + + // And invoke parent method + SPGroup::update((SPCtx *) &rctx, flags); + + // As last step set additional transform of drawing group + for (auto &v : views) { + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + g->setChildTransform(this->c2p); + } + } else { + // No-op + SPGroup::update(ctx, flags); + } +} + +void SPSymbol::modified(unsigned int flags) { + SPGroup::modified(flags); +} + + +Inkscape::XML::Node* SPSymbol::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:symbol"); + } + + if (refX._set) { + repr->setAttribute("refX", sp_svg_length_write_with_units(refX)); + } + if (refY._set) { + repr->setAttribute("refY", sp_svg_length_write_with_units(refY)); + } + + this->writeDimensions(repr); + this->write_viewBox(repr); + this->write_preserveAspectRatio(repr); + + SPGroup::write(xml_doc, repr, flags); + + return repr; +} + +Inkscape::DrawingItem* SPSymbol::show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) +{ + Inkscape::DrawingItem *ai = nullptr; + + if (cloned) { + // Cloned <symbol> is actually renderable + ai = SPGroup::show(drawing, key, flags); + + if (auto g = cast<Inkscape::DrawingGroup>(ai)) { + g->setChildTransform(this->c2p); + } + } + + return ai; +} + +void SPSymbol::hide(unsigned int key) { + if (this->cloned) { + /* Cloned <symbol> is actually renderable */ + SPGroup::hide(key); + } +} + + +Geom::OptRect SPSymbol::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const +{ + Geom::Affine const a = cloned ? c2p * transform : Geom::identity(); + return SPGroup::bbox(a, type); +} + +void SPSymbol::print(SPPrintContext* ctx) { + if (this->cloned) { + // Cloned <symbol> is actually renderable + + ctx->bind(this->c2p, 1.0); + + SPGroup::print(ctx); + + ctx->release (); + } +} + +/* + 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 : diff --git a/src/object/sp-symbol.h b/src/object/sp-symbol.h new file mode 100644 index 0000000..ab55995 --- /dev/null +++ b/src/object/sp-symbol.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SYMBOL_H +#define SEEN_SP_SYMBOL_H + +/* + * SVG <symbol> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2003 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * This is quite similar in logic to <svg> + * Maybe we should merge them somehow (Lauris) + */ + +#include <2geom/affine.h> +#include "sp-dimensions.h" +#include "sp-item-group.h" +#include "viewbox.h" + +class SPSymbol final : public SPGroup, public SPViewBox, public SPDimensions { +public: + SPSymbol(); + ~SPSymbol() override; + int tag() const override { return tag_of<decltype(*this)>; } + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttr key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + void unSymbol(); + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + void modified(unsigned int flags) override; + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + + std::optional<Geom::PathVector> documentExactBounds() const override; + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void print(SPPrintContext *ctx) override; + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + void hide (unsigned int key) override; + +public: + // reference point + SVGLength refX; + SVGLength refY; +}; + +#endif diff --git a/src/object/sp-tag-use-reference.cpp b/src/object/sp-tag-use-reference.cpp new file mode 100644 index 0000000..634442f --- /dev/null +++ b/src/object/sp-tag-use-reference.cpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to href of <inkscape:tagref> element. + * + * Copyright (C) Theodore Janeczko 2012-2014 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-tag-use-reference.h" + +#include <cstring> +#include <string> + +#include "bad-uri-exception.h" +#include "preferences.h" +#include "sp-shape.h" +#include "sp-text.h" +#include "uri.h" + + +bool SPTagUseReference::_acceptObject(SPObject * const obj) const +{ + if (is<SPItem>(obj)) { + return URIReference::_acceptObject(obj); + } else { + return false; + } +} + + +static void sp_usepath_href_changed(SPObject *old_ref, SPObject *ref, SPTagUsePath *offset); +static void sp_usepath_delete_self(SPObject *deleted, SPTagUsePath *offset); + +SPTagUsePath::SPTagUsePath(SPObject* i_owner):SPTagUseReference(i_owner) +{ + owner=i_owner; + sourceDirty=false; + sourceHref = nullptr; + sourceRepr = nullptr; + sourceObject = nullptr; + _changed_connection = changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_usepath_href_changed), this)); // listening to myself, this should be virtual instead + + user_unlink = nullptr; +} + +SPTagUsePath::~SPTagUsePath() +{ + _changed_connection.disconnect(); // to do before unlinking + + quit_listening(); + unlink(); +} + +void +SPTagUsePath::link(char *to) +{ + if ( to == nullptr ) { + quit_listening(); + unlink(); + } else { + if ( !sourceHref || ( strcmp(to, sourceHref) != 0 ) ) { + g_free(sourceHref); + sourceHref = g_strdup(to); + try { + attach(Inkscape::URI(to)); + } catch (Inkscape::BadURIException &e) { + /* TODO: Proper error handling as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. + */ + g_warning("%s", e.what()); + detach(); + } + } + } +} + +void +SPTagUsePath::unlink() +{ + g_free(sourceHref); + sourceHref = nullptr; + detach(); +} + +void +SPTagUsePath::start_listening(SPObject* to) +{ + if ( to == nullptr ) { + return; + } + sourceObject = to; + sourceRepr = to->getRepr(); + _delete_connection = to->connectDelete(sigc::bind(sigc::ptr_fun(&sp_usepath_delete_self), this)); +} + +void +SPTagUsePath::quit_listening() +{ + if ( sourceObject == nullptr ) { + return; + } + _delete_connection.disconnect(); + sourceRepr = nullptr; + sourceObject = nullptr; +} + +static void +sp_usepath_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, SPTagUsePath *offset) +{ + offset->quit_listening(); + SPItem *refobj = offset->getObject(); + if ( refobj ) { + offset->start_listening(refobj); + } +} + +static void +sp_usepath_delete_self(SPObject */*deleted*/, SPTagUsePath *offset) +{ + offset->owner->deleteObject(); +} + +/* + 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 : diff --git a/src/object/sp-tag-use-reference.h b/src/object/sp-tag-use-reference.h new file mode 100644 index 0000000..565fbbf --- /dev/null +++ b/src/object/sp-tag-use-reference.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_TAG_USE_REFERENCE_H +#define SEEN_SP_TAG_USE_REFERENCE_H + +/* + * The reference corresponding to href of <inkscape:tagref> element. + * + * Copyright (C) Theodore Janeczko 2012-2014 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include <glib.h> + +#include "sp-object.h" +#include "sp-item.h" +#include "uri-references.h" + +namespace Inkscape { +namespace XML { + class Node; +} +} + +class SPTagUseReference : public Inkscape::URIReference { +public: + SPTagUseReference(SPObject *owner) : URIReference(owner) {} + + SPItem *getObject() const { + return static_cast<SPItem *>(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject * const obj) const override; + +}; + + +class SPTagUsePath : public SPTagUseReference { +public: + bool sourceDirty; + + SPObject *owner; + gchar *sourceHref; + Inkscape::XML::Node *sourceRepr; + SPObject *sourceObject; + + sigc::connection _delete_connection; + sigc::connection _changed_connection; + + SPTagUsePath(SPObject* i_owner); + ~SPTagUsePath() override; + + void link(char* to); + void unlink(); + void start_listening(SPObject* to); + void quit_listening(); + void refresh_source(); + + void (*user_unlink) (SPObject *user); +}; + +#endif /* !SEEN_SP_USE_REFERENCE_H */ + +/* + 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 : diff --git a/src/object/sp-tag-use.cpp b/src/object/sp-tag-use.cpp new file mode 100644 index 0000000..7acaa89 --- /dev/null +++ b/src/object/sp-tag-use.cpp @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <inkscape:tagref> implementation + * + * Authors: + * Theodore Janeczko + * Liam P White + * + * Copyright (C) Theodore Janeczko 2012-2014 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-tag-use.h" + +#include <cstring> +#include <string> + +#include <glibmm/i18n.h> + +#include "bad-uri-exception.h" +#include "display/drawing-group.h" +#include "attributes.h" +#include "document.h" +#include "uri.h" +#include "xml/repr.h" +#include "xml/href-attribute-helper.h" +#include "preferences.h" +#include "style.h" +#include "sp-factory.h" +#include "sp-symbol.h" +#include "sp-tag-use-reference.h" + +SPTagUse::SPTagUse() +{ + href = nullptr; + //new (_changed_connection) sigc::connection; + ref = new SPTagUseReference(this); + + _changed_connection = ref->changedSignal().connect(sigc::mem_fun(*this, &SPTagUse::href_changed)); +} + +SPTagUse::~SPTagUse() +{ + + if (child) { + detach(child); + child = nullptr; + } + + ref->detach(); + delete ref; + ref = nullptr; + + _changed_connection.~connection(); //FIXME why? +} + +void +SPTagUse::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + readAttr(SPAttr::XLINK_HREF); + + // We don't need to create child here: + // reading xlink:href will attach ref, and that will cause the changed signal to be emitted, + // which will call sp_tag_use_href_changed, and that will take care of the child +} + +void +SPTagUse::release() +{ + + if (child) { + detach(child); + child = nullptr; + } + + _changed_connection.disconnect(); + + g_free(href); + href = nullptr; + + ref->detach(); + + SPObject::release(); +} + +void +SPTagUse::set(SPAttr key, gchar const *value) +{ + + switch (key) { + case SPAttr::XLINK_HREF: { + if ( value && href && ( strcmp(value, href) == 0 ) ) { + /* No change, do nothing. */ + } else { + g_free(href); + href = nullptr; + if (value) { + // First, set the href field, because sp_tag_use_href_changed will need it. + href = g_strdup(value); + + // Now do the attaching, which emits the changed signal. + try { + ref->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + ref->detach(); + } + } else { + ref->detach(); + } + } + break; + } + + default: + SPObject::set(key, value); + break; + } +} + +Inkscape::XML::Node * +SPTagUse::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("inkscape:tagref"); + } + + SPObject::write(xml_doc, repr, flags); + + if (ref->getURI()) { + auto uri_string = ref->getURI()->str(); + auto href_key = Inkscape::getHrefAttribute(*repr).first; + repr->setAttributeOrRemoveIfEmpty(href_key, uri_string); + } + + return repr; +} + +/** + * Returns the ultimate original of a SPTagUse (i.e. the first object in the chain of its originals + * which is not an SPTagUse). If no original is found, NULL is returned (it is the responsibility + * of the caller to make sure that this is handled correctly). + * + * Note that the returned is the clone object, i.e. the child of an SPTagUse (of the argument one for + * the trivial case) and not the "true original". + */ + +SPItem * SPTagUse::root() +{ + SPObject *orig = child; + while (orig && is<SPTagUse>(orig)) { + orig = cast<SPTagUse>(orig)->child; + } + if (!orig || !is<SPItem>(orig)) + return nullptr; + return cast<SPItem>(orig); +} + +void +SPTagUse::href_changed(SPObject */*old_ref*/, SPObject */*ref*/) +{ + if (href) { + SPItem *refobj = ref->getObject(); + if (refobj) { + Inkscape::XML::Node *childrepr = refobj->getRepr(); + const std::string typeString = NodeTraits::get_type_string(*childrepr); + + SPObject* child_ = SPFactory::createObject(typeString); + if (child_) { + child = child_; + attach(child_, lastChild()); + sp_object_unref(child_, nullptr); + child_->invoke_build(this->document, childrepr, TRUE); + + } + } + } +} + +SPItem * SPTagUse::get_original() +{ + SPItem *ref_ = nullptr; + if (ref) { + ref_ = ref->getObject(); + } + return ref_; +} + +/* + 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 : diff --git a/src/object/sp-tag-use.h b/src/object/sp-tag-use.h new file mode 100644 index 0000000..5a5cb99 --- /dev/null +++ b/src/object/sp-tag-use.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_TAG_USE_H__ +#define __SP_TAG_USE_H__ + +/* + * SVG <inkscape:tagref> implementation + * + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> +#include <cstddef> +#include <sigc++/sigc++.h> +#include "svg/svg-length.h" +#include "sp-object.h" + +class SPItem; +class SPTagUse; +class SPTagUseReference; + +class SPTagUse final : public SPObject { + +public: + int tag() const override { return tag_of<decltype(*this)>; } + + // item built from the original's repr (the visible clone) + // relative to the SPUse itself, it is treated as a child, similar to a grouped item relative to its group + SPObject *child; + gchar *href; +public: + SPTagUse(); + ~SPTagUse() override; + + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttr key, gchar const *value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + void release() override; + + virtual void href_changed(SPObject* old_ref, SPObject* ref); + + //virtual SPItem* unlink(); + virtual SPItem* get_original(); + virtual SPItem* root(); + + // the reference to the original object + SPTagUseReference *ref; + sigc::connection _changed_connection; +}; + +#endif diff --git a/src/object/sp-tag.cpp b/src/object/sp-tag.cpp new file mode 100644 index 0000000..cad4b26 --- /dev/null +++ b/src/object/sp-tag.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <inkscape:tag> implementation + * + * Authors: + * Theodore Janeczko + * Liam P. White + * + * Copyright (C) Theodore Janeczko 2012-2014 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "sp-tag.h" +#include "xml/repr.h" +#include <cstring> + +/* + * Move this SPItem into or after another SPItem in the doc + * \param target - the SPItem to move into or after + * \param intoafter - move to after the target (false), move inside (sublayer) of the target (true) + */ +void SPTag::moveTo(SPObject *target, gboolean intoafter) { + + Inkscape::XML::Node *target_ref = ( target ? target->getRepr() : nullptr ); + Inkscape::XML::Node *our_ref = getRepr(); + gboolean first = FALSE; + + if (target_ref == our_ref) { + // Move to ourself ignore + return; + } + + if (!target_ref) { + // Assume move to the "first" in the top node, find the top node + target_ref = our_ref; + while (target_ref->parent() != target_ref->root()) { + target_ref = target_ref->parent(); + } + first = TRUE; + } + + if (intoafter) { + // Move this inside of the target at the end + our_ref->parent()->removeChild(our_ref); + target_ref->addChild(our_ref, nullptr); + } else if (target_ref->parent() != our_ref->parent()) { + // Change in parent, need to remove and add + our_ref->parent()->removeChild(our_ref); + target_ref->parent()->addChild(our_ref, target_ref); + } else if (!first) { + // Same parent, just move + our_ref->parent()->changeOrder(our_ref, target_ref); + } +} + +/** + * Reads the Inkscape::XML::Node, and initializes SPTag variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void +SPTag::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + readAttr(SPAttr::INKSCAPE_EXPANDED); + SPObject::build(document, repr); +} + +/** + * Sets a specific value in the SPTag. + */ +void +SPTag::set(SPAttr key, gchar const *value) +{ + + switch (key) + { + case SPAttr::INKSCAPE_EXPANDED: + if ( value && !strcmp(value, "true") ) { + setExpanded(true); + } + break; + default: + SPObject::set(key, value); + break; + } +} + +void SPTag::setExpanded(bool isexpanded) { + //if ( _expanded != isexpanded ){ + _expanded = isexpanded; + //} +} + +/** + * Receives update notifications. + */ +void +SPTag::update(SPCtx *ctx, guint flags) +{ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + SPObject::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node * +SPTag::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = doc->createElement("inkscape:tag"); + } + + // Inkscape-only object, not copied during an "plain SVG" dump: + if (flags & SP_OBJECT_WRITE_EXT) { + if (_expanded) { + repr->setAttribute("inkscape:expanded", "true"); + } else { + repr->removeAttribute("inkscape:expanded"); + } + } + SPObject::write(doc, repr, flags); + return repr; +} + + +/* + 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 : diff --git a/src/object/sp-tag.h b/src/object/sp-tag.h new file mode 100644 index 0000000..52b16f7 --- /dev/null +++ b/src/object/sp-tag.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_TAG_H_SEEN +#define SP_TAG_H_SEEN + +/** \file + * SVG <inkscape:tag> implementation + * + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +/* Skeleton base class */ + +class SPTag final : public SPObject { +public: + SPTag() = default; + ~SPTag() override = default; + int tag() const override { return tag_of<decltype(*this)>; } + + void build(SPDocument * doc, Inkscape::XML::Node *repr) override; + //virtual void release(); + void set(SPAttr key, const gchar* value) override; + void update(SPCtx * ctx, unsigned flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + bool expanded() const { return _expanded; } + void setExpanded(bool isexpanded); + + void moveTo(SPObject *target, gboolean intoafter); + +private: + bool _expanded; +}; + +#endif /* !SP_SKELETON_H_SEEN */ + +/* + 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 : diff --git a/src/object/sp-text.cpp b/src/object/sp-text.cpp new file mode 100644 index 0000000..95b0a46 --- /dev/null +++ b/src/object/sp-text.cpp @@ -0,0 +1,1810 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <text> and <tspan> implementation + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * fixme: + * + * These subcomponents should not be items, or alternately + * we have to invent set of flags to mark, whether standard + * attributes are applicable to given item (I even like this + * idea somewhat - Lauris) + * + */ + +#include <2geom/affine.h> +#include <libnrtype/font-factory.h> +#include <libnrtype/font-instance.h> + +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include "svg/svg.h" +#include "display/drawing-text.h" +#include "attributes.h" +#include "document.h" +#include "preferences.h" +#include "desktop.h" +#include "desktop-style.h" +#include "sp-namedview.h" +#include "inkscape.h" +#include "xml/quote.h" +#include "mod360.h" +#include "path/path-boolop.h" + +#include "sp-title.h" +#include "sp-desc.h" +#include "sp-rect.h" +#include "sp-text.h" + +#include "sp-shape.h" +#include "sp-textpath.h" +#include "sp-tref.h" +#include "sp-tspan.h" +#include "sp-flowregion.h" + +#include "text-editing.h" + +// For SVG 2 text flow +#include "livarot/Path.h" +#include "livarot/Shape.h" +#include "display/curve.h" + +#include "layer-manager.h" + +/*##################################################### +# SPTEXT +#####################################################*/ +SPText::SPText() : SPItem() { +} + +SPText::~SPText() +{ + if (css) { + sp_repr_css_attr_unref(css); + } +}; + +void SPText::build(SPDocument *doc, Inkscape::XML::Node *repr) { + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + this->readAttr(SPAttr::DX); + this->readAttr(SPAttr::DY); + this->readAttr(SPAttr::ROTATE); + + // textLength and friends + this->readAttr(SPAttr::TEXTLENGTH); + this->readAttr(SPAttr::LENGTHADJUST); + SPItem::build(doc, repr); + css = nullptr; + this->readAttr(SPAttr::SODIPODI_LINESPACING); // has to happen after the styles are read +} + +void SPText::release() +{ + view_style_attachments.clear(); + SPItem::release(); +} + +void SPText::set(SPAttr key, const gchar* value) { + //std::cout << "SPText::set: " << sp_attribute_name( key ) << ": " << (value?value:"Null") << std::endl; + + if (this->attributes.readSingleAttribute(key, value, style, &viewport)) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else { + switch (key) { + case SPAttr::SODIPODI_LINESPACING: + // convert deprecated tag to css... but only if 'line-height' missing. + if (value && !this->style->line_height.set) { + this->style->line_height.set = TRUE; + this->style->line_height.inherit = FALSE; + this->style->line_height.normal = FALSE; + this->style->line_height.unit = SP_CSS_UNIT_PERCENT; + this->style->line_height.value = this->style->line_height.computed = sp_svg_read_percentage (value, 1.0); + } + // Remove deprecated attribute + this->removeAttribute("sodipodi:linespacing"); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); + break; + + default: + SPItem::set(key, value); + break; + } + } +} + +void SPText::child_added(Inkscape::XML::Node *rch, Inkscape::XML::Node *ref) { + SPItem::child_added(rch, ref); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_CONTENT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); +} + +void SPText::remove_child(Inkscape::XML::Node *rch) { + SPItem::remove_child(rch); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_CONTENT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); +} + + +void SPText::update(SPCtx *ctx, guint flags) { + + unsigned childflags = (flags & SP_OBJECT_MODIFIED_CASCADE); + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + // Create temporary list of children + std::vector<SPObject *> l; + for (auto& child: children) { + sp_object_ref(&child, this); + l.push_back(&child); + } + + for (auto child:l) { + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + /* fixme: Do we need transform? */ + child->updateDisplay(ctx, childflags); + } + sp_object_unref(child, this); + } + + // update ourselves after updating children + SPItem::update(ctx, flags); + + if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_LAYOUT_MODIFIED_FLAG ) ) + { + + SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx); + + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + attributes.update( em, ex, w, h ); + + // Set inline_size computed value if necessary (i.e. if unit is %). + if (has_inline_size()) { + if (style->inline_size.unit == SP_CSS_UNIT_PERCENT) { + if (is_horizontal()) { + style->inline_size.computed = style->inline_size.value * ictx->viewport.width(); + } else { + style->inline_size.computed = style->inline_size.value * ictx->viewport.height(); + } + } + } + + /* fixme: It is not nice to have it here, but otherwise children content changes does not work */ + /* fixme: Even now it may not work, as we are delayed */ + /* fixme: So check modification flag everywhere immediate state is used */ + this->rebuildLayout(); + + Geom::OptRect paintbox = this->geometricBounds(); + + for (auto &v : views) { + auto &sa = view_style_attachments[v.key]; + sa.unattachAll(); + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + _clearFlow(g); + g->setStyle(style, parent->style); + // pass the bbox of this as paintbox (used for paintserver fills) + layout.show(g, sa, paintbox); + } + } +} + +void SPText::modified(guint flags) { +// SPItem::onModified(flags); + + guint cflags = (flags & SP_OBJECT_MODIFIED_CASCADE); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + cflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + // FIXME: all that we need to do here is to call setStyle, to set the changed + // style, but there's no easy way to access the drawing glyphs or texts corresponding to a + // text this. Therefore we do here the same as in _update, that is, destroy all items + // and create new ones. This is probably quite wasteful. + if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG )) { + Geom::OptRect paintbox = geometricBounds(); + + for (auto &v : views) { + auto &sa = view_style_attachments[v.key]; + sa.unattachAll(); + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + _clearFlow(g); + g->setStyle(style, parent->style); + layout.show(g, sa, paintbox); + } + } + + // Create temporary list of children + std::vector<SPObject *> l; + for (auto& child: children) { + sp_object_ref(&child, this); + l.push_back(&child); + } + + for (auto child:l) { + if (cflags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(cflags); + } + sp_object_unref(child, this); + } +} + +Inkscape::XML::Node *SPText::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + if (!repr) { + repr = xml_doc->createElement("svg:text"); + // we preserve spaces in the text objects we create + repr->setAttribute("xml:space", "preserve"); + } + + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + if (is<SPTitle>(&child) || is<SPDesc>(&child)) { + continue; + } + + Inkscape::XML::Node *crepr = nullptr; + + if (is<SPString>(&child)) { + crepr = xml_doc->createTextNode(cast<SPString>(&child)->string.c_str()); + } else { + 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 (is<SPTitle>(&child) || is<SPDesc>(&child)) { + continue; + } + + if (is<SPString>(&child)) { + child.getRepr()->setContent(cast<SPString>(&child)->string.c_str()); + } else { + child.updateRepr(flags); + } + } + } + + this->attributes.writeTo(repr); + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + + +Geom::OptRect SPText::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const { + return this->layout.bounds(transform, type == SPItem::VISUAL_BBOX); +} + +Inkscape::DrawingItem* SPText::show(Inkscape::Drawing &drawing, unsigned key, unsigned /*flags*/) { + Inkscape::DrawingGroup *flowed = new Inkscape::DrawingGroup(drawing); + flowed->setPickChildren(false); + flowed->setStyle(this->style, this->parent->style); + + // pass the bbox of the text object as paintbox (used for paintserver fills) + layout.show(flowed, view_style_attachments[key], geometricBounds()); + + return flowed; +} + + +void SPText::hide(unsigned key) +{ + view_style_attachments.erase(key); + for (auto &v : views) { + if (v.key == key) { + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + _clearFlow(g); + } + } +} + +const char* SPText::typeName() const { + if (has_inline_size() || has_shape_inside()) + return "text-flow"; + return "text"; +} + +const char* SPText::displayName() const { + if (has_inline_size()) { + return _("Auto-wrapped text"); + } else if (has_shape_inside()) { + return _("Text in-a-shape"); + } else { + return _("Text"); + } +} + +gchar* SPText::description() const { + + SPStyle *style = this->style; + + char *n = xml_quote_strdup(style->font_family.value()); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + Inkscape::Util::Quantity q = Inkscape::Util::Quantity(style->font_size.computed, "px"); + q.quantity *= this->i2doc_affine().descrim(); + Glib::ustring xs = q.string(sp_style_get_css_unit_string(unit)); + + char const *trunc = ""; + Inkscape::Text::Layout const *layout = te_get_layout((SPItem *) this); + + if (layout && layout->inputTruncated()) { + trunc = _(" [truncated]"); + } + + char *ret = ( SP_IS_TEXT_TEXTPATH(this) + ? g_strdup_printf(_("on path%s (%s, %s)"), trunc, n, xs.c_str()) + : g_strdup_printf(_("%s (%s, %s)"), trunc, n, xs.c_str()) ); + return ret; +} + +void SPText::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_TEXT_BASELINE)) { + // Choose a point on the baseline for snapping from or to, with the horizontal position + // of this point depending on the text alignment (left vs. right) + Inkscape::Text::Layout const *layout = te_get_layout(this); + + if (layout != nullptr && layout->outputExists()) { + std::optional<Geom::Point> pt = layout->baselineAnchorPoint(); + + if (pt) { + p.emplace_back((*pt) * this->i2dt_affine(), Inkscape::SNAPSOURCE_TEXT_ANCHOR, Inkscape::SNAPTARGET_TEXT_ANCHOR); + } + } + } +} + +void SPText::hide_shape_inside() +{ + auto text = this; + SPStyle *item_style = this->style; + if (item_style && text && item_style->shape_inside.set) { + SPCSSAttr *css_unset = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET); + css = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET); + sp_repr_css_unset_property(css_unset, "shape-inside"); + sp_repr_css_attr_unref(css_unset); + this->changeCSS(css_unset, "style"); + } else { + css = nullptr; + } +} + +void SPText::show_shape_inside() +{ + if (css) { + this->changeCSS(css, "style"); + } +} + +Geom::Affine SPText::set_transform(Geom::Affine const &xform) { + // See if 'shape-inside' has rectangle + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/text/use_svg2", true)) { + if (style->shape_inside.set) { + return xform; + } + } + // we cannot optimize textpath because changing its fontsize will break its match to the path + + if (SP_IS_TEXT_TEXTPATH (this)) { + if (!this->_optimizeTextpathText) { + return xform; + } else { + this->_optimizeTextpathText = false; + } + } + + // we cannot optimize text with textLength because it may show different size than specified + if (this->attributes.getTextLength()->_set) + return xform; + + if (this->style && this->style->inline_size.set) + return xform; + + /* This function takes care of scaling & translation only, we return whatever parts we can't + handle. */ + +// TODO: pjrm tried to use fontsize_expansion(xform) here and it works for text in that font size +// is scaled more intuitively when scaling non-uniformly; however this necessitated using +// fontsize_expansion instead of expansion in other places too, where it was not appropriate +// (e.g. it broke stroke width on copy/pasting of style from horizontally stretched to vertically +// stretched shape). Using fontsize_expansion only here broke setting the style via font +// dialog. This needs to be investigated further. + double const ex = xform.descrim(); + if (ex == 0) { + return xform; + } + + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + ret[0] /= ex; + ret[1] /= ex; + ret[2] /= ex; + ret[3] /= ex; + + // Adjust x/y, dx/dy + this->_adjustCoordsRecursive (this, xform * ret.inverse(), ex); + + // Adjust font size + this->_adjustFontsizeRecursive (this, ex); + + // Adjust stroke width + this->adjust_stroke_width_recursive (ex); + + // Adjust pattern fill + this->adjust_pattern(xform * ret.inverse()); + + // Adjust gradient fill + this->adjust_gradient(xform * ret.inverse()); + + return ret; +} + +void SPText::print(SPPrintContext *ctx) { + Geom::OptRect pbox, bbox, dbox; + pbox = this->geometricBounds(); + bbox = this->desktopVisualBounds(); + dbox = Geom::Rect::from_xywh(Geom::Point(0,0), this->document->getDimensions()); + + Geom::Affine const ctm (this->i2dt_affine()); + + this->layout.print(ctx,pbox,dbox,bbox,ctm); +} + +/* + * Member functions + */ + +void SPText::_buildLayoutInit() +{ + + layout.strut.reset(); + layout.wrap_mode = Inkscape::Text::Layout::WRAP_NONE; // Default to SVG 1.1 + + if (style) { + + // Strut + auto font = FontFactory::get().FaceFromStyle(style); + if (font) { + font->FontMetrics(layout.strut.ascent, layout.strut.descent, layout.strut.xheight); + } + layout.strut *= style->font_size.computed; + if (style->line_height.normal ) { + layout.strut.computeEffective( Inkscape::Text::Layout::LINE_HEIGHT_NORMAL ); + } else if (style->line_height.unit == SP_CSS_UNIT_NONE) { + layout.strut.computeEffective( style->line_height.computed ); + } else { + if( style->font_size.computed > 0.0 ) { + layout.strut.computeEffective( style->line_height.computed/style->font_size.computed ); + } + } + + + // To do: follow SPItem clip_ref/mask_ref code + if (style->shape_inside.set ) { + layout.wrap_mode = Inkscape::Text::Layout::WRAP_SHAPE_INSIDE; + for (auto const *wrap_shape : makeEffectiveShapes()) { + layout.appendWrapShape(wrap_shape); + } + } else if (has_inline_size()) { + + layout.wrap_mode = Inkscape::Text::Layout::WRAP_INLINE_SIZE; + + // If both shape_inside and inline_size are set, shape_inside wins out. + + // We construct a rectangle with one dimension set by the computed value of 'inline-size' + // and the other dimension set to infinity. Text is laid out starting at the 'x' and 'y' + // attribute values. This is handled elsewhere. + + Geom::OptRect opt_frame = get_frame(); + Geom::Rect frame = *opt_frame; + + Shape *shape = new Shape; + shape->Reset(); + int v0 = shape->AddPoint(frame.corner(0)); + int v1 = shape->AddPoint(frame.corner(1)); + int v2 = shape->AddPoint(frame.corner(2)); + int v3 = shape->AddPoint(frame.corner(3)); + shape->AddEdge(v0, v1); + shape->AddEdge(v1, v2); + shape->AddEdge(v2, v3); + shape->AddEdge(v3, v0); + Shape *uncross = new Shape; + uncross->ConvertToShape( shape ); + + layout.appendWrapShape( uncross ); + + delete shape; + + } else if (style->white_space.value == SP_CSS_WHITE_SPACE_PRE || + style->white_space.value == SP_CSS_WHITE_SPACE_PREWRAP || + style->white_space.value == SP_CSS_WHITE_SPACE_PRELINE ) { + layout.wrap_mode = Inkscape::Text::Layout::WRAP_WHITE_SPACE; + } + + } // if (style) +} + +unsigned SPText::_buildLayoutInput(SPObject *object, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_optional_attrs, unsigned parent_attrs_offset, bool in_textpath) +{ + unsigned length = 0; + unsigned child_attrs_offset = 0; + Inkscape::Text::Layout::OptionalTextTagAttrs optional_attrs; + + // Per SVG spec, an object with 'display:none' doesn't contribute to text layout. + if (object->style->display.computed == SP_CSS_DISPLAY_NONE) { + return 0; + } + + auto text_object = cast<SPText>(object); + auto tspan_object = cast<SPTSpan>(object); + auto tref_object = cast<SPTRef>(object); + auto textpath_object = cast<SPTextPath>(object); + + if (text_object) { + + bool use_xy = true; + bool use_dxdyrotate = true; + + // SVG 2 Text wrapping. + if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_SHAPE_INSIDE || + layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) { + use_xy = false; + use_dxdyrotate = false; + } + + text_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, use_xy, use_dxdyrotate); + + // SVG 2 Text wrapping + if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) { + + // For horizontal text: + // 'x' is used to calculate the left/right edges of the rectangle but is not + // needed later. If not deleted here, it will cause an incorrect positioning + // of the first line. + // 'y' is used to determine where the first line box is located and is needed + // during the output stage. + // For vertical text: + // Follow above but exchange 'x' and 'y'. + // The SVG 2 spec currently says use the 'x' and 'y' from the <text> element, + // if not defined in the <text> element, use the 'x' and 'y' from the first child. + // We only look at the <text> element. (Doing otherwise means tracking if + // we've found 'x' and 'y' and then creating the Shape at the end.) + if (is_horizontal()) { + // Horizontal text + SVGLength* y = _getFirstYLength(); + if (y) { + optional_attrs.y.push_back(*y); + } else { + std::cerr << "SPText::_buildLayoutInput: No 'y' attribute value with horizontal 'inline-size'!" << std::endl; + } + } else { + // Vertical text + SVGLength* x = _getFirstXLength(); + if (x) { + optional_attrs.x.push_back(*x); + } else { + std::cerr << "SPText::_buildLayoutInput: No 'x' attribute value with vertical 'inline-size'!" << std::endl; + } + } + } + + // set textLength on the entire layout, see note in TNG-Layout.h + if (text_object->attributes.getTextLength()->_set) { + layout.textLength._set = true; + layout.textLength.value = text_object->attributes.getTextLength()->value; + layout.textLength.computed = text_object->attributes.getTextLength()->computed; + layout.textLength.unit = text_object->attributes.getTextLength()->unit; + layout.lengthAdjust = (Inkscape::Text::Layout::LengthAdjust) text_object->attributes.getLengthAdjust(); + } + } + + else if (tspan_object) { + + // x, y attributes are stripped from some tspans marked with role="line" as we do our own line layout. + // This should be checked carefully, as it can undo line layout in imported SVG files. + bool use_xy = !in_textpath && + (tspan_object->role == SP_TSPAN_ROLE_UNSPECIFIED || !tspan_object->attributes.singleXYCoordinates()); + bool use_dxdyrotate = true; + + // SVG 2 Text wrapping: see comment above. + if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_SHAPE_INSIDE || + layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) { + use_xy = false; + use_dxdyrotate = false; + } + + tspan_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, use_xy, use_dxdyrotate); + + if (tspan_object->role != SP_TSPAN_ROLE_UNSPECIFIED) { + // We are doing line wrapping using sodipodi:role="line". New lines have been stripped. + + // Insert paragraph break before text if not first tspan. + SPObject *prev_object = object->getPrev(); + if (prev_object && cast<SPTSpan>(prev_object)) { + if (!layout.inputExists()) { + // Add an object to store style, needed even if there is no text. When does this happen? + layout.appendText("", prev_object->style, prev_object, &optional_attrs); + } + layout.appendControlCode(Inkscape::Text::Layout::PARAGRAPH_BREAK, prev_object); + } + + // Create empty span to store info (any non-empty tspan with sodipodi:role="line" has a child). + if (!object->hasChildren()) { + layout.appendText("", object->style, object, &optional_attrs); + } + + length++; // interpreting line breaks as a character for the purposes of x/y/etc attributes + // is a liberal interpretation of the svg spec, but a strict reading would mean + // that if the first line is empty the second line would take its place at the + // start position. Very confusing. + // SVG 2 clarifies, attributes are matched to unicode input characters so line + // breaks do match to an x/y/etc attribute. + child_attrs_offset--; + } + } + + else if (tref_object) { + tref_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, true, true); + } + + else if (textpath_object) { + in_textpath = true; // This should be made local so we can mix normal text with textpath per SVG 2. + textpath_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, false, true); + optional_attrs.x.clear(); // Hmm, you can use x with horizontal text. So this is probably wrong. + optional_attrs.y.clear(); + } + + else { + optional_attrs = parent_optional_attrs; + child_attrs_offset = parent_attrs_offset; + } + + // Recurse + for (auto& child: object->children) { + auto str = cast<SPString>(&child); + if (str) { + Glib::ustring const &string = str->string; + // std::cout << " Appending: >" << string << "<" << std::endl; + layout.appendText(string, object->style, &child, &optional_attrs, child_attrs_offset + length); + length += string.length(); + } else if (!sp_repr_is_meta_element(child.getRepr())) { + /* ^^^^ XML Tree being directly used here while it shouldn't be.*/ + length += _buildLayoutInput(&child, optional_attrs, child_attrs_offset + length, in_textpath); + } + } + + return length; +} + +std::unique_ptr<Shape> SPText::getExclusionShape() const +{ + auto result = std::make_unique<Shape>(); // Union of all exclusion shapes + + for (auto *href : style->shape_subtract.hrefs) { + auto shape = href->getObject(); + if (!shape) { + continue; + } + if (!shape->curve()) { + shape->set_shape(); + } + SPCurve const *curve = shape->curve(); + if (!curve) { + continue; + } + + auto temp = std::make_unique<Path>(); + temp->LoadPathVector(curve->get_pathvector(), shape->transform, true); + + auto margin = std::make_unique<Path>(); + if (shape->style->shape_margin.set) { + temp->OutsideOutline(margin.get(), -shape->style->shape_margin.computed, join_round, butt_straight, 20.0); + } else { + margin = std::move(temp); + } + + margin->Convert(0.25); // Convert to polyline + auto livarot_shape = std::make_unique<Shape>(); + margin->Fill(livarot_shape.get(), 0); + + auto uncrossed = std::make_unique<Shape>(); + uncrossed->ConvertToShape(livarot_shape.get()); + + if (result->hasEdges()) { + auto shape_temp = std::make_unique<Shape>(); + shape_temp->Booleen(result.get(), uncrossed.get(), bool_op_union); + std::swap(result, shape_temp); + } else { + result->Copy(uncrossed.get()); + } + + } + return result; +} + +Shape* SPText::getInclusionShape(SPShape *shape) const +{ + if (!shape) { + return nullptr; + } + if (!shape->curve()) { + shape->set_shape(); + } + auto curve = shape->curve(); + if (!curve) { + return nullptr; + } + + bool padding = style->shape_padding.set; + double padding_amount = 0.0; + if (padding) { + padding_amount = std::abs(style->shape_padding.computed); + if (padding_amount < 1e-12) { + padding = false; + } + } + + auto pathvector = curve->get_pathvector(); + sp_flatten(pathvector, fill_nonZero); + + auto temp_path = std::make_unique<Path>(); + temp_path->LoadPathVector(pathvector, shape->transform, true); + + auto const make_nice_shape = [](std::unique_ptr<Path> const &contour) -> Shape * { + auto temp = std::make_unique<Shape>(); + contour->ConvertWithBackData(1.0); + contour->Fill(temp.get(), 0); + Shape *result = new Shape; + result->ConvertToShape(temp.get()); + return result; + }; + + Shape *result = nullptr; + if (padding) { + auto outline = std::make_unique<Path>(); + temp_path->Outline(outline.get(), style->shape_padding.computed, join_round, butt_straight, 20.0); + + std::unique_ptr<Shape> inclusion_shape{make_nice_shape(temp_path)}; + std::unique_ptr<Shape> thickened_border{make_nice_shape(outline)}; + + result = new Shape; + result->Booleen(inclusion_shape.get(), thickened_border.get(), bool_op_diff); + } else { + result = make_nice_shape(temp_path); + } + + return result; +} + +std::vector<Shape *> SPText::makeEffectiveShapes() const +{ + // Find union of all exclusion shapes + std::unique_ptr<Shape> exclusion_shape; + if (style->shape_subtract.set) { + exclusion_shape = getExclusionShape(); + } + bool const has_exclusion = exclusion_shape && exclusion_shape->hasEdges(); + + std::vector<Shape *> result; + // Find inside shape curves + for (auto *href : style->shape_inside.hrefs) { + auto obj = href->getObject(); + if (Shape *textarea_shape = getInclusionShape(obj)) { + if (has_exclusion) { + // Subtract exclusion shape + Shape *copy = new Shape; + copy->Booleen(textarea_shape, exclusion_shape.get(), bool_op_diff); + delete textarea_shape; + textarea_shape = copy; + } + result.push_back(textarea_shape); + } else { + std::cerr << __FUNCTION__ << ": Failed to get curve." << std::endl; + } + } + return result; +} + + +// SVG requires one to use the first x/y value found on a child element if x/y not given on text +// element. TODO: Recurse. +SVGLength* +SPText::_getFirstXLength() +{ + SVGLength* x = attributes.getFirstXLength(); + + if (!x) { + for (auto& child: children) { + if (is<SPTSpan>(&child)) { + auto tspan = cast<SPTSpan>(&child); + x = tspan->attributes.getFirstXLength(); + break; + } + } + } + + return x; +} + + +SVGLength* +SPText::_getFirstYLength() +{ + SVGLength* y = attributes.getFirstYLength(); + + if (!y) { + for (auto& child: children) { + if (is<SPTSpan>(&child)) { + auto tspan = cast<SPTSpan>(&child); + y = tspan->attributes.getFirstYLength(); + break; + } + } + } + + return y; +} + +SPCurve SPText::getNormalizedBpath() const +{ + return layout.convertToCurves(); +} + +void SPText::rebuildLayout() +{ + layout.clear(); + _buildLayoutInit(); + + Inkscape::Text::Layout::OptionalTextTagAttrs optional_attrs; + _buildLayoutInput(this, optional_attrs, 0, false); + + layout.calculateFlow(); + + for (auto& child: children) { + if (is<SPTextPath>(&child)) { + SPTextPath const *textpath = cast<SPTextPath>(&child); + if (textpath->originalPath != nullptr) { +#if DEBUG_TEXTLAYOUT_DUMPASTEXT + g_print("%s", layout.dumpAsText().c_str()); +#endif + layout.fitToPathAlign(textpath->startOffset, *textpath->originalPath); + } + } + } +#if DEBUG_TEXTLAYOUT_DUMPASTEXT + g_print("%s", layout.dumpAsText().c_str()); +#endif + + // set the x,y attributes on role:line spans + for (auto& child: children) { + if (is<SPTSpan>(&child)) { + auto tspan = cast<SPTSpan>(&child); + if ((tspan->role != SP_TSPAN_ROLE_UNSPECIFIED) + && tspan->attributes.singleXYCoordinates() ) { + Inkscape::Text::Layout::iterator iter = layout.sourceToIterator(tspan); + Geom::Point anchor_point = layout.chunkAnchorPoint(iter); + tspan->attributes.setFirstXY(anchor_point); + // repr needs to be updated but if we do it here we get a loop. + } + } + } +} + + +void SPText::_adjustFontsizeRecursive(SPItem *item, double ex, bool is_root) +{ + SPStyle *style = item->style; + + if (style && !Geom::are_near(ex, 1.0)) { + if (!style->font_size.set && is_root) { + style->font_size.set = true; + } + style->font_size.type = SP_FONT_SIZE_LENGTH; + style->font_size.computed *= ex; + style->letter_spacing.computed *= ex; + style->word_spacing.computed *= ex; + if (style->line_height.unit != SP_CSS_UNIT_NONE && + style->line_height.unit != SP_CSS_UNIT_PERCENT && + style->line_height.unit != SP_CSS_UNIT_EM && + style->line_height.unit != SP_CSS_UNIT_EX) { + // No unit on 'line-height' property has special behavior. + style->line_height.computed *= ex; + } + item->updateRepr(); + } + + for(auto& o: item->children) { + if (is<SPItem>(&o)) + _adjustFontsizeRecursive(cast<SPItem>(&o), ex, false); + } +} + +/** + * Get the position of the baseline point for this text object. + */ +std::optional<Geom::Point> SPText::getBaselinePoint() const +{ + if (layout.outputExists()) { + return layout.baselineAnchorPoint(); + } + return std::optional<Geom::Point>(); +} + +void +remove_newlines_recursive(SPObject* object, bool is_svg2) +{ + // Replace '\n' by space. + auto string = cast<SPString>(object); + if (string) { + static Glib::RefPtr<Glib::Regex> r = Glib::Regex::create("\n+"); + string->string = r->replace(string->string, 0, " ", (Glib::RegexMatchFlags)0); + string->getRepr()->setContent(string->string.c_str()); + } + + for (auto child : object->childList(false)) { + remove_newlines_recursive(child, is_svg2); + } + + // Add space at end of a line if line is created by sodipodi:role="line". + auto tspan = cast<SPTSpan>(object); + if (tspan && + tspan->role == SP_TSPAN_ROLE_LINE && + tspan->getNext() != nullptr && // Don't add space at end of last line. + !is_svg2) { // SVG2 uses newlines, should not have sodipodi:role. + + std::vector<SPObject *> children = tspan->childList(false); + + // Find last string (could be more than one if there is tspan in the middle of a tspan). + for (auto it = children.rbegin(); it != children.rend(); ++it) { + auto string = cast<SPString>(*it); + if (string) { + string->string += ' '; + string->getRepr()->setContent(string->string.c_str()); + break; + } + } + } +} + +// Prepare multi-line text for putting on path. +void +SPText::remove_newlines() +{ + remove_newlines_recursive(this, has_shape_inside() || has_inline_size()); + style->inline_size.clear(); + style->shape_inside.clear(); + updateRepr(); +} + +void SPText::_adjustCoordsRecursive(SPItem *item, Geom::Affine const &m, double ex, bool is_root) +{ + if (is<SPTSpan>(item)) + cast<SPTSpan>(item)->attributes.transform(m, ex, ex, is_root); + // it doesn't matter if we change the x,y for role=line spans because we'll just overwrite them anyway + else if (is<SPText>(item)) + cast<SPText>(item)->attributes.transform(m, ex, ex, is_root); + else if (is<SPTextPath>(item)) + cast<SPTextPath>(item)->attributes.transform(m, ex, ex, is_root); + else if (is<SPTRef>(item)) { + cast<SPTRef>(item)->attributes.transform(m, ex, ex, is_root); + } else { + g_warning("element is not text"); + return; + } + + for(auto& o: item->children) { + if (is<SPItem>(&o)) + _adjustCoordsRecursive(cast<SPItem>(&o), m, ex, false); + } +} + + +void SPText::_clearFlow(Inkscape::DrawingGroup *in_arena) +{ + in_arena->clearChildren(); +} + + +/** Remove 'x' and 'y' values on children (lines) or they will be interpreted as absolute positions + * when 'inline-size' is removed. + */ +void SPText::remove_svg11_fallback() { + for (auto& child: children) { + child.removeAttribute("x"); + child.removeAttribute("y"); + } +} + +/** Convert new lines in 'inline-size' text to tspans with sodipodi:role="tspan". + * Note sodipodi:role="tspan" will be removed in the future! + */ +void SPText::newline_to_sodipodi() { + + // New lines can come anywhere, we must search character-by-character. + auto it = layout.begin(); + while (it != layout.end()) { + if (layout.characterAt(it) == '\n') { + + // Delete newline ('\n'). + iterator_pair pair; + auto it_end = it; + it_end.nextCharacter(); + sp_te_delete (this, it, it_end, pair); + it = pair.first; + + // Insert newline (sodipodi:role="line"). + it = sp_te_insert_line(this, it); + } + + it.nextCharacter(); + layout.validateIterator(&it); + } +} + +/** Convert tspans with sodipodi:role="tspans" to '\n'. + * Note sodipodi:role="tspan" will be removed in the future! + */ +void SPText::sodipodi_to_newline() { + + // tspans with sodipodi:role="line" are only direct children of a <text> element. + for (auto child : childList(false)) { + auto tspan = cast<SPTSpan>(child); // Could have <desc> or <title>. + if (tspan && tspan->role == SP_TSPAN_ROLE_LINE) { + + // Remove sodipodi:role attribute. + tspan->removeAttribute("sodipodi:role"); + tspan->updateRepr(); + + // Insert '/n' if not last line. + // This may screw up dx, dy, rotate attribute counting but... SVG 2 text cannot have these values. + if (tspan != lastChild()) { + tspan->style->white_space.computed = SP_CSS_WHITE_SPACE_PRE; // Set so '\n' is not immediately stripped out before CSS recascaded! + auto last_child = tspan->lastChild(); + auto last_string = cast<SPString>(last_child); + if (last_string) { + // Add '\n' to string. + last_string->string += "\n"; + last_string->updateRepr(); + } else { + // Insert new string with '\n'. + auto tspan_node = tspan->getRepr(); + auto xml_doc = tspan_node->document(); + tspan_node->appendChild(xml_doc->createTextNode("\n")); + } + } + } + } +} + +bool SPText::is_horizontal() const +{ + unsigned mode = style->writing_mode.computed; + return (mode == SP_CSS_WRITING_MODE_LR_TB || mode == SP_CSS_WRITING_MODE_RL_TB); +} + +bool SPText::has_inline_size() const +{ + // If inline size is '0' it is as if it is not set. + return (style->inline_size.set && style->inline_size.value != 0); +} + +bool SPText::has_shape_inside() const +{ + return (style->shape_inside.set); +} + +// Gets rectangle defined by <text> x, y and inline-size ("infinite" in one direction). +Geom::OptRect SPText::get_frame() +{ + Geom::OptRect opt_frame; + Geom::Rect frame; + + if (has_inline_size()) { + double inline_size = style->inline_size.computed; + //unsigned mode = style->writing_mode.computed; + unsigned anchor = style->text_anchor.computed; + unsigned direction = style->direction.computed; + + if (is_horizontal()) { + // horizontal + frame = Geom::Rect::from_xywh(attributes.firstXY()[Geom::X], -100000, inline_size, 200000); + if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + frame *= Geom::Translate (-inline_size/2.0, 0 ); + } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) || + (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) { + frame *= Geom::Translate (-inline_size, 0); + } + } else { + // vertical + frame = Geom::Rect::from_xywh(-100000, attributes.firstXY()[Geom::Y], 200000, inline_size); + if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + frame *= Geom::Translate (0, -inline_size/2.0); + } else if (anchor == SP_CSS_TEXT_ANCHOR_END) { + frame *= Geom::Translate (0, -inline_size); + } + } + + opt_frame = frame; + + } else { + // See if 'shape-inside' has rectangle + Inkscape::XML::Node* rectangle = get_first_rectangle(); + + if (rectangle) { + double x = rectangle->getAttributeDouble("x", 0.0); + double y = rectangle->getAttributeDouble("y", 0.0); + double width = rectangle->getAttributeDouble("width", 0.0); + double height = rectangle->getAttributeDouble("height", 0.0); + frame = Geom::Rect::from_xywh(x, y, width, height); + opt_frame = frame; + } + } + + return opt_frame; +} + +// Find the node of the first rectangle (if it exists) in 'shape-inside'. +Inkscape::XML::Node* SPText::get_first_rectangle() +{ + if (style->shape_inside.set) { + + for (auto *href : style->shape_inside.hrefs) { + auto *shape = href->getObject(); + if (is<SPRect>(shape)) { + auto *item = shape->getRepr(); + g_return_val_if_fail(item, nullptr); + assert(strncmp("svg:rect", item->name(), 8) == 0); + return item; + } + } + } + + return nullptr; +} + +void SPText::getLinked(std::vector<SPObject *> &objects, bool ignore_clones) const +{ + for (auto item : get_all_shape_dependencies()) { + objects.push_back(item); + } + SPObject::getLinked(objects, ignore_clones); +} + +/** + * Get the first shape reference which affects the position and layout of + * this text item. This can be either a shape-inside or a textPath referenced + * shape. If this text does not depend on any other shape, then return NULL. + */ +SPItem *SPText::get_first_shape_dependency() +{ + for (auto item : get_all_shape_dependencies()) { + return item; + } + return nullptr; +} + +const std::vector<SPItem *> SPText::get_all_shape_dependencies() const +{ + std::vector<SPItem *> ret; + if (style->shape_inside.set) { + for (auto *href : style->shape_inside.hrefs) { + ret.push_back(href->getObject()); + } + } else if (auto textpath = cast<SPTextPath>(firstChild())) { + ret.push_back(sp_textpath_get_path_item(textpath)); + } + return ret; +} + +SPItem *create_text_with_inline_size (SPDesktop *desktop, Geom::Point p0, Geom::Point p1) +{ + SPDocument *doc = desktop->getDocument(); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *text_repr = xml_doc->createElement("svg:text"); + text_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create + + auto layer = desktop->layerManager().currentLayer(); + g_assert(layer != nullptr); + + auto text_object = cast<SPText>(layer->appendChildRepr(text_repr)); + g_assert(text_object != nullptr); + + // Invert coordinate system? + p0 *= desktop->dt2doc(); + p1 *= desktop->dt2doc(); + + // Pixels to user units + p0 *= layer->i2doc_affine().inverse(); + p1 *= layer->i2doc_affine().inverse(); + + text_repr->setAttributeSvgDouble("x", p0[Geom::X]); + text_repr->setAttributeSvgDouble("y", p0[Geom::Y]); + + double inline_size = p1[Geom::X] - p0[Geom::X]; + + text_object->style->inline_size.setDouble( inline_size ); + text_object->style->inline_size.set = true; + + Inkscape::XML::Node *text_node = xml_doc->createTextNode(""); + text_repr->appendChild(text_node); + + //text_object->transform = layer->i2doc_affine().inverse(); + text_object->updateRepr(); + + Inkscape::GC::release(text_repr); + Inkscape::GC::release(text_node); + + return text_object; +} + +SPItem *create_text_with_rectangle (SPDesktop *desktop, Geom::Point p0, Geom::Point p1) +{ + SPDocument *doc = desktop->getDocument(); + auto const parent = desktop->layerManager().currentLayer(); + assert(parent); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *text_repr = xml_doc->createElement("svg:text"); + text_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create + text_repr->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(parent->i2doc_affine().inverse())); + + auto text_object = cast<SPText>(parent->appendChildRepr(text_repr)); + g_assert(text_object != nullptr); + + // Invert coordinate system? + p0 *= desktop->dt2doc(); + p1 *= desktop->dt2doc(); + + // Create rectangle + Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect"); + rect_repr->setAttributeSvgDouble("x", p0[Geom::X]); + rect_repr->setAttributeSvgDouble("y", p0[Geom::Y]); + rect_repr->setAttributeSvgDouble("width", abs(p1[Geom::X]-p0[Geom::X])); + rect_repr->setAttributeSvgDouble("height", abs(p1[Geom::Y]-p0[Geom::Y])); + + // Find defs, if does not exist, create. + Inkscape::XML::Node *defs_repr = sp_repr_lookup_name (xml_doc->root(), "svg:defs"); + if (defs_repr == nullptr) { + defs_repr = xml_doc->createElement("svg:defs"); + xml_doc->root()->addChild(defs_repr, nullptr); + } + else Inkscape::GC::anchor(defs_repr); + + // Add rectangle to defs. + defs_repr->addChild(rect_repr, nullptr); + + // Apply desktop style (do before adding "shape-inside"). + sp_desktop_apply_style_tool(desktop, text_repr, "/tools/text", true); + SPCSSAttr *css = sp_repr_css_attr(text_repr, "style" ); + sp_repr_css_set_property (css, "white-space", "pre"); // Respect new lines. + + // Link rectangle to text + std::string value("url(#"); + value += rect_repr->attribute("id"); + value += ")"; + sp_repr_css_set_property (css, "shape-inside", value.c_str()); + sp_repr_css_set(text_repr, css, "style"); + + sp_repr_css_attr_unref(css); + + /* Create <tspan> */ + Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan"); + rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan? + Inkscape::XML::Node *text_node = xml_doc->createTextNode(""); + rtspan->appendChild(text_node); + text_repr->appendChild(rtspan); + + Inkscape::GC::release(rtspan); + Inkscape::GC::release(text_repr); + Inkscape::GC::release(text_node); + Inkscape::GC::release(defs_repr); + Inkscape::GC::release(rect_repr); + + return text_object; +} + +/* + * TextTagAttributes implementation + */ + +// Not used. +// void TextTagAttributes::readFrom(Inkscape::XML::Node const *node) +// { +// readSingleAttribute(SPAttr::X, node->attribute("x")); +// readSingleAttribute(SPAttr::Y, node->attribute("y")); +// readSingleAttribute(SPAttr::DX, node->attribute("dx")); +// readSingleAttribute(SPAttr::DY, node->attribute("dy")); +// readSingleAttribute(SPAttr::ROTATE, node->attribute("rotate")); +// readSingleAttribute(SPAttr::TEXTLENGTH, node->attribute("textLength")); +// readSingleAttribute(SPAttr::LENGTHADJUST, node->attribute("lengthAdjust")); +// } + +bool TextTagAttributes::readSingleAttribute(SPAttr key, gchar const *value, SPStyle const *style, Geom::Rect const *viewport) +{ + // std::cout << "TextTagAttributes::readSingleAttribute: key: " << key + // << " value: " << (value?value:"Null") << std::endl; + std::vector<SVGLength> *attr_vector; + bool update_x = false; + bool update_y = false; + switch (key) { + case SPAttr::X: attr_vector = &attributes.x; update_x = true; break; + case SPAttr::Y: attr_vector = &attributes.y; update_y = true; break; + case SPAttr::DX: attr_vector = &attributes.dx; update_x = true; break; + case SPAttr::DY: attr_vector = &attributes.dy; update_y = true; break; + case SPAttr::ROTATE: attr_vector = &attributes.rotate; break; + case SPAttr::TEXTLENGTH: + attributes.textLength.readOrUnset(value); + return true; + break; + case SPAttr::LENGTHADJUST: + attributes.lengthAdjust = (value && !strcmp(value, "spacingAndGlyphs")? + Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS : + Inkscape::Text::Layout::LENGTHADJUST_SPACING); // default is "spacing" + return true; + break; + default: return false; + } + + // FIXME: sp_svg_length_list_read() amalgamates repeated separators. This prevents unset values. + *attr_vector = sp_svg_length_list_read(value); + + if( (update_x || update_y) && style != nullptr && viewport != nullptr ) { + double const w = viewport->width(); + double const h = viewport->height(); + double const em = style->font_size.computed; + double const ex = em * 0.5; + for(auto & it : *attr_vector) { + if( update_x ) + it.update( em, ex, w ); + if( update_y ) + it.update( em, ex, h ); + } + } + return true; +} + +void TextTagAttributes::writeTo(Inkscape::XML::Node *node) const +{ + writeSingleAttributeVector(node, "x", attributes.x); + writeSingleAttributeVector(node, "y", attributes.y); + writeSingleAttributeVector(node, "dx", attributes.dx); + writeSingleAttributeVector(node, "dy", attributes.dy); + writeSingleAttributeVector(node, "rotate", attributes.rotate); + + writeSingleAttributeLength(node, "textLength", attributes.textLength); + + if (attributes.textLength._set) { + if (attributes.lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACING) { + node->setAttribute("lengthAdjust", "spacing"); + } else if (attributes.lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS) { + node->setAttribute("lengthAdjust", "spacingAndGlyphs"); + } + } +} + +void TextTagAttributes::update( double em, double ex, double w, double h ) +{ + for(auto & it : attributes.x) { + it.update( em, ex, w ); + } + for(auto & it : attributes.y) { + it.update( em, ex, h ); + } + for(auto & it : attributes.dx) { + it.update( em, ex, w ); + } + for(auto & it : attributes.dy) { + it.update( em, ex, h ); + } +} + +void TextTagAttributes::writeSingleAttributeLength(Inkscape::XML::Node *node, gchar const *key, const SVGLength &length) +{ + if (length._set) { + node->setAttribute(key, length.write()); + } else + node->removeAttribute(key); +} + +void TextTagAttributes::writeSingleAttributeVector(Inkscape::XML::Node *node, gchar const *key, std::vector<SVGLength> const &attr_vector) +{ + if (attr_vector.empty()) + node->removeAttribute(key); + else { + Glib::ustring string; + + // FIXME: this has no concept of unset values because sp_svg_length_list_read() can't read them back in + for (auto it : attr_vector) { + if (!string.empty()) string += ' '; + string += it.write(); + } + node->setAttributeOrRemoveIfEmpty(key, string); + } +} + +bool TextTagAttributes::singleXYCoordinates() const +{ + return attributes.x.size() <= 1 && attributes.y.size() <= 1; +} + +bool TextTagAttributes::anyAttributesSet() const +{ + return !attributes.x.empty() || !attributes.y.empty() || !attributes.dx.empty() || !attributes.dy.empty() || !attributes.rotate.empty(); +} + +Geom::Point TextTagAttributes::firstXY() const +{ + Geom::Point point; + if (attributes.x.empty()) point[Geom::X] = 0.0; + else point[Geom::X] = attributes.x[0].computed; + if (attributes.y.empty()) point[Geom::Y] = 0.0; + else point[Geom::Y] = attributes.y[0].computed; + return point; +} + +void TextTagAttributes::setFirstXY(Geom::Point &point) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (attributes.x.empty()) + attributes.x.resize(1, zero_length); + if (attributes.y.empty()) + attributes.y.resize(1, zero_length); + attributes.x[0] = point[Geom::X]; + attributes.y[0] = point[Geom::Y]; +} + +SVGLength* TextTagAttributes::getFirstXLength() +{ + if (!attributes.x.empty()) { + return &attributes.x[0]; + } else { + return nullptr; + } +} + +SVGLength* TextTagAttributes::getFirstYLength() +{ + if (!attributes.y.empty()) { + return &attributes.y[0]; + } else { + return nullptr; + } +} + +// Instance of TextTagAttributes contains attributes as defined by text/tspan element. +// output: What will be sent to the rendering engine. +// parent_attrs: Attributes collected from all ancestors. +// parent_attrs_offset: Where this element fits into the parent_attrs. +// copy_xy: Should this elements x, y attributes contribute to output (can preserve set values but not use them... kind of strange). +// copy_dxdxrotate: Should this elements dx, dy, rotate attributes contribute to output. +void TextTagAttributes::mergeInto(Inkscape::Text::Layout::OptionalTextTagAttrs *output, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_attrs, unsigned parent_attrs_offset, bool copy_xy, bool copy_dxdyrotate) const +{ + mergeSingleAttribute(&output->x, parent_attrs.x, parent_attrs_offset, copy_xy ? &attributes.x : nullptr); + mergeSingleAttribute(&output->y, parent_attrs.y, parent_attrs_offset, copy_xy ? &attributes.y : nullptr); + mergeSingleAttribute(&output->dx, parent_attrs.dx, parent_attrs_offset, copy_dxdyrotate ? &attributes.dx : nullptr); + mergeSingleAttribute(&output->dy, parent_attrs.dy, parent_attrs_offset, copy_dxdyrotate ? &attributes.dy : nullptr); + mergeSingleAttribute(&output->rotate, parent_attrs.rotate, parent_attrs_offset, copy_dxdyrotate ? &attributes.rotate : nullptr); + if (attributes.textLength._set) { // only from current node, this is not inherited from parent + output->textLength.value = attributes.textLength.value; + output->textLength.computed = attributes.textLength.computed; + output->textLength.unit = attributes.textLength.unit; + output->textLength._set = attributes.textLength._set; + output->lengthAdjust = attributes.lengthAdjust; + } +} + +void TextTagAttributes::mergeSingleAttribute(std::vector<SVGLength> *output_list, std::vector<SVGLength> const &parent_list, unsigned parent_offset, std::vector<SVGLength> const *overlay_list) +{ + output_list->clear(); + if (overlay_list == nullptr) { + if (parent_list.size() > parent_offset) + { + output_list->reserve(parent_list.size() - parent_offset); + std::copy(parent_list.begin() + parent_offset, parent_list.end(), std::back_inserter(*output_list)); + } + } else { + output_list->reserve(std::max((int)parent_list.size() - (int)parent_offset, (int)overlay_list->size())); + unsigned overlay_offset = 0; + while (parent_offset < parent_list.size() || overlay_offset < overlay_list->size()) { + SVGLength const *this_item; + if (overlay_offset < overlay_list->size()) { + this_item = &(*overlay_list)[overlay_offset]; + overlay_offset++; + parent_offset++; + } else { + this_item = &parent_list[parent_offset]; + parent_offset++; + } + output_list->push_back(*this_item); + } + } +} + +void TextTagAttributes::erase(unsigned start_index, unsigned n) +{ + if (n == 0) return; + if (!singleXYCoordinates()) { + eraseSingleAttribute(&attributes.x, start_index, n); + eraseSingleAttribute(&attributes.y, start_index, n); + } + eraseSingleAttribute(&attributes.dx, start_index, n); + eraseSingleAttribute(&attributes.dy, start_index, n); + eraseSingleAttribute(&attributes.rotate, start_index, n); +} + +void TextTagAttributes::eraseSingleAttribute(std::vector<SVGLength> *attr_vector, unsigned start_index, unsigned n) +{ + if (attr_vector->size() <= start_index) return; + if (attr_vector->size() <= start_index + n) + attr_vector->erase(attr_vector->begin() + start_index, attr_vector->end()); + else + attr_vector->erase(attr_vector->begin() + start_index, attr_vector->begin() + start_index + n); +} + +void TextTagAttributes::insert(unsigned start_index, unsigned n) +{ + if (n == 0) return; + if (!singleXYCoordinates()) { + insertSingleAttribute(&attributes.x, start_index, n, true); + insertSingleAttribute(&attributes.y, start_index, n, true); + } + insertSingleAttribute(&attributes.dx, start_index, n, false); + insertSingleAttribute(&attributes.dy, start_index, n, false); + insertSingleAttribute(&attributes.rotate, start_index, n, false); +} + +void TextTagAttributes::insertSingleAttribute(std::vector<SVGLength> *attr_vector, unsigned start_index, unsigned n, bool is_xy) +{ + if (attr_vector->size() <= start_index) return; + SVGLength zero_length; + zero_length = 0.0; + attr_vector->insert(attr_vector->begin() + start_index, n, zero_length); + if (is_xy) { + double begin = start_index == 0 ? (*attr_vector)[start_index + n].computed : (*attr_vector)[start_index - 1].computed; + double diff = ((*attr_vector)[start_index + n].computed - begin) / n; // n tested for nonzero in insert() + for (unsigned i = 0 ; i < n ; i++) + (*attr_vector)[start_index + i] = begin + diff * i; + } +} + +void TextTagAttributes::split(unsigned index, TextTagAttributes *second) +{ + if (!singleXYCoordinates()) { + splitSingleAttribute(&attributes.x, index, &second->attributes.x, false); + splitSingleAttribute(&attributes.y, index, &second->attributes.y, false); + } + splitSingleAttribute(&attributes.dx, index, &second->attributes.dx, true); + splitSingleAttribute(&attributes.dy, index, &second->attributes.dy, true); + splitSingleAttribute(&attributes.rotate, index, &second->attributes.rotate, true); +} + +void TextTagAttributes::splitSingleAttribute(std::vector<SVGLength> *first_vector, unsigned index, std::vector<SVGLength> *second_vector, bool trimZeros) +{ + second_vector->clear(); + if (first_vector->size() <= index) return; + second_vector->resize(first_vector->size() - index); + std::copy(first_vector->begin() + index, first_vector->end(), second_vector->begin()); + first_vector->resize(index); + if (trimZeros) + while (!first_vector->empty() && (!first_vector->back()._set || first_vector->back().value == 0.0)) + first_vector->resize(first_vector->size() - 1); +} + +void TextTagAttributes::join(TextTagAttributes const &first, TextTagAttributes const &second, unsigned second_index) +{ + if (second.singleXYCoordinates()) { + attributes.x = first.attributes.x; + attributes.y = first.attributes.y; + } else { + joinSingleAttribute(&attributes.x, first.attributes.x, second.attributes.x, second_index); + joinSingleAttribute(&attributes.y, first.attributes.y, second.attributes.y, second_index); + } + joinSingleAttribute(&attributes.dx, first.attributes.dx, second.attributes.dx, second_index); + joinSingleAttribute(&attributes.dy, first.attributes.dy, second.attributes.dy, second_index); + joinSingleAttribute(&attributes.rotate, first.attributes.rotate, second.attributes.rotate, second_index); +} + +void TextTagAttributes::joinSingleAttribute(std::vector<SVGLength> *dest_vector, std::vector<SVGLength> const &first_vector, std::vector<SVGLength> const &second_vector, unsigned second_index) +{ + if (second_vector.empty()) + *dest_vector = first_vector; + else { + dest_vector->resize(second_index + second_vector.size()); + if (first_vector.size() < second_index) { + std::copy(first_vector.begin(), first_vector.end(), dest_vector->begin()); + SVGLength zero_length; + zero_length = 0.0; + std::fill(dest_vector->begin() + first_vector.size(), dest_vector->begin() + second_index, zero_length); + } else + std::copy(first_vector.begin(), first_vector.begin() + second_index, dest_vector->begin()); + std::copy(second_vector.begin(), second_vector.end(), dest_vector->begin() + second_index); + } +} + +void TextTagAttributes::transform(Geom::Affine const &matrix, double scale_x, double scale_y, bool extend_zero_length) +{ + SVGLength zero_length; + zero_length = 0.0; + + /* edge testcases for this code: + 1) moving text elements whose position is done entirely with transform="...", no x,y attributes + 2) unflowing multi-line flowtext then moving it (it has x but not y) + */ + unsigned points_count = std::max(attributes.x.size(), attributes.y.size()); + if (extend_zero_length && points_count < 1) + points_count = 1; + for (unsigned i = 0 ; i < points_count ; i++) { + Geom::Point point; + if (i < attributes.x.size()) point[Geom::X] = attributes.x[i].computed; + else point[Geom::X] = 0.0; + if (i < attributes.y.size()) point[Geom::Y] = attributes.y[i].computed; + else point[Geom::Y] = 0.0; + point *= matrix; + if (i < attributes.x.size()) + attributes.x[i] = point[Geom::X]; + else if (point[Geom::X] != 0.0 && extend_zero_length) { + attributes.x.resize(i + 1, zero_length); + attributes.x[i] = point[Geom::X]; + } + if (i < attributes.y.size()) + attributes.y[i] = point[Geom::Y]; + else if (point[Geom::Y] != 0.0 && extend_zero_length) { + attributes.y.resize(i + 1, zero_length); + attributes.y[i] = point[Geom::Y]; + } + } + for (auto & it : attributes.dx) + it = it.computed * scale_x; + for (auto & it : attributes.dy) + it = it.computed * scale_y; +} + +double TextTagAttributes::getDx(unsigned index) +{ + if( attributes.dx.empty()) { + return 0.0; + } + if( index < attributes.dx.size() ) { + return attributes.dx[index].computed; + } else { + return 0.0; // attributes.dx.back().computed; + } +} + + +double TextTagAttributes::getDy(unsigned index) +{ + if( attributes.dy.empty() ) { + return 0.0; + } + if( index < attributes.dy.size() ) { + return attributes.dy[index].computed; + } else { + return 0.0; // attributes.dy.back().computed; + } +} + + +void TextTagAttributes::addToDx(unsigned index, double delta) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (attributes.dx.size() < index + 1) attributes.dx.resize(index + 1, zero_length); + attributes.dx[index] = attributes.dx[index].computed + delta; +} + +void TextTagAttributes::addToDy(unsigned index, double delta) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (attributes.dy.size() < index + 1) attributes.dy.resize(index + 1, zero_length); + attributes.dy[index] = attributes.dy[index].computed + delta; +} + +void TextTagAttributes::addToDxDy(unsigned index, Geom::Point const &adjust) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (adjust[Geom::X] != 0.0) { + if (attributes.dx.size() < index + 1) attributes.dx.resize(index + 1, zero_length); + attributes.dx[index] = attributes.dx[index].computed + adjust[Geom::X]; + } + if (adjust[Geom::Y] != 0.0) { + if (attributes.dy.size() < index + 1) attributes.dy.resize(index + 1, zero_length); + attributes.dy[index] = attributes.dy[index].computed + adjust[Geom::Y]; + } +} + +double TextTagAttributes::getRotate(unsigned index) +{ + if( attributes.rotate.empty() ) { + return 0.0; + } + if( index < attributes.rotate.size() ) { + return attributes.rotate[index].computed; + } else { + return attributes.rotate.back().computed; + } +} + + +void TextTagAttributes::addToRotate(unsigned index, double delta) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (attributes.rotate.size() < index + 2) { + if (attributes.rotate.empty()) + attributes.rotate.resize(index + 2, zero_length); + else + attributes.rotate.resize(index + 2, attributes.rotate.back()); + } + attributes.rotate[index] = mod360(attributes.rotate[index].computed + delta); +} + + +void TextTagAttributes::setRotate(unsigned index, double angle) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (attributes.rotate.size() < index + 2) { + if (attributes.rotate.empty()) + attributes.rotate.resize(index + 2, zero_length); + else + attributes.rotate.resize(index + 2, attributes.rotate.back()); + } + attributes.rotate[index] = mod360(angle); +} + + +/* + 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 : diff --git a/src/object/sp-text.h b/src/object/sp-text.h new file mode 100644 index 0000000..556743d --- /dev/null +++ b/src/object/sp-text.h @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_TEXT_H +#define SEEN_SP_TEXT_H + +/* + * SVG <text> and <tspan> implementation + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "desktop.h" +#include "sp-item.h" +#include "sp-string.h" // Provides many other headers with is<SPString> +#include "text-tag-attributes.h" +#include "display/curve.h" + +#include "libnrtype/Layout-TNG.h" +#include "libnrtype/style-attachments.h" + +#include <memory> + +/* Text specific flags */ +#define SP_TEXT_CONTENT_MODIFIED_FLAG SP_OBJECT_USER_MODIFIED_FLAG_A +#define SP_TEXT_LAYOUT_MODIFIED_FLAG SP_OBJECT_USER_MODIFIED_FLAG_A + +class SPShape; + +/* SPText */ +class SPText final : public SPItem { +public: + SPText(); + ~SPText() override; + int tag() const override { return tag_of<decltype(*this)>; } + + /** Converts the text object to its component curves */ + SPCurve getNormalizedBpath() const; + + /** Completely recalculates the layout. */ + void rebuildLayout(); + + //semiprivate: (need to be accessed by the C-style functions still) + TextTagAttributes attributes; + Inkscape::Text::Layout layout; + std::unordered_map<unsigned, Inkscape::Text::StyleAttachments> view_style_attachments; + + /** when the object is transformed it's nicer to change the font size + and coordinates when we can, rather than just applying a matrix + transform. is_root is used to indicate to the function that it should + extend zero-length position vectors to length 1 in order to record the + new position. This is necessary to convert from objects whose position is + completely specified by transformations. */ + static void _adjustCoordsRecursive(SPItem *item, Geom::Affine const &m, double ex, bool is_root = true); + static void _adjustFontsizeRecursive(SPItem *item, double ex, bool is_root = true); + /** + This two functions are useful because layout calculations need text visible for example + Calculating a invisible char position object or pasting text with paragraphs that overflow + shape defined. I have doubts about transform into a toggle function*/ + void show_shape_inside(); + void hide_shape_inside(); + + /** discards the drawing objects representing this text. */ + void _clearFlow(Inkscape::DrawingGroup *in_arena); + + bool _optimizeTextpathText = false; + + /** Union all exclusion shapes. */ + std::unique_ptr<Shape> getExclusionShape() const; + /** Add a single inclusion shape with padding */ + Shape* getInclusionShape(SPShape *shape) const; + /** Compute the final effective shapes: + * All inclusion shapes shrunk by the padding, + * from which we subtract the exclusion shapes expanded by their padding. + * + * @return A vector of pointers to a newly allocated Shape objects which must be eventually freed manually. + */ + std::vector<Shape *> makeEffectiveShapes() const; + + std::optional<Geom::Point> getBaselinePoint() const; + +private: + + /** Initializes layout from <text> (i.e. this node). */ + void _buildLayoutInit(); + + /** Recursively walks the xml tree adding tags and their contents. The + non-trivial code does two things: firstly, it manages the positioning + attributes and their inheritance rules, and secondly it keeps track of line + breaks and makes sure both that they are assigned the correct SPObject and + that we don't get a spurious extra one at the end of the flow. */ + unsigned _buildLayoutInput(SPObject *object, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_optional_attrs, unsigned parent_attrs_offset, bool in_textpath); + + /** Find first x/y values which may be in a descendent element. */ + SVGLength* _getFirstXLength(); + SVGLength* _getFirstYLength(); + SPCSSAttr *css; + + public: + /** Optimize textpath text on next set_transform. */ + void optimizeTextpathText() {_optimizeTextpathText = true;} + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + void set(SPAttr key, const char* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + void print(SPPrintContext *ctx) override; + const char* typeName() const override; + const char* displayName() const override; + char* description() const override; + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void hide(unsigned int key) override; + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + Geom::Affine set_transform(Geom::Affine const &transform) override; + void getLinked(std::vector<SPObject *> &objects, bool ignore_clones) const override; + + // For 'inline-size', need to also remove any 'x' and 'y' added by SVG 1.1 fallback. + void remove_svg11_fallback(); + + void newline_to_sodipodi(); // 'inline-size' to Inkscape multi-line text. + void sodipodi_to_newline(); // Inkscape mult-line text to SVG 2 text. + + bool is_horizontal() const; + bool has_inline_size() const; + bool has_shape_inside() const; + Geom::OptRect get_frame(); // Gets inline-size or shape-inside frame. + Inkscape::XML::Node* get_first_rectangle(); // Gets first shape-inside rectangle (if it exists). + SPItem *get_first_shape_dependency(); + const std::vector<SPItem *> get_all_shape_dependencies() const; + void remove_newlines(); // Removes newlines in text. +}; + +SPItem *create_text_with_inline_size (SPDesktop *desktop, Geom::Point p0, Geom::Point p1); +SPItem *create_text_with_rectangle (SPDesktop *desktop, Geom::Point p0, Geom::Point p1); + +#endif + +/* + 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 : diff --git a/src/object/sp-textpath.h b/src/object/sp-textpath.h new file mode 100644 index 0000000..3911669 --- /dev/null +++ b/src/object/sp-textpath.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SP_TEXTPATH_H +#define INKSCAPE_SP_TEXTPATH_H + +#include "svg/svg-length.h" +#include "sp-item.h" +#include "sp-text.h" + +class SPUsePath; +class Path; + +enum TextPathSide { + SP_TEXT_PATH_SIDE_LEFT, + SP_TEXT_PATH_SIDE_RIGHT +}; + +class SPTextPath final : public SPItem { +public: + SPTextPath(); + ~SPTextPath() override; + int tag() const override { return tag_of<decltype(*this)>; } + + TextTagAttributes attributes; + SVGLength startOffset; + TextPathSide side; + + Path *originalPath; + bool isUpdating; + SPUsePath *sourcePath; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttr key, const char* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +inline bool SP_IS_TEXT_TEXTPATH(SPObject const *obj) { return is<SPText>(obj) && obj->firstChild() && is<SPTextPath>(obj->firstChild()); } + +SPItem *sp_textpath_get_path_item(SPTextPath const *tp); +void sp_textpath_to_text(SPObject *tp); + +#endif /* !INKSCAPE_SP_TEXTPATH_H */ + +/* + 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 : diff --git a/src/object/sp-title.cpp b/src/object/sp-title.cpp new file mode 100644 index 0000000..fe295e4 --- /dev/null +++ b/src/object/sp-title.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <title> implementation + * + * Authors: + * Jeff Schiller <codedread@gmail.com> + * + * Copyright (C) 2008 Jeff Schiller + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-title.h" +#include "xml/repr.h" + +SPTitle::SPTitle() : SPObject() { +} + +SPTitle::~SPTitle() = default; + +Inkscape::XML::Node* SPTitle::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + SPTitle* object = this; + + if (!repr) { + repr = object->getRepr()->duplicate(xml_doc); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + diff --git a/src/object/sp-title.h b/src/object/sp-title.h new file mode 100644 index 0000000..18d52f1 --- /dev/null +++ b/src/object/sp-title.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_TITLE_H +#define SEEN_SP_TITLE_H + +/* + * SVG <title> implementation + * + * Authors: + * Jeff Schiller <codedread@gmail.com> + * + * Copyright (C) 2008 Jeff Schiller + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +class SPTitle final : public SPObject { +public: + SPTitle(); + ~SPTitle() override; + int tag() const override { return tag_of<decltype(*this)>; } + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif diff --git a/src/object/sp-tref-reference.cpp b/src/object/sp-tref-reference.cpp new file mode 100644 index 0000000..46a6865 --- /dev/null +++ b/src/object/sp-tref-reference.cpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to href of <tref> element. + * + * Copyright (C) 2007 Gail Banaszkiewicz + * + * This file was created based on sp-use-reference.cpp + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +//#include "enums.h" +#include "sp-tref-reference.h" + +#include "sp-text.h" +#include "sp-tref.h" + + +bool SPTRefReference::_acceptObject(SPObject * const obj) const +{ + SPObject *owner = getOwner(); + if (is<SPTRef>(owner)) + return URIReference::_acceptObject(obj); + else + return false; +} + + +void SPTRefReference::updateObserver() +{ + SPObject *referred = getObject(); + + if (referred) { + if (subtreeObserved) { + subtreeObserved->removeObserver(*this); + } + + subtreeObserved = std::make_unique<Inkscape::XML::Subtree>(*referred->getRepr()); + subtreeObserved->addObserver(*this); + } +} + + +void SPTRefReference::notifyChildAdded(Inkscape::XML::Node &/*node*/, Inkscape::XML::Node &/*child*/, + Inkscape::XML::Node */*prev*/) +{ + SPObject *owner = getOwner(); + + if (owner && is<SPTRef>(owner)) { + sp_tref_update_text(cast<SPTRef>(owner)); + } +} + + +void SPTRefReference::notifyChildRemoved(Inkscape::XML::Node &/*node*/, Inkscape::XML::Node &/*child*/, + Inkscape::XML::Node */*prev*/) +{ + SPObject *owner = getOwner(); + + if (owner && is<SPTRef>(owner)) { + sp_tref_update_text(cast<SPTRef>(owner)); + } +} + + +void SPTRefReference::notifyChildOrderChanged(Inkscape::XML::Node &/*node*/, Inkscape::XML::Node &/*child*/, + Inkscape::XML::Node */*old_prev*/, Inkscape::XML::Node */*new_prev*/) +{ + SPObject *owner = getOwner(); + + if (owner && is<SPTRef>(owner)) { + sp_tref_update_text(cast<SPTRef>(owner)); + } +} + + +void SPTRefReference::notifyContentChanged(Inkscape::XML::Node &/*node*/, + Inkscape::Util::ptr_shared /*old_content*/, + Inkscape::Util::ptr_shared /*new_content*/) +{ + SPObject *owner = getOwner(); + + if (owner && is<SPTRef>(owner)) { + sp_tref_update_text(cast<SPTRef>(owner)); + } +} + + +void SPTRefReference::notifyAttributeChanged(Inkscape::XML::Node &/*node*/, GQuark /*name*/, + Inkscape::Util::ptr_shared /*old_value*/, + Inkscape::Util::ptr_shared /*new_value*/) +{ + // Do nothing - tref only cares about textual content +} + + +/* + 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 : diff --git a/src/object/sp-tref-reference.h b/src/object/sp-tref-reference.h new file mode 100644 index 0000000..d09f453 --- /dev/null +++ b/src/object/sp-tref-reference.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_TREF_REFERENCE_H +#define SEEN_SP_TREF_REFERENCE_H + +/* + * The reference corresponding to href of <tref> element. + * + * This file was created based on sp-use-reference.h + * + * Copyright (C) 2007 Gail Banaszkiewicz + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <memory> +#include <sigc++/sigc++.h> + +#include "sp-item.h" +#include "uri-references.h" + +#include "util/share.h" +#include "xml/node-observer.h" +#include "xml/subtree.h" + +typedef unsigned int GQuark; + +class SPTRefReference : public Inkscape::URIReference, + public Inkscape::XML::NodeObserver { +public: + SPTRefReference(SPObject *owner) : URIReference(owner) { + updateObserver(); + } + + ~SPTRefReference() override { + if (subtreeObserved) { + subtreeObserved->removeObserver(*this); + } + } + + SPItem *getObject() const { + return static_cast<SPItem *>(URIReference::getObject()); + } + + void updateObserver(); + + // Node Observer Functions + void notifyChildAdded(Inkscape::XML::Node &node, Inkscape::XML::Node &child, Inkscape::XML::Node *prev) override; + void notifyChildRemoved(Inkscape::XML::Node &node, Inkscape::XML::Node &child, Inkscape::XML::Node *prev) override; + void notifyChildOrderChanged(Inkscape::XML::Node &node, Inkscape::XML::Node &child, + Inkscape::XML::Node *old_prev, Inkscape::XML::Node *new_prev) override; + void notifyContentChanged(Inkscape::XML::Node &node, + Inkscape::Util::ptr_shared old_content, + Inkscape::Util::ptr_shared new_content) override; + void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark name, + Inkscape::Util::ptr_shared old_value, + Inkscape::Util::ptr_shared new_value) override; + +protected: + bool _acceptObject(SPObject * obj) const override; + + std::unique_ptr<Inkscape::XML::Subtree> subtreeObserved; +}; + +#endif // !SEEN_SP_TREF_REFERENCE_H + +/* + 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 : diff --git a/src/object/sp-tref.cpp b/src/object/sp-tref.cpp new file mode 100644 index 0000000..68f4ac0 --- /dev/null +++ b/src/object/sp-tref.cpp @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <tref> implementation - All character data within the referenced + * element, including character data enclosed within additional markup, + * will be rendered. + * + * This file was created based on skeleton.cpp + */ +/* + * Authors: + * Gail Banaszkiewicz <Gail.Banaszkiewicz@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2007 Gail Banaszkiewicz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-tref.h" + +#include <glibmm/i18n.h> + +#include "bad-uri-exception.h" +#include "attributes.h" +#include "document.h" +#include "sp-factory.h" +#include "sp-text.h" +#include "style.h" +#include "text-editing.h" +#include "xml/href-attribute-helper.h" + +//#define DEBUG_TREF +#ifdef DEBUG_TREF +# define debug(f, a...) { g_message("%s(%d) %s:", \ + __FILE__,__LINE__,__FUNCTION__); \ + g_message(f, ## a); \ + g_message("\n"); \ + } +#else +# define debug(f, a...) /**/ +#endif + + +static void build_string_from_root(Inkscape::XML::Node *root, Glib::ustring *retString); + +/* TRef base class */ +static void sp_tref_href_changed(SPObject *old_ref, SPObject *ref, SPTRef *tref); +static void sp_tref_delete_self(SPObject *deleted, SPTRef *self); + +SPTRef::SPTRef() + : SPItem() + , href(nullptr) + , uriOriginalRef(this) + , stringChild(nullptr) +{ + _changed_connection = uriOriginalRef.changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_tref_href_changed), this)); +} + +SPTRef::~SPTRef() +{ +} + +void SPTRef::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPItem::build(document, repr); + + this->readAttr(SPAttr::XLINK_HREF); + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + this->readAttr(SPAttr::DX); + this->readAttr(SPAttr::DY); + this->readAttr(SPAttr::ROTATE); +} + +void SPTRef::release() { + //this->attributes.~TextTagAttributes(); + + this->_delete_connection.disconnect(); + this->_changed_connection.disconnect(); + + g_free(href); + href = nullptr; + + uriOriginalRef.detach(); + + SPItem::release(); +} + +void SPTRef::set(SPAttr key, const gchar* value) { + debug("0x%p %s(%u): '%s'",this, + sp_attribute_name(key),key,value ? value : "<no value>"); + + if (this->attributes.readSingleAttribute(key, value, style, &viewport)) { // x, y, dx, dy, rotate + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else if (key == SPAttr::XLINK_HREF) { // xlink:href + if ( !value ) { + // No value + g_free(href); + href = nullptr; + uriOriginalRef.detach(); + } else if ((this->href && strcmp(value, this->href) != 0) || (!this->href)) { + // Value has changed + + if ( this->href ) { + g_free(this->href); + this->href = nullptr; + } + + href = g_strdup(value); + + try { + uriOriginalRef.attach(Inkscape::URI(value)); + uriOriginalRef.updateObserver(); + } catch (Inkscape::BadURIException const &e) { + g_warning("%s", e.what()); + uriOriginalRef.detach(); + } + + // No matter what happened, an update should be in order + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } else { // default + SPItem::set(key, value); + } +} + +void SPTRef::update(SPCtx *ctx, guint flags) { + debug("0x%p",this); + + unsigned childflags = flags; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + + SPObject *child = this->stringChild; + + if (child) { + if ( childflags || ( child->uflags & SP_OBJECT_MODIFIED_FLAG )) { + child->updateDisplay(ctx, childflags); + } + } + + SPItem::update(ctx, flags); +} + +void SPTRef::modified(unsigned int flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + SPObject *child = this->stringChild; + + if (child) { + sp_object_ref(child); + + if (flags || (child->mflags & SP_OBJECT_MODIFIED_FLAG)) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +Inkscape::XML::Node* SPTRef::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + debug("0x%p",this); + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:tref"); + } + + this->attributes.writeTo(repr); + + if (uriOriginalRef.getURI()) { + auto uri = uriOriginalRef.getURI()->str(); + auto uri_string = uri.c_str(); + debug("uri_string=%s", uri_string); + Inkscape::setHrefAttribute(*repr, uri_string); + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + +Geom::OptRect SPTRef::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const { + Geom::OptRect bbox; + // find out the ancestor text which holds our layout + SPObject const *parent_text = this; + + while ( parent_text && !is<SPText>(parent_text) ) { + parent_text = parent_text->parent; + } + + if (parent_text == nullptr) { + return bbox; + } + + // get the bbox of our portion of the layout + return cast<SPText>(parent_text)->layout.bounds(transform, + type == SPItem::VISUAL_BBOX, + sp_text_get_length_upto(parent_text, this), + sp_text_get_length_upto(this, nullptr) - 1); +} + +const char* SPTRef::typeName() const { + return "text-data"; +} + +const char* SPTRef::displayName() const { + return _("Cloned Character Data"); +} + +gchar* SPTRef::description() const { + SPObject const *referred = this->getObjectReferredTo(); + + if (referred) { + char *child_desc; + + if (is<SPItem>(referred)) { + child_desc = cast<SPItem>(referred)->detailedDescription(); + } else { + child_desc = g_strdup(""); + } + + char *ret = g_strdup_printf("%s%s", + (is<SPItem>(referred) ? _(" from ") : ""), child_desc); + g_free(child_desc); + + return ret; + } + + return g_strdup(_("[orphaned]")); +} + + +/* For the sigc::connection changes (i.e. when the object being referred to changes) */ +static void +sp_tref_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, SPTRef *tref) +{ + if (tref) + { + // Save a pointer to the original object being referred to + SPObject *refRoot = tref->getObjectReferredTo(); + + tref->_delete_connection.disconnect(); + + if (tref->stringChild) { + tref->detach(tref->stringChild); + tref->stringChild = nullptr; + } + + // Ensure that we are referring to a legitimate object + if (tref->href && refRoot && sp_tref_reference_allowed(tref, refRoot)) { + + // Update the text being referred to (will create a new string child) + sp_tref_update_text(tref); + + // Restore the delete connection now that we're done messing with stuff + tref->_delete_connection = refRoot->connectDelete(sigc::bind(sigc::ptr_fun(&sp_tref_delete_self), tref)); + } + + } +} + + +/** + * Delete the tref object + */ +static void +sp_tref_delete_self(SPObject */*deleted*/, SPTRef *self) +{ + self->deleteObject(); +} + +/** + * Return the object referred to via the URI reference + */ +SPObject * SPTRef::getObjectReferredTo() +{ + return uriOriginalRef.getObject(); +} + +/** + * Return the object referred to via the URI reference + */ +SPObject const *SPTRef::getObjectReferredTo() const +{ + return uriOriginalRef.getObject(); +} + +/** + * Returns true when the given tref is allowed to refer to a particular object + */ +bool +sp_tref_reference_allowed(SPTRef *tref, SPObject *possible_ref) +{ + bool allowed = false; + + if (tref && possible_ref) { + if (tref != possible_ref) { + bool ancestor = false; + for (SPObject *obj = tref; obj; obj = obj->parent) { + if (possible_ref == obj) { + ancestor = true; + break; + } + } + allowed = !ancestor; + } + } + + return allowed; +} + + +/** + * Returns true if a tref is fully contained in the confines of the given + * iterators and layout (or if there is no tref). + */ +bool +sp_tref_fully_contained(SPObject *start_item, Glib::ustring::iterator &start, + SPObject *end_item, Glib::ustring::iterator &end) +{ + bool fully_contained = false; + + if (start_item && end_item) { + + // If neither the beginning or the end is a tref then we return true (whether there + // is a tref in the innards or not, because if there is one then it must be totally + // contained) + if (!(is<SPString>(start_item) && is<SPTRef>(start_item->parent)) + && !(is<SPString>(end_item) && is<SPTRef>(end_item->parent))) { + fully_contained = true; + } + + // Both the beginning and end are trefs; but in this case, the string iterators + // must be at the right places + else if ((is<SPString>(start_item) && is<SPTRef>(start_item->parent)) + && (is<SPString>(end_item) && is<SPTRef>(end_item->parent))) { + if (start == cast<SPString>(start_item)->string.begin() + && end == cast<SPString>(start_item)->string.end()) { + fully_contained = true; + } + } + + // If the beginning is a string that is a child of a tref, the iterator has to be + // at the beginning of the item + else if ((is<SPString>(start_item) && is<SPTRef>(start_item->parent)) + && !(is<SPString>(end_item) && is<SPTRef>(end_item->parent))) { + if (start == cast<SPString>(start_item)->string.begin()) { + fully_contained = true; + } + } + + // Same, but the for the end + else if (!(is<SPString>(start_item) && is<SPTRef>(start_item->parent)) + && (is<SPString>(end_item) && is<SPTRef>(end_item->parent))) { + if (end == cast<SPString>(start_item)->string.end()) { + fully_contained = true; + } + } + } + + return fully_contained; +} + + +void sp_tref_update_text(SPTRef *tref) +{ + if (tref) { + // Get the character data that will be used with this tref + Glib::ustring charData = ""; + build_string_from_root(tref->getObjectReferredTo()->getRepr(), &charData); + + if (tref->stringChild) { + tref->detach(tref->stringChild); + tref->stringChild = nullptr; + } + + // Create the node and SPString to be the tref's child + Inkscape::XML::Document *xml_doc = tref->document->getReprDoc(); + + Inkscape::XML::Node *newStringRepr = xml_doc->createTextNode(charData.c_str()); + tref->stringChild = SPFactory::createObject(NodeTraits::get_type_string(*newStringRepr)); + + // Add this SPString as a child of the tref + tref->attach(tref->stringChild, tref->lastChild()); + sp_object_unref(tref->stringChild, nullptr); + (tref->stringChild)->invoke_build(tref->document, newStringRepr, FALSE); + + Inkscape::GC::release(newStringRepr); + } +} + + + +/** + * Using depth-first search, build up a string by concatenating all SPStrings + * found in the tree starting at the root + */ +static void +build_string_from_root(Inkscape::XML::Node *root, Glib::ustring *retString) +{ + if (root && retString) { + + // Stop and concatenate when a SPString is found + if (root->type() == Inkscape::XML::NodeType::TEXT_NODE) { + *retString += (root->content()); + + debug("%s", retString->c_str()); + + // Otherwise, continue searching down the tree (with the assumption that no children nodes + // of a SPString are actually legal) + } else { + Inkscape::XML::Node *childNode; + for (childNode = root->firstChild(); childNode; childNode = childNode->next()) { + build_string_from_root(childNode, retString); + } + } + } +} + +/** + * This function will create a new tspan element with the same attributes as + * the tref had and add the same text as a child. The tref is replaced in the + * tree with the new tspan. + * The code is based partially on sp_use_unlink + */ +SPObject * +sp_tref_convert_to_tspan(SPObject *obj) +{ + SPObject * new_tspan = nullptr; + + //////////////////// + // BASE CASE + //////////////////// + if (is<SPTRef>(obj)) { + + auto tref = cast<SPTRef>(obj); + + if (tref && tref->stringChild) { + Inkscape::XML::Node *tref_repr = tref->getRepr(); + Inkscape::XML::Node *tref_parent = tref_repr->parent(); + + SPDocument *document = tref->document; + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + Inkscape::XML::Node *new_tspan_repr = xml_doc->createElement("svg:tspan"); + + // Add the new tspan element just after the current tref + tref_parent->addChild(new_tspan_repr, tref_repr); + Inkscape::GC::release(new_tspan_repr); + + new_tspan = document->getObjectByRepr(new_tspan_repr); + + // Create a new string child for the tspan + Inkscape::XML::Node *new_string_repr = tref->stringChild->getRepr()->duplicate(xml_doc); + new_tspan_repr->addChild(new_string_repr, nullptr); + + //SPObject * new_string_child = document->getObjectByRepr(new_string_repr); + + // Merge style from the tref + new_tspan->style->merge( tref->style ); + new_tspan->style->cascade( new_tspan->parent->style ); + new_tspan->updateRepr(); + + // Hold onto our SPObject and repr for now. + sp_object_ref(tref); + Inkscape::GC::anchor(tref_repr); + + // Remove ourselves, not propagating delete events to avoid a + // chain-reaction with other elements that might reference us. + tref->deleteObject(false); + + // Give the copy our old id and let go of our old repr. + new_tspan_repr->setAttribute("id", tref_repr->attribute("id")); + Inkscape::GC::release(tref_repr); + + // Establish the succession and let go of our object. + tref->setSuccessor(new_tspan); + sp_object_unref(tref); + } + } + //////////////////// + // RECURSIVE CASE + //////////////////// + else { + std::vector<SPObject *> l; + for (auto& child: obj->children) { + sp_object_ref(&child, obj); + l.push_back(&child); + } + for(auto child:l) { + // Note that there may be more than one conversion happening here, so if it's not a + // tref being passed into this function, the returned value can't be specifically known + new_tspan = sp_tref_convert_to_tspan(child); + + sp_object_unref(child, obj); + } + } + + return new_tspan; +} + + +/* + 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 : diff --git a/src/object/sp-tref.h b/src/object/sp-tref.h new file mode 100644 index 0000000..3cc0d28 --- /dev/null +++ b/src/object/sp-tref.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_TREF_H +#define SP_TREF_H + +/** \file + * SVG <tref> implementation, see sp-tref.cpp. + * + * This file was created based on skeleton.h + */ +/* + * Authors: + * Gail Banaszkiewicz <Gail.Banaszkiewicz@gmail.com> + * + * Copyright (C) 2007 Gail Banaszkiewicz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-item.h" +#include "sp-tref-reference.h" +#include "text-tag-attributes.h" + +/* tref base class */ + +class SPTRef final : public SPItem { +public: + SPTRef(); + ~SPTRef() override; + int tag() const override { return tag_of<decltype(*this)>; } + + // Attributes that are used in the same way they would be in a tspan + TextTagAttributes attributes; + + // Text stored in the xlink:href attribute + char *href; + + // URI reference to original object + SPTRefReference uriOriginalRef; + + // Shortcut pointer to the child of the tref (which is a copy + // of the character data stored at and/or below the node + // referenced by uriOriginalRef) + SPObject *stringChild; + + // The sigc connections for various notifications + sigc::connection _delete_connection; + sigc::connection _changed_connection; + + SPObject * getObjectReferredTo(); + SPObject const *getObjectReferredTo() const; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttr key, char const* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + const char* typeName() const override; + const char* displayName() const override; + char* description() const override; +}; + +void sp_tref_update_text(SPTRef *tref); +bool sp_tref_reference_allowed(SPTRef *tref, SPObject *possible_ref); +bool sp_tref_fully_contained(SPObject *start_item, Glib::ustring::iterator &start, + SPObject *end_item, Glib::ustring::iterator &end); +SPObject * sp_tref_convert_to_tspan(SPObject *item); + +#endif /* !SP_TREF_H */ + +/* + 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 : diff --git a/src/object/sp-tspan.cpp b/src/object/sp-tspan.cpp new file mode 100644 index 0000000..f096cd9 --- /dev/null +++ b/src/object/sp-tspan.cpp @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <text> and <tspan> implementation + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * fixme: + * + * These subcomponents should not be items, or alternately + * we have to invent set of flags to mark, whether standard + * attributes are applicable to given item (I even like this + * idea somewhat - Lauris) + * + */ + +#include <cstring> +#include <string> +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include "attributes.h" +#include "document.h" +#include "text-editing.h" + +#include "sp-textpath.h" +#include "sp-tref.h" +#include "sp-tspan.h" +#include "sp-use-reference.h" +#include "style.h" + +#include "display/curve.h" + +#include "livarot/Path.h" + +#include "svg/stringstream.h" +#include "xml/href-attribute-helper.h" + + +/*##################################################### +# SPTSPAN +#####################################################*/ +SPTSpan::SPTSpan() : SPItem() { + this->role = SP_TSPAN_ROLE_UNSPECIFIED; +} + +SPTSpan::~SPTSpan() = default; + +void SPTSpan::build(SPDocument *doc, Inkscape::XML::Node *repr) { + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + this->readAttr(SPAttr::DX); + this->readAttr(SPAttr::DY); + this->readAttr(SPAttr::ROTATE); + + // Strip sodipodi:role from SVG 2 flowed text. + // this->role = SP_TSPAN_ROLE_UNSPECIFIED; + auto text = cast<SPText>(parent); + if (text && !(text->has_shape_inside()|| text->has_inline_size())) { + this->readAttr(SPAttr::SODIPODI_ROLE); + } + + // We'll intercept "style" to strip "visibility" property (SVG 1.1 fallback for SVG 2 text) then pass it on. + this->readAttr(SPAttr::STYLE); + + SPItem::build(doc, repr); +} + +void SPTSpan::release() { + SPItem::release(); +} + +void SPTSpan::set(SPAttr key, const gchar* value) { + if (this->attributes.readSingleAttribute(key, value, style, &viewport)) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else { + switch (key) { + case SPAttr::SODIPODI_ROLE: + if (value && (!strcmp(value, "line") || !strcmp(value, "paragraph"))) { + this->role = SP_TSPAN_ROLE_LINE; + } else { + this->role = SP_TSPAN_ROLE_UNSPECIFIED; + } + break; + + case SPAttr::STYLE: + if (value) { + Glib::ustring style(value); + Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("visibility\\s*:\\s*hidden;*"); + Glib::ustring stripped = regex->replace_literal(style, 0, "", static_cast<Glib::RegexMatchFlags >(0)); + Inkscape::XML::Node *repr = getRepr(); + repr->setAttributeOrRemoveIfEmpty("style", stripped); + } + // Fall through + default: + SPItem::set(key, value); + break; + } + } +} + +void SPTSpan::update(SPCtx *ctx, guint flags) { + unsigned childflags = flags; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + + for (auto& ochild: children) { + if ( flags || ( ochild.uflags & SP_OBJECT_MODIFIED_FLAG )) { + ochild.updateDisplay(ctx, childflags); + } + } + + SPItem::update(ctx, flags); + + if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_LAYOUT_MODIFIED_FLAG ) ) + { + SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx); + + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + attributes.update( em, ex, w, h ); + } +} + +void SPTSpan::modified(unsigned int flags) { +// SPItem::onModified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + for (auto& ochild: children) { + if (flags || (ochild.mflags & SP_OBJECT_MODIFIED_FLAG)) { + ochild.emitModified(flags); + } + } +} + +Geom::OptRect SPTSpan::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const { + Geom::OptRect bbox; + // find out the ancestor text which holds our layout + SPObject const *parent_text = this; + + while (parent_text && !is<SPText>(parent_text)) { + parent_text = parent_text->parent; + } + + if (parent_text == nullptr) { + return bbox; + } + + // get the bbox of our portion of the layout + return cast<SPText>(parent_text)->layout.bounds(transform, + type == SPItem::VISUAL_BBOX, + sp_text_get_length_upto(parent_text, this), + sp_text_get_length_upto(this, nullptr) - 1); +} + +Inkscape::XML::Node* SPTSpan::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:tspan"); + } + + this->attributes.writeTo(repr); + + if ( flags&SP_OBJECT_WRITE_BUILD ) { + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + Inkscape::XML::Node* c_repr=nullptr; + + if ( is<SPTSpan>(&child) || is<SPTRef>(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( is<SPTextPath>(&child) ) { + //c_repr = child.updateRepr(xml_doc, NULL, flags); // shouldn't happen + } else if ( is<SPString>(&child) ) { + c_repr = xml_doc->createTextNode(cast<SPString>(&child)->string.c_str()); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + + for (auto i = l.rbegin(); i!= l.rend(); ++i) { + repr->addChild((*i), nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( is<SPTSpan>(&child) || is<SPTRef>(&child) ) { + child.updateRepr(flags); + } else if ( is<SPTextPath>(&child) ) { + //c_repr = child->updateRepr(xml_doc, NULL, flags); // shouldn't happen + } else if ( is<SPString>(&child) ) { + child.getRepr()->setContent(cast<SPString>(&child)->string.c_str()); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPTSpan::typeName() const { + return "text-data"; +} + +const char* SPTSpan::displayName() const { + return _("Text Span"); +} + + +/*##################################################### +# SPTEXTPATH +#####################################################*/ +void refresh_textpath_source(SPTextPath* offset); + +SPTextPath::SPTextPath() : SPItem() { + this->startOffset._set = false; + this->side = SP_TEXT_PATH_SIDE_LEFT; + this->originalPath = nullptr; + this->isUpdating=false; + + // set up the uri reference + this->sourcePath = new SPUsePath(this); + this->sourcePath->user_unlink = sp_textpath_to_text; +} + +SPTextPath::~SPTextPath() { + delete this->sourcePath; +} + +void SPTextPath::build(SPDocument *doc, Inkscape::XML::Node *repr) { + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + this->readAttr(SPAttr::DX); + this->readAttr(SPAttr::DY); + this->readAttr(SPAttr::ROTATE); + this->readAttr(SPAttr::STARTOFFSET); + this->readAttr(SPAttr::SIDE); + this->readAttr(SPAttr::XLINK_HREF); + + this->readAttr(SPAttr::STYLE); + + SPItem::build(doc, repr); +} + +void SPTextPath::release() { + //this->attributes.~TextTagAttributes(); + + if (this->originalPath) { + delete this->originalPath; + } + + this->originalPath = nullptr; + + SPItem::release(); +} + +void SPTextPath::set(SPAttr key, const gchar* value) { + + if (this->attributes.readSingleAttribute(key, value, style, &viewport)) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else { + switch (key) { + case SPAttr::XLINK_HREF: + this->sourcePath->link((char*)value); + break; + case SPAttr::SIDE: + if (!value) { + return; + } + + if (strncmp(value, "left", 4) == 0) + side = SP_TEXT_PATH_SIDE_LEFT; + else if (strncmp(value, "right", 5) == 0) + side = SP_TEXT_PATH_SIDE_RIGHT; + else { + std::cerr << "SPTextPath: Bad side value: " << (value?value:"null") << std::endl; + side = SP_TEXT_PATH_SIDE_LEFT; + } + break; + case SPAttr::STARTOFFSET: + this->startOffset.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPItem::set(key, value); + break; + } + } +} + +void SPTextPath::update(SPCtx *ctx, guint flags) { + this->isUpdating = true; + + if ( this->sourcePath->sourceDirty ) { + refresh_textpath_source(this); + } + + this->isUpdating = false; + + unsigned childflags = (flags & SP_OBJECT_MODIFIED_CASCADE); + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + for (auto& ochild: children) { + if (childflags || (ochild.uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + ochild.updateDisplay(ctx, childflags); + } + } + + SPItem::update(ctx, flags); + + if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_LAYOUT_MODIFIED_FLAG ) ) + { + SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx); + + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + attributes.update( em, ex, w, h ); + } +} + + +void refresh_textpath_source(SPTextPath* tp) +{ + if ( tp == nullptr ) { + return; + } + + tp->sourcePath->refresh_source(); + tp->sourcePath->sourceDirty=false; + + if ( tp->sourcePath->originalPath ) { + if (tp->originalPath) { + delete tp->originalPath; + } + + auto curve_copy = *tp->sourcePath->originalPath; + if (tp->side == SP_TEXT_PATH_SIDE_RIGHT) { + curve_copy.reverse(); + } + + auto item = cast<SPItem>(tp->sourcePath->sourceObject); + tp->originalPath = new Path; + tp->originalPath->LoadPathVector(curve_copy.get_pathvector(), item->transform, true); + tp->originalPath->ConvertWithBackData(0.01); + } +} + +void SPTextPath::modified(unsigned int flags) { +// SPItem::onModified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + for (auto& ochild: children) { + if (flags || (ochild.mflags & SP_OBJECT_MODIFIED_FLAG)) { + ochild.emitModified(flags); + } + } +} + +Inkscape::XML::Node* SPTextPath::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:textPath"); + } + + this->attributes.writeTo(repr); + + if (this->side == SP_TEXT_PATH_SIDE_RIGHT) { + this->setAttribute("side", "right"); + } + + if (this->startOffset._set) { + if (this->startOffset.unit == SVGLength::PERCENT) { + Inkscape::SVGOStringStream os; + os << (this->startOffset.computed * 100.0) << "%"; + this->setAttribute("startOffset", os.str()); + } else { + /* FIXME: This logic looks rather undesirable if e.g. startOffset is to be + in ems. */ + repr->setAttributeSvgDouble("startOffset", this->startOffset.computed); + } + } + + if ( this->sourcePath->sourceHref ) { + Inkscape::setHrefAttribute(*repr, this->sourcePath->sourceHref); + } + + if ( flags & SP_OBJECT_WRITE_BUILD ) { + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + Inkscape::XML::Node* c_repr=nullptr; + + if ( is<SPTSpan>(&child) || is<SPTRef>(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( is<SPTextPath>(&child) ) { + //c_repr = child->updateRepr(xml_doc, NULL, flags); // shouldn't happen + } else if ( is<SPString>(&child) ) { + c_repr = xml_doc->createTextNode(cast<SPString>(&child)->string.c_str()); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + + for( auto i = l.rbegin(); i != l.rend(); ++i ) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( is<SPTSpan>(&child) || is<SPTRef>(&child) ) { + child.updateRepr(flags); + } else if ( is<SPTextPath>(&child) ) { + //c_repr = child.updateRepr(xml_doc, NULL, flags); // shouldn't happen + } else if ( is<SPString>(&child) ) { + child.getRepr()->setContent(cast<SPString>(&child)->string.c_str()); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + + +SPItem *sp_textpath_get_path_item(SPTextPath const *tp) +{ + if (tp && tp->sourcePath) { + return tp->sourcePath->getObject(); + } + return nullptr; +} + +void sp_textpath_to_text(SPObject *tp) +{ + SPObject *text = tp->parent; + + // make a list of textpath children + std::vector<Inkscape::XML::Node *> tp_reprs; + + for (auto& o: tp->children) { + tp_reprs.push_back(o.getRepr()); + } + + for (auto i = tp_reprs.rbegin(); i != tp_reprs.rend(); ++i) { + // make a copy of each textpath child + Inkscape::XML::Node *copy = (*i)->duplicate(text->getRepr()->document()); + // remove the old repr from under textpath + tp->getRepr()->removeChild(*i); + // put its copy under text + text->getRepr()->addChild(copy, nullptr); // fixme: copy id + } + + // set x/y on text (to be near where it was when on path) + // Copied from Layout::fitToPathAlign + Path *path = cast<SPTextPath>(tp)->originalPath; + SVGLength const startOffset = cast<SPTextPath>(tp)->startOffset; + double offset = 0.0; + if (startOffset._set) { + if (startOffset.unit == SVGLength::PERCENT) + offset = startOffset.computed * path->Length(); + else + offset = startOffset.computed; + } + int unused = 0; + Path::cut_position *cut_pos = path->CurvilignToPosition(1, &offset, unused); + Geom::Point midpoint; + Geom::Point tangent; + path->PointAndTangentAt(cut_pos[0].piece, cut_pos[0].t, midpoint, tangent); + text->getRepr()->setAttributeSvgDouble("x", midpoint[Geom::X]); + text->getRepr()->setAttributeSvgDouble("y", midpoint[Geom::Y]); + + //remove textpath + tp->deleteObject(); +} + + +/* + 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 : diff --git a/src/object/sp-tspan.h b/src/object/sp-tspan.h new file mode 100644 index 0000000..feb5a1b --- /dev/null +++ b/src/object/sp-tspan.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SP_TSPAN_H +#define INKSCAPE_SP_TSPAN_H + +/* + * tspan and textpath, based on the flowtext routines + */ + +#include "sp-item.h" +#include "text-tag-attributes.h" + +enum { + SP_TSPAN_ROLE_UNSPECIFIED, + SP_TSPAN_ROLE_PARAGRAPH, + SP_TSPAN_ROLE_LINE +}; + +class SPTSpan final : public SPItem { +public: + SPTSpan(); + ~SPTSpan() override; + int tag() const override { return tag_of<decltype(*this)>; } + + unsigned int role : 2; + TextTagAttributes attributes; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttr key, const char* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + const char* typeName() const override; + const char* displayName() const override; +}; + +#endif /* !INKSCAPE_SP_TSPAN_H */ + +/* + 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 : diff --git a/src/object/sp-use-reference.cpp b/src/object/sp-use-reference.cpp new file mode 100644 index 0000000..3b46f77 --- /dev/null +++ b/src/object/sp-use-reference.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to href of <use> element. + * + * Copyright (C) 2004 Bulia Byak + * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-use-reference.h" + +#include <cstring> +#include <string> + +#include "bad-uri-exception.h" +#include "enums.h" + +#include "display/curve.h" +#include "livarot/Path.h" +#include "preferences.h" +#include "sp-shape.h" +#include "sp-text.h" +#include "uri.h" + +bool SPUseReference::_acceptObject(SPObject * const obj) const +{ + return URIReference::_acceptObject(obj); +} + + +static void sp_usepath_href_changed(SPObject *old_ref, SPObject *ref, SPUsePath *offset); +static void sp_usepath_move_compensate(Geom::Affine const *mp, SPItem *original, SPUsePath *self); +static void sp_usepath_delete_self(SPObject *deleted, SPUsePath *offset); +static void sp_usepath_source_modified(SPObject *iSource, guint flags, SPUsePath *offset); + +SPUsePath::SPUsePath(SPObject* i_owner): SPUseReference(i_owner) + , owner(i_owner) +{ + _changed_connection = changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_usepath_href_changed), this)); // listening to myself, this should be virtual instead + user_unlink = nullptr; +} + +SPUsePath::~SPUsePath() +{ + _changed_connection.disconnect(); // to do before unlinking + + quit_listening(); + unlink(); +} + +void +SPUsePath::link(char *to) +{ + if ( to == nullptr ) { + quit_listening(); + unlink(); + } else { + if ( !sourceHref || ( strcmp(to, sourceHref) != 0 ) ) { + g_free(sourceHref); + sourceHref = g_strdup(to); + try { + attach(Inkscape::URI(to)); + } catch (Inkscape::BadURIException &e) { + /* TODO: Proper error handling as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. + */ + g_warning("%s", e.what()); + detach(); + } + } + } +} + +void +SPUsePath::unlink() +{ + g_free(sourceHref); + sourceHref = nullptr; + detach(); +} + +void +SPUsePath::start_listening(SPObject* to) +{ + if ( to == nullptr ) { + return; + } + sourceObject = to; + sourceRepr = to->getRepr(); + _delete_connection = to->connectDelete(sigc::bind(sigc::ptr_fun(&sp_usepath_delete_self), this)); + _transformed_connection = cast<SPItem>(to)->connectTransformed(sigc::bind(sigc::ptr_fun(&sp_usepath_move_compensate), this)); + _modified_connection = to->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_usepath_source_modified), this)); +} + +void +SPUsePath::quit_listening() +{ + if ( sourceObject == nullptr ) { + return; + } + _modified_connection.disconnect(); + _delete_connection.disconnect(); + _transformed_connection.disconnect(); + sourceRepr = nullptr; + sourceObject = nullptr; +} + +static void +sp_usepath_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, SPUsePath *offset) +{ + offset->quit_listening(); + SPItem *refobj = offset->getObject(); + if ( refobj ) { + offset->start_listening(refobj); + } + offset->sourceDirty=true; + offset->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +static void +sp_usepath_move_compensate(Geom::Affine const *mp, SPItem *original, SPUsePath *self) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_PARALLEL); + if (mode == SP_CLONE_COMPENSATION_NONE) { + return; + } + auto item = cast<SPItem>(self->owner); + +// TODO kill naughty naughty #if 0 +#if 0 + Geom::Affine m(*mp); + if (!(m.is_translation())) { + return; + } + Geom::Affine const t(item->transform); + Geom::Affine clone_move = t.inverse() * m * t; + + // Calculate the compensation matrix and the advertized movement matrix. + Geom::Affine advertized_move; + if (mode == SP_CLONE_COMPENSATION_PARALLEL) { + //clone_move = clone_move.inverse(); + advertized_move.set_identity(); + } else if (mode == SP_CLONE_COMPENSATION_UNMOVED) { + clone_move = clone_move.inverse() * m; + advertized_move = m; + } else { + g_assert_not_reached(); + } + + // Commit the compensation. + item->transform *= clone_move; + sp_item_write_transform(item, item->getRepr(), item->transform, &advertized_move); +#else + (void)mp; + (void)original; +#endif + + self->sourceDirty = true; + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +static void +sp_usepath_delete_self(SPObject */*deleted*/, SPUsePath *offset) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint const mode = prefs->getInt("/options/cloneorphans/value", SP_CLONE_ORPHANS_UNLINK); + + if (mode == SP_CLONE_ORPHANS_UNLINK) { + // leave it be. just forget about the source + offset->quit_listening(); + offset->unlink(); + if (offset->user_unlink) + offset->user_unlink(offset->owner); + } else if (mode == SP_CLONE_ORPHANS_DELETE) { + offset->owner->deleteObject(); + } +} + +static void +sp_usepath_source_modified(SPObject */*iSource*/, guint /*flags*/, SPUsePath *offset) +{ + offset->sourceDirty = true; + offset->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPUsePath::refresh_source() +{ + sourceDirty = false; + + originalPath.reset(); + + SPObject *refobj = sourceObject; + if ( refobj == nullptr ) return; + + if (auto shape = cast<SPShape>(refobj)) { + if (shape->curve()) { + originalPath = *shape->curve(); + } else { + sourceDirty = true; + } + } else if (auto text = cast<SPText>(refobj)) { + originalPath = text->getNormalizedBpath(); + } +} + + +/* + 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 : diff --git a/src/object/sp-use-reference.h b/src/object/sp-use-reference.h new file mode 100644 index 0000000..34a6502 --- /dev/null +++ b/src/object/sp-use-reference.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_USE_REFERENCE_H +#define SEEN_SP_USE_REFERENCE_H + +/* + * The reference corresponding to href of <use> element. + * + * Copyright (C) 2004 Bulia Byak + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/sigc++.h> + +#include "sp-item.h" +#include "uri-references.h" +#include "display/curve.h" + +#include <memory> + +namespace Inkscape { +namespace XML { +class Node; +} +} + + +class SPUseReference : public Inkscape::URIReference { +public: + SPUseReference(SPObject *owner) : URIReference(owner) {} + + SPItem *getObject() const { + return static_cast<SPItem *>(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject * const obj) const override; + +}; + + +class SPUsePath : public SPUseReference { +public: + std::optional<SPCurve> originalPath; + bool sourceDirty{false}; + + SPObject *owner; + char *sourceHref{nullptr}; + Inkscape::XML::Node *sourceRepr{nullptr}; + SPObject *sourceObject{nullptr}; + + sigc::connection _modified_connection; + sigc::connection _delete_connection; + sigc::connection _changed_connection; + sigc::connection _transformed_connection; + + SPUsePath(SPObject* i_owner); + ~SPUsePath() override; + + void link(char* to); + void unlink(); + void start_listening(SPObject* to); + void quit_listening(); + void refresh_source(); + + void (*user_unlink) (SPObject *user); +}; + +#endif /* !SEEN_SP_USE_REFERENCE_H */ + +/* + 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 : diff --git a/src/object/sp-use.cpp b/src/object/sp-use.cpp new file mode 100644 index 0000000..3adc199 --- /dev/null +++ b/src/object/sp-use.cpp @@ -0,0 +1,860 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <use> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <2geom/transforms.h> +#include <glibmm/i18n.h> +#include <glibmm/markup.h> + +#include "bad-uri-exception.h" +#include "display/curve.h" +#include "display/drawing-group.h" +#include "attributes.h" +#include "document.h" +#include "sp-clippath.h" +#include "sp-mask.h" +#include "sp-factory.h" +#include "sp-flowregion.h" +#include "uri.h" +#include "print.h" +#include "xml/repr.h" +#include "xml/href-attribute-helper.h" +#include "svg/svg.h" +#include "preferences.h" +#include "style.h" + +#include "sp-use.h" +#include "sp-symbol.h" +#include "sp-root.h" +#include "sp-use-reference.h" +#include "sp-shape.h" +#include "sp-text.h" +#include "sp-flowtext.h" + +SPUse::SPUse() + : SPItem(), + SPDimensions(), + child(nullptr), + href(nullptr), + ref(new SPUseReference(this)), + _delete_connection(), + _changed_connection(), + _transformed_connection() +{ + this->x.unset(); + this->y.unset(); + this->width.unset(SVGLength::PERCENT, 1.0, 1.0); + this->height.unset(SVGLength::PERCENT, 1.0, 1.0); + + this->_changed_connection = this->ref->changedSignal().connect( + sigc::hide(sigc::hide(sigc::mem_fun(*this, &SPUse::href_changed))) + ); +} + +SPUse::~SPUse() { + if (this->child) { + this->detach(this->child); + this->child = nullptr; + } + + this->ref->detach(); + delete this->ref; + this->ref = nullptr; +} + +void SPUse::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPItem::build(document, repr); + + this->readAttr(SPAttr::X); + this->readAttr(SPAttr::Y); + this->readAttr(SPAttr::WIDTH); + this->readAttr(SPAttr::HEIGHT); + this->readAttr(SPAttr::XLINK_HREF); + + // We don't need to create child here: + // reading xlink:href will attach ref, and that will cause the changed signal to be emitted, + // which will call SPUse::href_changed, and that will take care of the child +} + +void SPUse::release() { + if (this->child) { + this->detach(this->child); + this->child = nullptr; + } + + this->_delete_connection.disconnect(); + this->_changed_connection.disconnect(); + this->_transformed_connection.disconnect(); + + g_free(this->href); + this->href = nullptr; + + this->ref->detach(); + + SPItem::release(); +} + +void SPUse::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::X: + this->x.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::Y: + this->y.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::WIDTH: + this->width.readOrUnset(value, SVGLength::PERCENT, 1.0, 1.0); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::HEIGHT: + this->height.readOrUnset(value, SVGLength::PERCENT, 1.0, 1.0); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SPAttr::XLINK_HREF: { + if ( value && this->href && ( strcmp(value, this->href) == 0 ) ) { + /* No change, do nothing. */ + } else { + g_free(this->href); + this->href = nullptr; + + if (value) { + // First, set the href field, because SPUse::href_changed will need it. + this->href = g_strdup(value); + + // Now do the attaching, which emits the changed signal. + try { + this->ref->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + this->ref->detach(); + } + } else { + this->ref->detach(); + } + } + break; + } + + default: + SPItem::set(key, value); + break; + } +} + +Inkscape::XML::Node* SPUse::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:use"); + } + + SPItem::write(xml_doc, repr, flags); + + this->writeDimensions(repr); + + if (this->ref->getURI()) { + auto uri_string = this->ref->getURI()->str(); + auto href_key = Inkscape::getHrefAttribute(*repr).first; + repr->setAttributeOrRemoveIfEmpty(href_key, uri_string); + } + + auto shape = cast<SPShape>(child); + if (shape) { + shape->set_shape(); // evaluate SPCurve of child + } else { + auto text = cast<SPText>(child); + if (text) { + text->rebuildLayout(); // refresh Layout, LP Bug 1339305 + } else { + auto flowtext = cast<SPFlowtext>(child); + if (flowtext) { + auto flowregion = cast<SPFlowregion>(flowtext->firstChild()); + if (flowregion) { + flowregion->UpdateComputed(); + } + flowtext->rebuildLayout(); + } + } + } + + return repr; +} + +Geom::OptRect SPUse::bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype) const { + Geom::OptRect bbox; + + if (this->child) { + Geom::Affine const ct(child->transform * Geom::Translate(this->x.computed, this->y.computed) * transform ); + + bbox = child->bounds(bboxtype, ct); + } + + return bbox; +} + +std::optional<Geom::PathVector> SPUse::documentExactBounds() const +{ + std::optional<Geom::PathVector> result; + auto *original = trueOriginal(); + if (!original) { + return result; + } + result = original->documentExactBounds(); + + Geom::Affine private_transform; + if (is<SPSymbol>(original)) { + private_transform = i2doc_affine(); + } else if (auto const *parent = cast<SPItem>(original->parent)) { + private_transform = get_root_transform() * parent->transform.inverse() * parent->i2doc_affine(); + } + result = result ? (*result // TODO: is there a simpler way to get the transform below? + * original->i2doc_affine().inverse() + * private_transform) + : result; + return result; +} + +void SPUse::print(SPPrintContext* ctx) { + bool translated = false; + + if ((this->x._set && this->x.computed != 0) || (this->y._set && this->y.computed != 0)) { + Geom::Affine tp(Geom::Translate(this->x.computed, this->y.computed)); + ctx->bind(tp, 1.0); + translated = true; + } + + if (this->child) { + this->child->invoke_print(ctx); + } + + if (translated) { + ctx->release(); + } +} + +const char* SPUse::typeName() const { + if (is<SPSymbol>(child)) { + return "symbol"; + } else { + return "clone"; + } +} + +const char* SPUse::displayName() const { + if (is<SPSymbol>(child)) { + return _("Symbol"); + } else { + return _("Clone"); + } +} + +gchar* SPUse::description() const { + if (child) { + if (is<SPSymbol>(child)) { + if (child->title()) { + return g_strdup_printf(_("called %s"), Glib::Markup::escape_text(Glib::ustring( g_dpgettext2(nullptr, "Symbol", child->title()))).c_str()); + } else if (child->getAttribute("id")) { + return g_strdup_printf(_("called %s"), Glib::Markup::escape_text(Glib::ustring( g_dpgettext2(nullptr, "Symbol", child->getAttribute("id")))).c_str()); + } else { + return g_strdup_printf(_("called %s"), _("Unnamed Symbol")); + } + } + + static unsigned recursion_depth = 0; + + if (recursion_depth >= 4) { + /* TRANSLATORS: Used for statusbar description for long <use> chains: + * "Clone of: Clone of: ... in Layer 1". */ + return g_strdup(_("...")); + /* We could do better, e.g. chasing the href chain until we reach something other than + * a <use>, and giving its description. */ + } + + ++recursion_depth; + char *child_desc = this->child->detailedDescription(); + --recursion_depth; + + char *ret = g_strdup_printf(_("of: %s"), child_desc); + g_free(child_desc); + + return ret; + } else { + return g_strdup(_("[orphaned]")); + } +} + +Inkscape::DrawingItem* SPUse::show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) { + + // std::cout << "SPUse::show: " << (getId()?getId():"null") << std::endl; + Inkscape::DrawingGroup *ai = new Inkscape::DrawingGroup(drawing); + ai->setPickChildren(false); + this->context_style = this->style; + ai->setStyle(this->style, this->context_style); + + if (this->child) { + Inkscape::DrawingItem *ac = this->child->invoke_show(drawing, key, flags); + + if (ac) { + ai->prependChild(ac); + } + + Geom::Translate t(this->x.computed, this->y.computed); + ai->setChildTransform(t); + } + + return ai; +} + +void SPUse::hide(unsigned int key) { + if (this->child) { + this->child->invoke_hide(key); + } + +// SPItem::onHide(key); +} + + +/** + * Returns the ultimate original of a SPUse (i.e. the first object in the chain of its originals + * which is not an SPUse). If no original is found, NULL is returned (it is the responsibility + * of the caller to make sure that this is handled correctly). + * + * Note that the returned is the clone object, i.e. the child of an SPUse (of the argument one for + * the trivial case) and not the "true original". If you want the true original, use trueOriginal(). + */ +SPItem *SPUse::root() { + SPItem *orig = this->child; + + auto use = cast<SPUse>(orig); + while (orig && use) { + orig = use->child; + use = cast<SPUse>(orig); + } + + return orig; +} + +SPItem const *SPUse::root() const { + return const_cast<SPUse*>(this)->root(); +} + +/** + * Returns the ultimate original of a SPUse, i.e., the first object in the chain of uses + * which is not itself an SPUse. If the chain of references is broken or no original is found, + * the return value will be nullptr. + */ +SPItem *SPUse::trueOriginal() const +{ + int const depth = cloneDepth(); + if (depth < 0) { + return nullptr; + } + + SPItem *original_item = (SPItem *)this; + for (int i = 0; i < depth; ++i) { + if (auto const *intermediate_clone = cast<SPUse>(original_item)) { + original_item = intermediate_clone->get_original(); + } else { + return nullptr; + } + } + return original_item; +} + +/** + * @brief Test the passed predicate on all items in a chain of uses. + * + * The chain includes this item, all of its intermediate ancestors in a chain of uses, as well as + * the ultimate original item. + * + * @return Whether any of the items in the chain satisfies the predicate. + */ +bool SPUse::anyInChain(bool (*predicate)(SPItem const *)) const +{ + int const depth = cloneDepth(); + if (depth < 0) { + return predicate(this); + } + + SPItem const *item = this; + if (predicate(item)) { + return true; + } + + for (int i = 0; i < depth; ++i) { + if (auto const *intermediate_clone = cast<SPUse>(item)) { + item = intermediate_clone->get_original(); + if (predicate(item)) { + return true; + } + } else { + break; + } + } + return false; +} + +/** + * Get the number of dereferences or calls to get_original() needed to get an object + * which is not an svg:use. Returns -1 if there is no original object. + */ +int SPUse::cloneDepth() const { + unsigned depth = 1; + SPItem *orig = this->child; + + while (orig && cast<SPUse>(orig)) { + ++depth; + orig = cast<SPUse>(orig)->child; + } + + if (!orig) { + return -1; + } else { + return depth; + } +} + +/** + * Returns the effective transform that goes from the ultimate original to given SPUse, both ends + * included. + */ +Geom::Affine SPUse::get_root_transform() const +{ + //track the ultimate source of a chain of uses + SPObject *orig = this->child; + + std::vector<SPItem const *> chain; + chain.push_back(this); + + while (cast<SPUse>(orig)) { + chain.push_back(cast<SPItem>(orig)); + orig = cast<SPUse>(orig)->child; + } + + chain.push_back(cast<SPItem>(orig)); + + // calculate the accumulated transform, starting from the original + Geom::Affine t(Geom::identity()); + + for (auto i=chain.rbegin(); i!=chain.rend(); ++i) { + auto *i_tem = *i; + + // "An additional transformation translate(x,y) is appended to the end (i.e., + // right-side) of the transform attribute on the generated 'g', where x and y + // represent the values of the x and y attributes on the 'use' element." - http://www.w3.org/TR/SVG11/struct.html#UseElement + auto *i_use = cast<SPUse>(i_tem); + if (i_use) { + if ((i_use->x._set && i_use->x.computed != 0) || (i_use->y._set && i_use->y.computed != 0)) { + t = t * Geom::Translate(i_use->x._set ? i_use->x.computed : 0, i_use->y._set ? i_use->y.computed : 0); + } + } + + t *= i_tem->transform; + } + return t; +} + +/** + * Returns the transform that leads to the use from its immediate original. + * Does not include the original's transform if any. + */ +Geom::Affine SPUse::get_parent_transform() const +{ + Geom::Affine t(Geom::identity()); + + if ((this->x._set && this->x.computed != 0) || (this->y._set && this->y.computed != 0)) { + t *= Geom::Translate(this->x._set ? this->x.computed : 0, this->y._set ? this->y.computed : 0); + } + + t *= this->transform; + return t; +} + +/** + * Sensing a movement of the original, this function attempts to compensate for it in such a way + * that the clone stays unmoved or moves in parallel (depending on user setting) regardless of the + * clone's transform. + */ +void SPUse::move_compensate(Geom::Affine const *mp) { + // the clone is orphaned; or this is not a real use, but a clone of another use; + // we skip it, otherwise duplicate compensation will occur + if (this->cloned) { + return; + } + + // never compensate uses which are used in flowtext + if (parent && cast<SPFlowregion>(parent)) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_PARALLEL); + // user wants no compensation + if (mode == SP_CLONE_COMPENSATION_NONE) + return; + + Geom::Affine m(*mp); + Geom::Affine t = this->get_parent_transform(); + Geom::Affine clone_move = t.inverse() * m * t; + + // this is not a simple move, do not try to compensate + if (!(m.isTranslation())){ + //BUT move clippaths accordingly. + //if clone has a clippath, move it accordingly + if (getClipObject()) { + for (auto &clip : getClipObject()->children) { + SPItem *item = (SPItem*) &clip; + if(item){ + item->transform *= m; + Geom::Affine identity; + item->doWriteTransform(item->transform, &identity); + } + } + } + if (getMaskObject()) { + for (auto &mask : getMaskObject()->children) { + SPItem *item = (SPItem*) &mask; + if(item){ + item->transform *= m; + Geom::Affine identity; + item->doWriteTransform(item->transform, &identity); + } + } + } + return; + } + + // restore item->transform field from the repr, in case it was changed by seltrans + this->readAttr (SPAttr::TRANSFORM); + + + // calculate the compensation matrix and the advertized movement matrix + Geom::Affine advertized_move; + if (mode == SP_CLONE_COMPENSATION_PARALLEL) { + clone_move = clone_move.inverse() * m; + advertized_move = m; + } else if (mode == SP_CLONE_COMPENSATION_UNMOVED) { + clone_move = clone_move.inverse(); + advertized_move.setIdentity(); + } else { + g_assert_not_reached(); + } + + //if clone has a clippath, move it accordingly + if (getClipObject()) { + for (auto &clip : getClipObject()->children) { + SPItem *item = (SPItem*) &clip; + if(item){ + item->transform *= clone_move.inverse(); + Geom::Affine identity; + item->doWriteTransform(item->transform, &identity); + } + } + } + if (getMaskObject()) { + for (auto &mask : getMaskObject()->children) { + SPItem *item = (SPItem*) &mask; + if(item){ + item->transform *= clone_move.inverse(); + Geom::Affine identity; + item->doWriteTransform(item->transform, &identity); + } + } + } + + + // commit the compensation + this->transform *= clone_move; + this->doWriteTransform(this->transform, &advertized_move); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPUse::href_changed() { + this->_delete_connection.disconnect(); + this->_transformed_connection.disconnect(); + + if (this->child) { + this->detach(this->child); + this->child = nullptr; + } + + if (this->href) { + SPItem *refobj = this->ref->getObject(); + + if (refobj) { + Inkscape::XML::Node *childrepr = refobj->getRepr(); + + SPObject* obj = SPFactory::createObject(NodeTraits::get_type_string(*childrepr)); + + auto item = cast<SPItem>(obj); + if (item) { + child = item; + + this->attach(this->child, this->lastChild()); + sp_object_unref(this->child, this); + + this->child->invoke_build(refobj->document, childrepr, TRUE); + + for (auto &v : views) { + auto ai = this->child->invoke_show(v.drawingitem->drawing(), v.key, v.flags); + if (ai) { + v.drawingitem->prependChild(ai); + } + } + + this->_delete_connection = refobj->connectDelete( + sigc::hide(sigc::mem_fun(*this, &SPUse::delete_self)) + ); + + this->_transformed_connection = refobj->connectTransformed( + sigc::hide(sigc::mem_fun(*this, &SPUse::move_compensate)) + ); + } else { + delete obj; + } + } + } +} + +void SPUse::delete_self() { + // always delete uses which are used in flowtext + if (parent && cast<SPFlowregion>(parent)) { + deleteObject(); + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint const mode = prefs->getInt("/options/cloneorphans/value", + SP_CLONE_ORPHANS_UNLINK); + + if (mode == SP_CLONE_ORPHANS_UNLINK) { + this->unlink(); + } else if (mode == SP_CLONE_ORPHANS_DELETE) { + this->deleteObject(); + } +} + +void SPUse::update(SPCtx *ctx, unsigned flags) { + // std::cout << "SPUse::update: " << (getId()?getId():"null") << std::endl; + SPItemCtx *ictx = (SPItemCtx *) ctx; + SPItemCtx cctx = *ictx; + + unsigned childflags = flags; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + childflags &= SP_OBJECT_MODIFIED_CASCADE; + + /* Set up child viewport */ + this->calcDimsFromParentViewport(ictx); + + childflags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; + + if (this->child) { + sp_object_ref(this->child); + + if (childflags || (this->child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + g_assert(child); + cctx.i2doc = child->transform * ictx->i2doc; + cctx.i2vp = child->transform * ictx->i2vp; + child->updateDisplay(&cctx, childflags); + } + + sp_object_unref(this->child); + } + + SPItem::update(ctx, flags); + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (auto &v : views) { + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + context_style = style; + g->setStyle(style, context_style); + } + } + + /* As last step set additional transform of arena group */ + for (auto &v : views) { + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + auto t = Geom::Translate(x.computed, y.computed); + g->setChildTransform(t); + } +} + +void SPUse::modified(unsigned flags) +{ + // std::cout << "SPUse::modified: " << (getId()?getId():"null") << std::endl; + flags = cascade_flags(flags); + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (auto &v : views) { + auto g = cast<Inkscape::DrawingGroup>(v.drawingitem.get()); + context_style = style; + g->setStyle(style, context_style); + } + } + + if (child) { + sp_object_ref(child); + + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +SPItem *SPUse::unlink() { + Inkscape::XML::Node *repr = this->getRepr(); + + if (!repr) { + return nullptr; + } + + Inkscape::XML::Node *parent = repr->parent(); + SPDocument *document = this->document; + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // Track the ultimate source of a chain of uses. + SPItem *orig = this->root(); + SPItem *origtrue = this->trueOriginal(); + if (!orig) { + return nullptr; + } + + // Calculate the accumulated transform, starting from the original. + Geom::Affine t = this->get_root_transform(); + + Inkscape::XML::Node *copy = nullptr; + + if (auto symbol = cast<SPSymbol>(orig)) { + // make a group, copy children + copy = xml_doc->createElement("svg:g"); + copy->setAttribute("display","none"); + + for (Inkscape::XML::Node *child = orig->getRepr()->firstChild() ; child != nullptr; child = child->next()) { + Inkscape::XML::Node *newchild = child->duplicate(xml_doc); + copy->appendChild(newchild); + } + + // viewBox transformation + t = symbol->c2p * t; + } else { // just copy + copy = orig->getRepr()->duplicate(xml_doc); + copy->setAttribute("display","none"); + } + // Add the duplicate repr just after the existing one. + parent->addChild(copy, repr); + + // Retrieve the SPItem of the resulting repr. + SPObject *unlinked = document->getObjectByRepr(copy); + if (origtrue) { + if (unlinked) { + origtrue->setTmpSuccessor(unlinked); + } + auto newLPEObj = cast<SPLPEItem>(unlinked); + if (newLPEObj) { + // force always fork + newLPEObj->forkPathEffectsIfNecessary(1, true, true); + sp_lpe_item_update_patheffect(newLPEObj, false, true, true); + } + origtrue->fixTmpSuccessors(); + origtrue->unsetTmpSuccessor(); + } + + // Merge style from the use. + unlinked->style->merge( this->style ); + unlinked->style->cascade( unlinked->parent->style ); + unlinked->updateRepr(); + unlinked->removeAttribute("display"); + + // Hold onto our SPObject and repr for now. + sp_object_ref(this); + Inkscape::GC::anchor(repr); + + // Remove ourselves, not propagating delete events to avoid a + // chain-reaction with other elements that might reference us. + this->deleteObject(false); + + // Give the copy our old id and let go of our old repr. + copy->setAttribute("id", repr->attribute("id")); + Inkscape::GC::release(repr); + + // Remove tiled clone attrs. + copy->removeAttribute("inkscape:tiled-clone-of"); + copy->removeAttribute("inkscape:tile-w"); + copy->removeAttribute("inkscape:tile-h"); + copy->removeAttribute("inkscape:tile-cx"); + copy->removeAttribute("inkscape:tile-cy"); + + // Establish the succession and let go of our object. + this->setSuccessor(unlinked); + sp_object_unref(this); + + auto item = cast<SPItem>(unlinked); + g_assert(item != nullptr); + + // Set the accummulated transform. + { + Geom::Affine nomove(Geom::identity()); + // Advertise ourselves as not moving. + item->doWriteTransform(t, &nomove); + } + document->fix_lpe_data(); + + return item; +} + +SPItem *SPUse::get_original() const +{ + SPItem *ref = nullptr; + + if (this->ref){ + ref = this->ref->getObject(); + } + + return ref; +} + +void SPUse::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + SPItem const *root = this->root(); + + if (!root) { + return; + } + + root->snappoints(p, snapprefs); +} + + +/* + 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 : diff --git a/src/object/sp-use.h b/src/object/sp-use.h new file mode 100644 index 0000000..5d14182 --- /dev/null +++ b/src/object/sp-use.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_USE_H +#define SEEN_SP_USE_H + +/* + * SVG <use> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2014 Authors + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "svg/svg-length.h" +#include "sp-dimensions.h" +#include "sp-item.h" +#include "enums.h" + +class SPUseReference; + +class SPUse final : public SPItem, public SPDimensions { +public: + SPUse(); + ~SPUse() override; + int tag() const override { return tag_of<decltype(*this)>; } + + // item built from the original's repr (the visible clone) + // relative to the SPUse itself, it is treated as a child, similar to a grouped item relative to its group + SPItem *child; + + // SVG attrs + char *href; + + // the reference to the original object + SPUseReference *ref; + + // a sigc connection for delete notifications + sigc::connection _delete_connection; + sigc::connection _changed_connection; + + // a sigc connection for transformed signal, used to do move compensation + sigc::connection _transformed_connection; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttr key, char const *value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype) const override; + std::optional<Geom::PathVector> documentExactBounds() const override; + const char* typeName() const override; + const char* displayName() const override; + char* description() const override; + void print(SPPrintContext *ctx) override; + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void hide(unsigned int key) override; + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + + SPItem *root(); + SPItem const *root() const; + int cloneDepth() const; + + SPItem *unlink(); + SPItem *get_original() const; + Geom::Affine get_parent_transform() const; + Geom::Affine get_root_transform() const; + SPItem *trueOriginal() const; + bool anyInChain(bool (*predicate)(SPItem const *)) const; + +private: + void href_changed(); + void move_compensate(Geom::Affine const *mp); + void delete_self(); +}; + +#endif + +/* + 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 : diff --git a/src/object/tags.h b/src/object/tags.h new file mode 100644 index 0000000..68cae22 --- /dev/null +++ b/src/object/tags.h @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: PBS <pbs3141@gmail.com> + * Copyright (C) 2022 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_OBJECT_TAGS_H +#define SP_OBJECT_TAGS_H + +#include "util/cast.h" + +// Class hierarchy structure + +#define SPOBJECT_HIERARCHY_DATA(X)\ +X(SPObject,\ + X(ColorProfile_PLACEHOLDER)\ + X(LivePathEffectObject)\ + X(Persp3D)\ + X(SPDefs)\ + X(SPDesc)\ + X(SPFeDistantLight)\ + X(SPFeFuncNode)\ + X(SPFeMergeNode)\ + X(SPFePointLight)\ + X(SPFeSpotLight)\ + X(SPFilter)\ + X(SPFilterPrimitive,\ + X(SPFeBlend)\ + X(SPFeColorMatrix)\ + X(SPFeComponentTransfer)\ + X(SPFeComposite)\ + X(SPFeConvolveMatrix)\ + X(SPFeDiffuseLighting)\ + X(SPFeDisplacementMap)\ + X(SPFeFlood)\ + X(SPFeImage)\ + X(SPFeMerge)\ + X(SPFeMorphology)\ + X(SPFeOffset)\ + X(SPFeSpecularLighting)\ + X(SPFeTile)\ + X(SPFeTurbulence)\ + X(SPGaussianBlur)\ + )\ + X(SPFlowline)\ + X(SPFlowregionbreak)\ + X(SPFont)\ + X(SPFontFace)\ + X(SPGlyph)\ + X(SPGlyphKerning,\ + X(SPHkern)\ + X(SPVkern)\ + )\ + X(SPGrid)\ + X(SPGuide)\ + X(SPHatchPath)\ + X(SPItem,\ + X(SPFlowdiv)\ + X(SPFlowpara)\ + X(SPFlowregion)\ + X(SPFlowregionExclude)\ + X(SPFlowtext)\ + X(SPFlowtspan)\ + X(SPImage)\ + X(SPLPEItem,\ + X(SPGroup,\ + X(SPAnchor)\ + X(SPBox3D)\ + X(SPMarker)\ + X(SPRoot)\ + X(SPSwitch)\ + X(SPSymbol)\ + )\ + X(SPShape,\ + X(SPGenericEllipse)\ + X(SPLine)\ + X(SPOffset)\ + X(SPPath)\ + X(SPPolyLine)\ + X(SPPolygon,\ + X(Box3DSide)\ + )\ + X(SPRect)\ + X(SPSpiral)\ + X(SPStar)\ + )\ + )\ + X(SPTRef)\ + X(SPTSpan)\ + X(SPText)\ + X(SPTextPath)\ + X(SPUse)\ + )\ + X(SPMeshpatch)\ + X(SPMeshrow)\ + X(SPMetadata)\ + X(SPMissingGlyph)\ + X(SPObjectGroup,\ + X(SPClipPath)\ + X(SPMask)\ + X(SPNamedView)\ + )\ + X(SPPage)\ + X(SPPaintServer,\ + X(SPGradient,\ + X(SPLinearGradient)\ + X(SPMeshGradient)\ + X(SPRadialGradient)\ + )\ + X(SPHatch)\ + X(SPPattern)\ + X(SPSolidColor)\ + )\ + X(SPScript)\ + X(SPStop)\ + X(SPString)\ + X(SPStyleElem)\ + X(SPTag)\ + X(SPTagUse)\ + X(SPTitle)\ +) + +// Forward declarations + +#define X(n, ...) class n; __VA_ARGS__ +SPOBJECT_HIERARCHY_DATA(X) +#undef X + +// Tag generation + +enum class SPObjectTag : int +{ + #define X(n, ...) n##_first, __VA_ARGS__ n##_tmp, n##_last = n##_tmp - 1, + SPOBJECT_HIERARCHY_DATA(X) + #undef X +}; + +// Tag specialization + +#define X(n, ...) template <> inline constexpr int first_tag<n> = static_cast<int>(SPObjectTag::n##_first); __VA_ARGS__ +SPOBJECT_HIERARCHY_DATA(X) +#undef X + +#define X(n, ...) template <> inline constexpr int last_tag<n> = static_cast<int>(SPObjectTag::n##_last); __VA_ARGS__ +SPOBJECT_HIERARCHY_DATA(X) +#undef X + +// Special case for Inkscape::ColorProfile which lives in its own namespace. + +namespace Inkscape { class ColorProfile; } + +template <> inline constexpr int first_tag<Inkscape::ColorProfile> = first_tag<ColorProfile_PLACEHOLDER>; +template <> inline constexpr int last_tag <Inkscape::ColorProfile> = last_tag <ColorProfile_PLACEHOLDER>; + +#undef SPOBJECT_HIERARCHY_DATA + +#endif // SP_OBJECT_TAGS_H + +/* + 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 : diff --git a/src/object/uri-references.cpp b/src/object/uri-references.cpp new file mode 100644 index 0000000..c7a1d34 --- /dev/null +++ b/src/object/uri-references.cpp @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Helper methods for resolving URI References + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Marc Jeanmougin + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "uri-references.h" + +#include <iostream> +#include <cstring> + +#include <glibmm/miscutils.h> +#include "live_effects/lpeobject.h" +#include "bad-uri-exception.h" +#include "document.h" +#include "sp-object.h" +#include "uri.h" +#include "extract-uri.h" +#include "sp-tag-use.h" + +namespace Inkscape { + +URIReference::URIReference(SPObject *owner) + : _owner(owner) + , _owner_document(nullptr) + , _obj(nullptr) + , _uri(nullptr) +{ + g_assert(_owner != nullptr); + /* FIXME !!! attach to owner's destroy signal to clean up in case */ +} + +URIReference::URIReference(SPDocument *owner_document) + : _owner(nullptr) + , _owner_document(owner_document) + , _obj(nullptr) + , _uri(nullptr) +{ + g_assert(_owner_document != nullptr); +} + +URIReference::~URIReference() +{ + detach(); +} + +/* + * The main ideas here are: + * (1) "If we are inside a clone, then we can accept if and only if our "original thing" can accept the reference" + * (this caused problems when there are clones because a change in ids triggers signals for the object hrefing this id, + * but also its cloned reprs(descendants of <use> referencing an ancestor of the href'ing object)). + * + * (2) Once we have an (potential owner) object, it can accept a href to obj, iff the graph of objects where directed + * edges are + * either parent->child relations , *** or href'ing to href'ed *** relations, stays acyclic. + * We can go either from owner and up in the tree, or from obj and down, in either case this will be in the worst case + *linear in the number of objects. + * There are no easy objects allowing to do the second proposition, while "hrefList" is a "list of objects href'ing us", + *so we'll take this. + * Then we keep a set of already visited elements, and do a DFS on this graph. if we find obj, then BOOM. + */ + +bool URIReference::_acceptObject(SPObject *obj) const +{ + // we go back following hrefList and parent to find if the object already references ourselves indirectly + std::set<SPObject *> done; + SPObject *owner = getOwner(); + //allow LPE as owner has any URI attached + auto lpobj = cast<LivePathEffectObject>(obj); + if (!owner || lpobj) + return true; + + while (owner->cloned) { + if(!owner->clone_original)//happens when the clone is existing and linking to something, even before the original objects exists. + //for instance, it can happen when you paste a filtered object in a already cloned group: The construction of the + //clone representation of the filtered object will finish before the original object, so the cloned repr will + //have to _accept the filter even though the original does not exist yet. In that case, we'll accept iff the parent of the + //original can accept it: loops caused by other relations than parent-child would be prevented when created on their base object. + //Fixes bug 1636533. + owner = owner->parent; + else + owner = owner->clone_original; + } + // once we have the "original" object (hopefully) we look at who is referencing it + if (obj == owner) + return false; + std::list<SPObject *> todo(owner->hrefList); + todo.push_front(owner->parent); + while (!todo.empty()) { + SPObject *e = todo.front(); + todo.pop_front(); + if (!e) + continue; + if (done.insert(e).second) { + if (e == obj) { + return false; + } + todo.push_front(e->parent); + todo.insert(todo.begin(), e->hrefList.begin(), e->hrefList.end()); + } + } + return true; +} + +void URIReference::attach(const URI &uri) +{ + SPDocument *document = nullptr; + + // Attempt to get the document that contains the URI + if (_owner) { + document = _owner->document; + } else if (_owner_document) { + document = _owner_document; + } + + // createChildDoc() assumes that the referenced file is an SVG. + // PNG and JPG files are allowed (in the case of feImage). + gchar const *filename = uri.getPath() ? uri.getPath() : ""; + bool skip = false; + if (g_str_has_suffix(filename, ".jpg") || g_str_has_suffix(filename, ".JPG") || + g_str_has_suffix(filename, ".png") || g_str_has_suffix(filename, ".PNG")) { + skip = true; + } + + // The path contains references to separate document files to load. + if (document && uri.getPath() && !skip) { + char const *base = document->getDocumentBase(); + auto absuri = URI::from_href_and_basedir(uri.str().c_str(), base); + std::string path; + + try { + path = absuri.toNativeFilename(); + } catch (const Glib::Error &e) { + g_warning("%s", e.what().c_str()); + } + + if (!path.empty()) { + document = document->createChildDoc(path); + } else { + document = nullptr; + } + } + if (!document) { + g_warning("Can't get document for referenced URI: %s", filename); + return; + } + + gchar const *fragment = uri.getFragment(); + if (!uri.isRelative() || uri.getQuery() || !fragment) { + throw UnsupportedURIException(); + } + + /* FIXME !!! real xpointer support should be delegated to document */ + /* for now this handles the minimal xpointer form that SVG 1.0 + * requires of us + */ + gchar *id = nullptr; + if (!strncmp(fragment, "xpointer(", 9)) { + /* FIXME !!! this is wasteful */ + /* FIXME: It looks as though this is including "))" in the id. I suggest moving + the strlen calculation and validity testing to before strdup, and copying just + the id without the "))". -- pjrm */ + if (!strncmp(fragment, "xpointer(id(", 12)) { + id = g_strdup(fragment + 12); + size_t const len = strlen(id); + if (len < 3 || strcmp(id + len - 2, "))")) { + g_free(id); + throw MalformedURIException(); + } + } else { + throw UnsupportedURIException(); + } + } else { + id = g_strdup(fragment); + } + + /* FIXME !!! validate id as an NCName somewhere */ + + _connection.disconnect(); + delete _uri; + _uri = new URI(uri); + + _setObject(document->getObjectById(id)); + _connection = document->connectIdChanged(id, sigc::mem_fun(*this, &URIReference::_setObject)); + g_free(id); +} + +bool URIReference::try_attach(char const *uri) +{ + if (uri && uri[0]) { + try { + attach(Inkscape::URI(uri)); + return true; + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + } + } + detach(); + return false; +} + +void URIReference::detach() +{ + _connection.disconnect(); + delete _uri; + _uri = nullptr; + _setObject(nullptr); +} + +void URIReference::_setObject(SPObject *obj) +{ + if (obj && !_acceptObject(obj)) { + obj = nullptr; + } + + if (obj == _obj) + return; + + SPObject *old_obj = _obj; + _obj = obj; + + _release_connection.disconnect(); + if (_obj && (!_owner || !_owner->cloned)) { + _obj->hrefObject(_owner); + _release_connection = _obj->connectRelease(sigc::mem_fun(*this, &URIReference::_release)); + } + _changed_signal.emit(old_obj, _obj); + if (old_obj && (!_owner || !_owner->cloned)) { + /* release the old object _after_ the signal emission */ + old_obj->unhrefObject(_owner); + } +} + +/* If an object is deleted, current semantics require that we release + * it on its "release" signal, rather than later, when its ID is actually + * unregistered from the document. + */ +void URIReference::_release(SPObject *obj) +{ + g_assert(_obj == obj); + _setObject(nullptr); +} + +} /* namespace Inkscape */ + + + +SPObject *sp_css_uri_reference_resolve(SPDocument *document, const gchar *uri) +{ + SPObject *ref = nullptr; + + if (document && uri && (strncmp(uri, "url(", 4) == 0)) { + auto trimmed = extract_uri(uri); + if (!trimmed.empty()) { + ref = sp_uri_reference_resolve(document, trimmed.c_str()); + } + } + + return ref; +} + +SPObject *sp_uri_reference_resolve(SPDocument *document, const gchar *uri) +{ + SPObject *ref = nullptr; + + if (uri && (*uri == '#')) { + ref = document->getObjectById(uri + 1); + } + + return ref; +} + +/* + 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 : diff --git a/src/object/uri-references.h b/src/object/uri-references.h new file mode 100644 index 0000000..c56213b --- /dev/null +++ b/src/object/uri-references.h @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_URI_REFERENCES_H +#define SEEN_SP_URI_REFERENCES_H + +/* + * Helper methods for resolving URI References + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <vector> +#include <set> +#include <sigc++/connection.h> +#include <sigc++/trackable.h> + +class SPObject; +class SPDocument; + +namespace Inkscape { + +class URI; + +/** + * A class encapsulating a reference to a particular URI; observers can + * be notified when the URI comes to reference a different SPObject. + * + * The URIReference increments and decrements the SPObject's hrefcount + * automatically. + * + * @see SPObject + */ +class URIReference : public sigc::trackable { +public: + /** + * Constructor. + * + * @param owner The object on whose behalf this URIReference + * is holding a reference to the target object. + */ + URIReference(SPObject *owner); + URIReference(SPDocument *owner_document); + + /* Definition-less to prevent accidental use. */ + void operator=(URIReference const& ref) = delete; + + /** + * Destructor. Calls shutdown() if the reference has not been + * shut down yet. + */ + virtual ~URIReference(); + + /** + * Attaches to a URI, relative to the specified document. + * + * Throws a BadURIException if the URI is unsupported, + * or the fragment identifier is xpointer and malformed. + * + * @param uri the URI to watch + */ + void attach(URI const& uri); + + /** + * Try to attach to a URI. Return false if URL is malformed and detach any + * previous attachment. + */ + bool try_attach(char const *uri); + + /** + * Detaches from the currently attached URI target, if any; + * the current referrent is signaled as NULL. + */ + void detach(); + + /** + * @brief Returns a pointer to the current referrent of the + * attached URI, or NULL. + * + * @return a pointer to the referenced SPObject or NULL + */ + SPObject *getObject() const { return _obj; } + + /** + * @brief Returns a pointer to the URIReference's owner + * + * @return a pointer to the URIReference's owner + */ + SPObject *getOwner() const { return _owner; } + + /** + * Accessor for the referrent change notification signal; + * this signal is emitted whenever the URIReference's + * referrent changes. + * + * Signal handlers take two parameters: the old and new + * referrents. + * + * @returns a signal + */ + sigc::signal<void (SPObject *, SPObject *)> changedSignal() { + return _changed_signal; + } + + /** + * Returns a pointer to a URI containing the currently attached + * URI, or NULL if no URI is currently attached. + * + * @returns the currently attached URI, or NULL + */ + URI const* getURI() const { + return _uri; + } + + /** + * Returns true if there is currently an attached URI + * + * @returns true if there is an attached URI + */ + bool isAttached() const { + return (bool)_uri; + } + + SPDocument *getOwnerDocument() { return _owner_document; } + SPObject *getOwnerObject() { return _owner; } + +protected: + virtual bool _acceptObject(SPObject *obj) const; +private: + SPObject *_owner; + SPDocument *_owner_document; + sigc::connection _connection; + sigc::connection _release_connection; + SPObject *_obj; + URI *_uri; + + sigc::signal<void (SPObject *, SPObject *)> _changed_signal; + + void _setObject(SPObject *object); + void _release(SPObject *object); +}; + +} + +/** + * Resolves an item referenced by a URI in CSS form contained in "url(...)" + */ +SPObject* sp_css_uri_reference_resolve( SPDocument *document, const char *uri ); + +SPObject *sp_uri_reference_resolve (SPDocument *document, const char *uri); + +#endif // SEEN_SP_URI_REFERENCES_H + +/* + 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 : diff --git a/src/object/uri.cpp b/src/object/uri.cpp new file mode 100644 index 0000000..05539a6 --- /dev/null +++ b/src/object/uri.cpp @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2003 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "uri.h" + +#include <cstring> + +#include <giomm/contenttype.h> +#include <giomm/file.h> +#include <glibmm/base64.h> +#include <glibmm/convert.h> +#include <glibmm/ustring.h> +#include <glibmm/miscutils.h> + +#include "bad-uri-exception.h" + +namespace Inkscape { + +auto const URI_ALLOWED_NON_ALNUM = "!#$%&'()*+,-./:;=?@_~"; + +/** + * Return true if the given URI string contains characters that need escaping. + * + * Note: It does not check if valid characters appear in invalid context (e.g. + * '%' not followed by two hex digits). + */ +static bool uri_needs_escaping(char const *uri) +{ + for (auto *p = uri; *p; ++p) { + if (!g_ascii_isalnum(*p) && !strchr(URI_ALLOWED_NON_ALNUM, *p)) { + return true; + } + } + return false; +} + +URI::URI() { + init(xmlCreateURI()); +} + +URI::URI(gchar const *preformed, char const *baseuri) +{ + xmlURIPtr uri; + if (!preformed) { + throw MalformedURIException(); + } + + // check for invalid characters, escape if needed + xmlChar *escaped = nullptr; + if (uri_needs_escaping(preformed)) { + escaped = xmlURIEscapeStr( // + (xmlChar const *)preformed, // + (xmlChar const *)URI_ALLOWED_NON_ALNUM); + preformed = (decltype(preformed))escaped; + } + + // make absolute + xmlChar *full = nullptr; + if (baseuri) { + full = xmlBuildURI( // + (xmlChar const *)preformed, // + (xmlChar const *)baseuri); +#if LIBXML_VERSION < 20905 + // libxml2 bug: "file:/some/file" instead of "file:///some/file" + auto f = (gchar const *)full; + if (f && g_str_has_prefix(f, "file:/") && f[6] != '/') { + auto fixed = std::string(f, 6) + "//" + std::string(f + 6); + xmlFree(full); + full = (xmlChar *)xmlMemStrdup(fixed.c_str()); + } +#endif + preformed = (decltype(preformed))full; + } + + uri = xmlParseURI(preformed); + + if (full) { + xmlFree(full); + } + if (escaped) { + xmlFree(escaped); + } + if (!uri) { + throw MalformedURIException(); + } + init(uri); +} + +URI::URI(char const *preformed, URI const &baseuri) + : URI::URI(preformed, baseuri.str().c_str()) +{ +} + +// From RFC 2396: +// +// URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] +// absoluteURI = scheme ":" ( hier_part | opaque_part ) +// relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] +// +// hier_part = ( net_path | abs_path ) [ "?" query ] +// opaque_part = uric_no_slash *uric +// +// uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" | +// "&" | "=" | "+" | "$" | "," +// +// net_path = "//" authority [ abs_path ] +// abs_path = "/" path_segments +// rel_path = rel_segment [ abs_path ] +// +// rel_segment = 1*( unreserved | escaped | +// ";" | "@" | "&" | "=" | "+" | "$" | "," ) +// +// authority = server | reg_name + +bool URI::isOpaque() const { + return getOpaque() != nullptr; +} + +bool URI::isRelative() const { + return !_xmlURIPtr()->scheme; +} + +bool URI::isNetPath() const { + return isRelative() && _xmlURIPtr()->server; +} + +bool URI::isRelativePath() const { + if (isRelative() && !_xmlURIPtr()->server) { + const gchar *path = getPath(); + return path && path[0] != '/'; + } + return false; +} + +bool URI::isAbsolutePath() const { + if (isRelative() && !_xmlURIPtr()->server) { + const gchar *path = getPath(); + return path && path[0] == '/'; + } + return false; +} + +const gchar *URI::getScheme() const { + return (gchar *)_xmlURIPtr()->scheme; +} + +const gchar *URI::getPath() const { + return (gchar *)_xmlURIPtr()->path; +} + +const gchar *URI::getQuery() const { + return (gchar *)_xmlURIPtr()->query; +} + +const gchar *URI::getFragment() const { + return (gchar *)_xmlURIPtr()->fragment; +} + +const gchar *URI::getOpaque() const { + if (!isRelative() && !_xmlURIPtr()->server) { + const gchar *path = getPath(); + if (path && path[0] != '/') { + return path; + } + } + return nullptr; +} + +std::string URI::toNativeFilename() const +{ // + auto uristr = str(); + + // remove fragment identifier + if (getFragment() != nullptr) { + uristr.resize(uristr.find('#')); + } + + return Glib::filename_from_uri(uristr); +} + +/* TODO !!! proper error handling */ +URI URI::from_native_filename(gchar const *path) { + gchar *uri = g_filename_to_uri(path, nullptr, nullptr); + URI result(uri); + g_free( uri ); + return result; +} + +URI URI::from_dirname(gchar const *path) +{ + std::string pathstr = path ? path : "."; + + if (!Glib::path_is_absolute(pathstr)) { + pathstr = Glib::build_filename(Glib::get_current_dir(), pathstr); + } + + auto uristr = Glib::filename_to_uri(pathstr); + + if (uristr[uristr.size() - 1] != '/') { + uristr.push_back('/'); + } + + return URI(uristr.c_str()); +} + +URI URI::from_href_and_basedir(char const *href, char const *basedir) +{ + try { + return URI(href, URI::from_dirname(basedir)); + } catch (...) { + return URI(); + } +} + +/** + * Replacement for buggy xmlBuildRelativeURI + * https://gitlab.gnome.org/GNOME/libxml2/merge_requests/12 + * + * Special case: Don't cross filesystem root, e.g. drive letter on Windows. + * This is an optimization to keep things practical, it's not required for correctness. + * + * @param uri an absolute URI + * @param base an absolute URI without any ".." path segments + * @return relative URI if possible, otherwise @a uri unchanged + */ +static std::string build_relative_uri(char const *uri, char const *base) +{ + size_t n_slash = 0; + size_t i = 0; + + // find longest common prefix + for (; uri[i]; ++i) { + if (uri[i] != base[i]) { + break; + } + + if (uri[i] == '/') { + ++n_slash; + } + } + + // URIs must share protocol://server/ + if (n_slash < 3) { + return uri; + } + + // Don't cross filesystem root + if (n_slash == 3 && g_str_has_prefix(base, "file:///") && base[8]) { + return uri; + } + + std::string relative; + + for (size_t j = i; base[j]; ++j) { + if (base[j] == '/') { + relative += "../"; + } + } + + while (uri[i - 1] != '/') { + --i; + } + + relative += (uri + i); + + if (relative.empty() && base[i]) { + relative = "./"; + } + + return relative; +} + +std::string URI::str(char const *baseuri) const +{ + std::string s; + auto saveuri = xmlSaveUri(_xmlURIPtr()); + if (saveuri) { + auto save = (const char *)saveuri; + if (baseuri && baseuri[0]) { + s = build_relative_uri(save, baseuri); + } else { + s = save; + } + xmlFree(saveuri); + } + return s; +} + +std::string URI::getMimeType() const +{ + const char *path = getPath(); + + if (path) { + if (hasScheme("data")) { + for (const char *p = path; *p; ++p) { + if (*p == ';' || *p == ',') { + return std::string(path, p); + } + } + } else { + bool uncertain; + auto type = Gio::content_type_guess(path, nullptr, 0, uncertain); + return Gio::content_type_get_mime_type(type).raw(); + } + } + + return "unknown/unknown"; +} + +std::string URI::getContents() const +{ + if (hasScheme("data")) { + // handle data URIs + + const char *p = getPath(); + const char *tok = nullptr; + + // scan "[<media type>][;base64]," header + for (; *p && *p != ','; ++p) { + if (*p == ';') { + tok = p + 1; + } + } + + // body follows after comma + if (*p != ',') { + g_critical("data URI misses comma"); + } else if (tok && strncmp("base64", tok, p - tok) == 0) { + // base64 encoded body + return Glib::Base64::decode(p + 1); + } else { + // raw body + return p + 1; + } + } else { + // handle non-data URIs with GVfs + auto file = Gio::File::create_for_uri(str()); + + gsize length = 0; + char *buffer = nullptr; + + if (file->load_contents(buffer, length)) { + auto contents = std::string(buffer, buffer + length); + g_free(buffer); + return contents; + } else { + g_critical("failed to load contents from %.100s", str().c_str()); + } + } + + return ""; +} + +bool URI::hasScheme(const char *scheme) const +{ + const char *s = getScheme(); + return s && g_ascii_strcasecmp(s, scheme) == 0; +} + +/** + * If \c s starts with a "%XX" triplet, return its byte value, 0 otherwise. + */ +static int uri_unescape_triplet(const char *s) +{ + int H1, H2; + + if (s[0] == '%' // + && (H1 = g_ascii_xdigit_value(s[1])) != -1 // + && (H2 = g_ascii_xdigit_value(s[2])) != -1) { + return (H1 << 4) | H2; + } + + return 0; +} + +/** + * If \c s starts with a percent-escaped UTF-8 sequence, unescape one code + * point and store it in \c out variable. Do nothing and return 0 if \c s + * doesn't start with UTF-8. + * + * @param[in] s percent-escaped string + * @param[out] out out-buffer, must have at least size 5 + * @return number of bytes read from \c s + */ +static int uri_unescape_utf8_codepoint(const char *s, char *out) +{ + int n = 0; + int value = uri_unescape_triplet(s); + + if ((value >> 5) == /* 0b110 */ 0x6) { + // 110xxxxx 10xxxxxx + n = 2; + } else if ((value >> 4) == /* 0b1110 */ 0xE) { + // 1110xxxx 10xxxxxx 10xxxxxx + n = 3; + } else if ((value >> 3) == /* 0b11110 */ 0x1E) { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + n = 4; + } else { + return 0; + } + + out[0] = value; + out[n] = 0; + + for (int i = 1; i < n; ++i) { + value = uri_unescape_triplet(s + (i * 3)); + + if ((value >> 6) != /* 0b10 */ 0x2) { + return 0; + } + + out[i] = value; + } + + return n * 3; +} + +std::string uri_to_iri(const char *uri) +{ + std::string iri; + + char utf8buf[5]; + + for (const char *p = uri; *p;) { + int n = uri_unescape_utf8_codepoint(p, utf8buf); + if (n) { + iri.append(utf8buf); + p += n; + } else { + iri += *p; + p += 1; + } + } + + return iri; +} + +} // namespace Inkscape + + +/* + 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 : diff --git a/src/object/uri.h b/src/object/uri.h new file mode 100644 index 0000000..381adec --- /dev/null +++ b/src/object/uri.h @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2003 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_URI_H +#define INKSCAPE_URI_H + +#include <libxml/uri.h> +#include <memory> +#include <string> + +namespace Inkscape { + +/** + * Represents an URI as per RFC 2396. + * + * Typical use-cases of this class: + * - converting between relative and absolute URIs + * - converting URIs to/from filenames (alternative: Glib functions, but those only handle absolute paths) + * - generic handling of data/file/http URIs (e.g. URI::getContents and URI::getMimeType) + * + * Wraps libxml2's URI functions. Direct usage of libxml2's C-API is discouraged if favor of + * Inkscape::URI. (At the time of writing this, no de-factor standard C++ URI library exists, so + * wrapping libxml2 seems like a good solution) + * + * Implementation detail: Immutable type, copies share a ref-counted data pointer. + */ +class URI { +public: + + /* Blank constructor */ + URI(); + + /** + * Constructor from a C-style ASCII string. + * + * @param preformed Properly quoted C-style string to be represented. + * @param baseuri If @a preformed is a relative URI, use @a baseuri to make it absolute + * + * @throw MalformedURIException + */ + explicit URI(char const *preformed, char const *baseuri = nullptr); + explicit URI(char const *preformed, URI const &baseuri); + + /** + * Determines if the URI represented is an 'opaque' URI. + * + * @return \c true if the URI is opaque, \c false if hierarchial. + */ + bool isOpaque() const; + + /** + * Determines if the URI represented is 'relative' as per RFC 2396. + * + * Relative URI references are distinguished by not beginning with a + * scheme name. + * + * @return \c true if the URI is relative, \c false if it is absolute. + */ + bool isRelative() const; + + /** + * Determines if the relative URI represented is a 'net-path' as per RFC 2396. + * + * A net-path is one that starts with "//". + * + * @return \c true if the URI is relative and a net-path, \c false otherwise. + */ + bool isNetPath() const; + + /** + * Determines if the relative URI represented is a 'relative-path' as per RFC 2396. + * + * A relative-path is one that starts with no slashes. + * + * @return \c true if the URI is relative and a relative-path, \c false otherwise. + */ + bool isRelativePath() const; + + /** + * Determines if the relative URI represented is a 'absolute-path' as per RFC 2396. + * + * An absolute-path is one that starts with a single "/". + * + * @return \c true if the URI is relative and an absolute-path, \c false otherwise. + */ + bool isAbsolutePath() const; + + /** + * Return the scheme, e.g.\ "http", or \c NULL if this is not an absolute URI. + */ + const char *getScheme() const; + + /** + * Return the path. + * + * Example: "http://host/foo/bar?query#frag" -> "/foo/bar" + * + * For an opaque URI, this is identical to getOpaque() + */ + const char *getPath() const; + + /** + * Return the query, which is the part between "?" and the optional fragment hash ("#") + */ + const char *getQuery() const; + + /** + * Return the fragment, which is everything after "#" + */ + const char *getFragment() const; + + /** + * For an opaque URI, return everything between the scheme colon (":") and the optional + * fragment hash ("#"). For non-opaque URIs, return NULL. + */ + const char *getOpaque() const; + + /** + * Construct a "file" URI from an absolute filename. + */ + static URI from_native_filename(char const *path); + + /** + * URI of a local directory. The URI path will end with a slash. + */ + static URI from_dirname(char const *path); + + /** + * Convenience function for the common use case given a xlink:href attribute and a local + * directory as the document base. Returns an empty URI on failure. + */ + static URI from_href_and_basedir(char const *href, char const *basedir); + + /** + * Convert this URI to a native filename. + * + * Discards the fragment identifier. + * + * @throw Glib::ConvertError If this is not a "file" URI + */ + std::string toNativeFilename() const; + + /** + * Return the string representation of this URI + * + * @param baseuri Return a relative path if this URI shares protocol and host with @a baseuri + */ + std::string str(char const *baseuri = nullptr) const; + + /** + * Get the MIME type (e.g.\ "image/png") + */ + std::string getMimeType() const; + + /** + * Return the contents of the file + * + * @throw Glib::Error If the URL can't be read + */ + std::string getContents() const; + + /** + * Return a CSS formatted url value + * + * @param baseuri Return a relative path if this URI shares protocol and host with @a baseuri + */ + std::string cssStr(char const *baseuri = nullptr) const { + return "url(" + str(baseuri) + ")"; + } + + /** + * True if the scheme equals the given string (not case sensitive) + */ + bool hasScheme(const char *scheme) const; + +private: + std::shared_ptr<xmlURI> m_shared; + + void init(xmlURI *ptr) { m_shared.reset(ptr, xmlFreeURI); } + + xmlURI *_xmlURIPtr() const { return m_shared.get(); } +}; + +/** + * Unescape the UTF-8 parts of the given URI. + * + * Does not decode non-UTF-8 escape sequences (e.g. reserved ASCII characters). + * Does not do any IDN (internationalized domain name) decoding. + * + * @param uri URI or part of a URI + * @return IRI equivalent of \c uri + */ +std::string uri_to_iri(const char *uri); + +} /* namespace Inkscape */ + +#endif + +/* + 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 : diff --git a/src/object/viewbox.cpp b/src/object/viewbox.cpp new file mode 100644 index 0000000..eee0fc5 --- /dev/null +++ b/src/object/viewbox.cpp @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * viewBox helper class, common code used by root, symbol, marker, pattern, image, view + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> (code extracted from symbol.cpp) + * Tavmjong Bah <tavmjong@free.fr> + * Johan Engelen + * + * Copyright (C) 2013-2014 Tavmjong Bah, authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <2geom/transforms.h> + +#include "viewbox.h" +#include "enums.h" +#include "sp-item.h" +#include "svg/stringstream.h" + +#include <map> + +namespace { +std::map<unsigned int, char const *> const ASPECT_ALIGN_STRINGS{ + {SP_ASPECT_NONE, "none"}, // + {SP_ASPECT_XMIN_YMIN, "xMinYMin"}, // + {SP_ASPECT_XMID_YMIN, "xMidYMin"}, // + {SP_ASPECT_XMAX_YMIN, "xMaxYMin"}, // + {SP_ASPECT_XMIN_YMID, "xMinYMid"}, // + {SP_ASPECT_XMID_YMID, "xMidYMid"}, // + {SP_ASPECT_XMAX_YMID, "xMaxYMid"}, // + {SP_ASPECT_XMIN_YMAX, "xMinYMax"}, // + {SP_ASPECT_XMID_YMAX, "xMidYMax"}, // + {SP_ASPECT_XMAX_YMAX, "xMaxYMax"}, // +}; +} + +SPViewBox::SPViewBox() + : viewBox_set(false) + , viewBox() + , aspect_set(false) + , aspect_align(SP_ASPECT_XMID_YMID) // Default per spec + , aspect_clip(SP_ASPECT_MEET) + , c2p(Geom::identity()) +{ +} + +void SPViewBox::set_viewBox(const gchar* value) { + + if (value) { + gchar *eptr = const_cast<gchar*>(value); // const-cast necessary because of const-incorrect interface definition of g_ascii_strtod + + double x = g_ascii_strtod (eptr, &eptr); + + while (*eptr && ((*eptr == ',') || (*eptr == ' '))) { + eptr++; + } + + double y = g_ascii_strtod (eptr, &eptr); + + while (*eptr && ((*eptr == ',') || (*eptr == ' '))) { + eptr++; + } + + double width = g_ascii_strtod (eptr, &eptr); + + while (*eptr && ((*eptr == ',') || (*eptr == ' '))) { + eptr++; + } + + double height = g_ascii_strtod (eptr, &eptr); + + while (*eptr && ((*eptr == ',') || (*eptr == ' '))) { + eptr++; + } + + if ((width > 0) && (height > 0)) { + /* Set viewbox */ + this->viewBox = Geom::Rect::from_xywh(x, y, width, height); + this->viewBox_set = true; + } else { + this->viewBox_set = false; + } + } else { + this->viewBox_set = false; + } + + // The C++ way? -- not necessarily using iostreams + // std::string sv( value ); + // std::replace( sv.begin(), sv.end(), ',', ' '); + // std::stringstream ss( sv ); + // double x, y, width, height; + // ss >> x >> y >> width >> height; +} + +void SPViewBox::set_preserveAspectRatio(const gchar* value) { + + /* Do setup before, so we can use break to escape */ + this->aspect_set = false; + this->aspect_align = SP_ASPECT_XMID_YMID; // Default per spec + this->aspect_clip = SP_ASPECT_MEET; + + if (value) { + const gchar *p = value; + + while (*p && (*p == 32)) { + p += 1; + } + + if (!*p) { + return; + } + + const gchar *e = p; + + while (*e && (*e != 32)) { + e += 1; + } + + int len = e - p; + + if (len > 8) { // Can't have buffer overflow as 8 < 256 + return; + } + + gchar c[256]; + memcpy (c, value, len); + + c[len] = 0; + + /* Now the actual part */ + unsigned int align = SP_ASPECT_NONE; + if (!strcmp (c, "none")) { + align = SP_ASPECT_NONE; + } else if (!strcmp (c, "xMinYMin")) { + align = SP_ASPECT_XMIN_YMIN; + } else if (!strcmp (c, "xMidYMin")) { + align = SP_ASPECT_XMID_YMIN; + } else if (!strcmp (c, "xMaxYMin")) { + align = SP_ASPECT_XMAX_YMIN; + } else if (!strcmp (c, "xMinYMid")) { + align = SP_ASPECT_XMIN_YMID; + } else if (!strcmp (c, "xMidYMid")) { + align = SP_ASPECT_XMID_YMID; + } else if (!strcmp (c, "xMaxYMid")) { + align = SP_ASPECT_XMAX_YMID; + } else if (!strcmp (c, "xMinYMax")) { + align = SP_ASPECT_XMIN_YMAX; + } else if (!strcmp (c, "xMidYMax")) { + align = SP_ASPECT_XMID_YMAX; + } else if (!strcmp (c, "xMaxYMax")) { + align = SP_ASPECT_XMAX_YMAX; + } else { + return; + } + + unsigned int clip = SP_ASPECT_MEET; + + while (*e && (*e == 32)) { + e += 1; + } + + if (*e) { + if (!strcmp (e, "meet")) { + clip = SP_ASPECT_MEET; + } else if (!strcmp (e, "slice")) { + clip = SP_ASPECT_SLICE; + } else { + return; + } + } + + this->aspect_set = true; + this->aspect_align = align; + this->aspect_clip = clip; + } +} + +// Apply scaling from viewbox +void SPViewBox::apply_viewbox(const Geom::Rect& in, double scale_none) { + + /* Determine actual viewbox in viewport coordinates */ + // scale_none is the scale that would apply if the viewbox and page size are same size + // it is passed here because it is a double-precision variable, while 'in' is originally float + double x = 0.0; + double y = 0.0; + double scale_x = in.width() / this->viewBox.width(); + double scale_y = in.height() / this->viewBox.height(); + double scale_uniform = 1.0; // used only if scaling is uniform + + if (Geom::are_near(scale_x / scale_y, 1.0, Geom::EPSILON)) { + // scaling is already uniform, reduce numerical error + scale_uniform = (scale_x + scale_y)/2.0; + if (Geom::are_near(scale_uniform / scale_none, 1.0, Geom::EPSILON)) + scale_uniform = scale_none; // objects are same size, reduce numerical error + scale_x = scale_uniform; + scale_y = scale_uniform; + } else if (this->aspect_align != SP_ASPECT_NONE) { + // scaling is not uniform, but force it to be + scale_uniform = (this->aspect_clip == SP_ASPECT_MEET) ? MIN (scale_x, scale_y) : MAX (scale_x, scale_y); + scale_x = scale_uniform; + scale_y = scale_uniform; + double width = this->viewBox.width() * scale_uniform; + double height = this->viewBox.height() * scale_uniform; + + /* Now place viewbox to requested position */ + switch (this->aspect_align) { + case SP_ASPECT_XMIN_YMIN: + break; + case SP_ASPECT_XMID_YMIN: + x = 0.5 * (in.width() - width); + break; + case SP_ASPECT_XMAX_YMIN: + x = 1.0 * (in.width() - width); + break; + case SP_ASPECT_XMIN_YMID: + y = 0.5 * (in.height() - height); + break; + case SP_ASPECT_XMID_YMID: + x = 0.5 * (in.width() - width); + y = 0.5 * (in.height() - height); + break; + case SP_ASPECT_XMAX_YMID: + x = 1.0 * (in.width() - width); + y = 0.5 * (in.height() - height); + break; + case SP_ASPECT_XMIN_YMAX: + y = 1.0 * (in.height() - height); + break; + case SP_ASPECT_XMID_YMAX: + x = 0.5 * (in.width() - width); + y = 1.0 * (in.height() - height); + break; + case SP_ASPECT_XMAX_YMAX: + x = 1.0 * (in.width() - width); + y = 1.0 * (in.height() - height); + break; + default: + break; + } + } + + /* Viewbox transform from scale and position */ + Geom::Affine q; + q[0] = scale_x; + q[1] = 0.0; + q[2] = 0.0; + q[3] = scale_y; + q[4] = x - scale_x * this->viewBox.left(); + q[5] = y - scale_y * this->viewBox.top(); + + // std::cout << " q\n" << q << std::endl; + + /* Append viewbox transformation */ + this->c2p = q * this->c2p; +} + +SPItemCtx SPViewBox::get_rctx(const SPItemCtx* ictx, double scale_none) { + + /* Create copy of item context */ + SPItemCtx rctx = *ictx; + + /* Calculate child to parent transformation */ + /* Apply parent translation (set up as viewport) */ + this->c2p = Geom::Translate(rctx.viewport.min()); + + if (this->viewBox_set) { + // Adjusts c2p for viewbox + apply_viewbox( rctx.viewport, scale_none ); + } + + rctx.i2doc = this->c2p * rctx.i2doc; + + /* If viewBox is set initialize child viewport */ + /* Otherwise it is already correct */ + if (this->viewBox_set) { + rctx.viewport = this->viewBox; + rctx.i2vp = Geom::identity(); + } + + return rctx; +} + +/** + * Write viewBox attribute to XML, if set. + */ +void SPViewBox::write_viewBox(Inkscape::XML::Node *repr) const +{ + if (viewBox_set) { + Inkscape::SVGOStringStream os; + os << viewBox.left() << " " // + << viewBox.top() << " " // + << viewBox.width() << " " // + << viewBox.height(); + + repr->setAttribute("viewBox", os.str()); + } +} + +/** + * Write preserveAspectRatio attribute to XML, if set. + */ +void SPViewBox::write_preserveAspectRatio(Inkscape::XML::Node *repr) const +{ + if (aspect_set) { + std::string aspect = ASPECT_ALIGN_STRINGS.at(aspect_align); + + if (aspect_clip == SP_ASPECT_SLICE) { + aspect += " slice"; + } + + repr->setAttribute("preserveAspectRatio", aspect); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/viewbox.h b/src/object/viewbox.h new file mode 100644 index 0000000..bf516a5 --- /dev/null +++ b/src/object/viewbox.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_VIEWBOX_H__ +#define __SP_VIEWBOX_H__ + +/* + * viewBox helper class, common code used by root, symbol, marker, pattern, image, view + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> (code extracted from sp-symbol.h) + * Tavmjong Bah + * Johan Engelen + * + * Copyright (C) 2013-2014 Tavmjong Bah, authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <2geom/rect.h> +#include <glib.h> + +namespace Inkscape::XML { +class Node; +} // namespace Inkscape::XML + +class SPItemCtx; + +class SPViewBox { + +public: + SPViewBox(); + + /* viewBox; */ + bool viewBox_set; + Geom::Rect viewBox; // Could use optrect + + /* preserveAspectRatio */ + bool aspect_set; + unsigned int aspect_align; // enum + unsigned int aspect_clip; // enum + + /* Child to parent additional transform */ + Geom::Affine c2p; + + void set_viewBox(const gchar* value); + void set_preserveAspectRatio(const gchar* value); + void write_viewBox(Inkscape::XML::Node *repr) const; + void write_preserveAspectRatio(Inkscape::XML::Node *repr) const; + + /* Adjusts c2p for viewbox */ + void apply_viewbox(const Geom::Rect& in, double scale_none = 1.0); + + SPItemCtx get_rctx( const SPItemCtx* ictx, double scale_none = 1.0); + +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/weakptr.h b/src/object/weakptr.h new file mode 100644 index 0000000..38db7e7 --- /dev/null +++ b/src/object/weakptr.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_OBJECT_WEAKPTR_H +#define INKSCAPE_OBJECT_WEAKPTR_H + +#include <sigc++/connection.h> + +namespace Inkscape { + +/** + * A weak pointer to an SPObject: it nulls itself upon the object's destruction. + */ +template <typename T> +class SPWeakPtr final +{ +public: + SPWeakPtr() = default; + explicit SPWeakPtr(T *obj) : _obj(obj) { attach(); } + SPWeakPtr &operator=(T *obj) { reset(obj); return *this; } + SPWeakPtr(SPWeakPtr const &other) : SPWeakPtr(other._obj) {} + SPWeakPtr &operator=(SPWeakPtr const &other) { reset(other._obj); return *this; } + ~SPWeakPtr() { detach(); } + + void reset() { detach(); _obj = nullptr; } + void reset(T *obj) { detach(); _obj = obj; attach(); } + explicit operator bool() const { return _obj; } + T *get() const { return _obj; } + T &operator*() const { return *_obj; } + T *operator->() const { return _obj; } + +private: + T *_obj = nullptr; + sigc::connection _conn; + + void attach() { if (_obj) _conn = _obj->connectRelease([this] (auto) { _conn.disconnect(); _obj = nullptr; }); } + void detach() { if (_obj) _conn.disconnect(); } +}; + +} // namespace Inkscape + +#endif // INKSCAPE_OBJECT_WEAKPTR_H |