Show a mask as soon as it is made
Benchmarks / CPU and I/O (per commit) (push) Successful in 3m52s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 39m53s
Build and test / Layer separation (push) Successful in 1m0s
🐳 Android image / Build and push (push) Successful in 4s
Build and test / android-image (push) Successful in 4s
Traceability / Requirement traces (push) Successful in 46s
Build and test / Android (aarch64) (push) Successful in 25m19s

Choosing a category is asking what it selected, and for a subject or a
category that question had no other answer on screen: the model's outline is
not derivable from anything visible, a fresh layer carries no adjustment to
judge it by, and the list it was chosen from says "architecture 23%" without
saying which 23%. The control that draws the mask existed but had to be found
and pressed, a panel's height away from the list the choice was made in.

So a new layer arrives with its mask showing, from the resting position only.
Somebody who has chosen the alpha or the outline keeps it, and nothing re-arms
in the background — every caller is a press that asked for a new mask.

That makes the canvas depend on how a layer arrived, which is correct and
worth stating: a session that has just made a mask draws a frame that a
session which read the same mask out of a sidecar does not. Viewing state is
not edit state and does not travel in a file, and
`a_stored_mask_renders_exactly_what_the_model_rendered` now says so at both
ends.
This commit is contained in:
2026-09-10 20:56:11 +02:00
parent 5d175cc668
commit ac0aea70ec
2 changed files with 50 additions and 6 deletions
+47
View File
@@ -2523,6 +2523,28 @@ impl DevelopSession {
.and_then(|i| RevealStyle::ALL.get(i).copied());
}
/// TRACES: FR-DEV-19c
/// Show the mask of a layer that has just been made.
///
/// **Making a mask is asking what it selected**, and for a subject or a
/// category that question has no other answer: the model's outline is not
/// derivable from anything on screen, the layer carries no adjustment yet,
/// and the list it was chosen from says "architecture 23%" and nothing
/// about *which* 23%. So the mask appears with the layer rather than
/// waiting to be asked for a second time.
///
/// Only from the resting position, and only on an explicit action — the
/// same rule arming a brush follows. Somebody who has switched to the
/// alpha or the outline keeps it, and this never re-arms in the background;
/// the trap `Masking.overlay-hidden` documents is an automatic reveal that
/// undoes a switch a photographer turned off, and every caller of this is
/// a press that asked for a new mask.
fn show_new_mask(&mut self) {
if self.reveal_style.is_none() {
self.reveal_style = Some(dr_pipeline::mask::RevealStyle::Tint);
}
}
// ----------------------------------------------------------------------
// The region overlay
// ----------------------------------------------------------------------
@@ -3356,6 +3378,7 @@ impl DevelopSession {
return None;
}
self.active_masks = vec![id.clone()];
self.show_new_mask();
self.history
.record(&self.graph, Edit::Action(labels::step::MASK_ADDED));
Some(id)
@@ -3399,6 +3422,7 @@ impl DevelopSession {
return None;
}
self.active_masks = vec![id.clone()];
self.show_new_mask();
self.history
.record(&self.graph, Edit::Action(labels::step::MASK_ADDED));
Some(id)
@@ -3429,6 +3453,7 @@ impl DevelopSession {
return None;
}
self.active_masks = vec![id.clone()];
self.show_new_mask();
self.history
.record(&self.graph, Edit::Action(labels::step::MASK_ADDED));
Some(id)
@@ -3460,6 +3485,7 @@ impl DevelopSession {
}
self.active_masks = vec![id.clone()];
self.active_part = 0;
self.show_new_mask();
self.history
.record(&self.graph, Edit::Action(labels::step::MASK_ADDED));
Some(id)
@@ -3486,6 +3512,7 @@ impl DevelopSession {
return None;
}
self.active_masks = vec![id.clone()];
self.show_new_mask();
self.history
.record(&self.graph, Edit::Action(labels::step::MASK_ADDED));
Some(id)
@@ -6543,6 +6570,16 @@ mod tests {
let mut live = session_with_a_left_half_subject(&ctx);
let id = live.add_subject_mask(0).expect("a subject layer");
// TRACES: FR-DEV-19c
// Making a mask shows it (`show_new_mask`), and this test is about the
// pixels the *edit* produces. Put away here rather than left on, and
// the asymmetry is the point rather than an inconvenience: the reveal
// is how somebody is looking at a photograph, so a session that has
// just made a layer legitimately draws a frame that a session which
// read the same layer out of a file does not. Both of those are
// correct, and only one of them is what a stored raster has to
// reproduce.
live.set_mask_view(0);
live.graph
.masks_mut()
.get_mut(&id)
@@ -6569,6 +6606,16 @@ mod tests {
"the point: nothing has run a model in this session"
);
reopened.apply_version(parsed.default_version().expect("a version"));
// TRACES: FR-DEV-19c
// And a sidecar carries no viewing state: a photograph reopened is not
// reopened with its masks tinted red. Checked here because this is the
// one test that puts an edit through a file and renders both ends, so
// it is where the property would first go wrong.
assert_eq!(
reopened.mask_view(),
0,
"restoring an edit must not turn an overlay on"
);
let from_the_file = read_back(&ctx, &reopened.render(64, 64).expect("render"));
assert_eq!(
+3 -6
View File
@@ -1027,14 +1027,11 @@ pub(crate) fn wire(
// until something is painted into it, so a press that made a row
// and left the pointer in "select" would be indistinguishable from
// a button that did nothing. The same reasoning as `on_part_added`
// below, and the tool's own handler turns the mask view on.
// below. Showing the mask is `DevelopSession::show_new_mask`'s
// job and every route to a new layer goes through it, so only the
// tool is set here.
if made {
w.global::<Masking>().set_tool(1);
if let Some(s) = session.borrow_mut().as_mut() {
if s.mask_view() == 0 {
s.set_mask_view(1);
}
}
}
sync(&w, &session);
sync_rows(&w, &rows, &session);