address review: trace over-budget puppet outlines at a coarser resolution; NaN expansion test

This commit is contained in:
Dylan M. Taylor
2026-11-03 21:26:46 +00:00
parent d06dc553f3
commit cb24841979
2 changed files with 74 additions and 12 deletions
+55 -10
View File
@@ -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<f32> {
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;
+19 -2
View File
@@ -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 })
}