Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
stores.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
5/*
6 * Copyright (C) 2022 PBS <pbs3141@gmail.com>
7 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
8 */
9
10#include "stores.h"
11
12#include <algorithm>
13#include <array>
14#include <cmath>
15#include <iostream>
16#include <utility>
17#include <vector>
18#include <2geom/transforms.h>
19#include <2geom/parallelogram.h>
20#include <2geom/point.h>
21#include <2geom/convex-hull.h>
22
23#include "graphics.h"
24#include "helper/geom.h"
25#include "prefs.h"
26
27namespace Inkscape::UI::Widget {
28
29namespace {
30
31// Determine whether an affine transformation approximately maps the unit square [0, 1]^2 to itself.
32bool preserves_unitsquare(Geom::Affine const &affine)
33{
34 return approx_dihedral(Geom::Translate(0.5, 0.5) * affine * Geom::Translate(-0.5, -0.5));
35}
36
37// Apply an affine transformation to a region, then return a strictly smaller region approximating it, made from chunks of size roughly d.
38// To reduce computation, only the intersection of the result with bounds will be valid.
39auto region_affine_approxinwards(Cairo::RefPtr<Cairo::Region> const &reg, Geom::Affine const &affine, Geom::IntRect const &bounds, int d = 200)
40{
41 // Trivial empty case.
42 if (reg->empty()) return Cairo::Region::create();
43
44 // Trivial identity case.
45 if (affine.isIdentity(0.001)) return reg->copy();
46
47 // Fast-path for rectilinear transformations.
48 if (affine.withoutTranslation().isScale(0.001)) {
49 auto regdst = Cairo::Region::create();
50
51 auto transform = [&] (const Geom::IntPoint &p) {
52 return (Geom::Point(p) * affine).round();
53 };
54
55 for (int i = 0; i < reg->get_num_rectangles(); i++) {
56 auto rect = cairo_to_geom(reg->get_rectangle(i));
57 regdst->do_union(geom_to_cairo(Geom::IntRect(transform(rect.min()), transform(rect.max()))));
58 }
59
60 return regdst;
61 }
62
63 // General case.
64 auto ext = cairo_to_geom(reg->get_extents());
65 auto rectdst = ((Geom::Parallelogram(ext) * affine).bounds().roundOutwards() & bounds).regularized();
66 if (!rectdst) return Cairo::Region::create();
67 auto rectsrc = (Geom::Parallelogram(*rectdst) * affine.inverse()).bounds().roundOutwards();
68
69 auto regdst = Cairo::Region::create(geom_to_cairo(*rectdst));
70 auto regsrc = Cairo::Region::create(geom_to_cairo(rectsrc));
71 regsrc->subtract(reg);
72
73 double fx = min(absolute(Geom::Point(1.0, 0.0) * affine.withoutTranslation()));
74 double fy = min(absolute(Geom::Point(0.0, 1.0) * affine.withoutTranslation()));
75
76 for (int i = 0; i < regsrc->get_num_rectangles(); i++) {
77 auto rect = cairo_to_geom(regsrc->get_rectangle(i));
78 int nx = std::ceil(rect.width() * fx / d);
79 int ny = std::ceil(rect.height() * fy / d);
80 auto pt = [&] (int x, int y) {
81 return rect.min() + (rect.dimensions() * Geom::IntPoint(x, y)) / Geom::IntPoint(nx, ny);
82 };
83 for (int x = 0; x < nx; x++) {
84 for (int y = 0; y < ny; y++) {
85 auto r = Geom::IntRect(pt(x, y), pt(x + 1, y + 1));
86 auto r2 = (Geom::Parallelogram(r) * affine).bounds().roundOutwards();
87 regdst->subtract(geom_to_cairo(r2));
88 }
89 }
90 }
91
92 return regdst;
93}
94
95} // namespace
96
98{
99 // Return the visible region of the view, plus the prerender and padding margins.
101}
102
104{
105 // Recreate the store at the view's affine.
106 _store.affine = view.affine;
107 _store.rect = centered(view);
108 _store.drawn = Cairo::Region::create();
109 // Tell the graphics to create a blank new store.
111}
112
114{
115 // Create a new fragment centred on the viewport.
116 auto rect = centered(view);
117 // Tell the graphics to copy the drawn part of the old store to the new store.
119 // Set the shifted store as the new store.
120 _store.rect = rect;
121 // Clip the drawn region to the new store.
122 _store.drawn->intersect(geom_to_cairo(_store.rect));
123};
124
126{
127 // Copy the store to the snapshot, leaving us temporarily in an invalid state.
128 _snapshot = std::move(_store);
129 // Tell the graphics to do the same, except swapping them so we can re-use the old snapshot store.
131 // Reset the store.
132 recreate_store(view);
133 // Transform the snapshot's drawn region to the new store's affine.
134 _snapshot.drawn = shrink_region(region_affine_approxinwards(_snapshot.drawn, _snapshot.affine.inverse() * _store.affine, _store.rect), 4, -2);
135}
136
138{
139 // Add the drawn region to the snapshot drawn region (they both exist in store space, so this is valid), and save its affine.
140 _snapshot.drawn->do_union(_store.drawn);
141 auto old_store_affine = _store.affine;
142
143 // Get the list of corner points in the store's drawn region and the snapshot bounds rect, all at the view's affine.
144 std::vector<Geom::Point> pts;
145 auto add_rect = [&, this] (Geom::Parallelogram const &pl) {
146 for (int i = 0; i < 4; i++) {
147 pts.emplace_back(Geom::Point(pl.corner(i)));
148 }
149 };
150 auto add_store = [&, this] (Store const &s) {
151 int nrects = s.drawn->get_num_rectangles();
152 auto affine = s.affine.inverse() * view.affine;
153 for (int i = 0; i < nrects; i++) {
154 add_rect(Geom::Parallelogram(cairo_to_geom(s.drawn->get_rectangle(i))) * affine);
155 }
156 };
157 add_store(_store);
159
160 // Compute their minimum-area bounding box as a fragment - an (affine, rect) pair.
161 auto const [rot, optrect] = Geom::ConvexHull(pts).minAreaRotation();
162 auto rect = *optrect; // non-empty since pts is non-empty
163 auto affine = view.affine * rot;
164
165 // Check if the paste transform takes the snapshot store exactly onto the new fragment, possibly with a dihedral transformation.
169 * affine
170 * Geom::Translate(-rect.min())
171 * Geom::Scale(rect.dimensions()).inverse();
172 if (preserves_unitsquare(paste)) {
173 // If so, simply take the new fragment to be exactly the same as the snapshot store.
174 rect = _snapshot.rect;
175 affine = _snapshot.affine;
176 }
177
178 // Compute the scale difference between the backing store and the new fragment, giving the amount of detail that would be lost by pasting.
179 if ( double scale_ratio = std::sqrt(std::abs(_store.affine.det() / affine.det()));
180 scale_ratio > 4.0 )
181 {
182 // Zoom the new fragment in to increase its quality.
183 double grow = scale_ratio / 2.0;
184 rect *= Geom::Scale(grow);
185 affine *= Geom::Scale(grow);
186 }
187
188 // Do not allow the fragment to become more detailed than the window.
189 if ( double scale_ratio = std::sqrt(std::abs(affine.det() / view.affine.det()));
190 scale_ratio > 1.0 )
191 {
192 // Zoom the new fragment out to reduce its quality.
193 double shrink = 1.0 / scale_ratio;
194 rect *= Geom::Scale(shrink);
195 affine *= Geom::Scale(shrink);
196 }
197
198 // Find the bounding rect of the visible region + prerender margin within the new fragment. We do not want to discard this content in the next clipping step.
199 auto renderable = (Geom::Parallelogram(expandedBy(view.rect, _prefs.prerender)) * view.affine.inverse() * affine).bounds() & rect;
200
201 // Cap the dimensions of the new fragment to slightly larger than the maximum dimension of the window by clipping it towards the screen centre. (Lower in Cairo mode since otherwise too slow to cope.)
202 double max_dimension = max(view.rect.dimensions()) * (_graphics->is_opengl() ? 1.7 : 0.8);
203 auto dimens = rect.dimensions();
204 dimens.x() = std::min(dimens.x(), max_dimension);
205 dimens.y() = std::min(dimens.y(), max_dimension);
206 auto center = Geom::Rect(view.rect).midpoint() * view.affine.inverse() * affine;
207 center.x() = Util::safeclamp(center.x(), rect.left() + dimens.x() * 0.5, rect.right() - dimens.x() * 0.5);
208 center.y() = Util::safeclamp(center.y(), rect.top() + dimens.y() * 0.5, rect.bottom() - dimens.y() * 0.5);
209 rect = Geom::Rect(center - dimens * 0.5, center + dimens * 0.5);
210
211 // Ensure the new fragment contains the renderable rect from earlier, enlarging it and reducing resolution if necessary.
212 if (!rect.contains(renderable)) {
213 auto oldrect = rect;
214 rect.unionWith(renderable);
215 double shrink = 1.0 / std::max(rect.width() / oldrect.width(), rect.height() / oldrect.height());
216 rect *= Geom::Scale(shrink);
217 affine *= Geom::Scale(shrink);
218 }
219
220 // Calculate the paste transform from the snapshot store to the new fragment (again).
224 * affine
225 * Geom::Translate(-rect.min())
226 * Geom::Scale(rect.dimensions()).inverse();
227
228 if (_prefs.debug_logging) std::cout << "New fragment dimensions " << rect.width() << ' ' << rect.height() << std::endl;
229
230 if (paste.isIdentity(0.001) && rect.dimensions().round() == _snapshot.rect.dimensions()) {
231 // Fast path: simply paste the backing store onto the snapshot store.
232 if (_prefs.debug_logging) std::cout << "Fast snapshot combine" << std::endl;
234 } else {
235 // General path: paste the snapshot store and then the backing store onto a new fragment, then set that as the snapshot store.
236 auto frag_rect = rect.roundOutwards();
237 _graphics->snapshot_combine(Fragment{ affine, frag_rect });
238 _snapshot.rect = frag_rect;
239 _snapshot.affine = affine;
240 }
241
242 // Start drawing again on a new blank store aligned to the screen.
243 recreate_store(view);
244 // Transform the snapshot clean region to the new store.
245 // Todo: Should really clip this to the new snapshot rect, only we can't because it's generally not aligned with the store's affine.
246 _snapshot.drawn = shrink_region(region_affine_approxinwards(_snapshot.drawn, old_store_affine.inverse() * _store.affine, _store.rect), 4, -2);
247};
248
250{
252 _store.drawn.reset();
253 _snapshot.drawn.reset();
254}
255
256// Handle transitions and actions in response to viewport changes.
257auto Stores::update(Fragment const &view) -> Action
258{
259 switch (_mode) {
260
261 case Mode::None: {
262 // Not yet initialised or just reset - create store for first time.
263 recreate_store(view);
264 _mode = Mode::Normal;
265 if (_prefs.debug_logging) std::cout << "Full reset" << std::endl;
266 return Action::Recreated;
267 }
268
269 case Mode::Normal: {
270 auto result = Action::None;
271 // Enter decoupled mode if the affine has changed from what the store was drawn at.
272 if (view.affine != _store.affine) {
273 // Snapshot and reset the store.
274 take_snapshot(view);
275 // Enter decoupled mode.
276 _mode = Mode::Decoupled;
277 if (_prefs.debug_logging) std::cout << "Enter decoupled mode" << std::endl;
278 result = Action::Recreated;
279 } else {
280 // Determine whether the view has moved sufficiently far that we need to shift the store.
281 if (!_store.rect.contains(expandedBy(view.rect, _prefs.prerender))) {
282 // The visible region + prerender margin has reached the edge of the store.
283 if (!(cairo_to_geom(_store.drawn->get_extents()) & expandedBy(view.rect, _prefs.prerender + _prefs.padding)).regularized()) {
284 // If the store contains no reusable content at all, recreate it.
285 recreate_store(view);
286 if (_prefs.debug_logging) std::cout << "Recreate store" << std::endl;
287 result = Action::Recreated;
288 } else {
289 // Otherwise shift it.
290 shift_store(view);
291 if (_prefs.debug_logging) std::cout << "Shift store" << std::endl;
292 result = Action::Shifted;
293 }
294 }
295 }
296 // After these operations, the store should now contain the visible region + prerender margin.
297 assert(_store.rect.contains(expandedBy(view.rect, _prefs.prerender)));
298 return result;
299 }
300
301 case Mode::Decoupled: {
302 // Completely cancel the previous redraw and start again if the viewing parameters have changed too much.
303 auto check_restart_redraw = [&, this] {
304 // With this debug feature on, redraws should never be restarted.
305 if (_prefs.debug_sticky_decoupled) return false;
306
307 // Restart if the store is no longer covering the middle 50% of the screen. (Usually triggered by rotating or zooming out.)
308 auto pl = Geom::Parallelogram(view.rect);
309 pl *= Geom::Translate(-pl.midpoint()) * Geom::Scale(0.5) * Geom::Translate(pl.midpoint());
310 pl *= view.affine.inverse() * _store.affine;
311 if (!Geom::Parallelogram(_store.rect).contains(pl)) {
312 if (_prefs.debug_logging) std::cout << "Restart redraw (store not fully covering screen)" << std::endl;
313 return true;
314 }
315
316 // Also restart if zoomed in or out too much.
317 auto scale_ratio = std::abs(view.affine.det() / _store.affine.det());
318 if (scale_ratio > 3.0 || scale_ratio < 0.7) {
319 // Todo: Un-hard-code these thresholds.
320 // * The threshold 3.0 is for zooming in. It says that if the quality of what is being redrawn is more than 3x worse than that of the screen, restart. This is necessary to ensure acceptably high resolution is kept as you zoom in.
321 // * The threshold 0.7 is for zooming out. It says that if the quality of what is being redrawn is too high compared to the screen, restart. This prevents wasting time redrawing the screen slowly, at too high a quality that will probably not ever be seen.
322 if (_prefs.debug_logging) std::cout << "Restart redraw (zoomed changed too much)" << std::endl;
323 return true;
324 }
325
326 // Don't restart.
327 return false;
328 };
329
330 if (check_restart_redraw()) {
331 // Re-use as much content as possible from the store and the snapshot, and set as the new snapshot.
332 snapshot_combine(view);
333 return Action::Recreated;
334 }
335
336 return Action::None;
337 }
338
339 default: {
340 assert(false);
341 return Action::None;
342 }
343 }
344}
345
347{
348 // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to reset the store to the correct affine.
349 if (_mode == Mode::Decoupled) {
350 if (_prefs.debug_sticky_decoupled) {
351 // Debug feature: stop redrawing, but stay in decoupled mode.
352 } else if (_store.affine == view.affine) {
353 // Store is at the correct affine - exit decoupled mode.
354 if (_prefs.debug_logging) std::cout << "Exit decoupled mode" << std::endl;
355 // Exit decoupled mode.
356 _mode = Mode::Normal;
357 _graphics->invalidate_snapshot();
358 } else {
359 // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine.
360 // Snapshot and reset the backing store.
361 take_snapshot(view);
362 if (_prefs.debug_logging) std::cout << "Remain in decoupled mode" << std::endl;
363 return Action::Recreated;
364 }
365 }
366
367 return Action::None;
368}
369
370} // namespace Inkscape::UI::Widget
371
372/*
373 Local Variables:
374 mode:c++
375 c-file-style:"stroustrup"
376 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
377 indent-tabs-mode:nil
378 fill-column:99
379 End:
380*/
381// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
Cartesian point / 2D vector and related operations.
void paste(InkscapeWindow *win)
Geom::IntRect bounds
Definition canvas.cpp:182
3x3 matrix representing an affine transformation.
Definition affine.h:70
Coord det() const
Calculate the determinant.
Definition affine.cpp:416
bool isScale(Coord eps=EPSILON) const
Check whether this matrix represents pure scaling.
Definition affine.cpp:147
bool isIdentity(Coord eps=EPSILON) const
Check whether this matrix is an identity matrix.
Definition affine.cpp:109
Affine inverse() const
Compute the inverse matrix.
Definition affine.cpp:388
Affine withoutTranslation() const
Definition affine.h:169
Convex hull based on the Andrew's monotone chain algorithm.
std::pair< Rotate, OptRect > minAreaRotation() const
Return a rotation that puts the convex hull into a position such that bounds() has minimal area,...
Axis aligned, non-empty, generic rectangle.
CPoint midpoint() const
Get the point in the geometric center of the rectangle.
CPoint min() const
Get the corner of the rectangle with smallest coordinate values.
CPoint dimensions() const
Get rectangle's width and height as a point.
Two-dimensional point with integer coordinates.
Definition int-point.h:57
Paralellogram, representing a linear transformation of a rectangle.
bool contains(Point const &) const
Two-dimensional point that doubles as a vector.
Definition point.h:66
Axis aligned, non-empty rectangle.
Definition rect.h:92
Scaling from the origin.
Definition transforms.h:150
Scale inverse() const
Definition transforms.h:172
Translation by a vector.
Definition transforms.h:115
virtual bool is_opengl() const =0
Whether this is an OpenGL backend.
virtual void shift_store(Fragment const &dest)=0
Called when the store fragment shifts position to dest.
virtual void recreate_store(Geom::IntPoint const &dims)=0
Set the store to a surface of the given size, of unspecified contents.
virtual void fast_snapshot_combine()=0
Paste the store onto the snapshot.
virtual void snapshot_combine(Fragment const &dest)=0
Paste the snapshot followed by the store onto a new snapshot at dest.
virtual void swap_stores()=0
Exchange the store and snapshot surfaces.
Pref< bool > debug_logging
Definition prefs.h:48
Pref< int > prerender
Definition prefs.h:40
void recreate_store(Fragment const &view)
Definition stores.cpp:103
Action update(Fragment const &view)
Respond to a viewport change. (Requires a valid graphics.)
Definition stores.cpp:257
Geom::IntRect centered(Fragment const &view) const
Definition stores.cpp:97
void shift_store(Fragment const &view)
Definition stores.cpp:113
void snapshot_combine(Fragment const &view)
Definition stores.cpp:137
void take_snapshot(Fragment const &view)
Definition stores.cpp:125
void reset()
Discards all stores. (The actual operation on the graphics is performed on the next update()....
Definition stores.cpp:249
Action finished_draw(Fragment const &view)
Respond to drawing of the backing store having finished. (Requires a valid graphics....
Definition stores.cpp:346
Convex hull data structures.
Css & result
bool approx_dihedral(Geom::Affine const &affine, double eps)
Returns whether an affine transformation is approximately a dihedral transformation,...
Definition geom.cpp:1099
Specific geometry functions for Inkscape, not provided my lib2geom.
auto absolute(Geom::Point const &a)
Definition geom.h:92
auto expandedBy(Geom::IntRect rect, int amount)
Definition geom.h:66
GenericRect< IntCoord > IntRect
Definition forward.h:57
Piecewise< SBasis > min(SBasis const &f, SBasis const &g)
Return the more negative of the two functions pointwise.
Custom widgets.
Definition desktop.h:126
Cairo::RefPtr< Cairo::Region > shrink_region(Cairo::RefPtr< Cairo::Region > const &reg, int d, int t)
Shrink a region by d/2 in all directions, while also translating it by (d/2 + t, d/2 + t).
Definition util.cpp:18
T safeclamp(T val, T lo, T hi)
Just like std::clamp, except it doesn't deliberately crash if lo > hi due to rounding errors,...
Definition mathfns.h:99
Abstraction of the store/snapshot mechanism.
A "fragment" is a rectangle of drawn content at a specfic place.
Definition fragment.h:12
Cairo::RefPtr< Cairo::Region > drawn
The region of space containing drawn content.
Definition stores.h:46
Affine transformation classes.
Cairo::RectangleInt geom_to_cairo(const Geom::IntRect &rect)
Definition util.cpp:352
Geom::IntRect cairo_to_geom(const Cairo::RectangleInt &rect)
Definition util.cpp:357