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.
|
||||
|
||||
@@ -264,6 +264,12 @@ pub(crate) fn sync(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSessio
|
||||
};
|
||||
masking.set_parts(ModelRc::new(VecModel::from(parts)));
|
||||
|
||||
// TRACES: FR-DEV-19c
|
||||
// Pushed rather than assumed, because the session is where it lives and
|
||||
// because it can change without the panel having asked: selecting a
|
||||
// second layer takes the reveal away, since a mask is one thing.
|
||||
masking.set_mask_view(s.mask_view() as i32);
|
||||
|
||||
let (radius, hardness, flow) = s.brush();
|
||||
masking.set_brush_radius(radius);
|
||||
masking.set_brush_hardness(hardness);
|
||||
@@ -279,7 +285,16 @@ pub(crate) fn sync(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSessio
|
||||
// proxy-sized RGBA buffer — a megabyte or so — and rebuilding it on every
|
||||
// slider event would be a memcpy per frame for a picture that changes only
|
||||
// when the level does.
|
||||
match s.overlay_image() {
|
||||
//
|
||||
// TRACES: FR-DEV-19c
|
||||
// **Stood down while a mask is being shown.** The two overlays answer
|
||||
// different questions — this one is what the model *detected*, the reveal
|
||||
// is what a layer resolves to — and both at once is a false-coloured
|
||||
// picture over a tinted one, through which neither can be read. The one
|
||||
// describing the layer being worked on wins, because by the time a mask
|
||||
// has been chosen the detections are what the photographer is choosing
|
||||
// *between* rather than what they are looking at.
|
||||
match s.overlay_image().filter(|_| s.mask_view() == 0) {
|
||||
Some(image) => {
|
||||
window.set_region_overlay(image);
|
||||
window.set_overlay_on(true);
|
||||
@@ -558,6 +573,7 @@ pub(crate) fn wire(
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
let rows = rows.clone();
|
||||
window
|
||||
.global::<Masking>()
|
||||
@@ -587,6 +603,11 @@ pub(crate) fn wire(
|
||||
// The scope changed, so the adjust panel below is now describing a
|
||||
// different chain.
|
||||
sync_rows(&w, &rows, &session);
|
||||
// TRACES: FR-DEV-19c
|
||||
// And so has the picture, when a mask is being shown: the
|
||||
// reveal follows the selection, so choosing another layer
|
||||
// draws another mask.
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
@@ -841,13 +862,66 @@ pub(crate) fn wire(
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
window.global::<Masking>().on_tool_picked(move |_tool| {
|
||||
// The tool itself lives in the interface — it arms a gesture and
|
||||
// changes no pixel — so nothing is set here. What this does is
|
||||
// re-sync, because arming the brush is what makes the parts of a
|
||||
// mask worth showing.
|
||||
let redraw = redraw.clone();
|
||||
window.global::<Masking>().on_tool_picked(move |tool| {
|
||||
// **The tool is written back here, and that is the whole of this
|
||||
// handler.** `Masking.tool` is an `in` property: the panel reads
|
||||
// it to light the right chip and `app.slint` reads it to decide
|
||||
// whether a drag on the photograph paints, but only Rust may write
|
||||
// it. So a version of this that recorded nothing left the strip
|
||||
// reporting "Select" however many times "Paint" was pressed, and
|
||||
// the paint area was never armed — the brush, the parts, the whole
|
||||
// of FR-DEV-19b, reachable from no control in the application.
|
||||
//
|
||||
// It still changes no pixel, which is what the previous note was
|
||||
// getting at: the tool arms a gesture and belongs to the
|
||||
// interface, not to the edit, so it takes no history step and is
|
||||
// not stored.
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let tool = tool.clamp(0, 2);
|
||||
w.global::<Masking>().set_tool(tool);
|
||||
|
||||
// TRACES: FR-DEV-19c
|
||||
// Arming a brush shows the mask, if nothing was showing it. The
|
||||
// same nudge `on_part_added` makes and on the same argument: a
|
||||
// photographer about to correct an edge by hand needs to see the
|
||||
// edge, and "the tool did nothing" is what a stroke into an
|
||||
// invisible mask looks like.
|
||||
//
|
||||
// A nudge on an explicit action, never a standing rule. Turning
|
||||
// the view off and then picking the eraser leaves it off — the
|
||||
// trap `overlay-hidden` documents is that an automatic reveal
|
||||
// which re-arms a switch somebody turned off is worse than no
|
||||
// automatic reveal at all.
|
||||
if tool > 0 {
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
if s.mask_view() == 0 {
|
||||
s.set_mask_view(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-synced because arming the brush is what makes the parts of a
|
||||
// mask worth showing.
|
||||
sync(&w, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
// TRACES: FR-DEV-19c
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window.global::<Masking>().on_mask_view_picked(move |view| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.set_mask_view(view.max(0) as usize);
|
||||
}
|
||||
// A redraw and not only a sync: the reveal is in the composed
|
||||
// shader, so what changed is the picture rather than the
|
||||
// panel.
|
||||
sync(&w, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
// TRACES: FR-DEV-19a
|
||||
@@ -1127,6 +1201,12 @@ pub(crate) fn reset(window: &AppWindow) {
|
||||
masking.set_segmenting(false);
|
||||
masking.set_refining(false);
|
||||
masking.set_segmented(false);
|
||||
// TRACES: FR-DEV-19b | FR-DEV-19c
|
||||
// The tool and the mask view are both about one selected layer, and the
|
||||
// next photograph has none. Left standing, they would arm a brush over a
|
||||
// photograph with nothing to paint into and claim a mask was being shown.
|
||||
masking.set_tool(0);
|
||||
masking.set_mask_view(0);
|
||||
masking.set_masks(ModelRc::new(VecModel::<MaskRow>::default()));
|
||||
masking.set_subjects(ModelRc::new(VecModel::<SubjectRow>::default()));
|
||||
masking.set_categories(ModelRc::new(VecModel::<CategoryRow>::default()));
|
||||
|
||||
+57
-3
@@ -582,7 +582,23 @@ export global Masking {
|
||||
in property <[PartRow]> parts;
|
||||
/// TRACES: FR-DEV-19b
|
||||
/// What a drag on the photograph does: 0 selects, 1 paints, 2 erases.
|
||||
///
|
||||
/// Written by Rust and read here, like every other `in` property on this
|
||||
/// global: `tool-picked` is what the strip reports, and the answer comes
|
||||
/// back through this. A version of the handler that recorded nothing left
|
||||
/// the strip stuck on "Select" and the brush reachable from no control.
|
||||
in property <int> tool: 0;
|
||||
/// TRACES: FR-DEV-19c
|
||||
/// How the selected layer's mask is drawn over the photograph: 0 not at
|
||||
/// all, then tint, alpha and edge — `dr_pipeline::mask::RevealStyle::ALL`
|
||||
/// shifted by one so that zero can mean off.
|
||||
///
|
||||
/// **Not `overlay-hidden`.** That switch belongs to the region overlay,
|
||||
/// which is a picture of what the *model detected*; this is the mask a
|
||||
/// layer actually resolves to, feather, morphology, invert and all. Two
|
||||
/// overlays that look alike and mean different things is worse than
|
||||
/// either, so they are named apart and switched apart.
|
||||
in property <int> mask-view: 0;
|
||||
/// The brush: radius as a fraction of the frame's shorter edge, hardness,
|
||||
/// and flow.
|
||||
in property <float> brush-radius: 0.05;
|
||||
@@ -630,6 +646,8 @@ export global Masking {
|
||||
|
||||
/// TRACES: FR-DEV-19b
|
||||
callback tool-picked(int);
|
||||
/// TRACES: FR-DEV-19c
|
||||
callback mask-view-picked(int);
|
||||
callback brush-changed(float, float, float);
|
||||
/// TRACES: FR-DEV-19a
|
||||
callback part-selected(string, int);
|
||||
@@ -638,6 +656,8 @@ export global Masking {
|
||||
callback part-added(string, int);
|
||||
|
||||
callback add-gradient(bool);
|
||||
/// TRACES: FR-DEV-19b
|
||||
callback add-brush();
|
||||
/// `true` asks for the colour range rather than the brightness one.
|
||||
callback add-range(bool);
|
||||
callback add-subject(int);
|
||||
@@ -680,6 +700,32 @@ export component MaskPanel inherits Rectangle {
|
||||
picked(i) => { Masking.tool-picked(i); }
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-19c
|
||||
// **Seeing the mask, which every tool above is unusable without.**
|
||||
// Nobody can refine an edge they are not being shown, and until this
|
||||
// existed the only thing drawn on the photograph was the model's
|
||||
// detections — which know nothing of a layer's feather, its falloff,
|
||||
// its morphology, its invert or its opacity, and nothing at all about
|
||||
// a gradient or a stroke.
|
||||
//
|
||||
// Four chips rather than a toggle because the three styles answer
|
||||
// three different questions and no one of them answers all three: a
|
||||
// tint says whether the right thing is selected, an alpha says where
|
||||
// the edge is, an outline says whether that edge is registered against
|
||||
// detail the other two hide.
|
||||
//
|
||||
// Beside the tool strip and under the same condition, because both
|
||||
// describe one selected mask — and `columns: 2` for the reason
|
||||
// `ChipGrid` exists: four chips in a row would set the width of the
|
||||
// develop column and take every other panel's controls off the edge.
|
||||
if Develop.enabled && Masking.parts.length > 0: Segmented {
|
||||
label: "Show mask";
|
||||
options: ["Off", "Tint", "Alpha", "Edge"];
|
||||
columns: 2;
|
||||
selected: Masking.mask-view;
|
||||
picked(i) => { Masking.mask-view-picked(i); }
|
||||
}
|
||||
|
||||
if Develop.enabled && Masking.tool > 0: SliderRow {
|
||||
label: "Size";
|
||||
value: Masking.brush-radius;
|
||||
@@ -752,10 +798,18 @@ export component MaskPanel inherits Rectangle {
|
||||
// a mask is judged against the photograph under it, and you cannot see
|
||||
// that photograph through the thing describing it.
|
||||
//
|
||||
// Reads as its own action rather than its state: "Show the mask" is
|
||||
// what pressing it will do, not what is currently true.
|
||||
// Reads as its own action rather than its state: "Show what was found"
|
||||
// is what pressing it will do, not what is currently true.
|
||||
//
|
||||
// TRACES: FR-DEV-19c
|
||||
// **Named for what it actually hides**, which is not the mask. This
|
||||
// said "Show the mask" / "Hide the mask" while switching the
|
||||
// false-coloured picture of what the *model detected* — and now that
|
||||
// there is a control which really does show a mask ("Show mask",
|
||||
// above), two things called the same thing and meaning different ones
|
||||
// would be worse than either.
|
||||
if Develop.enabled && Masking.segmented: Button {
|
||||
text: Masking.overlay-hidden ? "Show the mask" : "Hide the mask";
|
||||
text: Masking.overlay-hidden ? "Show what was found" : "Hide what was found";
|
||||
clicked => { Masking.overlay-hidden = !Masking.overlay-hidden; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user