From 36bccf0f34a4d0d8e5f4c6c0502bca4f6d253607 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 03:24:40 -0800 Subject: [PATCH] 3D: Inflate with no volume copies only shapes that might fit its limits With no volume, each shape waits to be solved until its solid is taken, so its path is copied first. A shape with more contours or anchors than the solid's point limit allows is now refused before it is copied, with the error solving it would give, instead of being copied in full. The check counts each contour's anchors without reading them. --- crates/three-d/src/solid/inflate.rs | 50 +++++++++++++++++++++++------ crates/three-d/src/solid/tests.rs | 32 ++++++++++++++++++ 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/crates/three-d/src/solid/inflate.rs b/crates/three-d/src/solid/inflate.rs index 5ef7ba6d4d..5edf57a894 100644 --- a/crates/three-d/src/solid/inflate.rs +++ b/crates/three-d/src/solid/inflate.rs @@ -21,14 +21,35 @@ struct Puff { height: f64, } +/// The pieces each curve of `path` is sampled in for Inflate's solid ([`curve_pieces`]); an error +/// when its outline does not fit the solid's limits. +fn outline_pieces(path: &PathData, bounds: Rect, o: &Inflate) -> Result { + // The sides take one face for each edge of the outline in each slice; the grid's faces are + // counted as they are built. + curve_pieces(path, bounds.width().max(bounds.height()), if o.depth > 0.0 { slices(o) } else { 0 }, 0) +} + +/// Whether `path` has few enough anchors that [`curve_pieces`] might take it: fewer than +/// [`MAX_SOLID_POINTS`] contours, and at most three times as many anchors (a contour of one anchor +/// adds one, and every other takes at most two anchors for each segment it adds). Counts the +/// contours' anchors without reading them. +pub(super) fn might_fit(path: &PathData) -> bool { + let mut anchors = 0usize; + for (contours, sp) in path.subpaths.iter().enumerate() { + anchors = anchors.saturating_add(sp.anchors.len()); + if contours >= MAX_SOLID_POINTS || anchors > MAX_SOLID_POINTS.saturating_mul(3) { + return false; + } + } + true +} + /// The rings of `path` and its membrane on the grid of `bounds`; `None` for a shape with no area. fn puff(path: &PathData, rule: FillRule, bounds: Rect, o: &Inflate) -> Result, String> { #[cfg(test)] PUFFED.with(|n| n.set(n.get() + 1)); - // The sides take one face for each edge of the outline in each slice; the grid's faces are - // counted as they are built. let size = bounds.width().max(bounds.height()); - let most = curve_pieces(path, size, if o.depth > 0.0 { slices(o) } else { 0 }, 0)?; + let most = outline_pieces(path, bounds, o)?; let rings = solid_rings(path, rule, size, MAX_SOLID_POINTS, most)?; if rings.is_empty() { return Ok(None); @@ -88,7 +109,18 @@ pub(crate) fn inflate_faces(shapes: &[(&PathData, FillRule)], bounds: Rect, opti #[cfg(test)] let later = later && !SOLVE_FIRST.with(std::cell::Cell::get); if later { - let shapes: Vec<(PathData, FillRule)> = shapes.iter().map(|(path, rule)| ((*path).clone(), *rule)).collect(); + // A shape is copied to be solved later only when its anchors are few enough for the + // solid's point limit; a larger one is refused now, with the error its puff gives. + let shapes: Vec> = shapes + .iter() + .map(|(path, rule)| { + check_bounds(bounds)?; + if !might_fit(path) { + outline_pieces(path, bounds, &o)?; + } + Ok(((*path).clone(), *rule)) + }) + .collect(); return Solids { pending: Pending::Later(shapes.into_iter()), bounds, o, view, reach: 0.0, keep }; } let puffs: Vec, String>> = @@ -98,10 +130,11 @@ pub(crate) fn inflate_faces(shapes: &[(&PathData, FillRule)], bounds: Rect, opti } /// The shapes' puffs still to take: every one solved when the shapes were laid out, or each -/// shape's path and fill rule, its puff solved when its solid is taken. +/// shape's path and fill rule (or the error its bounds or outline already gives), its puff solved +/// when its solid is taken. enum Pending { Solved(std::vec::IntoIter, String>>), - Later(std::vec::IntoIter<(PathData, FillRule)>), + Later(std::vec::IntoIter>), } /// The solids of [`inflate_faces`], one result for each shape in order, each built as it is taken. @@ -135,10 +168,7 @@ impl Iterator for Solids { fn next(&mut self) -> Option { let p = match &mut self.pending { Pending::Solved(puffs) => puffs.next()?, - Pending::Later(shapes) => { - let (path, rule) = shapes.next()?; - check_bounds(self.bounds).and_then(|()| puff(&path, rule, self.bounds, &self.o)) - } + Pending::Later(shapes) => shapes.next()?.and_then(|(path, rule)| puff(&path, rule, self.bounds, &self.o)), }; Some(match p { Ok(Some(p)) => inflated(p, self.bounds, self.o, self.view, self.reach, self.keep), diff --git a/crates/three-d/src/solid/tests.rs b/crates/three-d/src/solid/tests.rs index 3e516f7a57..a5984321ee 100644 --- a/crates/three-d/src/solid/tests.rs +++ b/crates/three-d/src/solid/tests.rs @@ -1097,6 +1097,38 @@ fn a_flat_inflate_solved_as_taken_matches_one_solved_first() { } } +/// With no volume, a shape with too many anchors or contours for the solid is refused before it +/// is kept for later, with the error that solving every puff first gives it. +#[test] +fn a_flat_inflate_refuses_a_shape_past_the_point_limit_as_one_solved_first() { + use super::inflate::might_fit; + let many = MAX_SOLID_POINTS * 3 + 10; + let anchors: Vec = (0..many).map(|i| Point::new((i % 200) as f64, (i / 200) as f64 / 10.0)).collect(); + let long = PathData::new(vec![vectorcraft_geom::SubPath::polyline(&anchors, true)]); + let mut nan = anchors.clone(); + if let Some(last) = nan.last_mut() { + *last = Point::new(f64::NAN, 0.0); + } + let invalid = PathData::new(vec![vectorcraft_geom::SubPath::polyline(&nan, true)]); + let dots = PathData::new((0..=MAX_SOLID_POINTS).map(|i| vectorcraft_geom::SubPath::polyline(&[Point::new(i as f64, 0.0)], false)).collect()); + let square = shape(&square(0.0, 0.0, 100.0)).0; + assert!(might_fit(&square) && !might_fit(&long) && !might_fit(&invalid) && !might_fit(&dots)); + let paths = [square, long, invalid, dots]; + let shapes: Vec<(&PathData, FillRule)> = paths.iter().map(|p| (p, FillRule::EvenOdd)).collect(); + let b = Rect::new(-10.0, -10.0, 250.0, 250.0); + let o = Inflate { volume: 0.0, ..Inflate::default() }; + let solids = |first: bool| { + SOLVE_FIRST.with(|s| s.set(first)); + let out: Vec> = inflate_faces(&shapes, b, o, front(), build::Keep::Facets).collect(); + SOLVE_FIRST.with(|s| s.set(false)); + out + }; + let (taken, first) = (solids(false), solids(true)); + assert!(taken[0].as_ref().is_ok_and(|(f, ..)| !f.is_empty())); + assert!(taken[1..].iter().all(Result::is_err), "{:?}", taken[1..].iter().map(|r| r.as_ref().err()).collect::>()); + assert_eq!(format!("{taken:?}"), format!("{first:?}")); +} + /// #1724: under the nonzero rule a contour inside another that runs the same way cuts no hole /// (the middle is filled, as the art is drawn); one running the other way does, and so does /// either under the even-odd rule.