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 <noreply@anthropic.com>

* Docs: what changes for masks that start with Intersect

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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 <noreply@anthropic.com>
This commit is contained in:
eliets2
2026-11-06 14:46:11 +01:00
committed by GitHub
co-authored by Claude Opus 5.5 eliets2 FredApps
parent 081190c92f
commit 8f638b33bd
4 changed files with 19 additions and 2 deletions
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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;
+11
View File
@@ -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));
}
+6
View File
@@ -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