Skip to content

Path shrink#21370

Open
masterpiga wants to merge 1 commit into
darktable-org:masterfrom
masterpiga:path_shrink
Open

Path shrink#21370
masterpiga wants to merge 1 commit into
darktable-org:masterfrom
masterpiga:path_shrink

Conversation

@masterpiga

@masterpiga masterpiga commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

This adds a true grow/shrink (outset/inset) operation to path masks, complementing the existing centroid-based "size" scaling. Instead of scaling node positions about the centroid — which only enlarges the existing outline proportionally — this offsets the actual boundary by a constant distance, the way a morphological dilation/erosion would. Concave bays, thin necks and corners behave correctly: a grow rounds outer corners and fills narrow gaps, a shrink can pinch a thin shape into two, etc.

It is exposed both as a scroll-wheel gesture in the darkroom and as a new "shrink or grow" slider in the mask manager, with a px / % unit toggle.

Screen.Recording.2026-06-20.at.07.34.45.mp4

How it works

A grow/shrink can't be done analytically on Bézier nodes, so each operation runs a three-stage pipeline (_path_morph_core in path.c):

  1. Rasterize the closed Bézier outline to a binary mask via Cairo (_path_rasterize), at a configurable internal resolution.
  2. Offset the boundary using an exact Euclidean Distance Transform (Meijster 2000, two-pass separable, O(N)) — _edt_compute/_edt_row. Grow keeps every pixel within r of the inside; shrink keeps inside pixels farther than r from the outside. The threshold is sub-pixel (compared in squared distance), so the offset is smooth rather than blocky.
  3. Re-vectorize with potrace (ras2forms), pick the largest true outer ('+') contour, simplify the dense boundary with Ramer–Douglas–Peucker (_path_simplify_rdp), normalize coordinates and regenerate Catmull-Rom handles.

Key design choices

  • Baseline + offset-keyed cache (lossless, non-compounding). Every grow/shrink is lossy and expensive, so applying them in sequence would both compound error and recompute needlessly. Instead each form gets a _resize_state_t holding a baseline (a deep copy of the points when resizing first started) plus a GHashTable of results keyed by the signed pixel offset. Every operation is computed from the baseline, so:

    • the slider/scroll value is an absolute offset from the baseline — moving to +5 then +8 gives exactly the same result as going straight to +8, and 0 restores the original;
    • revisiting an offset (scrolling back and forth, or returning the slider) is a cache hit — the exact prior shape is restored with no recomputation and no information loss;
    • the scroll gesture and the slider share one baseline and one cache, so mixing them stays coherent.

    The baseline is captured lazily on the first resize and invalidated whenever the shape changes by other means (node move/add/delete, segment drag, deletion), so the next resize re-baselines from the edited shape.

  • Corner preservation. RDP keeps potrace's per-node Bézier handles, including the zero-length handles at genuine corners, so sharp features survive a resize instead of being rounded away. A "sharp" / "balanced" / "smooth" preference trades node count against smoothness (it tunes both the potrace alphamax and the RDP tolerance, the latter expressed in image pixels so it means the same at any tracing resolution).

  • Self-intersection guard. Re-vectorized nodes can be packed tightly enough that auto-generated Catmull-Rom handles exceed their edge chord and create visible loops. _path_clamp_ctrl_points clamps handles to the chord length — but only after re-vectorization, never on hand-drawn masks.

  • Robustness. Large EDT buffers use g_try_malloc_n so an allocation failure no-ops the resize instead of aborting darktable. An over-shrink that erases the shape rolls back to the displayed shape and offset rather than snapping to the baseline. Clone/retouch sources are re-anchored so they don't drift when the path's first node moves.

Shortcut / gesture remapping

The plain scroll wheel over a selected path previously did centroid "size" scaling. That gesture is preserved but moved to a modifier, freeing plain scroll for the new operation:

  • Scroll: grow/shrink (outset/inset)
  • Ctrl+Shift+Scroll: change size (centroid scale)

Opacity and feather shortcuts are unchanged.

The on-canvas hint message and the mouse-action help list are updated to match.

User-visible changes

  • New "shrink or grow" slider in the masks manager, shown only when a single path is selected. Its quad toggles the unit between px and % of path size; the value is a signed offset from the shape's baseline (0 = original). Commits are debounced (~180 ms) since morphing is expensive, and the slider mirrors the current offset even after a scroll-wheel resize.
  • New preferences under a dedicated darkroom → masking section in darktableconfig.xml.in:
    • masks/path_resize_amount — scroll step size;
    • masks/path_resize_unit — px or % of path size;
    • masks/path_resize_resolution — internal tracing resolution (512–4096), trading detail for speed;
    • masks/path_resize_curve_smoothing — sharp / balanced / smooth.
    • The existing masks_scroll_down_increases and show_mask_indicator keys are also relocated into the new masking section.

Main code changes

  • src/develop/masks/path.c — the whole pipeline: rasterize / EDT / re-vectorize / RDP, the per-form baseline + result cache, the scroll (_path_resize_morph) and slider (_path_resize_amount) entry points, _path_resize_get for slider mirroring, cache invalidation at every editing call site, and the extracted legacy _path_resize_centroid.
  • src/develop/masks.h — two new optional vtable entries on dt_masks_functions_t: resize (absolute, baseline-relative, cached) and resize_get (report current offset). Implemented only by path masks for now.
  • src/libs/masks.c — the masks-manager slider, its px/% quad (custom paint), debounced commit, selection-aware visibility (_selected_single_path), and offset mirroring. The path mask owns the baseline/cache, so the lib side just sets and reads the current offset.

Co-authored with Claude.

@masterpiga masterpiga added this to the 5.8 milestone Jun 20, 2026
@masterpiga masterpiga added feature: enhancement current features to improve difficulty: average some changes across different parts of the code base scope: UI user interface and interactions scope: image processing correcting pixels labels Jun 20, 2026
@TurboGit

Copy link
Copy Markdown
Member

Windows compile error:

In file included from D:/a/_temp/msys64/ucrt64/include/windows.h:84,
                 from D:/a/_temp/msys64/ucrt64/include/winsock2.h:23,
                 from D:/a/darktable/darktable/src/src/win/win.h:5,
                 from D:/a/darktable/darktable/src/src/common/darktable.h:33,
                 from D:/a/darktable/darktable/src/src/common/colorlabels.h:21,
                 from D:/a/darktable/darktable/src/src/bauhaus/bauhaus.h:23,
                 from D:/a/darktable/darktable/src/src/develop/masks/path.c:19:
D:/a/darktable/darktable/src/src/develop/masks/path.c:1948:49: error: expected ';', ',' or ')' before numeric constant
 1948 | static void _edt_compute(const int *seeds, int *edt2, const int rw, const int rh)
      |                                                 ^~~~
compilation terminated due to -Wfatal-errors.

@masterpiga

masterpiga commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, done, there was a name conflict with some transitive import.

Grow/shrink a path mask by rasterizing it to a bitmap, applying an
exact Euclidean distance transform (Meijster 2000) to offset the
boundary, then re-vectorizing with potrace. Includes a LIFO per-form
cache to avoid recomputing at previously visited steps. Exposes user
preferences for step amount, unit (px / % of path size), tracing
resolution, and curve smoothing.

Adds options to the mask manager to grow/shrink a mask by a specific
amount for fine grained control.

Re-routes the pre-existent resize algo (distance from centroid) to use
CTRL+SHIFT+scroll, so that the two algorithms can co-exist.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

difficulty: average some changes across different parts of the code base feature: enhancement current features to improve scope: image processing correcting pixels scope: UI user interface and interactions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants