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,