From 8594cf0d4108398c5062549c936530b292c2fa99 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Mon, 2 Nov 2026 20:33:36 -0500 Subject: [PATCH] Puppet: clamp an imported Expansion before padding the outline grid --- crates/effects/src/puppet/mesh.rs | 24 ++++++++++++++++++++---- crates/effects/src/puppet/tests.rs | 16 ++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/crates/effects/src/puppet/mesh.rs b/crates/effects/src/puppet/mesh.rs index f01ee8f78..f783e9f4e 100644 --- a/crates/effects/src/puppet/mesh.rs +++ b/crates/effects/src/puppet/mesh.rs @@ -614,13 +614,29 @@ pub fn triangulate(outline: &[[f64; 2]], opts: &MeshOpts) -> Mesh { Mesh { verts, tris, outline: poly } } +/// The Expansion range the outline honours, in layer pixels (the property's slider range). A +/// project file can store any number; outside this range it is clamped, so the padded grid +/// below stays a few hundred pixels larger than the buffer. +pub const EXPANSION_RANGE: (f64, f64) = (-100.0, 200.0); +/// Most buffer pixels of padding the outline grid takes on each side (Expansion × buffer scale). +const MAX_PAD: f64 = 4096.0; + /// The outline of the alpha region under `seed` (buffer pixels → layer space via `scale` and -/// `offset`), expanded by `expansion` layer pixels. +/// `offset`), expanded by `expansion` layer pixels (clamped to [`EXPANSION_RANGE`]). pub fn outline(alpha: &[f32], w: usize, h: usize, scale: f64, offset: [f64; 2], seed: [f64; 2], expansion: f64) -> Vec<[f64; 2]> { + let expansion = if expansion.is_finite() { expansion.clamp(EXPANSION_RANGE.0, EXPANSION_RANGE.1) } else { 0.0 }; // Pad so expansion can grow past the buffer edge. - let pad = (expansion.max(0.0) * scale).ceil() as usize + 2; - let (pw, ph) = (w + 2 * pad, h + 2 * pad); - let mut on = vec![false; pw * ph]; + let pad = (expansion.max(0.0) * scale).ceil(); + if !pad.is_finite() || pad > MAX_PAD { + return vec![]; + } + let pad = pad as usize + 2; + let Some((pw, ph)) = w.checked_add(2 * pad).zip(h.checked_add(2 * pad)) else { return vec![] }; + let Some(cells) = pw.checked_mul(ph) else { return vec![] }; + if alpha.len() < w.saturating_mul(h) { + return vec![]; + } + let mut on = vec![false; cells]; for y in 0..h { for x in 0..w { on[(y + pad) * pw + x + pad] = alpha[y * w + x] >= 0.5; diff --git a/crates/effects/src/puppet/tests.rs b/crates/effects/src/puppet/tests.rs index 10426d46c..ed528f6a9 100644 --- a/crates/effects/src/puppet/tests.rs +++ b/crates/effects/src/puppet/tests.rs @@ -106,6 +106,22 @@ fn expansion_grows_the_outline() { assert!((am - pi * 225.0).abs() < 60.0, "{am}"); } +/// An imported Expansion far past the property's range is clamped instead of padding the grid +/// by that many pixels (a 1e12 expansion would ask for terabytes). +#[test] +fn hostile_expansion_is_clamped() { + let (w, h) = (40, 40); + let alpha = disc_alpha(w, h, [20.0, 20.0], 10.0); + let max = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], EXPANSION_RANGE.1)).abs(); + for e in [1e12, f64::MAX, f64::INFINITY] { + let a = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], e)).abs(); + assert!((a - max).abs() < 1.0 || e.is_infinite(), "{e}: {a} vs {max}"); + } + assert!(outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], f64::NAN).len() >= 3); + assert!(outline(&alpha, w, h, 1e9, [0.0; 2], [20.0, 20.0], 100.0).is_empty()); + let _ = outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], -1e12); +} + fn rect_mesh(w: f64, h: f64, tris: f64) -> Mesh { triangulate(&[[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]], &MeshOpts { triangles: tris, density: 50.0, expansion: 0.0 }) }