From e91d6d76256dfb96cdcf878d1d95ebdeac0db609 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 16:05:31 +0000 Subject: [PATCH] fix(geom): bound the points one polyline tessellation samples Polyline::tessellate sampled every arc segment (up to 4096 points each) with no total limit, and hatch boundary preparation did this for every loop, so compact curved vertices expanded into gigabytes. tessellate_within takes a point budget; once spent, remaining arcs are drawn as chords. Hatch loops share one budget. --- crates/geom/src/curve.rs | 18 +++++++++++++++++- crates/geom/src/lib.rs | 3 ++- crates/geom/src/tests.rs | 12 ++++++++++++ crates/render/src/hatch.rs | 5 ++++- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/geom/src/curve.rs b/crates/geom/src/curve.rs index 04f665f88..972d0f8e8 100644 --- a/crates/geom/src/curve.rs +++ b/crates/geom/src/curve.rs @@ -484,6 +484,9 @@ pub struct Polyline { pub closed: bool, } +/// Sampled points one [`Polyline::tessellate`] call produces before arcs become chords. +pub const MAX_TESSELLATED_POINTS: usize = 4_000_000; + impl Polyline { pub fn from_points(pts: &[Vec2], closed: bool) -> Self { Polyline { vertices: pts.iter().map(|p| PolyVertex::new(*p)).collect(), closed } @@ -516,10 +519,23 @@ impl Polyline { b } pub fn tessellate(&self, tol: f64) -> Vec { + let mut budget = MAX_TESSELLATED_POINTS; + self.tessellate_within(tol, &mut budget) + } + /// [`Polyline::tessellate`] spending at most `budget` sampled points (shared by the caller + /// across many polylines): once it is spent, the remaining arcs are drawn as chords, so the + /// output stays proportional to the input vertices. Each arc can sample thousands of points, + /// so a compact polyline could otherwise expand into gigabytes (CWE-400). + pub fn tessellate_within(&self, tol: f64, budget: &mut usize) -> Vec { let mut out: Vec = Vec::new(); for s in self.segments() { let mut pts = Vec::new(); - s.tessellate(tol, &mut pts); + if *budget > 0 { + s.tessellate(tol, &mut pts); + *budget = budget.saturating_sub(pts.len()); + } else { + pts.extend([s.start(), s.end()]); + } let skip = usize::from(out.last().is_some_and(|l| pts.first().is_some_and(|f| l.near(*f, 1e-9)))); out.extend(pts.into_iter().skip(skip)); } diff --git a/crates/geom/src/lib.rs b/crates/geom/src/lib.rs index e62f478fb..d482141fd 100644 --- a/crates/geom/src/lib.rs +++ b/crates/geom/src/lib.rs @@ -20,7 +20,8 @@ mod vec; pub use bounds::Bounds2; pub use curve::{ - Arc, Circle, Ellipse, Line, PolyVertex, Polyline, Segment, arc_to_bulge, bulge_to_arc, point_in_polygon, polygon_centroid, shoelace, + Arc, Circle, Ellipse, Line, MAX_TESSELLATED_POINTS, PolyVertex, Polyline, Segment, arc_to_bulge, bulge_to_arc, point_in_polygon, + polygon_centroid, shoelace, }; pub use fillet::{CornerFit, TanCurve, tangent_circles}; pub use intersect::{circle_circle, intersect_ext, intersect_segments, line_circle, line_line, line_line_infinite}; diff --git a/crates/geom/src/tests.rs b/crates/geom/src/tests.rs index 915a1071e..57ffcd663 100644 --- a/crates/geom/src/tests.rs +++ b/crates/geom/src/tests.rs @@ -221,6 +221,18 @@ fn segment_offset() { assert!(s.offset(6.0).is_none()); } +/// A polyline of many fine arcs samples at most the point budget, then draws chords. +#[test] +fn polyline_tessellation_is_bounded() { + let vertices: Vec = (0..3000).map(|i| PolyVertex { bulge: 1.0, ..PolyVertex::new(Vec2::new(f64::from(i) * 1e6, 0.0)) }).collect(); + let pl = Polyline { vertices, closed: false }; + let pts = pl.tessellate(1e-9); + assert!(pts.len() <= MAX_TESSELLATED_POINTS + 2 * 3000 + 4096, "{}", pts.len()); + assert!(pts.len() > 3000); + let mut budget = 10; + assert!(pl.tessellate_within(1e-9, &mut budget).len() <= 4096 + 2 * 3000); +} + #[test] fn arc_segments_bounded() { assert_eq!(arc_segments(0.0, 1.0, 0.1), 1); diff --git a/crates/render/src/hatch.rs b/crates/render/src/hatch.rs index 1bfeb1b6c..ecd78fdfe 100644 --- a/crates/render/src/hatch.rs +++ b/crates/render/src/hatch.rs @@ -128,7 +128,10 @@ const MAX_DEDUP_COMPARISONS: u64 = 50_000_000; /// within islands alternate), 1 outer keeps the outermost loops and their first islands, 2 /// ignore keeps only the outermost loops. Nesting depth = how many other loops enclose a loop. pub fn fill_loops(h: &Hatch, tol: f64) -> Vec> { - let mut loops: Vec> = h.loops.iter().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate(tol)).collect(); + // One point budget for all the loops: many short arcs across loops must not add up unbounded. + let mut budget = cadcraft_geom::MAX_TESSELLATED_POINTS; + let mut loops: Vec> = + h.loops.iter().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate_within(tol, &mut budget)).collect(); dedup_polygons(&mut loops); let max_depth = match h.style { 1 => 1,