Better mask editing near/outside image borders.#21382
Conversation
- When creating a mask, expand the canvas around the image to allow to position points outside the image area. - When editing mask elements, the canvas is always expanded to make all control points accessible.
|
I like it a lot, I always got frustrated from the automatic viewport centering. I quickly tested the changes in this PR, but it seems that the vertical centering is still active. E.g. in the following image I want to zoom in into the waypost to read it, but once I start zooming, the content behind my curser moves to the right instead of staying behind my cursor, eventually the waypost would move out of the viewport, which requires me to use panning gestures to bring it back into the viewport: Screencast.from.2026-06-21.13-07-04.mp4What do you think about this? Do you think that would make sense. And if yes, whether this would be related to the functionality within this PR or rather out-of-scope. |
|
One regression I noticed when testing: the two-finger panning gesture stopped working and now performs zoom-scrolling, which previously needed the additional CTRL key. |
|
Thanks for your feedback, @da-phil.
Ah, I see, you would like the image to zoom towards the cursor position, not towards the center. That makes sense, but I would say it is a different feature. Maybe you can open a feature request for tracking the feature, so that we can discuss if it's something that we want darktable to support.
I can't reproduce, gestures work fine for me (on mac). Maybe you have deselected this option?
|

What this does
In the darkroom, mask control points (nodes, feather/border handles, clone sources) can be dragged outside the image rectangle — and a handle that ends up off the picture stays reachable. Previously the viewport could never pan past the image, so any handle outside the frame was permanently unreachable: you couldn't grab it, and at the fit zoom it wasn't even on screen. This makes off-image mask editing work at every zoom level, including below fit.
The governing constraint is preserved: at fit the image still fills the view, and the image can never be panned completely off-canvas. Only the editable canvas is allowed to grow beyond the image, and only while a mask overlay is being edited.
Screen.Recording.2026-06-21.at.08.34.05.mp4
Why
darktable clamps the viewport centre so the image edge can never cross the viewport edge (
zoom_x = CLAMP(zoom_x, boxw/2 - .5, .5 - boxw/2)). That is exactly what makes a mask handle sitting outside the image impossible to reach. Lifting the clamp naively would let the user shove the whole picture off-screen, so instead the clamp is made mask-aware: the canvas grows only as much as the current overlay needs, and only while editing.How it works
1. Mask-aware viewport clamp —
src/develop/develop.cdt_dev_zoom_move's hard clamp is replaced by_clamp_zoom_to_mask(). The editable canvas is the image, optionally enlarged by:MASK_CREATION_MARGIN(0.15) on all sides while drawing, so new nodes can be dropped outside the picture;_dev_mask_overlay_bounds()), plusMASK_HANDLE_MARGIN(0.03) so an off-image handle isn't flush against the border.On each axis the viewport centre is clamped into
[home .. node], where home is the normal darktable position (image centred, or image edge pinned to viewport edge when zoomed past fit) and node is the extended off-image bound. Two design points matter here:2. Clip & assessment frame driven by image geometry, not the render ROI —
src/views/view.cThe image clip rectangle (and the color-assessment frame) are now positioned from the viewport centre (
zoom) and the image's own pixel size, intersected with the viewport — instead of from the backbuf ROI centre (offset). The render ROI is clamped to the image, so while the viewport is panned off-picture,offsetlags the livezoomin discrete re-render steps. Driving the clip from the image geometry makes the clip track the content smoothly.3. Coverage test measured against the reachable centre —
src/views/view.cThe
use_preview_fallbackcoverage check is measured againstreach_*(the closest centre the clamped-to-image ROI can actually render) rather than the raw pannedzoom. Off-image,offsetcan get no closer tozoomthan the image edge, so measuring against rawzoomalways reported the off-image background margin as "not covered" and re-triggered the pipe every frame. Measuring against the reachable centre stops that render loop — onceoffsetreaches it, a re-render cannot improve coverage and the residual is harmless background.4. Overlay alignment correction is capped —
src/views/darkroom.cOverlay points live in preview-pipe space and are shifted onto the full-pipe image by the sub-pixel difference between the two viewport centres. When panning far outside the image, the preview-pipe forward transform can extrapolate a non-linear distortion that blows up and drags the overlay off the content. The correction is now
CLAMPed to ±1.5 px (its normal sub-pixel magnitude), so it can never displace the overlay.5. Scrollbars/expose decoupled from the render centre —
src/views/darkroom.cNear fit, darktable zeroes the viewport centre it feeds to the scrollbars (to avoid a feedback resize). That zeroing is now applied only to the scrollbar values (
sb_zoom_*); the realzoom_x/zoom_yare kept for rendering. Otherwise the mask overlay/guides/pickers would pin to the image centre while the image itself follows an off-canvas pan below fit, breaking alignment.exposealso reads the actual stored centre whendt_dev_get_zoom_boundsreports no scrollable bounds, so a pan that exists purely to reach an off-image handle is still rendered.6. Re-validate the canvas when the overlay is hidden —
src/develop/masks/masks.cTurning the mask overlay off (or switching it) shrinks the canvas back to the image, leaving any off-image pan stale.
dt_masks_set_edit_modeissues a no-opDT_ZOOM_MOVEso the new clamp snaps the pan back in bounds immediately — the picture re-centres at once instead of lingering off-centre (which also kept the renderer churning) until the next manual pan.Files changed
src/develop/develop.c— mask-aware viewport clamp + overlay bounds helpersrc/views/view.c— clip/frame from image geometry; coverage vs reachable centresrc/views/darkroom.c— scrollbar/render-centre split; capped overlay correctionsrc/develop/masks/masks.c— re-validate viewport when overlay is hiddenCo-authored with Claude.