From cb248419792b5b3f819b4c8eee0116dcc3688383 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 21:26:46 +0000 Subject: [PATCH] address review: trace over-budget puppet outlines at a coarser resolution; NaN expansion test --- crates/effects/src/puppet/mesh.rs | 65 +++++++++++++++++++++++++----- crates/effects/src/puppet/tests.rs | 21 +++++++++- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/crates/effects/src/puppet/mesh.rs b/crates/effects/src/puppet/mesh.rs index f783e9f4e..d5fd66dae 100644 --- a/crates/effects/src/puppet/mesh.rs +++ b/crates/effects/src/puppet/mesh.rs @@ -620,23 +620,68 @@ pub fn triangulate(outline: &[[f64; 2]], opts: &MeshOpts) -> Mesh { 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; +/// Most cells the padded outline grid may have (it is expanded with two `f64` distance buffers). +const MAX_GRID_CELLS: usize = 1 << 25; +/// Most the alpha is shrunk by, per axis, to make the padded grid fit; a layer needing more has no +/// outline. +const MAX_DOWNSAMPLE: usize = 8; /// The outline of the alpha region under `seed` (buffer pixels → layer space via `scale` and -/// `offset`), expanded by `expansion` layer pixels (clamped to [`EXPANSION_RANGE`]). +/// `offset`), expanded by `expansion` layer pixels (clamped to [`EXPANSION_RANGE`]). When the +/// padded grid would pass [`MAX_PAD`] or [`MAX_GRID_CELLS`] (a high-scale layer with a large +/// Expansion), the alpha is traced at a coarser resolution instead of being skipped. 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(); - 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]; + let want = (expansion.max(0.0) * scale).ceil(); + if !want.is_finite() { + return vec![]; + } + for k in 1..=MAX_DOWNSAMPLE { + let pad = (want / k as f64).ceil(); + let (kw, kh) = (w.div_ceil(k), h.div_ceil(k)); + let fits = pad <= MAX_PAD && { + let pad = pad as usize + 2; + kw.checked_add(2 * pad).zip(kh.checked_add(2 * pad)).and_then(|(pw, ph)| pw.checked_mul(ph)).is_some_and(|c| c <= MAX_GRID_CELLS) + }; + if !fits { + continue; + } + if k == 1 { + return outline_padded(alpha, [w, h], scale, offset, seed, expansion, pad as usize + 2); + } + let small = shrink(alpha, w, h, k); + let kf = k as f64; + return outline_padded(&small, [kw, kh], scale / kf, [offset[0] / kf, offset[1] / kf], seed, expansion, pad as usize + 2); + } + vec![] +} + +/// `alpha` (`w × h`) with every `k × k` block averaged into one pixel. +fn shrink(alpha: &[f32], w: usize, h: usize, k: usize) -> Vec { + let (kw, kh) = (w.div_ceil(k), h.div_ceil(k)); + let mut out = vec![0.0f32; kw * kh]; + for y in 0..kh { + for x in 0..kw { + let (x1, y1) = (((x + 1) * k).min(w), ((y + 1) * k).min(h)); + let (mut sum, mut n) = (0.0f32, 0u32); + for yy in y * k..y1 { + for xx in x * k..x1 { + sum += alpha[yy * w + xx]; + n += 1; + } + } + out[y * kw + x] = sum / n.max(1) as f32; + } + } + out +} + +fn outline_padded(alpha: &[f32], [w, h]: [usize; 2], scale: f64, offset: [f64; 2], seed: [f64; 2], expansion: f64, pad: usize) -> Vec<[f64; 2]> { + let (pw, ph) = (w + 2 * pad, h + 2 * pad); + let mut on = vec![false; pw * ph]; 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 d9bb998b0..c2db999e1 100644 --- a/crates/effects/src/puppet/tests.rs +++ b/crates/effects/src/puppet/tests.rs @@ -115,16 +115,33 @@ fn hostile_expansion_is_clamped() { let max = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], EXPANSION_RANGE.1)).abs(); // A non-finite Expansion is taken as zero (the unexpanded outline), not clamped to the maximum. let zero = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], 0.0)).abs(); - for e in [1e12, f64::MAX, f64::INFINITY] { + for e in [1e12, f64::MAX, f64::INFINITY, f64::NAN] { let a = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], e)).abs(); let want = if e.is_finite() { max } else { zero }; assert!((a - want).abs() < 1.0, "{e}: {a} vs {want}"); } - 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); } +/// A high-scale layer with a large Expansion is traced at a coarser resolution, not skipped, and a +/// tiny buffer with a huge padded grid stays within the grid budget. +#[test] +fn high_scale_expansion_is_traced_coarser_instead_of_skipped() { + let (w, h) = (40, 40); + let alpha = disc_alpha(w, h, [20.0, 20.0], 10.0); + // Scale 32, Expansion 130: 4160 px of padding, past MAX_PAD, so it is shrunk by 2. + let scale = 32.0; + let o = outline(&alpha, w, h, scale, [0.0; 2], [20.0, 20.0], 130.0); + let area = signed_area(&o).abs(); + // The disc has radius 10 / 32 layer px; expanded by 130. + let r = 10.0 / scale + 130.0; + assert!((area - std::f64::consts::PI * r * r).abs() < 0.03 * std::f64::consts::PI * r * r, "{area}"); + // One pixel at scale 20.48 and Expansion 1e12 (clamped): its 8197 px square grid is over budget. + let o = outline(&[1.0], 1, 1, 20.48, [0.0; 2], [0.5, 0.5], 1e12); + assert!(o.len() >= 3); +} + 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 }) }