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
This commit is contained in:
Martin Vlach
2026-10-20 14:23:50 +02:00
committed by Matej Vrzala
parent 9e7929ee89
commit 03b2960f4d
+69 -1
View File
@@ -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<Arc<Node>>,
}
/// 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<Node>]) -> 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<Affine> {
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<Node> {
@@ -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<Node>| {
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)]);