From 0155eaf468575f9fe4a005dcfad60285d54116f8 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 15 Oct 2026 01:32:58 -0700 Subject: [PATCH] 3D and Materials: Inflate builds its edge index once inflate built an edge index (Edges) of the shape's rings, and Membrane::solve, which inflate calls first, built a second index of the same rings. Membrane::solve now takes the index that inflate builds. In a release build, one Edges::new takes 0.08 ms of the 27 ms that inflate takes on a ring of 2,000 points, and 2.0 ms of 219 ms on a ring of 1,000 spikes whose 2,000 edges fill 693,816 bucket entries. The facets do not change: for nine shapes, three views and 96 combinations of the Inflate settings, the Debug text of all 2,592 results is byte for byte the same on main and with the change. --- crates/three-d/src/solid.rs | 6 +++--- crates/three-d/src/tests_solid.rs | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/three-d/src/solid.rs b/crates/three-d/src/solid.rs index bb16efa21..f6f3e3661 100644 --- a/crates/three-d/src/solid.rs +++ b/crates/three-d/src/solid.rs @@ -886,8 +886,8 @@ struct Membrane { } impl Membrane { - fn solve(rings: &[Vec], origin: Point, step: f64, nx: usize, ny: usize) -> Self { - let edges = Edges::new(rings); + /// `edges` is the index of `rings`' edges (`Edges::new(rings)`). + fn solve(rings: &[Vec], edges: &Edges, origin: Point, step: f64, nx: usize, ny: usize) -> Self { let node = |i: usize, j: usize| Point::new(origin.x + i as f64 * step, origin.y + j as f64 * step); let idx = |i: usize, j: usize| j * nx + i; // A node on the outline itself is held at zero with the outside. @@ -1035,8 +1035,8 @@ pub fn inflate(path: &PathData, rule: FillRule, bounds: Rect, options: Inflate, 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)); - let m = Membrane::solve(&rings, bounds.origin(), cell, nx + 1, ny + 1); let edges = Edges::new(&rings); + let m = Membrane::solve(&rings, &edges, bounds.origin(), cell, nx + 1, ny + 1); let outline = PathData::new(rings.iter().map(|r| vectorcraft_geom::SubPath::polyline(r, true)).collect()); // A circle of radius r puffs into a half sphere of radius r at 50 % (its membrane peaks at r²/4). let height = 2.0 * m.peak.sqrt() * o.volume / 50.0; diff --git a/crates/three-d/src/tests_solid.rs b/crates/three-d/src/tests_solid.rs index 72f007cef..14c5ebca6 100644 --- a/crates/three-d/src/tests_solid.rs +++ b/crates/three-d/src/tests_solid.rs @@ -284,10 +284,12 @@ fn the_inflate_membrane_matches_known_solutions() { let mut c = BezPath::new(); c.extend(vectorcraft_geom::kurbo::Shape::path_elements(&vectorcraft_geom::kurbo::Circle::new((100.0, 100.0), 50.0), 0.01)); let (p, b) = shape(&c); - let disc = Membrane::solve(&rings(&p, FillRule::EvenOdd, 100.0, MAX_SOLID_POINTS).unwrap(), b.origin(), b.width() / 48.0, 49, 49); + let r = rings(&p, FillRule::EvenOdd, 100.0, MAX_SOLID_POINTS).unwrap(); + let disc = Membrane::solve(&r, &Edges::new(&r), b.origin(), b.width() / 48.0, 49, 49); assert!(close(disc.peak, 50.0 * 50.0 / 4.0, 625.0 * 0.02), "{}", disc.peak); let (p, b) = shape(&square(0.0, 0.0, 100.0)); - let sq = Membrane::solve(&rings(&p, FillRule::EvenOdd, 100.0, MAX_SOLID_POINTS).unwrap(), b.origin(), 100.0 / 48.0, 49, 49); + let r = rings(&p, FillRule::EvenOdd, 100.0, MAX_SOLID_POINTS).unwrap(); + let sq = Membrane::solve(&r, &Edges::new(&r), b.origin(), 100.0 / 48.0, 49, 49); assert!(close(sq.peak, 0.07367 * 100.0 * 100.0, 736.7 * 0.02), "{}", sq.peak); // Smooth across the middle: no crease where a distance-to-the-edge height would have one // (the diagonals). The second difference along a diagonal stays small and even.