From 70d234d97aceb2f0ff8a7d3c9fb90c717802f37e Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Mon, 2 Nov 2026 22:15:47 -0500 Subject: [PATCH] Shape operators: cap each expanding operator's output Zig Zag and Wiggle Paths split every segment into up to 1001 pieces, Twist into up to 64 and Offset Paths makes up to 1000 copies, each from the previous operator's output, so a short stack could grow a square into billions of points. Each now reduces its pieces or copies so its output stays within 2^20 segments (never below one per input segment). --- crates/path/src/ops.rs | 21 +++++++++++++++++---- crates/path/src/ops/tests.rs | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/crates/path/src/ops.rs b/crates/path/src/ops.rs index ba5349312..0e59046e6 100644 --- a/crates/path/src/ops.rs +++ b/crates/path/src/ops.rs @@ -351,7 +351,7 @@ fn round_sub(sub: &Sub, r: f64, out: &mut BezPath) { /// join. `copies` copies are made, copy *i* (0-based) offset by `amount · (i + 1 + copy_offset)`, /// all returned in the same compound path. pub fn offset(paths: &[BezPath], amount: f64, join: Join, miter_limit: f64, copies: f64, copy_offset: f64) -> Vec { - let n = copies.round().clamp(1.0, 1000.0) as usize; + let n = pieces_within_budget(paths, copies.round().clamp(1.0, 1000.0) as usize); paths .iter() .map(|p| { @@ -387,6 +387,7 @@ pub fn twist(paths: &[BezPath], angle_deg: f64, center: [f64; 2]) -> Vec Vec usize { + let segs: usize = paths.iter().map(|p| p.elements().len()).sum(); + want.min(MAX_OPERATOR_SEGMENTS / segs.max(1)).max(1) +} + /// Split every segment into `m` pieces of equal arc length. Original vertices get the average of /// their incoming and outgoing tangents (so a corner's normal is its bisector). fn resample(sub: &Sub, m: usize) -> Vec { @@ -503,7 +516,7 @@ fn emit(samples: &[Sample], disp: &[Vec2], closed: bool, smooth: bool, handle: f /// `smooth` makes rounded waves instead of sharp zig zags. A closed path with `n` segments ends up /// with `n · (ridges + 1)` vertices (open: one more). pub fn zigzag(paths: &[BezPath], size: f64, ridges: f64, smooth: bool) -> Vec { - let m = ridges.round().clamp(0.0, 1000.0) as usize + 1; + let m = pieces_within_budget(paths, ridges.round().clamp(0.0, 1000.0) as usize + 1); paths .iter() .map(|p| { @@ -586,7 +599,7 @@ pub fn wiggle(paths: &[BezPath], w: &WiggleParams, time_s: f64) -> Vec if w.size == 0.0 { return paths.to_vec(); } - let m = w.detail.round().clamp(0.0, 1000.0) as usize + 1; + let m = pieces_within_budget(paths, w.detail.round().clamp(0.0, 1000.0) as usize + 1); let seed = w.seed.round() as i64 as u64; let t = time_s * w.speed + w.phase_deg / 360.0; let corr = (w.correlation / 100.0).clamp(0.0, 1.0); diff --git a/crates/path/src/ops/tests.rs b/crates/path/src/ops/tests.rs index 1ebdcea5b..de8321be6 100644 --- a/crates/path/src/ops/tests.rs +++ b/crates/path/src/ops/tests.rs @@ -417,3 +417,24 @@ fn normalize_and_filled_area() { let n = crate::boolean::normalize(&p, FillRule::EvenOdd); assert!(close(n.area().abs(), 10000.0, 1e-6)); } + +// ---------------------------------------------------------------- operator budget + +#[test] +fn stacked_expanding_operators_stay_within_the_segment_budget() { + // Three Zig Zags at 1000 ridges would turn a square into 4 · 1001³ segments; each output + // is held to the budget instead, and the last operators still pass the geometry through. + let mut p = vec![sq(0.0, 0.0, 100.0)]; + for _ in 0..3 { + p = zigzag(&p, 1.0, 1000.0, false); + let n: usize = p.iter().map(|q| q.elements().len()).sum(); + assert!(n <= 2 * MAX_OPERATOR_SEGMENTS, "{n}"); + } + let w = wiggle(&p, &WiggleParams { detail: 1000.0, ..Default::default() }, 0.0); + let n: usize = w.iter().map(|q| q.elements().len()).sum(); + assert!(n <= 2 * MAX_OPERATOR_SEGMENTS, "{n}"); + let o = offset(&[sq(0.0, 0.0, 100.0)], 1.0, Join::Miter, 4.0, 1000.0, 0.0); + assert!(!o[0].elements().is_empty()); + // Within the budget nothing changes: 4 segments × 11 pieces. + assert_eq!(vertex_count(&zigzag(&[sq(0.0, 0.0, 100.0)], 1.0, 10.0, false)[0]), 44); +}