diff --git a/crates/effects/src/solids/source.rs b/crates/effects/src/solids/source.rs index 090688c4f..c5f664c68 100644 --- a/crates/effects/src/solids/source.rs +++ b/crates/effects/src/solids/source.rs @@ -129,16 +129,23 @@ impl Resolver<'_> { } self.spend(1)?; match &n.kind { - NodeKind::SymbolInstance { symbol, .. } => { - // Counted before the art is placed, so a symbol past the limit is never copied. - let held = self.doc.and_then(|d| d.symbols.iter().find(|s| s.name == *symbol)).map_or(0, |s| s.art.count_with_masks()); - if self.work.saturating_add(held) > MAX_WORK { + NodeKind::SymbolInstance { symbol, xf } => { + let missing = || format!("{label} can't build a solid from the instance of symbol \"{symbol}\": the symbol's art isn't available"); + let doc = self.doc.ok_or_else(missing)?; + let sym = doc.symbols.iter().find(|s| s.name == *symbol).ok_or_else(missing)?; + // Counted before the art is placed, so a symbol past the limit is never copied: + // first its own objects, which the instance's overrides keep, then the most + // objects placing makes, with those overrides and the symbol's 9-slice guides. + if self.work.saturating_add(sym.art.count_with_masks()) > MAX_WORK { return Err(self.too_much_work()); } - let art = crate::outline_object(self.doc, n).ok_or_else(|| { - format!("{label} can't build a solid from the instance of symbol \"{symbol}\": the symbol's art isn't available") - })?; - Ok(self.list(kids(&art), depth + 1)?.0) + let art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n); + let most = doc.symbol_slice_map(symbol, *xf).map_or(art.count_with_masks() as u64, |m| m.most_placed(&art, doc)); + if (self.work as u64).saturating_add(most) > MAX_WORK as u64 { + return Err(self.too_much_work()); + } + let placed = doc.place_symbol_art(symbol, &art, *xf).ok_or_else(missing)?; + Ok(self.list(&[Arc::new(placed)], depth + 1)?.0) } NodeKind::Blend { children, spec } => { // Counted before the steps are made, so a blend past the limit is never expanded. diff --git a/crates/effects/src/solids/tests_sources.rs b/crates/effects/src/solids/tests_sources.rs index 35e89e61f..4957f88fa 100644 --- a/crates/effects/src/solids/tests_sources.rs +++ b/crates/effects/src/solids/tests_sources.rs @@ -375,6 +375,54 @@ fn a_symbol_holding_more_objects_than_the_work_limit_is_refused_before_its_art_i assert!(work <= 2, "refused before the symbol's art is placed and listed: {work}"); } +/// A gradient fill: a path filled with one is drawn in clip groups where it crosses 9-slice guides. +fn gradient() -> Paint { + use vectorcraft_doc::color::{Gradient, GradientPaint}; + Paint::Gradient(Box::new(GradientPaint::new(Gradient::default()))) +} + +/// A document whose symbol "Sliced" has 9-slice guides and holds `count` paths across all of them, +/// each filled with `fill`, and an Extrude on an instance of it at twice its size. +fn sliced(count: usize, fill: impl Fn() -> Paint) -> (Document, Node) { + let mut d = document(); + let members = (0..count).map(|i| { + Arc::new(Node::path(NodeId(i as u64 + 1), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::basic(fill(), Paint::None, 0.0))) + }); + d.symbols.push(Symbol { dynamic: true, name: "Sliced".into(), art: Arc::new(Node::group(NodeId(0), members.collect())) }); + d.set_symbol_nine_slice("Sliced", Some(vectorcraft_doc::SymbolNineSlice::default())); + let n = with(instance(30, "Sliced", Affine::scale(2.0)), EXTRUDE, json!({})); + (d, n) +} + +#[test] +fn a_symbol_whose_9_slice_guides_would_place_too_many_objects_is_refused_before_its_art_is_placed() { + // Each gradient path becomes a group of nine clip groups, so the art placed is far past the + // limit while the symbol itself holds less than a tenth of it. + let (d, n) = sliced(source::MAX_WORK / 20, gradient); + assert!(d.symbols[1].art.count_with_masks() < source::MAX_WORK); + let err = validate_three_d_in(Some(&d), &n, EXTRUDE, &json!({})).unwrap_err(); + assert!(err.contains("hold too many objects"), "{err}"); + let work = source::WORK.with(std::cell::Cell::get); + assert!(work <= 2, "refused before the symbol's art is placed and listed: {work}"); +} + +#[test] +fn a_dynamic_symbol_whose_overrides_would_place_too_many_objects_is_refused_before_its_art_is_placed() { + // Solid paths are cut at the guides and stay one path each; the instance's overrides fill them + // with a gradient, which places each in nine clip groups. + let (d, mut n) = sliced(source::MAX_WORK / 20, || Paint::solid(Color::BLACK)); + n.extras_mut().symbol_overrides = (0..source::MAX_WORK / 20) + .map(|i| vectorcraft_doc::SymbolOverride { path: vec![i as u32], fill: Some(gradient()), ..Default::default() }) + .collect(); + let err = validate_three_d_in(Some(&d), &n, EXTRUDE, &json!({})).unwrap_err(); + assert!(err.contains("hold too many objects"), "{err}"); + let work = source::WORK.with(std::cell::Cell::get); + assert!(work <= 2, "refused before the instance's art is placed and listed: {work}"); + // A few such paths build. + let (d, n) = sliced(4, || Paint::solid(Color::BLACK)); + assert!(solid_art_in(Some(&d), &n).is_some()); +} + #[test] fn empty_sources_and_sources_inside_sources_build_or_refuse_cleanly() { let mut d = document();