Give every mask an eye and a colour, and put the brush where the mask is

The first build of seeing a mask showed the selected layer's, in one global
style, from a strip at the top of the panel. It answered the wrong question and
answered it somewhere nobody looked. What a photographer asks of two masks is
how they meet — where the sky's edge sits against the building's — and that
needs both on screen at once, in colours that can be told apart.

So each row of the stack has an eye, drawn in the colour its mask is shown in,
and each mask has six swatches to choose that colour from. Several can be open
at once; a new one comes up open, in the first colour nothing else is using.
The style — tint, alpha, outline — is the one setting that stays global, above
the stack, because three styles at once are three pictures that cannot be read
against each other. Alpha now draws every shown mask, each in its colour, on
black. In the pipeline a `Reveal` is a list of `(layer, colour)` rather than
one layer, and every reveal block carries its own colour.

The brush moves too. Select, Paint and Erase and the three sliders under them
sat at the top of the panel, appeared only once a row was selected, and said
nothing about which mask they acted on — so "how do I paint" and "how do I
correct the model's outline" both had the same answer and nobody found it.
They sit under the selected mask's parts now, beside the swatches, and on a
subject or a category the hint says what a stroke there does: it becomes a
part of this mask, joined to the model's, and can be taken out again.

Eyes and colours are viewing state, on the session and not on the layer, so a
photograph reopened has every eye closed — the stored-mask round-trip test
asserts it.
This commit is contained in:
2026-09-11 19:03:51 +02:00
parent 936490880b
commit a87139b838
12 changed files with 759 additions and 287 deletions
+35 -24
View File
@@ -396,9 +396,10 @@ usable along hair.
## 6. Seeing the mask
**Status: built.** `MaskStack::rendered`, `RevealStyle`,
`EditGraph::compose_revealing`, `MaskPass::render_revealing`, and the "Show
mask" strip in the Local panel.
**Status: built.** `MaskStack::rendered`, `Reveal` as a list of
`(layer, colour)`, `RevealStyle`, `EditGraph::compose_revealing`,
`MaskPass::render_revealing`; on the panel, an eye and a colour per row and one
"Show masks as" strip above the stack.
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
@@ -449,36 +450,46 @@ 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, 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.
- **Tint** — the mask over the picture in its colour at ~50%. The default,
and what every editor's photographers already expect. The colour is the
mask's own, chosen from the swatches on its row — which is what answers a
red tint over a red dress.
- **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.** 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.
**Per mask, not per selection.** The first build of this showed the *selected*
layer's mask in one global style, and it answered the wrong question. What a
photographer asks of two masks is how they meet — where the sky's edge sits
against the building's — and that needs both on screen at once, in colours that
can be told apart. So each row of the stack has an eye, and each mask a colour
from a six-entry palette (`MASK_COLOURS` in `develop.rs`); the eye is drawn in
that colour so the row says which shape on the picture is its. The style is
the one thing that stays global, because a tint beside an outline beside an
alpha would be three pictures that cannot be read against each other. Alpha
therefore draws every shown mask, each in its colour, on black.
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.
**When it appears.** A new layer arrives with its eye open, in the first colour
nothing else is using — making a mask is asking what it selected, and for a
subject or a category that question has no other answer on screen. Arming
Paint or Erase opens the selected layer's eye if it was closed, on the same
argument `on_part_added` makes: a stroke into an invisible mask is
indistinguishable from a tool that did nothing. Pressing a swatch opens the
eye too, since colouring a mask nobody can see would change no pixel.
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.
Only ever *this* layer's eye, and only on an explicit action. Every other eye
keeps whatever it was set to, and nothing re-arms in the background. 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.
Viewing state and not edit state: eyes and colours are on the session, not on
the layer, and a photograph reopened has every eye closed.
The ~1s reveal after a shaping slider is released, from the draft, is not
built. 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