1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
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 :
|