Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
masks_outline.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19/* The outline of a shape, as its consumers read it.
20 *
21 * A brush and a polygon both publish two index-aligned arrays: a centreline (the brush's spine,
22 * the polygon's path) and a border, one border sample per centreline sample, at the shape's
23 * local radius along the normal. The rasteriser paints the spokes between the two; what the GUI
24 * draws is the BOUNDARY of what the rasteriser paints. This file holds what deciding that
25 * boundary needs, and the one geometric choice the two shapes make the same way at a joint.
26 * It knows nothing about nodes, handles or payloads. */
27
29#include "develop/masks_types.h"
31#include "system/mem_alloc.h"
32#include "math/math.h"
33#include "common/logging.h"
34#include "common/times.h"
35
36#include <float.h>
37#include <math.h>
38#include <string.h>
39
40/* A border sample at @p radius from @p centre in the direction of (dx, dy). For an end that has
41 * a radius but no direction of its own: it borrows another's. Never a position copied from
42 * somewhere else -- a spoke of the right length in a borrowed direction is part of the disc
43 * union, a spoke to a copied position is of no particular length at all. */
44void dt_masks_outline_offset_along(const float *const centre, float dx, float dy, const float radius,
45 float *const border)
46{
47 const float len = dt_fast_hypotf(dx, dy);
48 if(len > 0.0f)
49 {
50 dx /= len;
51 dy /= len;
52 }
53 else
54 {
55 dx = 1.0f;
56 dy = 0.0f;
57 }
58 border[0] = centre[0] + radius * dx;
59 border[1] = centre[1] + radius * dy;
60}
61
62void dt_masks_outline_envelope_offset(const float *centre, float dx, float dy, float radius, float radius_rate,
63 float *out)
64{
65 const float length = dt_fast_hypotf(dx, dy);
66 if(!(length > 0.0f))
67 {
68 out[0] = centre[0];
69 out[1] = centre[1];
70 return;
71 }
72 const float l = 1.0f / length;
73 /* r' = dr/ds: the rate by the parameter over the speed by the parameter */
74 float along = -radius_rate * l;
76 const float across = sqrtf(1.0f - along * along);
77 const float tx = dx * l;
78 const float ty = dy * l;
79 out[0] = centre[0] + radius * (along * tx + across * ty);
80 out[1] = centre[1] + radius * (along * ty - across * tx);
81}
82
83/* Which way round a joint arc goes: from @p from to @p to about @p centre, the SHORT way.
84 *
85 * On the convex side of a turn the short way is the exterior wedge the spokes leave open, which
86 * is the whole point of the arc. On the concave side the two borders have crossed and the short
87 * way runs through the inside of the shape, painting nothing new but costing only the turn's
88 * worth of samples. Sweeping a fixed rotation instead covered the same wedge on one side and
89 * went the long way round on the other: a near-full circle of interior spokes at every joint.
90 *
91 * At a cusp the two are the same length and the choice matters: for a brush the two halves of
92 * the tip disc are covered by the two passes, one each, and which is which is the pass's own
93 * rotation (the rule its caps follow); for a polygon it is the winding. So a tie within a few
94 * degrees of pi keeps @p default_clockwise, and only a sweep clearly longer than pi flips. */
95gboolean dt_masks_outline_short_way(const float *const centre, const float *const from, const float *const to,
96 const gboolean default_clockwise)
97{
98 const float a1 = atan2f(from[1] - centre[1], from[0] - centre[0]);
99 const float a2 = atan2f(to[1] - centre[1], to[0] - centre[0]);
100 float sweep_cw = a2 - a1;
101 if(sweep_cw < 0.0f) sweep_cw += 2.0f * M_PI; /* the clockwise sweep, in (0, 2 pi) */
102
103 const float tie = 0.05f;
104 if(default_clockwise && sweep_cw > M_PI + tie) return FALSE;
105 if(!default_clockwise && sweep_cw < M_PI - tie) return TRUE;
106 return default_clockwise;
107}
108
109/* THE BOUNDARY OF A SHAPE.
110 *
111 * The rasteriser paints every spoke it is given, and it should: each one is a radius of a disc
112 * the stroke is the union of, so a spoke that lies inside another spoke's disc paints nothing
113 * new and costs nothing but time. The DRAWN outline is another matter. It is the boundary of
114 * that union, and a border sample that lies inside some other disc is not on it -- it is the
115 * inner side of a fold where the centreline bends tighter than its own radius, the inside of
116 * a joint arc, a cap swallowed by the next segment, or one side of the stroke running through
117 * the other. Every one of those used to be found AFTER the fact, by intersecting the outline
118 * with itself and cutting the loops out, and every ordering of those cuts moved the artefact
119 * somewhere else (issues #1352 and #1360; the three attempts recorded at the previous version
120 * of this function). The question the cuts were approximating is answered here directly, per
121 * sample: is this border sample strictly inside any other sample's disc? If so it is not on
122 * the boundary and the outline does not show it. Nothing is intersected and nothing is
123 * chosen between.
124 *
125 * Two searches, because the discs that can hide a sample come from two places, and what
126 * separates them is where they sit ALONG THE WALK, not where they sit in the plane. A disc
127 * can contain a border sample only if its centre is within its own radius of it, and the
128 * sample is one radius from its own spine point, so the two centres are within two radii of
129 * each other in the plane and -- the spine being a continuous line -- within two radii of
130 * each other along it: that much walk either side of the sample's own disc is exhaustive for
131 * folds, joints and caps. The window is measured in walk LENGTH, never in discs: a disc is
132 * made wherever the spine moves half a pixel or the radius steps, so a count of discs is a
133 * different length at every sampling density and at every flare, and the count this used to
134 * be (four times the largest radius, plus eight) was ten times too wide once the outline was
135 * sampled at the density the screen shows. The stroke can also come back on itself -- a
136 * hairpin, a crossing, a spiral, and the brush's own second pass down the other side -- and
137 * then the hiding disc is any distance away along the walk but within one radius in the
138 * plane. A bucket grid of one reach per cell finds those, and each bucket holds its discs as
139 * RUNS of consecutive indices: a run inside the window is the near part, already answered,
140 * and is dismissed in one comparison; only runs from far along the walk are tested disc by
141 * disc.
142 *
143 * The test itself is the cost, and it is arranged to be counted rather than computed. The
144 * discs are flat arrays -- centre x, centre y, and the SQUARED radius less the tolerance,
145 * negative for a disc the tolerance leaves nothing of -- so a probe is one squared distance
146 * against each, no square root anywhere, in blocks of eight the compiler can vectorise; and
147 * each block carries the box its centres span and its largest radius, so a block that cannot
148 * reach the probe costs four comparisons. The previous test took a hypot per disc for the
149 * copy test below, and dismissed a block on its first disc's distance against a reach padded
150 * by the largest step between discs: measured on the corpus, 4.5 ns per disc test and half
151 * the blocks tested.
152 *
153 * The copy test is a different question and has its own structure. The walk stamps a full
154 * disc at a node whose radius steps in BOTH passes, and bridges a joint with an arc about
155 * the same node at the same radius; every copy after the first traces a boundary the first
156 * already traces, and drawn on top of it with its own dash phase it fills the gaps of the
157 * dashes -- measured on a flaring brush: 3,171 samples per pass centred on the node to the
158 * float, 4,020 kept on a 2,114 px circumference, drawn as a near-solid line. A copy is not a
159 * boundary sample. Neither is the stretch where a segment leaves such a node: its envelope
160 * runs within the boundary tolerance of the node's circle for tens of pixels. So a sample
161 * that repeats an EARLIER one -- a border position within three quarters of a pixel, at
162 * least OUTLINE_REPEAT_MIN_WALK of border walked between the two -- is dropped. Position
163 * alone decides, whatever discs the two samples belong to: for drawing, two boundary samples
164 * within three quarters of a pixel are one line. The other side of the stroke is a diameter
165 * away and never matches. A sample's own run is excluded by the length of border walked
166 * between the two: an arc filler can sample closer than the tolerance, and the recursion
167 * samples a hundredth of a pixel apart around every integer crossing, so neither a count of
168 * samples nor a predecessor test can tell a run from its copy -- the walk can, a copy being
169 * the other pass or another stamp, thousands of pixels away along it. Keyed on the sample's
170 * disc or its spine point instead, this test missed the copies (a disc's centre is its first
171 * sample's, half a pixel off and differently per pass) or the junctions; each round was
172 * measured on the corpus before the next. The samples are hashed by pixel cell, so the test
173 * reads the nine cells around the probe and nothing else; it used to ride inside the disc
174 * test, walking the samples of every disc whose circle passed near the probe, and was what
175 * put the square root in that loop.
176 *
177 * A first version used a coarse occupancy map of the union for the far part instead. It was
178 * conservative, and so it left every sample within a few pixels of a far boundary undecided;
179 * refining those exactly meant refining every sample, because every boundary sample is within
180 * a few pixels of its OWN stroke's interior, and the build went from 30 ms to 250 ms on the
181 * corpus. Position cannot tell near from far; the walk can.
182 *
183 * Cost is bounded by decimation, not by the sample count: consecutive samples closer than half
184 * a pixel with the same radius are one disc, so the window and the grid both see a few
185 * thousand discs on a stroke of a hundred thousand samples. `-d masks -d perf' prints what a
186 * pass cost and how many discs it tested. */
187
188/* Discs per block of the containment test; a block is dismissed on its bounds or tested
189 * whole. */
190#define OUTLINE_BLOCK 8
191
192/* The bucket grid over the discs: one reach per cell, each bucket a linked list of RUNS of
193 * consecutive disc indices, so that a run inside the walk's window can be dismissed with two
194 * comparisons and only the discs from far along the walk are ever tested. */
207
208/* The discs, decimated, as the flat arrays the containment test streams through. */
209typedef struct _outline_discs_t
210{
211 float *cx;
212 float *cy;
213 float *rin2; /* (radius - eps)^2, or -1 for a disc the tolerance leaves nothing of */
214 float *dwalk; /* per disc, the length of spine walked to its centre */
215 int count;
217
218/* Per block of OUTLINE_BLOCK discs: the box its centres span, and how far past it the largest
219 * disc reaches -- zero when none of them can contain anything. */
220typedef struct _outline_blocks_t
221{
222 float *minx;
223 float *maxx;
224 float *miny;
225 float *maxy;
226 float *reach;
227 int count;
229
230/* The samples hashed by pixel cell, for the copy test. */
231typedef struct _outline_cells_t
232{
233 int *head;
234 int *next;
235 unsigned mask;
237
238/* Everything the per-sample test needs. */
240{
243 int *disc_of; /* per sample, the disc it belongs to */
244 const float *border_h; /* the border samples, past the header */
245 float *walk; /* per sample, the length of border walked to reach it */
246 float window; /* walk either side of a sample's own disc that can reach it */
247 float eps; /* boundary tolerance, in pixels */
249 gboolean have_grid;
251 /* what the pass did, for the perf trace */
252 long probes;
255
256/* How far back along the walk a sample may match: closer than this it is the sample's own
257 * run. Sixteen samples was the first guess, and the recursion samples a border a hundredth of
258 * a pixel apart around every integer crossing, where sixteen samples are less than a pixel. */
259#define OUTLINE_REPEAT_MIN_WALK 4.0f
260
262{
263 dt_free_align(b->discs.cx);
264 dt_free_align(b->discs.cy);
265 dt_free_align(b->discs.rin2);
266 dt_free_align(b->discs.dwalk);
267 dt_free_align(b->blocks.minx);
268 dt_free_align(b->blocks.maxx);
269 dt_free_align(b->blocks.miny);
270 dt_free_align(b->blocks.maxy);
271 dt_free_align(b->blocks.reach);
272 dt_free_align(b->disc_of);
273 dt_free_align(b->walk);
274 dt_free_align(b->grid.bucket_head);
275 dt_free_align(b->grid.run_start);
276 dt_free_align(b->grid.run_end);
277 dt_free_align(b->grid.run_next);
278 dt_free_align(b->cells.head);
279 dt_free_align(b->cells.next);
280 memset(b, 0, sizeof(*b));
281}
282
283/* The arrays a pass over @p n samples needs, all of them, or none. */
284static gboolean _outline_boundary_alloc(_outline_boundary_t *const b, const int n)
285{
286 const int nblock = n / OUTLINE_BLOCK + 1;
287 b->discs.cx = dt_alloc_align((size_t)n * sizeof(float));
288 b->discs.cy = dt_alloc_align((size_t)n * sizeof(float));
289 b->discs.rin2 = dt_alloc_align((size_t)n * sizeof(float));
290 b->discs.dwalk = dt_alloc_align((size_t)n * sizeof(float));
291 b->blocks.minx = dt_alloc_align((size_t)nblock * sizeof(float));
292 b->blocks.maxx = dt_alloc_align((size_t)nblock * sizeof(float));
293 b->blocks.miny = dt_alloc_align((size_t)nblock * sizeof(float));
294 b->blocks.maxy = dt_alloc_align((size_t)nblock * sizeof(float));
295 b->blocks.reach = dt_alloc_align((size_t)nblock * sizeof(float));
296 b->disc_of = dt_alloc_align((size_t)n * sizeof(int));
297 b->walk = dt_alloc_align((size_t)n * sizeof(float));
298 const gboolean ok = !IS_NULL_PTR(b->discs.cx) && !IS_NULL_PTR(b->discs.cy) && !IS_NULL_PTR(b->discs.rin2)
299 && !IS_NULL_PTR(b->discs.dwalk) && !IS_NULL_PTR(b->blocks.minx) && !IS_NULL_PTR(b->blocks.maxx)
300 && !IS_NULL_PTR(b->blocks.miny) && !IS_NULL_PTR(b->blocks.maxy) && !IS_NULL_PTR(b->blocks.reach)
301 && !IS_NULL_PTR(b->disc_of) && !IS_NULL_PTR(b->walk);
302 if(!ok) _outline_boundary_free(b);
303 return ok;
304}
305
306static inline unsigned _outline_cell_hash(const int cx, const int cy)
307{
308 return ((unsigned)cx * 73856093u) ^ ((unsigned)cy * 19349663u);
309}
310
311/* Hash every sample by the pixel cell it falls in. Returns FALSE when the table could not be
312 * allocated, in which case the copy test answers "no copy" and the stamps are drawn twice --
313 * the outline of a build under memory pressure, not a wrong one. */
314static gboolean _outline_cells_build(_outline_boundary_t *const b, const int n)
315{
316 unsigned size = 1;
317 while(size < 2u * (unsigned)n) size <<= 1;
318 b->cells.head = dt_alloc_align((size_t)size * sizeof(int));
319 b->cells.next = dt_alloc_align((size_t)n * sizeof(int));
320 if(IS_NULL_PTR(b->cells.head) || IS_NULL_PTR(b->cells.next))
321 {
322 dt_free_align(b->cells.head);
323 dt_free_align(b->cells.next);
324 b->cells.head = NULL;
325 b->cells.next = NULL;
326 return FALSE;
327 }
328 memset(b->cells.head, 0xff, (size_t)size * sizeof(int)); /* every head -1 */
329 b->cells.mask = size - 1;
330 for(int i = 0; i < n; i++)
331 {
332 const int cx = (int)floorf(b->border_h[2 * i]);
333 const int cy = (int)floorf(b->border_h[2 * i + 1]);
334 const unsigned h = _outline_cell_hash(cx, cy) & b->cells.mask;
335 b->cells.next[i] = b->cells.head[h];
336 b->cells.head[h] = i;
337 }
338 return TRUE;
339}
340
341/* Is there, in the hashed cell @p h, a sample within three quarters of a pixel of (bx, by) that
342 * lies at least OUTLINE_REPEAT_MIN_WALK before the probe along the border (@p walk_limit). */
343static inline gboolean _outline_cell_repeats(const _outline_boundary_t *const b, const unsigned h, const float bx,
344 const float by, const float walk_limit)
345{
346 for(int k = b->cells.head[h]; k >= 0; k = b->cells.next[k])
347 {
348 if(b->walk[k] > walk_limit) continue;
349 const float ex = b->border_h[2 * k] - bx;
350 const float ey = b->border_h[2 * k + 1] - by;
351 if(ex * ex + ey * ey <= 0.5625f) return TRUE;
352 }
353 return FALSE;
354}
355
356/* Does the sample at (bx, by), reached after @p walk of border, repeat an earlier one: a
357 * border position within three quarters of a pixel, at least OUTLINE_REPEAT_MIN_WALK before
358 * it along the walk. The nine pixel cells around it hold every candidate. */
359static inline gboolean _outline_sample_repeats(const _outline_boundary_t *const b, const float bx, const float by,
360 const float walk)
361{
362 if(IS_NULL_PTR(b->cells.head)) return FALSE;
363 const float walk_limit = walk - OUTLINE_REPEAT_MIN_WALK;
364 const int cx = (int)floorf(bx);
365 const int cy = (int)floorf(by);
366 for(int neighbour = 0; neighbour < 9; neighbour++)
367 {
368 const unsigned h = _outline_cell_hash(cx - 1 + neighbour % 3, cy - 1 + neighbour / 3) & b->cells.mask;
369 if(_outline_cell_repeats(b, h, bx, by, walk_limit)) return TRUE;
370 }
371 return FALSE;
372}
373
374/* Is (bx, by) strictly inside one of the discs [lo, hi], by more than the tolerance: block by
375 * block, a block dismissed on its bounds or streamed whole. */
376static inline gboolean _outline_discs_contain(_outline_boundary_t *const b, const int lo, const int hi,
377 const float bx, const float by)
378{
379 if(lo > hi) return FALSE;
380 const int blk_lo = lo / OUTLINE_BLOCK;
381 const int blk_hi = hi / OUTLINE_BLOCK;
382 for(int blk = blk_lo; blk <= blk_hi; blk++)
383 {
384 const float reach = b->blocks.reach[blk];
385 if(reach <= 0.0f) continue;
386 if(bx < b->blocks.minx[blk] - reach || bx > b->blocks.maxx[blk] + reach || by < b->blocks.miny[blk] - reach
387 || by > b->blocks.maxy[blk] + reach)
388 continue;
389 const int j0 = MAX(lo, blk * OUTLINE_BLOCK);
390 const int j1 = MIN(hi, blk * OUTLINE_BLOCK + OUTLINE_BLOCK - 1);
391 b->disc_tests += j1 - j0 + 1;
392 int hit = 0;
393 for(int j = j0; j <= j1; j++)
394 {
395 const float ex = b->discs.cx[j] - bx;
396 const float ey = b->discs.cy[j] - by;
397 hit |= (ex * ex + ey * ey < b->discs.rin2[j]);
398 }
399 if(hit) return TRUE;
400 }
401 return FALSE;
402}
403
404static inline int _outline_grid_cell(const _outline_disc_grid_t *const g, const float x, const float y)
405{
406 const int gx = CLAMP((int)((x - g->minx) / g->bucket) + 1, 0, g->bw - 1);
407 const int gy = CLAMP((int)((y - g->miny) / g->bucket) + 1, 0, g->bh - 1);
408 return gy * g->bw + gx;
409}
410
411/* @p bbox is { minx, maxx, miny, maxy } over the samples. Returns FALSE when the grid could not
412 * be allocated, in which case only the near test runs -- the behaviour of a stroke that never
413 * revisits its own ground. */
414static gboolean _outline_grid_build(_outline_boundary_t *const b, const float *const bbox, const float r_max)
415{
416 _outline_disc_grid_t *const g = &b->grid;
417 g->bucket = fmaxf(r_max, 16.0f);
418 g->minx = bbox[0];
419 g->miny = bbox[2];
420 g->bw = (int)((bbox[1] - bbox[0]) / g->bucket) + 3;
421 g->bh = (int)((bbox[3] - bbox[2]) / g->bucket) + 3;
422 g->bucket_head = dt_alloc_align((size_t)g->bw * g->bh * sizeof(int));
423 g->run_start = dt_alloc_align((size_t)b->discs.count * sizeof(int));
424 g->run_end = dt_alloc_align((size_t)b->discs.count * sizeof(int));
425 g->run_next = dt_alloc_align((size_t)b->discs.count * sizeof(int));
426 if(IS_NULL_PTR(g->bucket_head) || IS_NULL_PTR(g->run_start) || IS_NULL_PTR(g->run_end)
427 || IS_NULL_PTR(g->run_next))
428 return FALSE;
429
430 for(int cell = 0; cell < g->bw * g->bh; cell++) g->bucket_head[cell] = -1;
431 int nruns = 0;
432 int last_cell = -1;
433 for(int d = 0; d < b->discs.count; d++)
434 {
435 const int cell = _outline_grid_cell(g, b->discs.cx[d], b->discs.cy[d]);
436 if(cell == last_cell)
437 {
438 g->run_end[nruns - 1] = d; /* the walk is still in this bucket: extend its latest run */
439 continue;
440 }
441 g->run_start[nruns] = d;
442 g->run_end[nruns] = d;
443 g->run_next[nruns] = g->bucket_head[cell];
444 g->bucket_head[cell] = nruns;
445 nruns++;
446 last_cell = cell;
447 }
448 return TRUE;
449}
450
451/* The far test: every run in reach of the probe, but only the parts of it OUTSIDE the walk's
452 * window [lo, hi] -- the part inside is the near test's, already answered. */
453static inline gboolean _outline_far_contains(_outline_boundary_t *const b, const int lo, const int hi,
454 const float bx, const float by)
455{
456 const _outline_disc_grid_t *const g = &b->grid;
457 const int gx = CLAMP((int)((bx - g->minx) / g->bucket) + 1, 0, g->bw - 1);
458 const int gy = CLAMP((int)((by - g->miny) / g->bucket) + 1, 0, g->bh - 1);
459
460 for(int neighbour = 0; neighbour < 9; neighbour++)
461 {
462 const int xx = gx - 1 + neighbour % 3;
463 const int yy = gy - 1 + neighbour / 3;
464 if(xx < 0 || yy < 0 || xx >= g->bw || yy >= g->bh) continue;
465
466 for(int r = g->bucket_head[yy * g->bw + xx]; r >= 0; r = g->run_next[r])
467 {
468 const int start = g->run_start[r];
469 const int end = g->run_end[r];
470 if(start < lo && _outline_discs_contain(b, start, MIN(end, lo - 1), bx, by)) return TRUE;
471 if(end > hi && _outline_discs_contain(b, MAX(start, hi + 1), end, bx, by)) return TRUE;
472 }
473 }
474 return FALSE;
475}
476
477/* The discs within the window of walk either side of disc @p d0: the walk to a centre is
478 * monotone along the discs, so both ends are a bisection. */
479static inline void _outline_window(const _outline_boundary_t *const b, const int d0, int *const lo, int *const hi)
480{
481 const float w0 = b->discs.dwalk[d0];
482 const float w_lo = w0 - b->window;
483 int l = 0;
484 int h = d0;
485 while(l < h)
486 {
487 const int m = (l + h) / 2;
488 if(b->discs.dwalk[m] < w_lo) l = m + 1;
489 else h = m;
490 }
491 *lo = l;
492 const float w_hi = w0 + b->window;
493 l = d0;
494 h = b->discs.count - 1;
495 while(l < h)
496 {
497 const int m = (l + h + 1) / 2;
498 if(b->discs.dwalk[m] > w_hi) h = m - 1;
499 else l = m;
500 }
501 *hi = l;
502}
503
504/* Is border sample @p i, at (bx, by), strictly inside some other sample's disc, or a copy of
505 * an earlier sample: the window along the walk first, then whatever a far part of the stroke
506 * brings within reach, then the cells around it. */
507static inline gboolean _outline_sample_inside(_outline_boundary_t *const b, const int i, const float bx, const float by)
508{
509 int lo = 0;
510 int hi = 0;
511 _outline_window(b, b->disc_of[i], &lo, &hi);
512 b->probes++;
513 if(_outline_discs_contain(b, lo, hi, bx, by)) return TRUE;
514 if(b->have_grid && _outline_far_contains(b, lo, hi, bx, by)) return TRUE;
515 return _outline_sample_repeats(b, bx, by, b->walk[i]);
516}
517
518/* Settle the samples [from, to) between two probes: when both probes agreed, the samples take
519 * their answer (@p agreed is 0 or 1); when they disagreed (@p agreed is -1) each sample is
520 * asked itself, which is what makes the cut land on the sample it belongs to and not on a
521 * neighbour. Returns how many were dropped. */
522static inline int _outline_settle_span(_outline_boundary_t *const b, uint8_t *const dropped, const int from,
523 const int to, const int agreed)
524{
525 if(agreed == 0) return 0;
526 int ndropped = 0;
527 for(int j = from; j < to; j++)
528 {
529 const gboolean inside
530 = (agreed == 1) || _outline_sample_inside(b, j, b->border_h[j * 2], b->border_h[j * 2 + 1]);
531 if(!inside) continue;
532 dropped[j] = 1;
533 ndropped++;
534 }
535 return ndropped;
536}
537
538/* The discs, decimated: consecutive samples closer than half a pixel with the same radius are
539 * one disc. Fills @p b's disc arrays, disc_of, the block bounds and the sample bbox; returns
540 * the largest radius. */
541static float _outline_discs_from_outline(const float *const points_h, const float *const border_h, const int n,
542 _outline_boundary_t *const b, float *const bbox)
543{
544 int ndisc = 0;
545 float r_max = 0.0f;
546 float last_x = 0.0f;
547 float last_y = 0.0f;
548 float last_r = 0.0f;
549 float walked = 0.0f;
550 bbox[0] = FLT_MAX;
551 bbox[1] = -FLT_MAX;
552 bbox[2] = FLT_MAX;
553 bbox[3] = -FLT_MAX;
554 for(int i = 0; i < n; i++)
555 {
556 const float px = points_h[i * 2];
557 const float py = points_h[i * 2 + 1];
558 const float bx = border_h[i * 2];
559 const float by = border_h[i * 2 + 1];
560 const float r = dt_fast_hypotf(bx - px, by - py);
561 bbox[0] = fminf(bbox[0], fminf(bx, px));
562 bbox[1] = fmaxf(bbox[1], fmaxf(bx, px));
563 bbox[2] = fminf(bbox[2], fminf(by, py));
564 bbox[3] = fmaxf(bbox[3], fmaxf(by, py));
565
566 float moved = 0.0f;
567 gboolean new_disc = (ndisc == 0);
568 if(!new_disc)
569 {
570 moved = dt_fast_hypotf(px - last_x, py - last_y);
571 new_disc = (moved > 0.5f || fabsf(r - last_r) > 0.5f);
572 }
573 if(new_disc)
574 {
575 walked += moved;
576 const float rin = r - b->eps;
577 b->discs.cx[ndisc] = px;
578 b->discs.cy[ndisc] = py;
579 b->discs.rin2[ndisc] = (rin > 0.0f) ? rin * rin : -1.0f;
580 b->discs.dwalk[ndisc] = walked;
581 const int blk = ndisc / OUTLINE_BLOCK;
582 if(ndisc % OUTLINE_BLOCK == 0)
583 {
584 b->blocks.minx[blk] = px;
585 b->blocks.maxx[blk] = px;
586 b->blocks.miny[blk] = py;
587 b->blocks.maxy[blk] = py;
588 b->blocks.reach[blk] = fmaxf(rin, 0.0f);
589 }
590 else
591 {
592 b->blocks.minx[blk] = fminf(b->blocks.minx[blk], px);
593 b->blocks.maxx[blk] = fmaxf(b->blocks.maxx[blk], px);
594 b->blocks.miny[blk] = fminf(b->blocks.miny[blk], py);
595 b->blocks.maxy[blk] = fmaxf(b->blocks.maxy[blk], py);
596 b->blocks.reach[blk] = fmaxf(b->blocks.reach[blk], rin);
597 }
598 ndisc++;
599 r_max = fmaxf(r_max, r);
600 last_x = px;
601 last_y = py;
602 last_r = r;
603 }
604 b->disc_of[i] = ndisc - 1;
605 }
606 b->discs.count = ndisc;
607 b->blocks.count = (ndisc + OUTLINE_BLOCK - 1) / OUTLINE_BLOCK;
608 b->border_h = border_h;
609 /* the sample is one radius from its spine point, the hiding disc's centre one radius from
610 * the sample, and each of them half a pixel from its disc's centre */
611 b->window = 2.0f * r_max + 1.0f;
612 return r_max;
613}
614
615/* The next run of dropped samples at or after *cursor, as [from, to). A kept run of one or
616 * two samples between two dropped ones is noise at a crossing, not a boundary, and goes with
617 * them. Returns FALSE when none is left. */
618static inline gboolean _outline_next_dropped_run(const uint8_t *const dropped, const int n, int *const cursor,
619 int *const from, int *const to)
620{
621 int i = *cursor;
622 while(i < n && !dropped[i]) i++;
623 if(i >= n)
624 {
625 *cursor = n;
626 return FALSE;
627 }
628 int j = i;
629 while(j < n && dropped[j]) j++;
630 while(j + 2 < n && !dropped[j] && (dropped[j + 1] || dropped[j + 2]))
631 {
632 while(j < n && !dropped[j]) j++;
633 while(j < n && dropped[j]) j++;
634 }
635 *from = i;
636 *to = j;
637 *cursor = j;
638 return TRUE;
639}
640
641/* A dropped run of one or two samples between kept ones is kept again. Such a speck is a
642 * sample that coincides with a neighbour -- a gap filler's first point over the previous leaf's
643 * border, a sample a hair inside the next disc -- and hiding it changes nothing on screen while
644 * it cuts the run in two: every cut is a sub-path of its own to stroke, and a corpus brush went
645 * from one run to forty-seven for nothing. The mirror rule, a kept speck between dropped runs,
646 * lives in _outline_next_dropped_run(). */
647static void _outline_keep_specks(uint8_t *const dropped, const int n)
648{
649 int i = 0;
650 while(i < n)
651 {
652 if(!dropped[i])
653 {
654 i++;
655 continue;
656 }
657 int j = i;
658 while(j < n && dropped[j]) j++;
659 const gboolean kept_before = (i > 0) && !dropped[i - 1];
660 const gboolean kept_after = (j < n) && !dropped[j];
661 if(j - i <= 2 && kept_before && kept_after)
662 for(int k = i; k < j; k++) dropped[k] = 0;
663 i = j;
664 }
665}
666
667/* Runs of dropped samples become skip ranges, indices offset back past the header. */
668static int _outline_skips_from_dropped(const uint8_t *const dropped, const int n, const int header,
669 dt_masks_skip_range_t **const skips_out)
670{
671 int nskips = 0;
672 int cursor = 0;
673 int from = 0;
674 int to = 0;
675 while(_outline_next_dropped_run(dropped, n, &cursor, &from, &to)) nskips++;
676 if(nskips == 0) return 0;
677
679 if(IS_NULL_PTR(skips)) return 0;
680
681 int at = 0;
682 cursor = 0;
683 while(_outline_next_dropped_run(dropped, n, &cursor, &from, &to))
684 {
685 skips[at].jump_from = header + from;
686 skips[at].resume_at = header + to;
687 at++;
688 }
689 *skips_out = skips;
690 return nskips;
691}
692
693/* The probes.
694 *
695 * Not every sample: the border is sampled several times per pixel, and two samples a
696 * quarter of a pixel apart cannot be on different sides of a boundary that is decided to
697 * half a pixel. So a sample is probed when it has moved half a pixel from the last probe,
698 * and the samples between two probes that agree take their answer; only where two probes
699 * disagree is every sample between them probed. Measured on the corpus: the same skip
700 * ranges to the sample, at a third of the probes. Returns how many samples were dropped. */
701static int _outline_probe_samples(_outline_boundary_t *const b, uint8_t *const dropped, const int n)
702{
703 const float *const border_h = b->border_h;
704 int ndropped = 0;
705 int last_probe = -1;
706 gboolean last_inside = FALSE;
707 float last_bx = 0.0f;
708 float last_by = 0.0f;
709 for(int i = 0; i < n; i++)
710 {
711 const float bx = border_h[i * 2];
712 const float by = border_h[i * 2 + 1];
713 const gboolean moved = (fabsf(bx - last_bx) > 0.5f || fabsf(by - last_by) > 0.5f);
714 if(last_probe >= 0 && i != n - 1 && !moved) continue;
715
716 const gboolean inside = _outline_sample_inside(b, i, bx, by);
717 if(last_probe >= 0 && i > last_probe + 1)
718 {
719 const int agreed = (inside == last_inside) ? (int)inside : -1;
720 ndropped += _outline_settle_span(b, dropped, last_probe + 1, i, agreed);
721 }
722 if(inside)
723 {
724 dropped[i] = 1;
725 ndropped++;
726 }
727 last_probe = i;
728 last_inside = inside;
729 last_bx = bx;
730 last_by = by;
731 }
732 return ndropped;
733}
734
735int dt_masks_outline_boundary_skips(const float *const points, const float *const border,
736 const int count, const int header,
737 dt_masks_skip_range_t **skips_out)
738{
739 *skips_out = NULL;
740 const int n = count - header;
741 if(IS_NULL_PTR(points) || IS_NULL_PTR(border) || n < 8) return 0;
742 const float *const points_h = points + 2 * header;
743 const float *const border_h = border + 2 * header;
744
745 const double start = dt_get_wtime();
746 _outline_boundary_t b = { 0 };
747 uint8_t *dropped = dt_alloc_align((size_t)n);
748 if(IS_NULL_PTR(dropped) || !_outline_boundary_alloc(&b, n))
749 {
750 dt_free_align(dropped);
751 return 0;
752 }
753 memset(dropped, 0, (size_t)n);
754 b.eps = 0.5f;
755 b.walk[0] = 0.0f;
756 for(int i = 1; i < n; i++)
757 b.walk[i] = b.walk[i - 1]
758 + dt_fast_hypotf(border_h[2 * i] - border_h[2 * i - 2], border_h[2 * i + 1] - border_h[2 * i - 1]);
759
760 float bbox[4];
761 const float r_max = _outline_discs_from_outline(points_h, border_h, n, &b, bbox);
762 b.have_grid = _outline_grid_build(&b, bbox, r_max);
764 const double prepared = dt_get_wtime();
765
766 const int ndropped = _outline_probe_samples(&b, dropped, n);
769 "[masks] boundary pass: %d samples, %d discs, radius %.0f, %ld probes, %ld disc tests, %d dropped;"
770 " prepared in %.1f ms, probed in %.1f ms\n",
771 n, b.discs.count, r_max, b.probes, b.disc_tests, ndropped, 1000.0 * (prepared - start),
772 1000.0 * (dt_get_wtime() - prepared));
774
775 if(ndropped > 0) _outline_keep_specks(dropped, n);
776 const int nskips = (ndropped > 0) ? _outline_skips_from_dropped(dropped, n, header, skips_out) : 0;
777 dt_free_align(dropped);
778 return nskips;
779}
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
#define m
Definition basecurve.c:283
static const float x
const dt_colormatrix_t dt_aligned_pixel_t out
void * dt_alloc_align(size_t size)
Allocate cacheline-aligned memory.
Definition darktable.c:508
dt_map_box_t bbox
Definition location.c:4
@ DT_DEBUG_PERF
Definition logging.h:55
@ DT_DEBUG_MASKS
Definition logging.h:62
int32_t dt_get_debug_flags(void)
Definition darktable.c:2085
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
float *const restrict const size_t k
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
The per-shape function table, private to the masks implementation.
#define DT_MASKS_OUTLINE_TILT_MAX
#define OUTLINE_BLOCK
static gboolean _outline_sample_repeats(const _outline_boundary_t *const b, const float bx, const float by, const float walk)
#define OUTLINE_REPEAT_MIN_WALK
static void _outline_window(const _outline_boundary_t *const b, const int d0, int *const lo, int *const hi)
static int _outline_grid_cell(const _outline_disc_grid_t *const g, const float x, const float y)
static void _outline_keep_specks(uint8_t *const dropped, const int n)
static gboolean _outline_grid_build(_outline_boundary_t *const b, const float *const bbox, const float r_max)
static float _outline_discs_from_outline(const float *const points_h, const float *const border_h, const int n, _outline_boundary_t *const b, float *const bbox)
static gboolean _outline_far_contains(_outline_boundary_t *const b, const int lo, const int hi, const float bx, const float by)
void dt_masks_outline_envelope_offset(const float *centre, float dx, float dy, float radius, float radius_rate, float *out)
static gboolean _outline_cell_repeats(const _outline_boundary_t *const b, const unsigned h, const float bx, const float by, const float walk_limit)
int dt_masks_outline_boundary_skips(const float *const points, const float *const border, const int count, const int header, dt_masks_skip_range_t **skips_out)
static void _outline_boundary_free(_outline_boundary_t *const b)
static int _outline_probe_samples(_outline_boundary_t *const b, uint8_t *const dropped, const int n)
static gboolean _outline_sample_inside(_outline_boundary_t *const b, const int i, const float bx, const float by)
static gboolean _outline_cells_build(_outline_boundary_t *const b, const int n)
static int _outline_skips_from_dropped(const uint8_t *const dropped, const int n, const int header, dt_masks_skip_range_t **const skips_out)
static int _outline_settle_span(_outline_boundary_t *const b, uint8_t *const dropped, const int from, const int to, const int agreed)
static gboolean _outline_next_dropped_run(const uint8_t *const dropped, const int n, int *const cursor, int *const from, int *const to)
static gboolean _outline_boundary_alloc(_outline_boundary_t *const b, const int n)
void dt_masks_outline_offset_along(const float *const centre, float dx, float dy, const float radius, float *const border)
gboolean dt_masks_outline_short_way(const float *const centre, const float *const from, const float *const to, const gboolean default_clockwise)
static unsigned _outline_cell_hash(const int cx, const int cy)
static gboolean _outline_discs_contain(_outline_boundary_t *const b, const int lo, const int hi, const float bx, const float by)
The masks vocabulary: the enumerations a caller needs to NAME a shape or an operation,...
#define M_PI
Definition math.h:47
#define dt_free_align(ptr)
Release memory from dt_alloc_align() and set ptr to NULL.
Definition mem_alloc.h:214
size_t size
Definition mipmap_cache.c:3
#define dt_pixelpipe_cache_alloc_align_cache(size, id)
const float r
_outline_disc_grid_t grid
_outline_cells_t cells
_outline_discs_t discs
const float * border_h
_outline_blocks_t blocks
One cut in a shape's border outline: while walking the border buffer forward, on reaching index jump_...
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29
static double dt_get_wtime(void)
Definition times.h:43