Merge pull request #3223 from Matoo125/fix/nested-repeat-budget

Repeat: nested Repeats stop at 250,000 instances in all
This commit is contained in:
Dylan M. Taylor
2026-10-20 12:17:16 -04:00
committed by GitHub
+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)]);