Merge pull request #608 from vbasky/fix/527-first-mask-mode

This commit is contained in:
Claude committed 2026-10-10 16:28:05 +00:00
commit af1c856948
2 files changed
+46

No files matched your search

+7
View File
@@ -145,9 +145,16 @@ pub fn apply(ctx: &EvalCtx, layer: &Layer, buf: &mut Buf, blur: MaskBlur) -> boo
let n = (buf.img.width * buf.img.height) as usize;
let first_sub = matches!(list[0].kind, GroupKind::Mask { mode: MaskMode::Subtract, .. });
let mut acc = vec![if first_sub { 1.0f32 } else { 0.0 }; n];
// The first mask has nothing to combine with. Intersect and Darken would multiply or
// minimise against that empty coverage and hide the layer; they cover like Add instead.
// Lighten and Difference already do. Filling the accumulator first would make Add show
// the whole layer.
let mut first = true;
for g in list {
let GroupKind::Mask { mode, inverted, .. } = g.kind else { continue };
let Some(mut cov) = coverage(ctx, layer, g, buf, blur) else { continue };
let mode = if first && matches!(mode, MaskMode::Intersect | MaskMode::Darken) { MaskMode::Add } else { mode };
first = false;
let op = (ctx.f(layer, g, "opacity", 100.0) / 100.0) as f32;
cov.data.par_iter_mut().for_each(|c| {
if inverted {
+39
View File
@@ -80,6 +80,45 @@ fn mask_cuts_layer() {
assert!(img.get(150, 50)[3] < 0.01);
}
#[test]
fn first_intersect_and_darken_keep_their_rectangle() {
let render = |modes: &[MaskMode], centers: &[[f64; 2]]| {
let (mut p, cid, comp) = setup();
let mut l = solid(&mut p, &comp, [1.0, 1.0, 1.0], 200, 100);
for (i, (mode, center)) in modes.iter().zip(centers).enumerate() {
let mut next = p.next_id;
let m = build::mask(&mut Ids(&mut next), &format!("Mask {}", i + 1), ShapePath::rect(*center, 40.0, 40.0), *mode, [255, 255, 0]);
p.next_id = next;
l.props.sub_mut("masks").unwrap().children.push(m.into());
}
p.comp_mut(cid).unwrap().layers.push(l);
render_frame(&p, cid, Tick::ZERO, 1.0)
};
// A first Intersect or Darken has nothing to combine with, so it covers like Add.
// Lighten and Difference already do; Subtract still starts from the whole layer.
for mode in [MaskMode::Intersect, MaskMode::Darken, MaskMode::Add, MaskMode::Lighten, MaskMode::Difference] {
let img = render(&[mode], &[[50.0, 50.0]]);
assert!(img.get(50, 50)[3] > 0.99, "{mode:?} keeps the rectangle");
assert!(img.get(150, 50)[3] < 0.01, "{mode:?} stays outside it");
}
let hole = render(&[MaskMode::Subtract], &[[50.0, 50.0]]);
assert!(hole.get(50, 50)[3] < 0.01, "subtract still cuts its rectangle");
assert!(hole.get(150, 50)[3] > 0.99, "subtract leaves the rest of the layer");
// Add, then Intersect with a shifted rectangle: only the overlap remains.
let overlap = render(&[MaskMode::Add, MaskMode::Intersect], &[[50.0, 50.0], [70.0, 50.0]]);
assert!(overlap.get(50, 50)[3] > 0.99, "overlap stays");
assert!(overlap.get(35, 50)[3] < 0.01, "the first rectangle's exclusive side is cut");
assert!(overlap.get(85, 50)[3] < 0.01, "the second rectangle's exclusive side is cut");
let darkened = render(&[MaskMode::Add, MaskMode::Darken], &[[50.0, 50.0], [70.0, 50.0]]);
assert!(darkened.get(50, 50)[3] > 0.99 && darkened.get(35, 50)[3] < 0.01 && darkened.get(85, 50)[3] < 0.01);
// A later disjoint Add still contributes after a first Intersect.
let both = render(&[MaskMode::Intersect, MaskMode::Add], &[[50.0, 50.0], [150.0, 50.0]]);
assert!(both.get(50, 50)[3] > 0.99 && both.get(150, 50)[3] > 0.99);
assert!(both.get(100, 50)[3] < 0.01);
}
#[test]
fn track_matte_alpha() {
let (mut p, cid, comp) = setup();