From 197ccff7f4ae127ed42554f2b60cba39d44379c8 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 11 Oct 2026 12:51:29 +0300 Subject: [PATCH 1/2] new delaunay into api --- iTriangle/src/int/triangulator.rs | 163 +++++++++ iTriangle/tests/int_delaunay_into_tests.rs | 379 +++++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 iTriangle/tests/int_delaunay_into_tests.rs diff --git a/iTriangle/src/int/triangulator.rs b/iTriangle/src/int/triangulator.rs index b21b5f1..b29fd27 100644 --- a/iTriangle/src/int/triangulator.rs +++ b/iTriangle/src/int/triangulator.rs @@ -1,4 +1,5 @@ use crate::advanced::buffer::DelaunayBuffer; +use crate::advanced::delaunay::IntDelaunay; use crate::int::earcut::earcut_64::Earcut64; use crate::int::monotone::triangulator::MonotoneTriangulator; use crate::int::triangulation::{IndexType, IntTriangulation, RawIntTriangulation}; @@ -59,6 +60,64 @@ where I: OverlayInt, N: IndexType, { + /// Simplifies a contour and fills a Delaunay mesh + #[inline] + pub fn delaunay_contour_into( + &mut self, + contour: &IntContour, + delaunay: &mut IntDelaunay, + ) { + match self.overlay.simplify_contour(contour, self.fill_rule) { + None => self.uncheck_delaunay_contour_into(contour, delaunay), + Some(shapes) if shapes.is_empty() => { + delaunay.points.clear(); + delaunay.triangles.clear(); + } + Some(shapes) => self.uncheck_delaunay_shapes_into(&shapes, delaunay), + } + } + + /// Simplifies a shape and fills a Delaunay mesh + #[inline] + pub fn delaunay_shape_into(&mut self, shape: &IntShape, delaunay: &mut IntDelaunay) { + match self.overlay.simplify_shape(shape, self.fill_rule) { + None => self.uncheck_delaunay_shape_into(shape, delaunay), + Some(shapes) if shapes.is_empty() => { + delaunay.points.clear(); + delaunay.triangles.clear(); + } + Some(shapes) => self.uncheck_delaunay_shapes_into(&shapes, delaunay), + } + } + + /// Simplifies shapes and fills a Delaunay mesh + #[inline] + pub fn delaunay_shapes_into(&mut self, shapes: &IntShapes, delaunay: &mut IntDelaunay) { + let simple = self.overlay.simplify_source(shapes, self.fill_rule); + if simple.is_empty() { + delaunay.points.clear(); + delaunay.triangles.clear(); + } else { + self.uncheck_delaunay_shapes_into(&simple, delaunay); + } + } + + /// Simplifies flat contours in place and fills a Delaunay mesh + #[inline] + pub fn delaunay_flat_into( + &mut self, + flat: &mut FlatContoursBuffer, + delaunay: &mut IntDelaunay, + ) { + self.overlay.simplify_flat_buffer(flat, self.fill_rule); + if flat.is_empty() { + delaunay.points.clear(); + delaunay.triangles.clear(); + } else { + self.uncheck_delaunay_flat_into(flat, delaunay); + } + } + #[inline] pub fn triangulate_contour(&mut self, contour: &IntContour) -> IntTriangulation { match self.overlay.simplify_contour(contour, self.fill_rule) { @@ -172,6 +231,110 @@ where } } + /// Fills a Delaunay mesh from a prepared contour + #[inline] + pub fn uncheck_delaunay_contour_into( + &mut self, + contour: &IntContour, + delaunay: &mut IntDelaunay, + ) { + debug_assert!(contour.len() >= 3); + + let mut raw = RawIntTriangulation { + points: core::mem::take(&mut delaunay.points), + triangles: core::mem::take(&mut delaunay.triangles), + }; + + if self.earcut && contour.is_earcut_compatible() { + contour.earcut_net_triangulate_into(&mut raw); + } else { + self.triangulator + .contour_into_net_triangulation(contour, None, &mut raw); + } + + *delaunay = raw.into_delaunay_with_buffer(&mut self.delaunay_buffer); + } + + /// Fills a Delaunay mesh from a prepared shape + #[inline] + pub fn uncheck_delaunay_shape_into( + &mut self, + shape: &IntShape, + delaunay: &mut IntDelaunay, + ) { + debug_assert!(!shape.is_empty()); + debug_assert!(shape.iter().all(|contour| contour.len() >= 3)); + + if shape.len() == 1 { + self.uncheck_delaunay_contour_into(&shape[0], delaunay); + return; + } + + let mut raw = RawIntTriangulation { + points: core::mem::take(&mut delaunay.points), + triangles: core::mem::take(&mut delaunay.triangles), + }; + self.triangulator + .shape_into_net_triangulation(shape, None, &mut raw); + *delaunay = raw.into_delaunay_with_buffer(&mut self.delaunay_buffer); + } + + /// Fills a Delaunay mesh from prepared shapes + #[inline] + pub fn uncheck_delaunay_shapes_into( + &mut self, + shapes: &IntShapes, + delaunay: &mut IntDelaunay, + ) { + debug_assert!(!shapes.is_empty()); + + self.uncheck_delaunay_shape_into(&shapes[0], delaunay); + if shapes.len() == 1 { + return; + } + + let mut raw = self.raw_buffer.take().unwrap_or_default(); + for shape in &shapes[1..] { + let mut buffer = IntDelaunay { + points: raw.points, + triangles: raw.triangles, + }; + self.uncheck_delaunay_shape_into(shape, &mut buffer); + raw = buffer.into_raw(); + raw.shift(delaunay.points.len(), delaunay.triangles.len()); + delaunay.points.append(&mut raw.points); + delaunay.triangles.append(&mut raw.triangles); + } + self.raw_buffer = Some(raw); + } + + /// Fills a Delaunay mesh from prepared flat contours + #[inline] + pub fn uncheck_delaunay_flat_into( + &mut self, + flat: &FlatContoursBuffer, + delaunay: &mut IntDelaunay, + ) { + debug_assert!(!flat.is_empty()); + debug_assert!(!flat.ranges.is_empty()); + debug_assert!(flat.ranges.iter().all(|range| { + range.start <= range.end && range.end <= flat.points.len() && range.len() >= 3 + })); + + let mut raw = RawIntTriangulation { + points: core::mem::take(&mut delaunay.points), + triangles: core::mem::take(&mut delaunay.triangles), + }; + if self.earcut && flat.is_earcut_compatible() { + flat.as_first_contour() + .earcut_net_triangulate_into(&mut raw); + } else { + self.triangulator + .flat_into_net_triangulation(flat, &mut raw); + } + *delaunay = raw.into_delaunay_with_buffer(&mut self.delaunay_buffer); + } + #[inline] pub fn uncheck_triangulate_shape(&mut self, shape: &IntShape) -> IntTriangulation { let mut triangulation = Default::default(); diff --git a/iTriangle/tests/int_delaunay_into_tests.rs b/iTriangle/tests/int_delaunay_into_tests.rs new file mode 100644 index 0000000..b139def --- /dev/null +++ b/iTriangle/tests/int_delaunay_into_tests.rs @@ -0,0 +1,379 @@ +use i_triangle::advanced::delaunay::IntDelaunay; +use i_triangle::i_overlay::core::fill_rule::FillRule; +use i_triangle::i_overlay::core::integer::OverlayInt; +use i_triangle::i_overlay::i_float::int::number::wide_int::WideIntNumber; +use i_triangle::i_overlay::i_float::int::point::IntPoint; +use i_triangle::i_overlay::i_float::triangle::Triangle; +use i_triangle::i_overlay::i_shape::flat::buffer::FlatContoursBuffer; +use i_triangle::i_overlay::i_shape::int::shape::{IntContour, IntShape}; +use i_triangle::int::triangulation::IntTriangulation; +use i_triangle::int::triangulator::IntTriangulator; +use i_triangle::int::unchecked::IntUncheckedTriangulatable; + +fn contour>(points: &[(i16, i16)]) -> IntContour { + points + .iter() + .map(|&(x, y)| IntPoint::new(I::from(x), I::from(y))) + .collect() +} + +fn empty_mesh() -> IntDelaunay { + IntDelaunay { + points: Vec::new(), + triangles: Vec::new(), + } +} + +fn validate + core::fmt::Debug>( + mesh: &IntDelaunay, + expected_area_two: i16, +) { + let mut area = I::Wide::ZERO; + for (index, triangle) in mesh.triangles.iter().enumerate() { + let v = &triangle.vertices; + let abc = Triangle::area_two(v[0].point, v[1].point, v[2].point); + assert!(abc > I::Wide::ZERO); + area = area + abc; + for vertex in v { + assert!(vertex.index < mesh.points.len()); + assert_eq!(mesh.points[vertex.index], vertex.point); + } + for (side, &neighbor) in triangle.neighbors.iter().enumerate() { + if neighbor == usize::MAX { + continue; + } + assert_ne!(neighbor, index); + let other = &mesh.triangles[neighbor]; + let other_side = other.neighbors.iter().position(|&n| n == index).unwrap(); + assert_eq!( + v[(side + 1) % 3].index, + other.vertices[(other_side + 2) % 3].index + ); + assert_eq!( + v[(side + 2) % 3].index, + other.vertices[(other_side + 1) % 3].index + ); + } + } + assert!(area == I::from(expected_area_two).to_wide()); +} + +fn shape_with_hole() -> IntShape { + vec![ + contour(&[(0, 0), (20, 0), (20, 20), (0, 20)]), + contour(&[(5, 5), (5, 15), (15, 15), (15, 5)]), + ] +} + +#[test] +fn contour_reuses_buffers_across_sizes_algorithms_and_integer_types() { + fn check + core::fmt::Debug>() { + let large = contour(&[ + (0, 0), + (5, 0), + (10, 0), + (10, 5), + (10, 10), + (5, 10), + (0, 10), + (0, 5), + ]); + let small = contour(&[(0, 0), (10, 0), (0, 10)]); + let mut triangulator = IntTriangulator::::default(); + let mut mesh = empty_mesh(); + triangulator.uncheck_delaunay_contour_into(&large, &mut mesh); + let capacities = (mesh.points.capacity(), mesh.triangles.capacity()); + let pointers = (mesh.points.as_ptr(), mesh.triangles.as_ptr()); + for earcut in [true, false, true] { + triangulator.earcut = earcut; + for (input, area) in [(&small, 100), (&large, 200)] { + triangulator.uncheck_delaunay_contour_into(input, &mut mesh); + validate(&mesh, area); + assert_eq!(mesh.points.len(), input.len()); + assert_eq!( + capacities, + (mesh.points.capacity(), mesh.triangles.capacity()) + ); + assert_eq!(pointers, (mesh.points.as_ptr(), mesh.triangles.as_ptr())); + if earcut { + assert_eq!(&mesh.points, input); + } + } + } + } + check::(); + check::(); + check::(); +} + +#[test] +fn holes_and_disjoint_shapes_keep_boundary_edges_and_neighbors() { + let shape = shape_with_hole(); + let second = vec![contour(&[(30, 0), (40, 0), (40, 10), (30, 10)])]; + let shapes = vec![shape.clone(), second]; + let mut flat = FlatContoursBuffer::default(); + flat.set_with_shapes(&shapes); + let mut mesh = empty_mesh(); + let mut triangulator = IntTriangulator::::default(); + let mut render = IntTriangulation::::default(); + for earcut in [false, true] { + triangulator.earcut = earcut; + triangulator.uncheck_delaunay_shape_into(&shape, &mut mesh); + validate(&mesh, 600); + for use_flat in [false, true] { + if use_flat { + triangulator.uncheck_delaunay_flat_into(&flat, &mut mesh); + } else { + triangulator.uncheck_delaunay_shapes_into(&shapes, &mut mesh); + } + validate(&mesh, 800); + let mut boundary = Vec::new(); + for triangle in &mesh.triangles { + for (side, &neighbor) in triangle.neighbors.iter().enumerate() { + if neighbor == usize::MAX { + boundary.push(( + triangle.vertices[(side + 1) % 3].point, + triangle.vertices[(side + 2) % 3].point, + )); + } + } + } + assert_eq!(boundary.len(), flat.points.len()); + for range in &flat.ranges { + let points = &flat.points[range.clone()]; + for i in 0..points.len() { + assert!(boundary.contains(&(points[i], points[(i + 1) % points.len()]))); + } + } + render.fill_with_delaunay(&mesh); + assert_eq!(render.indices.len(), 3 * mesh.triangles.len()); + assert_eq!(render.points, mesh.points); + let convex_area: i64 = mesh + .to_convex_polygons() + .iter() + .map(|polygon| { + (1..polygon.len() - 1) + .map(|i| Triangle::area_two(polygon[0], polygon[i], polygon[i + 1])) + .sum::() + }) + .sum(); + assert_eq!(convex_area, 800); + } + } +} + +#[test] +fn output_buffers_survive_switching_between_all_input_forms() { + let shape = shape_with_hole(); + let small = contour(&[(30, 0), (40, 0), (30, 10)]); + let shapes = vec![shape.clone(), vec![small.clone()]]; + let mut flat = FlatContoursBuffer::default(); + flat.set_with_shapes(&shapes); + let mut triangulator = IntTriangulator::::default(); + let mut mesh = empty_mesh(); + triangulator.uncheck_delaunay_shapes_into(&shapes, &mut mesh); + let capacities = (mesh.points.capacity(), mesh.triangles.capacity()); + let pointers = (mesh.points.as_ptr(), mesh.triangles.as_ptr()); + for earcut in [true, false] { + triangulator.earcut = earcut; + for method in 0..5 { + let area = match method { + 0 => { + triangulator.uncheck_delaunay_shape_into(&shape, &mut mesh); + 600 + } + 1 => { + triangulator.uncheck_delaunay_contour_into(&small, &mut mesh); + 100 + } + 2 => { + triangulator.delaunay_shapes_into(&vec![], &mut mesh); + 0 + } + 3 => { + triangulator.uncheck_delaunay_flat_into(&flat, &mut mesh); + 700 + } + _ => { + triangulator.uncheck_delaunay_shapes_into(&shapes, &mut mesh); + 700 + } + }; + validate(&mesh, area); + assert_eq!( + capacities, + (mesh.points.capacity(), mesh.triangles.capacity()) + ); + assert_eq!(pointers, (mesh.points.as_ptr(), mesh.triangles.as_ptr())); + } + } +} + +#[test] +fn checked_methods_simplify_self_intersections_and_reversed_winding() { + let bowtie = contour(&[(0, 0), (10, 10), (0, 10), (10, 0)]); + let reversed = contour(&[(0, 0), (0, 10), (10, 10), (10, 0)]); + let mut triangulator = IntTriangulator::::default(); + let mut mesh = empty_mesh(); + for earcut in [false, true] { + triangulator.earcut = earcut; + for (input, area) in [(&bowtie, 100), (&reversed, 200)] { + triangulator.delaunay_contour_into(input, &mut mesh); + validate(&mesh, area); + let shape = vec![input.clone()]; + triangulator.delaunay_shape_into(&shape, &mut mesh); + validate(&mesh, area); + triangulator.delaunay_shapes_into(&vec![shape.clone()], &mut mesh); + validate(&mesh, area); + let mut flat = FlatContoursBuffer::default(); + flat.set_with_shape(&shape); + triangulator.delaunay_flat_into(&mut flat, &mut mesh); + validate(&mesh, area); + } + } +} + +#[test] +fn checked_shapes_and_flat_use_the_configured_fill_rule_together() { + let shapes = vec![ + vec![contour(&[(0, 0), (10, 0), (10, 10), (0, 10)])], + vec![contour(&[(5, 0), (15, 0), (15, 10), (5, 10)])], + ]; + let mut triangulator = IntTriangulator::::default(); + let mut mesh = empty_mesh(); + for (fill_rule, area) in [(FillRule::NonZero, 300), (FillRule::EvenOdd, 200)] { + triangulator.fill_rule = fill_rule; + triangulator.delaunay_shapes_into(&shapes, &mut mesh); + validate(&mesh, area); + let mut flat = FlatContoursBuffer::default(); + flat.set_with_shapes(&shapes); + triangulator.delaunay_flat_into(&mut flat, &mut mesh); + validate(&mesh, area); + } +} + +#[test] +fn checked_entry_points_clear_empty_output_without_losing_capacity() { + let valid = contour(&[(0, 0), (10, 0), (0, 10)]); + let mut triangulator = IntTriangulator::::default(); + let mut mesh = empty_mesh(); + for earcut in [false, true] { + triangulator.earcut = earcut; + for method in 0..4 { + for nested in [false, true] { + triangulator.uncheck_delaunay_contour_into(&valid, &mut mesh); + let capacities = (mesh.points.capacity(), mesh.triangles.capacity()); + let shape = if nested { vec![vec![], vec![]] } else { vec![] }; + let shapes = if nested { + vec![shape.clone(), vec![]] + } else { + vec![] + }; + let mut flat = FlatContoursBuffer::default(); + flat.set_with_shape(&shape); + match method { + 0 => triangulator.delaunay_contour_into(&vec![], &mut mesh), + 1 => triangulator.delaunay_shape_into(&shape, &mut mesh), + 2 => triangulator.delaunay_shapes_into(&shapes, &mut mesh), + _ => triangulator.delaunay_flat_into(&mut flat, &mut mesh), + } + assert!(mesh.points.is_empty() && mesh.triangles.is_empty()); + assert_eq!( + capacities, + (mesh.points.capacity(), mesh.triangles.capacity()) + ); + triangulator.uncheck_delaunay_contour_into(&valid, &mut mesh); + validate(&mesh, 100); + } + } + for len in 1..3 { + let short = valid[..len].to_vec(); + let mut flat = FlatContoursBuffer::default(); + flat.set_with_contour(&short); + for method in 0..3 { + triangulator.uncheck_delaunay_contour_into(&valid, &mut mesh); + match method { + 0 => triangulator.delaunay_contour_into(&short, &mut mesh), + 1 => triangulator.delaunay_shape_into(&vec![short.clone()], &mut mesh), + _ => triangulator.delaunay_flat_into(&mut flat, &mut mesh), + } + assert!(mesh.points.is_empty() && mesh.triangles.is_empty()); + } + } + } +} + +#[test] +fn checked_entry_points_handle_nonempty_input_that_simplifies_to_empty() { + let valid = contour(&[(0, 0), (10, 0), (0, 10)]); + let doubled = valid.iter().chain(&valid).copied().collect(); + let shape = vec![valid.clone(), valid.clone()]; + let shapes = vec![vec![valid.clone()], vec![valid.clone()]]; + let mut triangulator = IntTriangulator::::default(); + triangulator.fill_rule = FillRule::EvenOdd; + let mut mesh = empty_mesh(); + for method in 0..4 { + triangulator.uncheck_delaunay_contour_into(&valid, &mut mesh); + let capacities = (mesh.points.capacity(), mesh.triangles.capacity()); + let mut flat = FlatContoursBuffer::default(); + flat.set_with_shapes(&shapes); + match method { + 0 => triangulator.delaunay_contour_into(&doubled, &mut mesh), + 1 => triangulator.delaunay_shape_into(&shape, &mut mesh), + 2 => triangulator.delaunay_shapes_into(&shapes, &mut mesh), + _ => triangulator.delaunay_flat_into(&mut flat, &mut mesh), + } + assert!(mesh.points.is_empty() && mesh.triangles.is_empty()); + assert_eq!( + capacities, + (mesh.points.capacity(), mesh.triangles.capacity()) + ); + } +} + +#[test] +fn earcut_dispatches_at_the_64_point_boundary() { + let mut input = contour::(&[(0, 0)]); + input.extend((1..=32).map(|x| IntPoint::new(x, 0))); + input.extend((1..=32).map(|y| IntPoint::new(32, y))); + input.push(IntPoint::new(0, 32)); + assert_eq!(input.len(), 66); + let mut triangulator = IntTriangulator::::default(); + triangulator.earcut = true; + let mut mesh = empty_mesh(); + let mut flat = FlatContoursBuffer::default(); + for len in [66, 65, 64] { + assert_eq!(input.len(), len); + triangulator.uncheck_delaunay_contour_into(&input, &mut mesh); + validate(&mesh, 2048); + if len == 64 { + assert_eq!(mesh.points, input); + } + flat.set_with_contour(&input); + triangulator.uncheck_delaunay_flat_into(&flat, &mut mesh); + validate(&mesh, 2048); + if len == 64 { + assert_eq!(mesh.points, input); + } + input.remove(1); + } +} + +#[test] +fn refinement_is_applied_even_when_the_flat_output_flag_is_false() { + let input = contour::(&[(0, 0), (10, 0), (10, 6), (0, 10)]); + let raw = input.uncheck_triangulate(); + let before = raw.triangle_indices::(); + let expected = raw.into_delaunay(); + assert_ne!(before, expected.triangle_indices::()); + let mut triangulator = IntTriangulator::::default(); + assert!(!triangulator.delaunay); + let mut mesh = empty_mesh(); + triangulator.uncheck_delaunay_contour_into(&input, &mut mesh); + assert_eq!(mesh.points, expected.points); + assert_eq!( + mesh.triangle_indices::(), + expected.triangle_indices::() + ); + assert_eq!(mesh.triangle_neighbors(), expected.triangle_neighbors()); +} From 54f9a122d17a8d557bdd65246bb18ceab8011821 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 11 Oct 2026 13:15:22 +0300 Subject: [PATCH 2/2] float wrapper for new api --- iTriangle/src/float/delaunay.rs | 16 +- iTriangle/src/float/mod.rs | 1 - iTriangle/src/float/triangulation.rs | 22 +- iTriangle/src/float/triangulator.rs | 341 +++++++++++++++++++++++++++ 4 files changed, 377 insertions(+), 3 deletions(-) diff --git a/iTriangle/src/float/delaunay.rs b/iTriangle/src/float/delaunay.rs index 6a31a62..b2693ae 100644 --- a/iTriangle/src/float/delaunay.rs +++ b/iTriangle/src/float/delaunay.rs @@ -5,18 +5,32 @@ use crate::int::triangulation::IndexType; use alloc::vec::Vec; use i_overlay::i_float::adapter::FloatPointAdapter; use i_overlay::i_float::float::compatible::FloatPointCompatible; +use i_overlay::i_float::float::rect::FloatRect; use i_overlay::i_float::int::number::int::IntNumber; use i_overlay::i_shape::float::adapter::PathToFloat; /// A Delaunay-refined triangle mesh with float-mapped geometry. /// -/// Produced from [`Triangulation::into_delaunay`] by applying edge flips +/// Produced from [`RawTriangulation::into_delaunay`] by applying edge flips /// to satisfy the Delaunay condition. pub struct Delaunay { pub(super) delaunay: IntDelaunay, pub(super) adapter: FloatPointAdapter, } +impl Default for Delaunay { + #[inline] + fn default() -> Self { + Self { + delaunay: IntDelaunay { + points: Vec::new(), + triangles: Vec::new(), + }, + adapter: FloatPointAdapter::new_conservative(FloatRect::zero()), + } + } +} + impl RawTriangulation { #[inline] pub fn into_delaunay(self) -> Delaunay { diff --git a/iTriangle/src/float/mod.rs b/iTriangle/src/float/mod.rs index 933fa04..51e07f0 100644 --- a/iTriangle/src/float/mod.rs +++ b/iTriangle/src/float/mod.rs @@ -2,7 +2,6 @@ //! //! Coordinates must be finite, with absolute values at most `2^60` for `f32` //! or `2^500` for `f64`. Invalid input bounds panic; empty geometry produces -//! an empty mesh. Automatic conversion uses the conservative adapter constructors, //! reserving `I::BITS - 3` coordinate bits for integer differences, products, and //! rounding, including unchecked APIs. //! Unchecked APIs skip topology validation. Reusable `Triangulator` methods diff --git a/iTriangle/src/float/triangulation.rs b/iTriangle/src/float/triangulation.rs index 45e8cd1..4a6c5c6 100644 --- a/iTriangle/src/float/triangulation.rs +++ b/iTriangle/src/float/triangulation.rs @@ -1,4 +1,5 @@ -use crate::int::triangulation::{IndexType, IntTriangulation, RawIntTriangulation}; +use crate::float::delaunay::Delaunay; +use crate::int::triangulation::{IndexType, IndicesBuilder, IntTriangulation, RawIntTriangulation}; use alloc::vec::Vec; use i_overlay::i_float::adapter::FloatPointAdapter; use i_overlay::i_float::float::compatible::FloatPointCompatible; @@ -54,6 +55,25 @@ impl RawTriangulation { } impl Triangulation { + /// Fills the triangulation from a Delaunay mesh + #[inline] + pub fn set_with_delaunay(&mut self, delaunay: &Delaunay) + where + P: FloatPointCompatible, + { + let mesh = &delaunay.delaunay; + mesh.triangles + .feed_indices(mesh.points.len(), &mut self.indices); + + self.points.clear(); + self.points.reserve(mesh.points.len()); + self.points.extend( + mesh.points + .iter() + .map(|point| delaunay.adapter.int_to_float(point)), + ); + } + #[inline] pub fn with_capacity(capacity: usize) -> Self { Self { diff --git a/iTriangle/src/float/triangulator.rs b/iTriangle/src/float/triangulator.rs index 7b9a571..3f7726e 100644 --- a/iTriangle/src/float/triangulator.rs +++ b/iTriangle/src/float/triangulator.rs @@ -1,3 +1,4 @@ +use crate::float::delaunay::Delaunay; use crate::float::triangulation::Triangulation; use crate::int::triangulation::{IndexType, IntTriangulation}; use crate::int::triangulator::IntTriangulator; @@ -85,6 +86,41 @@ where I: OverlayInt, N: IndexType, { + /// Simplifies a resource and fills a Delaunay mesh + #[inline] + pub fn delaunay_into(&mut self, resource: &R, output: &mut Delaunay) + where + R: ShapeResource

+ ?Sized, + P: FloatPointCompatible, + { + let flat_buffer = self.flat_buffer.get_or_insert_with(Default::default); + let adapter = flat_buffer + .set_with_resource(resource) + .expect("Invalid triangulation bounds"); + + self.int_triangulator + .delaunay_flat_into(flat_buffer, &mut output.delaunay); + output.adapter = adapter; + } + + /// Fills a Delaunay mesh from a prepared resource + #[inline] + pub fn uncheck_delaunay_into(&mut self, resource: &R, output: &mut Delaunay) + where + R: ShapeResource

+ ?Sized, + P: FloatPointCompatible, + { + let flat_buffer = self.flat_buffer.get_or_insert_with(Default::default); + let adapter = flat_buffer + .set_with_resource(resource) + .expect("Invalid triangulation bounds"); + debug_assert!(!flat_buffer.is_empty()); + + self.int_triangulator + .uncheck_delaunay_flat_into(flat_buffer, &mut output.delaunay); + output.adapter = adapter; + } + /// Performs triangulation on the provided shape resource and returns a new `Triangulation`. /// /// - `resource`: A `ShapeResource` that define contours. @@ -233,3 +269,308 @@ where self.int_buffer = Some(int_buffer); } } + +#[cfg(test)] +mod tests { + use super::Triangulator; + use crate::float::delaunay::Delaunay; + use crate::float::triangulation::Triangulation; + use alloc::{vec, vec::Vec}; + use i_overlay::core::integer::OverlayInt; + use i_overlay::i_shape::flat::float::{FloatFlatContoursBuffer, FloatFlatShapesBuffer}; + use std::panic::{catch_unwind, AssertUnwindSafe}; + + #[test] + fn delaunay_reuses_buffers_and_updates_the_adapter() { + fn check() { + let large = [ + [0., 0.], + [2., 0.], + [4., 0.], + [4., 2.], + [4., 4.], + [2., 4.], + [0., 4.], + [0., 2.], + ]; + let shifted = [[100., 200.], [108., 200.], [100., 204.]]; + let mut triangulator = Triangulator::::default(); + let mut mesh = Delaunay::<[f64; 2], I>::default(); + assert!(mesh.points().is_empty()); + assert!(mesh.triangle_indices::().is_empty()); + let mut render = Triangulation::<_, u32>::with_capacity(0); + triangulator.uncheck_delaunay_into(&large, &mut mesh); + render.set_with_delaunay(&mesh); + let pointers = ( + mesh.delaunay.points.as_ptr(), + mesh.delaunay.triangles.as_ptr(), + ); + let capacities = ( + mesh.delaunay.points.capacity(), + mesh.delaunay.triangles.capacity(), + ); + let render_pointers = (render.points.as_ptr(), render.indices.as_ptr()); + let flat = triangulator.flat_buffer.as_ref().unwrap(); + let flat_pointers = (flat.points.as_ptr(), flat.ranges.as_ptr()); + for earcut in [false, true] { + triangulator.earcut(earcut); + for checked in [false, true] { + for input in [shifted.as_slice(), large.as_slice()] { + if checked { + triangulator.delaunay_into(input, &mut mesh); + } else { + triangulator.uncheck_delaunay_into(input, &mut mesh); + } + render.set_with_delaunay(&mesh); + render.validate(16., 0.000_001); + for point in &render.points { + assert!(input.contains(point)); + } + assert_eq!( + capacities, + ( + mesh.delaunay.points.capacity(), + mesh.delaunay.triangles.capacity() + ) + ); + assert_eq!( + pointers, + ( + mesh.delaunay.points.as_ptr(), + mesh.delaunay.triangles.as_ptr() + ) + ); + assert_eq!( + render_pointers, + (render.points.as_ptr(), render.indices.as_ptr()) + ); + let flat = triangulator.flat_buffer.as_ref().unwrap(); + assert_eq!(flat_pointers, (flat.points.as_ptr(), flat.ranges.as_ptr())); + } + } + } + let mut other = Delaunay::default(); + let previous = mesh.points(); + triangulator.delaunay_into(&shifted, &mut other); + assert_eq!(mesh.points(), previous); + } + check::(); + check::(); + check::(); + } + + fn validate(mesh: &Delaunay<[f64; 2]>, area: f64) { + mesh.to_triangulation::().validate(area, 0.000_001); + for (index, triangle) in mesh.delaunay.triangles.iter().enumerate() { + for (side, &neighbor) in triangle.neighbors.iter().enumerate() { + if neighbor == usize::MAX { + continue; + } + let other = &mesh.delaunay.triangles[neighbor]; + let other_side = other.neighbors.iter().position(|&n| n == index).unwrap(); + assert_eq!( + triangle.vertices[(side + 1) % 3].index, + other.vertices[(other_side + 2) % 3].index + ); + assert_eq!( + triangle.vertices[(side + 2) % 3].index, + other.vertices[(other_side + 1) % 3].index + ); + } + } + let convex_area: f64 = mesh + .to_convex_polygons() + .iter() + .map(|polygon| { + polygon + .iter() + .zip(polygon.iter().cycle().skip(1)) + .map(|(a, b)| a[0] * b[1] - a[1] * b[0]) + .sum::() + * 0.5 + }) + .sum(); + assert!((convex_area - area).abs() < 0.000_001); + } + + #[test] + fn delaunay_accepts_shapes_holes_and_float_flat_resources() { + let shape = vec![ + vec![[0., 0.], [20., 0.], [20., 20.], [0., 20.]], + vec![[5., 5.], [5., 15.], [15., 15.], [15., 5.]], + ]; + let shapes = vec![ + shape.clone(), + vec![vec![[30., 0.], [40., 0.], [40., 10.], [30., 10.]]], + ]; + let mut flat = FloatFlatContoursBuffer::default(); + flat.set_with_resource(&shapes); + let mut flat_shapes = FloatFlatShapesBuffer::default(); + flat_shapes.set_with_resource(&shapes); + let mut triangulator = Triangulator::::default(); + let mut mesh = Delaunay::default(); + for earcut in [false, true] { + triangulator.earcut(earcut); + triangulator.delaunay_into(&shape, &mut mesh); + validate(&mesh, 300.); + triangulator.uncheck_delaunay_into(&shape, &mut mesh); + validate(&mesh, 300.); + triangulator.delaunay_into(&shapes, &mut mesh); + validate(&mesh, 400.); + triangulator.uncheck_delaunay_into(&shapes, &mut mesh); + validate(&mesh, 400.); + triangulator.delaunay_into(&flat, &mut mesh); + validate(&mesh, 400.); + triangulator.uncheck_delaunay_into(&flat, &mut mesh); + validate(&mesh, 400.); + triangulator.delaunay_into(&flat_shapes, &mut mesh); + validate(&mesh, 400.); + triangulator.uncheck_delaunay_into(&flat_shapes, &mut mesh); + validate(&mesh, 400.); + } + } + + #[test] + fn checked_delaunay_simplifies_and_clears_empty_results() { + use i_overlay::core::fill_rule::FillRule; + let bowtie = [[0., 0.], [10., 10.], [0., 10.], [10., 0.]]; + let mut triangulator = Triangulator::::default(); + let mut mesh = Delaunay::default(); + let mut render = Triangulation::<_, u32>::with_capacity(0); + triangulator.delaunay_into(&bowtie, &mut mesh); + validate(&mesh, 50.); + render.set_with_delaunay(&mesh); + let capacities = ( + mesh.delaunay.points.capacity(), + mesh.delaunay.triangles.capacity(), + ); + let render_capacities = (render.points.capacity(), render.indices.capacity()); + let empty: Vec>> = vec![vec![vec![]], vec![]]; + triangulator.delaunay_into(&empty, &mut mesh); + assert!(mesh.delaunay.points.is_empty() && mesh.delaunay.triangles.is_empty()); + assert_eq!( + capacities, + ( + mesh.delaunay.points.capacity(), + mesh.delaunay.triangles.capacity() + ) + ); + render.set_with_delaunay(&mesh); + assert!(render.points.is_empty() && render.indices.is_empty()); + assert_eq!( + render_capacities, + (render.points.capacity(), render.indices.capacity()) + ); + + triangulator.delaunay_into(&bowtie, &mut mesh); + triangulator.int_triangulator.fill_rule = FillRule::EvenOdd; + triangulator.delaunay_into(&[bowtie.as_slice(), bowtie.as_slice()], &mut mesh); + assert!(mesh.delaunay.points.is_empty() && mesh.delaunay.triangles.is_empty()); + } + + #[test] + fn delaunay_f32_and_export_indices_are_independent_of_triangulator_indices() { + let input = [[0_f32, 0.], [4., 0.], [4., 4.], [0., 4.]]; + let mut triangulator = Triangulator::::default(); + let mut mesh = Delaunay::default(); + let mut render = Triangulation::<_, u32>::with_capacity(0); + for earcut in [false, true] { + triangulator.earcut(earcut); + triangulator.uncheck_delaunay_into(&input, &mut mesh); + render.set_with_delaunay(&mesh); + render.validate(16., 0.000_001); + triangulator.delaunay_into(&input, &mut mesh); + render.set_with_delaunay(&mesh); + render.validate(16., 0.000_001); + } + } + + #[test] + fn delaunay_refines_without_enabling_the_flat_output_flag() { + use crate::float::unchecked::UncheckedTriangulatable; + let input = [[0., 0.], [10., 0.], [10., 6.], [0., 10.]]; + let raw = input.unchecked_triangulate(); + let before = raw.triangle_indices::(); + let expected = raw.into_delaunay(); + assert_ne!(before, expected.triangle_indices::()); + let mut triangulator = Triangulator::::default(); + let mut mesh = Delaunay::default(); + assert!(!triangulator.is_delaunay()); + triangulator.uncheck_delaunay_into(&input, &mut mesh); + assert_eq!( + mesh.triangle_indices::(), + expected.triangle_indices::() + ); + assert_eq!(mesh.triangle_neighbors(), expected.triangle_neighbors()); + validate(&mesh, 80.); + } + + #[test] + fn delaunay_export_rejects_indices_that_cannot_represent_the_mesh() { + let input: Vec<_> = (0..=128) + .map(|x| [x as f64, 0.]) + .chain((0..=128).rev().map(|x| [x as f64, 128.])) + .collect(); + let mut triangulator = Triangulator::::default(); + let mut mesh = Delaunay::default(); + triangulator.uncheck_delaunay_into(&input, &mut mesh); + assert_eq!(mesh.delaunay.points.len(), 258); + let mut narrow = Triangulation::<_, u8>::with_capacity(0); + assert!(catch_unwind(AssertUnwindSafe(|| { + narrow.set_with_delaunay(&mesh); + })) + .is_err()); + let mut render = Triangulation::<_, u32>::with_capacity(0); + render.set_with_delaunay(&mesh); + render.validate(16384., 0.000_001); + } + + #[test] + fn invalid_float_bounds_preserve_the_previous_delaunay() { + let valid = [[0., 0.], [4., 0.], [0., 4.]]; + let invalid = [[f64::INFINITY, 0.], [4., 0.], [0., 4.]]; + let mut triangulator = Triangulator::::default(); + let mut mesh = Delaunay::default(); + triangulator.delaunay_into(&valid, &mut mesh); + let points = mesh.points(); + let indices = mesh.triangle_indices::(); + for checked in [false, true] { + assert!(catch_unwind(AssertUnwindSafe(|| { + if checked { + triangulator.delaunay_into(&invalid, &mut mesh); + } else { + triangulator.uncheck_delaunay_into(&invalid, &mut mesh); + } + })) + .is_err()); + assert_eq!(mesh.points(), points); + assert_eq!(mesh.triangle_indices::(), indices); + triangulator.uncheck_delaunay_into(&valid, &mut mesh); + validate(&mesh, 8.); + } + } + + #[cfg(debug_assertions)] + #[test] + fn unchecked_delaunay_asserts_nonempty_contours_before_replacing_the_mesh() { + let valid = vec![[0., 0.], [4., 0.], [0., 4.]]; + let mut triangulator = Triangulator::::default(); + let mut mesh = Delaunay::default(); + triangulator.uncheck_delaunay_into(&valid, &mut mesh); + let points = mesh.points(); + let indices = mesh.triangle_indices::(); + for input in [ + vec![], + vec![vec![]], + vec![valid[..2].to_vec()], + vec![valid, vec![]], + ] { + assert!(catch_unwind(AssertUnwindSafe(|| { + triangulator.uncheck_delaunay_into(&input, &mut mesh); + })) + .is_err()); + assert_eq!(mesh.points(), points); + assert_eq!(mesh.triangle_indices::(), indices); + } + } +}