From 4f5db2030469b1e66b186290072261b094defa90 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Tue, 16 Jun 2026 16:32:03 -0700 Subject: [PATCH 1/5] refactor(api)!: preserve Delaunay verification error sources - Replace string-only Level 4 verification failures with DelaunayVerificationError sources that distinguish flip-predicate and empty-circumsphere validation paths. - Surface compact verification source kinds through explicit validation summaries and public preludes. - Fix validation guide examples to handle coordinate conversion errors alongside construction errors. - Adopt stable assert_matches! in eligible tests and move the #329 roadmap item into v0.8.0. BREAKING CHANGE: DelaunayTriangulationValidationError::VerificationFailed now carries a Box source instead of a message String. Closes #329 --- docs/roadmap.md | 4 +- docs/validation.md | 44 ++++- src/core/algorithms/incremental_insertion.rs | 16 +- src/core/collections/spatial_hash_grid.rs | 21 +-- src/core/tds.rs | 25 +-- src/core/util/canonical_points.rs | 17 +- src/core/validation.rs | 19 +- src/delaunay/builder.rs | 73 ++++++-- src/delaunay/construction.rs | 29 +-- src/delaunay/validation.rs | 179 +++++++++++++++++-- src/geometry/point.rs | 5 +- src/lib.rs | 26 ++- tests/prelude_exports.rs | 30 +++- 13 files changed, 394 insertions(+), 94 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 3b63fe80..33ba5724 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -51,6 +51,8 @@ work intentionally deferred from v0.7.8 cleanup: `SphericalSpace::canonicalize_point()`. - **Iterator cleanup (#353):** prefer iterator-based collection-building paths where that improves clarity and allocation behavior. +- **Rust test cleanup (#329):** adopt stable `assert_matches!` in tests now + that the MSRV supports it. ### v0.9.0 and later horizon @@ -61,8 +63,6 @@ tightly coupled to the v0.8.0 paper/API push: triangulations, Voronoi diagrams, and weakly-visible hull facets. - **Visualization and high-dimensional tuning (#64/#106):** built-in visualization and convex-hull buffer allocation work for D > 7. -- **Future Rust cleanup (#329):** adopt `assert_matches!` in tests once it is - stable. ## Ongoing Performance Monitoring diff --git a/docs/validation.md b/docs/validation.md index b3b9fe2e..fcfd0721 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -165,9 +165,18 @@ use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyGuarantee, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::validation::ValidationPolicy; -fn main() -> Result<(), DelaunayTriangulationConstructionError> { +#[derive(Debug, thiserror::Error)] +enum ValidationExampleError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), +} + +fn main() -> Result<(), ValidationExampleError> { let vertices = vec![ delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, @@ -317,9 +326,18 @@ use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyGuarantee, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::validation::ValidationPolicy; -fn main() -> Result<(), DelaunayTriangulationConstructionError> { +#[derive(Debug, thiserror::Error)] +enum ValidationExampleError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), +} + +fn main() -> Result<(), ValidationExampleError> { let vertices = vec![ delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, @@ -402,9 +420,18 @@ use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyGuarantee, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::validation::ValidationPolicy; -fn main() -> Result<(), DelaunayTriangulationConstructionError> { +#[derive(Debug, thiserror::Error)] +enum ValidationExampleError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), +} + +fn main() -> Result<(), ValidationExampleError> { let vertices = vec![ delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, @@ -474,9 +501,18 @@ use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyGuarantee, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::validation::ValidationPolicy; -fn main() -> Result<(), DelaunayTriangulationConstructionError> { +#[derive(Debug, thiserror::Error)] +enum ValidationExampleError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), +} + +fn main() -> Result<(), ValidationExampleError> { let vertices = vec![ delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, diff --git a/src/core/algorithms/incremental_insertion.rs b/src/core/algorithms/incremental_insertion.rs index 40269b05..fea566bc 100644 --- a/src/core/algorithms/incremental_insertion.rs +++ b/src/core/algorithms/incremental_insertion.rs @@ -4565,6 +4565,7 @@ mod tests { InvalidCoordinateValue, }; use crate::topology::characteristics::euler::TopologyClassification; + use crate::validation::DelaunayVerificationError; use slotmap::KeyData; use std::assert_matches; @@ -5383,6 +5384,17 @@ mod tests { } } + fn synthetic_delaunay_verification_error( + message: &str, + ) -> DelaunayTriangulationValidationError { + DelaunayTriangulationValidationError::VerificationFailed { + source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { + message: message.to_string(), + }) + .into(), + } + } + #[test] fn test_delaunay_repair_error_summary_covers_all_kinds() { let cases = [ @@ -5463,9 +5475,7 @@ mod tests { ), ( InsertionError::DelaunayValidationFailed { - source: DelaunayTriangulationValidationError::VerificationFailed { - message: "non-Delaunay facet".to_string(), - }, + source: synthetic_delaunay_verification_error("non-Delaunay facet"), }, InsertionErrorKind::DelaunayValidationFailed, Some(InsertionErrorSourceKind::Delaunay( diff --git a/src/core/collections/spatial_hash_grid.rs b/src/core/collections/spatial_hash_grid.rs index 3826ea99..31695a2b 100644 --- a/src/core/collections/spatial_hash_grid.rs +++ b/src/core/collections/spatial_hash_grid.rs @@ -335,6 +335,7 @@ mod tests { use crate::geometry::traits::coordinate::InvalidCoordinateValue; use approx::assert_abs_diff_eq; use slotmap::SlotMap; + use std::assert_matches; #[test] fn test_hash_grid_index_try_new_rejects_invalid_cell_size() { @@ -342,32 +343,32 @@ mod tests { assert!(grid.is_usable()); assert_abs_diff_eq!(grid.cell_size(), 1.0, epsilon = f64::EPSILON); - assert!(matches!( + assert_matches!( HashGridIndex::<2>::try_new(0.0), Err(HashGridIndexError::NonPositiveCellSize { value: 0.0 }) - )); - assert!(matches!( + ); + assert_matches!( HashGridIndex::<2>::try_new(-1.0), Err(HashGridIndexError::NonPositiveCellSize { value: -1.0 }) - )); - assert!(matches!( + ); + assert_matches!( HashGridIndex::<2>::try_new(f64::NAN), Err(HashGridIndexError::NonFiniteCellSize { value: InvalidCoordinateValue::Nan }) - )); - assert!(matches!( + ); + assert_matches!( HashGridIndex::<2>::try_new(f64::INFINITY), Err(HashGridIndexError::NonFiniteCellSize { value: InvalidCoordinateValue::PositiveInfinity }) - )); - assert!(matches!( + ); + assert_matches!( HashGridIndex::<2>::try_new(f64::NEG_INFINITY), Err(HashGridIndexError::NonFiniteCellSize { value: InvalidCoordinateValue::NegativeInfinity }) - )); + ); } #[test] diff --git a/src/core/tds.rs b/src/core/tds.rs index 274e8a39..50e4e682 100644 --- a/src/core/tds.rs +++ b/src/core/tds.rs @@ -7521,7 +7521,7 @@ mod tests { use crate::geometry::point::Point; use crate::repair::DelaunayRepairOperation; use crate::topology::characteristics::euler::TopologyClassification; - use crate::validation::DelaunayTriangulationValidationError; + use crate::validation::{DelaunayTriangulationValidationError, DelaunayVerificationError}; use slotmap::KeyData; use std::assert_matches; use std::sync::Arc; @@ -7530,6 +7530,17 @@ mod tests { // TEST HELPER FUNCTIONS // ============================================================================= + fn synthetic_delaunay_verification_error( + message: &str, + ) -> DelaunayTriangulationValidationError { + DelaunayTriangulationValidationError::VerificationFailed { + source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { + message: message.to_string(), + }) + .into(), + } + } + fn vertex_with_uuid( point: Point, uuid: Uuid, @@ -7800,9 +7811,7 @@ mod tests { DelaunayValidationErrorKind::Triangulation, ), ( - DelaunayTriangulationValidationError::VerificationFailed { - message: "non-Delaunay facet".to_string(), - }, + synthetic_delaunay_verification_error("non-Delaunay facet"), DelaunayValidationErrorKind::VerificationFailed, ), ( @@ -10695,9 +10704,7 @@ mod tests { #[test] fn test_invariant_error_from_delaunay_validation_error() { - let dt_err = DelaunayTriangulationValidationError::VerificationFailed { - message: "test".to_string(), - }; + let dt_err = synthetic_delaunay_verification_error("test"); let inv = InvariantError::from(dt_err); assert_matches!(inv, InvariantError::Delaunay(_)); } @@ -10722,9 +10729,7 @@ mod tests { ), ), ( - InvariantError::from(DelaunayTriangulationValidationError::VerificationFailed { - message: "non-Delaunay facet".to_string(), - }), + InvariantError::from(synthetic_delaunay_verification_error("non-Delaunay facet")), InvariantErrorSummaryKind::Delaunay, InvariantErrorSummaryDetail::Delaunay( DelaunayValidationErrorKind::VerificationFailed, diff --git a/src/core/util/canonical_points.rs b/src/core/util/canonical_points.rs index 840459b2..03d6ca22 100644 --- a/src/core/util/canonical_points.rs +++ b/src/core/util/canonical_points.rs @@ -183,6 +183,7 @@ mod tests { use crate::core::vertex::Vertex; use crate::geometry::kernel::{AdaptiveKernel, Kernel}; use slotmap::KeyData; + use std::assert_matches; // ========================================================================= // HELPER FUNCTIONS @@ -263,13 +264,13 @@ mod tests { let err = sorted_simplex_points(&tds, &simplex).unwrap_err(); - assert!(matches!( + assert_matches!( err, CanonicalSimplexPointError::InvalidArity { expected: 3, found: 2, } - )); + ); } #[test] @@ -281,10 +282,10 @@ mod tests { let err = sorted_simplex_points(&tds, &simplex).unwrap_err(); - assert!(matches!( + assert_matches!( err, CanonicalSimplexPointError::MissingVertex { vertex_key } if vertex_key == missing - )); + ); } // ========================================================================= @@ -327,13 +328,13 @@ mod tests { let err = sorted_facet_points_with_extra(&tds, &[keys[0]], extra).unwrap_err(); - assert!(matches!( + assert_matches!( err, CanonicalFacetPointError::InvalidArity { expected: 2, found: 1, } - )); + ); } #[test] @@ -344,10 +345,10 @@ mod tests { let err = sorted_facet_points_with_extra(&tds, &[keys[0], missing], extra).unwrap_err(); - assert!(matches!( + assert_matches!( err, CanonicalFacetPointError::MissingVertex { vertex_key } if vertex_key == missing - )); + ); } // ========================================================================= diff --git a/src/core/validation.rs b/src/core/validation.rs index 75d2a8e5..4b5819a6 100644 --- a/src/core/validation.rs +++ b/src/core/validation.rs @@ -1588,6 +1588,7 @@ fn start_insertion_timing(telemetry_mode: InsertionTelemetryMode) -> Option DelaunayTriangulationValidationError { + DelaunayTriangulationValidationError::VerificationFailed { + source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { + message: message.to_string(), + }) + .into(), + } + } + fn insert_test_vertex_with_coords( tds: &mut Tds<(), (), D>, entries: &[(usize, f64)], @@ -2304,10 +2316,7 @@ mod tests { Triangulation::, (), (), 3>::invariant_error_to_insertion_error(inv); assert_matches!(ins, InsertionError::TopologyValidationFailed { .. }); - let inv = - InvariantError::Delaunay(DelaunayTriangulationValidationError::VerificationFailed { - message: "test".to_string(), - }); + let inv = InvariantError::Delaunay(synthetic_delaunay_verification_error("test")); let ins = Triangulation::, (), (), 3>::invariant_error_to_insertion_error(inv); assert_matches!(ins, InsertionError::DelaunayValidationFailed { .. }); diff --git a/src/delaunay/builder.rs b/src/delaunay/builder.rs index 179b43e1..eceabc63 100644 --- a/src/delaunay/builder.rs +++ b/src/delaunay/builder.rs @@ -172,7 +172,7 @@ use crate::topology::traits::topological_space::{ GlobalTopology, TopologyKind, ToroidalConstructionMode, ToroidalDomain, ToroidalDomainError, }; use crate::triangulation::DelaunayTriangulation; -use crate::validation::DelaunayTriangulationValidationError; +use crate::validation::{DelaunayTriangulationValidationError, DelaunayVerificationErrorKind}; use num_traits::ToPrimitive; use rand::SeedableRng; use rand::rngs::StdRng; @@ -696,6 +696,8 @@ pub enum ExplicitDelaunayValidationSourceKind { Tds(TdsErrorKind), /// Lower-layer topology validation failed. Triangulation(TriangulationValidationErrorKind), + /// Level 4 verification failed in a specific verification path. + Verification(DelaunayVerificationErrorKind), /// Typed flip repair failed during a mutating operation. Repair(DelaunayRepairErrorKind), } @@ -714,12 +716,17 @@ pub enum ExplicitDelaunayValidationSourceKind { /// /// ```rust /// use delaunay::prelude::construction::{ -/// DelaunayTriangulationValidationError, ExplicitDelaunayValidationError, -/// ExplicitDelaunayValidationErrorKind, +/// DelaunayTriangulationValidationError, DelaunayVerificationError, +/// DelaunayVerificationErrorKind, ExplicitDelaunayValidationError, +/// ExplicitDelaunayValidationErrorKind, ExplicitDelaunayValidationSourceKind, /// }; +/// use delaunay::prelude::repair::DelaunayRepairError; /// /// let source = DelaunayTriangulationValidationError::VerificationFailed { -/// message: "non-Delaunay facet".to_string(), +/// source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { +/// message: "non-Delaunay facet".to_string(), +/// }) +/// .into(), /// }; /// let summary = ExplicitDelaunayValidationError::from(source); /// @@ -727,7 +734,12 @@ pub enum ExplicitDelaunayValidationSourceKind { /// summary.kind, /// ExplicitDelaunayValidationErrorKind::VerificationFailed, /// ); -/// assert!(summary.source_kind.is_none()); +/// assert_eq!( +/// summary.source_kind, +/// Some(ExplicitDelaunayValidationSourceKind::Verification( +/// DelaunayVerificationErrorKind::FlipPredicates, +/// )), +/// ); /// ``` #[must_use] #[derive(Clone, Debug, Error, PartialEq, Eq)] @@ -767,11 +779,13 @@ impl From for ExplicitDelaunayValidationEr DelaunayTriangulationValidationError::Triangulation(source) => Some( ExplicitDelaunayValidationSourceKind::Triangulation(source.as_ref().into()), ), + DelaunayTriangulationValidationError::VerificationFailed { source } => Some( + ExplicitDelaunayValidationSourceKind::Verification(source.as_ref().into()), + ), DelaunayTriangulationValidationError::RepairOperationFailed { source, .. } => Some( ExplicitDelaunayValidationSourceKind::Repair(source.as_ref().into()), ), - DelaunayTriangulationValidationError::VerificationFailed { .. } - | DelaunayTriangulationValidationError::RepairFailed { .. } => None, + DelaunayTriangulationValidationError::RepairFailed { .. } => None, }; Self { kind, @@ -3195,8 +3209,9 @@ mod tests { use crate::core::simplex::SimplexValidationError; use crate::core::tds::{ DelaunayValidationErrorKind, EntityKind, GeometricError, NeighborValidationError, - TdsConstructionError, + SimplexKey, TdsConstructionError, }; + use crate::core::util::DelaunayValidationError; use crate::core::util::uuid::UuidValidationError; use crate::core::validation::TriangulationValidationError; use crate::core::vertex::VertexValidationError; @@ -3210,6 +3225,7 @@ mod tests { use crate::topology::traits::topological_space::{ GlobalTopology, TopologyKind, ToroidalConstructionMode, ToroidalDomainError, }; + use crate::validation::DelaunayVerificationError; use approx::assert_relative_eq; use slotmap::{Key, KeyData}; use std::assert_matches; @@ -3233,6 +3249,17 @@ mod tests { ); } + fn synthetic_delaunay_verification_error( + message: &str, + ) -> DelaunayTriangulationValidationError { + DelaunayTriangulationValidationError::VerificationFailed { + source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { + message: message.to_string(), + }) + .into(), + } + } + #[derive(Clone, Copy, Debug)] struct ValidationFailureModel; @@ -3319,6 +3346,32 @@ mod tests { )) ); + let delaunay_verification = ExplicitDelaunayValidationError::from( + synthetic_delaunay_verification_error("non-Delaunay facet"), + ); + assert_eq!( + delaunay_verification.source_kind, + Some(ExplicitDelaunayValidationSourceKind::Verification( + DelaunayVerificationErrorKind::FlipPredicates, + )) + ); + let delaunay_empty_circumsphere = ExplicitDelaunayValidationError::from( + DelaunayTriangulationValidationError::VerificationFailed { + source: DelaunayVerificationError::from( + DelaunayValidationError::DelaunayViolation { + simplex_key: SimplexKey::default(), + }, + ) + .into(), + }, + ); + assert_eq!( + delaunay_empty_circumsphere.source_kind, + Some(ExplicitDelaunayValidationSourceKind::Verification( + DelaunayVerificationErrorKind::EmptyCircumsphere, + )) + ); + let delaunay_tds = ExplicitDelaunayValidationError::from( DelaunayTriangulationValidationError::from(TdsError::InconsistentDataStructure { message: "dangling simplex".to_string(), @@ -3638,9 +3691,7 @@ mod tests { fn explicit_insertion_error_preserves_nested_validation_source_kinds() { assert_explicit_insertion_error( InsertionError::DelaunayValidationFailed { - source: DelaunayTriangulationValidationError::VerificationFailed { - message: "non-Delaunay facet".to_string(), - }, + source: synthetic_delaunay_verification_error("non-Delaunay facet"), }, ExplicitInsertionErrorKind::DelaunayValidationFailed, Some(InsertionErrorSourceKind::Delaunay( diff --git a/src/delaunay/construction.rs b/src/delaunay/construction.rs index 8c4b256a..e3302ec6 100644 --- a/src/delaunay/construction.rs +++ b/src/delaunay/construction.rs @@ -5041,7 +5041,7 @@ mod tests { use crate::geometry::util::RandomPointGenerationError; use crate::repair::DelaunayRepairPolicy; use crate::topology::characteristics::euler::TopologyClassification; - use crate::validation::DelaunayTriangulationValidationError; + use crate::validation::{DelaunayTriangulationValidationError, DelaunayVerificationError}; use slotmap::KeyData; use std::assert_matches; use std::num::NonZeroUsize; @@ -5051,6 +5051,17 @@ mod tests { type TestDelaunay = DelaunayTriangulation, (), (), D>; + fn synthetic_delaunay_verification_error( + message: &str, + ) -> DelaunayTriangulationValidationError { + DelaunayTriangulationValidationError::VerificationFailed { + source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { + message: message.to_string(), + }) + .into(), + } + } + #[test] fn test_random_point_generation_error_variant_preserved() { let source = RandomPointGenerationError::InvalidCoordinateRange { @@ -7086,9 +7097,7 @@ mod tests { reason: HullExtensionReason::NoVisibleFacets, }, InsertionError::DelaunayValidationFailed { - source: DelaunayTriangulationValidationError::VerificationFailed { - message: "test".to_string(), - }, + source: synthetic_delaunay_verification_error("test"), }, InsertionError::DelaunayRepairFailed { source: Box::new(DelaunayRepairError::PostconditionFailed { @@ -7259,9 +7268,7 @@ mod tests { ); let delaunay = InsertionError::DelaunayValidationFailed { - source: DelaunayTriangulationValidationError::VerificationFailed { - message: "test".to_string(), - }, + source: synthetic_delaunay_verification_error("test"), }; let mapped = TestDelaunay::<3>::map_insertion_error(delaunay); assert_matches!( @@ -7317,9 +7324,7 @@ mod tests { let failure = DelaunayConstructionFailure::from( TriangulationConstructionError::InsertionDelaunayValidation { - source: DelaunayTriangulationValidationError::VerificationFailed { - message: "delaunay check".to_string(), - }, + source: synthetic_delaunay_verification_error("delaunay check"), }, ); assert_matches!( @@ -7551,9 +7556,7 @@ mod tests { .into(); let final_delaunay_err = DelaunayTriangulationConstructionError::Triangulation( DelaunayConstructionFailure::FinalDelaunayValidation { - source: DelaunayTriangulationValidationError::VerificationFailed { - message: "final Level 4 check failed".to_string(), - }, + source: synthetic_delaunay_verification_error("final Level 4 check failed"), }, ); diff --git a/src/delaunay/validation.rs b/src/delaunay/validation.rs index b42c90d3..48b5306c 100644 --- a/src/delaunay/validation.rs +++ b/src/delaunay/validation.rs @@ -13,7 +13,7 @@ use crate::core::tds::{ }; use crate::core::traits::data_type::DataType; use crate::core::triangulation::Triangulation; -use crate::core::util::is_delaunay_property_only; +use crate::core::util::{DelaunayValidationError, is_delaunay_property_only}; use crate::core::validation::{TopologyGuarantee, TriangulationValidationError}; use crate::geometry::kernel::Kernel; use crate::repair::DelaunayRepairOperation; @@ -22,6 +22,108 @@ use crate::triangulation::DelaunayTriangulation; use std::num::NonZeroUsize; use thiserror::Error; +/// Typed source for Level 4 Delaunay verification failures. +/// +/// Passive validation has two implementation paths: +/// - flip-predicate verification via [`verify_delaunay_for_triangulation`], used by +/// [`DelaunayTriangulation::is_valid`](crate::DelaunayTriangulation::is_valid) +/// - empty-circumsphere validation via `is_delaunay_property_only`, used when +/// reconstructing Euclidean triangulations from raw [`Tds`] +/// +/// This wrapper preserves which path failed and carries the original typed +/// error so callers can inspect predicate, topology, and simplex-key context +/// without parsing display text. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::repair::DelaunayRepairError; +/// use delaunay::prelude::validation::{ +/// DelaunayVerificationError, DelaunayVerificationErrorKind, +/// }; +/// +/// let source = +/// DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { +/// message: "non-Delaunay facet".to_string(), +/// }); +/// +/// assert_eq!( +/// DelaunayVerificationErrorKind::from(&source), +/// DelaunayVerificationErrorKind::FlipPredicates, +/// ); +/// ``` +#[derive(Clone, Debug, Error, PartialEq)] +#[non_exhaustive] +pub enum DelaunayVerificationError { + /// Flip-predicate verification failed. + #[error("flip-predicate verification failed: {source}")] + FlipPredicates { + /// Underlying flip verification error. + #[source] + source: Box, + }, + + /// Empty-circumsphere validation failed. + #[error("empty-circumsphere validation failed: {source}")] + EmptyCircumsphere { + /// Underlying Delaunay property validation error. + #[source] + source: Box, + }, +} + +impl From for DelaunayVerificationError { + fn from(source: DelaunayRepairError) -> Self { + Self::FlipPredicates { + source: Box::new(source), + } + } +} + +impl From for DelaunayVerificationError { + fn from(source: DelaunayValidationError) -> Self { + Self::EmptyCircumsphere { + source: Box::new(source), + } + } +} + +/// Discriminant for compact Level 4 verification-source summaries. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::repair::DelaunayRepairError; +/// use delaunay::prelude::validation::{ +/// DelaunayVerificationError, DelaunayVerificationErrorKind, +/// }; +/// +/// let source = +/// DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { +/// message: "non-Delaunay facet".to_string(), +/// }); +/// let kind = DelaunayVerificationErrorKind::from(&source); +/// +/// assert_eq!(kind, DelaunayVerificationErrorKind::FlipPredicates); +/// ``` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum DelaunayVerificationErrorKind { + /// Flip-predicate verification failed. + FlipPredicates, + /// Empty-circumsphere validation failed. + EmptyCircumsphere, +} + +impl From<&DelaunayVerificationError> for DelaunayVerificationErrorKind { + fn from(source: &DelaunayVerificationError) -> Self { + match source { + DelaunayVerificationError::FlipPredicates { .. } => Self::FlipPredicates, + DelaunayVerificationError::EmptyCircumsphere { .. } => Self::EmptyCircumsphere, + } + } +} + /// Errors that can occur during Delaunay triangulation validation and repair. /// /// The first three variants are returned by [`DelaunayTriangulation::validate`](crate::DelaunayTriangulation::validate) @@ -86,10 +188,13 @@ pub enum DelaunayTriangulationValidationError { /// This is returned by [`DelaunayTriangulation::is_valid`](crate::DelaunayTriangulation::is_valid) when the fast /// O(simplices) flip-predicate scan finds a Delaunay violation. The error is /// a Level 4 (Delaunay property) issue, not a Level 1–2 structural problem. - #[error("Delaunay verification failed: {message}")] + /// The [`DelaunayVerificationError`] source distinguishes flip-predicate + /// validation from empty-circumsphere reconstruction validation. + #[error("Delaunay verification failed: {source}")] VerificationFailed { - /// Description of the verification failure. - message: String, + /// Typed verification failure source. + #[source] + source: Box, }, /// Flip-based Delaunay repair failed with string-only context. @@ -256,9 +361,11 @@ where /// /// # Errors /// - /// Returns a [`DelaunayTriangulationValidationError`] if the empty-circumsphere test fails, or if - /// the underlying triangulation state is inconsistent and prevents geometric predicates - /// from being evaluated. + /// Returns a [`DelaunayTriangulationValidationError`] if Level 4 verification + /// detects a Delaunay violation, or if the underlying triangulation state is + /// inconsistent and prevents geometric predicates from being evaluated. The + /// [`VerificationFailed`](DelaunayTriangulationValidationError::VerificationFailed) + /// variant preserves the typed [`DelaunayVerificationError`] source. /// /// # Examples /// @@ -290,9 +397,9 @@ where /// ``` pub fn is_valid(&self) -> Result<(), DelaunayTriangulationValidationError> { // Use fast flip-based verification (O(simplices) instead of O(simplices × vertices)) - self.is_delaunay_via_flips().map_err(|err| { + self.is_delaunay_via_flips().map_err(|source| { DelaunayTriangulationValidationError::VerificationFailed { - message: err.to_string(), + source: Box::new(DelaunayVerificationError::from(source)), } }) } @@ -458,12 +565,12 @@ where } // Level 4 (Delaunay property) - if let Err(e) = self.is_delaunay_via_flips() { + if let Err(source) = self.is_delaunay_via_flips() { report.violations.push(InvariantViolation { kind: InvariantKind::DelaunayProperty, error: InvariantError::Delaunay( DelaunayTriangulationValidationError::VerificationFailed { - message: e.to_string(), + source: Box::new(DelaunayVerificationError::from(source)), }, ), }); @@ -693,9 +800,9 @@ where })?; if candidate.global_topology().is_euclidean() { - is_delaunay_property_only(&candidate.tri.tds).map_err(|e| { + is_delaunay_property_only(&candidate.tri.tds).map_err(|source| { DelaunayTriangulationValidationError::VerificationFailed { - message: format!("kernel-independent reconstruction validation failed: {e}"), + source: Box::new(DelaunayVerificationError::from(source)), } })?; } else { @@ -744,6 +851,7 @@ mod tests { use crate::core::simplex::Simplex; use crate::core::tds::{SimplexKey, TriangulationConstructionState, VertexKey}; use crate::geometry::kernel::AdaptiveKernel; + use std::assert_matches; use std::{error::Error, sync::Once}; use uuid::Uuid; @@ -796,6 +904,12 @@ mod tests { tds } + fn synthetic_flip_verification_source(message: &str) -> DelaunayVerificationError { + DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { + message: message.to_string(), + }) + } + #[test] fn validation_cadence_maps_optional_every() { assert_eq!( @@ -827,7 +941,10 @@ mod tests { #[test] fn verification_failed_display_includes_context() { let err = DelaunayTriangulationValidationError::VerificationFailed { - message: "flip predicate detected non-Delaunay facet".to_string(), + source: synthetic_flip_verification_source( + "flip predicate detected non-Delaunay facet", + ) + .into(), }; let msg = err.to_string(); @@ -839,6 +956,40 @@ mod tests { msg.contains("flip predicate detected non-Delaunay facet"), "Display should contain inner message: {msg}" ); + let DelaunayTriangulationValidationError::VerificationFailed { source } = &err else { + panic!("expected typed flip-predicate verification source, got {err:?}"); + }; + assert_matches!( + source.as_ref(), + DelaunayVerificationError::FlipPredicates { source } + if matches!( + source.as_ref(), + DelaunayRepairError::PostconditionFailed { .. } + ) + ); + } + + #[test] + fn verification_error_kind_covers_empty_circumsphere_source() { + let simplex_key = SimplexKey::default(); + let source = DelaunayVerificationError::from(DelaunayValidationError::DelaunayViolation { + simplex_key, + }); + + assert_eq!( + DelaunayVerificationErrorKind::from(&source), + DelaunayVerificationErrorKind::EmptyCircumsphere, + ); + assert!(source.to_string().contains("empty-circumsphere")); + let DelaunayVerificationError::EmptyCircumsphere { source } = source else { + panic!("expected empty-circumsphere source"); + }; + assert_matches!( + source.as_ref(), + DelaunayValidationError::DelaunayViolation { + simplex_key: actual, + } if *actual == simplex_key + ); } #[test] diff --git a/src/geometry/point.rs b/src/geometry/point.rs index fb471c65..62f31446 100644 --- a/src/geometry/point.rs +++ b/src/geometry/point.rs @@ -458,6 +458,7 @@ impl From<&Point> for [f64; D] { mod tests { use super::*; use approx::assert_relative_eq; + use std::assert_matches; use std::cmp::Ordering; use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, HashSet}; @@ -1356,13 +1357,13 @@ mod tests { let err = Point::<1>::try_from([9_007_199_254_740_993_u64]) .expect_err("integer coordinates that cannot round-trip through f64 must fail"); - assert!(matches!( + assert_matches!( err, CoordinateConversionError::ConversionFailed { coordinate_index: 0, .. } - )); + ); } #[test] diff --git a/src/lib.rs b/src/lib.rs index d79bdd93..fcaa7278 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -808,7 +808,9 @@ pub use crate::repair::{ DelaunayRepairOperation, DelaunayRepairOutcome, DelaunayRepairPolicy, }; pub use crate::triangulation::*; -pub use crate::validation::DelaunayTriangulationValidationError; +pub use crate::validation::{ + DelaunayTriangulationValidationError, DelaunayVerificationError, DelaunayVerificationErrorKind, +}; /// Creates vertices from points by re-validating coordinates at the public boundary. /// @@ -1135,11 +1137,12 @@ pub mod prelude { DelaunayRepairOutcome, DelaunayRepairPolicy, DelaunayTriangulation, DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, DelaunayTriangulationConstructionErrorWithStatistics, DelaunayTriangulationValidationError, - DuplicateDetectionMetrics, InitialSimplexStrategy, InsertionOrderStrategy, InsertionResult, - PlManifoldRepairError, PlManifoldRepairStats, RepairDecision, RepairSkipReason, - RetryPolicy, TopologicalOperation, TopologyGuarantee, Triangulation, - TriangulationConstructionError, TriangulationValidationError, ValidationConfigurationError, - ValidationPolicy, try_vertices_from_points, + DelaunayVerificationError, DelaunayVerificationErrorKind, DuplicateDetectionMetrics, + InitialSimplexStrategy, InsertionOrderStrategy, InsertionResult, PlManifoldRepairError, + PlManifoldRepairStats, RepairDecision, RepairSkipReason, RetryPolicy, TopologicalOperation, + TopologyGuarantee, Triangulation, TriangulationConstructionError, + TriangulationValidationError, ValidationConfigurationError, ValidationPolicy, + try_vertices_from_points, }; // Re-export utility items, but avoid exporting the util module names themselves. @@ -1269,7 +1272,10 @@ pub mod prelude { GlobalTopology, GlobalTopologyModelError, TopologyKind, ToroidalConstructionMode, ToroidalDomain, ToroidalDomainError, }; - pub use crate::validation::DelaunayTriangulationValidationError; + pub use crate::validation::{ + DelaunayTriangulationValidationError, DelaunayVerificationError, + DelaunayVerificationErrorKind, + }; pub use crate::{ CavityFillingError, CavityRepairStage, DelaunayTriangulation, SpatialIndexConstructionFailure, TopologyGuarantee, Triangulation, @@ -1414,7 +1420,8 @@ pub mod prelude { }; pub use crate::{ DelaunayRepairErrorKind, DelaunayRepairErrorSummary, DelaunayRepairOperation, - DelaunayTriangulation, DelaunayTriangulationValidationError, + DelaunayTriangulation, DelaunayTriangulationValidationError, DelaunayVerificationError, + DelaunayVerificationErrorKind, }; pub use crate::{DelaunayValidationError, find_delaunay_violations}; pub use crate::{ @@ -1447,7 +1454,8 @@ pub mod prelude { pub mod validation { pub use crate::validation::*; pub use crate::{ - DelaunayTriangulationValidationError, TopologyGuarantee, TriangulationValidationError, + DelaunayTriangulationValidationError, DelaunayVerificationError, + DelaunayVerificationErrorKind, TopologyGuarantee, TriangulationValidationError, ValidationConfigurationError, ValidationPolicy, }; } diff --git a/tests/prelude_exports.rs b/tests/prelude_exports.rs index 38007776..aa5a65f8 100644 --- a/tests/prelude_exports.rs +++ b/tests/prelude_exports.rs @@ -29,6 +29,8 @@ use delaunay::prelude::construction::{ DedupTolerance, DeduplicationError, DelaunayConstructionFailure, DelaunayRepairPolicy, DelaunayTriangulation, DelaunayTriangulationConstructionError, DelaunayTriangulationValidationError as ConstructionDelaunayTriangulationValidationError, + DelaunayVerificationError as ConstructionDelaunayVerificationError, + DelaunayVerificationErrorKind as ConstructionDelaunayVerificationErrorKind, ExplicitConstructionError, ExplicitDelaunayValidationError, ExplicitDelaunayValidationErrorKind, ExplicitDelaunayValidationSourceKind, ExplicitInsertionError, ExplicitInsertionErrorKind, ExplicitInvariantError, @@ -947,14 +949,36 @@ fn construction_prelude_covers_random_point_generation_failure_variant() assert_matches!( DelaunayConstructionFailure::FinalDelaunayValidation { source: ConstructionDelaunayTriangulationValidationError::VerificationFailed { - message: "synthetic final Level 4 failure".to_string(), + source: ConstructionDelaunayVerificationError::from( + DelaunayRepairError::PostconditionFailed { + message: "synthetic final Level 4 failure".to_string(), + }, + ) + .into(), }, }, DelaunayConstructionFailure::FinalDelaunayValidation { source: ConstructionDelaunayTriangulationValidationError::VerificationFailed { - message, + source, }, - } if message == "synthetic final Level 4 failure" + } if source.to_string().contains("synthetic final Level 4 failure") + ); + + let validation_summary = ExplicitDelaunayValidationError::from( + ConstructionDelaunayTriangulationValidationError::VerificationFailed { + source: ConstructionDelaunayVerificationError::from( + DelaunayRepairError::PostconditionFailed { + message: "synthetic Level 4 summary failure".to_string(), + }, + ) + .into(), + }, + ); + assert_eq!( + validation_summary.source_kind, + Some(ExplicitDelaunayValidationSourceKind::Verification( + ConstructionDelaunayVerificationErrorKind::FlipPredicates, + )) ); Ok(()) From befffb04c67a1d4983b023cfb21af9292fbccbcc Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Tue, 16 Jun 2026 21:03:31 -0700 Subject: [PATCH 2/5] refactor(api)!: require typed construction and repair errors (#443) - Parse explicit simplex specs before builder storage with fallible explicit-construction APIs. - Preserve construction, insertion, validation, and flip repair failures as typed variants, summaries, and source-bearing errors instead of parseable strings. - Route temporary triangulation assembly through candidate/proof types and add guardrails for constructor naming, debug assertions, unchecked constructors, and erased errors. BREAKING CHANGE: Explicit connectivity construction now uses `try_from_vertices_and_simplices*` and can fail before `build`; legacy string-only repair/catch-all error variants and unchecked construction paths were removed. Closes #443 --- docs/ORIENTATION_SPEC.md | 8 +- docs/dev/rust.md | 55 +- docs/validation.md | 56 +- semgrep.yaml | 78 ++- src/core/algorithms/flips.rs | 643 ++++++++++++++---- src/core/algorithms/incremental_insertion.rs | 125 ++-- src/core/construction.rs | 44 +- src/core/insertion.rs | 7 +- src/core/orientation.rs | 6 +- src/core/repair.rs | 9 +- src/core/tds.rs | 68 +- src/core/tds_snapshot.rs | 58 +- src/core/validation.rs | 23 +- src/core/vertex.rs | 12 +- src/delaunay/builder.rs | 345 +++++----- src/delaunay/construction.rs | 66 +- src/delaunay/delaunayize.rs | 22 +- src/delaunay/flips.rs | 3 + src/delaunay/insertion.rs | 22 +- src/delaunay/repair.rs | 161 +++-- src/delaunay/validation.rs | 218 ++++-- src/geometry/algorithms/convex_hull.rs | 2 +- src/geometry/util/triangulation_generation.rs | 4 +- src/lib.rs | 57 +- tests/delaunayize_workflow.rs | 12 +- tests/euler_characteristic.rs | 6 +- tests/prelude_exports.rs | 64 +- tests/semgrep/src/project_rules/rust_style.rs | 70 +- tests/triangulation_builder.rs | 136 ++-- 29 files changed, 1651 insertions(+), 729 deletions(-) diff --git a/docs/ORIENTATION_SPEC.md b/docs/ORIENTATION_SPEC.md index c298fea6..0ffe2eba 100644 --- a/docs/ORIENTATION_SPEC.md +++ b/docs/ORIENTATION_SPEC.md @@ -196,10 +196,10 @@ drive repair, but replacement-simplex orientation itself uses `robust_orientatio `src/delaunay/builder.rs` normalizes explicit and periodic construction: -- `from_vertices_and_simplices(...)` accepts user-provided simplex orderings, assembles - the TDS, calls `normalize_and_promote_positive_orientation()`, validates TDS - structure/topology, rejects geometrically degenerate simplices, and then enforces - the Delaunay property. +- `try_from_vertices_and_simplices(...)` validates user-provided simplex specs + before storage, assembles the TDS, calls `normalize_and_promote_positive_orientation()`, + validates TDS structure/topology, rejects geometrically degenerate simplices, + and then enforces the Delaunay property. - `.try_toroidal([..])` builds an image-point triangulation and then runs orientation normalization, lifted geometric orientation validation, final Levels 1-3 topology validation, and final Level 4 Delaunay validation before diff --git a/docs/dev/rust.md b/docs/dev/rust.md index 75d5d4d0..73e4a949 100644 --- a/docs/dev/rust.md +++ b/docs/dev/rust.md @@ -262,8 +262,17 @@ types and where already-validated values are merely assembled. Use fallible names for raw or invariant-bearing input: -- `try_new*`, `try_from_*`, `parse`, `FromStr`, or `TryFrom` parse caller - input and reject invalid values before storage. +- `try_new*` is the default smart-constructor family for raw values becoming a + proof-bearing domain type. +- `try_from_*`, `TryFrom`, `parse`, and `FromStr` are appropriate when the source + shape matters, especially conversions from another representation, + deserialized snapshot data, or textual/raw DTO input. +- `try_` is appropriate for fallible enum variant constructors, such as + `DedupPolicy::try_epsilon`, when the variant name is the clearest API. +- `try_` is appropriate for fallible builder setters, such as + `DelaunayTriangulationBuilder::try_toroidal`, when the builder remains an + intermediate state and final construction still happens at `build`. +- All of these names parse caller input and reject invalid values before storage. - Raw numeric coordinates, slotmap keys, facet indexes, dimensions, UUIDs, explicit connectivity, deserialized snapshots, and topology data are invariant-bearing input unless a narrower validated type already carries the @@ -292,7 +301,9 @@ being parsed: - Empty containers and empty triangulations may use `empty`, `new_empty`, or `with_empty_*` because no user geometry or topology is accepted. - Builder creation may use `Builder::new` when validation is explicitly deferred - to `build`; builder setters remain infallible and return `Self`. + to `build`; fallible builder setters must use descriptive `try_*` names, while + infallible builder setters keep `with_*` or domain-specific names and return + `Self`. - Configuration and statistics types may derive or implement `Default` when the default value is valid and documented as a policy choice or accumulator state. - `from_*` is acceptable for passive report/view extraction or infallible @@ -306,10 +317,10 @@ Current migration targets for API-normalization work: `DelaunayTriangulation::try_with_*` methods are the fallible custom-kernel constructors. Infallible empty constructors remain `empty` and `with_empty_*` because they accept no user geometry or topology. -- `DelaunayTriangulationBuilder::from_vertices_and_simplices*` stores explicit - connectivity for later validation in `build`. If this API is renamed, keep the - validation boundary at `build` or introduce a fallible `try_from_*` path that - proves connectivity before storage. +- `DelaunayTriangulationBuilder::try_from_vertices_and_simplices*` validates + explicit simplex specs before storing them in a private proof-bearing wrapper. + Full TDS/topology/Delaunay validation still happens at `build`, where the + assembled triangulation exists. - `ConvexHull::try_from_triangulation` is the fallible hull-snapshot constructor. Reserve `from_*` for infallible conversions from proof-bearing input or passive view/report extraction. @@ -317,11 +328,17 @@ Current migration targets for API-normalization work: they consume proof-bearing inputs and cannot fail; rename to `try_from_*` when they parse raw invalidable state. -Semgrep guardrails for constructor names should stay narrow and repo-specific: -protect established public parse boundaries such as `DelaunayTriangulation` and -`ConvexHull`, while preserving intentional exceptions such as empty -constructors, builder `new`, and infallible construction from proof-bearing -values. +Semgrep guardrails for constructor names should stay narrow and repo-specific. +They enforce that fallible constructor definitions do not use misleading `new` +or `from_*` names, and they protect established public parse boundaries such as +`DelaunayTriangulation` and `ConvexHull`. Do not make the rules require every +fallible boundary to be named `try_new*`; descriptive `try_*` names are allowed +for builder setters and enum variant constructors when they better describe the +operation. +Do not add `from_unchecked_*` constructors; use an explicit candidate type for +temporarily assembled state, then consume validation proof before converting to +the final domain type. Other infallible `from_*` names remain acceptable only +for total conversions, passive report/view extraction, or proof-bearing input. --- @@ -329,11 +346,23 @@ values. Panics should be avoided in library code. +User-facing Rust surfaces must also avoid panic-based examples. Do not use +unwrap or expect calls in committed examples, benchmarks, Markdown Rust blocks, +or doctests. These artifacts are copied by users and should model typed error +propagation with `?`, local `thiserror` enums, or crate error types. Reserve +unwrap and expect calls for unit tests and test-only fixtures, where a panic +clearly reports a broken test assumption. + Acceptable panic situations: - internal invariants violated - unreachable logic errors -- debugging assertions + +Do not use `debug_assert!`, `debug_assert_eq!`, or `debug_assert_ne!` in +production source. Debug-only assertions disappear in release builds, so they +cannot protect library invariants or serve as parse-don't-validate boundaries. +Encode the invariant in a type, return a typed error, or cover the assumption +with tests instead. Prefer returning: diff --git a/docs/validation.md b/docs/validation.md index fcfd0721..6f8d4eac 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -317,7 +317,7 @@ Validates the combinatorial structure of the Triangulation Data Structure. - **Production**: After construction or major modifications - **Tests**: In test suites to catch structural bugs -- **Debug builds**: Use `debug_assert!(dt.tds().is_valid().is_ok())` +- **Development builds**: Run explicit validation and propagate the typed error ### Example @@ -595,22 +595,32 @@ fn test_my_triangulation_operation() { } ``` -### Pattern 2: Debug Build Validation +### Pattern 2: Development Validation ```rust use delaunay::prelude::query::*; +use delaunay::prelude::tds::{InvariantError, TdsError}; -pub fn my_algorithm(dt: &mut DelaunayTriangulation, (), (), 3>) { +#[derive(Debug, thiserror::Error)] +pub enum DevelopmentValidationError { + #[error(transparent)] + Tds(#[from] TdsError), + #[error(transparent)] + Topology(#[from] InvariantError), +} + +pub fn my_algorithm( + dt: &mut DelaunayTriangulation, (), (), 3>, +) -> Result<(), DevelopmentValidationError> { // Do work... #[cfg(debug_assertions)] { - debug_assert!(dt.tds().is_valid().is_ok(), "TDS structure violated"); - debug_assert!( - dt.as_triangulation().is_valid().is_ok(), - "Topology invariant violated" - ); + dt.tds().is_valid()?; + dt.as_triangulation().is_valid()?; } + + Ok(()) } ``` @@ -618,13 +628,33 @@ pub fn my_algorithm(dt: &mut DelaunayTriangulation, (), (), 3>) ```rust use delaunay::prelude::query::*; +use delaunay::prelude::tds::{InvariantError, TdsError}; +use delaunay::DelaunayTriangulationValidationError; + +#[derive(Debug, thiserror::Error)] +pub enum ValidationLevelError { + #[error(transparent)] + Tds(#[from] TdsError), + #[error(transparent)] + Topology(#[from] InvariantError), + #[error(transparent)] + Delaunay(#[from] DelaunayTriangulationValidationError), + #[error("unsupported validation level {level}; expected 2, 3, or 4")] + UnsupportedLevel { level: u8 }, +} -pub fn validate_with_level(dt: &DelaunayTriangulation, (), (), 3>, level: u8) -> Result<(), String> { +pub fn validate_with_level( + dt: &DelaunayTriangulation, (), (), 3>, + level: u8, +) -> Result<(), ValidationLevelError> { match level { - 2 => dt.tds().is_valid().map_err(|e| e.to_string()), - 3 => dt.as_triangulation().is_valid().map_err(|e| e.to_string()), - 4 => dt.is_valid().map_err(|e| e.to_string()), - _ => Err("Invalid validation level".to_string()), + 2 => dt.tds().is_valid().map_err(ValidationLevelError::from), + 3 => dt + .as_triangulation() + .is_valid() + .map_err(ValidationLevelError::from), + 4 => dt.is_valid().map_err(ValidationLevelError::from), + _ => Err(ValidationLevelError::UnsupportedLevel { level }), } } ``` diff --git a/semgrep.yaml b/semgrep.yaml index 6f0a4088..03b61bfa 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -377,6 +377,45 @@ rules: ... } + - id: delaunay.rust.no-production-debug-assert + languages: + - rust + severity: WARNING + message: "Avoid debug_assert! in production source; encode invariants in types or return typed errors." + metadata: + category: correctness + rationale: >- + Debug-only assertions disappear in release builds, so they cannot be + parse-don't-validate boundaries for library invariants. + paths: + include: + - "/src/**/*.rs" + patterns: + - pattern-either: + - pattern: debug_assert!(...) + - pattern: std::debug_assert!(...) + - pattern: core::debug_assert!(...) + - pattern: debug_assert_eq!(...) + - pattern: std::debug_assert_eq!(...) + - pattern: core::debug_assert_eq!(...) + - pattern: debug_assert_ne!(...) + - pattern: std::debug_assert_ne!(...) + - pattern: core::debug_assert_ne!(...) + - pattern-not-inside: | + mod tests { + ... + } + - pattern-not-inside: | + #[cfg(test)] + mod $MOD { + ... + } + - pattern-not-inside: | + #[cfg(any(test, ...))] + mod $MOD { + ... + } + - id: delaunay.rust.no-public-surface-unwrap-panic languages: - rust @@ -571,6 +610,25 @@ rules: (?s)\bimpl[^{}]*\bDelaunayTriangulation\b[^{}]*\{(?:(?!^\}).)*?^\s*pub\s+(?:const\s+)?fn\s+(?:new|new_with_(?:construction_statistics|options(?:_and_construction_statistics)?|topology_guarantee)|with_(?:kernel|topology_guarantee(?:_and_options)?|options_and_statistics))\s*\( - pattern-regex: '(?s)\bimpl[^{}]*\bConvexHull\b[^{}]*\{(?:(?!^\}).)*?^\s*pub\s+fn\s+from_triangulation\s*(?:<|\()' + - id: delaunay.rust.no-fallible-new-or-from-constructor-definitions + languages: + - generic + severity: WARNING + message: "Fallible constructor definitions must not use new/from_*; use try_new*, try_from_*, parse, FromStr/TryFrom, or descriptive try_* names." + metadata: + category: correctness + tracking_issue: "https://github.com/acgetchell/delaunay/issues/459" + rationale: >- + Constructor definitions returning Result are parse boundaries. Naming + them new or from_* hides fallibility and invites storing raw invalidable + values before validation. + paths: + include: + - "/src/**/*.rs" + - "/tests/semgrep/src/project_rules/**/*.rs" + pattern-regex: >- + (?s)^\s*(?:pub(?:\([^)]*\))?\s+)?(?:const\s+|async\s+)?fn\s+(?:new|from_[A-Za-z0-9_]+)(?:<[^>{}]*>)?\s*\([^;{}]*?\)\s*->\s*Result\s*< + - id: delaunay.rust.no-public-from-validated-constructors languages: - generic @@ -607,6 +665,24 @@ rules: - "/tests/semgrep/src/project_rules/**/*.rs" pattern-regex: '\bfn\s+from_validated[A-Za-z0-9_]*_with_data\s*\(' + - id: delaunay.rust.no-unreviewed-from-unchecked-constructors + languages: + - generic + severity: WARNING + message: "Do not add from_unchecked_* constructors; use a candidate type, try_new*/try_from_*, or from_validated*." + metadata: + category: correctness + tracking_issue: "https://github.com/acgetchell/delaunay/issues/459" + rationale: >- + Temporarily assembled invalidable state should live in an explicit + candidate type until validation proof is consumed. New unchecked + constructors silently create another invalid-state escape hatch. + paths: + include: + - "/src/**/*.rs" + - "/tests/semgrep/src/project_rules/**/*.rs" + pattern-regex: '^\s*(?:pub(?:\([^)]*\))?\s+)?(?:const\s+|async\s+)?fn\s+from_unchecked_[A-Za-z0-9_]*\s*\(' + - id: delaunay.rust.no-slotmap-key-topology-deserialization languages: - generic @@ -1043,7 +1119,7 @@ rules: - "/src/project_rules/**/*.rs" exclude: - "/tests/semgrep/**" - pattern-regex: '^\s*pub(?:\([^)]*\))?\s+(?:const\s+|async\s+|unsafe\s+)?fn\s+[A-Za-z_][A-Za-z0-9_]*_unchecked\s*\(' # yamllint disable-line rule:line-length + pattern-regex: '^\s*pub\s+(?:const\s+|async\s+|unsafe\s+)?fn\s+(?:[A-Za-z_][A-Za-z0-9_]*_unchecked|from_unchecked_[A-Za-z0-9_]*)\s*\(' # yamllint disable-line rule:line-length - id: delaunay.rust.no-public-vertex-new-with-uuid languages: diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index 5640d52c..e9e7874b 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -28,9 +28,10 @@ #![forbid(unsafe_code)] use crate::core::algorithms::incremental_insertion::{ - CavityFillingError, HullExtensionReason, InsertionError, NeighborWiringError, - SpatialIndexConstructionFailure, TdsConstructionFailure, TdsValidationFailure, - external_facets_for_boundary, wire_cavity_neighbors, + CavityFillingError, HullExtensionReason, InsertionError, InsertionErrorKind, + InsertionTopologyValidationContext, NeighborWiringError, SpatialIndexConstructionFailure, + TdsConstructionFailure, TdsValidationFailure, external_facets_for_boundary, + wire_cavity_neighbors, }; use crate::core::algorithms::locate::{ConflictError, LocateError, extract_cavity_boundary}; use crate::core::collections::{ @@ -1964,7 +1965,7 @@ where format!( "k={} removed_face={:?} inserted_face={:?} removed_simplices={:?} new_simplices={:?} ridge_simplex_is_new={} global_simplices_in_new={global_simplices_in_new:?} predecessor_new_simplex_vertices={predecessor_new_simplex_vertices:?}", - last_applied_flip.k_move, + last_applied_flip.kind.k(), last_applied_flip.removed_face_vertices, last_applied_flip.inserted_face_vertices, last_applied_flip.removed_simplices, @@ -2105,7 +2106,7 @@ where format!( "k={} removed_face={:?} inserted_face={:?} removed_simplices={:?} new_simplices={:?} incident_simplices_in_new={incident_simplices_in_new:?} incident_simplices_in_removed={incident_simplices_in_removed:?} predecessor_new_simplex_vertices={predecessor_new_simplex_vertices:?} predecessor_removed_simplex_vertices={predecessor_removed_simplex_vertices:?}", - last_applied_flip.k_move, + last_applied_flip.kind.k(), last_applied_flip.removed_face_vertices, last_applied_flip.inserted_face_vertices, last_applied_flip.removed_simplices, @@ -2310,8 +2311,8 @@ pub enum FlipOrientationCheckStage { /// Detect repeated flip signatures and abort on cycles. #[derive(Debug, Clone, Copy)] struct FlipCycleContext<'a> { - signature: u64, - k_move: usize, + signature: FlipSignature, + kind: BistellarFlipKind, direction: FlipDirection, removed_face_vertices: &'a [VertexKey], inserted_face_vertices: &'a [VertexKey], @@ -2320,16 +2321,16 @@ struct FlipCycleContext<'a> { impl<'a> FlipCycleContext<'a> { /// Bundles the flip data needed for diagnostics without cloning vertex /// buffers on every repair step. - const fn new( - signature: u64, - k_move: usize, + const fn from_validated_flip( + signature: FlipSignature, + kind: BistellarFlipKind, direction: FlipDirection, removed_face_vertices: &'a [VertexKey], inserted_face_vertices: &'a [VertexKey], ) -> Self { Self { signature, - k_move, + kind, direction, removed_face_vertices, inserted_face_vertices, @@ -2377,7 +2378,7 @@ where max_flips, config.attempt, config.queue_order, - context.k_move, + context.kind.k(), context.direction, removed_details, inserted_details, @@ -3041,9 +3042,6 @@ pub enum FlipNeighborHullExtensionFailureKind { /// Lower-layer TDS error. #[error("TDS")] Tds, - /// Other hull-extension failure. - #[error("other")] - Other, } impl From<&HullExtensionReason> for FlipNeighborHullExtensionFailureKind { @@ -3053,7 +3051,6 @@ impl From<&HullExtensionReason> for FlipNeighborHullExtensionFailureKind { HullExtensionReason::InvalidPatch { .. } => Self::InvalidPatch, HullExtensionReason::PredicateFailed(_) => Self::PredicateFailed, HullExtensionReason::Tds(_) => Self::Tds, - HullExtensionReason::Other { .. } => Self::Other, } } } @@ -3077,9 +3074,6 @@ pub enum FlipNeighborDelaunayValidationFailureKind { /// Delaunay verification failed. #[error("verification failed")] VerificationFailed, - /// Legacy repair validation failed. - #[error("repair failed")] - RepairFailed, /// Repair operation validation failed. #[error("repair operation failed")] RepairOperationFailed, @@ -3093,7 +3087,6 @@ impl From<&DelaunayTriangulationValidationError> for FlipNeighborDelaunayValidat DelaunayTriangulationValidationError::VerificationFailed { .. } => { Self::VerificationFailed } - DelaunayTriangulationValidationError::RepairFailed { .. } => Self::RepairFailed, DelaunayTriangulationValidationError::RepairOperationFailed { .. } => { Self::RepairOperationFailed } @@ -3173,10 +3166,10 @@ pub enum FlipNeighborRepairFailure { diagnostics: FlipNeighborRepairDiagnostics, }, /// Repair completed but left a Delaunay violation. - #[error("repair postcondition failed: {message}")] + #[error("repair postcondition failed: {reason}")] PostconditionFailed { - /// Additional context describing the postcondition failure. - message: String, + /// Structured postcondition failure reason. + reason: DelaunayRepairPostconditionFailure, }, /// Post-repair verification could not evaluate a local flip predicate. #[error("repair verification failed during {context}: {source_kind}")] @@ -3187,10 +3180,10 @@ pub enum FlipNeighborRepairFailure { source_kind: FlipFailureKind, }, /// Repair completed but orientation canonicalization failed. - #[error("repair orientation canonicalization failed: {message}")] + #[error("repair orientation canonicalization failed: {reason}")] OrientationCanonicalizationFailed { - /// Additional context describing the canonicalization failure. - message: String, + /// Structured canonicalization failure reason. + reason: DelaunayRepairOrientationCanonicalizationFailureKind, }, /// Flip-based repair is not admissible under the current topology guarantee. #[error("repair requires {required:?} topology, found {found:?}: {message}")] @@ -3203,10 +3196,10 @@ pub enum FlipNeighborRepairFailure { message: &'static str, }, /// Heuristic rebuild failed during advanced repair. - #[error("heuristic rebuild failed: {message}")] + #[error("heuristic rebuild failed: {reason}")] HeuristicRebuildFailed { - /// Additional context for the rebuild failure. - message: String, + /// Structured rebuild failure category. + reason: DelaunayRepairHeuristicRebuildFailureKind, }, /// Underlying flip error. #[error("flip error: {source_kind}")] @@ -3303,10 +3296,10 @@ pub enum FlipNeighborWiringError { uuid: uuid::Uuid, }, /// Level 3 topology validation failed while preparing flip neighbor wiring. - #[error("topology validation error reached flip neighbor wiring: {message}: {source}")] + #[error("topology validation error reached flip neighbor wiring: {context}: {source}")] TopologyValidationFailed { /// High-level insertion context. - message: String, + context: InsertionTopologyValidationContext, /// Underlying topology validation error. #[source] source: TriangulationValidationError, @@ -3371,8 +3364,8 @@ impl From for FlipNeighborWiringError { Self::DuplicateCoordinates { coordinates } } InsertionError::DuplicateUuid { entity, uuid } => Self::DuplicateUuid { entity, uuid }, - InsertionError::TopologyValidationFailed { message, source } => { - Self::TopologyValidationFailed { message, source } + InsertionError::TopologyValidationFailed { context, source } => { + Self::TopologyValidationFailed { context, source } } InsertionError::MaxSimplicesRemovedExceeded { max_simplices_removed, @@ -4546,6 +4539,332 @@ impl fmt::Display for DelaunayRepairVerificationContext { } } +/// Structured reason a repair pass failed its postcondition. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum DelaunayRepairPostconditionFailure { + /// Repair disconnected the triangulation neighbor graph. + Disconnected { + /// Number of simplices remaining when the disconnected graph was detected. + simplex_count: usize, + }, + /// A local k=2 facet flip opportunity remained after repair. + LocalK2Violation { + /// Facet whose flip predicate still reports a violation. + facet: FacetHandle, + /// Optional opt-in diagnostic details captured under repair debug flags. + debug_details: Option, + }, + /// A local k=3 ridge flip opportunity remained after repair. + LocalK3Violation { + /// Ridge whose flip predicate still reports a violation. + ridge: RidgeHandle, + }, + /// A local inverse k=2 edge-collapse opportunity remained after repair. + LocalInverseK2Violation { + /// Edge whose inverse flip predicate still reports a violation. + edge: EdgeKey, + }, + /// A local inverse k=3 triangle-collapse opportunity remained after repair. + LocalInverseK3Violation { + /// Triangle whose inverse flip predicate still reports a violation. + triangle: TriangleHandle, + }, +} + +impl fmt::Display for DelaunayRepairPostconditionFailure { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Disconnected { simplex_count } => write!( + f, + "repair pass disconnected the triangulation ({simplex_count} simplices remain); neighbor wiring is incomplete" + ), + Self::LocalK2Violation { + facet, + debug_details, + } => { + write!( + f, + "local k=2 violation remains after repair (facet={facet:?})" + )?; + if let Some(details) = debug_details { + write!(f, "; {details}")?; + } + Ok(()) + } + Self::LocalK3Violation { ridge } => { + write!( + f, + "local k=3 violation remains after repair (ridge={ridge:?})" + ) + } + Self::LocalInverseK2Violation { edge } => { + write!( + f, + "local inverse k=2 flip remains applicable after repair (edge={edge:?})" + ) + } + Self::LocalInverseK3Violation { triangle } => write!( + f, + "local inverse k=3 flip remains applicable after repair (triangle={triangle:?})" + ), + } + } +} + +/// Structured reason orientation canonicalization failed after repair. +#[derive(Clone, Debug, Error, PartialEq)] +#[non_exhaustive] +pub enum DelaunayRepairOrientationCanonicalizationFailure { + /// Positive-orientation promotion failed after a flip-repair pass. + #[error("after flip repair: {source}")] + AfterFlipRepair { + /// Insertion-layer failure produced by orientation promotion. + #[source] + source: Box, + }, +} + +/// Compact orientation-canonicalization failure category for non-recursive summaries. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum DelaunayRepairOrientationCanonicalizationFailureKind { + /// Positive-orientation promotion failed after a flip-repair pass. + #[error("after flip repair: {source_kind:?}")] + AfterFlipRepair { + /// Category of the insertion-layer failure. + source_kind: InsertionErrorKind, + }, +} + +impl From<&DelaunayRepairOrientationCanonicalizationFailure> + for DelaunayRepairOrientationCanonicalizationFailureKind +{ + fn from(source: &DelaunayRepairOrientationCanonicalizationFailure) -> Self { + match source { + DelaunayRepairOrientationCanonicalizationFailure::AfterFlipRepair { source } => { + Self::AfterFlipRepair { + source_kind: insertion_error_kind(source), + } + } + } + } +} + +/// Passive vertex context reported with heuristic rebuild failures. +/// +/// The fields identify the vertex position, UUID, and coordinates that were +/// being replayed when rebuild failed. This is diagnostic context only; repair +/// algorithms do not accept it back as proof of a valid vertex. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub struct DelaunayRepairHeuristicVertexContext { + /// Position of the vertex in the shuffled rebuild order. + pub index: usize, + /// Stable vertex UUID. + pub vertex_uuid: uuid::Uuid, + /// Vertex coordinates at the rebuild boundary. + pub coordinates: CoordinateValues, +} + +impl fmt::Display for DelaunayRepairHeuristicVertexContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "idx={} uuid={} coords={}", + self.index, self.vertex_uuid, self.coordinates + ) + } +} + +/// Structured reason heuristic rebuild failed during advanced repair. +#[derive(Clone, Debug, Error, PartialEq)] +#[non_exhaustive] +pub enum DelaunayRepairHeuristicRebuildFailure { + /// Heuristic rebuild recursion exceeded its guard depth. + #[error("heuristic rebuild recursion depth exceeded {max_depth}")] + RecursionDepthExceeded { + /// Maximum permitted nested heuristic rebuild depth. + max_depth: usize, + }, + /// Primary repair, robust fallback, and heuristic rebuild all failed. + #[error("primary repair failed ({primary}); robust fallback failed ({robust}); {heuristic}")] + FallbackChainFailed { + /// Primary flip-repair failure. + #[source] + primary: Box, + /// Robust-kernel fallback failure. + robust: Box, + /// Heuristic rebuild failure. + heuristic: Box, + }, + /// A non-heuristic repair error escaped the heuristic rebuild path. + #[error("heuristic rebuild failed with unexpected repair error: {source}")] + UnexpectedRepairFailure { + /// Repair error returned by the heuristic path. + #[source] + source: Box, + }, + /// The attempt loop exited without recording a rebuild attempt. + #[error("heuristic rebuild made no attempts")] + NoAttempts, + /// Vertex insertion failed during heuristic rebuild. + #[error("heuristic rebuild insertion failed at {vertex}: {source}")] + InsertionFailed { + /// Vertex being inserted. + vertex: DelaunayRepairHeuristicVertexContext, + /// Insertion failure. + #[source] + source: Box, + }, + /// Local repair failed after a heuristic rebuild insertion. + #[error("heuristic rebuild repair failed at {vertex}: {source}")] + RepairFailed { + /// Vertex whose insertion triggered repair. + vertex: DelaunayRepairHeuristicVertexContext, + /// Insertion-layer repair failure. + #[source] + source: Box, + }, + /// Delaunay check failed after a heuristic rebuild insertion. + #[error("heuristic rebuild Delaunay check failed at {vertex}: {source}")] + DelaunayCheckFailed { + /// Vertex whose insertion triggered the check. + vertex: DelaunayRepairHeuristicVertexContext, + /// Insertion-layer check failure. + #[source] + source: Box, + }, + /// A vertex was skipped during heuristic rebuild. + #[error("heuristic rebuild skipped vertex at {vertex}: {source}")] + SkippedVertex { + /// Skipped vertex. + vertex: DelaunayRepairHeuristicVertexContext, + /// Insertion-layer skip reason. + #[source] + source: Box, + }, + /// One deterministic rebuild attempt failed. + #[error( + "attempt {attempt}/{max_attempts} (shuffle_seed={shuffle_seed} perturbation_seed={perturbation_seed}): {source}" + )] + AttemptFailed { + /// 1-based attempt number. + attempt: usize, + /// Maximum number of attempts. + max_attempts: usize, + /// Shuffle seed used for this attempt. + shuffle_seed: u64, + /// Perturbation seed used for this attempt. + perturbation_seed: u64, + /// Attempt failure. + #[source] + source: Box, + }, + /// Every deterministic heuristic rebuild attempt failed. + #[error("heuristic rebuild failed after {attempts} attempts: {last_failure}")] + ExhaustedAttempts { + /// Number of attempts tried. + attempts: usize, + /// Last observed attempt failure. + #[source] + last_failure: Box, + }, +} + +/// Compact heuristic-rebuild failure category for non-recursive summaries. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum DelaunayRepairHeuristicRebuildFailureKind { + /// Heuristic rebuild recursion exceeded its guard depth. + #[error("recursion depth exceeded")] + RecursionDepthExceeded, + /// Primary repair, robust fallback, and heuristic rebuild all failed. + #[error("fallback chain failed")] + FallbackChainFailed, + /// A non-heuristic repair error escaped the heuristic rebuild path. + #[error("unexpected repair failure")] + UnexpectedRepairFailure, + /// The attempt loop exited without recording a rebuild attempt. + #[error("no attempts")] + NoAttempts, + /// Vertex insertion failed during heuristic rebuild. + #[error("insertion failed")] + InsertionFailed, + /// Local repair failed after a heuristic rebuild insertion. + #[error("repair failed")] + RepairFailed, + /// Delaunay check failed after a heuristic rebuild insertion. + #[error("Delaunay check failed")] + DelaunayCheckFailed, + /// A vertex was skipped during heuristic rebuild. + #[error("skipped vertex")] + SkippedVertex, + /// One deterministic rebuild attempt failed. + #[error("attempt failed")] + AttemptFailed, + /// Every deterministic heuristic rebuild attempt failed. + #[error("attempts exhausted")] + ExhaustedAttempts, +} + +impl From<&DelaunayRepairHeuristicRebuildFailure> for DelaunayRepairHeuristicRebuildFailureKind { + fn from(source: &DelaunayRepairHeuristicRebuildFailure) -> Self { + match source { + DelaunayRepairHeuristicRebuildFailure::RecursionDepthExceeded { .. } => { + Self::RecursionDepthExceeded + } + DelaunayRepairHeuristicRebuildFailure::FallbackChainFailed { .. } => { + Self::FallbackChainFailed + } + DelaunayRepairHeuristicRebuildFailure::UnexpectedRepairFailure { .. } => { + Self::UnexpectedRepairFailure + } + DelaunayRepairHeuristicRebuildFailure::NoAttempts => Self::NoAttempts, + DelaunayRepairHeuristicRebuildFailure::InsertionFailed { .. } => Self::InsertionFailed, + DelaunayRepairHeuristicRebuildFailure::RepairFailed { .. } => Self::RepairFailed, + DelaunayRepairHeuristicRebuildFailure::DelaunayCheckFailed { .. } => { + Self::DelaunayCheckFailed + } + DelaunayRepairHeuristicRebuildFailure::SkippedVertex { .. } => Self::SkippedVertex, + DelaunayRepairHeuristicRebuildFailure::AttemptFailed { .. } => Self::AttemptFailed, + DelaunayRepairHeuristicRebuildFailure::ExhaustedAttempts { .. } => { + Self::ExhaustedAttempts + } + } + } +} + +const fn insertion_error_kind(source: &InsertionError) -> InsertionErrorKind { + match source { + InsertionError::ConflictRegion(_) => InsertionErrorKind::ConflictRegion, + InsertionError::Location(_) => InsertionErrorKind::Location, + InsertionError::CavityFilling { .. } => InsertionErrorKind::CavityFilling, + InsertionError::NeighborWiring { .. } => InsertionErrorKind::NeighborWiring, + InsertionError::NonManifoldTopology { .. } => InsertionErrorKind::NonManifoldTopology, + InsertionError::HullExtension { .. } => InsertionErrorKind::HullExtension, + InsertionError::DelaunayValidationFailed { .. } => { + InsertionErrorKind::DelaunayValidationFailed + } + InsertionError::DelaunayRepairFailed { .. } => InsertionErrorKind::DelaunayRepairFailed, + InsertionError::DuplicateCoordinates { .. } => InsertionErrorKind::DuplicateCoordinates, + InsertionError::DuplicateUuid { .. } => InsertionErrorKind::DuplicateUuid, + InsertionError::TopologyValidation(_) => InsertionErrorKind::TopologyValidation, + InsertionError::TopologyValidationFailed { .. } => { + InsertionErrorKind::TopologyValidationFailed + } + InsertionError::MaxSimplicesRemovedExceeded { .. } => { + InsertionErrorKind::MaxSimplicesRemovedExceeded + } + InsertionError::SpatialIndexConstruction { .. } => { + InsertionErrorKind::SpatialIndexConstruction + } + InsertionError::PerturbedCoordinateInvalid { .. } => { + InsertionErrorKind::PerturbedCoordinateInvalid + } + } +} + /// Errors that can occur during flip-based Delaunay repair. /// /// Large typed payloads are boxed to keep the public enum small and cheap to @@ -4588,10 +4907,10 @@ pub enum DelaunayRepairError { diagnostics: Box, }, /// Repair completed but left a Delaunay violation. - #[error("Delaunay repair postcondition failed: {message}")] + #[error("Delaunay repair postcondition failed: {reason}")] PostconditionFailed { - /// Additional context describing the postcondition failure. - message: String, + /// Structured postcondition failure reason. + reason: Box, }, /// Post-repair verification could not evaluate a local flip predicate. #[error("Delaunay repair verification failed during {context}: {source}")] @@ -4603,10 +4922,11 @@ pub enum DelaunayRepairError { source: Box, }, /// Repair completed but orientation canonicalization failed. - #[error("Delaunay repair orientation canonicalization failed: {message}")] + #[error("Delaunay repair orientation canonicalization failed: {reason}")] OrientationCanonicalizationFailed { - /// Additional context describing the canonicalization failure. - message: String, + /// Structured canonicalization failure reason. + #[source] + reason: Box, }, /// Flip-based repair is not admissible under the current topology guarantee. #[error("Delaunay repair requires {required:?} topology, found {found:?}: {message}")] @@ -4619,10 +4939,11 @@ pub enum DelaunayRepairError { message: &'static str, }, /// Heuristic rebuild failed during advanced repair. - #[error("Heuristic rebuild failed: {message}")] + #[error("Heuristic rebuild failed: {reason}")] HeuristicRebuildFailed { - /// Additional context for the rebuild failure. - message: String, + /// Structured rebuild failure reason. + #[source] + reason: Box, }, /// A lower-level [`FlipError`] stopped repair. /// @@ -4660,8 +4981,8 @@ impl From for FlipNeighborRepairFailure { max_flips, diagnostics: (*diagnostics).into(), }, - DelaunayRepairError::PostconditionFailed { message } => { - Self::PostconditionFailed { message } + DelaunayRepairError::PostconditionFailed { reason } => { + Self::PostconditionFailed { reason: *reason } } DelaunayRepairError::VerificationFailed { context, source } => { Self::VerificationFailed { @@ -4669,8 +4990,10 @@ impl From for FlipNeighborRepairFailure { source_kind: FlipFailureKind::from(source.as_ref()), } } - DelaunayRepairError::OrientationCanonicalizationFailed { message } => { - Self::OrientationCanonicalizationFailed { message } + DelaunayRepairError::OrientationCanonicalizationFailed { reason } => { + Self::OrientationCanonicalizationFailed { + reason: reason.as_ref().into(), + } } DelaunayRepairError::InvalidTopology { required, @@ -4681,8 +5004,10 @@ impl From for FlipNeighborRepairFailure { found, message, }, - DelaunayRepairError::HeuristicRebuildFailed { message } => { - Self::HeuristicRebuildFailed { message } + DelaunayRepairError::HeuristicRebuildFailed { reason } => { + Self::HeuristicRebuildFailed { + reason: reason.as_ref().into(), + } } DelaunayRepairError::Flip { source } => Self::Flip { source_kind: FlipFailureKind::from(source.as_ref()), @@ -5934,17 +6259,18 @@ where } diagnostics.record_applicable_repair_site(); + let kind = BistellarFlipKind::k2(D); let signature = flip_signature( - 2, + kind, context.direction, &context.removed_face_vertices, &context.inserted_face_vertices, ); check_flip_cycle( tds, - FlipCycleContext::new( + FlipCycleContext::from_validated_flip( signature, - 2, + kind, context.direction, &context.removed_face_vertices, &context.inserted_face_vertices, @@ -6720,11 +7046,9 @@ where // stronger boundary guarantee than final validation already enforces. if connectivity == ConnectivityPostcondition::Check && !tds.is_connected() { return Err(DelaunayRepairError::PostconditionFailed { - message: format!( - "repair pass disconnected the triangulation \ - ({} simplices remain); neighbor wiring is incomplete", - tds.number_of_simplices() - ), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: tds.number_of_simplices(), + }), }); } @@ -6820,9 +7144,7 @@ where diagnostics, last_applied_flip, ); - let mut message = - format!("local k=2 violation remains after repair (facet={facet:?})"); - if env::var_os("DELAUNAY_REPAIR_DEBUG_FACETS").is_some() { + let debug_details = if env::var_os("DELAUNAY_REPAIR_DEBUG_FACETS").is_some() { let removed_details: Vec<_> = context .removed_face_vertices .iter() @@ -6833,11 +7155,18 @@ where .iter() .filter_map(|&vkey| tds.vertex(vkey).map(|vertex| (vkey, *vertex.point()))) .collect(); - message = format!( - "{message}; removed_face={removed_details:?}; inserted_face={inserted_details:?}" - ); - } - return Err(DelaunayRepairError::PostconditionFailed { message }); + Some(format!( + "removed_face={removed_details:?}; inserted_face={inserted_details:?}" + )) + } else { + None + }; + return Err(DelaunayRepairError::PostconditionFailed { + reason: Box::new(DelaunayRepairPostconditionFailure::LocalK2Violation { + facet, + debug_details, + }), + }); } Ok(false) => { // No violation detected. @@ -6939,7 +7268,9 @@ where debug_ridge_context(tds, ridge, None, diagnostics, last_applied_flip); } return Err(DelaunayRepairError::PostconditionFailed { - message: format!("local k=3 violation remains after repair (ridge={ridge:?})"), + reason: Box::new(DelaunayRepairPostconditionFailure::LocalK3Violation { + ridge, + }), }); } Ok(false) => { @@ -7037,8 +7368,8 @@ where ); } return Err(DelaunayRepairError::PostconditionFailed { - message: format!( - "local inverse k=2 flip remains applicable after repair (edge={edge:?})" + reason: Box::new( + DelaunayRepairPostconditionFailure::LocalInverseK2Violation { edge }, ), }); } @@ -7113,8 +7444,8 @@ where ); } return Err(DelaunayRepairError::PostconditionFailed { - message: format!( - "local inverse k=3 flip remains applicable after repair (triangle={triangle:?})" + reason: Box::new( + DelaunayRepairPostconditionFailure::LocalInverseK3Violation { triangle }, ), }); } @@ -7134,13 +7465,22 @@ const FLIP_SIGNATURE_WINDOW: usize = 4096; // pathological cases while giving legitimate repair sequences room to converge. const MAX_REPEAT_SIGNATURE: usize = 128; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct FlipSignature(u64); + +impl fmt::Display for FlipSignature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + #[derive(Debug, Default)] struct RepairDiagnostics { ambiguous_predicates: usize, ambiguous_samples: Vec, predicate_failures: usize, cycle_detections: usize, - cycle_samples: Vec, + cycle_samples: Vec, inserted_simplex_skips: usize, inserted_simplex_sample: Option, invalid_ridge_multiplicity_skips: usize, @@ -7148,8 +7488,8 @@ struct RepairDiagnostics { missing_simplex_skips: usize, missing_simplex_sample: Option, saw_applicable_repair_site: bool, - flip_signature_window: VecDeque, - flip_signature_counts: FastHashMap, + flip_signature_window: VecDeque, + flip_signature_counts: FastHashMap, ridge_debug_emitted: usize, postcondition_facet_debug_emitted: usize, } @@ -7275,7 +7615,7 @@ impl RepairDiagnostics { /// Maintains a sliding signature window so cycle detection is bounded in /// memory but still catches local oscillations. - fn record_flip_signature(&mut self, signature: u64) { + fn record_flip_signature(&mut self, signature: FlipSignature) { let count = self.flip_signature_counts.entry(signature).or_insert(0); *count = count.saturating_add(1); @@ -7302,7 +7642,7 @@ impl RepairDiagnostics { /// Preserves the signature that triggered a non-convergence abort even if it /// was already sampled earlier. - fn record_cycle_abort(&mut self, signature: u64) { + fn record_cycle_abort(&mut self, signature: FlipSignature) { self.cycle_detections = self.cycle_detections.saturating_add(1); if self.cycle_samples.len() < CYCLE_SAMPLE_LIMIT && !self.cycle_samples.contains(&signature) { @@ -7374,7 +7714,11 @@ fn non_convergent_error( ambiguous_predicate_samples: diagnostics.ambiguous_samples.clone(), predicate_failures: diagnostics.predicate_failures, cycle_detections: diagnostics.cycle_detections, - cycle_signature_samples: diagnostics.cycle_samples.clone(), + cycle_signature_samples: diagnostics + .cycle_samples + .iter() + .map(|signature| signature.0) + .collect(), attempt: config.attempt, queue_order: config.queue_order, }), @@ -7447,11 +7791,11 @@ fn predicate_key_from_vertices(simplex_vertices: &[VertexKey], test_vertex: Vert /// Canonicalizes a flip attempt into a compact key for cycle detection. fn flip_signature( - k_move: usize, + kind: BistellarFlipKind, direction: FlipDirection, removed_face_vertices: &[VertexKey], inserted_face_vertices: &[VertexKey], -) -> u64 { +) -> FlipSignature { let mut removed: SmallBuffer = removed_face_vertices.iter().copied().collect(); removed.sort_unstable(); @@ -7461,7 +7805,7 @@ fn flip_signature( inserted.sort_unstable(); let mut hasher = FastHasher::default(); - k_move.hash(&mut hasher); + kind.k().hash(&mut hasher); match direction { FlipDirection::Forward => 0_u8.hash(&mut hasher), FlipDirection::Inverse => 1_u8.hash(&mut hasher), @@ -7474,19 +7818,20 @@ fn flip_signature( for vkey in &inserted { vkey.hash(&mut hasher); } - hasher.finish() + FlipSignature(hasher.finish()) } #[derive(Debug, Clone)] struct LastAppliedFlip { - k_move: usize, + kind: BistellarFlipKind, removed_face_vertices: SmallBuffer, inserted_face_vertices: SmallBuffer, removed_simplices: SimplexKeyBuffer, new_simplices: SimplexKeyBuffer, /// Snapshot of each removed simplex's vertex list captured before the flip's /// `remove_simplices_by_keys` call; pairs 1:1 with `removed_simplices`. Empty - /// inner buffers only appear in placeholder instances built via `Self::new`. + /// inner buffers only appear in placeholder instances built from validated + /// flip faces. removed_simplex_vertices: RemovedSimplexVertexSnapshot, } @@ -7494,7 +7839,11 @@ impl LastAppliedFlip { /// Sorts faces so immediate-reversal detection is independent of local simplex /// vertex order. Simplex lists stay empty here because this constructor is also /// used for temporary reversal checks. - fn new(k_move: usize, removed: &[VertexKey], inserted: &[VertexKey]) -> Self { + fn from_validated_flip_faces( + kind: BistellarFlipKind, + removed: &[VertexKey], + inserted: &[VertexKey], + ) -> Self { let mut removed_face_vertices: SmallBuffer = removed.iter().copied().collect(); removed_face_vertices.sort_unstable(); @@ -7504,7 +7853,7 @@ impl LastAppliedFlip { inserted_face_vertices.sort_unstable(); Self { - k_move, + kind, removed_face_vertices, inserted_face_vertices, removed_simplices: SimplexKeyBuffer::new(), @@ -7517,8 +7866,8 @@ impl LastAppliedFlip { /// whether the immediately preceding move created the bad local star. fn from_applied_flip(applied: &AppliedFlip) -> Self { let info = &applied.info; - let mut last = Self::new( - info.kind.k(), + let mut last = Self::from_validated_flip_faces( + info.kind, &info.removed_face_vertices, &info.inserted_face_vertices, ); @@ -7531,7 +7880,8 @@ impl LastAppliedFlip { /// Formats each removed simplex as `SimplexKey(N): vertices=[...]` using the /// snapshot captured before the flip's simplex removal. Falls back to - /// `missing-snapshot` only for placeholder rows created by `Self::new`. + /// `missing-snapshot` only for placeholder rows built from validated flip + /// faces. fn removed_simplex_vertex_lines(&self) -> Vec { self.removed_simplices .iter() @@ -7553,7 +7903,7 @@ impl LastAppliedFlip { /// consume the global flip budget. fn would_immediately_reverse_last_flip( last: Option<&LastAppliedFlip>, - k_move: usize, + kind: BistellarFlipKind, removed_face_vertices: &[VertexKey], inserted_face_vertices: &[VertexKey], ) -> bool { @@ -7561,11 +7911,15 @@ fn would_immediately_reverse_last_flip( return false; }; - if k_move + last_flip.k_move != D + 2 { + if kind.k() + last_flip.kind.k() != D + 2 { return false; } - let current = LastAppliedFlip::new(k_move, removed_face_vertices, inserted_face_vertices); + let current = LastAppliedFlip::from_validated_flip_faces( + kind, + removed_face_vertices, + inserted_face_vertices, + ); current.removed_face_vertices == last_flip.inserted_face_vertices && current.inserted_face_vertices == last_flip.removed_face_vertices } @@ -8018,7 +8372,7 @@ where if would_immediately_reverse_last_flip::( last_applied_flip.as_ref(), - 3, + BistellarFlipKind::k3(D), &context.removed_face_vertices, &context.inserted_face_vertices, ) { @@ -8030,17 +8384,18 @@ where return Ok(true); } + let kind = BistellarFlipKind::k3(D); let signature = flip_signature( - 3, + kind, context.direction, &context.removed_face_vertices, &context.inserted_face_vertices, ); check_flip_cycle( tds, - FlipCycleContext::new( + FlipCycleContext::from_validated_flip( signature, - 3, + kind, context.direction, &context.removed_face_vertices, &context.inserted_face_vertices, @@ -8211,7 +8566,7 @@ where if would_immediately_reverse_last_flip::( last_applied_flip.as_ref(), - D, + BistellarFlipKind::k2(D).inverse(), &context.removed_face_vertices, &context.inserted_face_vertices, ) { @@ -8222,17 +8577,18 @@ where } return Ok(true); } + let kind = BistellarFlipKind::k2(D).inverse(); let signature = flip_signature( - D, + kind, context.direction, &context.removed_face_vertices, &context.inserted_face_vertices, ); check_flip_cycle( tds, - FlipCycleContext::new( + FlipCycleContext::from_validated_flip( signature, - D, + kind, context.direction, &context.removed_face_vertices, &context.inserted_face_vertices, @@ -8260,7 +8616,7 @@ where ); } }; - let applied = match apply_delaunay_flip_dynamic(tds, D, &context) { + let applied = match apply_delaunay_flip_dynamic(tds, kind.k(), &context) { Ok(applied) => applied, Err(err) if let FlipError::InsertedSimplexAlreadyExists { .. } = &err => { diagnostics.record_inserted_simplex_skip(InsertedSimplexSkipSample { @@ -8394,7 +8750,7 @@ where if would_immediately_reverse_last_flip::( last_applied_flip.as_ref(), - D - 1, + BistellarFlipKind::k3(D).inverse(), &context.removed_face_vertices, &context.inserted_face_vertices, ) { @@ -8405,17 +8761,18 @@ where } return Ok(true); } + let kind = BistellarFlipKind::k3(D).inverse(); let signature = flip_signature( - D - 1, + kind, context.direction, &context.removed_face_vertices, &context.inserted_face_vertices, ); check_flip_cycle( tds, - FlipCycleContext::new( + FlipCycleContext::from_validated_flip( signature, - D - 1, + kind, context.direction, &context.removed_face_vertices, &context.inserted_face_vertices, @@ -8443,7 +8800,7 @@ where ); } }; - let applied = match apply_delaunay_flip_dynamic(tds, D - 1, &context) { + let applied = match apply_delaunay_flip_dynamic(tds, kind.k(), &context) { Ok(applied) => applied, Err(err) if let FlipError::InsertedSimplexAlreadyExists { .. } = &err => { diagnostics.record_inserted_simplex_skip(InsertedSimplexSkipSample { @@ -8571,7 +8928,7 @@ where if would_immediately_reverse_last_flip::( last_applied_flip.as_ref(), - 2, + BistellarFlipKind::k2(D), &context.removed_face_vertices, &context.inserted_face_vertices, ) { @@ -8583,17 +8940,18 @@ where return Ok(true); } + let kind = BistellarFlipKind::k2(D); let signature = flip_signature( - 2, + kind, context.direction, &context.removed_face_vertices, &context.inserted_face_vertices, ); check_flip_cycle( tds, - FlipCycleContext::new( + FlipCycleContext::from_validated_flip( signature, - 2, + kind, context.direction, &context.removed_face_vertices, &context.inserted_face_vertices, @@ -9991,7 +10349,7 @@ mod tests { }; let last = LastAppliedFlip::from_applied_flip(&applied); - assert_eq!(last.k_move, 2); + assert_eq!(last.kind, BistellarFlipKind::k2($dim)); assert_eq!( last.removed_face_vertices .iter() @@ -10020,7 +10378,8 @@ mod tests { assert!(lines[0].contains(&format!("{removed_simplex:?}: vertices="))); assert!(!lines[0].contains("missing-snapshot")); - let mut placeholder = LastAppliedFlip::new(1, &[v1], &[v2]); + let mut placeholder = + LastAppliedFlip::from_validated_flip_faces(BistellarFlipKind::k2($dim), &[v1], &[v2]); placeholder.removed_simplices.push(removed_simplex); assert_eq!( placeholder.removed_simplex_vertex_lines(), @@ -11352,17 +11711,17 @@ mod tests { fn test_repair_diagnostics_cycle_detection_records_repeats() { init_tracing(); let mut diagnostics = RepairDiagnostics::default(); - diagnostics.record_flip_signature(10); - diagnostics.record_flip_signature(20); + diagnostics.record_flip_signature(FlipSignature(10)); + diagnostics.record_flip_signature(FlipSignature(20)); assert_eq!(diagnostics.cycle_detections, 0); - diagnostics.record_flip_signature(10); + diagnostics.record_flip_signature(FlipSignature(10)); assert_eq!(diagnostics.cycle_detections, 1); - assert_eq!(diagnostics.cycle_samples, vec![10]); + assert_eq!(diagnostics.cycle_samples, vec![FlipSignature(10)]); - diagnostics.record_flip_signature(10); + diagnostics.record_flip_signature(FlipSignature(10)); assert_eq!(diagnostics.cycle_detections, 2); - assert_eq!(diagnostics.cycle_samples, vec![10]); + assert_eq!(diagnostics.cycle_samples, vec![FlipSignature(10)]); } #[test] @@ -14342,10 +14701,8 @@ mod tests { } fn sample_tds_validation_failure() -> TdsValidationFailure { - TdsValidationFailure::InvalidNeighbors { - reason: NeighborValidationError::Other { - message: "synthetic neighbor mismatch".to_string(), - }, + TdsValidationFailure::InconsistentDataStructure { + message: "synthetic neighbor mismatch".to_string(), } } @@ -14574,13 +14931,13 @@ mod tests { #[test] fn test_delaunay_repair_error_partial_eq() { let post_test = DelaunayRepairError::PostconditionFailed { - message: "test".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }; let post_test_copy = DelaunayRepairError::PostconditionFailed { - message: "test".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }; let post_other = DelaunayRepairError::PostconditionFailed { - message: "other".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 2 }), }; assert_eq!(post_test, post_test_copy); assert_ne!(post_test, post_other); @@ -14607,13 +14964,31 @@ mod tests { assert_ne!(flip_err, flip_other); let canonicalization_err = DelaunayRepairError::OrientationCanonicalizationFailed { - message: "test".to_string(), + reason: Box::new( + DelaunayRepairOrientationCanonicalizationFailure::AfterFlipRepair { + source: Box::new(InsertionError::DuplicateCoordinates { + coordinates: CoordinateValues::from([0.0, 0.0]), + }), + }, + ), }; let canonicalization_err_copy = DelaunayRepairError::OrientationCanonicalizationFailed { - message: "test".to_string(), + reason: Box::new( + DelaunayRepairOrientationCanonicalizationFailure::AfterFlipRepair { + source: Box::new(InsertionError::DuplicateCoordinates { + coordinates: CoordinateValues::from([0.0, 0.0]), + }), + }, + ), }; let canonicalization_other = DelaunayRepairError::OrientationCanonicalizationFailed { - message: "other".to_string(), + reason: Box::new( + DelaunayRepairOrientationCanonicalizationFailure::AfterFlipRepair { + source: Box::new(InsertionError::DuplicateCoordinates { + coordinates: CoordinateValues::from([1.0, 1.0]), + }), + }, + ), }; assert_eq!(canonicalization_err, canonicalization_err_copy); assert_ne!(canonicalization_err, canonicalization_other); @@ -14656,6 +15031,24 @@ mod tests { assert_matches!(source.as_ref(), FlipError::DegenerateSimplex); } + #[test] + fn test_heuristic_exhausted_attempts_exposes_last_failure_source() { + let exhausted = DelaunayRepairHeuristicRebuildFailure::ExhaustedAttempts { + attempts: 6, + last_failure: Box::new(DelaunayRepairHeuristicRebuildFailure::NoAttempts), + }; + + let source = exhausted + .source() + .expect("exhausted attempts should expose the last failure source") + .downcast_ref::>() + .expect("source should remain a typed boxed heuristic failure"); + assert_matches!( + source.as_ref(), + DelaunayRepairHeuristicRebuildFailure::NoAttempts + ); + } + #[test] fn test_flip_error_boxes_nested_typed_payloads() { let max_nested_payload_size = [ diff --git a/src/core/algorithms/incremental_insertion.rs b/src/core/algorithms/incremental_insertion.rs index fea566bc..37d7c0c7 100644 --- a/src/core/algorithms/incremental_insertion.rs +++ b/src/core/algorithms/incremental_insertion.rs @@ -89,11 +89,6 @@ pub enum HullExtensionReason { /// Preserves the structured [`TdsError`] (e.g. from boundary-facet retrieval) /// rather than collapsing it into a string. Tds(TdsError), - /// Other failure. - Other { - /// Underlying error message. - message: String, - }, } impl fmt::Display for HullExtensionReason { @@ -110,7 +105,44 @@ impl fmt::Display for HullExtensionReason { write!(f, "Geometric predicate failed: {source}") } Self::Tds(source) => write!(f, "TDS error: {source}"), - Self::Other { message } => f.write_str(message), + } + } +} + +/// Fixed context for a Level 3 topology validation failure during insertion. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum InsertionTopologyValidationContext { + /// Conversion from a generic invariant error back into an insertion error. + InvariantConversion, + /// Validation after a point insertion. + PostInsertion, + /// Validation while repairing local topology. + LocalRepair, + /// Validation of a structural topology repair path. + StructuralRepair, + /// Validation while repairing stale incident-simplex pointers. + StaleIncidentSimplexRepair, + /// Positive-orientation promotion failed its bounded convergence check. + PositiveOrientationPromotion, + /// Ridge-link validation after Delaunay repair. + DelaunayRepair, +} + +impl fmt::Display for InsertionTopologyValidationContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvariantConversion => f.write_str("topology validation failed"), + Self::PostInsertion => f.write_str("post-insertion topology validation failed"), + Self::LocalRepair => f.write_str("local topology validation failed"), + Self::StructuralRepair => f.write_str("structural topology validation failed"), + Self::StaleIncidentSimplexRepair => { + f.write_str("truly isolated vertex detected during stale incident-simplex repair") + } + Self::PositiveOrientationPromotion => { + f.write_str("positive-orientation promotion failed to converge") + } + Self::DelaunayRepair => f.write_str("topology invalid after Delaunay repair"), } } } @@ -801,15 +833,16 @@ impl From<&DelaunayRepairError> for DelaunayRepairErrorKind { /// ```rust /// use delaunay::prelude::repair::{ /// DelaunayRepairError, DelaunayRepairErrorKind, DelaunayRepairErrorSummary, +/// DelaunayRepairPostconditionFailure, /// }; /// /// let source = DelaunayRepairError::PostconditionFailed { -/// message: "remaining non-Delaunay facet".to_string(), +/// reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), /// }; /// let summary = DelaunayRepairErrorSummary::from(&source); /// /// assert_eq!(summary.kind, DelaunayRepairErrorKind::PostconditionFailed); -/// assert!(summary.message.contains("remaining non-Delaunay facet")); +/// assert!(summary.message.contains("disconnected the triangulation")); /// ``` #[must_use] #[derive(Debug, Clone, thiserror::Error)] @@ -914,11 +947,13 @@ pub enum InsertionErrorSourceKind { /// InsertionError, InsertionErrorKind, InsertionErrorSourceKind, /// InsertionErrorSummary, /// }; -/// use delaunay::prelude::repair::DelaunayRepairError; +/// use delaunay::prelude::repair::{ +/// DelaunayRepairError, DelaunayRepairPostconditionFailure, +/// }; /// /// let source = InsertionError::DelaunayRepairFailed { /// source: Box::new(DelaunayRepairError::PostconditionFailed { -/// message: "remaining non-Delaunay facet".to_string(), +/// reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), /// }), /// context: DelaunayRepairFailureContext::LocalRepair, /// }; @@ -1648,10 +1683,10 @@ pub enum InsertionError { /// This preserves the structured [`TriangulationValidationError`] without wrapping it into a /// [`TdsError`], /// avoiding lower-layer (`Tds`) errors depending on higher-layer (`Triangulation`) errors. - #[error("{message}: {source}")] + #[error("{context}: {source}")] TopologyValidationFailed { /// High-level context for when the topology validation failed. - message: String, + context: InsertionTopologyValidationContext, /// The underlying Level 3 validation error. #[source] @@ -4553,7 +4588,9 @@ mod tests { use super::*; use crate::DelaunayTriangulation; use crate::core::algorithms::flips::{ - DelaunayRepairDiagnostics, DelaunayRepairVerificationContext, FlipError, RepairQueueOrder, + DelaunayRepairDiagnostics, DelaunayRepairHeuristicRebuildFailure, + DelaunayRepairOrientationCanonicalizationFailure, DelaunayRepairPostconditionFailure, + DelaunayRepairVerificationContext, FlipError, RepairQueueOrder, }; use crate::core::algorithms::locate::InternalInconsistencySite; use crate::core::collections::SimplexKeyBuffer; @@ -5387,9 +5424,12 @@ mod tests { fn synthetic_delaunay_verification_error( message: &str, ) -> DelaunayTriangulationValidationError { + let _ = message; DelaunayTriangulationValidationError::VerificationFailed { source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { - message: message.to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }) .into(), } @@ -5407,7 +5447,9 @@ mod tests { ), ( DelaunayRepairError::PostconditionFailed { - message: "remaining non-Delaunay facet".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }, DelaunayRepairErrorKind::PostconditionFailed, ), @@ -5420,7 +5462,13 @@ mod tests { ), ( DelaunayRepairError::OrientationCanonicalizationFailed { - message: "orientation pass failed".to_string(), + reason: Box::new( + DelaunayRepairOrientationCanonicalizationFailure::AfterFlipRepair { + source: Box::new(InsertionError::DuplicateCoordinates { + coordinates: CoordinateValues::from([0.0, 0.0]), + }), + }, + ), }, DelaunayRepairErrorKind::OrientationCanonicalizationFailed, ), @@ -5434,7 +5482,11 @@ mod tests { ), ( DelaunayRepairError::HeuristicRebuildFailed { - message: "fallback rebuild failed".to_string(), + reason: Box::new( + DelaunayRepairHeuristicRebuildFailure::RecursionDepthExceeded { + max_depth: 1, + }, + ), }, DelaunayRepairErrorKind::HeuristicRebuildFailed, ), @@ -5465,7 +5517,7 @@ mod tests { ), ( InsertionError::TopologyValidationFailed { - message: "post-insertion topology validation failed".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::Disconnected { simplex_count: 2 }, }, InsertionErrorKind::TopologyValidationFailed, @@ -5485,7 +5537,11 @@ mod tests { ( InsertionError::DelaunayRepairFailed { source: Box::new(DelaunayRepairError::HeuristicRebuildFailed { - message: "rebuild could not restore topology".to_string(), + reason: Box::new( + DelaunayRepairHeuristicRebuildFailure::RecursionDepthExceeded { + max_depth: 1, + }, + ), }), context: DelaunayRepairFailureContext::LocalRepair, }, @@ -5555,7 +5611,7 @@ mod tests { #[test] fn test_robust_fallback_context_preserves_initial_repair_summary() { let initial = DelaunayRepairError::PostconditionFailed { - message: "local predicate violation".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }; let context = DelaunayRepairFailureContext::LocalRepairRobustFallback { initial: DelaunayRepairErrorSummary::from(&initial), @@ -5563,7 +5619,7 @@ mod tests { let msg = context.to_string(); assert!(msg.contains("local repair failed")); - assert!(msg.contains("local predicate violation")); + assert!(msg.contains("disconnected the triangulation")); assert!(msg.contains("robust fallback also failed")); } @@ -5658,7 +5714,7 @@ mod tests { InsertionError::CavityFilling { reason: CavityFillingError::NeighborRebuild { reason: NeighborRebuildError::from(InsertionError::TopologyValidationFailed { - message: "local topology validation failed".to_string(), + context: InsertionTopologyValidationContext::LocalRepair, source: TriangulationValidationError::ManifoldFacetMultiplicity { facet_key: 0x1234, simplex_count: 3, @@ -5672,7 +5728,7 @@ mod tests { !InsertionError::CavityFilling { reason: CavityFillingError::NeighborRebuild { reason: NeighborRebuildError::from(InsertionError::TopologyValidationFailed { - message: "structural topology validation failed".to_string(), + context: InsertionTopologyValidationContext::StructuralRepair, source: TriangulationValidationError::Disconnected { simplex_count: 2 }, }), }, @@ -5791,7 +5847,7 @@ mod tests { // perturbing coordinates changes the conflict region. assert!( InsertionError::TopologyValidationFailed { - message: "test".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::IsolatedVertex { vertex_key: VertexKey::from(KeyData::from_ffi(1)), vertex_uuid: uuid::Uuid::nil(), @@ -5803,7 +5859,7 @@ mod tests { // TopologyValidationFailed wrapping a structural error is non-retryable. assert!( !InsertionError::TopologyValidationFailed { - message: "test".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::EulerCharacteristicMismatch { computed: 3, expected: 2, @@ -5820,7 +5876,7 @@ mod tests { }; assert!( InsertionError::TopologyValidationFailed { - message: "test".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: geometry_l3, } .is_retryable() @@ -5829,7 +5885,7 @@ mod tests { // TopologyValidationFailed wrapping BoundaryRidgeMultiplicity is retryable. assert!( InsertionError::TopologyValidationFailed { - message: "test".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::BoundaryRidgeMultiplicity { ridge_key: 0xab, boundary_facet_count: 3, @@ -5840,7 +5896,7 @@ mod tests { // TopologyValidationFailed wrapping RidgeLinkNotManifold is retryable. assert!( InsertionError::TopologyValidationFailed { - message: "test".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::RidgeLinkNotManifold { ridge_key: 0xcd, link_vertex_count: 4, @@ -5855,7 +5911,7 @@ mod tests { // TopologyValidationFailed wrapping VertexLinkNotManifold is retryable. assert!( InsertionError::TopologyValidationFailed { - message: "test".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::VertexLinkNotManifold { vertex_key: VertexKey::from(KeyData::from_ffi(1)), link_vertex_count: 3, @@ -5872,7 +5928,7 @@ mod tests { // (wildcard fallback). assert!( !InsertionError::TopologyValidationFailed { - message: "test".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::EulerCharacteristicMismatch { computed: 3, expected: 2, @@ -6073,15 +6129,6 @@ mod tests { .is_retryable() ); - assert!( - !InsertionError::HullExtension { - reason: HullExtensionReason::Other { - message: "Failed to get boundary facets: test".to_string() - } - } - .is_retryable() - ); - assert!( !InsertionError::HullExtension { reason: HullExtensionReason::PredicateFailed( diff --git a/src/core/construction.rs b/src/core/construction.rs index 51e482b5..74a75d59 100644 --- a/src/core/construction.rs +++ b/src/core/construction.rs @@ -7,7 +7,8 @@ //! with the triangulation type until they can be split into narrower modules. use crate::core::algorithms::incremental_insertion::{ - CavityFillingError, HullExtensionReason, SpatialIndexConstructionFailure, + CavityFillingError, HullExtensionReason, InsertionTopologyValidationContext, + SpatialIndexConstructionFailure, }; use crate::core::algorithms::locate::{ConflictError, LocateError}; use crate::core::collections::{MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer}; @@ -25,6 +26,39 @@ use crate::geometry::traits::coordinate::CoordinateValues; use crate::validation::DelaunayTriangulationValidationError; use thiserror::Error; +/// Fixed context for final topology validation after construction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum FinalTopologyValidationContext { + /// Standard final validation after Euclidean construction. + ConstructionFinalize, + /// Final Levels 1-3 topology validation for a periodic quotient. + PeriodicQuotientTopology, + /// Final Level 4 Delaunay validation for a periodic quotient. + PeriodicQuotientDelaunay, + /// Final Levels 1-3 topology validation for a generated random triangulation. + RandomGeneration, +} + +impl std::fmt::Display for FinalTopologyValidationContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ConstructionFinalize => { + f.write_str("topology validation failed after construction") + } + Self::PeriodicQuotientTopology => { + f.write_str("periodic quotient failed final Levels 1-3 topology validation") + } + Self::PeriodicQuotientDelaunay => { + f.write_str("periodic quotient failed final Level 4 Delaunay validation") + } + Self::RandomGeneration => { + f.write_str("random triangulation failed final Levels 1-3 topology validation") + } + } + } +} + /// Errors that can occur during triangulation construction. /// /// # Examples @@ -147,10 +181,10 @@ pub enum TriangulationConstructionError { }, /// Level 3 topology validation failed during incremental construction. - #[error("{message}: {source}")] + #[error("{context}: {source}")] InsertionTopologyValidation { /// High-level insertion context. - message: String, + context: InsertionTopologyValidationContext, /// Underlying topology validation error. #[source] source: TriangulationValidationError, @@ -171,10 +205,10 @@ pub enum TriangulationConstructionError { /// /// Mirrors [`InsertionTopologyValidation`](Self::InsertionTopologyValidation) /// for post-build checks that run after the incremental insertion phase. - #[error("{message}: {source}")] + #[error("{context}: {source}")] FinalTopologyValidation { /// High-level finalization context. - message: String, + context: FinalTopologyValidationContext, /// Underlying validation error. #[source] source: InvariantErrorSummary, diff --git a/src/core/insertion.rs b/src/core/insertion.rs index ac86681c..92fa805c 100644 --- a/src/core/insertion.rs +++ b/src/core/insertion.rs @@ -587,8 +587,11 @@ where // to retry with perturbed coordinates. let perturbed_point = Point::try_new(perturbed_coords) .map_err(|source| InsertionError::PerturbedCoordinateInvalid { source })?; - current_vertex = - Vertex::new_with_uuid(perturbed_point, original_uuid, current_vertex.data); + current_vertex = Vertex::from_validated_point_with_uuid( + perturbed_point, + original_uuid, + current_vertex.data, + ); } // Duplicate coordinate detection uses the hash grid when available; otherwise it diff --git a/src/core/orientation.rs b/src/core/orientation.rs index 5fc45f3a..1ae8e8a9 100644 --- a/src/core/orientation.rs +++ b/src/core/orientation.rs @@ -5,7 +5,9 @@ //! slot order, and normalizing coherent orientation after construction or edits. //! Predicate implementations remain in the geometry layer. -use crate::core::algorithms::incremental_insertion::InsertionError; +use crate::core::algorithms::incremental_insertion::{ + InsertionError, InsertionTopologyValidationContext, +}; use crate::core::collections::{MAX_PRACTICAL_DIMENSION_SIZE, SimplexKeyBuffer, SmallBuffer}; use crate::core::simplex::Simplex; use crate::core::tds::{GeometricError, SimplexKey, TdsError, VertexKey}; @@ -350,7 +352,7 @@ where } let sampled: Vec = sample_keys.into_iter().flatten().collect(); return Err(InsertionError::TopologyValidationFailed { - message: "Positive-orientation promotion failed to converge".to_string(), + context: InsertionTopologyValidationContext::PositiveOrientationPromotion, source: TriangulationValidationError::OrientationPromotionNonConvergence { residual_count, sampled, diff --git a/src/core/repair.rs b/src/core/repair.rs index 57feee05..8f49c887 100644 --- a/src/core/repair.rs +++ b/src/core/repair.rs @@ -4,9 +4,9 @@ //! repair, and vertex-removal cavity retriangulation for [`Triangulation`](crate::prelude::triangulation::Triangulation). use crate::core::algorithms::incremental_insertion::{ - CavityFillingError, CavityRepairStage, InsertionError, external_facets_for_boundary, - fill_cavity_replacing_simplices, repair_neighbor_pointers, repair_neighbor_pointers_local, - wire_cavity_neighbors, + CavityFillingError, CavityRepairStage, InsertionError, InsertionTopologyValidationContext, + external_facets_for_boundary, fill_cavity_replacing_simplices, repair_neighbor_pointers, + repair_neighbor_pointers_local, wire_cavity_neighbors, }; use crate::core::algorithms::locate::extract_cavity_boundary; use crate::core::collections::{ @@ -157,8 +157,7 @@ where } else { // Truly isolated: no simplex in the TDS contains this vertex. return Err(InsertionError::TopologyValidationFailed { - message: "Truly isolated vertex detected during stale incident-simplex repair" - .to_string(), + context: InsertionTopologyValidationContext::StaleIncidentSimplexRepair, source: TriangulationValidationError::IsolatedVertex { vertex_key: vk, vertex_uuid: uuid, diff --git a/src/core/tds.rs b/src/core/tds.rs index 50e4e682..666f3747 100644 --- a/src/core/tds.rs +++ b/src/core/tds.rs @@ -897,12 +897,6 @@ pub enum NeighborValidationError { #[source] reason: Box, }, - /// Neighbor validation failed in a context that is still being migrated to structured fields. - #[error("{message}")] - Other { - /// Diagnostic detail. - message: String, - }, } /// Errors that can occur during triangulation validation (post-construction). @@ -1195,12 +1189,10 @@ impl From<&TdsError> for TdsErrorKind { /// # Examples /// /// ``` -/// use delaunay::prelude::tds::{NeighborValidationError, TdsError, TdsMutationError}; +/// use delaunay::prelude::tds::{TdsError, TdsMutationError}; /// -/// let err = TdsError::InvalidNeighbors { -/// reason: NeighborValidationError::Other { -/// message: "bad neighbors".to_string(), -/// }, +/// let err = TdsError::InconsistentDataStructure { +/// message: "bad neighbors".to_string(), /// }; /// let mutation: TdsMutationError = err.clone().into(); /// let round_trip: TdsError = mutation.clone().into(); @@ -1287,12 +1279,10 @@ pub enum InvariantKind { /// # Examples /// /// ``` -/// use delaunay::prelude::tds::{InvariantError, NeighborValidationError, TdsError}; +/// use delaunay::prelude::tds::{InvariantError, TdsError}; /// -/// let err = InvariantError::Tds(TdsError::InvalidNeighbors { -/// reason: NeighborValidationError::Other { -/// message: "bad neighbors".to_string(), -/// }, +/// let err = InvariantError::Tds(TdsError::InconsistentDataStructure { +/// message: "bad neighbors".to_string(), /// }); /// std::assert_matches!(err, InvariantError::Tds(_)); /// ``` @@ -1385,8 +1375,6 @@ pub enum DelaunayValidationErrorKind { Triangulation, /// Delaunay verification failed. VerificationFailed, - /// Legacy string-only repair validation failed. - RepairFailed, /// Typed repair validation failed. RepairOperationFailed, } @@ -1399,7 +1387,6 @@ impl From<&DelaunayTriangulationValidationError> for DelaunayValidationErrorKind DelaunayTriangulationValidationError::VerificationFailed { .. } => { Self::VerificationFailed } - DelaunayTriangulationValidationError::RepairFailed { .. } => Self::RepairFailed, DelaunayTriangulationValidationError::RepairOperationFailed { .. } => { Self::RepairOperationFailed } @@ -1488,15 +1475,13 @@ impl From for InvariantErrorSummary { /// /// ``` /// use delaunay::prelude::tds::{ -/// InvariantError, InvariantKind, InvariantViolation, NeighborValidationError, TdsError, +/// InvariantError, InvariantKind, InvariantViolation, TdsError, /// }; /// /// let violation = InvariantViolation { /// kind: InvariantKind::Topology, -/// error: InvariantError::Tds(TdsError::InvalidNeighbors { -/// reason: NeighborValidationError::Other { -/// message: "bad neighbors".to_string(), -/// }, +/// error: InvariantError::Tds(TdsError::InconsistentDataStructure { +/// message: "bad neighbors".to_string(), /// }), /// }; /// assert_eq!(violation.kind, InvariantKind::Topology); @@ -7512,7 +7497,7 @@ mod tests { use super::*; use crate::DelaunayTriangulation; use crate::builder::DelaunayTriangulationBuilder; - use crate::core::algorithms::flips::DelaunayRepairError; + use crate::core::algorithms::flips::{DelaunayRepairError, DelaunayRepairPostconditionFailure}; use crate::core::algorithms::incremental_insertion::InsertionError; use crate::core::facet::FacetError; use crate::core::simplex::Simplex; @@ -7533,9 +7518,12 @@ mod tests { fn synthetic_delaunay_verification_error( message: &str, ) -> DelaunayTriangulationValidationError { + let _ = message; DelaunayTriangulationValidationError::VerificationFailed { source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { - message: message.to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }) .into(), } @@ -7590,8 +7578,10 @@ mod tests { ); assert_tds_error_kind( &TdsError::InvalidNeighbors { - reason: NeighborValidationError::Other { - message: "neighbor invariant failed".to_string(), + reason: NeighborValidationError::NonPeriodicSelfNeighbor { + simplex_key, + simplex_uuid: uuid, + facet_index: 0, }, }, TdsErrorKind::InvalidNeighbors, @@ -7814,17 +7804,13 @@ mod tests { synthetic_delaunay_verification_error("non-Delaunay facet"), DelaunayValidationErrorKind::VerificationFailed, ), - ( - DelaunayTriangulationValidationError::RepairFailed { - message: "repair did not converge".to_string(), - }, - DelaunayValidationErrorKind::RepairFailed, - ), ( DelaunayTriangulationValidationError::RepairOperationFailed { operation: DelaunayRepairOperation::VertexRemoval, source: Box::new(DelaunayRepairError::PostconditionFailed { - message: "remaining violation".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }), }, DelaunayValidationErrorKind::RepairOperationFailed, @@ -10670,10 +10656,8 @@ mod tests { #[test] fn test_tds_mutation_error_accessors() { - let inner = TdsError::InvalidNeighbors { - reason: NeighborValidationError::Other { - message: "test".to_string(), - }, + let inner = TdsError::InconsistentDataStructure { + message: "test".to_string(), }; let mutation = TdsMutationError::from(inner.clone()); @@ -10777,10 +10761,8 @@ mod tests { fn test_invariant_violation_stores_kind_and_error() { let violation = InvariantViolation { kind: InvariantKind::NeighborConsistency, - error: InvariantError::Tds(TdsError::InvalidNeighbors { - reason: NeighborValidationError::Other { - message: "test".to_string(), - }, + error: InvariantError::Tds(TdsError::InconsistentDataStructure { + message: "test".to_string(), }), }; assert_eq!(violation.kind, InvariantKind::NeighborConsistency); diff --git a/src/core/tds_snapshot.rs b/src/core/tds_snapshot.rs index 659a15bd..ed280923 100644 --- a/src/core/tds_snapshot.rs +++ b/src/core/tds_snapshot.rs @@ -409,11 +409,11 @@ impl SnapshotVertexUuidSlots { } } - Ok(Self::from_checked_slice(slots)) + Ok(Self::from_validated_slice(slots)) } /// Builds vertex UUID slots from validated runtime state before serialization. - fn from_runtime(simplex_uuid: Uuid, slots: &[Uuid]) -> Result { + fn try_from_runtime(simplex_uuid: Uuid, slots: &[Uuid]) -> Result { validate_snapshot_vertex_slot_arity::(simplex_uuid, slots.len())?; let mut seen_vertex_uuids = fast_hash_set_with_capacity(slots.len()); @@ -426,11 +426,11 @@ impl SnapshotVertexUuidSlots { } } - Ok(Self::from_checked_slice(slots)) + Ok(Self::from_validated_slice(slots)) } /// Stores an already checked vertex UUID slice in the snapshot buffer type. - fn from_checked_slice(slots: &[Uuid]) -> Self { + fn from_validated_slice(slots: &[Uuid]) -> Self { let mut checked_slots = SimplexVertexUuidBuffer::with_capacity(slots.len()); checked_slots.extend(slots.iter().copied()); Self { @@ -473,17 +473,20 @@ impl SnapshotNeighborUuidSlots { } } - Ok(Self::from_checked_slice(slots)) + Ok(Self::from_validated_slice(slots)) } /// Builds neighbor UUID slots from validated runtime neighbor keys. - fn from_runtime(simplex_uuid: Uuid, slots: &[Option]) -> Result { + fn try_from_runtime( + simplex_uuid: Uuid, + slots: &[Option], + ) -> Result { validate_snapshot_neighbor_slot_arity::(simplex_uuid, slots.len())?; - Ok(Self::from_checked_slice(slots)) + Ok(Self::from_validated_slice(slots)) } /// Stores an already checked neighbor UUID slice in the snapshot buffer type. - fn from_checked_slice(slots: &[Option]) -> Self { + fn from_validated_slice(slots: &[Option]) -> Self { let mut checked_slots = NeighborBuffer::with_capacity(slots.len()); checked_slots.extend(slots.iter().copied()); Self { @@ -523,16 +526,16 @@ impl SnapshotPeriodicOffsetSlots { })?; slots.push(parsed_offset); } - Self::from_parsed_offsets(simplex_uuid, slots) + Self::try_from_parsed_offsets(simplex_uuid, slots) } /// Builds periodic-offset slots from runtime fixed-size offset arrays. - fn from_runtime(simplex_uuid: Uuid, offsets: &[[i8; D]]) -> Result { - Self::from_parsed_offsets(simplex_uuid, offsets.iter().copied()) + fn try_from_runtime(simplex_uuid: Uuid, offsets: &[[i8; D]]) -> Result { + Self::try_from_parsed_offsets(simplex_uuid, offsets.iter().copied()) } /// Stores parsed offsets after proving there is one offset per simplex vertex. - fn from_parsed_offsets( + fn try_from_parsed_offsets( simplex_uuid: Uuid, offsets: impl IntoIterator, ) -> Result { @@ -570,7 +573,7 @@ impl TdsSnapshotSimplex { /// The relationship checks are identical for owned and borrowed payloads, so /// this helper lets [`Tds`] serialization borrow `U`/`V` data while tests can /// still build owned raw snapshots for mutation. - fn from_simplex_with_data( + fn try_from_simplex_with_data( tds: &Tds, simplex: &Simplex, data: Option, @@ -598,12 +601,12 @@ impl TdsSnapshotSimplex { .transpose() }) .collect::, TdsSnapshotError>>()?; - let vertex_uuids = SnapshotVertexUuidSlots::from_runtime(simplex_uuid, &vertex_uuids)?; + let vertex_uuids = SnapshotVertexUuidSlots::try_from_runtime(simplex_uuid, &vertex_uuids)?; let neighbor_uuids = - SnapshotNeighborUuidSlots::from_runtime(simplex_uuid, &neighbor_uuids)?; + SnapshotNeighborUuidSlots::try_from_runtime(simplex_uuid, &neighbor_uuids)?; let periodic_vertex_offsets = simplex .periodic_vertex_offsets() - .map(|offsets| SnapshotPeriodicOffsetSlots::from_runtime(simplex_uuid, offsets)) + .map(|offsets| SnapshotPeriodicOffsetSlots::try_from_runtime(simplex_uuid, offsets)) .transpose()?; Ok(Self { @@ -799,7 +802,7 @@ impl TdsSnapshot { /// Production serialization uses the borrowed `from_tds` path below so non-`Copy` /// payloads can still cross the public [`Tds`] codec boundary. #[cfg(test)] - fn from_tds_owned(tds: &Tds) -> Result + fn try_from_tds_owned(tds: &Tds) -> Result where U: Copy, V: Copy, @@ -814,7 +817,7 @@ impl TdsSnapshot { let simplices = tds .simplices() .map(|(_simplex_key, simplex)| { - TdsSnapshotSimplex::from_simplex_with_data(tds, simplex, simplex.data) + TdsSnapshotSimplex::try_from_simplex_with_data(tds, simplex, simplex.data) }) .collect::, TdsSnapshotError>>()?; @@ -888,15 +891,18 @@ impl<'a, U, V, const D: usize> TdsSnapshot<&'a U, &'a V, D> { /// This is the production serialization path for [`Tds`]. It validates the /// live topology, stores UUID relationships, and borrows payload data so /// callers only need [`DataSerialize`] rather than `Copy`. - fn from_tds(tds: &'a Tds) -> Result { + fn try_from_tds(tds: &'a Tds) -> Result { tds.validate() .map_err(|source| TdsSnapshotError::SourceValidationFailed { source })?; let vertices = tds .vertices() .map(|(_vertex_key, vertex)| { - let mut snapshot_vertex = - Vertex::new_with_uuid(*vertex.point(), vertex.uuid(), vertex.data()); + let mut snapshot_vertex = Vertex::from_validated_point_with_uuid( + *vertex.point(), + vertex.uuid(), + vertex.data(), + ); snapshot_vertex.set_incident_simplex(vertex.incident_simplex()); snapshot_vertex }) @@ -904,7 +910,7 @@ impl<'a, U, V, const D: usize> TdsSnapshot<&'a U, &'a V, D> { let simplices = tds .simplices() .map(|(_simplex_key, simplex)| { - TdsSnapshotSimplex::from_simplex_with_data(tds, simplex, simplex.data()) + TdsSnapshotSimplex::try_from_simplex_with_data(tds, simplex, simplex.data()) }) .collect::, TdsSnapshotError>>()?; @@ -1177,7 +1183,7 @@ where where S: serde::Serializer, { - TdsSnapshot::from_tds(self) + TdsSnapshot::try_from_tds(self) .map_err(serde::ser::Error::custom)? .into_raw() .serialize(serializer) @@ -1254,7 +1260,7 @@ mod tests { U: Copy, V: Copy, { - TdsSnapshot::from_tds_owned(tds) + TdsSnapshot::try_from_tds_owned(tds) .expect("TDS should snapshot") .into_raw() } @@ -2091,7 +2097,7 @@ mod tests { .uuid(); tds.clear_all_neighbors(); - let err = TdsSnapshot::from_tds(&tds) + let err = TdsSnapshot::try_from_tds(&tds) .expect_err("snapshotting a TDS without assigned neighbors should fail"); assert_matches!( @@ -2446,7 +2452,7 @@ mod tests { ]) .expect("fixture neighbor arity should match"); - let err = TdsSnapshot::from_tds(&tds) + let err = TdsSnapshot::try_from_tds(&tds) .expect_err("snapshotting dangling runtime neighbor key should fail"); assert_matches!(err, TdsSnapshotError::SourceValidationFailed { .. }); diff --git a/src/core/validation.rs b/src/core/validation.rs index 4b5819a6..db2577a1 100644 --- a/src/core/validation.rs +++ b/src/core/validation.rs @@ -87,7 +87,9 @@ //! with the correct boundary behavior (a necessary condition), but does not attempt to //! distinguish spheres/balls from other manifolds (not sufficient in general). -use crate::core::algorithms::incremental_insertion::InsertionError; +use crate::core::algorithms::incremental_insertion::{ + InsertionError, InsertionTopologyValidationContext, +}; use crate::core::collections::{ FacetToSimplicesMap, FastHashSet, SimplexKeyBuffer, SimplexKeySet, fast_hash_set_with_capacity, }; @@ -1304,7 +1306,7 @@ where match err { InvariantError::Tds(tds_err) => InsertionError::TopologyValidation(tds_err), InvariantError::Triangulation(tri_err) => InsertionError::TopologyValidationFailed { - message: "Topology validation failed".to_string(), + context: InsertionTopologyValidationContext::InvariantConversion, source: tri_err, }, InvariantError::Delaunay(dt_err) => { @@ -1588,7 +1590,7 @@ fn start_insertion_timing(telemetry_mode: InsertionTelemetryMode) -> Option DelaunayTriangulationValidationError { + let _ = message; DelaunayTriangulationValidationError::VerificationFailed { source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { - message: message.to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }) .into(), } @@ -1859,10 +1864,8 @@ mod tests { #[test] fn triangulation_validation_error_try_from_manifold_error_preserves_detail() { - let tds_err = TdsError::InvalidNeighbors { - reason: NeighborValidationError::Other { - message: "unit test".to_string(), - }, + let tds_err = TdsError::InconsistentDataStructure { + message: "unit test".to_string(), }; assert_eq!( @@ -2277,7 +2280,7 @@ mod tests { vertex_uuid: Uuid::nil(), }; let error = InsertionError::TopologyValidationFailed { - message: "outer".to_string(), + context: InsertionTopologyValidationContext::InvariantConversion, source: inner.clone(), }; assert_eq!( @@ -3145,7 +3148,7 @@ mod tests { let mut tri = Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); - let invalid_vertex: Vertex<(), 3> = Vertex::new_with_uuid( + let invalid_vertex: Vertex<(), 3> = Vertex::from_validated_point_with_uuid( Point::try_new([0.25, 0.25, 0.25]).expect("finite point coordinates"), Uuid::nil(), None, diff --git a/src/core/vertex.rs b/src/core/vertex.rs index 245a1e3e..3b46f561 100644 --- a/src/core/vertex.rs +++ b/src/core/vertex.rs @@ -620,7 +620,7 @@ impl Vertex { ) -> Result { validate_uuid(&uuid)?; - Ok(Self::new_with_uuid(point, uuid, data)) + Ok(Self::from_validated_point_with_uuid(point, uuid, data)) } /// Creates a vertex with a UUID already known to be valid. @@ -640,7 +640,11 @@ impl Vertex { /// # Returns /// /// A new `Vertex` with the specified UUID and data. - pub(crate) const fn new_with_uuid(point: Point, uuid: Uuid, data: Option) -> Self { + pub(crate) const fn from_validated_point_with_uuid( + point: Point, + uuid: Uuid, + data: Option, + ) -> Self { Self { point, uuid, @@ -1013,12 +1017,12 @@ mod tests { #[test] fn test_try_into_hashmap_rejects_duplicate_uuid() { let uuid = make_uuid(); - let first: Vertex<(), 2> = Vertex::new_with_uuid( + let first: Vertex<(), 2> = Vertex::from_validated_point_with_uuid( Point::try_new([0.0, 0.0]).expect("finite point coordinates"), uuid, None, ); - let second: Vertex<(), 2> = Vertex::new_with_uuid( + let second: Vertex<(), 2> = Vertex::from_validated_point_with_uuid( Point::try_new([1.0, 0.0]).expect("finite point coordinates"), uuid, None, diff --git a/src/delaunay/builder.rs b/src/delaunay/builder.rs index eceabc63..8bc30009 100644 --- a/src/delaunay/builder.rs +++ b/src/delaunay/builder.rs @@ -151,7 +151,7 @@ use crate::core::collections::{ FastHashMap, MAX_PRACTICAL_DIMENSION_SIZE, PeriodicOffsetBuffer, SmallBuffer, Uuid, VertexKeySet, }; -use crate::core::construction::TriangulationConstructionError; +use crate::core::construction::{FinalTopologyValidationContext, TriangulationConstructionError}; use crate::core::operations::InsertionOutcome; use crate::core::simplex::{Simplex, SimplexValidationError}; use crate::core::tds::{ @@ -172,7 +172,10 @@ use crate::topology::traits::topological_space::{ GlobalTopology, TopologyKind, ToroidalConstructionMode, ToroidalDomain, ToroidalDomainError, }; use crate::triangulation::DelaunayTriangulation; -use crate::validation::{DelaunayTriangulationValidationError, DelaunayVerificationErrorKind}; +use crate::validation::{ + DelaunayTriangulationCandidate, DelaunayTriangulationValidationError, + DelaunayTriangulationValidationProof, DelaunayVerificationErrorKind, +}; use num_traits::ToPrimitive; use rand::SeedableRng; use rand::rngs::StdRng; @@ -682,8 +685,6 @@ pub enum ExplicitDelaunayValidationErrorKind { Triangulation, /// Level 4 Delaunay verification failed. VerificationFailed, - /// Legacy string-only repair validation failed. - RepairFailed, /// Typed repair validation failed. RepairOperationFailed, } @@ -720,11 +721,13 @@ pub enum ExplicitDelaunayValidationSourceKind { /// DelaunayVerificationErrorKind, ExplicitDelaunayValidationError, /// ExplicitDelaunayValidationErrorKind, ExplicitDelaunayValidationSourceKind, /// }; -/// use delaunay::prelude::repair::DelaunayRepairError; +/// use delaunay::prelude::repair::{ +/// DelaunayRepairError, DelaunayRepairPostconditionFailure, +/// }; /// /// let source = DelaunayTriangulationValidationError::VerificationFailed { /// source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { -/// message: "non-Delaunay facet".to_string(), +/// reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), /// }) /// .into(), /// }; @@ -765,9 +768,6 @@ impl From for ExplicitDelaunayValidationEr DelaunayTriangulationValidationError::VerificationFailed { .. } => { ExplicitDelaunayValidationErrorKind::VerificationFailed } - DelaunayTriangulationValidationError::RepairFailed { .. } => { - ExplicitDelaunayValidationErrorKind::RepairFailed - } DelaunayTriangulationValidationError::RepairOperationFailed { .. } => { ExplicitDelaunayValidationErrorKind::RepairOperationFailed } @@ -785,7 +785,6 @@ impl From for ExplicitDelaunayValidationEr DelaunayTriangulationValidationError::RepairOperationFailed { source, .. } => Some( ExplicitDelaunayValidationSourceKind::Repair(source.as_ref().into()), ), - DelaunayTriangulationValidationError::RepairFailed { .. } => None, }; Self { kind, @@ -828,11 +827,18 @@ impl From for ExplicitDelaunayValidationEr /// ]; /// let simplices = vec![vec![0, 1]]; // Wrong arity for 2D (needs 3 vertices) /// +/// let Err(err) = +/// DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) +/// else { +/// panic!("bad simplex specs should be rejected"); +/// }; /// std::assert_matches!( -/// DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices).build::<()>(), -/// Err(DelaunayTriangulationConstructionError::ExplicitConstruction( -/// ExplicitConstructionError::InvalidSimplexArity { simplex_index: 0, actual: 2, expected: 3 }, -/// )) +/// err, +/// ExplicitConstructionError::InvalidSimplexArity { +/// simplex_index: 0, +/// actual: 2, +/// expected: 3, +/// } /// ); /// # Ok(()) /// # } @@ -965,6 +971,67 @@ pub enum ExplicitConstructionError { }, } +#[derive(Clone, Copy)] +struct ValidatedExplicitSimplices<'v> { + specs: &'v [Vec], +} + +impl<'v> ValidatedExplicitSimplices<'v> { + fn try_new( + vertex_count: usize, + specs: &'v [Vec], + ) -> Result { + validate_explicit_simplex_specs::(vertex_count, specs)?; + Ok(Self::from_validated_specs(specs)) + } + + const fn from_validated_specs(specs: &'v [Vec]) -> Self { + Self { specs } + } + + const fn as_slice(self) -> &'v [Vec] { + self.specs + } +} + +/// Validates explicit simplex specifications before storing them in a builder. +fn validate_explicit_simplex_specs( + vertex_count: usize, + simplices: &[Vec], +) -> Result<(), ExplicitConstructionError> { + if simplices.is_empty() { + return Err(ExplicitConstructionError::EmptySimplices); + } + + for (simplex_idx, simplex_spec) in simplices.iter().enumerate() { + if simplex_spec.len() != D + 1 { + return Err(ExplicitConstructionError::InvalidSimplexArity { + simplex_index: simplex_idx, + actual: simplex_spec.len(), + expected: D + 1, + }); + } + for (i, &vi) in simplex_spec.iter().enumerate() { + if vi >= vertex_count { + return Err(ExplicitConstructionError::IndexOutOfBounds { + simplex_index: simplex_idx, + vertex_index: vi, + bound: vertex_count, + }); + } + for &vj in &simplex_spec[i + 1..] { + if vi == vj { + return Err(ExplicitConstructionError::DuplicateVertexInSimplex { + simplex_index: simplex_idx, + }); + } + } + } + } + + Ok(()) +} + // ============================================================================= // BUILDER STRUCT // ============================================================================= @@ -1025,8 +1092,7 @@ pub struct DelaunayTriangulationBuilder<'v, U, const D: usize> { /// /// When set, the builder constructs a triangulation from the given vertices and /// simplices directly, bypassing point-insertion-based Delaunay construction. - /// Each inner slice must contain exactly D+1 vertex indices. - explicit_simplices: Option<&'v [Vec]>, + explicit_simplices: Option>, /// Runtime global topology metadata. /// /// When set to a non-Euclidean topology (e.g. `Toroidal`), Euler characteristic @@ -1110,6 +1176,16 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, U, D> { /// explicit connectivity is rejected because it requires Level 4 handling /// that is not available for quotient meshes. /// + /// # Errors + /// + /// Returns [`ExplicitConstructionError::EmptySimplices`] when no simplices + /// are provided, [`ExplicitConstructionError::InvalidSimplexArity`] when a + /// simplex does not contain `D + 1` vertex indices, + /// [`ExplicitConstructionError::IndexOutOfBounds`] when a simplex references + /// a missing vertex, or + /// [`ExplicitConstructionError::DuplicateVertexInSimplex`] when a simplex + /// repeats a vertex index. + /// /// # Examples /// /// ```rust @@ -1123,6 +1199,8 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, U, D> { /// # #[error(transparent)] /// # Source(#[from] DelaunayTriangulationConstructionError), /// # #[error(transparent)] + /// # Explicit(#[from] ExplicitConstructionError), + /// # #[error(transparent)] /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { @@ -1134,42 +1212,52 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, U, D> { /// ]; /// let simplices = vec![vec![0, 1, 2], vec![0, 2, 3]]; /// - /// let dt = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + /// let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices)? /// .build::<()>()?; /// /// assert_eq!(dt.number_of_vertices(), 4); /// assert_eq!(dt.number_of_simplices(), 2); /// /// let bad_simplices = vec![vec![0, 1]]; // Wrong arity for a 2D simplex. - /// std::assert_matches!( - /// DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &bad_simplices).build::<()>(), - /// Err(DelaunayTriangulationConstructionError::ExplicitConstruction( - /// ExplicitConstructionError::InvalidSimplexArity { .. }, - /// )) - /// ); + /// let Err(err) = + /// DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &bad_simplices) + /// else { + /// panic!("bad simplex specs should be rejected"); + /// }; + /// std::assert_matches!(err, ExplicitConstructionError::InvalidSimplexArity { .. }); /// # Ok(()) /// # } /// ``` - #[must_use] - pub fn from_vertices_and_simplices( + pub fn try_from_vertices_and_simplices( vertices: &'v [Vertex], simplices: &'v [Vec], - ) -> Self { - Self::from_vertices_and_simplices_generic(vertices, simplices) + ) -> Result { + Self::try_from_vertices_and_simplices_generic(vertices, simplices) } /// Creates a builder from explicit vertex and simplex specifications. /// /// This constructs a triangulation from the given connectivity without /// Delaunay point insertion. /// - /// The explicit connectivity is still validated when - /// [`build`](Self::build) or [`build_with_kernel`](Self::build_with_kernel) - /// is called. Euclidean explicit meshes are checked at Levels 1–4, including - /// the Delaunay empty-circumsphere property. Non-Euclidean explicit - /// connectivity is rejected because there is no successful Levels 1–3-only + /// Simplex arity, bounds, and duplicate indices are validated before the + /// builder stores the explicit connectivity. Euclidean explicit meshes are + /// checked at Levels 1–4 during [`build`](Self::build) or + /// [`build_with_kernel`](Self::build_with_kernel), including the Delaunay + /// empty-circumsphere property. Non-Euclidean explicit connectivity is + /// rejected at build time because there is no successful Levels 1–3-only /// path for the public `DelaunayTriangulation` wrapper; quotient meshes need /// Level 4 handling before they can be accepted. /// + /// # Errors + /// + /// Returns [`ExplicitConstructionError::EmptySimplices`] when no simplices + /// are provided, [`ExplicitConstructionError::InvalidSimplexArity`] when a + /// simplex does not contain `D + 1` vertex indices, + /// [`ExplicitConstructionError::IndexOutOfBounds`] when a simplex references + /// a missing vertex, or + /// [`ExplicitConstructionError::DuplicateVertexInSimplex`] when a simplex + /// repeats a vertex index. + /// /// # Examples /// /// ```rust @@ -1182,6 +1270,8 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, U, D> { /// # #[error(transparent)] /// # Construction(#[from] delaunay::prelude::construction::DelaunayTriangulationConstructionError), /// # #[error(transparent)] + /// # Explicit(#[from] delaunay::prelude::construction::ExplicitConstructionError), + /// # #[error(transparent)] /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { @@ -1192,7 +1282,7 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, U, D> { /// ]; /// let simplices = vec![vec![0, 1, 2]]; /// - /// let dt = DelaunayTriangulationBuilder::from_vertices_and_simplices_generic(&vertices, &simplices) + /// let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices_generic(&vertices, &simplices)? /// .build::<()>()?; /// /// assert_eq!(dt.number_of_vertices(), 3); @@ -1200,53 +1290,15 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, U, D> { /// # Ok(()) /// # } /// ``` - #[must_use] - pub fn from_vertices_and_simplices_generic( + pub fn try_from_vertices_and_simplices_generic( vertices: &'v [Vertex], simplices: &'v [Vec], - ) -> Self { + ) -> Result { + let explicit_simplices = + ValidatedExplicitSimplices::try_new::(vertices.len(), simplices)?; let mut builder = Self::new(vertices); - builder.explicit_simplices = Some(simplices); - builder - } - - /// Creates a builder from a vertex slice. - /// - /// For raw coordinate arrays, prefer [`new`](DelaunayTriangulationBuilder::new) which - /// infers all type parameters without explicit annotations. Use `from_vertices` - /// when callers already have validated [`Vertex`] values. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, Vertex, - /// }; - /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::construction::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices: Vec> = vec![ - /// Vertex::try_new([0.0, 0.0])?, - /// Vertex::try_new([1.0, 0.0])?, - /// Vertex::try_new([0.0, 1.0])?, - /// ]; - /// - /// let dt = DelaunayTriangulationBuilder::from_vertices(&vertices) - /// .build::<()>()?; - /// - /// assert_eq!(dt.number_of_vertices(), 3); - /// # Ok(()) - /// # } - /// ``` - #[must_use] - pub fn from_vertices(vertices: &'v [Vertex]) -> Self { - Self::new(vertices) + builder.explicit_simplices = Some(explicit_simplices); + Ok(builder) } /// Enables periodic toroidal topology via the image-point method. @@ -1276,6 +1328,8 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, U, D> { /// # #[error(transparent)] /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), /// # #[error(transparent)] + /// # Explicit(#[from] delaunay::prelude::construction::ExplicitConstructionError), + /// # #[error(transparent)] /// # Topology(#[from] delaunay::prelude::construction::ToroidalDomainError), /// # } /// # fn main() -> Result<(), ExampleError> { @@ -1529,6 +1583,8 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, U, D> { /// # #[error(transparent)] /// # Construction(#[from] delaunay::prelude::construction::DelaunayTriangulationConstructionError), /// # #[error(transparent)] + /// # Explicit(#[from] delaunay::prelude::construction::ExplicitConstructionError), + /// # #[error(transparent)] /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), /// # #[error(transparent)] /// # Topology(#[from] delaunay::prelude::construction::ToroidalDomainError), @@ -1546,7 +1602,7 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, U, D> { /// ToroidalConstructionMode::Explicit, /// ) /// ?; - /// let result = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + /// let result = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices)? /// .global_topology(topology) /// .build::<()>(); /// @@ -1739,7 +1795,7 @@ where }, ) })?; - let new_vertex = Vertex::new_with_uuid(new_point, v.uuid(), v.data); + let new_vertex = Vertex::from_validated_point_with_uuid(new_point, v.uuid(), v.data); out.push(new_vertex); } @@ -1873,7 +1929,7 @@ where return Self::build_explicit( kernel, self.vertices, - simplices, + simplices.as_slice(), self.topology_guarantee, self.global_topology, ); @@ -1951,15 +2007,13 @@ where })?; dt.as_triangulation().validate().map_err(|e| { TriangulationConstructionError::FinalTopologyValidation { - message: "Periodic quotient failed final Levels 1-3 topology validation" - .to_string(), + context: FinalTopologyValidationContext::PeriodicQuotientTopology, source: e.into(), } })?; dt.is_valid().map_err(|e| { TriangulationConstructionError::FinalTopologyValidation { - message: "Periodic quotient failed final Level 4 Delaunay validation" - .to_string(), + context: FinalTopologyValidationContext::PeriodicQuotientDelaunay, source: InvariantError::Delaunay(e).into(), } })?; @@ -1983,7 +2037,7 @@ where /// 2. Build a `Tds`: insert all vertices, then insert simplices from the specifications. /// 3. Compute adjacency via `assign_neighbors()`. /// 4. Assign incident simplices via `assign_incident_simplices()`. - /// 5. Wrap in `DelaunayTriangulation` via `from_tds_with_topology_guarantee`. + /// 5. Wrap in a validation candidate. /// 6. Normalize coherent orientation and promote to positive canonical sign /// via `normalize_and_promote_positive_orientation()`. /// 7. Reject non-Euclidean explicit connectivity until Level 4 quotient @@ -2004,7 +2058,7 @@ where K: Kernel, V: DataType, { - Self::validate_explicit_simplex_specs(vertices.len(), simplices)?; + validate_explicit_simplex_specs::(vertices.len(), simplices)?; Self::reject_explicit_non_euclidean_topology(global_topology)?; let vertex_count = vertices.len(); @@ -2061,16 +2115,13 @@ where // Construct the DT first so the Triangulation-layer helpers // (orientation promotion, topology checks) operate on the assembled // complex. - let mut dt = DelaunayTriangulation::from_tds_with_topology_guarantee( - tds, - kernel.clone(), - topology_guarantee, - ); + let mut candidate = + DelaunayTriangulationCandidate::assemble(tds, kernel.clone(), topology_guarantee); // Set global topology metadata before validation so that // validate_topology_core() uses the correct Euler characteristic // expectation (e.g. χ = 0 for toroidal instead of χ = 2 for sphere). - dt.set_global_topology(global_topology); + candidate.set_global_topology(global_topology); // --- Normalize orientation and promote to positive --- // @@ -2080,7 +2131,7 @@ where // 3. Bounded per-simplex promotion passes for FP-precision edge cases // This ensures the returned DT has positive geometric orientation, // matching the invariant expected by validate_geometric_simplex_orientation. - dt.tri + candidate .normalize_and_promote_positive_orientation() .map_err( |source| ExplicitConstructionError::OrientationNormalization { @@ -2090,7 +2141,7 @@ where // Level 1–2: TDS structural validation (mappings, neighbors, facet // sharing, coherent orientation, etc.). - if let Err(e) = dt.tri.tds.validate() { + if let Err(e) = candidate.validate_tds_structure() { return Err( ExplicitConstructionError::StructuralValidation { source: e.into() }.into(), ); @@ -2102,12 +2153,12 @@ where // them. We call `is_valid_topology_only()` which covers all these; // the only check we intentionally omit is // `validate_geometric_simplex_orientation`. - if let Err(e) = dt.tri.is_valid_topology_only() { + if let Err(e) = candidate.validate_topology_only() { return Err(ExplicitConstructionError::TopologyValidation { source: e.into() }.into()); } // Completion-time PL-manifold check (vertex links) if required. - if let Err(e) = dt.tri.validate_at_completion() { + if let Err(e) = candidate.validate_at_completion() { return Err( ExplicitConstructionError::CompletionValidation { source: e.into() }.into(), ); @@ -2120,53 +2171,15 @@ where // may tolerate near-degenerate simplices from flip-based repair, // explicit construction should not silently accept geometrically // collapsed simplices supplied by the user. - if let Err(e) = dt.tri.validate_geometric_nondegeneracy() { + if let Err(e) = candidate.validate_geometric_nondegeneracy() { return Err( ExplicitConstructionError::GeometricNondegeneracy { source: e.into() }.into(), ); } - Self::enforce_explicit_delaunay_property(&dt)?; + let proof = Self::enforce_explicit_delaunay_property(&candidate)?; - Ok(dt) - } - - /// Validates explicit simplex specifications before constructing a TDS. - fn validate_explicit_simplex_specs( - vertex_count: usize, - simplices: &[Vec], - ) -> Result<(), ExplicitConstructionError> { - if simplices.is_empty() { - return Err(ExplicitConstructionError::EmptySimplices); - } - - for (simplex_idx, simplex_spec) in simplices.iter().enumerate() { - if simplex_spec.len() != D + 1 { - return Err(ExplicitConstructionError::InvalidSimplexArity { - simplex_index: simplex_idx, - actual: simplex_spec.len(), - expected: D + 1, - }); - } - for (i, &vi) in simplex_spec.iter().enumerate() { - if vi >= vertex_count { - return Err(ExplicitConstructionError::IndexOutOfBounds { - simplex_index: simplex_idx, - vertex_index: vi, - bound: vertex_count, - }); - } - for &vj in &simplex_spec[i + 1..] { - if vi == vj { - return Err(ExplicitConstructionError::DuplicateVertexInSimplex { - simplex_index: simplex_idx, - }); - } - } - } - } - - Ok(()) + Ok(candidate.into_validated_delaunay(proof)) } /// Enforces Level 4 validation before returning the Delaunay wrapper. @@ -2176,13 +2189,13 @@ where /// this API boundary. Explicit non-Euclidean topology is rejected earlier in /// `build_explicit` until a Level 4 validator exists for quotient connectivity. fn enforce_explicit_delaunay_property( - dt: &DelaunayTriangulation, - ) -> Result<(), DelaunayTriangulationConstructionError> + candidate: &DelaunayTriangulationCandidate, + ) -> Result where K: Kernel, V: DataType, { - dt.is_valid().map_err(|source| { + candidate.validate_delaunay_property().map_err(|source| { ExplicitConstructionError::DelaunayValidation { source: source.into(), } @@ -2375,7 +2388,8 @@ where })?; if is_canonical { image_uuid_to_canonical_with_offset.insert(v.uuid(), (v.uuid(), [0_i8; D])); - let canonical_v = Vertex::new_with_uuid(new_point, v.uuid(), v.data); + let canonical_v = + Vertex::from_validated_point_with_uuid(new_point, v.uuid(), v.data); expanded.push(canonical_v); } else { let image_v: Vertex = Vertex::from_validated_point(new_point, None); @@ -3183,11 +3197,14 @@ where .into()); } - Ok(DelaunayTriangulation::from_tds_with_topology_guarantee( - tds_mut, - kernel.clone(), - topology_guarantee, - )) + let candidate = + DelaunayTriangulationCandidate::assemble(tds_mut, kernel.clone(), topology_guarantee); + let proof = candidate.validate_tds_structure().map_err(|e| { + TriangulationConstructionError::GeometricDegeneracy { + message: format!("Periodic quotient TDS invalid before return: {e}"), + } + })?; + Ok(candidate.into_structurally_valid_delaunay(proof)) } } @@ -3199,7 +3216,10 @@ where mod tests { use super::*; use crate::construction::{DelaunayConstructionFailure, InsertionOrderStrategy}; - use crate::core::algorithms::flips::DelaunayRepairError; + use crate::core::algorithms::flips::{ + DelaunayRepairError, DelaunayRepairHeuristicRebuildFailure, + DelaunayRepairPostconditionFailure, + }; use crate::core::algorithms::incremental_insertion::{ CavityFillingError, DelaunayRepairFailureContext, HullExtensionReason, NeighborWiringError, SpatialIndexConstructionFailure, @@ -3252,9 +3272,12 @@ mod tests { fn synthetic_delaunay_verification_error( message: &str, ) -> DelaunayTriangulationValidationError { + let _ = message; DelaunayTriangulationValidationError::VerificationFailed { source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { - message: message.to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }) .into(), } @@ -3335,7 +3358,11 @@ mod tests { DelaunayTriangulationValidationError::RepairOperationFailed { operation: DelaunayRepairOperation::VertexRemoval, source: Box::new(DelaunayRepairError::HeuristicRebuildFailed { - message: "rebuild failed".to_string(), + reason: Box::new( + DelaunayRepairHeuristicRebuildFailure::RecursionDepthExceeded { + max_depth: 1, + }, + ), }), }, ); @@ -3432,8 +3459,10 @@ mod tests { ); assert_explicit_tds_error_kind( TdsError::InvalidNeighbors { - reason: NeighborValidationError::Other { - message: "neighbor invariant failed".to_string(), + reason: NeighborValidationError::NonPeriodicSelfNeighbor { + simplex_key, + simplex_uuid: uuid, + facet_index: 0, }, }, ExplicitTdsErrorKind::InvalidNeighbors, @@ -3701,7 +3730,9 @@ mod tests { assert_explicit_insertion_error( InsertionError::DelaunayRepairFailed { source: Box::new(DelaunayRepairError::PostconditionFailed { - message: "remaining violation".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }), context: DelaunayRepairFailureContext::LocalRepair, }, @@ -3729,7 +3760,7 @@ mod tests { degree_one_vertices: 1, connected: false, }, - message: "scoped topology validation".to_string(), + context: crate::InsertionTopologyValidationContext::PostInsertion, }, ExplicitInsertionErrorKind::TopologyValidationFailed, Some(InsertionErrorSourceKind::Triangulation( @@ -4067,13 +4098,13 @@ mod tests { } // ------------------------------------------------------------------------- - // Generic path (from_vertices) + // Generic builder path // ------------------------------------------------------------------------- - /// `from_vertices` is required when vertices carry user data (`U ≠ ()`). - /// Verify that the data is preserved after canonicalized toroidal wrapping. + /// `new` accepts vertices carrying user data (`U ≠ ()`). Verify that the + /// data is preserved after canonicalized toroidal wrapping. #[test] - fn test_builder_from_vertices_preserves_vertex_data() { + fn test_builder_new_preserves_vertex_data() { let vertices: Vec> = vec![ Vertex::try_new_with_data([0.2_f64, 0.3], 1_i32).unwrap(), Vertex::try_new_with_data([1.8_f64, 0.1], 2_i32).unwrap(), // x → 0.8 diff --git a/src/delaunay/construction.rs b/src/delaunay/construction.rs index e3302ec6..eeae6038 100644 --- a/src/delaunay/construction.rs +++ b/src/delaunay/construction.rs @@ -57,8 +57,8 @@ use crate::core::algorithms::flips::{ repair_delaunay_with_flips_k2_k3, }; use crate::core::algorithms::incremental_insertion::{ - CavityFillingError, HullExtensionReason, InsertionError, SpatialIndexConstructionFailure, - TdsConstructionFailure, + CavityFillingError, HullExtensionReason, InsertionError, InsertionTopologyValidationContext, + SpatialIndexConstructionFailure, TdsConstructionFailure, }; use crate::core::algorithms::locate::{ConflictError, LocateError}; use crate::core::collections::spatial_hash_grid::HashGridIndex; @@ -66,7 +66,7 @@ use crate::core::collections::{ FastHashSet, FastHasher, MAX_PRACTICAL_DIMENSION_SIZE, SecureHashMap, SimplexKeyBuffer, SmallBuffer, }; -use crate::core::construction::TriangulationConstructionError; +use crate::core::construction::{FinalTopologyValidationContext, TriangulationConstructionError}; use crate::core::insertion::record_duplicate_detection_metrics; use crate::core::operations::{ DelaunayInsertionState, InsertionOutcome, InsertionResult, InsertionStatistics, @@ -408,10 +408,10 @@ pub enum DelaunayConstructionFailure { }, /// Level 3 topology validation failed during insertion. - #[error("topology validation failed during insertion: {message}: {source}")] + #[error("topology validation failed during insertion: {context}: {source}")] InsertionTopologyValidation { /// High-level insertion context. - message: String, + context: InsertionTopologyValidationContext, /// Underlying topology validation error. #[source] source: TriangulationValidationError, @@ -429,10 +429,10 @@ pub enum DelaunayConstructionFailure { }, /// Final topology validation failed after construction. - #[error("final topology validation failed after construction: {message}: {source}")] + #[error("final topology validation failed after construction: {context}: {source}")] FinalTopologyValidation { /// Validation failure detail. - message: String, + context: FinalTopologyValidationContext, /// Underlying validation error. #[source] source: crate::core::tds::InvariantErrorSummary, @@ -502,8 +502,8 @@ impl From for DelaunayConstructionFailure { TriangulationConstructionError::InsertionDelaunayValidation { source } => { Self::InsertionDelaunayValidation { source } } - TriangulationConstructionError::InsertionTopologyValidation { message, source } => { - Self::InsertionTopologyValidation { message, source } + TriangulationConstructionError::InsertionTopologyValidation { context, source } => { + Self::InsertionTopologyValidation { context, source } } TriangulationConstructionError::LocalRepairBudgetExceeded { max_simplices_removed, @@ -512,8 +512,8 @@ impl From for DelaunayConstructionFailure { max_simplices_removed, attempted, }, - TriangulationConstructionError::FinalTopologyValidation { message, source } => { - Self::FinalTopologyValidation { message, source } + TriangulationConstructionError::FinalTopologyValidation { context, source } => { + Self::FinalTopologyValidation { context, source } } } } @@ -3922,7 +3922,7 @@ where ); if let Err(err) = validation_result { return Err(TriangulationConstructionError::FinalTopologyValidation { - message: "topology validation failed after construction".to_string(), + context: FinalTopologyValidationContext::ConstructionFinalize, source: err.into(), } .into()); @@ -4790,8 +4790,8 @@ where InsertionError::DelaunayValidationFailed { source } => { TriangulationConstructionError::InsertionDelaunayValidation { source } } - InsertionError::TopologyValidationFailed { message, source } => { - TriangulationConstructionError::InsertionTopologyValidation { message, source } + InsertionError::TopologyValidationFailed { context, source } => { + TriangulationConstructionError::InsertionTopologyValidation { context, source } } InsertionError::MaxSimplicesRemovedExceeded { max_simplices_removed, @@ -5016,7 +5016,8 @@ fn construction_retry_trace_enabled() -> bool { mod tests { use super::*; use crate::core::algorithms::flips::{ - DelaunayRepairDiagnostics, DelaunayRepairVerificationContext, FlipContextError, + DelaunayRepairDiagnostics, DelaunayRepairOrientationCanonicalizationFailure, + DelaunayRepairPostconditionFailure, DelaunayRepairVerificationContext, FlipContextError, FlipPredicateError, FlipPredicateOperation, RepairQueueOrder, }; use crate::core::algorithms::incremental_insertion::{ @@ -5054,9 +5055,12 @@ mod tests { fn synthetic_delaunay_verification_error( message: &str, ) -> DelaunayTriangulationValidationError { + let _ = message; DelaunayTriangulationValidationError::VerificationFailed { source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { - message: message.to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }) .into(), } @@ -6798,7 +6802,7 @@ mod tests { assert!(TestDelaunay::<4>::can_soft_fail(&nonconvergent)); let postcondition = DelaunayRepairError::PostconditionFailed { - message: "unresolved facet".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }; assert!(TestDelaunay::<4>::can_soft_fail(&postcondition)); @@ -6822,7 +6826,13 @@ mod tests { assert!(!TestDelaunay::<4>::can_soft_fail(&verification_error)); let canonicalization_error = DelaunayRepairError::OrientationCanonicalizationFailed { - message: "after flip repair: broken orientation".to_string(), + reason: Box::new( + DelaunayRepairOrientationCanonicalizationFailure::AfterFlipRepair { + source: Box::new(InsertionError::DuplicateCoordinates { + coordinates: CoordinateValues::from([0.0, 0.0, 0.0]), + }), + }, + ), }; assert!(!TestDelaunay::<4>::can_soft_fail(&canonicalization_error)); @@ -6994,7 +7004,7 @@ mod tests { #[test] fn test_map_orientation_canonicalization_error_isolated_vertex_is_internal() { let error = InsertionError::TopologyValidationFailed { - message: "test".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::IsolatedVertex { vertex_key: VertexKey::from(KeyData::from_ffi(1)), vertex_uuid: Uuid::nil(), @@ -7013,7 +7023,7 @@ mod tests { #[test] fn test_map_orientation_canonicalization_error_topology_validation_failed_is_internal() { let error = InsertionError::TopologyValidationFailed { - message: "post-insertion".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::EulerCharacteristicMismatch { computed: 3, expected: 2, @@ -7101,7 +7111,9 @@ mod tests { }, InsertionError::DelaunayRepairFailed { source: Box::new(DelaunayRepairError::PostconditionFailed { - message: "test".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }), context: DelaunayRepairFailureContext::LocalRepair, }, @@ -7277,7 +7289,7 @@ mod tests { ); let topology = InsertionError::TopologyValidationFailed { - message: "test".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::EulerCharacteristicMismatch { computed: 3, expected: 2, @@ -7337,7 +7349,7 @@ mod tests { let vertex_key = VertexKey::from(KeyData::from_ffi(2)); let failure = DelaunayConstructionFailure::from( TriangulationConstructionError::InsertionTopologyValidation { - message: "post-insertion".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::IsolatedVertex { vertex_key, vertex_uuid: Uuid::nil(), @@ -7347,12 +7359,12 @@ mod tests { assert_matches!( failure, DelaunayConstructionFailure::InsertionTopologyValidation { - message, + context, source: TriangulationValidationError::IsolatedVertex { vertex_key: preserved_key, .. }, - } if message == "post-insertion" && preserved_key == vertex_key + } if context == InsertionTopologyValidationContext::PostInsertion && preserved_key == vertex_key ); } @@ -7535,7 +7547,7 @@ mod tests { let vertex_key = VertexKey::from(KeyData::from_ffi(1)); let insertion_err: DelaunayTriangulationConstructionError = TriangulationConstructionError::InsertionTopologyValidation { - message: "test".to_string(), + context: InsertionTopologyValidationContext::PostInsertion, source: TriangulationValidationError::IsolatedVertex { vertex_key, vertex_uuid: Uuid::nil(), @@ -7544,7 +7556,7 @@ mod tests { .into(); let final_err: DelaunayTriangulationConstructionError = TriangulationConstructionError::FinalTopologyValidation { - message: "test".to_string(), + context: FinalTopologyValidationContext::ConstructionFinalize, source: InvariantError::Triangulation( TriangulationValidationError::IsolatedVertex { vertex_key, diff --git a/src/delaunay/delaunayize.rs b/src/delaunay/delaunayize.rs index 85832456..4dec645c 100644 --- a/src/delaunay/delaunayize.rs +++ b/src/delaunay/delaunayize.rs @@ -56,7 +56,13 @@ // Re-export outcome/error field types so users can name the public contract // without reaching into lower-level modules. pub use crate::construction::DelaunayTriangulationConstructionError; -pub use crate::flips::{DelaunayRepairError, DelaunayRepairStats}; +pub use crate::flips::{ + DelaunayRepairError, DelaunayRepairHeuristicRebuildFailure, + DelaunayRepairHeuristicRebuildFailureKind, DelaunayRepairHeuristicVertexContext, + DelaunayRepairOrientationCanonicalizationFailure, + DelaunayRepairOrientationCanonicalizationFailureKind, DelaunayRepairPostconditionFailure, + DelaunayRepairStats, +}; pub use crate::tds::SimplexValidationError; pub use crate::{PlManifoldRepairError, PlManifoldRepairStats}; @@ -268,7 +274,7 @@ pub struct DelaunayizeOutcome { /// /// let err = DelaunayizeError::DelaunayRepairFailed { /// source: DelaunayRepairError::PostconditionFailed { -/// message: "still non-Delaunay after repair".to_string(), +/// reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), /// }, /// }; /// assert!(err.to_string().contains("Delaunay repair failed")); @@ -393,7 +399,7 @@ where { let vertices = tds .vertices() - .map(|(_, v)| Vertex::new_with_uuid(*v.point(), v.uuid(), v.data)) + .map(|(_, v)| Vertex::from_validated_point_with_uuid(*v.point(), v.uuid(), v.data)) .collect::>(); let simplex_data = collect_simplex_data(tds)?; Ok((vertices, simplex_data)) @@ -1037,7 +1043,7 @@ mod tests { #[test] fn test_repair_snapshot_error_source() { let source = DelaunayRepairError::PostconditionFailed { - message: "synthetic postcondition".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }; let snapshot_error = SimplexValidationError::VertexKeyNotFound { key: VertexKey::from(KeyData::from_ffi(0xBAD)), @@ -1070,7 +1076,7 @@ mod tests { simplices_removed: 4, }; let delaunay_source = DelaunayRepairError::PostconditionFailed { - message: "synthetic postcondition".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }; let restore_error = SimplexValidationError::VertexKeyNotFound { key: VertexKey::from(KeyData::from_ffi(0xBAD)), @@ -1159,7 +1165,7 @@ mod tests { #[test] fn test_delaunay_rebuild_error_mapping() { let source = DelaunayRepairError::PostconditionFailed { - message: "synthetic postcondition".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }; let rebuild_error = construction_error(); let restore_error = SimplexValidationError::VertexKeyNotFound { @@ -1331,7 +1337,7 @@ mod tests { let tds = &dt.as_triangulation().tds; let vertices: Vec<_> = tds .vertices() - .map(|(_, v)| Vertex::new_with_uuid(*v.point(), v.uuid(), v.data)) + .map(|(_, v)| Vertex::from_validated_point_with_uuid(*v.point(), v.uuid(), v.data)) .collect(); let simplex_data = collect_simplex_data(tds).unwrap(); @@ -1367,7 +1373,7 @@ mod tests { let rebuild_vertices: Vec<_> = tds .vertices() - .map(|(_, v)| Vertex::new_with_uuid(*v.point(), v.uuid(), v.data)) + .map(|(_, v)| Vertex::from_validated_point_with_uuid(*v.point(), v.uuid(), v.data)) .collect(); let simplex_data = collect_simplex_data(&tds).unwrap(); let kernel = AdaptiveKernel::new(); diff --git a/src/delaunay/flips.rs b/src/delaunay/flips.rs index 111d5130..66ac4a79 100644 --- a/src/delaunay/flips.rs +++ b/src/delaunay/flips.rs @@ -10,6 +10,9 @@ pub use crate::core::algorithms::flips::{ BistellarFlipKind, BistellarMove, ConstK, DelaunayRepairDiagnostics, DelaunayRepairError, + DelaunayRepairHeuristicRebuildFailure, DelaunayRepairHeuristicRebuildFailureKind, + DelaunayRepairHeuristicVertexContext, DelaunayRepairOrientationCanonicalizationFailure, + DelaunayRepairOrientationCanonicalizationFailureKind, DelaunayRepairPostconditionFailure, DelaunayRepairStats, DelaunayRepairVerificationContext, FlipContextError, FlipDirection, FlipEdgeAdjacencyError, FlipError, FlipInfo, FlipMutationError, FlipNeighborWiringError, FlipOrientationCheckStage, FlipPredicateError, FlipPredicateOperation, diff --git a/src/delaunay/insertion.rs b/src/delaunay/insertion.rs index e594b803..d0039591 100644 --- a/src/delaunay/insertion.rs +++ b/src/delaunay/insertion.rs @@ -24,6 +24,7 @@ use crate::core::algorithms::flips::{ }; use crate::core::algorithms::incremental_insertion::{ DelaunayRepairErrorSummary, DelaunayRepairFailureContext, InsertionError, + InsertionTopologyValidationContext, }; use crate::core::collections::spatial_hash_grid::HashGridIndex; use crate::core::collections::{FastHashSet, SimplexKeyBuffer}; @@ -38,15 +39,15 @@ use crate::geometry::kernel::Kernel; use crate::repair::{DelaunayRepairOperation, DelaunayRepairPolicy}; use crate::topology::manifold::{ManifoldError, validate_ridge_links_for_simplices}; use crate::triangulation::DelaunayTriangulation; +#[cfg(test)] +use crate::validation::DelaunayTriangulationCandidate; use crate::validation::DelaunayTriangulationValidationError; use std::env; -const RIDGE_LINK_REPAIR_VALIDATION_MESSAGE: &str = "Topology invalid after Delaunay repair"; - fn ridge_link_repair_validation_error(err: ManifoldError) -> InsertionError { match TriangulationValidationError::try_from(err) { Ok(source) => InsertionError::TopologyValidationFailed { - message: RIDGE_LINK_REPAIR_VALIDATION_MESSAGE.to_string(), + context: InsertionTopologyValidationContext::DelaunayRepair, source, }, Err(source) => InsertionError::TopologyValidation(source), @@ -1090,10 +1091,8 @@ mod tests { #[test] fn test_ridge_link_repair_validation_error_routes_tds_errors_to_tds_layer() { - let tds_err = TdsError::InvalidNeighbors { - reason: NeighborValidationError::Other { - message: "unit test".to_string(), - }, + let tds_err = TdsError::InconsistentDataStructure { + message: "unit test".to_string(), }; match ridge_link_repair_validation_error(ManifoldError::Tds(tds_err.clone())) { @@ -1111,8 +1110,8 @@ mod tests { }); match error { - InsertionError::TopologyValidationFailed { message, source } => { - assert_eq!(message, RIDGE_LINK_REPAIR_VALIDATION_MESSAGE); + InsertionError::TopologyValidationFailed { context, source } => { + assert_eq!(context, InsertionTopologyValidationContext::DelaunayRepair); assert_matches!( source, TriangulationValidationError::BoundaryRidgeMultiplicity { @@ -1509,11 +1508,12 @@ mod tests { fn test_validate_ridge_links_after_full_reseed_repair_uses_mutation_frontier() { init_tracing(); let (tds, incident_to_invalid_ridge, nonincident) = wedge_two_spheres_share_vertex_tds_2d(); - let dt = DelaunayTriangulation::from_tds_with_topology_guarantee( + let dt = DelaunayTriangulationCandidate::assemble( tds, AdaptiveKernel::new(), TopologyGuarantee::PLManifold, - ); + ) + .into_repairable_delaunay_for_test(); let stats = DelaunayRepairStats { flips_performed: 1, ..DelaunayRepairStats::default() diff --git a/src/delaunay/repair.rs b/src/delaunay/repair.rs index 3a193dca..73339146 100644 --- a/src/delaunay/repair.rs +++ b/src/delaunay/repair.rs @@ -13,7 +13,9 @@ #[cfg(test)] use crate::construction::test_hooks; use crate::core::algorithms::flips::{ - DelaunayRepairError, DelaunayRepairRun, DelaunayRepairStats, repair_delaunay_with_flips_k2_k3, + DelaunayRepairError, DelaunayRepairHeuristicRebuildFailure, + DelaunayRepairHeuristicVertexContext, DelaunayRepairOrientationCanonicalizationFailure, + DelaunayRepairRun, DelaunayRepairStats, repair_delaunay_with_flips_k2_k3, repair_delaunay_with_flips_k2_k3_run, }; use crate::core::collections::FastHasher; @@ -24,7 +26,10 @@ use crate::core::util::stable_hash_u64_slice; use crate::core::validation::TopologyGuarantee; use crate::core::vertex::Vertex; use crate::geometry::kernel::{ExactPredicates, Kernel, RobustKernel}; +use crate::geometry::traits::coordinate::CoordinateValues; use crate::triangulation::DelaunayTriangulation; +#[cfg(test)] +use crate::validation::DelaunayTriangulationCandidate; use rand::SeedableRng; use rand::seq::SliceRandom; use std::{ @@ -59,8 +64,10 @@ impl HeuristicRebuildRecursionGuard { }); if prior_depth >= MAX_HEURISTIC_REBUILD_DEPTH { return Err(DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild recursion depth exceeded {MAX_HEURISTIC_REBUILD_DEPTH}" + reason: Box::new( + DelaunayRepairHeuristicRebuildFailure::RecursionDepthExceeded { + max_depth: MAX_HEURISTIC_REBUILD_DEPTH, + }, ), }); } @@ -427,7 +434,11 @@ where self.tri .normalize_and_promote_positive_orientation() .map_err(|e| DelaunayRepairError::OrientationCanonicalizationFailed { - message: format!("after flip repair: {e}"), + reason: Box::new( + DelaunayRepairOrientationCanonicalizationFailure::AfterFlipRepair { + source: Box::new(e), + }, + ), }) } @@ -634,15 +645,23 @@ where let (candidate, stats, used_seeds) = self .rebuild_with_heuristic(seeds, max_flips) .map_err(|heuristic_err| { - let heuristic_message = match heuristic_err { - DelaunayRepairError::HeuristicRebuildFailed { message } => { - message + let heuristic = match heuristic_err { + DelaunayRepairError::HeuristicRebuildFailed { reason } => { + reason } - other => other.to_string(), + other => Box::new( + DelaunayRepairHeuristicRebuildFailure::UnexpectedRepairFailure { + source: Box::new(other), + }, + ), }; DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "primary repair failed ({primary_err}); robust fallback failed ({robust_err}); {heuristic_message}" + reason: Box::new( + DelaunayRepairHeuristicRebuildFailure::FallbackChainFailed { + primary: Box::new(primary_err.clone()), + robust: Box::new(robust_err), + heuristic, + }, ), } })?; @@ -674,7 +693,7 @@ where { let base_vertices = self.collect_vertices_for_rebuild(); - let mut last_error: Option = None; + let mut last_error: Option = None; for attempt in 0..HEURISTIC_REBUILD_ATTEMPTS { let seeds = if attempt == 0 { @@ -734,6 +753,11 @@ where for (idx, vertex) in vertices.into_iter().enumerate() { let uuid = vertex.uuid(); let coords = *vertex.point().coords(); + let vertex_context = DelaunayRepairHeuristicVertexContext { + index: idx, + vertex_uuid: uuid, + coordinates: CoordinateValues::from(coords), + }; let hint = candidate.insertion_state.last_inserted_simplex; let insert_detail = { @@ -747,10 +771,15 @@ where spatial_index.as_mut(), Some(idx), ) - .map_err(|e| DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild insertion failed at idx={idx} uuid={uuid} coords={coords:?}: {e}" - ), + .map_err(|e| { + DelaunayRepairError::HeuristicRebuildFailed { + reason: Box::new( + DelaunayRepairHeuristicRebuildFailure::InsertionFailed { + vertex: vertex_context.clone(), + source: Box::new(e), + }, + ), + } })? }; let repair_seed_simplices = insert_detail.repair_seed_simplices; @@ -773,24 +802,33 @@ where max_flips_override, ) .map_err(|e| DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild repair failed at idx={idx} uuid={uuid} coords={coords:?}: {e}" + reason: Box::new( + DelaunayRepairHeuristicRebuildFailure::RepairFailed { + vertex: vertex_context.clone(), + source: Box::new(e), + }, ), })?; } - candidate - .maybe_check_after_insertion() - .map_err(|e| DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild Delaunay check failed at idx={idx} uuid={uuid} coords={coords:?}: {e}" + candidate.maybe_check_after_insertion().map_err(|e| { + DelaunayRepairError::HeuristicRebuildFailed { + reason: Box::new( + DelaunayRepairHeuristicRebuildFailure::DelaunayCheckFailed { + vertex: vertex_context, + source: Box::new(e), + }, ), - })?; + } + })?; } InsertionOutcome::Skipped { error } => { return Err(DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild skipped vertex at idx={idx} uuid={uuid} coords={coords:?}: {error}" + reason: Box::new( + DelaunayRepairHeuristicRebuildFailure::SkippedVertex { + vertex: vertex_context, + source: Box::new(error), + }, ), }); } @@ -827,22 +865,24 @@ where match rebuild_attempt { Ok((candidate, stats)) => return Ok((candidate, stats, seeds)), Err(err) => { - last_error = Some(format!( - "attempt {}/{} (shuffle_seed={} perturbation_seed={}): {err}", - attempt + 1, - HEURISTIC_REBUILD_ATTEMPTS, - seeds.shuffle_seed, - seeds.perturbation_seed, - )); + last_error = Some(DelaunayRepairHeuristicRebuildFailure::AttemptFailed { + attempt: attempt + 1, + max_attempts: HEURISTIC_REBUILD_ATTEMPTS, + shuffle_seed: seeds.shuffle_seed, + perturbation_seed: seeds.perturbation_seed, + source: Box::new(err), + }); } } } Err(DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild failed after {HEURISTIC_REBUILD_ATTEMPTS} attempts: {}", - last_error.unwrap_or_else(|| "unknown error".to_string()) - ), + reason: Box::new(DelaunayRepairHeuristicRebuildFailure::ExhaustedAttempts { + attempts: HEURISTIC_REBUILD_ATTEMPTS, + last_failure: Box::new( + last_error.unwrap_or(DelaunayRepairHeuristicRebuildFailure::NoAttempts), + ), + }), }) } @@ -852,7 +892,9 @@ where self.tri .tds .vertices() - .map(|(_, vertex)| Vertex::new_with_uuid(*vertex.point(), vertex.uuid(), vertex.data)) + .map(|(_, vertex)| { + Vertex::from_validated_point_with_uuid(*vertex.point(), vertex.uuid(), vertex.data) + }) .collect() } @@ -875,7 +917,8 @@ mod tests { use super::*; use crate::construction::test_hooks; use crate::core::algorithms::flips::{ - DelaunayRepairDiagnostics, FlipError, RepairQueueOrder, verify_delaunay_via_flip_predicates, + DelaunayRepairDiagnostics, DelaunayRepairPostconditionFailure, FlipError, RepairQueueOrder, + verify_delaunay_via_flip_predicates, }; use crate::core::simplex::Simplex; use crate::core::tds::{Tds, TriangulationConstructionState}; @@ -1263,11 +1306,8 @@ mod tests { assert!(verify_delaunay_via_flip_predicates(&tds, &kernel).is_err()); assert!(verify_delaunay_via_flip_predicates(&tds, &robust_kernel).is_err()); let mut dt: DelaunayTriangulation, (), (), 2> = - DelaunayTriangulation::from_tds_with_topology_guarantee( - tds, - kernel, - TopologyGuarantee::PLManifold, - ); + DelaunayTriangulationCandidate::assemble(tds, kernel, TopologyGuarantee::PLManifold) + .into_repairable_delaunay_for_test(); dt.set_topology_guarantee(TopologyGuarantee::PLManifold); // max_flips=0 should fail (flips are needed but budget is zero). @@ -1297,11 +1337,12 @@ mod tests { // Reconstruct dt from the same raw TDS in case the previous attempt mutated it. let tds2 = non_delaunay_quad_tds(); let mut dt2: DelaunayTriangulation, (), (), 2> = - DelaunayTriangulation::from_tds_with_topology_guarantee( + DelaunayTriangulationCandidate::assemble( tds2, AdaptiveKernel::new(), TopologyGuarantee::PLManifold, - ); + ) + .into_repairable_delaunay_for_test(); dt2.set_topology_guarantee(TopologyGuarantee::PLManifold); let outcome_generous = dt2 .repair_delaunay_with_flips_advanced(config_generous) @@ -1394,21 +1435,23 @@ mod tests { }), }; let robust_err = DelaunayRepairError::PostconditionFailed { - message: "robust postcondition failure".to_string(), - }; - let heuristic_inner = DelaunayRepairError::HeuristicRebuildFailed { - message: "heuristic rebuild failed after 3 attempts: attempt 3/3 (shuffle_seed=1 perturbation_seed=2): inner".to_string(), - }; - - // Simulate the map_err closure in repair_delaunay_with_flips_advanced. - let heuristic_message = match heuristic_inner { - DelaunayRepairError::HeuristicRebuildFailed { message } => message, - other => other.to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }; let combined = DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "primary repair failed ({primary_err}); robust fallback failed ({robust_err}); {heuristic_message}" - ), + reason: Box::new(DelaunayRepairHeuristicRebuildFailure::FallbackChainFailed { + primary: Box::new(primary_err), + robust: Box::new(robust_err), + heuristic: Box::new(DelaunayRepairHeuristicRebuildFailure::ExhaustedAttempts { + attempts: 3, + last_failure: Box::new(DelaunayRepairHeuristicRebuildFailure::AttemptFailed { + attempt: 3, + max_attempts: 3, + shuffle_seed: 1, + perturbation_seed: 2, + source: Box::new(DelaunayRepairError::from(FlipError::DegenerateSimplex)), + }), + }), + }), }; let msg = combined.to_string(); @@ -1421,7 +1464,7 @@ mod tests { "error should mention robust failure: {msg}" ); assert!( - msg.contains("robust postcondition failure"), + msg.contains("disconnected the triangulation"), "error should include robust failure details: {msg}" ); assert!( diff --git a/src/delaunay/validation.rs b/src/delaunay/validation.rs index 48b5306c..dc953b27 100644 --- a/src/delaunay/validation.rs +++ b/src/delaunay/validation.rs @@ -7,6 +7,7 @@ #![forbid(unsafe_code)] use crate::core::algorithms::flips::{DelaunayRepairError, verify_delaunay_for_triangulation}; +use crate::core::algorithms::incremental_insertion::InsertionError; use crate::core::operations::DelaunayInsertionState; use crate::core::tds::{ InvariantError, InvariantKind, InvariantViolation, Tds, TdsError, TriangulationValidationReport, @@ -22,6 +23,140 @@ use crate::triangulation::DelaunayTriangulation; use std::num::NonZeroUsize; use thiserror::Error; +/// Proof that a candidate's underlying TDS passed structural validation. +#[derive(Clone, Copy, Debug)] +pub(crate) struct TdsStructureValidationProof(()); + +/// Proof that a candidate passed the Delaunay-layer validation boundary. +#[derive(Clone, Copy, Debug)] +pub(crate) struct DelaunayTriangulationValidationProof(()); + +/// Internal assembly stage for a triangulation that has not crossed its validation boundary yet. +/// +/// This keeps raw or freshly assembled [`Tds`] values out of the final +/// [`DelaunayTriangulation`] wrapper until the caller has proved the relevant +/// invariants for the construction path. +#[derive(Clone, Debug)] +pub(crate) struct DelaunayTriangulationCandidate { + candidate: DelaunayTriangulation, +} + +impl DelaunayTriangulationCandidate +where + K: Kernel, + U: DataType, + V: DataType, +{ + /// Assembles a validation candidate from a TDS and topology guarantee. + pub(crate) const fn assemble( + tds: Tds, + kernel: K, + topology_guarantee: TopologyGuarantee, + ) -> Self { + let validation_policy = topology_guarantee.default_validation_policy(); + Self { + candidate: DelaunayTriangulation { + tri: Triangulation { + kernel, + tds, + global_topology: GlobalTopology::DEFAULT, + validation_policy, + topology_guarantee, + }, + insertion_state: DelaunayInsertionState::new(), + spatial_index: None, + }, + } + } + + /// Sets runtime global-topology metadata before validation. + pub(crate) const fn set_global_topology(&mut self, global_topology: GlobalTopology) { + self.candidate.tri.set_global_topology(global_topology); + } + + /// Normalizes coherent orientation on the assembled candidate. + pub(crate) fn normalize_and_promote_positive_orientation( + &mut self, + ) -> Result<(), InsertionError> { + self.candidate + .tri + .normalize_and_promote_positive_orientation() + } + + /// Validates Level 1–2 TDS structure and returns proof for structural-only assembly paths. + pub(crate) fn validate_tds_structure(&self) -> Result { + self.candidate.tri.tds.validate()?; + Ok(TdsStructureValidationProof(())) + } + + /// Validates Level 3 topology without geometric orientation checks. + pub(crate) fn validate_topology_only(&self) -> Result<(), InvariantError> { + self.candidate.tri.is_valid_topology_only() + } + + /// Validates completion-time PL-manifold constraints. + pub(crate) fn validate_at_completion(&self) -> Result<(), InvariantError> { + self.candidate.tri.validate_at_completion() + } + + /// Validates explicit geometric nondegeneracy constraints. + pub(crate) fn validate_geometric_nondegeneracy(&self) -> Result<(), TdsError> { + self.candidate.tri.validate_geometric_nondegeneracy() + } + + /// Validates the Delaunay property and returns proof for final conversion. + pub(crate) fn validate_delaunay_property( + &self, + ) -> Result { + self.candidate.is_valid()?; + Ok(DelaunayTriangulationValidationProof(())) + } + + /// Validates all public reconstruction invariants and returns the final wrapper. + pub(crate) fn try_into_validated_delaunay( + self, + ) -> Result, DelaunayTriangulationValidationError> { + self.candidate.tri.validate().map_err(|e| match e { + InvariantError::Tds(tds_err) => tds_err.into(), + InvariantError::Triangulation(tri_err) => tri_err.into(), + InvariantError::Delaunay(dt_err) => dt_err, + })?; + + if self.candidate.global_topology().is_euclidean() { + is_delaunay_property_only(&self.candidate.tri.tds).map_err(|source| { + DelaunayTriangulationValidationError::VerificationFailed { + source: Box::new(DelaunayVerificationError::from(source)), + } + })?; + } else { + self.candidate.is_valid()?; + } + + Ok(self.candidate) + } + + /// Converts a candidate after the caller has proved the Delaunay boundary. + pub(crate) fn into_validated_delaunay( + self, + _proof: DelaunayTriangulationValidationProof, + ) -> DelaunayTriangulation { + self.candidate + } + + /// Converts a candidate after the caller has proved structural validity. + pub(crate) fn into_structurally_valid_delaunay( + self, + _proof: TdsStructureValidationProof, + ) -> DelaunayTriangulation { + self.candidate + } + + #[cfg(test)] + pub(crate) fn into_repairable_delaunay_for_test(self) -> DelaunayTriangulation { + self.candidate + } +} + /// Typed source for Level 4 Delaunay verification failures. /// /// Passive validation has two implementation paths: @@ -37,14 +172,14 @@ use thiserror::Error; /// # Examples /// /// ```rust -/// use delaunay::prelude::repair::DelaunayRepairError; +/// use delaunay::prelude::repair::{DelaunayRepairError, DelaunayRepairPostconditionFailure}; /// use delaunay::prelude::validation::{ /// DelaunayVerificationError, DelaunayVerificationErrorKind, /// }; /// /// let source = /// DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { -/// message: "non-Delaunay facet".to_string(), +/// reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), /// }); /// /// assert_eq!( @@ -93,14 +228,14 @@ impl From for DelaunayVerificationError { /// # Examples /// /// ```rust -/// use delaunay::prelude::repair::DelaunayRepairError; +/// use delaunay::prelude::repair::{DelaunayRepairError, DelaunayRepairPostconditionFailure}; /// use delaunay::prelude::validation::{ /// DelaunayVerificationError, DelaunayVerificationErrorKind, /// }; /// /// let source = /// DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { -/// message: "non-Delaunay facet".to_string(), +/// reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), /// }); /// let kind = DelaunayVerificationErrorKind::from(&source); /// @@ -197,20 +332,6 @@ pub enum DelaunayTriangulationValidationError { source: Box, }, - /// Flip-based Delaunay repair failed with string-only context. - /// - /// This variant is retained for compatibility with existing callers. New - /// mutating operations that can preserve the repair source should prefer - /// [`RepairOperationFailed`](Self::RepairOperationFailed). - /// - /// **Not** returned by `validate()` or `is_valid()` — those use - /// [`VerificationFailed`](Self::VerificationFailed) for passive checks. - #[error("Delaunay repair failed: {message}")] - RepairFailed { - /// Description of the repair failure. - message: String, - }, - /// Flip-based Delaunay repair failed during a specific mutating operation. /// /// This preserves the underlying [`DelaunayRepairError`] so callers can @@ -791,63 +912,19 @@ where topology_guarantee: TopologyGuarantee, global_topology: GlobalTopology, ) -> Result { - let mut candidate = Self::from_tds_with_topology_guarantee(tds, kernel, topology_guarantee); + let mut candidate = + DelaunayTriangulationCandidate::assemble(tds, kernel, topology_guarantee); candidate.set_global_topology(global_topology); - candidate.tri.validate().map_err(|e| match e { - InvariantError::Tds(tds_err) => tds_err.into(), - InvariantError::Triangulation(tri_err) => tri_err.into(), - InvariantError::Delaunay(dt_err) => dt_err, - })?; - - if candidate.global_topology().is_euclidean() { - is_delaunay_property_only(&candidate.tri.tds).map_err(|source| { - DelaunayTriangulationValidationError::VerificationFailed { - source: Box::new(DelaunayVerificationError::from(source)), - } - })?; - } else { - candidate.is_valid()?; - } - Ok(candidate) - } - - /// Assemble a `DelaunayTriangulation` from a `Tds` with an explicit topology guarantee. - /// - /// This crate-internal constructor performs no validation; public callers - /// must use [`try_from_tds_with_topology_guarantee`](Self::try_from_tds_with_topology_guarantee). - /// The initial - /// [`ValidationPolicy`](crate::ValidationPolicy) is derived from the guarantee: - /// [`PLManifoldStrict`](TopologyGuarantee::PLManifoldStrict) uses - /// [`Always`](crate::ValidationPolicy::Always), - /// [`PLManifold`](TopologyGuarantee::PLManifold) uses - /// [`ExplicitOnly`](crate::ValidationPolicy::ExplicitOnly), and - /// [`Pseudomanifold`](TopologyGuarantee::Pseudomanifold) uses - /// [`OnSuspicion`](crate::ValidationPolicy::OnSuspicion). - #[must_use] - pub(crate) const fn from_tds_with_topology_guarantee( - tds: Tds, - kernel: K, - topology_guarantee: TopologyGuarantee, - ) -> Self { - let validation_policy = topology_guarantee.default_validation_policy(); - Self { - tri: Triangulation { - kernel, - tds, - global_topology: GlobalTopology::DEFAULT, - validation_policy, - topology_guarantee, - }, - insertion_state: DelaunayInsertionState::new(), - spatial_index: None, - } + candidate.try_into_validated_delaunay() } } #[cfg(test)] mod tests { use super::*; - use crate::core::algorithms::flips::{DelaunayRepairDiagnostics, RepairQueueOrder}; + use crate::core::algorithms::flips::{ + DelaunayRepairDiagnostics, DelaunayRepairPostconditionFailure, RepairQueueOrder, + }; use crate::core::simplex::Simplex; use crate::core::tds::{SimplexKey, TriangulationConstructionState, VertexKey}; use crate::geometry::kernel::AdaptiveKernel; @@ -905,8 +982,9 @@ mod tests { } fn synthetic_flip_verification_source(message: &str) -> DelaunayVerificationError { + let _ = message; DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { - message: message.to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }) } @@ -953,7 +1031,7 @@ mod tests { "Display should contain prefix: {msg}" ); assert!( - msg.contains("flip predicate detected non-Delaunay facet"), + msg.contains("repair pass disconnected the triangulation"), "Display should contain inner message: {msg}" ); let DelaunayTriangulationValidationError::VerificationFailed { source } = &err else { diff --git a/src/geometry/algorithms/convex_hull.rs b/src/geometry/algorithms/convex_hull.rs index a9509a7f..f7ff0fac 100644 --- a/src/geometry/algorithms/convex_hull.rs +++ b/src/geometry/algorithms/convex_hull.rs @@ -7308,7 +7308,7 @@ mod tests { .expect("tetrahedron should have vertices") .1 .uuid(); - let duplicate_uuid_vertex = Vertex::new_with_uuid( + let duplicate_uuid_vertex = Vertex::from_validated_point_with_uuid( Point::try_new([0.25, 0.25, 0.125]).expect("finite point coordinates"), duplicate_uuid, None, diff --git a/src/geometry/util/triangulation_generation.rs b/src/geometry/util/triangulation_generation.rs index 465a5cb6..97b74774 100644 --- a/src/geometry/util/triangulation_generation.rs +++ b/src/geometry/util/triangulation_generation.rs @@ -13,7 +13,7 @@ use crate::construction::{ ConstructionOptions, DelaunayConstructionFailure, DelaunayTriangulationConstructionError, InsertionOrderStrategy, RetryPolicy, }; -use crate::core::construction::TriangulationConstructionError; +use crate::core::construction::{FinalTopologyValidationContext, TriangulationConstructionError}; use crate::core::simplex::SimplexValidationError; use crate::core::traits::data_type::DataType; use crate::core::validation::TopologyGuarantee; @@ -80,7 +80,7 @@ where { dt.as_triangulation().validate().map_err(|e| { TriangulationConstructionError::FinalTopologyValidation { - message: "random triangulation failed final Levels 1-3 topology validation".to_string(), + context: FinalTopologyValidationContext::RandomGeneration, source: e.into(), } })?; diff --git a/src/lib.rs b/src/lib.rs index fcaa7278..6a609c0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -779,14 +779,17 @@ pub use crate::core::algorithms::incremental_insertion::{ CavityFillingError, CavityRepairStage, DelaunayRepairErrorKind, DelaunayRepairErrorSummary, DelaunayRepairFailureContext, HullExtensionReason, InitialSimplexConstructionError, InitialSimplexUnexpectedInsertionStage, InsertionError, InsertionErrorKind, - InsertionErrorSourceKind, InsertionErrorSummary, NeighborRebuildError, NeighborWiringError, - SpatialIndexConstructionFailure, TdsConstructionFailure, TdsValidationFailure, extend_hull, - fill_cavity, repair_neighbor_pointers, repair_neighbor_pointers_local, wire_cavity_neighbors, + InsertionErrorSourceKind, InsertionErrorSummary, InsertionTopologyValidationContext, + NeighborRebuildError, NeighborWiringError, SpatialIndexConstructionFailure, + TdsConstructionFailure, TdsValidationFailure, extend_hull, fill_cavity, + repair_neighbor_pointers, repair_neighbor_pointers_local, wire_cavity_neighbors, }; pub use crate::core::algorithms::pl_manifold_repair::{ PlManifoldRepairError, PlManifoldRepairStats, }; -pub use crate::core::construction::TriangulationConstructionError; +pub use crate::core::construction::{ + FinalTopologyValidationContext, TriangulationConstructionError, +}; pub use crate::core::insertion::DuplicateDetectionMetrics; pub use crate::core::operations::{ InsertionOutcome, InsertionResult, InsertionStatistics, RepairDecision, RepairSkipReason, @@ -1138,11 +1141,11 @@ pub mod prelude { DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, DelaunayTriangulationConstructionErrorWithStatistics, DelaunayTriangulationValidationError, DelaunayVerificationError, DelaunayVerificationErrorKind, DuplicateDetectionMetrics, - InitialSimplexStrategy, InsertionOrderStrategy, InsertionResult, PlManifoldRepairError, - PlManifoldRepairStats, RepairDecision, RepairSkipReason, RetryPolicy, TopologicalOperation, - TopologyGuarantee, Triangulation, TriangulationConstructionError, - TriangulationValidationError, ValidationConfigurationError, ValidationPolicy, - try_vertices_from_points, + FinalTopologyValidationContext, InitialSimplexStrategy, InsertionOrderStrategy, + InsertionResult, PlManifoldRepairError, PlManifoldRepairStats, RepairDecision, + RepairSkipReason, RetryPolicy, TopologicalOperation, TopologyGuarantee, Triangulation, + TriangulationConstructionError, TriangulationValidationError, ValidationConfigurationError, + ValidationPolicy, try_vertices_from_points, }; // Re-export utility items, but avoid exporting the util module names themselves. @@ -1183,18 +1186,23 @@ pub mod prelude { CavityFillingError, CavityRepairStage, DelaunayRepairErrorKind, DelaunayRepairErrorSummary, DelaunayRepairFailureContext, HullExtensionReason, InitialSimplexConstructionError, InitialSimplexUnexpectedInsertionStage, InsertionError, InsertionErrorKind, - InsertionErrorSourceKind, InsertionErrorSummary, NeighborRebuildError, NeighborWiringError, - SpatialIndexConstructionFailure, TdsConstructionFailure, TdsValidationFailure, + InsertionErrorSourceKind, InsertionErrorSummary, InsertionTopologyValidationContext, + NeighborRebuildError, NeighborWiringError, SpatialIndexConstructionFailure, + TdsConstructionFailure, TdsValidationFailure, }; pub use crate::{InsertionOutcome, InsertionStatistics, SuspicionFlags}; // Re-export diagnostic types for scientific analysis of construction and repair pub use crate::flips::{ - DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairStats, - DelaunayRepairVerificationContext, FlipContextError, FlipEdgeAdjacencyError, FlipError, - FlipMutationError, FlipNeighborWiringError, FlipOrientationCheckStage, FlipPredicateError, - FlipPredicateOperation, FlipTriangleAdjacencyError, FlipVertexAdjacencyError, - RepairQueueOrder, TriangleHandleError, + DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairHeuristicRebuildFailure, + DelaunayRepairHeuristicRebuildFailureKind, DelaunayRepairHeuristicVertexContext, + DelaunayRepairOrientationCanonicalizationFailure, + DelaunayRepairOrientationCanonicalizationFailureKind, DelaunayRepairPostconditionFailure, + DelaunayRepairStats, DelaunayRepairVerificationContext, FlipContextError, + FlipEdgeAdjacencyError, FlipError, FlipMutationError, FlipNeighborWiringError, + FlipOrientationCheckStage, FlipPredicateError, FlipPredicateOperation, + FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RepairQueueOrder, + TriangleHandleError, }; // Re-export commonly used collection types from the public collections facade. @@ -1278,8 +1286,8 @@ pub mod prelude { }; pub use crate::{ CavityFillingError, CavityRepairStage, DelaunayTriangulation, - SpatialIndexConstructionFailure, TopologyGuarantee, Triangulation, - TriangulationConstructionError, try_vertices_from_points, + FinalTopologyValidationContext, SpatialIndexConstructionFailure, TopologyGuarantee, + Triangulation, TriangulationConstructionError, try_vertices_from_points, }; } @@ -1382,9 +1390,10 @@ pub mod prelude { DelaunayRepairErrorSummary, DelaunayRepairFailureContext, HullExtensionReason, InitialSimplexConstructionError, InitialSimplexUnexpectedInsertionStage, InsertionError, InsertionErrorKind, InsertionErrorSourceKind, InsertionErrorSummary, - NeighborRebuildError, NeighborWiringError, SpatialIndexConstructionFailure, - TdsConstructionFailure, TdsValidationFailure, extend_hull, fill_cavity, - repair_neighbor_pointers, repair_neighbor_pointers_local, wire_cavity_neighbors, + InsertionTopologyValidationContext, NeighborRebuildError, NeighborWiringError, + SpatialIndexConstructionFailure, TdsConstructionFailure, TdsValidationFailure, + extend_hull, fill_cavity, repair_neighbor_pointers, repair_neighbor_pointers_local, + wire_cavity_neighbors, }; pub use crate::{InsertionOutcome, InsertionResult, InsertionStatistics}; } @@ -1407,7 +1416,11 @@ pub mod prelude { /// [`DelaunayRepairErrorKind`]: crate::prelude::repair::DelaunayRepairErrorKind pub mod repair { pub use crate::flips::{ - DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairStats, + DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairHeuristicRebuildFailure, + DelaunayRepairHeuristicRebuildFailureKind, DelaunayRepairHeuristicVertexContext, + DelaunayRepairOrientationCanonicalizationFailure, + DelaunayRepairOrientationCanonicalizationFailureKind, + DelaunayRepairPostconditionFailure, DelaunayRepairStats, DelaunayRepairVerificationContext, FlipContextError, FlipEdgeAdjacencyError, FlipError, FlipMutationError, FlipNeighborWiringError, FlipOrientationCheckStage, FlipPredicateError, FlipPredicateOperation, FlipTriangleAdjacencyError, diff --git a/tests/delaunayize_workflow.rs b/tests/delaunayize_workflow.rs index c2e5c6e1..f4c1863f 100644 --- a/tests/delaunayize_workflow.rs +++ b/tests/delaunayize_workflow.rs @@ -342,12 +342,12 @@ fn test_error_display_topology_repair_failed() { #[test] fn test_error_display_delaunay_repair_failed() { let inner = DelaunayRepairError::PostconditionFailed { - message: "test postcondition".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }; let err: DelaunayizeError = inner.clone().into(); let msg = err.to_string(); assert!(msg.contains("Delaunay repair failed"), "{msg}"); - assert!(msg.contains("test postcondition"), "{msg}"); + assert!(msg.contains("disconnected the triangulation"), "{msg}"); // Typed source is preserved end-to-end — no stringification. assert_eq!( @@ -419,7 +419,7 @@ fn test_error_display_delaunay_repair_with_rebuild() { } .into(); let source = DelaunayRepairError::PostconditionFailed { - message: "synthetic postcondition".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }), }; let err = DelaunayizeError::DelaunayRepairFailedWithRebuild { source: source.clone(), @@ -428,7 +428,7 @@ fn test_error_display_delaunay_repair_with_rebuild() { let msg = err.to_string(); assert!(msg.contains("Delaunay repair failed"), "{msg}"); - assert!(msg.contains("synthetic postcondition"), "{msg}"); + assert!(msg.contains("disconnected the triangulation"), "{msg}"); assert!(msg.contains("fallback rebuild also failed"), "{msg}"); assert!(msg.contains("synthetic rebuild degeneracy"), "{msg}"); @@ -445,7 +445,9 @@ fn test_error_display_delaunay_repair_with_rebuild() { .source() .expect("source() must be Some for the with-rebuild variant"); assert!( - source.to_string().contains("synthetic postcondition"), + source + .to_string() + .contains("disconnected the triangulation"), "source display should match the underlying DelaunayRepairError: {source}" ); } diff --git a/tests/euler_characteristic.rs b/tests/euler_characteristic.rs index d6f822f1..6ca4a211 100644 --- a/tests/euler_characteristic.rs +++ b/tests/euler_characteristic.rs @@ -267,7 +267,8 @@ fn test_2d_toroidal_explicit_construction_rejected() { let topology = GlobalTopology::try_toroidal([1.0, 1.0], ToroidalConstructionMode::Explicit).unwrap(); - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .global_topology(topology) .topology_guarantee(TopologyGuarantee::Pseudomanifold) .build::<()>() @@ -348,7 +349,8 @@ fn test_3d_toroidal_explicit_construction_rejected() { let topology = GlobalTopology::try_toroidal([1.0, 1.0, 1.0], ToroidalConstructionMode::Explicit).unwrap(); - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .global_topology(topology) .topology_guarantee(TopologyGuarantee::Pseudomanifold) .build::<()>() diff --git a/tests/prelude_exports.rs b/tests/prelude_exports.rs index aa5a65f8..c4b6a99a 100644 --- a/tests/prelude_exports.rs +++ b/tests/prelude_exports.rs @@ -35,6 +35,7 @@ use delaunay::prelude::construction::{ ExplicitDelaunayValidationErrorKind, ExplicitDelaunayValidationSourceKind, ExplicitInsertionError, ExplicitInsertionErrorKind, ExplicitInvariantError, ExplicitInvariantErrorKind, ExplicitTdsError, ExplicitTdsErrorKind, + FinalTopologyValidationContext, GlobalTopologyModelError as ConstructionGlobalTopologyModelError, InsertionOrderStrategy, InvalidCoordinateValue as ConstructionInvalidCoordinateValue, InvalidPositiveScalar as ConstructionInvalidPositiveScalar, RandomPointGenerationError, @@ -68,13 +69,15 @@ use delaunay::prelude::geometry::AdaptiveKernel; use delaunay::prelude::geometry::{ ArrayConversionFailureReason, CircumcenterError, CircumcenterFailureReason, CoordinateConversionError, CoordinateConversionValue, CoordinateValidationError, - DegenerateGeometry, DegenerateMeasure, DegenerateSimplexReason, FiniteCoordinateValue, - InvalidCoordinateValue, LaError, MatrixError, Point, QualitySimplexVerticesError, - SurfaceMeasureError, ValueConversionError, ValueConversionFailureReason, + CoordinateValues, DegenerateGeometry, DegenerateMeasure, DegenerateSimplexReason, + FiniteCoordinateValue, InvalidCoordinateValue, LaError, MatrixError, Point, + QualitySimplexVerticesError, SurfaceMeasureError, ValueConversionError, + ValueConversionFailureReason, }; use delaunay::prelude::insertion::{ InitialSimplexConstructionError, InitialSimplexUnexpectedInsertionStage, InsertionError, - NeighborRebuildError, Tds as InsertionTds, TdsMutationError, repair_neighbor_pointers_local, + InsertionTopologyValidationContext, NeighborRebuildError, Tds as InsertionTds, + TdsMutationError, repair_neighbor_pointers_local, }; use delaunay::prelude::ordering::{ HilbertBitDepth, HilbertError, HilbertQuantizedBatch, MAX_HILBERT_BITS, hilbert_index_in_range, @@ -88,8 +91,12 @@ use delaunay::prelude::query::{ QueryError, }; use delaunay::prelude::repair::{ - DelaunayCheckPolicy, DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairOperation, - DelaunayRepairOutcome, DelaunayRepairStats, DelaunayRepairVerificationContext, + DelaunayCheckPolicy, DelaunayRepairDiagnostics, DelaunayRepairError, + DelaunayRepairHeuristicRebuildFailure, DelaunayRepairHeuristicRebuildFailureKind, + DelaunayRepairHeuristicVertexContext, DelaunayRepairOperation, + DelaunayRepairOrientationCanonicalizationFailure, + DelaunayRepairOrientationCanonicalizationFailureKind, DelaunayRepairOutcome, + DelaunayRepairPostconditionFailure, DelaunayRepairStats, DelaunayRepairVerificationContext, DelaunayTriangulationValidationError, FlipEdgeAdjacencyError, FlipError, FlipOrientationCheckStage as RepairFlipOrientationCheckStage, FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RepairQueueOrder, verify_delaunay_for_triangulation, @@ -206,6 +213,14 @@ fn construction_prelude_covers_dedup_policy() { #[test] fn construction_prelude_covers_typed_construction_failure_variants() { + assert_eq!( + FinalTopologyValidationContext::ConstructionFinalize.to_string(), + "topology validation failed after construction" + ); + assert_eq!( + InsertionTopologyValidationContext::PostInsertion.to_string(), + "post-insertion topology validation failed" + ); assert_matches!( DelaunayConstructionFailure::GeometricDegeneracy { message: "synthetic".to_string(), @@ -951,7 +966,9 @@ fn construction_prelude_covers_random_point_generation_failure_variant() source: ConstructionDelaunayTriangulationValidationError::VerificationFailed { source: ConstructionDelaunayVerificationError::from( DelaunayRepairError::PostconditionFailed { - message: "synthetic final Level 4 failure".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }, ) .into(), @@ -961,14 +978,16 @@ fn construction_prelude_covers_random_point_generation_failure_variant() source: ConstructionDelaunayTriangulationValidationError::VerificationFailed { source, }, - } if source.to_string().contains("synthetic final Level 4 failure") + } if source.to_string().contains("disconnected the triangulation") ); let validation_summary = ExplicitDelaunayValidationError::from( ConstructionDelaunayTriangulationValidationError::VerificationFailed { source: ConstructionDelaunayVerificationError::from( DelaunayRepairError::PostconditionFailed { - message: "synthetic Level 4 summary failure".to_string(), + reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { + simplex_count: 1, + }), }, ) .into(), @@ -1023,6 +1042,33 @@ fn diagnostic_preludes_cover_repair_apis() -> Result<(), PreludeExportTestError> DelaunayRepairError::from(FlipError::DegenerateSimplex), DelaunayRepairError::Flip { .. } ); + let orientation_reason = DelaunayRepairOrientationCanonicalizationFailure::AfterFlipRepair { + source: Box::new(InsertionError::DuplicateCoordinates { + coordinates: CoordinateValues::from([0.0, 0.0, 0.0]), + }), + }; + assert!(orientation_reason.to_string().contains("after flip repair")); + let orientation_kind = DelaunayRepairOrientationCanonicalizationFailureKind::AfterFlipRepair { + source_kind: delaunay::prelude::insertion::InsertionErrorKind::DuplicateCoordinates, + }; + assert!(matches!( + orientation_kind, + DelaunayRepairOrientationCanonicalizationFailureKind::AfterFlipRepair { .. } + )); + let heuristic_vertex: Option = None; + assert!(heuristic_vertex.is_none()); + let heuristic_reason = + DelaunayRepairHeuristicRebuildFailure::RecursionDepthExceeded { max_depth: 1 }; + assert!( + heuristic_reason + .to_string() + .contains("recursion depth exceeded") + ); + let heuristic_kind = DelaunayRepairHeuristicRebuildFailureKind::RecursionDepthExceeded; + assert_eq!( + heuristic_kind, + DelaunayRepairHeuristicRebuildFailureKind::RecursionDepthExceeded + ); assert_send_sync_unpin::(); assert_send_sync_unpin::(); assert_send_sync_unpin::(); diff --git a/tests/semgrep/src/project_rules/rust_style.rs b/tests/semgrep/src/project_rules/rust_style.rs index 08ce7e34..9a0e77a3 100644 --- a/tests/semgrep/src/project_rules/rust_style.rs +++ b/tests/semgrep/src/project_rules/rust_style.rs @@ -113,6 +113,11 @@ pub fn public_panic_bypass() { panic!("public APIs should return typed errors instead"); } +pub fn production_debug_assert_bypass(value: usize) { + // ruleid: delaunay.rust.no-production-debug-assert + debug_assert!(value > 0); +} + // ruleid: delaunay.rust.no-legacy-coordinate-generic-api type LegacyPoint = Point; @@ -229,6 +234,65 @@ impl ParallelValidatedDataConstructorFixture { } } +impl FallibleConstructorDefinitionFixture { + // ruleid: delaunay.rust.no-fallible-new-or-from-constructor-definitions + pub fn new(value: usize) -> Result { + Ok(Self { value }) + } + + // ruleid: delaunay.rust.no-fallible-new-or-from-constructor-definitions + fn from_runtime(value: usize) -> Result { + Ok(Self { value }) + } + + // ruleid: delaunay.rust.no-fallible-new-or-from-constructor-definitions + fn from_simplex_with_data( + value: usize, + _data: Option, + ) -> Result { + Ok(Self { value }) + } + + // ok: delaunay.rust.no-fallible-new-or-from-constructor-definitions + fn try_new(value: usize) -> Result { + Ok(Self { value }) + } + + // ok: delaunay.rust.no-fallible-new-or-from-constructor-definitions + fn try_from_runtime(value: usize) -> Result { + Ok(Self { value }) + } + + // ok: delaunay.rust.no-fallible-new-or-from-constructor-definitions + fn from_validated_value(value: usize) -> Self { + Self { value } + } +} + +impl UncheckedConstructorFixture { + // ruleid: delaunay.rust.no-unreviewed-from-unchecked-constructors + pub(crate) const fn from_unchecked_tds_with_topology_guarantee() -> Self { + Self + } + + // ruleid: delaunay.rust.no-unreviewed-from-unchecked-constructors + pub(crate) fn from_unchecked_raw_state() -> Self { + Self + } + + // ok: delaunay.rust.no-unreviewed-from-unchecked-constructors + pub(crate) fn assemble_tds_with_topology_guarantee() -> Self { + Self + } +} + +impl PublicUncheckedPrefixConstructorFixture { + // ruleid: delaunay.rust.no-unreviewed-from-unchecked-constructors, delaunay.rust.no-public-unchecked-apis + pub fn from_unchecked_tds_with_topology_guarantee() -> Self { + Self + } +} + pub fn triangulation_fallible_constructor_names_bad( vertices: &[Vertex<(), 3>], options: ConstructionOptions, @@ -622,7 +686,11 @@ impl PublicVertexUuidConstructorFixture { impl CratePrivateVertexUuidConstructorFixture { // ok: delaunay.rust.no-public-vertex-new-with-uuid - pub(crate) const fn new_with_uuid(point: Point<3>, uuid: Uuid, data: Option<()>) -> Self { + pub(crate) const fn from_validated_point_with_uuid( + point: Point<3>, + uuid: Uuid, + data: Option<()>, + ) -> Self { Self { point, uuid, data } } diff --git a/tests/triangulation_builder.rs b/tests/triangulation_builder.rs index 5a738eb6..81ee6465 100644 --- a/tests/triangulation_builder.rs +++ b/tests/triangulation_builder.rs @@ -566,7 +566,8 @@ fn test_explicit_toroidal_heawood_torus_rejected() { let topology = GlobalTopology::try_toroidal([2.0, 2.0], ToroidalConstructionMode::Explicit).unwrap(); - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .global_topology(topology) .build::<()>() .expect_err("explicit toroidal connectivity requires a Level 4 quotient validator"); @@ -599,7 +600,8 @@ fn test_explicit_toroidal_torus_euler_mismatch_without_override() { } // Build with default Euclidean topology — should fail at Euler validation. - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .expect_err("explicit torus without Toroidal metadata should fail Euler validation"); @@ -636,9 +638,19 @@ fn test_explicit_toroidal_torus_euler_mismatch_without_override() { } // ============================================================================= -// Explicit construction (from_vertices_and_simplices) +// Explicit construction (try_from_vertices_and_simplices) // ============================================================================= +fn explicit_builder_parse_error( + vertices: &[Vertex], + simplices: &[Vec], +) -> ExplicitConstructionError { + match DelaunayTriangulationBuilder::try_from_vertices_and_simplices(vertices, simplices) { + Ok(_) => panic!("explicit simplex specs should be rejected before builder storage"), + Err(err) => err, + } +} + /// 2D: Build two triangles forming a quad from explicit vertices and simplices. #[test] fn test_explicit_2d_two_triangle_quad() { @@ -650,7 +662,8 @@ fn test_explicit_2d_two_triangle_quad() { ]; let simplices = vec![vec![0, 1, 2], vec![0, 2, 3]]; - let dt = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .expect("explicit 2D build should succeed"); @@ -696,7 +709,8 @@ fn test_explicit_normalizes_incoherent_simplex_order() { let mut simplices = vec![vec![0, 1, 2], vec![0, 2, 3]]; simplices[1].swap(0, 1); - let dt = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .expect("explicit build should normalize incoherent simplex ordering"); @@ -719,7 +733,8 @@ fn test_explicit_3d_two_tetrahedra() { // Two tetrahedra sharing face (0, 1, 2) let simplices = vec![vec![0, 1, 2, 3], vec![0, 1, 2, 4]]; - let dt = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .expect("explicit 3D build should succeed"); @@ -770,10 +785,11 @@ fn test_explicit_round_trip_3d() { simplex_specs.push(spec); } - let dt_reconstructed = DelaunayTriangulationBuilder::from_vertices_and_simplices( + let dt_reconstructed = DelaunayTriangulationBuilder::try_from_vertices_and_simplices( &extracted_vertices, &simplex_specs, ) + .unwrap() .build::<()>() .expect("explicit 3D reconstruction should succeed"); @@ -829,10 +845,11 @@ fn test_explicit_round_trip_2d() { } // Reconstruct via explicit. - let dt_reconstructed = DelaunayTriangulationBuilder::from_vertices_and_simplices( + let dt_reconstructed = DelaunayTriangulationBuilder::try_from_vertices_and_simplices( &extracted_vertices, &simplex_specs, ) + .unwrap() .build::<()>() .expect("explicit reconstruction should succeed"); @@ -856,8 +873,8 @@ fn test_explicit_error_empty_simplices() { ]; let simplices: Vec> = vec![]; - let result = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) - .build::<()>(); + let result = + DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices); assert!(result.is_err(), "Empty simplices should produce an error"); } @@ -873,8 +890,8 @@ fn test_explicit_error_wrong_arity() { // 2D expects 3 vertices per simplex, but we provide 2. let simplices = vec![vec![0, 1]]; - let result = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) - .build::<()>(); + let result = + DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices); assert!(result.is_err(), "Wrong arity should produce an error"); } @@ -889,8 +906,8 @@ fn test_explicit_error_index_out_of_bounds() { ]; let simplices = vec![vec![0, 1, 99]]; // 99 is out of bounds - let result = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) - .build::<()>(); + let result = + DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices); assert!( result.is_err(), @@ -908,8 +925,8 @@ fn test_explicit_error_duplicate_vertex_in_simplex() { ]; let simplices = vec![vec![0, 1, 1]]; // Duplicate vertex 1 - let result = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) - .build::<()>(); + let result = + DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices); assert!(result.is_err(), "Duplicate vertex should produce an error"); } @@ -924,7 +941,8 @@ fn test_explicit_2d_single_triangle() { ]; let simplices = vec![vec![0, 1, 2]]; - let dt = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .expect("single triangle should succeed"); @@ -944,7 +962,8 @@ fn test_explicit_3d_single_tetrahedron() { ]; let simplices = vec![vec![0, 1, 2, 3]]; - let dt = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .expect("single tetrahedron should succeed"); @@ -974,7 +993,8 @@ fn test_explicit_non_delaunay_mesh() { // the circumcircle of triangle ABC. let simplices = vec![vec![0, 1, 2], vec![0, 2, 3]]; - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .expect_err("non-Delaunay mesh must not construct a DelaunayTriangulation"); @@ -1003,7 +1023,8 @@ fn test_explicit_topology_guarantee_propagated() { ]; let simplices = vec![vec![0, 1, 2]]; - let dt = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .topology_guarantee(TopologyGuarantee::Pseudomanifold) .build::<()>() .expect("build should succeed"); @@ -1021,7 +1042,8 @@ fn test_explicit_preserves_vertex_data() { ]; let simplices = vec![vec![0, 1, 2]]; - let dt = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .expect("explicit build with vertex data should succeed"); @@ -1048,7 +1070,8 @@ fn test_explicit_validate_delaunay_mesh() { ]; let simplices = vec![vec![0, 1, 2]]; - let dt = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .expect("build should succeed"); @@ -1069,7 +1092,8 @@ fn test_explicit_unreferenced_vertices_rejected() { ]; let simplices = vec![vec![0, 1, 2]]; - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .unwrap_err(); @@ -1090,17 +1114,10 @@ fn test_explicit_error_variant_empty_simplices() { let vertices = vec![Vertex::<(), _>::try_new([0.0_f64, 0.0]).unwrap()]; let simplices: Vec> = vec![]; - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) - .build::<()>() - .unwrap_err(); + let err = explicit_builder_parse_error(&vertices, &simplices); assert!( - matches!( - err, - DelaunayTriangulationConstructionError::ExplicitConstruction( - ExplicitConstructionError::EmptySimplices - ) - ), + matches!(err, ExplicitConstructionError::EmptySimplices), "Expected ExplicitConstruction(EmptySimplices), got: {err}" ); } @@ -1115,20 +1132,16 @@ fn test_explicit_error_variant_wrong_arity() { ]; let simplices = vec![vec![0, 1]]; // 2D expects 3 vertices - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) - .build::<()>() - .unwrap_err(); + let err = explicit_builder_parse_error(&vertices, &simplices); assert!( matches!( err, - DelaunayTriangulationConstructionError::ExplicitConstruction( - ExplicitConstructionError::InvalidSimplexArity { - simplex_index: 0, - actual: 2, - expected: 3 - } - ) + ExplicitConstructionError::InvalidSimplexArity { + simplex_index: 0, + actual: 2, + expected: 3 + } ), "Expected InvalidSimplexArity, got: {err}" ); @@ -1148,7 +1161,8 @@ fn test_explicit_error_variant_non_manifold_facet() { ]; let simplices = vec![vec![0, 1, 2], vec![0, 1, 3], vec![0, 1, 4]]; - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .unwrap_err(); @@ -1174,16 +1188,12 @@ fn test_explicit_error_variant_duplicate_vertex_in_simplex() { ]; let simplices = vec![vec![0, 1, 1]]; // Duplicate vertex 1 - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) - .build::<()>() - .unwrap_err(); + let err = explicit_builder_parse_error(&vertices, &simplices); assert!( matches!( err, - DelaunayTriangulationConstructionError::ExplicitConstruction( - ExplicitConstructionError::DuplicateVertexInSimplex { simplex_index: 0 } - ) + ExplicitConstructionError::DuplicateVertexInSimplex { simplex_index: 0 } ), "Expected DuplicateVertexInSimplex, got: {err}" ); @@ -1199,7 +1209,8 @@ fn test_explicit_error_variant_incompatible_topology() { ]; let simplices = vec![vec![0, 1, 2]]; - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .try_canonicalized_toroidal([1.0, 1.0]) .unwrap() .build::<()>() @@ -1226,7 +1237,8 @@ fn test_explicit_error_variant_unsupported_construction_options() { ]; let simplices = vec![vec![0, 1, 2]]; - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .construction_options( ConstructionOptions::default().with_insertion_order(InsertionOrderStrategy::Input), ) @@ -1254,7 +1266,8 @@ fn test_explicit_error_variant_duplicate_simplices_structural_validation() { ]; let simplices = vec![vec![0, 1, 2], vec![0, 1, 2]]; - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .unwrap_err(); @@ -1278,7 +1291,8 @@ fn test_explicit_error_variant_geometric_nondegeneracy() { ]; let simplices = vec![vec![0, 1, 2]]; - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) + let err = DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) + .unwrap() .build::<()>() .unwrap_err(); @@ -1347,20 +1361,16 @@ fn test_explicit_error_variant_index_out_of_bounds() { ]; let simplices = vec![vec![0, 1, 99]]; - let err = DelaunayTriangulationBuilder::from_vertices_and_simplices(&vertices, &simplices) - .build::<()>() - .unwrap_err(); + let err = explicit_builder_parse_error(&vertices, &simplices); assert!( matches!( err, - DelaunayTriangulationConstructionError::ExplicitConstruction( - ExplicitConstructionError::IndexOutOfBounds { - simplex_index: 0, - vertex_index: 99, - bound: 3, - } - ) + ExplicitConstructionError::IndexOutOfBounds { + simplex_index: 0, + vertex_index: 99, + bound: 3, + } ), "Expected IndexOutOfBounds, got: {err}" ); From 638ea321c6135a463d4ab078758e23799f714f9e Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Wed, 17 Jun 2026 05:48:32 -0700 Subject: [PATCH 3/5] refactor(api)!: enforce typed validation boundaries (#443) Closes #443 - Split final topology and final Delaunay validation contexts so callers can handle Level 4 failures without parsing display text. - Preserve typed source chains for construction, insertion, hull-extension, and repair postcondition failures. - Require checked TDS reconstruction to consume validation proofs before returning Delaunay wrappers. - Align examples and developer guidance with fallible vertex construction and typed error propagation. BREAKING CHANGE: final Delaunay validation failures now use FinalDelaunayValidationContext instead of the topology-validation context, and affected construction error variants expose additional typed context/source structure. --- README.md | 40 +- docs/api_design.md | 70 ++-- docs/dev/rust.md | 8 +- docs/diagnostics.md | 31 +- docs/numerical_robustness_guide.md | 14 +- docs/topology.md | 6 +- docs/validation.md | 52 ++- docs/workflows.md | 142 ++++--- semgrep.yaml | 2 +- src/core/algorithms/flips.rs | 387 +++++++++++++----- src/core/algorithms/incremental_insertion.rs | 149 +++++-- src/core/construction.rs | 122 +++++- src/core/orientation.rs | 12 +- src/core/query.rs | 6 +- src/core/tds.rs | 63 ++- src/core/validation.rs | 10 +- src/delaunay/builder.rs | 10 +- src/delaunay/construction.rs | 44 +- src/delaunay/query.rs | 17 +- src/delaunay/validation.rs | 215 ++++++---- src/lib.rs | 131 +++--- tests/prelude_exports.rs | 8 +- tests/semgrep/src/project_rules/rust_style.rs | 5 + 23 files changed, 1039 insertions(+), 505 deletions(-) diff --git a/README.md b/README.md index 986e60a8..a6cd4586 100644 --- a/README.md +++ b/README.md @@ -121,31 +121,26 @@ prelude map and namespace policy, see the ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; #[derive(Debug, thiserror::Error)] enum ExampleError { #[error(transparent)] Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] - Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), ExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0, 0.0]) - ?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0, 0.0]) - ?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0, 0.0]) - ?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0, 0.0]) - ?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0, 1.0]) - ?, - delaunay::prelude::Vertex::<(), _>::try_new([0.2, 0.2, 0.2, 0.2]) - ?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.2, 0.2, 0.2, 0.2])?, ]; let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -168,31 +163,28 @@ For coordinate wrapping on a toroidal domain, use ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyKind, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyKind, Vertex, ToroidalDomainError, }; +use delaunay::prelude::geometry::CoordinateConversionError; #[derive(Debug, thiserror::Error)] enum ExampleError { #[error(transparent)] Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] - Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + Coordinate(#[from] CoordinateConversionError), #[error(transparent)] Topology(#[from] ToroidalDomainError), } fn main() -> Result<(), ExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.1, 0.2]) - ?, - delaunay::prelude::Vertex::<(), _>::try_new([0.8, 0.3]) - ?, - delaunay::prelude::Vertex::<(), _>::try_new([0.5, 0.7]) - ?, + Vertex::<(), _>::try_new([0.1, 0.2])?, + Vertex::<(), _>::try_new([0.8, 0.3])?, + Vertex::<(), _>::try_new([0.5, 0.7])?, // Wraps to [0.2, 0.4]. - delaunay::prelude::Vertex::<(), _>::try_new([1.2, 0.4]) - ?, + Vertex::<(), _>::try_new([1.2, 0.4])?, ]; let dt = DelaunayTriangulationBuilder::new(&vertices) diff --git a/docs/api_design.md b/docs/api_design.md index cb15105d..1c194734 100644 --- a/docs/api_design.md +++ b/docs/api_design.md @@ -66,8 +66,9 @@ For most use cases, the builder with default options is sufficient: ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::insertion::InsertionError; use delaunay::prelude::tds::InvariantError; @@ -79,20 +80,22 @@ enum ExampleError { Insertion(#[from] InsertionError), #[error(transparent)] Topology(#[from] InvariantError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), ExampleError> { // Simple construction from vertices (Euclidean space, default options) let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; // Incremental insertion (maintains Delaunay property) - let new_vertex = delaunay::prelude::Vertex::<(), _>::try_new([0.5, 0.5, 0.5])?; + let new_vertex = Vertex::<(), _>::try_new([0.5, 0.5, 0.5])?; dt.insert(new_vertex)?; // Vertex removal (topology-preserving, with automatic repair when enabled) @@ -111,7 +114,9 @@ use `DelaunayTriangulationBuilder`: ```rust use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyGuarantee, + Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::insertion::InsertionError; use delaunay::prelude::validation::ValidationPolicy; @@ -121,14 +126,16 @@ enum ExampleError { Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] Insertion(#[from] InsertionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), ExampleError> { // Canonicalized toroidal triangulation in 2D let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.1, 0.1])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.9, 0.9])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.5, 0.5])?, + Vertex::<(), _>::try_new([0.1, 0.1])?, + Vertex::<(), _>::try_new([0.9, 0.9])?, + Vertex::<(), _>::try_new([0.5, 0.5])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices) @@ -140,7 +147,7 @@ fn main() -> Result<(), ExampleError> { dt.set_validation_policy(ValidationPolicy::Always); // Works like any other DelaunayTriangulation - dt.insert(delaunay::prelude::Vertex::<(), _>::try_new([0.25, 0.75])?)?; + dt.insert(Vertex::<(), _>::try_new([0.25, 0.75])?)?; Ok(()) } ``` @@ -189,9 +196,10 @@ The Edit API is exposed through the `BistellarFlips` trait in `prelude::flips`: ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; use delaunay::prelude::flips::*; +use delaunay::prelude::geometry::CoordinateConversionError; #[derive(Debug, thiserror::Error)] enum ExampleError { @@ -199,15 +207,17 @@ enum ExampleError { Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] Flip(#[from] FlipError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), ExampleError> { // Start with a valid triangulation let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -215,7 +225,7 @@ fn main() -> Result<(), ExampleError> { let Some((simplex_key, _)) = dt.simplices().next() else { return Ok(()); }; - let info = dt.flip_k1_insert(simplex_key, delaunay::prelude::Vertex::<(), _>::try_new([0.25, 0.25, 0.25])?)?; + let info = dt.flip_k1_insert(simplex_key, Vertex::<(), _>::try_new([0.25, 0.25, 0.25])?)?; // k=1 inverse: Remove a vertex (collapses its star) let vertex_key = info.inserted_face_vertices[0]; @@ -312,9 +322,10 @@ You can mix both APIs in the same workflow: ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; use delaunay::prelude::flips::*; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::insertion::InsertionError; #[derive(Debug, thiserror::Error)] @@ -325,20 +336,22 @@ enum ExampleError { Insertion(#[from] InsertionError), #[error(transparent)] Flip(#[from] FlipError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), ExampleError> { // 1. Build initial triangulation (Builder API) let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; // 2. Add vertices using Builder API (maintains Delaunay) - dt.insert(delaunay::prelude::Vertex::<(), _>::try_new([0.5, 0.5, 0.5])?)?; + dt.insert(Vertex::<(), _>::try_new([0.5, 0.5, 0.5])?)?; // 3. Make custom topology edits (Edit API) let facet = /* ... */; @@ -422,11 +435,12 @@ common "repair topology then restore Delaunay" workflow: ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; use delaunay::prelude::delaunayize::{ DelaunayizeConfig, DelaunayizeError, delaunayize_by_flips, }; +use delaunay::prelude::geometry::CoordinateConversionError; #[derive(Debug, thiserror::Error)] enum ExampleError { @@ -434,14 +448,16 @@ enum ExampleError { Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] Delaunayize(#[from] DelaunayizeError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), ExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; diff --git a/docs/dev/rust.md b/docs/dev/rust.md index 73e4a949..ba9dcc89 100644 --- a/docs/dev/rust.md +++ b/docs/dev/rust.md @@ -264,9 +264,11 @@ Use fallible names for raw or invariant-bearing input: - `try_new*` is the default smart-constructor family for raw values becoming a proof-bearing domain type. -- `try_from_*`, `TryFrom`, `parse`, and `FromStr` are appropriate when the source - shape matters, especially conversions from another representation, - deserialized snapshot data, or textual/raw DTO input. +- `try_from_*`, `TryFrom`, and clearly named `parse` methods are appropriate + when the source shape matters, especially conversions from another + representation, deserialized snapshot data, or textual/raw DTO input. Prefer + these names over owned `from_str` constructors so fallibility remains visible + in the repository's constructor taxonomy. - `try_` is appropriate for fallible enum variant constructors, such as `DedupPolicy::try_epsilon`, when the variant name is the clearest API. - `try_` is appropriate for fallible builder setters, such as diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 02c53dfa..9f04a304 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -70,15 +70,24 @@ For most validation work, start with the always-available APIs: ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; + +#[derive(Debug, thiserror::Error)] +enum DiagnosticsExampleError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), +} -fn main() -> Result<(), DelaunayTriangulationConstructionError> { +fn main() -> Result<(), DiagnosticsExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -118,7 +127,7 @@ empty-circumsphere violations: ```rust use delaunay::prelude::diagnostics::delaunay_violation_report; use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::DelaunayValidationError; @@ -135,10 +144,10 @@ enum DiagnosticsExampleError { fn main() -> Result<(), DiagnosticsExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; diff --git a/docs/numerical_robustness_guide.md b/docs/numerical_robustness_guide.md index e34a971d..3c6d4c06 100644 --- a/docs/numerical_robustness_guide.md +++ b/docs/numerical_robustness_guide.md @@ -123,15 +123,15 @@ kernel, use the explicit-kernel constructors: ```rust use delaunay::prelude::geometry::RobustKernel; -use delaunay::prelude::construction::{DelaunayTriangulation}; +use delaunay::prelude::construction::{DelaunayTriangulation, Vertex}; let kernel = RobustKernel::::new(); let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let dt: DelaunayTriangulation, (), (), 3> = @@ -202,13 +202,13 @@ cases involve cavity/topology failures rather than predicate degeneracies. Use `insert_best_effort_with_statistics()` to observe this behavior: ```rust -use delaunay::prelude::construction::{DelaunayTriangulation}; +use delaunay::prelude::construction::{DelaunayTriangulation, Vertex}; use delaunay::prelude::insertion::InsertionOutcome; let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); let (outcome, stats) = dt - .insert_best_effort_with_statistics(delaunay::prelude::Vertex::<(), _>::try_new([0.5, 0.5, 0.5])?)?; + .insert_best_effort_with_statistics(Vertex::<(), _>::try_new([0.5, 0.5, 0.5])?)?; if stats.used_perturbation() { println!("used perturbation (attempts={})", stats.attempts); diff --git a/docs/topology.md b/docs/topology.md index b34824fc..6d8b8f23 100644 --- a/docs/topology.md +++ b/docs/topology.md @@ -184,12 +184,12 @@ Toroidal (periodic) triangulations are **fully implemented and functional**. You construct toroidal triangulations using `DelaunayTriangulationBuilder`: ```rust -use delaunay::prelude::construction::{DelaunayTriangulationBuilder}; +use delaunay::prelude::construction::{DelaunayTriangulationBuilder, Vertex}; // 2D canonicalized toroidal triangulation let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.1, 0.1])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.9, 0.9])?, + Vertex::<(), _>::try_new([0.1, 0.1])?, + Vertex::<(), _>::try_new([0.9, 0.9])?, // ... ]; diff --git a/docs/validation.md b/docs/validation.md index 6f8d4eac..c74fdf80 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -112,10 +112,10 @@ enum ValidationExampleError { fn main() -> Result<(), ValidationExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -178,10 +178,10 @@ enum ValidationExampleError { fn main() -> Result<(), ValidationExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -271,7 +271,7 @@ use delaunay::prelude::construction::{ }; use delaunay::prelude::validation::ValidationPolicy; -let v = delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?; +let v = Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?; assert!(v.is_valid().is_ok()); ``` @@ -339,10 +339,10 @@ enum ValidationExampleError { fn main() -> Result<(), ValidationExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -433,11 +433,11 @@ enum ValidationExampleError { fn main() -> Result<(), ValidationExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.25, 0.25, 0.25])?, // Interior point + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.25, 0.25, 0.25])?, // Interior point ]; let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -514,10 +514,10 @@ enum ValidationExampleError { fn main() -> Result<(), ValidationExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -577,6 +577,14 @@ helper. ## Common Patterns +`Triangulation::is_valid()` returns `InvariantError`, the public wrapper enum +used for validation failures across Levels 1–4. Its variants preserve the +failing layer's typed error: `TdsError` for Levels 1–2, +`TriangulationValidationError` for Level 3 topology failures, and +`DelaunayTriangulationValidationError` for Level 4 Delaunay failures. In normal +Level 3 code, handle the wrapper as shown in Patterns 2 and 3 rather than +expecting `TriangulationValidationError` directly. + ### Pattern 1: Test Suite Validation ```rust diff --git a/docs/workflows.md b/docs/workflows.md index 6eb678a3..12fef306 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -19,15 +19,24 @@ For most use cases, construction is a single call: ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; -fn main() -> Result<(), DelaunayTriangulationConstructionError> { +#[derive(Debug, thiserror::Error)] +enum ExampleError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), +} + +fn main() -> Result<(), ExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -110,8 +119,9 @@ You can also run a global repair pass manually: ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::repair::DelaunayRepairError; #[derive(Debug, thiserror::Error)] @@ -120,14 +130,16 @@ enum RepairExampleError { Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] Repair(#[from] DelaunayRepairError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), RepairExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -171,22 +183,25 @@ detections, etc.). ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::repair::DelaunayRepairError; #[derive(Debug, thiserror::Error)] enum RepairExampleError { #[error(transparent)] Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), RepairExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -223,8 +238,9 @@ from the current vertex set. ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::repair::{DelaunayRepairError, DelaunayRepairHeuristicConfig}; #[derive(Debug, thiserror::Error)] @@ -233,14 +249,16 @@ enum RepairExampleError { Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] Repair(#[from] DelaunayRepairError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), RepairExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -264,8 +282,9 @@ uses the image-point method to build a true periodic quotient in the validated ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::insertion::InsertionError; #[derive(Debug, thiserror::Error)] @@ -274,14 +293,16 @@ enum ToroidalExampleError { Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] Insertion(#[from] InsertionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), ToroidalExampleError> { // 2D canonicalized toroidal triangulation with unit square domain let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.1, 0.1])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.9, 0.9])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.5, 0.5])?, + Vertex::<(), _>::try_new([0.1, 0.1])?, + Vertex::<(), _>::try_new([0.9, 0.9])?, + Vertex::<(), _>::try_new([0.5, 0.5])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices) @@ -290,8 +311,8 @@ fn main() -> Result<(), ToroidalExampleError> { .build::<()>()?; // Insert more points - they'll be wrapped to [0,1)×[0,1) - dt.insert(delaunay::prelude::Vertex::<(), _>::try_new([1.2, 0.3])?)?; // wraps to [0.2, 0.3] - dt.insert(delaunay::prelude::Vertex::<(), _>::try_new([-0.1, 0.7])?)?; // wraps to [0.9, 0.7] + dt.insert(Vertex::<(), _>::try_new([1.2, 0.3])?)?; // wraps to [0.2, 0.3] + dt.insert(Vertex::<(), _>::try_new([-0.1, 0.7])?)?; // wraps to [0.9, 0.7] Ok(()) } ``` @@ -319,13 +340,22 @@ and modified post-construction via `set_vertex_data` / `set_simplex_data`. use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; + +#[derive(Debug, thiserror::Error)] +enum DataExampleError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), +} -fn main() -> Result<(), DelaunayTriangulationConstructionError> { +fn main() -> Result<(), DataExampleError> { // Attach integer labels at construction time let vertices: [Vertex; 3] = [ - delaunay::prelude::Vertex::<_, _>::try_new_with_data([0.0, 0.0], 10i32)?, - delaunay::prelude::Vertex::<_, _>::try_new_with_data([1.0, 0.0], 20)?, - delaunay::prelude::Vertex::<_, _>::try_new_with_data([0.0, 1.0], 30)?, + Vertex::<_, _>::try_new_with_data([0.0, 0.0], 10i32)?, + Vertex::<_, _>::try_new_with_data([1.0, 0.0], 20)?, + Vertex::<_, _>::try_new_with_data([0.0, 1.0], 30)?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -379,13 +409,23 @@ want to keep going after skipped vertices, use the explicitly best-effort `insert_best_effort_with_statistics()`. ```rust -use delaunay::prelude::construction::{DelaunayTriangulation}; +use delaunay::prelude::construction::{DelaunayTriangulation, Vertex}; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::insertion::{InsertionError, InsertionOutcome}; -fn main() -> Result<(), InsertionError> { +#[derive(Debug, thiserror::Error)] +enum InsertionStatsExampleError { + #[error(transparent)] + Insertion(#[from] InsertionError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), +} + +fn main() -> Result<(), InsertionStatsExampleError> { let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); - let (outcome, stats) = dt.insert_best_effort_with_statistics(delaunay::prelude::Vertex::<(), _>::try_new([0.5, 0.5, 0.5])?)?; + let (outcome, stats) = + dt.insert_best_effort_with_statistics(Vertex::<(), _>::try_new([0.5, 0.5, 0.5])?)?; if stats.used_perturbation() { println!("used perturbation (attempts={})", stats.attempts); @@ -415,8 +455,9 @@ the operation rolls back to the pre-removal triangulation. ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::tds::InvariantError; #[derive(Debug, thiserror::Error)] @@ -425,15 +466,17 @@ enum RemovalExampleError { Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] Invariant(#[from] InvariantError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), RemovalExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.2, 0.2, 0.2])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.2, 0.2, 0.2])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -473,23 +516,28 @@ After using flips, you typically: See [`api_design.md`](api_design.md) for the full Builder vs Edit API design. ```rust -use delaunay::prelude::construction::{DelaunayTriangulationBuilder}; +use delaunay::prelude::construction::{ + DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, +}; use delaunay::prelude::flips::*; +use delaunay::prelude::geometry::CoordinateConversionError; #[derive(Debug, thiserror::Error)] enum FlipExampleError { #[error(transparent)] - Construction(#[from] delaunay::prelude::construction::DelaunayTriangulationConstructionError), + Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] Flip(#[from] FlipError), + #[error(transparent)] + Coordinate(#[from] CoordinateConversionError), } fn main() -> Result<(), FlipExampleError> { let vertices = vec![ - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, - delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, + Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, ]; let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; @@ -497,7 +545,7 @@ fn main() -> Result<(), FlipExampleError> { let Some((simplex_key, _)) = dt.simplices().next() else { return Ok(()); }; - let info = dt.flip_k1_insert(simplex_key, delaunay::prelude::Vertex::<(), _>::try_new([0.1, 0.1, 0.1])?)?; + let info = dt.flip_k1_insert(simplex_key, Vertex::<(), _>::try_new([0.1, 0.1, 0.1])?)?; let inserted_vertex = info.inserted_face_vertices[0]; // k=1 inverse: remove the inserted vertex (collapse its star). diff --git a/semgrep.yaml b/semgrep.yaml index 03b61bfa..b2e310d5 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -614,7 +614,7 @@ rules: languages: - generic severity: WARNING - message: "Fallible constructor definitions must not use new/from_*; use try_new*, try_from_*, parse, FromStr/TryFrom, or descriptive try_* names." + message: "Fallible constructor definitions must not use new/from_*; use try_new*, try_from_*, TryFrom, parse, or descriptive try_* names." metadata: category: correctness tracking_issue: "https://github.com/acgetchell/delaunay/issues/459" diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index e9e7874b..4dc88937 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -3169,6 +3169,7 @@ pub enum FlipNeighborRepairFailure { #[error("repair postcondition failed: {reason}")] PostconditionFailed { /// Structured postcondition failure reason. + #[source] reason: DelaunayRepairPostconditionFailure, }, /// Post-repair verification could not evaluate a local flip predicate. @@ -4612,6 +4613,8 @@ impl fmt::Display for DelaunayRepairPostconditionFailure { } } +impl std::error::Error for DelaunayRepairPostconditionFailure {} + /// Structured reason orientation canonicalization failed after repair. #[derive(Clone, Debug, Error, PartialEq)] #[non_exhaustive] @@ -4910,6 +4913,7 @@ pub enum DelaunayRepairError { #[error("Delaunay repair postcondition failed: {reason}")] PostconditionFailed { /// Structured postcondition failure reason. + #[source] reason: Box, }, /// Post-repair verification could not evaluate a local flip predicate. @@ -10131,6 +10135,15 @@ mod tests { .try_init(); }); } + + fn sample_heuristic_vertex_context() -> DelaunayRepairHeuristicVertexContext { + DelaunayRepairHeuristicVertexContext { + index: 3, + vertex_uuid: Uuid::nil(), + coordinates: CoordinateValues::from([1.0, 2.0]), + } + } + /// Builds a simplex-basis vertex coordinate for dimension-generic flip tests. fn unit_vector(index: usize) -> [f64; D] { let mut coords = [0.0; D]; @@ -11182,19 +11195,17 @@ mod tests { &[FacetHandle::from_validated(external_simplex_key, 0)], ); - assert!( - matches!( - result, - Err(FlipError::InvalidFlipContext { ref reason }) - if matches!( - reason.as_ref(), - FlipContextError::ConflictingReplacementPeriodicFrameTranslation { - source_simplex_key, - target_simplex_index: 0, - .. - } if *source_simplex_key == external_simplex_key - ) - ), + assert_matches!( + &result, + Err(FlipError::InvalidFlipContext { reason }) + if matches!( + reason.as_ref(), + FlipContextError::ConflictingReplacementPeriodicFrameTranslation { + source_simplex_key, + target_simplex_index: 0, + .. + } if *source_simplex_key == external_simplex_key + ), "conflicting periodic external facet translations should fail before mutation: {result:?}" ); } @@ -11213,18 +11224,16 @@ mod tests { &[], ); - assert!( - matches!( - result, - Err(FlipError::InvalidFlipContext { ref reason }) - if matches!( - reason.as_ref(), - FlipContextError::ReplacementPeriodicOffsetCountMismatch { - simplex_count: 1, - offset_count: 0, - } - ) - ), + assert_matches!( + &result, + Err(FlipError::InvalidFlipContext { reason }) + if matches!( + reason.as_ref(), + FlipContextError::ReplacementPeriodicOffsetCountMismatch { + simplex_count: 1, + offset_count: 0, + } + ), "replacement offset sidecar length mismatch should fail explicitly: {result:?}" ); } @@ -11249,17 +11258,15 @@ mod tests { &[FacetHandle::from_validated(external_simplex_key, 0)], ); - assert!( - matches!( - result, - Err(FlipError::InvalidFlipContext { ref reason }) - if matches!( - reason.as_ref(), - FlipContextError::MissingReplacementPeriodicOffsets { - simplex_index: 0, - } - ) - ), + assert_matches!( + &result, + Err(FlipError::InvalidFlipContext { reason }) + if matches!( + reason.as_ref(), + FlipContextError::MissingReplacementPeriodicOffsets { + simplex_index: 0, + } + ), "periodic external parity should require replacement offsets: {result:?}" ); } @@ -11286,19 +11293,17 @@ mod tests { &[FacetHandle::from_validated(external_simplex_key, 0)], ); - assert!( - matches!( - result, - Err(FlipError::InvalidFlipContext { ref reason }) - if matches!( - reason.as_ref(), - FlipContextError::ReplacementPeriodicOffsetLengthMismatch { - simplex_index: 0, - offset_count: $dim, - vertex_count, - } if *vertex_count == $dim + 1 - ) - ), + assert_matches!( + &result, + Err(FlipError::InvalidFlipContext { reason }) + if matches!( + reason.as_ref(), + FlipContextError::ReplacementPeriodicOffsetLengthMismatch { + simplex_index: 0, + offset_count: $dim, + vertex_count, + } if *vertex_count == $dim + 1 + ), "replacement periodic offsets should stay slot-aligned with vertices: {result:?}" ); } @@ -11322,11 +11327,9 @@ mod tests { &[FacetHandle::from_validated(external_simplex_key, 0)], ); - assert!( - matches!( - result, - Err(FlipError::MissingSimplex { simplex_key }) if simplex_key == external_simplex_key - ), + assert_matches!( + &result, + Err(FlipError::MissingSimplex { simplex_key }) if *simplex_key == external_simplex_key, "missing external simplex should fail explicitly: {result:?}" ); } @@ -11361,18 +11364,16 @@ mod tests { .filter_map(|(idx, &vertex)| (idx != 1).then_some(vertex)) .collect::>(); assert_eq!(order.iter().copied().collect::>(), expected_order); - assert!( - matches!( - facet_order(&source, source.len()), - Err(FlipError::InvalidFlipContext { ref reason }) - if matches!( - reason.as_ref(), - FlipContextError::ReplacementFacetIndexOutOfRange { - facet_index, - vertex_count, - } if *facet_index == source.len() && *vertex_count == source.len() - ) - ), + assert_matches!( + facet_order(&source, source.len()), + Err(FlipError::InvalidFlipContext { reason }) + if matches!( + reason.as_ref(), + FlipContextError::ReplacementFacetIndexOutOfRange { + facet_index, + vertex_count, + } if *facet_index == source.len() && *vertex_count == source.len() + ), "out-of-range facet indices should be rejected" ); @@ -11385,11 +11386,9 @@ mod tests { assert_eq!(shared_facet_indices(&source, &two_unique), None); assert!(!facet_orders_coherent(&source, $dim, &neighbor, $dim).unwrap()); - assert!( - matches!( - facet_orders_coherent(&source, source.len(), &neighbor, $dim), - Err(FlipError::InvalidFlipContext { .. }) - ), + assert_matches!( + facet_orders_coherent(&source, source.len(), &neighbor, $dim), + Err(FlipError::InvalidFlipContext { .. }), "invalid facet-order constraints should surface as invalid context" ); @@ -11409,30 +11408,26 @@ mod tests { assert!(set_flip_assignment(&mut assignments, 0, true).unwrap()); assert_eq!(assignments[0], Some(true)); assert!(!set_flip_assignment(&mut assignments, 0, true).unwrap()); - assert!( - matches!( - set_flip_assignment(&mut assignments, 0, false), - Err(FlipError::InvalidFlipContext { ref reason }) - if matches!( - reason.as_ref(), - FlipContextError::ConflictingReplacementOrientationForSimplex { - simplex_index: 0, - } - ) - ), + assert_matches!( + set_flip_assignment(&mut assignments, 0, false), + Err(FlipError::InvalidFlipContext { reason }) + if matches!( + reason.as_ref(), + FlipContextError::ConflictingReplacementOrientationForSimplex { + simplex_index: 0, + } + ), "conflicting parity assignments should fail" ); - assert!( - matches!( - set_flip_assignment(&mut assignments, 1, false), - Err(FlipError::InvalidFlipContext { reason }) - if matches!( - reason.as_ref(), - FlipContextError::ReplacementOrientationIndexOutOfRange { - simplex_index: 1, - } - ) - ), + assert_matches!( + set_flip_assignment(&mut assignments, 1, false), + Err(FlipError::InvalidFlipContext { reason }) + if matches!( + reason.as_ref(), + FlipContextError::ReplacementOrientationIndexOutOfRange { + simplex_index: 1, + } + ), "out-of-range parity assignments should fail" ); } @@ -11513,12 +11508,10 @@ mod tests { negative_vertices.swap(1, 2); let negative = vertex_key_buffer(&negative_vertices); let negative_result = validate_replacement_orientation(&tds, &[negative]); - assert!( - matches!( - negative_result, - Err(FlipError::NegativeOrientation { ref simplex_vertices }) - if simplex_vertices == &negative_vertices - ), + assert_matches!( + &negative_result, + Err(FlipError::NegativeOrientation { simplex_vertices }) + if simplex_vertices == &negative_vertices, "negative replacement simplices should fail before mutation: {negative_result:?}" ); @@ -11526,8 +11519,9 @@ mod tests { degenerate_vertices[$dim] = v_collinear; let degenerate = vertex_key_buffer(°enerate_vertices); let degenerate_result = validate_replacement_orientation(&tds, &[degenerate]); - assert!( - matches!(degenerate_result, Err(FlipError::DegenerateSimplex)), + assert_matches!( + °enerate_result, + Err(FlipError::DegenerateSimplex), "degenerate replacement simplices should fail before mutation: {degenerate_result:?}" ); } @@ -15011,6 +15005,197 @@ mod tests { assert_ne!(post_test, canonicalization_err); } + #[test] + fn test_postcondition_failure_display_covers_variants() { + let simplex = SimplexKey::from(KeyData::from_ffi(91)); + let v0 = VertexKey::from(KeyData::from_ffi(101)); + let v1 = VertexKey::from(KeyData::from_ffi(102)); + let v2 = VertexKey::from(KeyData::from_ffi(103)); + let facet = FacetHandle::from_validated(simplex, 0); + let ridge = RidgeHandle::from_validated(simplex, 0, 1); + let edge = EdgeKey::from_validated_endpoints(v0, v1); + let triangle = TriangleHandle::try_new(v0, v1, v2).unwrap(); + + assert_eq!( + DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 2 }.to_string(), + "repair pass disconnected the triangulation (2 simplices remain); neighbor wiring is incomplete" + ); + + let k2 = DelaunayRepairPostconditionFailure::LocalK2Violation { + facet, + debug_details: Some("debug facet snapshot".to_string()), + } + .to_string(); + assert!(k2.contains("local k=2 violation remains after repair")); + assert!(k2.contains("debug facet snapshot")); + + let k3 = DelaunayRepairPostconditionFailure::LocalK3Violation { ridge }.to_string(); + assert!(k3.contains("local k=3 violation remains after repair")); + + let inverse_k2 = + DelaunayRepairPostconditionFailure::LocalInverseK2Violation { edge }.to_string(); + assert!(inverse_k2.contains("local inverse k=2 flip remains applicable after repair")); + + let inverse_k3 = + DelaunayRepairPostconditionFailure::LocalInverseK3Violation { triangle }.to_string(); + assert!(inverse_k3.contains("local inverse k=3 flip remains applicable after repair")); + } + + #[test] + fn test_postcondition_failure_exposes_source() { + let reason = DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }; + let repair = DelaunayRepairError::PostconditionFailed { + reason: Box::new(reason.clone()), + }; + let source = repair + .source() + .and_then(|source| source.downcast_ref::>()); + assert_eq!(source.map(Box::as_ref), Some(&reason)); + + let neighbor_repair = FlipNeighborRepairFailure::PostconditionFailed { reason }; + assert_matches!( + std::error::Error::source(&neighbor_repair) + .and_then(|source| source.downcast_ref::()), + Some(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }) + ); + } + + #[test] + fn test_heuristic_vertex_context_display() { + let context = sample_heuristic_vertex_context().to_string(); + + assert!(context.contains("idx=3")); + assert!(context.contains("uuid=00000000-0000-0000-0000-000000000000")); + assert!(context.contains("coords=[1.0, 2.0]")); + } + + #[test] + fn test_orientation_failure_kind_conversion() { + let orientation_failure = + DelaunayRepairOrientationCanonicalizationFailure::AfterFlipRepair { + source: Box::new(InsertionError::DuplicateCoordinates { + coordinates: CoordinateValues::from([0.0, 0.0]), + }), + }; + + assert_eq!( + DelaunayRepairOrientationCanonicalizationFailureKind::from(&orientation_failure), + DelaunayRepairOrientationCanonicalizationFailureKind::AfterFlipRepair { + source_kind: InsertionErrorKind::DuplicateCoordinates, + }, + ); + + let orientation_repair = DelaunayRepairError::OrientationCanonicalizationFailed { + reason: Box::new(orientation_failure), + }; + assert_matches!( + FlipNeighborRepairFailure::from(orientation_repair), + FlipNeighborRepairFailure::OrientationCanonicalizationFailed { + reason: DelaunayRepairOrientationCanonicalizationFailureKind::AfterFlipRepair { + source_kind: InsertionErrorKind::DuplicateCoordinates + } + } + ); + } + + #[test] + fn test_heuristic_rebuild_failure_kind_conversion() { + let insertion_failure = InsertionError::DuplicateCoordinates { + coordinates: CoordinateValues::from([0.0, 0.0]), + }; + let repair_source = || DelaunayRepairError::from(FlipError::DegenerateSimplex); + let vertex = sample_heuristic_vertex_context(); + let heuristic_cases = [ + ( + DelaunayRepairHeuristicRebuildFailure::RecursionDepthExceeded { max_depth: 1 }, + DelaunayRepairHeuristicRebuildFailureKind::RecursionDepthExceeded, + ), + ( + DelaunayRepairHeuristicRebuildFailure::FallbackChainFailed { + primary: Box::new(repair_source()), + robust: Box::new(repair_source()), + heuristic: Box::new(DelaunayRepairHeuristicRebuildFailure::NoAttempts), + }, + DelaunayRepairHeuristicRebuildFailureKind::FallbackChainFailed, + ), + ( + DelaunayRepairHeuristicRebuildFailure::UnexpectedRepairFailure { + source: Box::new(repair_source()), + }, + DelaunayRepairHeuristicRebuildFailureKind::UnexpectedRepairFailure, + ), + ( + DelaunayRepairHeuristicRebuildFailure::NoAttempts, + DelaunayRepairHeuristicRebuildFailureKind::NoAttempts, + ), + ( + DelaunayRepairHeuristicRebuildFailure::InsertionFailed { + vertex: vertex.clone(), + source: Box::new(insertion_failure.clone()), + }, + DelaunayRepairHeuristicRebuildFailureKind::InsertionFailed, + ), + ( + DelaunayRepairHeuristicRebuildFailure::RepairFailed { + vertex: vertex.clone(), + source: Box::new(insertion_failure.clone()), + }, + DelaunayRepairHeuristicRebuildFailureKind::RepairFailed, + ), + ( + DelaunayRepairHeuristicRebuildFailure::DelaunayCheckFailed { + vertex: vertex.clone(), + source: Box::new(insertion_failure.clone()), + }, + DelaunayRepairHeuristicRebuildFailureKind::DelaunayCheckFailed, + ), + ( + DelaunayRepairHeuristicRebuildFailure::SkippedVertex { + vertex, + source: Box::new(insertion_failure), + }, + DelaunayRepairHeuristicRebuildFailureKind::SkippedVertex, + ), + ( + DelaunayRepairHeuristicRebuildFailure::AttemptFailed { + attempt: 1, + max_attempts: 2, + shuffle_seed: 3, + perturbation_seed: 4, + source: Box::new(repair_source()), + }, + DelaunayRepairHeuristicRebuildFailureKind::AttemptFailed, + ), + ( + DelaunayRepairHeuristicRebuildFailure::ExhaustedAttempts { + attempts: 2, + last_failure: Box::new(DelaunayRepairHeuristicRebuildFailure::NoAttempts), + }, + DelaunayRepairHeuristicRebuildFailureKind::ExhaustedAttempts, + ), + ]; + + for (failure, expected_kind) in heuristic_cases { + assert_eq!( + DelaunayRepairHeuristicRebuildFailureKind::from(&failure), + expected_kind, + ); + } + } + + #[test] + fn test_flip_neighbor_repair_failure_conversion() { + let heuristic_repair = DelaunayRepairError::HeuristicRebuildFailed { + reason: Box::new(DelaunayRepairHeuristicRebuildFailure::NoAttempts), + }; + assert_matches!( + FlipNeighborRepairFailure::from(heuristic_repair), + FlipNeighborRepairFailure::HeuristicRebuildFailed { + reason: DelaunayRepairHeuristicRebuildFailureKind::NoAttempts + } + ); + } + #[test] fn test_delaunay_repair_error_boxes_large_flip_sources() { assert!( diff --git a/src/core/algorithms/incremental_insertion.rs b/src/core/algorithms/incremental_insertion.rs index 37d7c0c7..b93abfc7 100644 --- a/src/core/algorithms/incremental_insertion.rs +++ b/src/core/algorithms/incremental_insertion.rs @@ -35,7 +35,9 @@ use crate::core::collections::{ FastHashMap, FastHashSet, FastHasher, MAX_PRACTICAL_DIMENSION_SIZE, SimplexKeyBuffer, SmallBuffer, VertexKeyBuffer, }; -use crate::core::construction::TriangulationConstructionError; +use crate::core::construction::{ + FinalDelaunayValidationContext, FinalTopologyValidationContext, TriangulationConstructionError, +}; use crate::core::facet::{FacetError, FacetHandle}; use crate::core::simplex::{NeighborSlot, Simplex, SimplexValidationError}; use crate::core::tds::{ @@ -69,12 +71,16 @@ use std::hash::{Hash, Hasher}; /// let reason = HullExtensionReason::NoVisibleFacets; /// std::assert_matches!(reason, HullExtensionReason::NoVisibleFacets); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, thiserror::Error, PartialEq)] #[non_exhaustive] pub enum HullExtensionReason { /// No visible boundary facets (coplanar with hull surface). + #[error( + "No visible boundary facets found for exterior vertex (may be coplanar with hull surface)" + )] NoVisibleFacets, /// Visible facets form an invalid patch. + #[error("Visible boundary facets are not a valid patch: {details}")] InvalidPatch { /// Details about why the patch was invalid. details: String, @@ -83,30 +89,14 @@ pub enum HullExtensionReason { /// /// Preserves the structured [`CoordinateConversionError`] from the kernel or /// robust-predicate evaluation rather than collapsing it into a string. - PredicateFailed(CoordinateConversionError), + #[error("Geometric predicate failed: {0}")] + PredicateFailed(#[source] CoordinateConversionError), /// Lower-layer TDS error encountered during hull extension. /// /// Preserves the structured [`TdsError`] (e.g. from boundary-facet retrieval) /// rather than collapsing it into a string. - Tds(TdsError), -} - -impl fmt::Display for HullExtensionReason { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NoVisibleFacets => f.write_str( - "No visible boundary facets found for exterior vertex (may be coplanar with hull surface)", - ), - Self::InvalidPatch { details } => write!( - f, - "Visible boundary facets are not a valid patch: {details}" - ), - Self::PredicateFailed(source) => { - write!(f, "Geometric predicate failed: {source}") - } - Self::Tds(source) => write!(f, "TDS error: {source}"), - } - } + #[error("TDS error: {0}")] + Tds(#[source] TdsError), } /// Fixed context for a Level 3 topology validation failure during insertion. @@ -538,6 +528,7 @@ pub enum InitialSimplexUnexpectedInsertionStage { #[error("hull extension failed during insertion: {reason}")] HullExtension { /// Structured hull-extension failure reason. + #[source] reason: HullExtensionReason, }, @@ -550,21 +541,35 @@ pub enum InitialSimplexUnexpectedInsertionStage { }, /// Topology validation escaped initial-simplex construction. - #[error("topology validation failed during insertion: {source}")] + #[error("{context}: {source}")] TopologyValidation { + /// Validation phase that failed. + context: InsertionTopologyValidationContext, /// Underlying topology validation error. #[source] source: Box, }, /// Final topology validation escaped initial-simplex construction. - #[error("final topology validation failed after construction: {source}")] + #[error("{context}: {source}")] FinalTopologyValidation { + /// Finalization phase that failed. + context: FinalTopologyValidationContext, /// Underlying final topology validation summary. #[source] source: InvariantErrorSummary, }, + /// Final Delaunay validation escaped initial-simplex construction. + #[error("{context}: {source}")] + FinalDelaunayValidation { + /// Finalization phase that failed. + context: FinalDelaunayValidationContext, + /// Underlying final Delaunay validation error. + #[source] + source: DelaunayTriangulationValidationError, + }, + /// Spatial index construction escaped initial-simplex construction. #[error("spatial index construction failed during insertion: {reason}")] SpatialIndexConstruction { @@ -682,6 +687,10 @@ impl From for InitialSimplexConstructionError { } impl From for InitialSimplexConstructionError { + #[expect( + clippy::too_many_lines, + reason = "Conversion must exhaustively preserve typed construction error variants" + )] fn from(source: TriangulationConstructionError) -> Self { match source { TriangulationConstructionError::Tds(source) => source.into(), @@ -760,9 +769,10 @@ impl From for InitialSimplexConstructionError { }), } } - TriangulationConstructionError::InsertionTopologyValidation { source, .. } => { + TriangulationConstructionError::InsertionTopologyValidation { context, source } => { Self::UnexpectedInsertionStage { reason: Box::new(InitialSimplexUnexpectedInsertionStage::TopologyValidation { + context, source: Box::new(source), }), } @@ -774,10 +784,23 @@ impl From for InitialSimplexConstructionError { max_simplices_removed, attempted, }, - TriangulationConstructionError::FinalTopologyValidation { source, .. } => { + TriangulationConstructionError::FinalTopologyValidation { context, source } => { + Self::UnexpectedInsertionStage { + reason: Box::new( + InitialSimplexUnexpectedInsertionStage::FinalTopologyValidation { + context, + source, + }, + ), + } + } + TriangulationConstructionError::FinalDelaunayValidation { context, source } => { Self::UnexpectedInsertionStage { reason: Box::new( - InitialSimplexUnexpectedInsertionStage::FinalTopologyValidation { source }, + InitialSimplexUnexpectedInsertionStage::FinalDelaunayValidation { + context, + source, + }, ), } } @@ -1627,6 +1650,7 @@ pub enum InsertionError { #[error("Hull extension failed: {reason}")] HullExtension { /// Structured reason for failure. + #[source] reason: HullExtensionReason, }, @@ -1983,13 +2007,14 @@ impl InsertionError { HullExtensionReason::NoVisibleFacets | HullExtensionReason::InvalidPatch { .. } ) } - InitialSimplexUnexpectedInsertionStage::TopologyValidation { source } => { + InitialSimplexUnexpectedInsertionStage::TopologyValidation { source, .. } => { Self::is_level3_error_retryable(source) } - InitialSimplexUnexpectedInsertionStage::FinalTopologyValidation { source } => { + InitialSimplexUnexpectedInsertionStage::FinalTopologyValidation { source, .. } => { Self::is_invariant_error_summary_retryable(source) } - InitialSimplexUnexpectedInsertionStage::Location { .. } + InitialSimplexUnexpectedInsertionStage::FinalDelaunayValidation { .. } + | InitialSimplexUnexpectedInsertionStage::Location { .. } | InitialSimplexUnexpectedInsertionStage::DelaunayValidation { .. } | InitialSimplexUnexpectedInsertionStage::SpatialIndexConstruction { .. } => false, } @@ -4634,6 +4659,70 @@ mod tests { None } + #[test] + fn test_topology_validation_context_display() { + let cases = [ + ( + InsertionTopologyValidationContext::InvariantConversion, + "topology validation failed", + ), + ( + InsertionTopologyValidationContext::PostInsertion, + "post-insertion topology validation failed", + ), + ( + InsertionTopologyValidationContext::LocalRepair, + "local topology validation failed", + ), + ( + InsertionTopologyValidationContext::StructuralRepair, + "structural topology validation failed", + ), + ( + InsertionTopologyValidationContext::StaleIncidentSimplexRepair, + "truly isolated vertex detected during stale incident-simplex repair", + ), + ( + InsertionTopologyValidationContext::PositiveOrientationPromotion, + "positive-orientation promotion failed to converge", + ), + ( + InsertionTopologyValidationContext::DelaunayRepair, + "topology invalid after Delaunay repair", + ), + ]; + + for (context, expected) in cases { + assert_eq!(context.to_string(), expected); + } + } + + #[test] + fn test_hull_extension_reason_exposes_typed_sources() { + let predicate_source = CoordinateConversionError::InvalidSimplexPointCount { + actual: 2, + expected: 3, + dimension: 2, + }; + let predicate_reason = HullExtensionReason::PredicateFailed(predicate_source.clone()); + let predicate_error = std::error::Error::source(&predicate_reason) + .and_then(|source| source.downcast_ref::()); + assert_eq!(predicate_error, Some(&predicate_source)); + + let tds_source = TdsError::InconsistentDataStructure { + message: "missing boundary facet".to_string(), + }; + let tds_reason = HullExtensionReason::Tds(tds_source.clone()); + let tds_error = std::error::Error::source(&tds_reason) + .and_then(|source| source.downcast_ref::()); + assert_eq!(tds_error, Some(&tds_source)); + + let insertion_error = InsertionError::HullExtension { reason: tds_reason }; + let insertion_source = std::error::Error::source(&insertion_error) + .and_then(|source| source.downcast_ref::()); + assert_matches!(insertion_source, Some(HullExtensionReason::Tds(_))); + } + /// Macro to generate cavity filling tests for different dimensions macro_rules! test_fill_cavity { ($dim:literal, $initial_vertices:expr, $new_vertex:expr, $expected_facets:literal) => { diff --git a/src/core/construction.rs b/src/core/construction.rs index 74a75d59..f85b7e1d 100644 --- a/src/core/construction.rs +++ b/src/core/construction.rs @@ -26,7 +26,12 @@ use crate::geometry::traits::coordinate::CoordinateValues; use crate::validation::DelaunayTriangulationValidationError; use thiserror::Error; -/// Fixed context for final topology validation after construction. +/// Classifies the construction phase that failed final Levels 1–3 validation. +/// +/// This context is carried by +/// [`TriangulationConstructionError::FinalTopologyValidation`] so callers can +/// distinguish ordinary construction finalization from periodic-quotient and +/// random-generation validation failures without parsing display text. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum FinalTopologyValidationContext { @@ -34,8 +39,6 @@ pub enum FinalTopologyValidationContext { ConstructionFinalize, /// Final Levels 1-3 topology validation for a periodic quotient. PeriodicQuotientTopology, - /// Final Level 4 Delaunay validation for a periodic quotient. - PeriodicQuotientDelaunay, /// Final Levels 1-3 topology validation for a generated random triangulation. RandomGeneration, } @@ -49,9 +52,6 @@ impl std::fmt::Display for FinalTopologyValidationContext { Self::PeriodicQuotientTopology => { f.write_str("periodic quotient failed final Levels 1-3 topology validation") } - Self::PeriodicQuotientDelaunay => { - f.write_str("periodic quotient failed final Level 4 Delaunay validation") - } Self::RandomGeneration => { f.write_str("random triangulation failed final Levels 1-3 topology validation") } @@ -59,6 +59,34 @@ impl std::fmt::Display for FinalTopologyValidationContext { } } +/// Classifies the construction phase that failed final Level 4 validation. +/// +/// This context is carried by +/// [`TriangulationConstructionError::FinalDelaunayValidation`] so callers can +/// distinguish ordinary construction finalization from periodic-quotient +/// Delaunay checks without collapsing the typed source error. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum FinalDelaunayValidationContext { + /// Standard final Level 4 Delaunay validation after construction. + ConstructionFinalize, + /// Final Level 4 Delaunay validation for a periodic quotient. + PeriodicQuotientDelaunay, +} + +impl std::fmt::Display for FinalDelaunayValidationContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ConstructionFinalize => { + f.write_str("Delaunay validation failed after construction") + } + Self::PeriodicQuotientDelaunay => { + f.write_str("periodic quotient failed final Level 4 Delaunay validation") + } + } + } +} + /// Errors that can occur during triangulation construction. /// /// # Examples @@ -115,6 +143,7 @@ pub enum TriangulationConstructionError { /// The dimension that was attempted. dimension: usize, /// The underlying simplex validation error. + #[source] source: SimplexValidationError, }, @@ -169,6 +198,7 @@ pub enum TriangulationConstructionError { #[error("Hull extension failed during insertion: {reason}")] InsertionHullExtension { /// Structured hull-extension failure reason. + #[source] reason: HullExtensionReason, }, @@ -207,13 +237,23 @@ pub enum TriangulationConstructionError { /// for post-build checks that run after the incremental insertion phase. #[error("{context}: {source}")] FinalTopologyValidation { - /// High-level finalization context. + /// Finalization phase that produced the validation failure. context: FinalTopologyValidationContext, /// Underlying validation error. #[source] source: InvariantErrorSummary, }, + /// Final Delaunay validation failed after construction. + #[error("{context}: {source}")] + FinalDelaunayValidation { + /// Finalization phase that produced the validation failure. + context: FinalDelaunayValidationContext, + /// Underlying Delaunay validation error. + #[source] + source: DelaunayTriangulationValidationError, + }, + /// Attempted to insert a vertex with coordinates that already exist. #[error( "Duplicate coordinates: vertex with coordinates {coordinates} already exists in the triangulation" @@ -414,6 +454,74 @@ mod tests { ); } + #[test] + fn final_topology_validation_context_display() { + let cases = [ + ( + FinalTopologyValidationContext::ConstructionFinalize, + "topology validation failed after construction", + ), + ( + FinalTopologyValidationContext::PeriodicQuotientTopology, + "periodic quotient failed final Levels 1-3 topology validation", + ), + ( + FinalTopologyValidationContext::RandomGeneration, + "random triangulation failed final Levels 1-3 topology validation", + ), + ]; + + for (context, expected) in cases { + assert_eq!(context.to_string(), expected); + } + } + + #[test] + fn final_delaunay_validation_context_display() { + assert_eq!( + FinalDelaunayValidationContext::ConstructionFinalize.to_string(), + "Delaunay validation failed after construction" + ); + assert_eq!( + FinalDelaunayValidationContext::PeriodicQuotientDelaunay.to_string(), + "periodic quotient failed final Level 4 Delaunay validation" + ); + } + + #[test] + fn insertion_hull_extension_exposes_typed_source() { + let reason = HullExtensionReason::Tds(TdsError::InconsistentDataStructure { + message: "missing boundary facet".to_string(), + }); + let error = TriangulationConstructionError::InsertionHullExtension { reason }; + let source = std::error::Error::source(&error) + .and_then(|source| source.downcast_ref::()); + assert_matches!(source, Some(HullExtensionReason::Tds(_))); + } + + #[test] + fn insufficient_vertices_exposes_typed_source() { + let error = TriangulationConstructionError::InsufficientVertices { + dimension: 3, + source: SimplexValidationError::InsufficientVertices { + actual: 3, + expected: 4, + dimension: 3, + }, + }; + + let source = std::error::Error::source(&error) + .and_then(|source| source.downcast_ref::()); + assert_matches!( + source, + Some(SimplexValidationError::InsufficientVertices { + actual: 3, + expected: 4, + dimension: 3, + }) + ); + } + macro_rules! test_build_initial_simplex { ($dim:expr, [$($simplex_coords:expr),+ $(,)?]) => { pastey::paste! { diff --git a/src/core/orientation.rs b/src/core/orientation.rs index 1ae8e8a9..6840954a 100644 --- a/src/core/orientation.rs +++ b/src/core/orientation.rs @@ -612,13 +612,11 @@ mod tests { let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); let err = tri.validate_geometric_simplex_orientation().unwrap_err(); - assert!( - matches!( - &err, - TdsError::Geometric(GeometricError::NegativeOrientation { message }) - if message.contains("negative geometric orientation") - && message.contains("vertices") - ), + assert_matches!( + &err, + TdsError::Geometric(GeometricError::NegativeOrientation { message }) + if message.contains("negative geometric orientation") + && message.contains("vertices"), "Error should contain vertex keys: {err}" ); } diff --git a/src/core/query.rs b/src/core/query.rs index 15790497..cdb9ddf2 100644 --- a/src/core/query.rs +++ b/src/core/query.rs @@ -15,7 +15,6 @@ use crate::core::simplex::Simplex; use crate::core::tds::{SimplexKey, TdsError, VertexKey}; use crate::core::triangulation::Triangulation; use crate::core::vertex::Vertex; -use crate::geometry::kernel::Kernel; use std::sync::Arc; #[cfg(debug_assertions)] use std::sync::atomic::{AtomicU64, Ordering}; @@ -92,10 +91,7 @@ pub enum QueryError { }, } -impl Triangulation -where - K: Kernel, -{ +impl Triangulation { /// Returns an iterator over all simplices in the triangulation. /// /// Delegates to the underlying Tds. diff --git a/src/core/tds.rs b/src/core/tds.rs index 666f3747..1d55cbd7 100644 --- a/src/core/tds.rs +++ b/src/core/tds.rs @@ -1058,9 +1058,9 @@ pub enum TdsError { }, /// Internal data structure inconsistency. /// - /// This is the catch-all for structural invariant violations that do not - /// fit a more specific variant (e.g. topology contradictions, error - /// wrapping, operational failures). Prefer [`SimplexNotFound`], + /// This is the fallback for structural invariant violations that carry + /// open-ended diagnostic context and do not fit a more specific variant. + /// Prefer [`SimplexNotFound`], /// [`VertexNotFound`], [`DimensionMismatch`], or [`IndexOutOfBounds`] /// when applicable. /// @@ -8073,8 +8073,9 @@ mod tests { // Same coordinates again (distinct UUID, constructed via Vertex smart constructors) let result = dt.insert(duplicate); - assert!( - matches!(result, Err(InsertionError::DuplicateCoordinates { .. })), + assert_matches!( + &result, + Err(InsertionError::DuplicateCoordinates { .. }), "insert() should reject duplicate coordinates created via Vertex::try_new (before UUID), got: {result:?}" ); } @@ -8094,14 +8095,12 @@ mod tests { None, ); let result = dt.insert(vertex2); - assert!( - matches!( - result, - Err(InsertionError::DuplicateUuid { - entity: EntityKind::Vertex, - .. - }) - ), + assert_matches!( + &result, + Err(InsertionError::DuplicateUuid { + entity: EntityKind::Vertex, + .. + }), "Same UUID with different coordinates should fail with DuplicateUuid" ); } @@ -11154,8 +11153,9 @@ mod tests { .unwrap(); let err = tds.validate_simplex_coordinate_uniqueness().unwrap_err(); - assert!( - matches!(err, TdsError::DuplicateCoordinatesInSimplex { .. }), + assert_matches!( + &err, + TdsError::DuplicateCoordinatesInSimplex { .. }, "Expected DuplicateCoordinatesInSimplex, got {err:?}" ); } @@ -11205,25 +11205,24 @@ mod tests { let err = tds.validate_facet_sharing().unwrap_err(); let message = err.to_string(); - assert!( - matches!( - &err, - TdsError::FacetSharingViolation { - existing_incident_count: 2, - attempted_incident_count: 3, - max_incident_count: 2, - candidate_facet_index: 2, - .. - } - ), + assert_matches!( + &err, + TdsError::FacetSharingViolation { + existing_incident_count: 2, + attempted_incident_count: 3, + max_incident_count: 2, + candidate_facet_index: 2, + .. + }, "Expected over-shared facet error, got {err:?}" ); assert!(message.contains("exceeds incident-simplex limit")); assert!(!message.contains("inserting candidate simplex")); let err = tds.is_valid().unwrap_err(); - assert!( - matches!(err, TdsError::FacetSharingViolation { .. }), + assert_matches!( + &err, + TdsError::FacetSharingViolation { .. }, "Expected is_valid to surface facet-sharing violation, got {err:?}" ); @@ -11233,11 +11232,9 @@ mod tests { .iter() .find(|violation| violation.kind == InvariantKind::FacetSharing) .expect("validation_report should include the facet-sharing violation"); - assert!( - matches!( - &facet_violation.error, - InvariantError::Tds(TdsError::FacetSharingViolation { .. }) - ), + assert_matches!( + &facet_violation.error, + InvariantError::Tds(TdsError::FacetSharingViolation { .. }), "Expected validation_report to preserve structured facet-sharing error, got {:?}", facet_violation.error ); diff --git a/src/core/validation.rs b/src/core/validation.rs index db2577a1..34752578 100644 --- a/src/core/validation.rs +++ b/src/core/validation.rs @@ -669,10 +669,7 @@ pub(crate) enum InsertionValidationWork { RequiredTopologyLinks, } -impl Triangulation -where - K: Kernel, -{ +impl Triangulation { /// Returns the topology guarantee used for Level 3 topology validation. #[inline] #[must_use] @@ -833,7 +830,12 @@ where tracing::warn!("{err}. Topology guarantee not updated."); } } +} +impl Triangulation +where + K: Kernel, +{ /// Traverses the simplex neighbor graph for validation without assuming global connectivity. /// /// If `allowed` is `Some`, traversal is restricted to that set. Neighbors diff --git a/src/delaunay/builder.rs b/src/delaunay/builder.rs index 8bc30009..3b163ee6 100644 --- a/src/delaunay/builder.rs +++ b/src/delaunay/builder.rs @@ -151,7 +151,9 @@ use crate::core::collections::{ FastHashMap, MAX_PRACTICAL_DIMENSION_SIZE, PeriodicOffsetBuffer, SmallBuffer, Uuid, VertexKeySet, }; -use crate::core::construction::{FinalTopologyValidationContext, TriangulationConstructionError}; +use crate::core::construction::{ + FinalDelaunayValidationContext, FinalTopologyValidationContext, TriangulationConstructionError, +}; use crate::core::operations::InsertionOutcome; use crate::core::simplex::{Simplex, SimplexValidationError}; use crate::core::tds::{ @@ -2012,9 +2014,9 @@ where } })?; dt.is_valid().map_err(|e| { - TriangulationConstructionError::FinalTopologyValidation { - context: FinalTopologyValidationContext::PeriodicQuotientDelaunay, - source: InvariantError::Delaunay(e).into(), + TriangulationConstructionError::FinalDelaunayValidation { + context: FinalDelaunayValidationContext::PeriodicQuotientDelaunay, + source: e, } })?; Ok(dt) diff --git a/src/delaunay/construction.rs b/src/delaunay/construction.rs index eeae6038..d4db7cba 100644 --- a/src/delaunay/construction.rs +++ b/src/delaunay/construction.rs @@ -66,7 +66,9 @@ use crate::core::collections::{ FastHashSet, FastHasher, MAX_PRACTICAL_DIMENSION_SIZE, SecureHashMap, SimplexKeyBuffer, SmallBuffer, }; -use crate::core::construction::{FinalTopologyValidationContext, TriangulationConstructionError}; +use crate::core::construction::{ + FinalDelaunayValidationContext, FinalTopologyValidationContext, TriangulationConstructionError, +}; use crate::core::insertion::record_duplicate_detection_metrics; use crate::core::operations::{ DelaunayInsertionState, InsertionOutcome, InsertionResult, InsertionStatistics, @@ -396,6 +398,7 @@ pub enum DelaunayConstructionFailure { #[error("hull extension failed during insertion: {reason}")] InsertionHullExtension { /// Structured hull-extension failure reason. + #[source] reason: HullExtensionReason, }, @@ -431,7 +434,7 @@ pub enum DelaunayConstructionFailure { /// Final topology validation failed after construction. #[error("final topology validation failed after construction: {context}: {source}")] FinalTopologyValidation { - /// Validation failure detail. + /// Finalization phase that produced the validation failure. context: FinalTopologyValidationContext, /// Underlying validation error. #[source] @@ -439,8 +442,10 @@ pub enum DelaunayConstructionFailure { }, /// Final Delaunay-property validation failed after construction. - #[error("final Delaunay validation failed after construction: {source}")] + #[error("final Delaunay validation failed after construction: {context}: {source}")] FinalDelaunayValidation { + /// Finalization phase that produced the validation failure. + context: FinalDelaunayValidationContext, /// Underlying Delaunay validation error. #[source] source: DelaunayTriangulationValidationError, @@ -515,6 +520,9 @@ impl From for DelaunayConstructionFailure { TriangulationConstructionError::FinalTopologyValidation { context, source } => { Self::FinalTopologyValidation { context, source } } + TriangulationConstructionError::FinalDelaunayValidation { context, source } => { + Self::FinalDelaunayValidation { context, source } + } } } } @@ -2894,7 +2902,10 @@ where ); delaunay_result.map_err(|source| { DelaunayTriangulationConstructionError::Triangulation( - DelaunayConstructionFailure::FinalDelaunayValidation { source }, + DelaunayConstructionFailure::FinalDelaunayValidation { + context: FinalDelaunayValidationContext::ConstructionFinalize, + source, + }, ) })?; @@ -2946,7 +2957,10 @@ where if let Err(err) = delaunay_result { return Err(DelaunayTriangulationConstructionErrorWithStatistics { error: DelaunayTriangulationConstructionError::Triangulation( - DelaunayConstructionFailure::FinalDelaunayValidation { source: err }, + DelaunayConstructionFailure::FinalDelaunayValidation { + context: FinalDelaunayValidationContext::ConstructionFinalize, + source: err, + }, ), statistics: stats, }); @@ -3150,7 +3164,7 @@ where // During batch construction, use suspicion-driven validation instead of // per-insertion validation (see _with_construction_statistics variant for - // rationale: O(n²) avoidance + post-construction catch-all). + // rationale: O(n²) avoidance + post-construction validation fallback). // // Exception: PLManifoldStrict requires per-insertion vertex-link validation, // so we must use ValidationPolicy::Always to satisfy that guarantee. @@ -5052,10 +5066,7 @@ mod tests { type TestDelaunay = DelaunayTriangulation, (), (), D>; - fn synthetic_delaunay_verification_error( - message: &str, - ) -> DelaunayTriangulationValidationError { - let _ = message; + fn synthetic_delaunay_verification_error() -> DelaunayTriangulationValidationError { DelaunayTriangulationValidationError::VerificationFailed { source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { @@ -7107,7 +7118,7 @@ mod tests { reason: HullExtensionReason::NoVisibleFacets, }, InsertionError::DelaunayValidationFailed { - source: synthetic_delaunay_verification_error("test"), + source: synthetic_delaunay_verification_error(), }, InsertionError::DelaunayRepairFailed { source: Box::new(DelaunayRepairError::PostconditionFailed { @@ -7280,7 +7291,7 @@ mod tests { ); let delaunay = InsertionError::DelaunayValidationFailed { - source: synthetic_delaunay_verification_error("test"), + source: synthetic_delaunay_verification_error(), }; let mapped = TestDelaunay::<3>::map_insertion_error(delaunay); assert_matches!( @@ -7334,9 +7345,13 @@ mod tests { } ); + let source = std::error::Error::source(&failure) + .and_then(|source| source.downcast_ref::()); + assert_eq!(source, Some(&HullExtensionReason::NoVisibleFacets)); + let failure = DelaunayConstructionFailure::from( TriangulationConstructionError::InsertionDelaunayValidation { - source: synthetic_delaunay_verification_error("delaunay check"), + source: synthetic_delaunay_verification_error(), }, ); assert_matches!( @@ -7568,7 +7583,8 @@ mod tests { .into(); let final_delaunay_err = DelaunayTriangulationConstructionError::Triangulation( DelaunayConstructionFailure::FinalDelaunayValidation { - source: synthetic_delaunay_verification_error("final Level 4 check failed"), + context: FinalDelaunayValidationContext::ConstructionFinalize, + source: synthetic_delaunay_verification_error(), }, ); diff --git a/src/delaunay/query.rs b/src/delaunay/query.rs index e7dfdfcd..105c6309 100644 --- a/src/delaunay/query.rs +++ b/src/delaunay/query.rs @@ -16,7 +16,6 @@ use crate::core::tds::{SimplexKey, Tds, VertexKey}; use crate::core::triangulation::Triangulation; use crate::core::validation::{TopologyGuarantee, ValidationConfigurationError, ValidationPolicy}; use crate::core::vertex::Vertex; -use crate::geometry::kernel::Kernel; use crate::repair::{DelaunayCheckPolicy, DelaunayRepairPolicy}; use crate::topology::traits::topological_space::{GlobalTopology, TopologyKind}; use crate::triangulation::DelaunayTriangulation; @@ -25,16 +24,7 @@ use crate::triangulation::DelaunayTriangulation; // QUERY, ACCESSORS, AND CONFIGURATION (Minimal Bounds) // ============================================================================= // -// Methods that only need f64-backed kernels. Downstream generic code -// (e.g. `delaunayize_by_flips`) does not need extra coordinate-conversion -// bounds when calling these methods. -// -// Follows the precedent of the existing PURE STRUCT ASSEMBLY impl block. - -impl DelaunayTriangulation -where - K: Kernel, -{ +impl DelaunayTriangulation { // ------------------------------------------------------------------------- // QUERY / ACCESSORS // ------------------------------------------------------------------------- @@ -768,10 +758,7 @@ where // CONFIGURATION & TRAVERSAL (Minimal Bounds, continued) // ============================================================================= -impl DelaunayTriangulation -where - K: Kernel, -{ +impl DelaunayTriangulation { // ------------------------------------------------------------------------- // CONFIGURATION // ------------------------------------------------------------------------- diff --git a/src/delaunay/validation.rs b/src/delaunay/validation.rs index dc953b27..b46da5e4 100644 --- a/src/delaunay/validation.rs +++ b/src/delaunay/validation.rs @@ -27,7 +27,10 @@ use thiserror::Error; #[derive(Clone, Copy, Debug)] pub(crate) struct TdsStructureValidationProof(()); -/// Proof that a candidate passed the Delaunay-layer validation boundary. +/// Proof that a candidate passed the full validation boundary for a Delaunay wrapper. +/// +/// The proof is minted only after Levels 1–3 structural/topological validation and +/// the Level 4 Delaunay-property check succeed for the candidate's topology model. #[derive(Clone, Copy, Debug)] pub(crate) struct DelaunayTriangulationValidationProof(()); @@ -41,12 +44,7 @@ pub(crate) struct DelaunayTriangulationCandidate { candidate: DelaunayTriangulation, } -impl DelaunayTriangulationCandidate -where - K: Kernel, - U: DataType, - V: DataType, -{ +impl DelaunayTriangulationCandidate { /// Assembles a validation candidate from a TDS and topology guarantee. pub(crate) const fn assemble( tds: Tds, @@ -74,6 +72,38 @@ where self.candidate.tri.set_global_topology(global_topology); } + /// Validates Level 1–2 TDS structure and returns proof for structural-only assembly paths. + pub(crate) fn validate_tds_structure(&self) -> Result { + self.candidate.tri.tds.validate()?; + Ok(TdsStructureValidationProof(())) + } + + /// Converts a candidate using proof from [`Self::validate_delaunay_property`]. + pub(crate) fn into_validated_delaunay( + self, + _proof: DelaunayTriangulationValidationProof, + ) -> DelaunayTriangulation { + self.candidate + } + + /// Converts a candidate after the caller has proved structural validity. + pub(crate) fn into_structurally_valid_delaunay( + self, + _proof: TdsStructureValidationProof, + ) -> DelaunayTriangulation { + self.candidate + } + + #[cfg(test)] + pub(crate) fn into_repairable_delaunay_for_test(self) -> DelaunayTriangulation { + self.candidate + } +} + +impl DelaunayTriangulationCandidate +where + K: Kernel, +{ /// Normalizes coherent orientation on the assembled candidate. pub(crate) fn normalize_and_promote_positive_orientation( &mut self, @@ -83,12 +113,6 @@ where .normalize_and_promote_positive_orientation() } - /// Validates Level 1–2 TDS structure and returns proof for structural-only assembly paths. - pub(crate) fn validate_tds_structure(&self) -> Result { - self.candidate.tri.tds.validate()?; - Ok(TdsStructureValidationProof(())) - } - /// Validates Level 3 topology without geometric orientation checks. pub(crate) fn validate_topology_only(&self) -> Result<(), InvariantError> { self.candidate.tri.is_valid_topology_only() @@ -103,19 +127,24 @@ where pub(crate) fn validate_geometric_nondegeneracy(&self) -> Result<(), TdsError> { self.candidate.tri.validate_geometric_nondegeneracy() } +} - /// Validates the Delaunay property and returns proof for final conversion. +impl DelaunayTriangulationCandidate +where + K: Kernel, + U: DataType, + V: DataType, +{ + /// Validates all invariants required before exposing a Delaunay wrapper. + /// + /// This preserves the public reconstruction contract for + /// [`DelaunayTriangulation`]: a candidate cannot cross the boundary until + /// its underlying [`Triangulation`] passes Levels 1–3 validation and the + /// Level 4 Delaunay property is checked with the topology-appropriate + /// validator. pub(crate) fn validate_delaunay_property( &self, ) -> Result { - self.candidate.is_valid()?; - Ok(DelaunayTriangulationValidationProof(())) - } - - /// Validates all public reconstruction invariants and returns the final wrapper. - pub(crate) fn try_into_validated_delaunay( - self, - ) -> Result, DelaunayTriangulationValidationError> { self.candidate.tri.validate().map_err(|e| match e { InvariantError::Tds(tds_err) => tds_err.into(), InvariantError::Triangulation(tri_err) => tri_err.into(), @@ -132,28 +161,7 @@ where self.candidate.is_valid()?; } - Ok(self.candidate) - } - - /// Converts a candidate after the caller has proved the Delaunay boundary. - pub(crate) fn into_validated_delaunay( - self, - _proof: DelaunayTriangulationValidationProof, - ) -> DelaunayTriangulation { - self.candidate - } - - /// Converts a candidate after the caller has proved structural validity. - pub(crate) fn into_structurally_valid_delaunay( - self, - _proof: TdsStructureValidationProof, - ) -> DelaunayTriangulation { - self.candidate - } - - #[cfg(test)] - pub(crate) fn into_repairable_delaunay_for_test(self) -> DelaunayTriangulation { - self.candidate + Ok(DelaunayTriangulationValidationProof(())) } } @@ -915,7 +923,8 @@ where let mut candidate = DelaunayTriangulationCandidate::assemble(tds, kernel, topology_guarantee); candidate.set_global_topology(global_topology); - candidate.try_into_validated_delaunay() + let proof = candidate.validate_delaunay_property()?; + Ok(candidate.into_validated_delaunay(proof)) } } @@ -927,6 +936,7 @@ mod tests { }; use crate::core::simplex::Simplex; use crate::core::tds::{SimplexKey, TriangulationConstructionState, VertexKey}; + use crate::core::vertex::Vertex; use crate::geometry::kernel::AdaptiveKernel; use std::assert_matches; use std::{error::Error, sync::Once}; @@ -947,24 +957,16 @@ mod tests { fn non_delaunay_quad_tds() -> Tds<(), (), 2> { let mut tds: Tds<(), (), 2> = Tds::empty(); let v0 = tds - .insert_vertex_with_mapping( - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0]).unwrap(), - ) + .insert_vertex_with_mapping(Vertex::<(), _>::try_new([0.0, 0.0]).unwrap()) .unwrap(); let v1 = tds - .insert_vertex_with_mapping( - crate::core::vertex::Vertex::<(), _>::try_new([4.0, 0.0]).unwrap(), - ) + .insert_vertex_with_mapping(Vertex::<(), _>::try_new([4.0, 0.0]).unwrap()) .unwrap(); let v2 = tds - .insert_vertex_with_mapping( - crate::core::vertex::Vertex::<(), _>::try_new([4.0, 2.0]).unwrap(), - ) + .insert_vertex_with_mapping(Vertex::<(), _>::try_new([4.0, 2.0]).unwrap()) .unwrap(); let v3 = tds - .insert_vertex_with_mapping( - crate::core::vertex::Vertex::<(), _>::try_new([1.0, 2.0]).unwrap(), - ) + .insert_vertex_with_mapping(Vertex::<(), _>::try_new([1.0, 2.0]).unwrap()) .unwrap(); tds.insert_simplex_with_mapping( @@ -1149,14 +1151,69 @@ mod tests { ); } + #[test] + fn try_from_tds_rejects_structural_validation_failure() { + init_tracing(); + let vertices = [ + Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), + ]; + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::try_new(&vertices).unwrap(); + let mut tds = dt.tds().clone(); + + let vk = tds.vertex_keys().next().unwrap(); + let uuid = tds.vertex(vk).unwrap().uuid(); + tds.uuid_to_vertex_key.remove(&uuid); + + let err = DelaunayTriangulation::try_from_tds(tds, AdaptiveKernel::new()) + .expect_err("checked TDS reconstruction must reject broken UUID mappings"); + assert_matches!( + err, + DelaunayTriangulationValidationError::Tds(source) + if matches!(source.as_ref(), TdsError::MappingInconsistency { .. }) + ); + } + + #[test] + fn try_from_tds_rejects_topology_validation_failure() { + init_tracing(); + let vertices = [ + Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), + ]; + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::try_new(&vertices).unwrap(); + let mut tds = dt.tds().clone(); + + let _ = tds + .insert_vertex_with_mapping(Vertex::<(), _>::try_new([0.5, 0.5, 0.5]).unwrap()) + .unwrap(); + + let err = DelaunayTriangulation::try_from_tds(tds, AdaptiveKernel::new()) + .expect_err("checked TDS reconstruction must reject isolated vertices"); + assert_matches!( + err, + DelaunayTriangulationValidationError::Triangulation(source) + if matches!( + source.as_ref(), + TriangulationValidationError::IsolatedVertex { .. } + ) + ); + } + #[test] fn test_validation_report_ok_for_valid_triangulation() { init_tracing(); let vertices = [ - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), ]; let dt: DelaunayTriangulation<_, (), (), 3> = @@ -1168,10 +1225,10 @@ mod tests { fn test_validation_report_returns_mapping_failures_only() { init_tracing(); let vertices = [ - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), ]; let mut dt: DelaunayTriangulation<_, (), (), 3> = @@ -1203,10 +1260,10 @@ mod tests { fn test_validation_report_includes_vertex_incidence_violation() { init_tracing(); let vertices = [ - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), ]; let mut dt: DelaunayTriangulation<_, (), (), 3> = @@ -1233,10 +1290,10 @@ mod tests { fn test_dt_validate_maps_tds_error_to_tds_variant() { init_tracing(); let vertices = [ - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), ]; let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::try_new(&vertices).unwrap(); @@ -1259,10 +1316,10 @@ mod tests { fn test_dt_validate_maps_topology_error_to_triangulation_variant() { init_tracing(); let vertices = [ - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), - crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([1.0, 0.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 1.0, 0.0]).unwrap(), + Vertex::<(), _>::try_new([0.0, 0.0, 1.0]).unwrap(), ]; let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::try_new(&vertices).unwrap(); @@ -1270,9 +1327,7 @@ mod tests { // Add an isolated vertex so Level 3 (topology) fails. let _ = dt .tds_mut() - .insert_vertex_with_mapping( - crate::core::vertex::Vertex::<(), _>::try_new([0.5, 0.5, 0.5]).unwrap(), - ) + .insert_vertex_with_mapping(Vertex::<(), _>::try_new([0.5, 0.5, 0.5]).unwrap()) .unwrap(); match dt.validate() { diff --git a/src/lib.rs b/src/lib.rs index 6a609c0c..6519e647 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,8 +100,9 @@ //! //! ```rust //! use delaunay::prelude::construction::{ -//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, +//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, //! }; +//! use delaunay::prelude::geometry::CoordinateConversionError; //! use delaunay::prelude::insertion::InsertionError; //! //! # #[derive(Debug, thiserror::Error)] @@ -109,14 +110,14 @@ //! # #[error(transparent)] //! # Source(#[from] DelaunayTriangulationConstructionError), //! # #[error(transparent)] -//! # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), +//! # Coordinate(#[from] CoordinateConversionError), //! # } //! # fn main() -> Result<(), ExampleError> { //! let vertices = vec![ -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, +//! Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, //! ]; //! let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! @@ -140,7 +141,9 @@ //! ```rust //! use delaunay::prelude::construction::{ //! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyGuarantee, +//! Vertex, //! }; +//! use delaunay::prelude::geometry::CoordinateConversionError; //! use delaunay::prelude::validation::ValidationPolicy; //! //! # #[derive(Debug, thiserror::Error)] @@ -148,14 +151,14 @@ //! # #[error(transparent)] //! # Source(#[from] DelaunayTriangulationConstructionError), //! # #[error(transparent)] -//! # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), +//! # Coordinate(#[from] CoordinateConversionError), //! # } //! # fn main() -> Result<(), ExampleError> { //! let vertices = vec![ -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, +//! Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, //! ]; //! let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! @@ -175,8 +178,9 @@ //! //! ```rust //! use delaunay::prelude::construction::{ -//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, +//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, //! }; +//! use delaunay::prelude::geometry::CoordinateConversionError; //! use delaunay::prelude::insertion::InsertionError; //! //! # #[derive(Debug, thiserror::Error)] @@ -184,13 +188,13 @@ //! # #[error(transparent)] //! # Source(#[from] DelaunayTriangulationConstructionError), //! # #[error(transparent)] -//! # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), +//! # Coordinate(#[from] CoordinateConversionError), //! # } //! # fn main() -> Result<(), ExampleError> { //! let vertices = vec![ -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0])?, +//! Vertex::<(), _>::try_new([1.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 1.0])?, //! ]; //! let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! @@ -198,7 +202,7 @@ //! let before_simplices = dt.number_of_simplices(); //! //! // Duplicate coordinates are rejected. -//! let result = dt.insert(delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0])?); +//! let result = dt.insert(Vertex::<(), _>::try_new([0.0, 0.0])?); //! std::assert_matches!(result, Err(InsertionError::DuplicateCoordinates { .. })); //! //! // On error, the triangulation is unchanged. @@ -292,8 +296,9 @@ //! //! ```rust //! use delaunay::prelude::construction::{ -//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, +//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, //! }; +//! use delaunay::prelude::geometry::CoordinateConversionError; //! use delaunay::prelude::insertion::InsertionError; //! use delaunay::prelude::validation::{ValidationConfigurationError, ValidationPolicy}; //! @@ -306,14 +311,14 @@ //! # #[error(transparent)] //! # ValidationConfiguration(#[from] ValidationConfigurationError), //! # #[error(transparent)] -//! # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), +//! # Coordinate(#[from] CoordinateConversionError), //! # } //! # fn main() -> Result<(), ExampleError> { //! let vertices = vec![ -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, +//! Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, //! ]; //! let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! @@ -322,7 +327,7 @@ //! dt.try_set_validation_policy(ValidationPolicy::ExplicitOnly)?; //! //! // Do incremental work... -//! dt.insert(delaunay::prelude::Vertex::<(), _>::try_new([0.2, 0.2, 0.2])?)?; +//! dt.insert(Vertex::<(), _>::try_new([0.2, 0.2, 0.2])?)?; //! //! // ...then explicitly validate the topology layer when you need a certificate. //! assert!(dt.as_triangulation().validate().is_ok()); @@ -355,22 +360,23 @@ //! //! ```rust //! use delaunay::prelude::construction::{ -//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, +//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, //! }; +//! use delaunay::prelude::geometry::CoordinateConversionError; //! //! # #[derive(Debug, thiserror::Error)] //! # enum ExampleError { //! # #[error(transparent)] //! # Source(#[from] DelaunayTriangulationConstructionError), //! # #[error(transparent)] -//! # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), +//! # Coordinate(#[from] CoordinateConversionError), //! # } //! # fn main() -> Result<(), ExampleError> { //! let vertices = vec![ -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, +//! Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, //! ]; //! let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! @@ -383,22 +389,23 @@ //! //! ```rust //! use delaunay::prelude::construction::{ -//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, +//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, //! }; +//! use delaunay::prelude::geometry::CoordinateConversionError; //! //! # #[derive(Debug, thiserror::Error)] //! # enum ExampleError { //! # #[error(transparent)] //! # Source(#[from] DelaunayTriangulationConstructionError), //! # #[error(transparent)] -//! # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), +//! # Coordinate(#[from] CoordinateConversionError), //! # } //! # fn main() -> Result<(), ExampleError> { //! let vertices = vec![ -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, -//! delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, +//! Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, +//! Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, //! ]; //! let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! @@ -788,7 +795,7 @@ pub use crate::core::algorithms::pl_manifold_repair::{ PlManifoldRepairError, PlManifoldRepairStats, }; pub use crate::core::construction::{ - FinalTopologyValidationContext, TriangulationConstructionError, + FinalDelaunayValidationContext, FinalTopologyValidationContext, TriangulationConstructionError, }; pub use crate::core::insertion::DuplicateDetectionMetrics; pub use crate::core::operations::{ @@ -831,15 +838,17 @@ pub use crate::validation::{ /// # Examples /// /// ```rust -/// use delaunay::prelude::geometry::Point; +/// use delaunay::prelude::geometry::{ +/// CoordinateConversionError, CoordinateValidationError, Point, +/// }; /// use delaunay::try_vertices_from_points; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] -/// # Conversion(#[from] delaunay::prelude::geometry::CoordinateConversionError), +/// # Conversion(#[from] CoordinateConversionError), /// # #[error(transparent)] -/// # Validation(#[from] delaunay::prelude::geometry::CoordinateValidationError), +/// # Validation(#[from] CoordinateValidationError), /// # } /// # fn main() -> Result<(), ExampleError> { /// let points = [Point::try_new([0.0, 0.0])?, Point::try_new([1.0, 0.0])?]; @@ -879,8 +888,9 @@ pub fn try_vertices_from_points( /// /// ```rust /// use delaunay::prelude::construction::{ -/// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, +/// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, /// }; +/// use delaunay::prelude::geometry::CoordinateConversionError; /// use delaunay::prelude::topology::validation; /// /// # #[derive(Debug, thiserror::Error)] @@ -890,14 +900,14 @@ pub fn try_vertices_from_points( /// # #[error(transparent)] /// # Topology(#[from] delaunay::topology::TopologyError), /// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), +/// # Coordinate(#[from] CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { /// let vertices = vec![ -/// delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, -/// delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, -/// delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, -/// delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, +/// Vertex::<(), _>::try_new([0.0, 0.0, 0.0])?, +/// Vertex::<(), _>::try_new([1.0, 0.0, 0.0])?, +/// Vertex::<(), _>::try_new([0.0, 1.0, 0.0])?, +/// Vertex::<(), _>::try_new([0.0, 0.0, 1.0])?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; /// @@ -1141,11 +1151,11 @@ pub mod prelude { DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, DelaunayTriangulationConstructionErrorWithStatistics, DelaunayTriangulationValidationError, DelaunayVerificationError, DelaunayVerificationErrorKind, DuplicateDetectionMetrics, - FinalTopologyValidationContext, InitialSimplexStrategy, InsertionOrderStrategy, - InsertionResult, PlManifoldRepairError, PlManifoldRepairStats, RepairDecision, - RepairSkipReason, RetryPolicy, TopologicalOperation, TopologyGuarantee, Triangulation, - TriangulationConstructionError, TriangulationValidationError, ValidationConfigurationError, - ValidationPolicy, try_vertices_from_points, + FinalDelaunayValidationContext, FinalTopologyValidationContext, InitialSimplexStrategy, + InsertionOrderStrategy, InsertionResult, PlManifoldRepairError, PlManifoldRepairStats, + RepairDecision, RepairSkipReason, RetryPolicy, TopologicalOperation, TopologyGuarantee, + Triangulation, TriangulationConstructionError, TriangulationValidationError, + ValidationConfigurationError, ValidationPolicy, try_vertices_from_points, }; // Re-export utility items, but avoid exporting the util module names themselves. @@ -1229,21 +1239,22 @@ pub mod prelude { /// /// ```rust /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, /// }; + /// use delaunay::prelude::geometry::CoordinateConversionError; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] /// # Source(#[from] DelaunayTriangulationConstructionError), /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # Coordinate(#[from] CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { /// let vertices = vec![ - /// delaunay::prelude::Vertex::<(), _>::try_new([0.0, 0.0])?, - /// delaunay::prelude::Vertex::<(), _>::try_new([1.0, 0.0])?, - /// delaunay::prelude::Vertex::<(), _>::try_new([0.0, 1.0])?, + /// Vertex::<(), _>::try_new([0.0, 0.0])?, + /// Vertex::<(), _>::try_new([1.0, 0.0])?, + /// Vertex::<(), _>::try_new([0.0, 1.0])?, /// ]; /// let triangulation = DelaunayTriangulationBuilder::new(&vertices) /// .build::<()>()?; @@ -1286,8 +1297,9 @@ pub mod prelude { }; pub use crate::{ CavityFillingError, CavityRepairStage, DelaunayTriangulation, - FinalTopologyValidationContext, SpatialIndexConstructionFailure, TopologyGuarantee, - Triangulation, TriangulationConstructionError, try_vertices_from_points, + FinalDelaunayValidationContext, FinalTopologyValidationContext, + SpatialIndexConstructionFailure, TopologyGuarantee, Triangulation, + TriangulationConstructionError, try_vertices_from_points, }; } @@ -1305,13 +1317,14 @@ pub mod prelude { /// use delaunay::prelude::triangulation::{ /// FastKernel, Triangulation, TriangulationConstructionError, Vertex, /// }; + /// use delaunay::prelude::geometry::CoordinateConversionError; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] /// # Source(#[from] TriangulationConstructionError), /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # Coordinate(#[from] CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { /// let vertices = vec![ diff --git a/tests/prelude_exports.rs b/tests/prelude_exports.rs index c4b6a99a..ec7f2605 100644 --- a/tests/prelude_exports.rs +++ b/tests/prelude_exports.rs @@ -35,7 +35,7 @@ use delaunay::prelude::construction::{ ExplicitDelaunayValidationErrorKind, ExplicitDelaunayValidationSourceKind, ExplicitInsertionError, ExplicitInsertionErrorKind, ExplicitInvariantError, ExplicitInvariantErrorKind, ExplicitTdsError, ExplicitTdsErrorKind, - FinalTopologyValidationContext, + FinalDelaunayValidationContext, FinalTopologyValidationContext, GlobalTopologyModelError as ConstructionGlobalTopologyModelError, InsertionOrderStrategy, InvalidCoordinateValue as ConstructionInvalidCoordinateValue, InvalidPositiveScalar as ConstructionInvalidPositiveScalar, RandomPointGenerationError, @@ -217,6 +217,10 @@ fn construction_prelude_covers_typed_construction_failure_variants() { FinalTopologyValidationContext::ConstructionFinalize.to_string(), "topology validation failed after construction" ); + assert_eq!( + FinalDelaunayValidationContext::PeriodicQuotientDelaunay.to_string(), + "periodic quotient failed final Level 4 Delaunay validation" + ); assert_eq!( InsertionTopologyValidationContext::PostInsertion.to_string(), "post-insertion topology validation failed" @@ -963,6 +967,7 @@ fn construction_prelude_covers_random_point_generation_failure_variant() assert_matches!( DelaunayConstructionFailure::FinalDelaunayValidation { + context: FinalDelaunayValidationContext::ConstructionFinalize, source: ConstructionDelaunayTriangulationValidationError::VerificationFailed { source: ConstructionDelaunayVerificationError::from( DelaunayRepairError::PostconditionFailed { @@ -975,6 +980,7 @@ fn construction_prelude_covers_random_point_generation_failure_variant() }, }, DelaunayConstructionFailure::FinalDelaunayValidation { + context: FinalDelaunayValidationContext::ConstructionFinalize, source: ConstructionDelaunayTriangulationValidationError::VerificationFailed { source, }, diff --git a/tests/semgrep/src/project_rules/rust_style.rs b/tests/semgrep/src/project_rules/rust_style.rs index 9a0e77a3..da0eeed8 100644 --- a/tests/semgrep/src/project_rules/rust_style.rs +++ b/tests/semgrep/src/project_rules/rust_style.rs @@ -245,6 +245,11 @@ impl FallibleConstructorDefinitionFixture { Ok(Self { value }) } + // ruleid: delaunay.rust.no-fallible-new-or-from-constructor-definitions + fn from_str(value: &str) -> Result { + Ok(Self { value: value.len() }) + } + // ruleid: delaunay.rust.no-fallible-new-or-from-constructor-definitions fn from_simplex_with_data( value: usize, From 8f97a085e9ddba77b3eec0919896c032454b3ffb Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Wed, 17 Jun 2026 09:42:13 -0700 Subject: [PATCH 4/5] refactor(api)!: preserve typed hull-extension diagnostics (#443) - Replace string-backed hull-extension patch failures with orthogonal typed variants for boundary-edge split counts, duplicate split facets, and disconnected visible patches. - Preserve those categories through flip-neighbor repair diagnostics and export the summary types through the public flips facade and repair prelude. - Clarify that structural Delaunay conversion is internal repair machinery until full validation proof types can model topology-specific wrappers. BREAKING CHANGE: `HullExtensionReason::InvalidPatch { details }` is replaced by `BoundaryEdgeSplitFacetCount`, `MultipleBoundaryEdgeSplitFacets`, and `DisconnectedVisiblePatch`. BREAKING CHANGE: `FlipNeighborHullExtensionFailureKind::InvalidPatch` is replaced by typed summary variants for boundary-edge split counts, multiple split facets, and disconnected visible patches. --- src/core/algorithms/flips.rs | 37 +++- src/core/algorithms/incremental_insertion.rs | 181 ++++++++++++++----- src/core/validation.rs | 2 +- src/delaunay/flips.rs | 4 +- src/delaunay/validation.rs | 7 + src/lib.rs | 29 ++- 6 files changed, 197 insertions(+), 63 deletions(-) diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index 4dc88937..e3500896 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -3033,9 +3033,15 @@ pub enum FlipNeighborHullExtensionFailureKind { /// No visible facets were found. #[error("no visible facets")] NoVisibleFacets, - /// Visible facets formed an invalid patch. - #[error("invalid patch")] - InvalidPatch, + /// Boundary-edge split matched the wrong number of facets. + #[error("boundary edge split facet count")] + BoundaryEdgeSplitFacetCount, + /// Boundary-edge split matched more than one candidate facet. + #[error("multiple boundary edge split facets")] + MultipleBoundaryEdgeSplitFacets, + /// Visible facets formed a disconnected or non-manifold patch. + #[error("disconnected visible patch")] + DisconnectedVisiblePatch, /// Geometric predicate failed. #[error("predicate failed")] PredicateFailed, @@ -3048,7 +3054,13 @@ impl From<&HullExtensionReason> for FlipNeighborHullExtensionFailureKind { fn from(source: &HullExtensionReason) -> Self { match source { HullExtensionReason::NoVisibleFacets => Self::NoVisibleFacets, - HullExtensionReason::InvalidPatch { .. } => Self::InvalidPatch, + HullExtensionReason::BoundaryEdgeSplitFacetCount { .. } => { + Self::BoundaryEdgeSplitFacetCount + } + HullExtensionReason::MultipleBoundaryEdgeSplitFacets => { + Self::MultipleBoundaryEdgeSplitFacets + } + HullExtensionReason::DisconnectedVisiblePatch { .. } => Self::DisconnectedVisiblePatch, HullExtensionReason::PredicateFailed(_) => Self::PredicateFailed, HullExtensionReason::Tds(_) => Self::Tds, } @@ -14806,15 +14818,20 @@ mod tests { ); assert_eq!(cavity_kind.to_string(), "unsupported degenerate location"); - let hull_kind = - FlipNeighborHullExtensionFailureKind::from(&HullExtensionReason::InvalidPatch { - details: "non-manifold visible patch".to_string(), - }); + let hull_kind = FlipNeighborHullExtensionFailureKind::from( + &HullExtensionReason::DisconnectedVisiblePatch { + boundary_ridges: 1, + ridge_fans: 0, + components: 2, + boundary_components: 2, + boundary_subface_nonmanifold: 0, + }, + ); assert_eq!( hull_kind, - FlipNeighborHullExtensionFailureKind::InvalidPatch + FlipNeighborHullExtensionFailureKind::DisconnectedVisiblePatch ); - assert_eq!(hull_kind.to_string(), "invalid patch"); + assert_eq!(hull_kind.to_string(), "disconnected visible patch"); let validation_kind = FlipNeighborDelaunayValidationFailureKind::from( &DelaunayTriangulationValidationError::RepairOperationFailed { diff --git a/src/core/algorithms/incremental_insertion.rs b/src/core/algorithms/incremental_insertion.rs index b93abfc7..3ee98179 100644 --- a/src/core/algorithms/incremental_insertion.rs +++ b/src/core/algorithms/incremental_insertion.rs @@ -79,11 +79,32 @@ pub enum HullExtensionReason { "No visible boundary facets found for exterior vertex (may be coplanar with hull surface)" )] NoVisibleFacets, - /// Visible facets form an invalid patch. - #[error("Visible boundary facets are not a valid patch: {details}")] - InvalidPatch { - /// Details about why the patch was invalid. - details: String, + /// Boundary-edge split matched the wrong number of facets. + #[error("2D boundary edge split expected {expected} facets, got {actual}")] + BoundaryEdgeSplitFacetCount { + /// Expected split-facet count. + expected: usize, + /// Actual split-facet count. + actual: usize, + }, + /// Boundary-edge split matched more than one candidate facet. + #[error("2D boundary edge split matched multiple facets")] + MultipleBoundaryEdgeSplitFacets, + /// Visible facets form a disconnected or non-manifold patch. + #[error( + "visible patch is disconnected or non-manifold: boundary_ridges={boundary_ridges}, ridge_fans={ridge_fans}, components={components}, boundary_components={boundary_components}, boundary_subface_nonmanifold={boundary_subface_nonmanifold}" + )] + DisconnectedVisiblePatch { + /// Count of boundary ridges in the visible patch. + boundary_ridges: usize, + /// Count of ridges shared by more than two visible facets. + ridge_fans: usize, + /// Connected components in the visible-facet graph. + components: usize, + /// Connected components in the boundary-subface graph. + boundary_components: usize, + /// Count of boundary subfaces with non-manifold incidence. + boundary_subface_nonmanifold: usize, }, /// Geometric predicate (orientation / in-sphere) failed. /// @@ -1890,7 +1911,10 @@ impl InsertionError { // be resolved by a perturbation retry. matches!( reason, - HullExtensionReason::NoVisibleFacets | HullExtensionReason::InvalidPatch { .. } + HullExtensionReason::NoVisibleFacets + | HullExtensionReason::BoundaryEdgeSplitFacetCount { .. } + | HullExtensionReason::MultipleBoundaryEdgeSplitFacets + | HullExtensionReason::DisconnectedVisiblePatch { .. } ) } Self::CavityFilling { reason } => Self::is_cavity_filling_error_retryable(reason), @@ -2001,12 +2025,13 @@ impl InsertionError { ) } InitialSimplexUnexpectedInsertionStage::NonManifoldTopology { .. } => true, - InitialSimplexUnexpectedInsertionStage::HullExtension { reason } => { - matches!( - reason, - HullExtensionReason::NoVisibleFacets | HullExtensionReason::InvalidPatch { .. } - ) - } + InitialSimplexUnexpectedInsertionStage::HullExtension { reason } => matches!( + reason, + HullExtensionReason::NoVisibleFacets + | HullExtensionReason::BoundaryEdgeSplitFacetCount { .. } + | HullExtensionReason::MultipleBoundaryEdgeSplitFacets + | HullExtensionReason::DisconnectedVisiblePatch { .. } + ), InitialSimplexUnexpectedInsertionStage::TopologyValidation { source, .. } => { Self::is_level3_error_retryable(source) } @@ -3895,18 +3920,53 @@ fn invalid_boundary_facet_index(facet_index: u8, facet_count: usize) -> Insertio }) } -fn validate_boundary_edge_split_facet_count(facet_count: usize) -> Result<(), InsertionError> { +/// Preserves the public hull-extension error contract for 2D boundary-edge splits. +/// +/// A valid split replaces one boundary edge with exactly two facets. Reporting +/// count mismatches through a typed [`HullExtensionReason`] lets callers decide +/// retry behavior without parsing diagnostic text. +const fn validate_boundary_edge_split_facet_count( + facet_count: usize, +) -> Result<(), InsertionError> { if facet_count == 2 { return Ok(()); } Err(InsertionError::HullExtension { - reason: HullExtensionReason::InvalidPatch { - details: format!("2D boundary edge split expected 2 facets, got {facet_count}"), + reason: HullExtensionReason::BoundaryEdgeSplitFacetCount { + expected: 2, + actual: facet_count, }, }) } +/// Converts visible-patch topology summary counts into a typed hull-extension failure. +const fn visible_patch_failure_reason( + boundary_ridges: usize, + ridge_fans: usize, + components: usize, + boundary_components: usize, + boundary_subface_nonmanifold: usize, + dimension: usize, +) -> Option { + if ridge_fans > 0 + || components > 1 + || boundary_ridges == 0 + || (dimension >= 3 && boundary_components > 1) + || (dimension >= 3 && boundary_subface_nonmanifold > 0) + { + Some(HullExtensionReason::DisconnectedVisiblePatch { + boundary_ridges, + ridge_fans, + components, + boundary_components, + boundary_subface_nonmanifold, + }) + } else { + None + } +} + fn find_boundary_edge_split_facet( tds: &Tds, point: &Point, @@ -4014,9 +4074,7 @@ where if on_segment { if match_facet.is_some() { return Err(InsertionError::HullExtension { - reason: HullExtensionReason::InvalidPatch { - details: "2D boundary edge split matched multiple facets".to_string(), - }, + reason: HullExtensionReason::MultipleBoundaryEdgeSplitFacets, }); } match_facet = Some(FacetHandle::from_validated(simplex_key, facet_index)); @@ -4504,12 +4562,14 @@ where component_sizes.push(component_size); } - if over_shared_ridges > 0 - || components > 1 - || boundary_ridges == 0 - || (D >= 3 && boundary_components > 1) - || (D >= 3 && boundary_subface_nonmanifold > 0) - { + if let Some(reason) = visible_patch_failure_reason( + boundary_ridges, + over_shared_ridges, + components, + boundary_components, + boundary_subface_nonmanifold, + D, + ) { #[cfg(debug_assertions)] if detail_enabled || log_enabled { let visible_sample = &visible_facets[..visible_facets.len().min(10)]; @@ -4538,13 +4598,7 @@ where "find_visible_boundary_facets: invalid patch" ); } - return Err(InsertionError::HullExtension { - reason: HullExtensionReason::InvalidPatch { - details: format!( - "boundary_ridges={boundary_ridges}, ridge_fans={over_shared_ridges}, components={components}, boundary_components={boundary_components}, boundary_subface_nonmanifold={boundary_subface_nonmanifold}", - ), - }, - }); + return Err(InsertionError::HullExtension { reason }); } #[cfg(debug_assertions)] @@ -4853,13 +4907,11 @@ mod tests { .collect(); let result = fill_cavity(tds, invalid_vkey, &boundary_facets); - assert!( - matches!( - result, - Err(InsertionError::CavityFilling { - reason: CavityFillingError::MissingInsertedVertex { .. }, - }) - ), + assert_matches!( + result, + Err(InsertionError::CavityFilling { + reason: CavityFillingError::MissingInsertedVertex { .. }, + }), "Expected CavityFilling error, got: {result:?}" ); } @@ -6211,8 +6263,12 @@ mod tests { assert!( InsertionError::HullExtension { - reason: HullExtensionReason::InvalidPatch { - details: "test".to_string(), + reason: HullExtensionReason::DisconnectedVisiblePatch { + boundary_ridges: 1, + ridge_fans: 0, + components: 2, + boundary_components: 2, + boundary_subface_nonmanifold: 0, } } .is_retryable() @@ -6745,15 +6801,48 @@ mod tests { } #[test] - fn test_boundary_edge_split_invalid_boundary_count_is_retryable_invalid_patch() { + fn test_boundary_edge_split_invalid_boundary_count_is_retryable_typed_reason() { let err = validate_boundary_edge_split_facet_count(1).unwrap_err(); assert!(err.is_retryable()); assert_matches!( err, InsertionError::HullExtension { - reason: HullExtensionReason::InvalidPatch { details }, - } if details == "2D boundary edge split expected 2 facets, got 1" + reason: HullExtensionReason::BoundaryEdgeSplitFacetCount { + expected: 2, + actual: 1, + }, + } + ); + } + + #[test] + fn test_visible_patch_failure_reason_preserves_typed_counts() { + assert_eq!( + visible_patch_failure_reason(0, 2, 3, 4, 5, 3), + Some(HullExtensionReason::DisconnectedVisiblePatch { + boundary_ridges: 0, + ridge_fans: 2, + components: 3, + boundary_components: 4, + boundary_subface_nonmanifold: 5, + }) + ); + } + + #[test] + fn test_visible_patch_failure_reason_respects_dimension_specific_boundary_checks() { + assert_eq!(visible_patch_failure_reason(1, 0, 1, 2, 3, 2), None); + + assert_matches!( + visible_patch_failure_reason(1, 0, 1, 2, 0, 3), + Some(HullExtensionReason::DisconnectedVisiblePatch { + boundary_ridges: 1, + ridge_fans: 0, + components: 1, + boundary_components: 2, + boundary_subface_nonmanifold: 0, + }) ); } @@ -6772,7 +6861,7 @@ mod tests { } #[test] - fn test_find_boundary_edge_split_facet_hull_vertex_is_retryable_invalid_patch() { + fn test_find_boundary_edge_split_facet_hull_vertex_is_retryable_typed_reason() { let vertices = vec![ crate::core::vertex::Vertex::<(), _>::try_new([0.0, 0.0]).unwrap(), crate::core::vertex::Vertex::<(), _>::try_new([1.0, 0.0]).unwrap(), @@ -6787,8 +6876,8 @@ mod tests { assert_matches!( err, InsertionError::HullExtension { - reason: HullExtensionReason::InvalidPatch { details }, - } if details == "2D boundary edge split matched multiple facets" + reason: HullExtensionReason::MultipleBoundaryEdgeSplitFacets, + } ); } diff --git a/src/core/validation.rs b/src/core/validation.rs index 34752578..239d1ebb 100644 --- a/src/core/validation.rs +++ b/src/core/validation.rs @@ -1303,7 +1303,7 @@ where /// /// - `InvariantError::Tds(e)` → `InsertionError::TopologyValidation(e)` /// - `InvariantError::Triangulation(e)` → `InsertionError::TopologyValidationFailed { source: e }` - /// - `InvariantError::Delaunay(e)` → `InsertionError::DelaunayValidationFailed { message }` + /// - `InvariantError::Delaunay(e)` → `InsertionError::DelaunayValidationFailed { source: e }` pub(crate) fn invariant_error_to_insertion_error(err: InvariantError) -> InsertionError { match err { InvariantError::Tds(tds_err) => InsertionError::TopologyValidation(tds_err), diff --git a/src/delaunay/flips.rs b/src/delaunay/flips.rs index 66ac4a79..3ea7ac27 100644 --- a/src/delaunay/flips.rs +++ b/src/delaunay/flips.rs @@ -14,7 +14,9 @@ pub use crate::core::algorithms::flips::{ DelaunayRepairHeuristicVertexContext, DelaunayRepairOrientationCanonicalizationFailure, DelaunayRepairOrientationCanonicalizationFailureKind, DelaunayRepairPostconditionFailure, DelaunayRepairStats, DelaunayRepairVerificationContext, FlipContextError, FlipDirection, - FlipEdgeAdjacencyError, FlipError, FlipInfo, FlipMutationError, FlipNeighborWiringError, + FlipEdgeAdjacencyError, FlipError, FlipInfo, FlipMutationError, FlipNeighborCavityFailureKind, + FlipNeighborDelaunayValidationFailureKind, FlipNeighborHullExtensionFailureKind, + FlipNeighborRepairDiagnostics, FlipNeighborRepairFailure, FlipNeighborWiringError, FlipOrientationCheckStage, FlipPredicateError, FlipPredicateOperation, FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RepairQueueOrder, RidgeHandle, TriangleHandle, TriangleHandleError, verify_delaunay_for_triangulation, diff --git a/src/delaunay/validation.rs b/src/delaunay/validation.rs index b46da5e4..b47ede45 100644 --- a/src/delaunay/validation.rs +++ b/src/delaunay/validation.rs @@ -87,6 +87,13 @@ impl DelaunayTriangulationCandidate { } /// Converts a candidate after the caller has proved structural validity. + /// + /// This is intentionally crate-private repair/construction machinery for + /// paths that already return a `DelaunayTriangulation` wrapper while only + /// promising Level 1-2 TDS structure at this boundary. General reconstruction + /// must use [`Self::validate_delaunay_property`] plus + /// [`Self::into_validated_delaunay`] so the returned wrapper carries the full + /// Levels 1-4 validation contract. pub(crate) fn into_structurally_valid_delaunay( self, _proof: TdsStructureValidationProof, diff --git a/src/lib.rs b/src/lib.rs index 6519e647..2990d68a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1209,7 +1209,9 @@ pub mod prelude { DelaunayRepairOrientationCanonicalizationFailure, DelaunayRepairOrientationCanonicalizationFailureKind, DelaunayRepairPostconditionFailure, DelaunayRepairStats, DelaunayRepairVerificationContext, FlipContextError, - FlipEdgeAdjacencyError, FlipError, FlipMutationError, FlipNeighborWiringError, + FlipEdgeAdjacencyError, FlipError, FlipMutationError, FlipNeighborCavityFailureKind, + FlipNeighborDelaunayValidationFailureKind, FlipNeighborHullExtensionFailureKind, + FlipNeighborRepairDiagnostics, FlipNeighborRepairFailure, FlipNeighborWiringError, FlipOrientationCheckStage, FlipPredicateError, FlipPredicateOperation, FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RepairQueueOrder, TriangleHandleError, @@ -1427,6 +1429,20 @@ pub mod prelude { /// /// [`DelaunayRepairErrorSummary`]: crate::prelude::repair::DelaunayRepairErrorSummary /// [`DelaunayRepairErrorKind`]: crate::prelude::repair::DelaunayRepairErrorKind + /// + /// ```rust + /// use delaunay::prelude::repair::{ + /// FlipNeighborHullExtensionFailureKind, FlipNeighborRepairFailure, + /// }; + /// + /// let reason = FlipNeighborHullExtensionFailureKind::DisconnectedVisiblePatch; + /// std::assert_matches!( + /// reason, + /// FlipNeighborHullExtensionFailureKind::DisconnectedVisiblePatch + /// ); + /// + /// let _ = std::mem::size_of::(); + /// ``` pub mod repair { pub use crate::flips::{ DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairHeuristicRebuildFailure, @@ -1435,10 +1451,13 @@ pub mod prelude { DelaunayRepairOrientationCanonicalizationFailureKind, DelaunayRepairPostconditionFailure, DelaunayRepairStats, DelaunayRepairVerificationContext, FlipContextError, FlipEdgeAdjacencyError, FlipError, - FlipMutationError, FlipNeighborWiringError, FlipOrientationCheckStage, - FlipPredicateError, FlipPredicateOperation, FlipTriangleAdjacencyError, - FlipVertexAdjacencyError, RepairQueueOrder, TriangleHandleError, - verify_delaunay_for_triangulation, verify_delaunay_via_flip_predicates, + FlipMutationError, FlipNeighborCavityFailureKind, + FlipNeighborDelaunayValidationFailureKind, FlipNeighborHullExtensionFailureKind, + FlipNeighborRepairDiagnostics, FlipNeighborRepairFailure, FlipNeighborWiringError, + FlipOrientationCheckStage, FlipPredicateError, FlipPredicateOperation, + FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RepairQueueOrder, + TriangleHandleError, verify_delaunay_for_triangulation, + verify_delaunay_via_flip_predicates, }; pub use crate::repair::{ DelaunayCheckPolicy, DelaunayRepairHeuristicConfig, DelaunayRepairHeuristicSeeds, From b772486c41d4207b26900f80b6b520ce39e480dc Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Wed, 17 Jun 2026 13:21:19 -0700 Subject: [PATCH 5/5] test(api): enforce typed repair error hygiene (#443) - Cover all hull-extension repair failure kind conversions with typed variant assertions. - Keep explicit-construction doctests panic-free while matching structured error variants. - Document concrete Delaunay repair error variants for flip repair APIs. - Allow idiomatic fallible `from_str` parser names in constructor-name guardrails. Closes #443 --- semgrep.yaml | 2 +- src/core/algorithms/flips.rs | 55 ++++++++++++++----- src/delaunay/builder.rs | 25 ++++----- src/delaunay/repair.rs | 18 ++++-- tests/semgrep/src/project_rules/rust_style.rs | 2 +- 5 files changed, 66 insertions(+), 36 deletions(-) diff --git a/semgrep.yaml b/semgrep.yaml index b2e310d5..e2ad3be4 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -627,7 +627,7 @@ rules: - "/src/**/*.rs" - "/tests/semgrep/src/project_rules/**/*.rs" pattern-regex: >- - (?s)^\s*(?:pub(?:\([^)]*\))?\s+)?(?:const\s+|async\s+)?fn\s+(?:new|from_[A-Za-z0-9_]+)(?:<[^>{}]*>)?\s*\([^;{}]*?\)\s*->\s*Result\s*< + (?s)^\s*(?:pub(?:\([^)]*\))?\s+)?(?:const\s+|async\s+)?fn\s+(?:new|from_(?!str\b)[A-Za-z0-9_]+)(?:<[^>{}]*>)?\s*\([^;{}]*?\)\s*->\s*Result\s*< - id: delaunay.rust.no-public-from-validated-constructors languages: diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index e3500896..4a351ef0 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -14793,6 +14793,46 @@ mod tests { ); } + fn assert_hull_extension_failure_kind( + source: &HullExtensionReason, + expected: FlipNeighborHullExtensionFailureKind, + expected_display: &str, + ) { + let hull_kind = FlipNeighborHullExtensionFailureKind::from(source); + assert_eq!(hull_kind, expected); + assert_eq!(hull_kind.to_string(), expected_display); + } + + #[test] + fn test_flip_neighbor_hull_extension_failure_kind_conversions() { + assert_hull_extension_failure_kind( + &HullExtensionReason::BoundaryEdgeSplitFacetCount { + expected: 2, + actual: 1, + }, + FlipNeighborHullExtensionFailureKind::BoundaryEdgeSplitFacetCount, + "boundary edge split facet count", + ); + + assert_hull_extension_failure_kind( + &HullExtensionReason::MultipleBoundaryEdgeSplitFacets, + FlipNeighborHullExtensionFailureKind::MultipleBoundaryEdgeSplitFacets, + "multiple boundary edge split facets", + ); + + assert_hull_extension_failure_kind( + &HullExtensionReason::DisconnectedVisiblePatch { + boundary_ridges: 1, + ridge_fans: 0, + components: 2, + boundary_components: 2, + boundary_subface_nonmanifold: 0, + }, + FlipNeighborHullExtensionFailureKind::DisconnectedVisiblePatch, + "disconnected visible patch", + ); + } + #[test] fn test_flip_neighbor_conversion_kinds_cover_insertion_suberrors() { let cavity_kind = FlipNeighborCavityFailureKind::from( @@ -14818,21 +14858,6 @@ mod tests { ); assert_eq!(cavity_kind.to_string(), "unsupported degenerate location"); - let hull_kind = FlipNeighborHullExtensionFailureKind::from( - &HullExtensionReason::DisconnectedVisiblePatch { - boundary_ridges: 1, - ridge_fans: 0, - components: 2, - boundary_components: 2, - boundary_subface_nonmanifold: 0, - }, - ); - assert_eq!( - hull_kind, - FlipNeighborHullExtensionFailureKind::DisconnectedVisiblePatch - ); - assert_eq!(hull_kind.to_string(), "disconnected visible patch"); - let validation_kind = FlipNeighborDelaunayValidationFailureKind::from( &DelaunayTriangulationValidationError::RepairOperationFailed { operation: DelaunayRepairOperation::VertexRemoval, diff --git a/src/delaunay/builder.rs b/src/delaunay/builder.rs index 3b163ee6..f5e58094 100644 --- a/src/delaunay/builder.rs +++ b/src/delaunay/builder.rs @@ -829,18 +829,15 @@ impl From for ExplicitDelaunayValidationEr /// ]; /// let simplices = vec![vec![0, 1]]; // Wrong arity for 2D (needs 3 vertices) /// -/// let Err(err) = -/// DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices) -/// else { -/// panic!("bad simplex specs should be rejected"); -/// }; +/// let result = +/// DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices); /// std::assert_matches!( -/// err, -/// ExplicitConstructionError::InvalidSimplexArity { +/// result.err(), +/// Some(ExplicitConstructionError::InvalidSimplexArity { /// simplex_index: 0, /// actual: 2, /// expected: 3, -/// } +/// }) /// ); /// # Ok(()) /// # } @@ -1221,12 +1218,12 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, U, D> { /// assert_eq!(dt.number_of_simplices(), 2); /// /// let bad_simplices = vec![vec![0, 1]]; // Wrong arity for a 2D simplex. - /// let Err(err) = - /// DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &bad_simplices) - /// else { - /// panic!("bad simplex specs should be rejected"); - /// }; - /// std::assert_matches!(err, ExplicitConstructionError::InvalidSimplexArity { .. }); + /// let result = + /// DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &bad_simplices); + /// std::assert_matches!( + /// result.err(), + /// Some(ExplicitConstructionError::InvalidSimplexArity { .. }) + /// ); /// # Ok(()) /// # } /// ``` diff --git a/src/delaunay/repair.rs b/src/delaunay/repair.rs index 73339146..e8e9f1bf 100644 --- a/src/delaunay/repair.rs +++ b/src/delaunay/repair.rs @@ -348,8 +348,12 @@ where /// /// # Errors /// - /// Returns a [`DelaunayRepairError`] if the repair fails to converge, an underlying - /// flip operation fails, or post-repair orientation canonicalization fails. + /// Returns [`DelaunayRepairError::NonConvergent`] if the flip budget is + /// exhausted, [`DelaunayRepairError::PostconditionFailed`] if repair + /// finishes with a remaining violation or disconnected triangulation, + /// [`DelaunayRepairError::OrientationCanonicalizationFailed`] if the final + /// positive-orientation pass fails, or another [`DelaunayRepairError`] + /// variant for lower-level flip, topology, or predicate failures. /// /// # Examples /// @@ -566,9 +570,13 @@ where /// /// # Errors /// - /// Returns [`DelaunayRepairError`] if the flip-based repair fails, the heuristic - /// rebuild fallback cannot construct a valid triangulation, or post-repair - /// orientation canonicalization fails. + /// Returns [`DelaunayRepairError::HeuristicRebuildFailed`] when the + /// deterministic rebuild fallback cannot replay all vertices into a valid + /// triangulation, [`DelaunayRepairError::PostconditionFailed`] when a repair + /// pass leaves a violation, [`DelaunayRepairError::OrientationCanonicalizationFailed`] + /// when final positive-orientation promotion fails, or another + /// [`DelaunayRepairError`] variant for lower-level flip, topology, or + /// predicate failures. /// /// # Examples /// diff --git a/tests/semgrep/src/project_rules/rust_style.rs b/tests/semgrep/src/project_rules/rust_style.rs index da0eeed8..749bb8c6 100644 --- a/tests/semgrep/src/project_rules/rust_style.rs +++ b/tests/semgrep/src/project_rules/rust_style.rs @@ -245,7 +245,7 @@ impl FallibleConstructorDefinitionFixture { Ok(Self { value }) } - // ruleid: delaunay.rust.no-fallible-new-or-from-constructor-definitions + // ok: delaunay.rust.no-fallible-new-or-from-constructor-definitions fn from_str(value: &str) -> Result { Ok(Self { value: value.len() }) }