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
+11 -2
View File
@@ -5,7 +5,7 @@
Every entry here is extracted from the comment beside the code that implements it, so this file cannot describe a gesture the application does not have. Add one by writing a `GESTURE:` block next to the implementation; there is nowhere else to write it.
36 gestures, in 4 places.
37 gestures, in 4 places.
## Develop
@@ -139,7 +139,16 @@ The column is 280px wide and the colour mixer alone puts thirty-six of these in
Disabling a layer is the before-and-after a local edit constantly wants, so it is one press away rather than inside the row. It is an edit and does take a history step, unlike holding "Before" — the layer really is off until it is switched back on.
<sub>`ui/dr-ui/ui/masks.slint:196`</sub>
<sub>`ui/dr-ui/ui/masks.slint:209`</sub>
### Show or hide one mask on the photograph
- **Touch** — Tap the eye on its row
- **Pointer** — Click the eye on its row
A mask is judged by seeing where it falls, and two are judged by seeing where they meet — so each row has its own eye rather than the panel having one, and the eye is drawn in the colour the mask shows in, so the row says which shape on the picture is its. Nothing about the edit changes: this is how the photograph is looked at, and takes no history step.
<sub>`ui/dr-ui/ui/masks.slint:274`</sub>
## Collections sidebar
+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
+9 -7
View File
@@ -517,13 +517,15 @@ hardness and flow the photographer sets. Strokes are stored as normalised source
rasterised on the device, so a correction stays on what it was painted on through a crop, a zoom and
an export at any size. A whole stroke is one step in the history.
**FR-DEV-19c — Seeing the mask.** The mask a selected layer actually resolves to shall be drawable
over the photograph, in a tint, as an alpha, or as an outline. What is shown is the finished mask —
every part folded, with the layer's feather, falloff, morphology, invert and opacity applied — and
it is shown for a layer that carries no adjustment yet, which is the state every mask is in for its
first few seconds. No rendered output ever carries it: the reveal is a property of looking at an
edit, not of the edit, so it reaches the pipeline through the composition that draws the canvas and
through no other.
**FR-DEV-19c — Seeing the mask.** Each layer's mask shall be drawable over the photograph, switched
per layer by an eye on its row and drawn in a colour of that layer's own, so that several can be
shown at once and told apart; the style — a tint, an alpha, or an outline — is one setting for all
of them. What is shown is the finished mask — every part folded, with the layer's feather, falloff,
morphology, invert and opacity applied — and it is shown for a layer that carries no adjustment
yet, which is the state every mask is in for its first few seconds. No rendered output ever carries
it: the reveal is a property of looking at an edit, not of the edit, so it reaches the pipeline
through the composition that draws the canvas and through no other, and it does not travel in a
sidecar.
Nobody can refine an edge they are not being shown. Before this the only thing drawn on the
photograph was FR-DEV-3's region overlay — a false-coloured picture of what the *model detected*,
+36 -36
View File
File diff suppressed because one or more lines are too long