From 2108c3cc9931d1b74b3d64415207fd2231c343f8 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Fri, 16 Oct 2026 21:02:55 +0200 Subject: [PATCH 1/2] Render: a glow's pixmap is drawn without the paint transform a pattern fill left (#2426) Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/tests_pattern.rs | 40 ++++++++++++++++++++++++++++++ crates/render/src/group.rs | 3 +++ 2 files changed, 43 insertions(+) diff --git a/crates/engine/src/tests_pattern.rs b/crates/engine/src/tests_pattern.rs index 5b5789d95..9aeff6c70 100644 --- a/crates/engine/src/tests_pattern.rs +++ b/crates/engine/src/tests_pattern.rs @@ -115,6 +115,8 @@ fn edit_then_cancel_restores_definition() { fn edit_defaults_to_selected_objects_pattern() { let mut s = session(); make_dots(&mut s); + let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); + s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 100.0, 100.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); sel(&mut s, &[big]); @@ -129,6 +131,8 @@ fn edit_defaults_to_selected_objects_pattern() { fn set_fill_with_pattern_swatch() { let mut s = session(); make_dots(&mut s); + let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); + s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 100.0, 100.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); assert!(matches!(node(&s, big).appearance.fill_paint(), Paint::Pattern { pattern, .. } if pattern == "Dots")); @@ -170,6 +174,8 @@ fn options_set_tile_type_spacing_and_overlap() { fn options_rename_updates_swatch_and_paints() { let mut s = session(); make_dots(&mut s); + let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); + s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); s.execute("pattern.options", &json!({"name": "Dots", "newName": "Spots"})).unwrap(); @@ -182,6 +188,8 @@ fn options_rename_updates_swatch_and_paints() { fn delete_pattern_unpaints_objects() { let mut s = session(); make_dots(&mut s); + let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); + s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); let v = s.execute("pattern.delete", &json!({"name": "Dots"})).unwrap(); @@ -195,6 +203,8 @@ fn delete_pattern_unpaints_objects() { fn transform_patterns_only_moves_placement() { let mut s = session(); make_dots(&mut s); + let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); + s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); let before = node(&s, big).geometric_bounds(); @@ -212,6 +222,8 @@ fn transform_patterns_only_moves_placement() { fn transforms_without_transform_objects_move_only_the_pattern() { let mut s = session(); make_dots(&mut s); + let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); + s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); s.execute("select.set", &json!({"ids": [big.0]})).unwrap(); @@ -296,6 +308,8 @@ fn hit_testing_in_edit_mode_only_reaches_tile_layer() { fn save_load_roundtrip_keeps_patterns_and_repeats() { let mut s = session(); make_dots(&mut s); + let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); + s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); let r = rect(&mut s, 300.0, 50.0, 10.0, 10.0); @@ -312,6 +326,8 @@ fn save_load_roundtrip_keeps_patterns_and_repeats() { fn svg_export_writes_pattern_def() { let mut s = session(); make_dots(&mut s); + let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); + s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); let svg = vectorcraft_svg::export(doc(&s), &vectorcraft_svg::ExportOptions::default()); @@ -329,6 +345,8 @@ fn svg_export_writes_pattern_def() { fn pdf_export_expands_pattern_fills() { let mut s = session(); make_dots(&mut s); + let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); + s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); let rep = vectorcraft_pdf::export_with_report(doc(&s), &Default::default()).unwrap(); @@ -587,3 +605,25 @@ fn picking_another_pattern_swatch_keeps_the_pattern_transform() { assert_eq!(pattern, "Dots"); assert_eq!(xf.as_coeffs(), [2.0, 0.0, 0.0, 2.0, 15.0, 10.0]); } + +#[test] +fn inner_glow_on_a_pattern_fill_stays_inside_the_object_on_the_threaded_renderer() { + // #2426: the glow's pixmap was drawn with the pattern's paint transform, so it landed + // scaled at the top left instead of along the object's edges. + let mut s = session(); + make_dots(&mut s); + let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); + s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); + let big = rect(&mut s, 100.0, 100.0, 150.0, 150.0); + s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); + sel(&mut s, &[big]); + s.execute("effect.apply", &json!({"effect": "stylize.innerGlow", "params": {"blur": 10}})).unwrap(); + let mut r = vectorcraft_render::Renderer::new(); + r.threads = 2; + let view = Affine::scale(1.5) * Affine::translate((-20.0, -30.0)); + let out = r.render(&s.doc().unwrap().doc, 400, 400, view, &Default::default()); + let [red, ..] = out.pixel(110, 100); + assert!(red < 0x30, "glow outside the object: red {red}"); + let [red, ..] = out.pixel(123, 200); + assert!(red > 0x40, "no glow along the edge: red {red}"); +} diff --git a/crates/render/src/group.rs b/crates/render/src/group.rs index e32be592d..c7f7b5acd 100644 --- a/crates/render/src/group.rs +++ b/crates/render/src/group.rs @@ -274,6 +274,9 @@ pub(crate) fn draw_pixmap(ctx: &mut RenderContext, pm: Arc, at: (f64, f6 #[cfg(test)] PIXELS_DRAWN.with(|n| n.set(n.get() + u64::from(pm.width()) * u64::from(pm.height()))); ctx.set_transform(Affine::translate(at)); + // The pixmap maps 1:1 onto the rect: a paint transform left by an earlier fill (a pattern, + // radial or freeform gradient) would resample it (#2426). + ctx.reset_paint_transform(); ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm), sampler: peniko::ImageSampler::default() }); ctx.fill_rect(&Rect::new(0.0, 0.0, w, h)); ctx.set_transform(Affine::IDENTITY); From 8fead43ed23fe0b04e551f9edfb071b5d43c9916 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 17 Oct 2026 12:53:22 +0700 Subject: [PATCH 2/2] Tests: only the new Inner Glow test lays a background under the pattern (#2426) --- crates/engine/src/tests_pattern.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/crates/engine/src/tests_pattern.rs b/crates/engine/src/tests_pattern.rs index 9aeff6c70..d19e2d8c2 100644 --- a/crates/engine/src/tests_pattern.rs +++ b/crates/engine/src/tests_pattern.rs @@ -115,8 +115,6 @@ fn edit_then_cancel_restores_definition() { fn edit_defaults_to_selected_objects_pattern() { let mut s = session(); make_dots(&mut s); - let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); - s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 100.0, 100.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); sel(&mut s, &[big]); @@ -131,8 +129,6 @@ fn edit_defaults_to_selected_objects_pattern() { fn set_fill_with_pattern_swatch() { let mut s = session(); make_dots(&mut s); - let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); - s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 100.0, 100.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); assert!(matches!(node(&s, big).appearance.fill_paint(), Paint::Pattern { pattern, .. } if pattern == "Dots")); @@ -174,8 +170,6 @@ fn options_set_tile_type_spacing_and_overlap() { fn options_rename_updates_swatch_and_paints() { let mut s = session(); make_dots(&mut s); - let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); - s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); s.execute("pattern.options", &json!({"name": "Dots", "newName": "Spots"})).unwrap(); @@ -188,8 +182,6 @@ fn options_rename_updates_swatch_and_paints() { fn delete_pattern_unpaints_objects() { let mut s = session(); make_dots(&mut s); - let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); - s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); let v = s.execute("pattern.delete", &json!({"name": "Dots"})).unwrap(); @@ -203,8 +195,6 @@ fn delete_pattern_unpaints_objects() { fn transform_patterns_only_moves_placement() { let mut s = session(); make_dots(&mut s); - let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); - s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); let before = node(&s, big).geometric_bounds(); @@ -222,8 +212,6 @@ fn transform_patterns_only_moves_placement() { fn transforms_without_transform_objects_move_only_the_pattern() { let mut s = session(); make_dots(&mut s); - let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); - s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); s.execute("select.set", &json!({"ids": [big.0]})).unwrap(); @@ -308,8 +296,6 @@ fn hit_testing_in_edit_mode_only_reaches_tile_layer() { fn save_load_roundtrip_keeps_patterns_and_repeats() { let mut s = session(); make_dots(&mut s); - let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); - s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); let r = rect(&mut s, 300.0, 50.0, 10.0, 10.0); @@ -326,8 +312,6 @@ fn save_load_roundtrip_keeps_patterns_and_repeats() { fn svg_export_writes_pattern_def() { let mut s = session(); make_dots(&mut s); - let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); - s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); let svg = vectorcraft_svg::export(doc(&s), &vectorcraft_svg::ExportOptions::default()); @@ -345,8 +329,6 @@ fn svg_export_writes_pattern_def() { fn pdf_export_expands_pattern_fills() { let mut s = session(); make_dots(&mut s); - let bg = rect(&mut s, 0.0, 0.0, 400.0, 400.0); - s.execute("paint.setFill", &json!({"ids": [bg.0], "color": "#223344"})).unwrap(); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); let rep = vectorcraft_pdf::export_with_report(doc(&s), &Default::default()).unwrap();