From 087e38aafc443727b87e676f13ddf1cf1a22ce92 Mon Sep 17 00:00:00 2001 From: Taha-Mahmoodi <85902429+Taha-Mahmoodi@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:06:50 +0430 Subject: [PATCH] Render: a dashed brush stroke keeps the layered path for object opacity Follow-up to #3697 (CodeRabbit): a brush stroke is drawn as separate pieces (draw_brush), so folding object opacity into its colour isn't exact. A non-dashed brush stroke already counted as one paint before #3697; to keep that change's scope, a dashed stroke counts as one paint only when it has no brush. dashed_stroke_opacity_folds_into_its_colour checks that a dashed brush stroke isn't folded; it fails without the change. --- crates/render/src/lib.rs | 5 +++-- crates/render/src/tests.rs | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 2599c9b56..a13291eb2 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -2231,14 +2231,15 @@ fn single_plain_fill(n: &Node) -> bool { /// Number of visible painted fill/stroke items (opacity folding is exact only for one). A dashed /// stroke is one paint: its dashes are outlined into one path and filled once, so overlapping -/// dashes don't darken where they overlap. +/// dashes don't darken where they overlap. A dashed brush stroke isn't: its pieces are drawn one +/// by one. fn painted_items(n: &Node) -> usize { n.appearance .items .iter() .filter(|i| match i { AppearanceItem::Fill(f) => f.visible && !f.paint.is_none() && matches!(f.paint, vectorcraft_color::Paint::Solid { .. }), - AppearanceItem::Stroke(s) => s.visible && !s.paint.is_none() && s.width > 0.0 && matches!(s.paint, vectorcraft_color::Paint::Solid { .. }), + AppearanceItem::Stroke(s) => s.visible && !s.paint.is_none() && s.width > 0.0 && matches!(s.paint, vectorcraft_color::Paint::Solid { .. }) && (s.dash.is_none() || s.brush.is_none()), }) .count() .max(if n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && !matches!(f.paint, vectorcraft_color::Paint::Solid { .. } | vectorcraft_color::Paint::None)) || matches!(i, AppearanceItem::Stroke(s) if s.visible && !matches!(s.paint, vectorcraft_color::Paint::Solid { .. } | vectorcraft_color::Paint::None))) { 2 } else { 0 }) diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index f41c29dc1..2f907c5cd 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -980,6 +980,9 @@ fn dashed_stroke_opacity_folds_into_its_colour() { }; let mut d = Document::new(100.0, 100.0); assert_eq!(painted_items(&dashed(&mut d, 0.6, 1.0)), 1, "a dashed stroke takes the folded fast path"); + let mut brushed = dashed(&mut d, 0.6, 1.0); + brushed.appearance.stroke_mut().unwrap().brush = Some("Calligraphic".into()); + assert_ne!(painted_items(&brushed), 1, "a dashed brush stroke draws its pieces one by one: no folding"); let folded = render(&doc(0.6, 1.0)); let layered = render(&doc(1.0, 0.6)); assert_ne!(folded.pixel(20, 40), [255, 255, 255, 255], "the stroke is drawn");