From 03b2960f4d776c9f4442651f19eaa3d592f4c8c7 Mon Sep 17 00:00:00 2001 From: Martin Vlach Date: Tue, 13 Oct 2026 16:13:08 +0200 Subject: [PATCH] Repeat: nested Repeats stop at 250,000 instances in all One Repeat makes at most 250,000 instances (a 500 x 500 grid; radial counts stop at 1,000), but its source may hold another Repeat and every instance draws that one again: expand() clones the inner Repeat once per instance, and the renderer, the exporters (expand_deep) and hit testing evaluate each copy. Nothing bounds the product, so a document under 1 KB with a 500 x 500 grid of a 500 x 500 grid asks for 62.5 billion instances: opening it freezes the app or runs it out of memory, and the unsaved work in other documents is lost with it. A Repeat now counts the instances the Repeats nested in its source make in all and keeps only as many of its own as fit in 250,000, the budget pattern fills already keep (at least one, so the source still shows). A Repeat with nothing nested keeps all its instances, as before. The test nests a 500 x 500 grid in a 500 x 500 grid and in a mirror, counts the instances from the transforms (62.5 billion before) and checks they stay within the budget, while one grid and a small nest keep all of theirs. Reported-by: BS.ehlas.cz --- crates/doc/src/pattern.rs | 70 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/crates/doc/src/pattern.rs b/crates/doc/src/pattern.rs index 6b9d902ba..1b587e70f 100644 --- a/crates/doc/src/pattern.rs +++ b/crates/doc/src/pattern.rs @@ -467,6 +467,14 @@ impl RepeatKind { RepeatKind::Mirror { .. } => "Mirror Repeat", } } + /// How many instances [`repeat_transforms`] makes (with its clamps). + fn count(&self) -> usize { + match self { + RepeatKind::Radial { instances, .. } => (*instances).clamp(1, 1000) as usize, + RepeatKind::Grid { rows, cols, .. } => (*rows).clamp(1, 500) as usize * (*cols).clamp(1, 500) as usize, + RepeatKind::Mirror { .. } => 2, + } + } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -475,6 +483,23 @@ pub struct RepeatSpec { pub source: Vec>, } +/// Most instances a Repeat makes in all, those of the Repeats nested in its source included: as +/// many as one 500 × 500 grid, the budget [`PatternDef::offsets_covering`] also keeps. +const MAX_REPEAT_INSTANCES: usize = 250_000; + +/// Instances the Repeats in `nodes` make in all (0 when there are none). +fn nested_instances(nodes: &[Arc]) -> usize { + let mut sum = 0usize; + for n in nodes { + sum = sum.saturating_add(match (&n.kind, n.children()) { + (NodeKind::Repeat(r), _) => r.instances_in_all(), + (_, Some(c)) => nested_instances(c), + _ => 0, + }); + } + sum +} + /// Reflection across the line through `c` with direction angle `deg`. pub fn reflect_about(c: Point, deg: f64) -> Affine { let t = (2.0 * deg).to_radians(); @@ -536,7 +561,21 @@ impl RepeatSpec { crate::live::nodes_bounds(&self.source) } pub fn transforms(&self) -> Vec { - repeat_transforms(&self.kind, self.source_bounds()) + let mut v = repeat_transforms(&self.kind, self.source_bounds()); + // Each instance draws the Repeats in the source again, so nested ones multiply (a 500 × 500 + // grid of one is 62.5 billion instances): keep the first that fit the budget. + v.truncate(self.kept(nested_instances(&self.source))); + v + } + /// How many of its instances this Repeat keeps when each makes `nested` instances of the + /// Repeats in its source: as many as fit in [`MAX_REPEAT_INSTANCES`], at least one. + fn kept(&self, nested: usize) -> usize { + self.kind.count().min(MAX_REPEAT_INSTANCES / nested.max(1)).max(1) + } + /// Instances made in all, those of the Repeats nested in the source included. + fn instances_in_all(&self) -> usize { + let nested = nested_instances(&self.source); + self.kept(nested).saturating_mul(nested.max(1)) } /// Evaluated instances: one group of the (transformed) source per instance (ids 0). pub fn expand(&self) -> Vec { @@ -838,6 +877,35 @@ mod tests { assert!((angle.rem_euclid(180.0) - 0.0).abs() < 1e-9 || (angle.rem_euclid(180.0) - 180.0).abs() < 1e-9, "{angle}"); } + #[test] + fn nested_repeats_share_one_instance_budget() { + // Instances made in all, counted from the transforms without drawing any. + fn made(r: &RepeatSpec) -> usize { + let each: usize = r.source.iter().map(|s| if let NodeKind::Repeat(n) = &s.kind { made(n) } else { 1 }).sum(); + r.transforms().len().saturating_mul(each) + } + let grid = |source: Arc| { + let mut g = RepeatSpec::grid(vec![source], 0.0, 0.0); + if let RepeatKind::Grid { rows, cols, .. } = &mut g.kind { + *rows = 500; + *cols = 500; + } + g + }; + let repeat = |r: RepeatSpec| Arc::new(Node::new(NodeId(2), NodeKind::Repeat(r))); + // One grid keeps all its instances. + let inner = grid(sq(0.0, 0.0, 1.0)); + assert_eq!(made(&inner), 250_000); + // A 500 × 500 grid of it made 62.5 billion. + let outer = grid(repeat(inner.clone())); + assert!(made(&outer) <= 250_000, "{} instances", made(&outer)); + assert_eq!(outer.expand().len(), outer.transforms().len()); + assert!(made(&RepeatSpec::mirror(vec![repeat(inner)], 90.0, 5.0)) <= 250_000); + // Small nests are left as they are. + let small = RepeatSpec::mirror(vec![repeat(RepeatSpec::grid(vec![sq(0.0, 0.0, 10.0)], 1.0, 1.0))], 90.0, 5.0); + assert_eq!(made(&small), 18); + } + #[test] fn serde_roundtrip() { let mut def = PatternDef::new("Dots", vec![sq(0.0, 0.0, 10.0)]);