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); +}