Show the photographer the mask they are shaping

Nobody can refine an edge they are not being shown. The only thing drawn on
the canvas was the region overlay — a false-coloured picture of what the model
*detected* — which knows nothing of a layer's feather, its falloff, its
morphology, its invert or its opacity, and nothing at all about a gradient, a
range or a stroke. Every control added for mask editing therefore acted on
something invisible, which is why the whole feature reads as absent rather
than as unfinished.

A layer's finished mask now draws over the photograph in one of three styles:
a tint for whether the right thing is selected, an alpha for where the edge
is, an outline for whether that edge is registered against the detail the
other two hide.

The hard part is not the shader. A selection with no adjustment on it changes
no pixel, so it is not active, so it holds no slice of the mask array and is
never rasterised — and that is exactly the layer somebody wants to look at,
for the whole of the time between choosing a subject and deciding what to do
to it. So `MaskStack::rendered` is `active()` plus the layer being looked at,
and the rasteriser, the composer and the distance-field builder all index by
position in it. Which is also why the design's "two uniforms, no recompile" is
not available: a uniform can select a slot, it cannot conjure one.

The reveal is never on the graph. It reaches the pipeline as an argument to
`compose_revealing`, and `compose_for` — which the exporter, the thumbnail and
the neutral probe all call — has no way to ask for one. A flag on the graph
would have been shorter, would have type-checked, and would have been one
forgotten reset away from a red tint baked into an exported file.

And the tools that shape a mask now arm. `Masking.tool` is an `in` property
only Rust may write, and the handler wrote nothing back, so the strip reported
"Select" however many times Paint was pressed and the paint area was never
enabled — the brush, the parts and the whole of FR-DEV-19b reachable from no
control in the application.

The region overlay stands down while a mask is being shown, and its button now
says what it hides: two overlays that look alike and mean different things is
worse than either.
This commit is contained in:
2026-09-10 20:28:06 +02:00
parent 193b35a249
commit c045702a47
10 changed files with 927 additions and 51 deletions
+96 -19
View File
@@ -36,12 +36,18 @@ the *whole* boundary. When the model's coverage stops two pixels inside the
shoulder and leaks four pixels into the hair, no global number fixes both, and
that is the ordinary case rather than a corner one.
**And you cannot see the mask.** The overlay on the canvas is
[`overlay_rgba`](../ui/dr-ui/src/segmentation.rs) — a CPU-built false-colour
picture of *what the model detected*, at proxy resolution. It is not the
layer's alpha: it knows nothing of the layer's feather, its falloff, its
morphology, its invert, or its opacity. Nobody can refine an edge they are not
being shown.
**And you cannot see the mask.** *(Built — see §6.)* The overlay on the canvas
was [`overlay_rgba`](../ui/dr-ui/src/segmentation.rs) and nothing else — a
CPU-built false-colour picture of *what the model detected*, at proxy
resolution. It is not the layer's alpha: it knows nothing of the layer's
feather, its falloff, its morphology, its invert, or its opacity. Nobody can
refine an edge they are not being shown.
That one turned out to be load-bearing for the other three rather than the
last of four. With no way to see a mask, choosing a category produced a layer
whose extent was invisible and whose adjustment had not been touched yet — so
the correct behaviour and the broken one look identical, and "the segmentation
does not make masks" is what it reads as from the outside.
Those four are one feature, and this is its specification.
@@ -390,31 +396,89 @@ usable along hair.
## 6. Seeing the mask
The mask array is **already bound to the composed adjust shader** — this is
almost free, and it is the first thing to build, because every other tool here
**Status: built.** `MaskStack::rendered`, `RevealStyle`,
`EditGraph::compose_revealing`, `MaskPass::render_revealing`, and the "Show
mask" strip in the Local panel.
The mask array is **already bound to the composed adjust shader**, so this is
almost free, and it is the first thing to build because every other tool here
is unusable without it.
Two uniforms in the composed shader: which layer to reveal (−1 for none) and
which style. The block is always emitted, guarded by the uniform, so switching
the overlay on is a uniform write rather than a shader recompile.
### 6.1 One correction to this section, found in the building
The draft said "two uniforms — which layer to reveal (−1 for none) and which
style — always emitted and guarded by the uniform, so switching the overlay on
is a uniform write rather than a shader recompile". The second half of that is
not available, and the reason is the case the feature exists for.
A uniform can *select* a slot. It cannot conjure one. A layer with no
adjustment on it changes no pixel, so it is not `is_active`, so it occupies no
slice of the mask array and the rasteriser never draws it — and that is
precisely the layer a photographer wants to look at, for the whole of the time
between choosing a subject and deciding what to do to it. Revealing it means
*rendering* it, which changes the sequence of layers, which changes the
uniform block. The composition moves either way.
So the slot and the style are written into the source, and turning the reveal
on, off, or onto another layer recompiles the fused shader. That is a button
press rather than a frame, and the uniform would only have added a branch per
pixel on top of a recomposition that was happening anyway.
`MaskStack::rendered(reveal)` is the one sequence this rests on: `active()`
plus the layer being looked at. The rasteriser, the composer and the distance
field builder all index by position in it, so all three must be given the same
`reveal` — two of them disagreeing shows as an adjustment applied through
another layer's mask, which is why they take it as an argument rather than
reading a flag.
### 6.2 Where the block runs, and why not with the others
After the output transform, immediately before the clip and the encode — not
among the layer blocks. Everything there runs on scene-referred colour in the
working space, where a flat tint would be pushed through the base curve and
the camera matrix and arrive as some other colour, and an alpha's white on
black would arrive as neither.
### 6.3 Not on the graph
The reveal is an argument to `EditGraph::compose_revealing`, and
`compose_for` — which the exporter, the thumbnail and the neutral probe all
call — has no way to ask for one. A flag on the graph would have been fewer
parameters, would have type-checked, and would have been one forgotten reset
away from a red tint baked into an exported file.
Three styles, all read from the same alpha:
- **Tint** — the mask over the picture in a flat colour at ~50%. The default,
and what every editor's photographers already expect. Red by default and
configurable, since a red tint over a red dress shows nothing.
and what every editor's photographers already expect. Red, and *not yet*
configurable: the case for choosing the colour is a red tint over a red
dress, and the answer to that is the Alpha style beside it until somebody
finds a picture where neither works.
- **Alpha** — the mask alone, white on black. For judging an edge, where a
tint over a busy picture cannot be read.
- **Edge** — the boundary outlined over the untouched picture. For checking
registration against detail the other two hide, and the same reasoning the
region overlay's white outline already carries.
**When it appears.** Automatically while a mask tool is armed, while a part
row is selected, and for ~1s after a shaping slider is released; manually from
a control in the Local panel. The existing `overlay-hidden` property is the
precedent and the trap it documents applies unchanged: the photographer's
switch and the automatic reveal are separate questions, and an automatic
reveal must never silently re-arm a switch the photographer turned off.
**When it appears.** From the "Show mask" strip in the Local panel, which sits
beside the tool strip and under the same condition — both describe one
selected mask. Off is the resting state, so entering local mode does not paint
a photograph red.
Automatic in exactly one place: arming Paint or Erase turns the tint on if
nothing was showing the mask, which is the same nudge `on_part_added` makes and
on the same argument — a stroke into an invisible mask is indistinguishable
from a tool that did nothing.
A nudge on an explicit action, never a standing rule. Turning the view off and
then picking the eraser leaves it off. The existing `overlay-hidden` property
is the precedent and the trap it documents applies unchanged: an automatic
reveal that re-arms a switch somebody turned off is worse than no automatic
reveal at all.
The two remaining triggers in the draft — while a part row is selected, and for
~1s after a shaping slider is released — are not built. The second is the one
worth returning to, and it is a timer rather than a decision.
The region overlay stays exactly what it is — a picture of what the model
detected — and gains a name in the interface that says so, because two
@@ -597,6 +661,19 @@ the mask pass beyond the blend variants; no change to distance fields, because
a painted part needs none. *This is the whole of the user-visible ask except
push and intersect,* and it is deliberately the milestone that stands alone.
Done: parts with `Union` and `Subtract`, painted parts, the tool strip, the
canvas gesture, and the overlay (§6). Outstanding: the brush HUD and cursor —
radius, hardness and flow are sliders with no ring drawn on the photograph,
so the size of the brush is a number rather than a thing you can see — and
the incremental raster of §5.4, without which a layer's whole stroke history
is redrawn per dab.
One lesson from the order it was actually built in, since §6 said it and the
build did not listen: the overlay is not the last quarter of M1, it is the
first. Parts and painting shipped without it and the result was a feature
nobody could tell was working — the panel listed a mask, the photograph showed
nothing, and every report of it came back as "the masks do not work".
**M2 — Combine properly.** `Intersect`, model and gradient and range parts,
per-part distance fields (§5.5), the part list with its join chips, folding
two layers.