Fix #2788: a dashed stroke with object opacity folds the opacity into its colour instead of compositing a canvas-wide layer

This commit is contained in:
Taha-Mahmoodi
2026-10-23 03:22:55 +07:00
committed by bflatastic
parent 368fa3def2
commit 32072c77fa
2 changed files with 35 additions and 2 deletions
+4 -2
View File
@@ -2229,14 +2229,16 @@ fn single_plain_fill(n: &Node) -> bool {
matches!((painted.next(), painted.next()), (Some(AppearanceItem::Fill(f)), None) if f.blend == vectorcraft_color::BlendMode::Normal && f.opacity >= 1.0 && !matches!(f.paint, vectorcraft_color::Paint::Pattern { .. }))
}
/// Number of visible painted fill/stroke items (opacity folding is exact only for one).
/// 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.
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 { .. }) && s.dash.is_none(),
AppearanceItem::Stroke(s) => s.visible && !s.paint.is_none() && s.width > 0.0 && matches!(s.paint, vectorcraft_color::Paint::Solid { .. }),
})
.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 })
+31
View File
@@ -956,3 +956,34 @@ fn zero_length_subpaths_draw_dots_with_their_cap() {
let butt = dots(vectorcraft_doc::LineCap::Butt);
assert_eq!(butt.pixels.iter().filter(|&&p| p != 255).count(), 0, "a butt cap draws no dot");
}
/// #2788: a dashed solid stroke is one paint, so its object opacity folds into its colour instead
/// of compositing a viewport-wide layer; the result matches the layered one, overlapping dashes
/// included.
#[test]
fn dashed_stroke_opacity_folds_into_its_colour() {
let dashed = |d: &mut Document, object: f32, stroke: f32| {
let mut n = rect_node(d, Rect::new(20.0, 20.0, 80.0, 60.0), Paint::None, Paint::solid(Color::rgb(0.2, 0.4, 0.9)), 6.0);
let st = n.appearance.stroke_mut().unwrap();
st.dash = Some(vectorcraft_doc::Dash { pattern: vec![4.0, 1.0], ..Default::default() });
st.cap = vectorcraft_doc::LineCap::Round;
st.opacity = stroke;
n.opacity = object;
n
};
let doc = |object: f32, stroke: f32| {
let mut d = Document::new(100.0, 100.0);
let n = dashed(&mut d, object, stroke);
let l = d.layers[0].id;
d.insert(Some(l), 0, n).unwrap();
d
};
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 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");
for (a, b) in folded.pixels.chunks(4).zip(layered.pixels.chunks(4)) {
assert!(a.iter().zip(b).all(|(x, y)| (*x as i32 - *y as i32).abs() <= 1), "folded {a:?} vs layered {b:?}");
}
}