From 8f638b33bd3bc3f31ae4d66c9fdd003df783aba1 Mon Sep 17 00:00:00 2001 From: eliets2 Date: Fri, 6 Nov 2026 15:46:11 +0200 Subject: [PATCH] Treat a first intersect mask component as its base selection (#3854) * Treat a first intersect mask component as its base selection A mask created with Intersect as its first component currently renders as empty, so the overlay and local adjustments never appear. Using that component as the base matches how Add and Subtract first components already behave. Change evaluate_one so the first component always sets the base alpha regardless of its combine op. Add a regression test with a single Intersect linear-gradient component asserting the start side is selected. Test: intersect_first_component_selects_its_shape failed before the change and passes after. * Masks: the GPU treats a first Intersect as the base too, and the process doc says so Co-Authored-By: Claude Opus 5.5 * Docs: what changes for masks that start with Intersect Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- crates/gpu/src/render.rs | 2 +- crates/pipeline/src/masks.rs | 2 +- crates/pipeline/src/masks/tests.rs | 11 +++++++++++ docs/process-versions.md | 6 ++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/gpu/src/render.rs b/crates/gpu/src/render.rs index 8e6afa3700..de8c7ff16c 100644 --- a/crates/gpu/src/render.rs +++ b/crates/gpu/src/render.rs @@ -1480,7 +1480,7 @@ fn masks(cx: &mut Cx<'_>, lin: &Buf, prep: &Prep, plan: &Plan<'_>, host: &mut Ho let mut first = true; for comp in &m.components { let op = match comp.op { - _ if first && comp.op != MaskOp::Intersect => 0u32, + _ if first => 0u32, MaskOp::Add => 1, MaskOp::Subtract => 2, MaskOp::Intersect => 3, diff --git a/crates/pipeline/src/masks.rs b/crates/pipeline/src/masks.rs index 097e080a7e..b40e3dcd97 100644 --- a/crates/pipeline/src/masks.rs +++ b/crates/pipeline/src/masks.rs @@ -161,7 +161,7 @@ pub fn evaluate_one(m: &Mask, frame: &Frame, w: usize, h: usize, img: &Rgb32f, l if comp.invert { c.data.iter_mut().for_each(|v| *v = 1.0 - *v); } - if first && comp.op != MaskOp::Intersect { + if first { alpha = c; first = false; continue; diff --git a/crates/pipeline/src/masks/tests.rs b/crates/pipeline/src/masks/tests.rs index c97cd290e4..b57f9f6688 100644 --- a/crates/pipeline/src/masks/tests.rs +++ b/crates/pipeline/src/masks/tests.rs @@ -629,3 +629,14 @@ fn people_and_subject_use_person_mattes() { let lips = MaskShape::People { person: 0, parts: vec!["Lips".into()] }; assert_eq!(eval(&lips), shape_alpha(&lips, &f, w, h, &img, &l, 0.0, None)); } + +#[test] +fn intersect_first_component_selects_its_shape() { + let f = frame(100, 100); + let img = Rgb32f::new(100, 100); + let l = Plane::new(100, 100); + let shape = MaskShape::Radial { center: Point::new(0.5, 0.5), rx: 0.2, ry: 0.2, angle: 0.0, feather: 20.0, invert: false }; + let m = Mask { components: vec![MaskComponent { name: None, op: MaskOp::Intersect, invert: false, shape }], ..Default::default() }; + let a = evaluate_one(&m, &f, 100, 100, &img, &l, 0.0, None); + assert!(a.get(50, 50) > 0.99 && a.get(5, 5) < 0.01, "intersect first: {} {}", a.get(50, 50), a.get(5, 5)); +} diff --git a/docs/process-versions.md b/docs/process-versions.md index 7d1ae1927c..154906de0b 100644 --- a/docs/process-versions.md +++ b/docs/process-versions.md @@ -40,6 +40,12 @@ exports. Existing AI masks become 1–2 px tighter at a subject's edges, on ever inside, their outside and edges without contrast in the photo are unchanged. It landed in place as a bug fix: the outline is output that isn't in the photo nor in the edit, not a look anyone chose. +**A mask whose first component is Intersect** starts from that component, on the CPU and the GPU +(issue #1889). It used to start from nothing, so alone it selected nothing, with a later Add it gave +only what was added, and with the mask's Invert it gave the whole photo. Such masks now include the +first component, on every process. It landed in place as a bug fix: intersecting with nothing is not +a look anyone chose. + ## Adding a process 1. Add a variant to `Process` (`V2`), append it to `Process::ALL` (oldest first), give it its number in