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:
+109
-7
@@ -865,6 +865,20 @@ pub struct DevelopSession {
|
||||
selected_spot: Option<String>,
|
||||
/// Whether to draw the false-coloured region overlay.
|
||||
show_overlay: bool,
|
||||
/// TRACES: FR-DEV-19c
|
||||
/// How the selected layer's mask is being shown, or `None` for not at all.
|
||||
///
|
||||
/// Interface state, like `show_overlay` beside it and `active_masks` above
|
||||
/// — it changes no pixel of the photograph, it is not in the sidecar and
|
||||
/// it is not on the undo stack. It reaches the pipeline as an argument to
|
||||
/// the one composition that draws the canvas, which is what makes an
|
||||
/// export structurally unable to carry it (`EditGraph::compose_revealing`).
|
||||
///
|
||||
/// The *style* is stored and not the layer, because the layer is always
|
||||
/// the selected one: a reveal that stayed pointed at a mask nobody was
|
||||
/// working on would be describing the wrong thing every time the selection
|
||||
/// moved, and there is no gesture that wants it.
|
||||
reveal_style: Option<dr_pipeline::mask::RevealStyle>,
|
||||
/// Which attribute the panel is filtered to, or all of them.
|
||||
///
|
||||
/// `None` is "show everything" and is what a frontend that ignores
|
||||
@@ -1020,6 +1034,7 @@ impl DevelopSession {
|
||||
),
|
||||
selected_spot: None,
|
||||
show_overlay: false,
|
||||
reveal_style: None,
|
||||
active_tab: None,
|
||||
curve_channel: 0,
|
||||
display_space: dr_types::ColourSpace::Srgb,
|
||||
@@ -1958,7 +1973,22 @@ impl DevelopSession {
|
||||
}
|
||||
};
|
||||
|
||||
for layer in self.graph.masks().active() {
|
||||
// TRACES: FR-DEV-19c
|
||||
// The reveal is in the key because it is in the *sequence*: revealing
|
||||
// a layer with no adjustment on it gives that layer a slot, which
|
||||
// renumbers every field after it. Leaving it out is the bug where
|
||||
// clicking a subject shows the mask of whichever layer happened to be
|
||||
// beneath it.
|
||||
let reveal = self.reveal();
|
||||
mix(reveal.as_ref().map_or(0, |r| {
|
||||
let mut h: u64 = 1;
|
||||
for b in r.layer.as_bytes() {
|
||||
h = h.wrapping_mul(31).wrapping_add(*b as u64);
|
||||
}
|
||||
h
|
||||
}));
|
||||
|
||||
for layer in self.graph.masks().rendered(reveal.as_ref()) {
|
||||
match &layer.base().source {
|
||||
MaskSource::Subject { index, .. } => {
|
||||
mix(1);
|
||||
@@ -2147,10 +2177,13 @@ impl DevelopSession {
|
||||
}
|
||||
};
|
||||
|
||||
// In `active()` order, because that is the order the rasteriser walks
|
||||
// and the order it indexes these by.
|
||||
// In `rendered()` order, because that is the order the rasteriser
|
||||
// walks and the order it indexes these by — the revealed layer
|
||||
// included, which is why the reveal is asked for here and folded into
|
||||
// the key above.
|
||||
let reveal = self.reveal();
|
||||
let mut fields: Vec<Vec<f32>> = Vec::new();
|
||||
for layer in self.graph.masks().active() {
|
||||
for layer in self.graph.masks().rendered(reveal.as_ref()) {
|
||||
let field = match self.layer_coverage(layer, pw, ph) {
|
||||
Some(coverage) => {
|
||||
dr_segment::Shaped::build(
|
||||
@@ -2193,7 +2226,16 @@ impl DevelopSession {
|
||||
}
|
||||
|
||||
fn rasterise_masks(&mut self) -> bool {
|
||||
if self.graph.masks().is_neutral() {
|
||||
// TRACES: FR-DEV-19c
|
||||
// A stack that changes no pixel is normally not worth a pass — except
|
||||
// when one of its layers is being looked at, which is exactly the
|
||||
// state a fresh selection is in. Asking `rendered_count` rather than
|
||||
// `is_neutral` is what makes "click a category, see its mask" work at
|
||||
// all: before it, the array was never rasterised, the slice the reveal
|
||||
// samples held whatever was last in it, and the answer was a blank
|
||||
// photograph.
|
||||
let reveal = self.reveal();
|
||||
if self.graph.masks().rendered_count(reveal.as_ref()) == 0 {
|
||||
return false;
|
||||
}
|
||||
// **Source space, at the segmentation's proxy size** — not the
|
||||
@@ -2239,13 +2281,14 @@ impl DevelopSession {
|
||||
// No label field: region masks were the watershed's, and nothing
|
||||
// produces one any more. A stored layer that still names regions is
|
||||
// skipped by the rasteriser rather than drawn wrong.
|
||||
pass.render(
|
||||
pass.render_revealing(
|
||||
self.graph.masks(),
|
||||
None,
|
||||
subjects,
|
||||
Some(source.as_ref()),
|
||||
pw,
|
||||
ph,
|
||||
reveal.as_ref(),
|
||||
)
|
||||
.inspect_err(|e| log::warn!("mask rasterisation failed: {e}"))
|
||||
.is_ok()
|
||||
@@ -2426,6 +2469,60 @@ impl DevelopSession {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Seeing the mask (FR-DEV-19c)
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/// TRACES: FR-DEV-19c
|
||||
/// What the canvas should draw over the photograph, if anything.
|
||||
///
|
||||
/// **Only when exactly one layer is selected**, on the same rule the part
|
||||
/// list and the brush follow: a mask is one thing, and drawing the union
|
||||
/// of three because three rows were control-clicked would answer a
|
||||
/// question nobody asked. `None` there rather than picking the first, so
|
||||
/// the reveal disappearing is itself the signal that the selection is not
|
||||
/// what it needs to be.
|
||||
///
|
||||
/// Rebuilt per call rather than kept in step with the selection, because
|
||||
/// it is two fields and a clone of one id — cheaper than the invalidation
|
||||
/// a cached copy would need every time a layer is added, removed, renamed
|
||||
/// or reordered.
|
||||
pub(crate) fn reveal(&self) -> Option<dr_pipeline::mask::Reveal> {
|
||||
let style = self.reveal_style?;
|
||||
let [id] = self.active_masks.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
Some(dr_pipeline::mask::Reveal {
|
||||
layer: id.clone(),
|
||||
style,
|
||||
})
|
||||
}
|
||||
|
||||
/// How the selected layer's mask is being shown, as an index into
|
||||
/// [`dr_pipeline::mask::RevealStyle::ALL`], or `0` for not at all.
|
||||
///
|
||||
/// An index because the panel offers it as a strip of chips and an index
|
||||
/// is what a strip of chips reports. The enum stays the thing that is
|
||||
/// stored, so a fourth style is a variant and a label rather than a number
|
||||
/// two files have to agree on.
|
||||
pub fn mask_view(&self) -> usize {
|
||||
use dr_pipeline::mask::RevealStyle;
|
||||
self.reveal_style
|
||||
.and_then(|s| RevealStyle::ALL.iter().position(|&a| a == s))
|
||||
.map_or(0, |i| i + 1)
|
||||
}
|
||||
|
||||
/// Show the selected layer's mask, or stop.
|
||||
///
|
||||
/// Takes no history step and marks nothing dirty: this is how the
|
||||
/// photograph is being *looked at*, not an edit to it.
|
||||
pub fn set_mask_view(&mut self, view: usize) {
|
||||
use dr_pipeline::mask::RevealStyle;
|
||||
self.reveal_style = view
|
||||
.checked_sub(1)
|
||||
.and_then(|i| RevealStyle::ALL.get(i).copied());
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// The region overlay
|
||||
// ----------------------------------------------------------------------
|
||||
@@ -3738,7 +3835,12 @@ impl DevelopSession {
|
||||
// always entered the structure hash. Moving the window to a P3 panel
|
||||
// therefore costs one shader compile and no pipeline change at all.
|
||||
let space = self.display_space;
|
||||
let shader = self.graph.compose_for(space);
|
||||
// TRACES: FR-DEV-19c
|
||||
// **The one composition that may show a mask.** Every other caller of
|
||||
// the graph — `render_the_file`, the thumbnail, `sample_as_shot` —
|
||||
// goes through `compose_for`, which cannot ask for a reveal, so no
|
||||
// exported file can carry one.
|
||||
let shader = self.graph.compose_revealing(space, self.reveal().as_ref());
|
||||
|
||||
// Rasterise the masks first: the shader addresses array slices by
|
||||
// index, so the array has to describe *this* stack before it is bound.
|
||||
|
||||
Reference in New Issue
Block a user