Skip to content

Take the dodge/burn mask tint off the canvas while a mask is adjusted - #888

Merged
marcinz606 merged 1 commit into
marcinz606:mainfrom
Icodextrin:feat/local-mask-tint-mute
Aug 18, 2026
Merged

Take the dodge/burn mask tint off the canvas while a mask is adjusted#888
marcinz606 merged 1 commit into
marcinz606:mainfrom
Icodextrin:feat/local-mask-tint-mute

Conversation

@Icodextrin

@Icodextrin Icodextrin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What

The canvas tint that shows a Dodge & Burn mask's extent now mutes for the duration of an edit gesture. This also applies to any other masks that overlap with the mask being modified.

  • Triggered by a press and hold on Burn, Feather or Grade.
  • Grabbing a vertex does the same.
  • Every mask whose tinted area intersects the one being worked on drops its fill too.
  • Outlines and handles stay visible.

Why

The previous workflow required manually hiding the mask, tweaking the values, then revealing the mask if you needed to change the area again. This change makes dodge and burn tweaks more ergonomic.

How overlap is decided

overlapping_masks() (negpy/features/local/logic.py) rasterises each mask on a coarse grid in raw-normalised space, through the same rasterise() that the render, the canvas tint and the printing-notes map already share. Consequences worth noting:

  • The awkward shapes come out right without special cases: a Card Edge covers its whole exposed side of the frame, an inverted mask covers its surround, and a polygon's area is its smoothed outline (which bulges roughly a tenth of an edge past the control points), not the vertex hull.
  • Raw space makes the answer rotation-, zoom- and pan-invariant, since geometry moves every mask together — no screen coords or UV grid involved.

Reviewer notes

  • The muted set is fixed when the gesture starts, not recomputed per frame. Recomputing would make a neighbour blink on and off as a vertex was dragged across it.
  • A tint counts as present above alpha 0.25. Masks sharing a feather tail mute together; masks a clear gap apart do not. At the default feather that boundary falls between a gap of 0.05 and 0.20 of the frame. This threshold is the one knob if the behaviour reads as too eager or too shy.
  • _try_start_vertex_edit was restructured to a single exit so the mute and the repaint happen once instead of being duplicated across its two branches. Behaviour is unchanged: the insert branch is polygon-only, so the oval-center anchor still resolves to None there.
  • Nothing here touches either render engine — the tint is overlay paint only, so there is no CPU/GPU parity surface and no docs/PIPELINE.md change. docs/USER_GUIDE.md gains one bullet in the Dodge & Burn section.

Testing

  • New tests/test_local_overlap.py covers the pure overlap function: symmetry, feather reach in both directions, card edge, invert, and unfinished/missing masks.
  • tests/test_local_overlay.py rebuilt around three masks (selected / crossing / clear) and asserts through a real paint onto a QImage that the first two go to alpha 0 while the third keeps its tint — for both a slider drag and a vertex drag.
  • tests/test_local_sidebar.py asserts all three sliders emit the drag signal on grab and release.
  • make all clean: lint, types, 4285 passed / 11 skipped.

The tint that shows a mask's extent sits exactly over the area its Burn,
Feather and Grade sliders change, so the change was judged through a yellow or
blue veil. The fill now steps aside for the gesture: press a slider or grab a
vertex and it goes, release and it comes back. Outlines and handles stay, so
the mask is still findable while the picture under it is clear.

Every mask whose tinted area meets the one being worked on drops its fill too.
Stacked tints hide the area worst, and adjusting a mask that lies on top of an
existing one was the case with nothing to see through.

overlapping_masks() rasterises each mask on a coarse grid in raw-normalised
space, through the same rasteriser the render, the canvas tint and the
printing-notes map already share. That keeps the awkward shapes honest for
free: a card edge covers its whole exposed side, an inverted mask covers its
surround, and a polygon's area is its smoothed outline, which bulges about a
tenth of an edge past the control points. Raw space also makes the answer
rotation-, zoom- and pan-invariant, since geometry moves every mask together.

The set is fixed when the gesture starts rather than recomputed per frame: a
vertex dragged across a neighbour would otherwise make it blink. A tint counts
as present above alpha 0.25, so masks sharing a feather tail mute together
while ones a clear gap apart do not — at the default feather that boundary sits
between a gap of 0.05 and 0.20 of the frame.

Drawing a brand-new mask over an existing one is not covered: there is no
selected mask yet and an unclosed polygon has no area to intersect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Icodextrin
Icodextrin marked this pull request as ready for review August 18, 2026 16:05
@marcinz606
marcinz606 merged commit 9c48515 into marcinz606:main Aug 18, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants