mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 02:34:09 +00:00
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:
@@ -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).
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user