3D effects: every ring pair counts toward the containment budget; no zero-sized inflate cells (#5413)

* fix(three-d): charge every ring pair; refuse zero-sized inflate cells

* fix(three-d): clippy-friendly cell check

* address review: reject subnormal inflate cells
This commit is contained in:
Dylan M. Taylor
2026-11-03 23:21:52 -05:00
committed by GitHub
parent 3226c2ade7
commit 5cb58c0507
3 changed files with 35 additions and 2 deletions
+7 -1
View File
@@ -28,7 +28,13 @@ fn puff(path: &PathData, rule: FillRule, bounds: Rect, o: &Inflate) -> Result<Op
}
const CELLS: usize = 48;
let cell = size / CELLS as f64;
let (nx, ny) = (((bounds.width() / cell).ceil() as usize).max(1), ((bounds.height() / cell).ceil() as usize).max(1));
// A subnormal size gives cells of no size (the grid would be `usize::MAX` cells wide), and a
// subnormal cell underflows the solver's stencil products.
if !cell.is_normal() {
return Ok(None);
}
let side = |len: f64| ((len / cell).ceil() as usize).clamp(1, CELLS + 1);
let (nx, ny) = (side(bounds.width()), side(bounds.height()));
let edges = Edges::new(&rings);
let m = Membrane::solve(&rings, &edges, bounds.origin(), cell, nx + 1, ny + 1);
// A circle of radius r puffs into a half sphere of radius r at 50 % (its membrane peaks at r²/4).
+6 -1
View File
@@ -63,14 +63,19 @@ pub(super) fn rings(path: &PathData, rule: FillRule, size: f64, limit: usize) ->
}
}
// Which rings hold which: each ring against the others, its edges walked only where its
// bounds hold the point, and within [`MAX_CONTAINMENT_EDGES`] edges in all.
// bounds hold the point, and within [`MAX_CONTAINMENT_EDGES`] edges and pairs in all (a pair
// whose bounds miss the point costs a step too: many small rings are a square of pairs).
let too_complex = || String::from("The shape is too complex for a 3D effect");
let mut work = 0usize;
let mut within = |out: &[Vec<Point>], bounds: &[Rect], i: usize| -> Result<Vec<usize>, String> {
let p = out.get(i).and_then(|r| r.first()).copied().unwrap_or_default();
let mut held_by = vec![];
for (j, (o, b)) in out.iter().zip(bounds).enumerate() {
work = work.saturating_add(1);
if j == i || !(b.x0 <= p.x && p.x <= b.x1 && b.y0 <= p.y && p.y <= b.y1) {
if work > MAX_CONTAINMENT_EDGES {
return Err(too_complex());
}
continue;
}
work = work.saturating_add(o.len());
+22
View File
@@ -732,3 +732,25 @@ fn nonzero_shapes_keep_the_middle_a_same_way_contour_encloses() {
assert!(caps(&nested(false)) > caps(&nested(true)) + 1000.0);
assert!(inflate(&nested(false), FillRule::NonZero, outer, Inflate::default(), front()).is_ok());
}
#[test]
fn inflate_over_bounds_of_subnormal_size_makes_no_grid() {
let (p, _) = shape(&square(0.0, 0.0, 10.0));
// 5e-324 divides to a zero cell; 1e-307 is a normal size whose cell (size / 48) is subnormal.
for size in [5e-324, 1e-307] {
let tiny = Rect::new(0.0, 0.0, size, size);
assert_eq!(inflate(&p, FillRule::EvenOdd, tiny, Inflate::default(), front()).unwrap().len(), 0, "{size}");
}
}
#[test]
fn many_small_rings_far_apart_are_too_complex() {
// 8,000 squares in a row: no square's bounds hold another's corner, yet each pair is a step.
let mut b = BezPath::new();
for i in 0..8000 {
b.extend(square(i as f64 * 3.0, 0.0, 1.0));
}
let (p, r) = shape(&b);
let e = rings(&p, FillRule::NonZero, r.width().max(r.height()), usize::MAX).unwrap_err();
assert!(e.contains("too complex"), "{e}");
}