diff --git a/iTriangle/README.md b/iTriangle/README.md index 19b1390..f6dc269 100644 --- a/iTriangle/README.md +++ b/iTriangle/README.md @@ -176,6 +176,13 @@ let convex_polygons = shape.triangulate().into_delaunay().to_convex_polygons(); println!("convex polygons: {:?}", convex_polygons); +// Limit each convex polygon to at most 6 vertices (limits below 3 are treated as 3). +// Oversized regions are split into a fan; new diagonals may cross mesh triangles. +let bounded_convex_polygons = shape + .triangulate() + .into_delaunay() + .to_convex_polygons_with_max_vertices(6); + let tessellation: Triangulation<[f64; 2], u16> = shape .triangulate() .into_delaunay() diff --git a/iTriangle/src/advanced/convex.rs b/iTriangle/src/advanced/convex.rs index 29161de..9078022 100644 --- a/iTriangle/src/advanced/convex.rs +++ b/iTriangle/src/advanced/convex.rs @@ -212,6 +212,20 @@ impl IntDelaunay { /// assert!(!polygons.is_empty()); /// ``` pub fn to_convex_polygons(&self) -> Vec> { + self.convex_polygons(usize::MAX) + } + + /// Returns counter-clockwise convex polygons with at most `max_vertices.max(3)` vertices. + /// + /// First builds the same convex regions as [`Self::to_convex_polygons`], then + /// splits oversized, simplified contours into a fan using existing vertices. + /// + /// Values of `max_vertices` below 3 are treated as 3. + pub fn to_convex_polygons_with_max_vertices(&self, max_vertices: usize) -> Vec> { + self.convex_polygons(max_vertices.max(3)) + } + + fn convex_polygons(&self, max_vertices: usize) -> Vec> { let mut result = Vec::new(); let n = self.triangles.len(); @@ -238,13 +252,39 @@ impl IntDelaunay { } } - result.push(builder.to_contour()) + append_fan_parts(builder.to_contour(), max_vertices, &mut result); } result } } +fn append_fan_parts( + contour: IntContour, + max_vertices: usize, + result: &mut Vec>, +) { + if contour.len() <= max_vertices { + result.push(contour); + return; + } + + let triangle_count = contour.len() - 2; + let part_count = triangle_count.div_ceil(max_vertices - 2); + let triangles_per_part = triangle_count / part_count; + let extra = triangle_count % part_count; + let mut start = 1; + + for i in 0..part_count { + let end = start + triangles_per_part + usize::from(i < extra); + let mut part = Vec::with_capacity(end - start + 2); + part.push(contour[0]); + part.extend_from_slice(&contour[start..=end]); + result.push(part); + start = end; + } +} + #[cfg(test)] mod tests { use crate::int::triangulatable::IntTriangulatable; diff --git a/iTriangle/src/float/convex.rs b/iTriangle/src/float/convex.rs index 4fd3ac1..fe214ad 100644 --- a/iTriangle/src/float/convex.rs +++ b/iTriangle/src/float/convex.rs @@ -13,4 +13,14 @@ impl Delaunay { pub fn to_convex_polygons(&self) -> Vec> { self.delaunay.to_convex_polygons().to_float(&self.adapter) } + + /// Returns counter-clockwise convex polygons with at most `max_vertices.max(3)` vertices. + /// + /// Values of `max_vertices` below 3 are treated as 3. + #[inline] + pub fn to_convex_polygons_with_max_vertices(&self, max_vertices: usize) -> Vec> { + self.delaunay + .to_convex_polygons_with_max_vertices(max_vertices) + .to_float(&self.adapter) + } } diff --git a/iTriangle/tests/convex_max_vertices_tests.rs b/iTriangle/tests/convex_max_vertices_tests.rs new file mode 100644 index 0000000..e5f4f5f --- /dev/null +++ b/iTriangle/tests/convex_max_vertices_tests.rs @@ -0,0 +1,163 @@ +use i_overlay::core::integer::OverlayInt; +use i_overlay::i_float::int::point::IntPoint; +use i_triangle::advanced::delaunay::IntDelaunay; +use i_triangle::float::triangulatable::Triangulatable; +use i_triangle::int::triangulatable::IntTriangulatable; + +type Point = IntPoint; + +fn cross(a: Point, b: Point, c: Point) -> i64 { + (b.x as i64 - a.x as i64) * (c.y as i64 - a.y as i64) + - (b.y as i64 - a.y as i64) * (c.x as i64 - a.x as i64) +} + +fn area_two(polygon: &[Point]) -> i64 { + (1..polygon.len() - 1) + .map(|i| cross(polygon[0], polygon[i], polygon[i + 1])) + .sum() +} + +fn separated(a: &[Point], b: &[Point]) -> bool { + (0..a.len()).any(|i| b.iter().all(|&p| cross(a[i], a[(i + 1) % a.len()], p) <= 0)) +} + +fn validate(mesh: &IntDelaunay, limit: usize) -> Vec> { + let original = mesh.to_convex_polygons(); + let parts = mesh.to_convex_polygons_with_max_vertices(limit); + assert_eq!( + parts.iter().map(|p| area_two(p)).sum::(), + original.iter().map(|p| area_two(p)).sum::() + ); + for (i, part) in parts.iter().enumerate() { + assert!((3..=limit).contains(&part.len())); + for j in 0..part.len() { + assert!( + cross( + part[j], + part[(j + 1) % part.len()], + part[(j + 2) % part.len()] + ) > 0 + ); + } + // Each part stays inside one original convex region and uses its vertices. + assert!(original.iter().any(|region| { + part.iter().all(|p| region.contains(p)) + && (0..region.len()).all(|j| { + part.iter() + .all(|&p| cross(region[j], region[(j + 1) % region.len()], p) >= 0) + }) + })); + for other in &parts[..i] { + assert!(separated(part, other) || separated(other, part)); + } + } + parts +} + +#[test] +fn convex_contours_have_minimal_balanced_parts() { + for n in (3..=40).chain([257]) { + let contour: Vec<_> = (0..n).map(|x| Point::new(x, x * x)).collect(); + let mesh = contour.triangulate().into_delaunay(); + let original = mesh.to_convex_polygons(); + assert_eq!(original.len(), 1); + assert_eq!(original[0].len(), n as usize); + for limit in [3, 4, 5, 6, 9, n as usize, usize::MAX] { + let parts = validate(&mesh, limit); + assert_eq!(parts.len(), (n as usize - 2).div_ceil(limit - 2)); + let min = parts.iter().map(Vec::len).min().unwrap(); + let max = parts.iter().map(Vec::len).max().unwrap(); + assert!(max - min <= 1); + if n as usize <= limit { + assert_eq!(parts, original); + } + } + } +} + +#[test] +fn limit_applies_after_collinear_vertices_are_removed() { + let contour = vec![ + Point::new(0, 0), + Point::new(2, 0), + Point::new(4, 0), + Point::new(4, 2), + Point::new(4, 4), + Point::new(2, 4), + Point::new(0, 4), + Point::new(0, 2), + ]; + let mesh = contour.triangulate().into_delaunay(); + let parts = validate(&mesh, 4); + assert_eq!(parts.len(), 1); + assert_eq!(parts[0].len(), 4); + assert_eq!(parts, mesh.to_convex_polygons()); + assert_eq!(validate(&mesh, 3).len(), 2); +} + +#[test] +fn decomposition_preserves_a_hole() { + let outer: Vec<_> = [[0, 0], [20, 0], [20, 20], [0, 20]] + .map(|[x, y]| Point::new(x, y)) + .into(); + let hole: Vec<_> = [[5, 5], [5, 15], [15, 15], [15, 5]] + .map(|[x, y]| Point::new(x, y)) + .into(); + let mesh = vec![outer, hole].triangulate().into_delaunay(); + for limit in [3, 4, 6] { + let parts = validate(&mesh, limit); + assert_eq!(parts.iter().map(|p| area_two(p)).sum::(), 600); + } +} + +#[test] +fn empty_mesh_and_limits_below_three() { + let mesh = IntDelaunay:: { + triangles: Vec::new(), + points: Vec::new(), + }; + for limit in 0..=3 { + assert!(mesh.to_convex_polygons_with_max_vertices(limit).is_empty()); + } + + let contour: Vec<_> = (0..11).map(|x| Point::new(x, x * x)).collect(); + let mesh = contour.triangulate().into_delaunay(); + let triangles = validate(&mesh, 3); + for limit in 0..3 { + assert_eq!(mesh.to_convex_polygons_with_max_vertices(limit), triangles); + } +} + +#[test] +fn float_api_converts_the_integer_parts() { + check_float::(); + check_float::(); + check_float::(); +} + +fn check_float() { + let contour: Vec<[f64; 2]> = (0..11).map(|x| [x as f64, (x * x) as f64]).collect(); + let mesh = contour.triangulate_as::().into_delaunay(); + let original = mesh.to_convex_polygons(); + assert_eq!(original.len(), 1); + assert_eq!(original[0].len(), 11); + let parts = mesh.to_convex_polygons_with_max_vertices(6); + assert_eq!(parts.iter().map(Vec::len).collect::>(), [5, 5, 5]); + let area = |p: &Vec<[f64; 2]>| { + (0..p.len()) + .map(|i| { + let a = p[i]; + let b = p[(i + 1) % p.len()]; + a[0] * b[1] - a[1] * b[0] + }) + .sum::() + }; + assert!((parts.iter().map(area).sum::() - area(&original[0])).abs() < 1e-8); + assert!(parts.iter().flatten().all(|p| original[0].contains(p))); + assert_eq!(mesh.to_convex_polygons_with_max_vertices(11), original); + let triangles = mesh.to_convex_polygons_with_max_vertices(3); + assert!(triangles.iter().all(|p| p.len() == 3)); + for limit in 0..3 { + assert_eq!(mesh.to_convex_polygons_with_max_vertices(limit), triangles); + } +}