From ad485a3ed2c8edd49f2604c464b7d6f0b12c82ff Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 25 Jun 2025 15:58:25 +0200 Subject: [PATCH 1/2] failible prototype for try_bulk_load skipping conflicting edges This serves as discussion starters, I imagine this comes at a performance price and will need 2 different functions. --- src/cdt.rs | 53 +++++++++++++++++++++++++++++----- src/delaunay_core/bulk_load.rs | 44 +++++++++++++++++++++++----- src/delaunay_core/mod.rs | 2 +- 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/cdt.rs b/src/cdt.rs index 0da2092..19b4777 100644 --- a/src/cdt.rs +++ b/src/cdt.rs @@ -7,16 +7,16 @@ use serde::{Deserialize, Serialize}; use crate::cdt::ConflictRegionEnd::{EdgeOverlap, Existing}; use crate::delaunay_core::dcel_operations::flip_cw; -use crate::delaunay_core::{bulk_load_cdt, bulk_load_stable}; +use crate::delaunay_core::{bulk_load_cdt, bulk_load_stable, try_bulk_load_cdt}; +use crate::{ + cdt, mitigate_underflow, DelaunayTriangulation, HasPosition, HintGenerator, InsertionError, + LastUsedVertexHintGenerator, Point2, Triangulation, TriangulationExt, +}; use crate::{ delaunay_core::Dcel, intersection_iterator::LineIntersectionIterator, PositionInTriangulation, SpadeNum, }; use crate::{handles::*, intersection_iterator::Intersection}; -use crate::{ - mitigate_underflow, DelaunayTriangulation, HasPosition, HintGenerator, InsertionError, - LastUsedVertexHintGenerator, Point2, Triangulation, TriangulationExt, -}; /// Undirected edge type of a [ConstrainedDelaunayTriangulation] (CDT). /// @@ -410,10 +410,27 @@ where vertices: Vec, edges: Vec<[usize; 2]>, ) -> Result { - let mut result: Self = - bulk_load_stable(move |vertices| bulk_load_cdt(vertices, edges), vertices)?; + Self::try_bulk_load_cdt_stable(vertices, edges).map(|(cdt, _)| cdt) + } + + /// See [Self::bulk_load_cdt_stable] + pub fn try_bulk_load_cdt_stable( + vertices: Vec, + edges: Vec<[usize; 2]>, + ) -> Result<(Self, Vec<[usize; 2]>), InsertionError> { + let mut conflicting_edges = Vec::new(); + let mut result: Self = bulk_load_stable( + |vertices| match try_bulk_load_cdt(vertices, edges) { + Ok((cdt, new_conflicting_edges)) => { + conflicting_edges = new_conflicting_edges; + Ok(cdt) + } + Err(e) => Err(e), + }, + vertices, + )?; *result.hint_generator_mut() = L::initialize_from_triangulation(&result); - Ok(result) + Ok((result, conflicting_edges)) } /// # Handle invalidation @@ -1995,6 +2012,26 @@ mod test { Cdt::bulk_load_cdt_stable(vertices, vec![[3, 2], [5, 4], [7, 6]]) } + #[test] + fn get_try_cdt_for_duplicate_vertices() -> Result<(), InsertionError> { + let vertices = vec![ + Point2::new(0.0, -10.0), + Point2::new(76.0, 0.0), + Point2::new(76.0, 0.0), // Duplicated vertex + Point2::new(20.0, -30.0), + Point2::new(45.0, 25.0), + Point2::new(32.0, -35.0), + Point2::new(60.0, 20.0), + Point2::new(60.0, -30.0), + Point2::new(50.0, -34.0), + ]; + let (_, conflicting_edges) = + Cdt::try_bulk_load_cdt_stable(vertices, vec![[3, 2], [5, 4], [7, 6]])?; + // Hardcoded values, may change if CDT algorithm change + assert_eq!(&conflicting_edges, &[[6, 7,], [4, 5,]]); + Ok(()) + } + #[test] fn test_single_split() -> Result<(), InsertionError> { let vertices = vec![ diff --git a/src/delaunay_core/bulk_load.rs b/src/delaunay_core/bulk_load.rs index 9d51f0e..5a39781 100644 --- a/src/delaunay_core/bulk_load.rs +++ b/src/delaunay_core/bulk_load.rs @@ -150,10 +150,38 @@ where Ok(result) } +/// Returns the [`ConstrainedDelaunayTriangulation`]. +/// +/// Conflicting edges are ignored and returned. +/// If any conflicting edge is returned, this means input is incorrect and you may want to look into fixing it. pub fn bulk_load_cdt( elements: Vec, mut edges: Vec<[usize; 2]>, ) -> Result, InsertionError> +where + V: HasPosition, + DE: Default, + UE: Default, + F: Default, + L: HintGenerator<::Scalar>, +{ + try_bulk_load_cdt(elements, edges).map(|(cdt, _)| cdt) +} + +/// Returns the [`ConstrainedDelaunayTriangulation`]. +/// +/// Conflicting edges are ignored and returned. +/// If any conflicting edge is returned, this means input is incorrect and you may want to look into fixing it. +pub fn try_bulk_load_cdt( + elements: Vec, + mut edges: Vec<[usize; 2]>, +) -> Result< + ( + ConstrainedDelaunayTriangulation, + Vec<[usize; 2]>, + ), + InsertionError, +> where V: HasPosition, DE: Default, @@ -162,11 +190,11 @@ where L: HintGenerator<::Scalar>, { if elements.is_empty() { - return Ok(ConstrainedDelaunayTriangulation::new()); + return Ok((ConstrainedDelaunayTriangulation::new(), Vec::new())); } if edges.is_empty() { - return bulk_load(elements); + return bulk_load(elements).map(|cdt| (cdt, Vec::new())); } let mut point_sum = Point2::::new(0.0, 0.0); @@ -232,7 +260,7 @@ where let mut next_constraint = edges.pop(); let mut buffer = Vec::new(); - + let mut failed_constraints = Vec::new(); let mut add_constraints_for_new_vertex = |result: &mut ConstrainedDelaunayTriangulation, index| { while let Some([from, to]) = next_constraint { @@ -241,7 +269,9 @@ where let [new_from, new_to] = [from, to].map(|v| FixedVertexHandle::new(old_to_new[v])); // Insert constraint edge - result.add_constraint(new_from, new_to); + if result.try_add_constraint(new_from, new_to).is_empty() { + failed_constraints.push([from, to]); + } next_constraint = edges.pop(); } else { break; @@ -251,7 +281,7 @@ where let mut hull = loop { let Some((old_index, next)) = elements.pop() else { - return Ok(result); + return Ok((result, failed_constraints)); }; result.insert(next)?; add_constraints_for_new_vertex(&mut result, old_index); @@ -282,7 +312,7 @@ where elements.push((old_index, skipped)); hull = loop { let Some((old_index, next)) = elements.pop() else { - return Ok(result); + return Ok((result, failed_constraints)); }; result.insert(next)?; add_constraints_for_new_vertex(&mut result, old_index); @@ -302,7 +332,7 @@ where hull_sanity_check(&result, &hull); } - Ok(result) + Ok((result, failed_constraints)) } fn try_get_hull_center(result: &T) -> Option> diff --git a/src/delaunay_core/mod.rs b/src/delaunay_core/mod.rs index 2509653..016de02 100644 --- a/src/delaunay_core/mod.rs +++ b/src/delaunay_core/mod.rs @@ -15,7 +15,7 @@ pub mod refinement; pub mod interpolation; pub mod math; -pub use bulk_load::{bulk_load, bulk_load_cdt, bulk_load_stable}; +pub use bulk_load::{bulk_load, bulk_load_cdt, bulk_load_stable, try_bulk_load_cdt}; pub use triangulation_ext::{RemovalResult, TriangulationExt}; From 8785ab8158b469ded1c27bc5ba7d28694c316a44 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Thu, 26 Jun 2025 09:44:30 +0200 Subject: [PATCH 2/2] change try_bulk_load to pass a function for more modularity Reinstate the panic behaviour fixup docs --- src/cdt.rs | 74 +++++++++++++++++++++++++--------- src/delaunay_core/bulk_load.rs | 42 +++++++++---------- 2 files changed, 77 insertions(+), 39 deletions(-) diff --git a/src/cdt.rs b/src/cdt.rs index 19b4777..05b2655 100644 --- a/src/cdt.rs +++ b/src/cdt.rs @@ -8,15 +8,15 @@ use serde::{Deserialize, Serialize}; use crate::cdt::ConflictRegionEnd::{EdgeOverlap, Existing}; use crate::delaunay_core::dcel_operations::flip_cw; use crate::delaunay_core::{bulk_load_cdt, bulk_load_stable, try_bulk_load_cdt}; -use crate::{ - cdt, mitigate_underflow, DelaunayTriangulation, HasPosition, HintGenerator, InsertionError, - LastUsedVertexHintGenerator, Point2, Triangulation, TriangulationExt, -}; use crate::{ delaunay_core::Dcel, intersection_iterator::LineIntersectionIterator, PositionInTriangulation, SpadeNum, }; use crate::{handles::*, intersection_iterator::Intersection}; +use crate::{ + mitigate_underflow, DelaunayTriangulation, HasPosition, HintGenerator, InsertionError, + LastUsedVertexHintGenerator, Point2, Triangulation, TriangulationExt, +}; /// Undirected edge type of a [ConstrainedDelaunayTriangulation] (CDT). /// @@ -345,7 +345,42 @@ where /// /// Panics if any constraint edges overlap. Panics if the edges contain an invalid index (out of range). pub fn bulk_load_cdt(vertices: Vec, edges: Vec<[usize; 2]>) -> Result { - let mut result = bulk_load_cdt(vertices, edges)?; + let mut result = bulk_load_cdt(vertices, edges).unwrap(); + *result.hint_generator_mut() = L::initialize_from_triangulation(&result); + Ok(result) + } + + /// Same behaviour as [bulk_load_cdt], but rather than panicking, + /// ignores and calls the parameter function `on_conflict_found` for each conflicting edges encountered. + /// + /// # Example + /// ``` + /// # fn main() -> Result<(), spade::InsertionError> { + /// use spade::{ConstrainedDelaunayTriangulation, Point2, Triangulation}; + /// let mut vertices = vec![ + /// Point2::new(0.0, 1.0), + /// Point2::new(1.0, 2.0), + /// Point2::new(3.0, -3.0), + /// Point2::new(-1.0, -2.0), + /// Point2::new(-4.0, -5.0), + /// ]; + /// let mut conflicting_edges = Vec::new(); + /// let mut edges = vec![[0, 1], [1, 2], [2, 3], [3, 4]]; + /// let cdt = ConstrainedDelaunayTriangulation::<_>::try_bulk_load_cdt(vertices.clone(), edges, |e| conflicting_edges.push(e))?; + /// + /// assert_eq!(cdt.num_vertices(), 5); + /// assert_eq!(cdt.num_constraints(), 4); + /// // The order will usually change + /// assert_ne!(cdt.vertices().map(|v| v.position()).collect::>(), vertices); + /// # Ok(()) + /// # } + /// ``` + pub fn try_bulk_load_cdt( + vertices: Vec, + edges: Vec<[usize; 2]>, + on_conflict_found: impl FnMut([usize; 2]), + ) -> Result { + let mut result = try_bulk_load_cdt(vertices, edges, on_conflict_found)?; *result.hint_generator_mut() = L::initialize_from_triangulation(&result); Ok(result) } @@ -406,31 +441,31 @@ where /// # Ok(()) /// # } /// ``` + /// + /// # See also + /// + /// See also [Self::try_bulk_load_cdt_stable] pub fn bulk_load_cdt_stable( vertices: Vec, edges: Vec<[usize; 2]>, ) -> Result { - Self::try_bulk_load_cdt_stable(vertices, edges).map(|(cdt, _)| cdt) + Self::try_bulk_load_cdt_stable(vertices, edges, |e| { + panic!("Conflicting edge encountered: {};{}", e[0], e[1]) + }) } /// See [Self::bulk_load_cdt_stable] pub fn try_bulk_load_cdt_stable( vertices: Vec, edges: Vec<[usize; 2]>, - ) -> Result<(Self, Vec<[usize; 2]>), InsertionError> { - let mut conflicting_edges = Vec::new(); + on_conflict_found: impl FnMut([usize; 2]), + ) -> Result { let mut result: Self = bulk_load_stable( - |vertices| match try_bulk_load_cdt(vertices, edges) { - Ok((cdt, new_conflicting_edges)) => { - conflicting_edges = new_conflicting_edges; - Ok(cdt) - } - Err(e) => Err(e), - }, + |vertices| try_bulk_load_cdt(vertices, edges, on_conflict_found), vertices, )?; *result.hint_generator_mut() = L::initialize_from_triangulation(&result); - Ok((result, conflicting_edges)) + Ok(result) } /// # Handle invalidation @@ -2025,10 +2060,13 @@ mod test { Point2::new(60.0, -30.0), Point2::new(50.0, -34.0), ]; - let (_, conflicting_edges) = - Cdt::try_bulk_load_cdt_stable(vertices, vec![[3, 2], [5, 4], [7, 6]])?; + let mut conflicting_edges = Vec::new(); + let cdt = Cdt::try_bulk_load_cdt_stable(vertices, vec![[3, 2], [5, 4], [7, 6]], |e| { + conflicting_edges.push(e) + })?; // Hardcoded values, may change if CDT algorithm change assert_eq!(&conflicting_edges, &[[6, 7,], [4, 5,]]); + assert_eq!(cdt.num_constraints, 1); Ok(()) } diff --git a/src/delaunay_core/bulk_load.rs b/src/delaunay_core/bulk_load.rs index 5a39781..1de2d41 100644 --- a/src/delaunay_core/bulk_load.rs +++ b/src/delaunay_core/bulk_load.rs @@ -152,11 +152,14 @@ where /// Returns the [`ConstrainedDelaunayTriangulation`]. /// -/// Conflicting edges are ignored and returned. -/// If any conflicting edge is returned, this means input is incorrect and you may want to look into fixing it. +/// Panics if it encounters any conflicting edges. See [try_bulk_load_cdt] for a non-panicking version. +/// +/// # See also +/// +/// See also [ConstrainedDelaunayTriangulation::bulk_load_cdt] pub fn bulk_load_cdt( elements: Vec, - mut edges: Vec<[usize; 2]>, + edges: Vec<[usize; 2]>, ) -> Result, InsertionError> where V: HasPosition, @@ -165,23 +168,21 @@ where F: Default, L: HintGenerator<::Scalar>, { - try_bulk_load_cdt(elements, edges).map(|(cdt, _)| cdt) + try_bulk_load_cdt(elements, edges, |e| { + panic!("Conflicting edge encountered: {};{}", e[0], e[1]) + }) } -/// Returns the [`ConstrainedDelaunayTriangulation`]. +/// Efficient bulk loading of a constraint delaunay triangulation, including both vertices and constraint edges. +/// See [ConstrainedDelaunayTriangulation::bulk_load_cdt] for a related example and documentation. /// -/// Conflicting edges are ignored and returned. -/// If any conflicting edge is returned, this means input is incorrect and you may want to look into fixing it. +/// This function does not panic if any two constraints intersect. +/// It will instead call `on_conflict_found` on all edges that could not be added as they intersect a previously added constraint. pub fn try_bulk_load_cdt( elements: Vec, mut edges: Vec<[usize; 2]>, -) -> Result< - ( - ConstrainedDelaunayTriangulation, - Vec<[usize; 2]>, - ), - InsertionError, -> + mut on_conflict_found: impl FnMut([usize; 2]), +) -> Result, InsertionError> where V: HasPosition, DE: Default, @@ -190,11 +191,11 @@ where L: HintGenerator<::Scalar>, { if elements.is_empty() { - return Ok((ConstrainedDelaunayTriangulation::new(), Vec::new())); + return Ok(ConstrainedDelaunayTriangulation::new()); } if edges.is_empty() { - return bulk_load(elements).map(|cdt| (cdt, Vec::new())); + return bulk_load(elements); } let mut point_sum = Point2::::new(0.0, 0.0); @@ -260,7 +261,6 @@ where let mut next_constraint = edges.pop(); let mut buffer = Vec::new(); - let mut failed_constraints = Vec::new(); let mut add_constraints_for_new_vertex = |result: &mut ConstrainedDelaunayTriangulation, index| { while let Some([from, to]) = next_constraint { @@ -270,7 +270,7 @@ where [from, to].map(|v| FixedVertexHandle::new(old_to_new[v])); // Insert constraint edge if result.try_add_constraint(new_from, new_to).is_empty() { - failed_constraints.push([from, to]); + on_conflict_found([from, to]); } next_constraint = edges.pop(); } else { @@ -281,7 +281,7 @@ where let mut hull = loop { let Some((old_index, next)) = elements.pop() else { - return Ok((result, failed_constraints)); + return Ok(result); }; result.insert(next)?; add_constraints_for_new_vertex(&mut result, old_index); @@ -312,7 +312,7 @@ where elements.push((old_index, skipped)); hull = loop { let Some((old_index, next)) = elements.pop() else { - return Ok((result, failed_constraints)); + return Ok(result); }; result.insert(next)?; add_constraints_for_new_vertex(&mut result, old_index); @@ -332,7 +332,7 @@ where hull_sanity_check(&result, &hull); } - Ok((result, failed_constraints)) + Ok(result) } fn try_get_hull_center(result: &T) -> Option>