summaryrefslogtreecommitdiffstats
path: root/src/display/drawing-pattern.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/display/drawing-pattern.cpp304
1 files changed, 304 insertions, 0 deletions
diff --git a/src/display/drawing-pattern.cpp b/src/display/drawing-pattern.cpp
new file mode 100644
index 0000000..fb21cbc
--- /dev/null
+++ b/src/display/drawing-pattern.cpp
@@ -0,0 +1,304 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Canvas belonging to SVG pattern.
+ *//*
+ * Authors:
+ * Tomasz Boczkowski <penginsbacon@gmail.com>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cairomm/region.h>
+#include "cairo-utils.h"
+#include "drawing-context.h"
+#include "drawing-pattern.h"
+#include "drawing-surface.h"
+#include "drawing.h"
+#include "helper/geom.h"
+#include "ui/util.h"
+
+namespace Inkscape {
+
+DrawingPattern::Surface::Surface(Geom::IntRect const &rect, int device_scale)
+ : rect(rect)
+ , surface(Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, rect.width() * device_scale, rect.height() * device_scale))
+{
+ cairo_surface_set_device_scale(surface->cobj(), device_scale, device_scale);
+}
+
+DrawingPattern::DrawingPattern(Drawing &drawing)
+ : DrawingGroup(drawing)
+ , _overflow_steps(1)
+{
+}
+
+void DrawingPattern::setPatternToUserTransform(Geom::Affine const &transform)
+{
+ defer([=] {
+ auto constexpr EPS = 1e-18;
+ auto current = _pattern_to_user ? *_pattern_to_user : Geom::identity();
+ if (Geom::are_near(transform, current, EPS)) return;
+ _markForRendering();
+ _pattern_to_user = transform.isIdentity(EPS) ? nullptr : std::make_unique<Geom::Affine>(transform);
+ _markForUpdate(STATE_ALL, true);
+ });
+}
+
+void DrawingPattern::setTileRect(Geom::Rect const &tile_rect)
+{
+ defer([=] {
+ _tile_rect = tile_rect;
+ _markForUpdate(STATE_ALL, true);
+ });
+}
+
+void DrawingPattern::setOverflow(Geom::Affine const &initial_transform, int steps, Geom::Affine const &step_transform)
+{
+ defer([=] {
+ _overflow_initial_transform = initial_transform;
+ _overflow_steps = steps;
+ _overflow_step_transform = step_transform;
+ });
+}
+
+cairo_pattern_t *DrawingPattern::renderPattern(RenderContext &rc, Geom::IntRect const &area, float opacity, int device_scale) const
+{
+ if (opacity < 1e-3) {
+ // Invisible.
+ return nullptr;
+ }
+
+ if (!_tile_rect || _tile_rect->hasZeroArea()) {
+ // Empty.
+ return nullptr;
+ }
+
+ // Calculate various transforms.
+ auto const dt = Geom::Translate(-_tile_rect->min()) * Geom::Scale(_pattern_resolution / _tile_rect->dimensions()); // AKA user_to_tile.
+ auto const idt = dt.inverse();
+ auto const pattern_to_tile = _pattern_to_user ? _pattern_to_user->inverse() * dt : dt;
+ auto const screen_to_tile = _ctm.inverse() * pattern_to_tile;
+
+ // Return a canonical choice of rectangle with the same periodic tiling as rect.
+ auto canonicalised = [&, this] (Geom::IntRect rect) {
+ for (int i = 0; i < 2; i++) {
+ if (rect.dimensions()[i] >= _pattern_resolution[i]) {
+ rect[i] = {0, _pattern_resolution[i]};
+ } else {
+ rect[i] -= Util::rounddown(rect[i].min(), _pattern_resolution[i]);
+ }
+ }
+ return rect;
+ };
+
+ // Return whether the periodic tiling of a contains the periodic tiling of b.
+ auto wrapped_contains = [&] (Geom::IntRect const &a, Geom::IntRect const &b) {
+ auto check = [&] (int i) {
+ int const period = _pattern_resolution[i];
+ if (a[i].extent() >= period) return true;
+ if (b[i].extent() > a[i].extent()) return false;
+ return Util::rounddown(b[i].min() - a[i].min(), period) >= b[i].max() - a[i].max();
+ };
+ return check(0) && check(1);
+ };
+
+ // Return whether the periodic tiling of a intersects with or touches the periodic tiling of b.
+ auto wrapped_touches = [&] (Geom::IntRect const &a, Geom::IntRect const &b) {
+ auto check = [&] (int i) {
+ int const period = _pattern_resolution[i];
+ if (a[i].extent() >= period) return true;
+ if (b[i].extent() >= period) return true;
+ return Util::rounddown(b[i].max() - a[i].min(), period) >= b[i].min() - a[i].max();
+ };
+ return check(0) && check(1);
+ };
+
+ // Calculate the minimum and maximum translates of a that overlap with b.
+ auto overlapping_translates = [&, this] (Geom::IntRect const &a, Geom::IntRect const &b) {
+ Geom::IntPoint min, max;
+ for (int i = 0; i < 2; i++) {
+ min[i] = Util::roundup (b[i].min() - a[i].max() + 1, _pattern_resolution[i]);
+ max[i] = Util::rounddown(b[i].max() - a[i].min() - 1, _pattern_resolution[i]);
+ }
+ return std::make_pair(min, max);
+ };
+
+ // Paint the periodic tiling of a into b, and remove the painted region from dirty.
+ auto wrapped_paint = [&, this] (Surface const &a, Geom::IntRect &b, Cairo::RefPtr<Cairo::Context> const &cr, Cairo::RefPtr<Cairo::Region> const &dirty) {
+ auto const [min, max] = overlapping_translates(a.rect, b);
+ for (int x = min.x(); x <= max.x(); x += _pattern_resolution.x()) {
+ for (int y = min.y(); y <= max.y(); y += _pattern_resolution.y()) {
+ auto const rect = a.rect + Geom::IntPoint(x, y);
+ dirty->subtract(geom_to_cairo(rect));
+ cr->set_source(a.surface, rect.left(), rect.top());
+ cr->paint();
+ }
+ }
+ };
+
+ // Calculate the requested area to draw within tile rasterisation space.
+ auto const area_orig = (Geom::Rect(area) * screen_to_tile).roundOutwards();
+ auto const area_tile = canonicalised(area_orig);
+
+ // Simplest solution for now to protecting pattern cache is a mutex. This makes all
+ // pattern rendering single-threaded, however patterns are typically not the bottleneck.
+ auto lock = std::lock_guard(mutables);
+
+ auto get_surface = [&, this] () -> std::pair<Surface*, Cairo::RefPtr<Cairo::Region>> {
+ // If there is a rectangle containing the requested area, just use that.
+ for (auto &s : surfaces) {
+ if (wrapped_contains(s.rect, area_tile)) {
+ return { &s, {} };
+ }
+ }
+
+ // Otherwise, recursively merge the requested area with all overlapping or touching rectangles, and paint the missing part.
+ std::vector<Surface> merged;
+ auto expanded = area_tile;
+
+ while (true) {
+ bool modified = false;
+
+ for (auto it = surfaces.begin(); it != surfaces.end(); ) {
+ if (wrapped_touches(expanded, it->rect)) {
+ expanded.unionWith(it->rect + rounddown(expanded.max() - it->rect.min(), _pattern_resolution));
+ merged.emplace_back(std::move(*it));
+ *it = std::move(surfaces.back());
+ surfaces.pop_back();
+ modified = true;
+ } else {
+ ++it;
+ }
+ }
+
+ if (!modified) break;
+ }
+
+ // Canonicalise the expanded rectangle. (Stops Cairo's coordinates overflowing and the pattern disappearing.)
+ expanded = canonicalised(expanded);
+
+ // Create a new surface covering the expanded rectangle.
+ auto surface = Surface(expanded, device_scale);
+ auto cr = Cairo::Context::create(surface.surface);
+ cr->translate(-surface.rect.left(), -surface.rect.top());
+
+ // Paste all the old surfaces into the new surface, tracking the remaining dirty region.
+ auto dirty = Cairo::Region::create(geom_to_cairo(expanded));
+
+ for (auto &m : merged) {
+ wrapped_paint(m, expanded, cr, dirty);
+ }
+
+ // Emplace the surface, and return it along with the remaining dirty region.
+ surfaces.emplace_back(std::move(surface));
+ return std::make_pair(&surfaces.back(), std::move(dirty));
+ };
+
+ // Find an already-drawn surface containing the requested area, or create if it none exists.
+ auto [surface, dirty] = get_surface();
+
+ // Draw the pattern contents to the dirty areas of the surface, taking care of possible wrapping.
+ Inkscape::DrawingContext dc(surface->surface->cobj(), surface->rect.min());
+
+ auto paint = [&, this] (Geom::IntRect const &rect) {
+ if (_overflow_steps == 1) {
+ render(dc, rc, rect);
+ } else {
+ // Overflow transforms need to be transformed to the old coordinate system
+ // before stretching to the pattern resolution.
+ auto const initial_transform = idt * _overflow_initial_transform * dt;
+ auto const step_transform = idt * _overflow_step_transform * dt;
+ dc.transform(initial_transform);
+ for (int i = 0; i < _overflow_steps; i++) {
+ // render() fails to handle transforms applied here when using cache.
+ render(dc, rc, rect, RENDER_BYPASS_CACHE);
+ dc.transform(step_transform);
+ // auto raw = pattern_surface.raw();
+ // auto filename = "drawing-pattern" + std::to_string(i) + ".png";
+ // cairo_surface_write_to_png(pattern_surface.raw(), filename.c_str());
+ }
+ }
+ };
+
+ if (dirty) {
+ for (int i = 0; i < dirty->get_num_rectangles(); i++) {
+ auto const rect = cairo_to_geom(dirty->get_rectangle(i));
+ for (int x = 0; x <= 1; x++) {
+ for (int y = 0; y <= 1; y++) {
+ auto const wrap = _pattern_resolution * Geom::IntPoint(x, y);
+ auto const rect2 = rect & Geom::IntRect(wrap, wrap + _pattern_resolution);
+ if (!rect2) continue;
+ auto save = DrawingContext::Save(dc);
+ // Clip to rectangle to be drawn.
+ dc.rectangle(*rect2);
+ dc.clip();
+ // Draw the pattern.
+ dc.translate(wrap);
+ paint(*rect2 - wrap);
+ // Apply opacity, if necessary.
+ if (opacity < 1.0 - 1e-3) {
+ dc.setOperator(CAIRO_OPERATOR_DEST_IN);
+ dc.setSource(0.0, 0.0, 0.0, opacity);
+ dc.paint();
+ }
+ }
+ }
+ }
+ dirty.clear();
+ }
+
+ // Debug: Show pattern tile.
+ // surface->surface->write_to_png("/tmp/patternsurface.png");
+
+ // Create and return pattern.
+ auto cp = cairo_pattern_create_for_surface(surface->surface->cobj());
+ auto const shift = surface->rect.min() + rounddown(area_orig.min() - surface->rect.min(), _pattern_resolution);
+ ink_cairo_pattern_set_matrix(cp, pattern_to_tile * Geom::Translate(-shift));
+ cairo_pattern_set_extend(cp, CAIRO_EXTEND_REPEAT);
+ return cp;
+}
+
+unsigned DrawingPattern::_updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset)
+{
+ _dropPatternCache();
+
+ if (!_tile_rect || _tile_rect->hasZeroArea()) {
+ return STATE_NONE;
+ }
+
+ // Calculate the desired resolution of a pattern tile.
+ double const det_ctm = ctx.ctm.det();
+ double const det_ps2user = _pattern_to_user ? _pattern_to_user->det() : 1.0;
+ double scale = std::sqrt(std::abs(det_ctm * det_ps2user));
+ // Fixme: When scale is too big (zooming in a pattern), Cairo doesn't render the pattern.
+ // More precisely it fails when setting pattern matrix in DrawingPattern::renderPattern.
+ // Correct solution should make use of visible area and change pattern tile rect accordingly.
+ auto const c = _tile_rect->dimensions() * scale;
+ _pattern_resolution = c.ceil();
+
+ // Map tile rect to the origin and stretch it to the desired resolution.
+ auto const dt = Geom::Translate(-_tile_rect->min()) * Geom::Scale(_pattern_resolution / _tile_rect->dimensions());
+
+ // Apply this transform to the actual pattern tree.
+ return DrawingGroup::_updateItem(Geom::IntRect::infinite(), { dt }, flags, reset);
+}
+
+void DrawingPattern::_dropPatternCache()
+{
+ surfaces.clear();
+}
+
+} // 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 :