From 63a36fec51fcb4a06fe914a0770cd5471df941a6 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 07:26:48 -0800 Subject: [PATCH] Image Tools: geometry effects keep a color mask An image with a geometry effect (Transform, for example) is drawn from a copy of the image, and that copy now keeps the image's color mask. The canvas and outputs show the masked pixels, and Expand Appearance keeps the mask on the image it makes. Test: a_transform_effect_keeps_the_mask. --- crates/effects/src/reshape.rs | 6 +++++- .../engine/src/cmd/addons/image/mask/tests.rs | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index c629cc10ca..8da6a075b4 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -548,7 +548,11 @@ fn as_art_opts( let mut art = match &n.kind { NodeKind::Image(im) => { let frame = shapes::rectangle(Rect::new(0.0, 0.0, im.width as f64, im.height as f64)).transformed(im.xf); - let image = Node::new(n.id, n.kind.clone()); + let mut image = Node::new(n.id, n.kind.clone()); + // The image keeps its color mask, so it shows its masked pixels. + if let Some(m) = &n.extras().color_mask { + image.extras_mut().color_mask = Some(m.clone()); + } let clipped = || { let mut clip = Node::path(n.id, frame.clone(), Appearance::default()); if let NodeKind::Path { clipping, .. } = &mut clip.kind { diff --git a/crates/engine/src/cmd/addons/image/mask/tests.rs b/crates/engine/src/cmd/addons/image/mask/tests.rs index 35fbf950f4..3e1999888a 100644 --- a/crates/engine/src/cmd/addons/image/mask/tests.rs +++ b/crates/engine/src/cmd/addons/image/mask/tests.rs @@ -657,6 +657,23 @@ fn an_envelope_warp_draws_the_masked_pixels() { assert!(drawn_at(doc(&s), red)[3] > 0, "the red block"); } +/// A geometry effect draws the image's masked pixels, and Expand Appearance keeps the mask on the +/// image it makes. +#[test] +fn a_transform_effect_keeps_the_mask() { + let (mut s, id) = open(&framed()); + s.execute(COLOR_MASK, &hide_white()).unwrap(); + let moved = |p: vectorcraft_geom::Point| vectorcraft_geom::Point::new(p.x + 30.0, p.y); + let (white, red) = (moved(pixel_at(doc(&s), id, 1, 1)), moved(pixel_at(doc(&s), id, 4, 4))); + s.execute("effect.apply", &json!({"ids": [id.0], "effect": "distort.transform", "params": {"moveH": 30}})).unwrap(); + assert_eq!(drawn_at(doc(&s), white)[3], 0, "the hidden white"); + assert_eq!(drawn_at(doc(&s), red), RED, "the red block"); + s.execute("effect.expandAppearance", &json!({})).unwrap(); + let mut masks = vec![]; + doc(&s).walk(|n| masks.extend(Document::color_mask_of(n).cloned())); + assert_eq!(masks.len(), 1, "the expanded image keeps its mask"); +} + /// A pattern whose tile holds a masked image counts as one with a see-through image, so outputs /// without transparency expand it before they flatten. #[test]