diff --git a/crates/effects/src/solids/source.rs b/crates/effects/src/solids/source.rs index 9ab348f4a..090688c4f 100644 --- a/crates/effects/src/solids/source.rs +++ b/crates/effects/src/solids/source.rs @@ -99,14 +99,15 @@ impl Resolver<'_> { fn spend(&mut self, steps: usize) -> Result<(), String> { self.work = self.work.saturating_add(steps); if self.work > MAX_WORK { - return Err(format!( - "{} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects", - self.label - )); + return Err(self.too_much_work()); } Ok(()) } + fn too_much_work(&self) -> String { + format!("{} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects", self.label) + } + /// `f` run on the art of `c`. The effects of an object or group can move its art into a clip, /// so inside one with effects no image is left out for lying outside the clip ([`Self::shown`]). fn inside(&mut self, c: &Node, f: impl FnOnce(&mut Self) -> Result) -> Result { @@ -129,6 +130,11 @@ 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 { + 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") })?; diff --git a/crates/effects/src/solids/tests_sources.rs b/crates/effects/src/solids/tests_sources.rs index ab97bdb51..35e89e61f 100644 --- a/crates/effects/src/solids/tests_sources.rs +++ b/crates/effects/src/solids/tests_sources.rs @@ -363,6 +363,18 @@ fn nested_symbol_instances_that_repeat_hidden_art_are_refused_past_the_work_limi assert!(solid_art_in(Some(&d), &n).is_some()); } +#[test] +fn a_symbol_holding_more_objects_than_the_work_limit_is_refused_before_its_art_is_placed() { + let mut d = document(); + let members = (0..=source::MAX_WORK).map(|i| Arc::new(rect(0, i as f64, 0.0, i as f64 + 1.0, 1.0, 0.0))).collect(); + d.symbols.push(Symbol { dynamic: false, name: "Big".into(), art: Arc::new(Node::group(NodeId(0), members)) }); + let n = with(instance(30, "Big", Affine::IDENTITY), EXTRUDE, json!({})); + 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 empty_sources_and_sources_inside_sources_build_or_refuse_cleanly() { let mut d = document();