From f0f3320d398fa7be2cd9108a6712218e982e6d9c Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 14:27:40 +0000 Subject: [PATCH] Brushes: a brush name the library lacks doesn't make art draw a brushed stroke Follow-up to #5293. draws_brushed rejected masks, symbol instances and pattern paints whose art names a brush the library doesn't have, though such a stroke draws plain. It now counts only brush names that resolve. --- crates/brush/src/lib.rs | 8 +++++++- crates/brush/src/tests.rs | 11 ++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/brush/src/lib.rs b/crates/brush/src/lib.rs index 40225c054..093ce531a 100644 --- a/crates/brush/src/lib.rs +++ b/crates/brush/src/lib.rs @@ -848,9 +848,15 @@ fn draws_brushed(doc: &Document, def: Named<'_>, known: &mut Known) -> bool { }; // Without recursion, each one looked at once: they may use one another without end. let (mut seen, mut stack) = (std::collections::HashSet::from([key.clone()]), art(def)); + // A brush name the library lacks draws a plain stroke ([`node_pieces`]), which runs no brush. + let mut names: Option> = None; + let mut resolves = |n: &Node| { + let names = names.get_or_insert_with(|| library(doc).into_iter().map(|b| b.name).collect()); + n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(s) if s.visible && s.brush.as_ref().is_some_and(|b| names.contains(b)))) + }; let mut answer = false; while let Some(n) = stack.pop() { - if has_brush(n) { + if has_brush(n) && resolves(n) { answer = true; break; } diff --git a/crates/brush/src/tests.rs b/crates/brush/src/tests.rs index eb7bd8865..7871b61af 100644 --- a/crates/brush/src/tests.rs +++ b/crates/brush/src/tests.rs @@ -1301,6 +1301,7 @@ fn pieces_leave_out_what_draws_a_brushed_stroke_by_name() { }; let art = Node::group(NodeId(0), [instance("plain"), instance("brushed"), painted("plain"), painted("brushed")].map(std::sync::Arc::new).into()); let b = Brush { name: "s".into(), kind: BrushKind::Scatter(Scatter { art, colorization: Colorization::None, ..Default::default() }) }; + store(&mut doc, std::slice::from_ref(&b)); let out = stroke_pieces(&doc, &b, &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0)); assert!(!out.is_empty()); for piece in &out { @@ -1325,7 +1326,10 @@ fn pieces_leave_out_a_mask_whose_root_draws_a_brushed_stroke() { let mut doc = Document::new(100.0, 100.0); let mut brushed = rect_art(10.0, 4.0); brushed.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(StrokeLayer { brush: Some("s".into()), ..stroke(1.0) })); - for (name, art) in [("plain", rect_art(10.0, 4.0)), ("brushed", brushed)] { + // A brush the library lacks draws a plain stroke: its mask stays. + let mut unresolved = rect_art(10.0, 4.0); + unresolved.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(StrokeLayer { brush: Some("missing".into()), ..stroke(1.0) })); + for (name, art) in [("plain", rect_art(10.0, 4.0)), ("brushed", brushed), ("unresolved", unresolved)] { doc.symbols.push(Symbol { dynamic: false, name: name.into(), art: std::sync::Arc::new(art) }); } let instance = |symbol: &str| Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: symbol.into(), xf: vectorcraft_geom::Affine::IDENTITY }); @@ -1334,8 +1338,9 @@ fn pieces_leave_out_a_mask_whose_root_draws_a_brushed_stroke() { n.extras_mut().mask = Some(Box::new(OpacityMask::new(instance(symbol), true))); n }; - let art = Node::group(NodeId(0), [masked("plain"), masked("brushed")].map(std::sync::Arc::new).into()); + let art = Node::group(NodeId(0), [masked("plain"), masked("brushed"), masked("unresolved")].map(std::sync::Arc::new).into()); let b = Brush { name: "s".into(), kind: BrushKind::Scatter(Scatter { art, colorization: Colorization::None, ..Default::default() }) }; + store(&mut doc, std::slice::from_ref(&b)); let out = stroke_pieces(&doc, &b, &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0)); assert!(!out.is_empty()); for piece in &out { @@ -1348,7 +1353,7 @@ fn pieces_leave_out_a_mask_whose_root_draws_a_brushed_stroke() { _ => None, }) .collect(); - assert_eq!(roots, [Some("plain"), None], "the plain mask stays and the brushed one goes"); + assert_eq!(roots, [Some("plain"), None, Some("unresolved")], "the plain and unresolved masks stay and the brushed one goes"); } }