#![forbid(unsafe_code)]
pub use crate::core::algorithms::flips::{DelaunayRepairError, DelaunayRepairStats};
pub use crate::core::algorithms::pl_manifold_repair::{
PlManifoldRepairError, PlManifoldRepairStats,
};
pub use crate::core::cell::CellValidationError;
pub use crate::triangulation::delaunay::DelaunayTriangulationConstructionError;
use crate::core::algorithms::pl_manifold_repair::{
PlManifoldRepairConfig, repair_facet_oversharing,
};
use crate::core::cell::Cell;
use crate::core::collections::{Entry, FastHashMap, Uuid};
use crate::core::tds::{CellKey, Tds};
use crate::core::traits::data_type::DataType;
use crate::core::vertex::Vertex;
use crate::geometry::kernel::{ExactPredicates, Kernel};
use crate::geometry::traits::coordinate::CoordinateScalar;
use crate::triangulation::delaunay::{DelaunayRepairHeuristicConfig, DelaunayTriangulation};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DelaunayizeConfig {
pub topology_max_iterations: usize,
pub topology_max_cells_removed: usize,
pub fallback_rebuild: bool,
pub delaunay_max_flips: Option<usize>,
}
impl Default for DelaunayizeConfig {
fn default() -> Self {
Self {
topology_max_iterations: 64,
topology_max_cells_removed: 10_000,
fallback_rebuild: false,
delaunay_max_flips: None,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DelaunayizeOutcome<T, U, V, const D: usize> {
pub topology_repair: PlManifoldRepairStats<T, U, V, D>,
pub delaunay_repair: DelaunayRepairStats,
pub used_fallback_rebuild: bool,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum DelaunayizeError {
#[error("Topology repair failed: {source}")]
TopologyRepairFailed {
#[from]
#[source]
source: PlManifoldRepairError,
},
#[error("Topology repair failed ({source}); fallback rebuild also failed: {rebuild_error}")]
TopologyRepairFailedWithRebuild {
#[source]
source: PlManifoldRepairError,
rebuild_error: DelaunayTriangulationConstructionError,
},
#[error(
"Topology repair failed ({source}); fallback rebuild succeeded but cell-data restore failed: {restore_error}"
)]
TopologyRepairFailedWithRebuildRestore {
#[source]
source: PlManifoldRepairError,
restore_error: CellValidationError,
},
#[error("Delaunay repair failed: {source}")]
DelaunayRepairFailed {
#[from]
#[source]
source: DelaunayRepairError,
},
#[error(
"Delaunay repair failed ({source}); fallback cell-data snapshot failed: {snapshot_error}"
)]
DelaunayRepairFailedWithCellDataSnapshot {
#[source]
source: DelaunayRepairError,
snapshot_error: CellValidationError,
},
#[error("Delaunay repair failed ({source}); fallback rebuild also failed: {rebuild_error}")]
DelaunayRepairFailedWithRebuild {
#[source]
source: DelaunayRepairError,
rebuild_error: DelaunayTriangulationConstructionError,
},
#[error(
"Delaunay repair failed ({source}); fallback rebuild succeeded but cell-data restore failed: {restore_error}"
)]
DelaunayRepairFailedWithRebuildRestore {
#[source]
source: DelaunayRepairError,
restore_error: CellValidationError,
},
#[error("Fallback cell-data snapshot failed before repair; no repair attempted: {source}")]
FallbackCellDataSnapshotFailed {
#[from]
#[source]
source: CellValidationError,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum CellDataMatch<V> {
Unique(Option<V>),
Ambiguous,
}
type CellDataByVertexUuids<V> = FastHashMap<Vec<Uuid>, CellDataMatch<V>>;
type FallbackRebuildState<T, U, V, const D: usize> =
(Vec<Vertex<T, U, D>>, CellDataByVertexUuids<V>);
fn snapshot_rebuild_state<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
) -> Result<FallbackRebuildState<T, U, V, D>, CellValidationError>
where
T: CoordinateScalar,
U: DataType,
V: DataType,
{
let vertices = tds
.vertices()
.map(|(_, v)| Vertex::new_with_uuid(*v.point(), v.uuid(), v.data))
.collect::<Vec<_>>();
let cell_data = collect_cell_data(tds)?;
Ok((vertices, cell_data))
}
fn collect_cell_data<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
) -> Result<CellDataByVertexUuids<V>, CellValidationError>
where
U: DataType,
V: DataType,
{
let mut cell_data = FastHashMap::default();
for (_, cell) in tds.cells() {
let vertex_uuids = cell_vertex_uuids(tds, cell)?;
match cell_data.entry(vertex_uuids) {
Entry::Vacant(entry) => {
entry.insert(CellDataMatch::Unique(cell.data().copied()));
}
Entry::Occupied(mut entry) => {
entry.insert(CellDataMatch::Ambiguous);
}
}
}
Ok(cell_data)
}
fn cell_vertex_uuids<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
cell: &Cell<T, U, V, D>,
) -> Result<Vec<Uuid>, CellValidationError>
where
U: DataType,
V: DataType,
{
let mut vertex_uuids = cell
.vertex_uuid_iter(tds)
.collect::<Result<Vec<_>, CellValidationError>>()?;
vertex_uuids.sort_unstable();
Ok(vertex_uuids)
}
fn restore_cell_data<K, U, V, const D: usize>(
rebuilt: &mut DelaunayTriangulation<K, U, V, D>,
original_cell_data: &CellDataByVertexUuids<V>,
) -> Result<(), CellValidationError>
where
K: Kernel<D>,
U: DataType,
V: DataType,
{
let mut assignments: Vec<(CellKey, V)> = Vec::new();
for (cell_key, cell) in rebuilt.cells() {
let vertex_uuids = cell_vertex_uuids(rebuilt.tds(), cell)?;
let Some(CellDataMatch::Unique(Some(data))) = original_cell_data.get(&vertex_uuids) else {
continue;
};
assignments.push((cell_key, *data));
}
for (cell_key, data) in assignments {
rebuilt.set_cell_data(cell_key, Some(data));
}
Ok(())
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
enum FallbackRebuildError {
#[error("fallback rebuild failed: {source}")]
Construction {
#[from]
#[source]
source: DelaunayTriangulationConstructionError,
},
#[error("fallback cell-data restore failed: {source}")]
Restore {
#[from]
#[source]
source: CellValidationError,
},
}
fn topology_rebuild_error(
source: PlManifoldRepairError,
fallback_error: FallbackRebuildError,
) -> DelaunayizeError {
match fallback_error {
FallbackRebuildError::Construction {
source: rebuild_error,
} => DelaunayizeError::TopologyRepairFailedWithRebuild {
source,
rebuild_error,
},
FallbackRebuildError::Restore {
source: restore_error,
} => DelaunayizeError::TopologyRepairFailedWithRebuildRestore {
source,
restore_error,
},
}
}
fn delaunay_rebuild_error(
source: DelaunayRepairError,
fallback_error: FallbackRebuildError,
) -> DelaunayizeError {
match fallback_error {
FallbackRebuildError::Construction {
source: rebuild_error,
} => DelaunayizeError::DelaunayRepairFailedWithRebuild {
source,
rebuild_error,
},
FallbackRebuildError::Restore {
source: restore_error,
} => DelaunayizeError::DelaunayRepairFailedWithRebuildRestore {
source,
restore_error,
},
}
}
fn rebuild_preserving_data<K, U, V, const D: usize>(
kernel: &K,
vertices: &[Vertex<K::Scalar, U, D>],
original_cell_data: &CellDataByVertexUuids<V>,
) -> Result<DelaunayTriangulation<K, U, V, D>, FallbackRebuildError>
where
K: Kernel<D> + ExactPredicates,
U: DataType,
V: DataType,
{
let mut rebuilt = DelaunayTriangulation::with_kernel(kernel, vertices)?;
restore_cell_data(&mut rebuilt, original_cell_data)?;
Ok(rebuilt)
}
#[expect(
clippy::result_large_err,
reason = "DelaunayizeError preserves typed source and rebuild_error values on the *WithRebuild variants (no boxing) so callers can pattern-match both errors while Error::source exposes the primary repair error; this is a cold error path."
)]
pub fn delaunayize_by_flips<K, U, V, const D: usize>(
dt: &mut DelaunayTriangulation<K, U, V, D>,
config: DelaunayizeConfig,
) -> Result<DelaunayizeOutcome<K::Scalar, U, V, D>, DelaunayizeError>
where
K: Kernel<D> + ExactPredicates,
U: DataType,
V: DataType,
{
let fallback_state: Option<FallbackRebuildState<K::Scalar, U, V, D>> =
if config.fallback_rebuild {
let tds = &dt.as_triangulation().tds;
Some(snapshot_rebuild_state(tds)?)
} else {
None
};
let pl_config = PlManifoldRepairConfig {
max_iterations: config.topology_max_iterations,
max_cells_removed: config.topology_max_cells_removed,
};
let topology_stats =
match repair_facet_oversharing(&mut dt.as_triangulation_mut().tds, &pl_config) {
Ok(stats) => stats,
Err(topo_err) if let Some((ref verts, ref cell_data)) = fallback_state => {
match rebuild_preserving_data(&dt.as_triangulation().kernel, verts, cell_data) {
Ok(rebuilt) => {
*dt = rebuilt;
return Ok(DelaunayizeOutcome {
topology_repair: PlManifoldRepairStats {
succeeded: true,
..PlManifoldRepairStats::default()
},
delaunay_repair: DelaunayRepairStats::default(),
used_fallback_rebuild: true,
});
}
Err(fallback_error) => {
return Err(topology_rebuild_error(topo_err, fallback_error));
}
}
}
Err(topo_err) => return Err(topo_err.into()),
};
let delaunay_result = if let Some(max_flips) = config.delaunay_max_flips {
dt.repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig {
max_flips: Some(max_flips),
..DelaunayRepairHeuristicConfig::default()
})
.map(|outcome| outcome.stats)
} else {
dt.repair_delaunay_with_flips()
};
match delaunay_result {
Ok(delaunay_stats) => Ok(DelaunayizeOutcome {
topology_repair: topology_stats,
delaunay_repair: delaunay_stats,
used_fallback_rebuild: false,
}),
Err(repair_err) => {
if config.fallback_rebuild {
let tds = &dt.as_triangulation().tds;
let (vertices, cell_data) = match snapshot_rebuild_state(tds) {
Ok(state) => state,
Err(snapshot_error) => {
return Err(DelaunayizeError::DelaunayRepairFailedWithCellDataSnapshot {
source: repair_err,
snapshot_error,
});
}
};
match rebuild_preserving_data(&dt.as_triangulation().kernel, &vertices, &cell_data)
{
Ok(rebuilt) => {
*dt = rebuilt;
Ok(DelaunayizeOutcome {
topology_repair: topology_stats,
delaunay_repair: DelaunayRepairStats::default(),
used_fallback_rebuild: true,
})
}
Err(fallback_error) => Err(delaunay_rebuild_error(repair_err, fallback_error)),
}
} else {
Err(DelaunayizeError::from(repair_err))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{tds::VertexKey, triangulation::TriangulationConstructionError};
use crate::geometry::kernel::AdaptiveKernel;
use crate::triangulation::builder::DelaunayTriangulationBuilder;
use crate::vertex;
use slotmap::KeyData;
use std::error::Error as StdError;
fn init_tracing() {
let _ = tracing_subscriber::fmt::try_init();
}
fn construction_error() -> DelaunayTriangulationConstructionError {
DelaunayTriangulationConstructionError::Triangulation(
TriangulationConstructionError::FailedToCreateCell {
message: "synthetic rebuild degeneracy".to_string(),
},
)
}
#[test]
fn test_config_defaults() {
init_tracing();
let config = DelaunayizeConfig::default();
assert_eq!(config.topology_max_iterations, 64);
assert_eq!(config.topology_max_cells_removed, 10_000);
assert!(!config.fallback_rebuild);
assert!(config.delaunay_max_flips.is_none());
}
#[test]
fn test_already_delaunay_3d() {
init_tracing();
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::new(&vertices).unwrap();
let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap();
assert!(outcome.topology_repair.succeeded);
assert!(!outcome.used_fallback_rebuild);
assert!(dt.validate().is_ok());
}
#[test]
fn test_already_delaunay_2d() {
init_tracing();
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
vertex!([1.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices).unwrap();
let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap();
assert!(outcome.topology_repair.succeeded);
assert!(dt.validate().is_ok());
}
#[test]
fn test_outcome_populated_on_success() {
init_tracing();
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
vertex!([0.5, 0.5, 0.5]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::new(&vertices).unwrap();
let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap();
assert!(outcome.topology_repair.succeeded);
assert_eq!(outcome.topology_repair.cells_removed, 0);
assert!(!outcome.used_fallback_rebuild);
}
#[test]
fn test_collect_cell_data_missing_vertex() {
let mut tds: Tds<f64, (), i32, 2> = Tds::empty();
let vertex_keys: Vec<_> = [
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
]
.iter()
.map(|vertex| tds.insert_vertex_with_mapping(*vertex).unwrap())
.collect();
let missing = vertex_keys[0];
let cell = Cell::new(vertex_keys, Some(7)).unwrap();
let dangling_cell = cell.clone();
let cell_key = tds.insert_cell_with_mapping(cell).unwrap();
assert_eq!(tds.remove_cells_by_keys(&[cell_key]), 1);
tds.remove_isolated_vertex(missing);
tds.cells_mut().insert(dangling_cell);
let err = collect_cell_data(&tds).unwrap_err();
assert_eq!(err, CellValidationError::VertexKeyNotFound { key: missing });
}
#[test]
fn test_snapshot_error_source() {
let source = CellValidationError::VertexKeyNotFound {
key: VertexKey::from(KeyData::from_ffi(0xBAD)),
};
let err = DelaunayizeError::FallbackCellDataSnapshotFailed {
source: source.clone(),
};
assert_eq!(
err,
DelaunayizeError::FallbackCellDataSnapshotFailed {
source: source.clone()
}
);
assert!(
err.to_string()
.contains("Fallback cell-data snapshot failed")
);
assert!(err.to_string().contains("no repair attempted"));
let error_source = StdError::source(&err).unwrap();
assert_eq!(error_source.to_string(), source.to_string());
}
#[test]
fn test_repair_snapshot_error_source() {
let source = DelaunayRepairError::PostconditionFailed {
message: "synthetic postcondition".to_string(),
};
let snapshot_error = CellValidationError::VertexKeyNotFound {
key: VertexKey::from(KeyData::from_ffi(0xBAD)),
};
let err = DelaunayizeError::DelaunayRepairFailedWithCellDataSnapshot {
source: source.clone(),
snapshot_error: snapshot_error.clone(),
};
assert_eq!(
err,
DelaunayizeError::DelaunayRepairFailedWithCellDataSnapshot {
source: source.clone(),
snapshot_error,
}
);
assert!(
err.to_string()
.contains("fallback cell-data snapshot failed")
);
let error_source = StdError::source(&err).unwrap();
assert_eq!(error_source.to_string(), source.to_string());
}
#[test]
fn test_restore_error_sources() {
let topology_source = PlManifoldRepairError::NoProgress {
over_shared_facets: 2,
iterations: 3,
cells_removed: 4,
};
let delaunay_source = DelaunayRepairError::PostconditionFailed {
message: "synthetic postcondition".to_string(),
};
let restore_error = CellValidationError::VertexKeyNotFound {
key: VertexKey::from(KeyData::from_ffi(0xBAD)),
};
let topology_err = DelaunayizeError::TopologyRepairFailedWithRebuildRestore {
source: topology_source.clone(),
restore_error: restore_error.clone(),
};
let delaunay_err = DelaunayizeError::DelaunayRepairFailedWithRebuildRestore {
source: delaunay_source.clone(),
restore_error,
};
assert!(
topology_err
.to_string()
.contains("cell-data restore failed")
);
assert_eq!(
StdError::source(&topology_err).unwrap().to_string(),
topology_source.to_string()
);
assert!(
delaunay_err
.to_string()
.contains("cell-data restore failed")
);
assert_eq!(
StdError::source(&delaunay_err).unwrap().to_string(),
delaunay_source.to_string()
);
}
#[test]
fn test_topology_rebuild_error_mapping() {
let source = PlManifoldRepairError::NoProgress {
over_shared_facets: 2,
iterations: 3,
cells_removed: 4,
};
let rebuild_error = construction_error();
let restore_error = CellValidationError::VertexKeyNotFound {
key: VertexKey::from(KeyData::from_ffi(0xBAD)),
};
let rebuild_err = topology_rebuild_error(
source.clone(),
FallbackRebuildError::Construction {
source: rebuild_error.clone(),
},
);
assert_eq!(
rebuild_err,
DelaunayizeError::TopologyRepairFailedWithRebuild {
source: source.clone(),
rebuild_error,
}
);
assert!(
rebuild_err
.to_string()
.contains("fallback rebuild also failed")
);
let restore_err = topology_rebuild_error(
source.clone(),
FallbackRebuildError::Restore {
source: restore_error.clone(),
},
);
assert_eq!(
restore_err,
DelaunayizeError::TopologyRepairFailedWithRebuildRestore {
source,
restore_error,
}
);
assert!(
restore_err
.to_string()
.contains("fallback rebuild succeeded but cell-data restore failed")
);
}
#[test]
fn test_delaunay_rebuild_error_mapping() {
let source = DelaunayRepairError::PostconditionFailed {
message: "synthetic postcondition".to_string(),
};
let rebuild_error = construction_error();
let restore_error = CellValidationError::VertexKeyNotFound {
key: VertexKey::from(KeyData::from_ffi(0xBAD)),
};
let rebuild_err = delaunay_rebuild_error(
source.clone(),
FallbackRebuildError::Construction {
source: rebuild_error.clone(),
},
);
assert_eq!(
rebuild_err,
DelaunayizeError::DelaunayRepairFailedWithRebuild {
source: source.clone(),
rebuild_error,
}
);
assert!(
rebuild_err
.to_string()
.contains("fallback rebuild also failed")
);
let restore_err = delaunay_rebuild_error(
source.clone(),
FallbackRebuildError::Restore {
source: restore_error.clone(),
},
);
assert_eq!(
restore_err,
DelaunayizeError::DelaunayRepairFailedWithRebuildRestore {
source,
restore_error,
}
);
assert!(
restore_err
.to_string()
.contains("fallback rebuild succeeded but cell-data restore failed")
);
}
#[test]
fn test_fallback_disabled_by_default() {
init_tracing();
let config = DelaunayizeConfig::default();
assert!(!config.fallback_rebuild);
}
#[test]
fn test_fallback_enabled_on_valid_triangulation() {
init_tracing();
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::new(&vertices).unwrap();
let config = DelaunayizeConfig {
fallback_rebuild: true,
..DelaunayizeConfig::default()
};
let outcome = delaunayize_by_flips(&mut dt, config).unwrap();
assert!(!outcome.used_fallback_rebuild);
}
#[test]
fn test_rebuild_restores_cell_data() {
init_tracing();
let vertices = [
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let mut dt = DelaunayTriangulationBuilder::new(&vertices)
.build::<i32>()
.unwrap();
let original_cell_key = dt.cells().next().unwrap().0;
dt.set_cell_data(original_cell_key, Some(42));
let tds = &dt.as_triangulation().tds;
let vertices: Vec<_> = tds
.vertices()
.map(|(_, v)| Vertex::new_with_uuid(*v.point(), v.uuid(), v.data))
.collect();
let cell_data = collect_cell_data(tds).unwrap();
let rebuilt =
rebuild_preserving_data(&dt.as_triangulation().kernel, &vertices, &cell_data).unwrap();
let (_, rebuilt_cell) = rebuilt.cells().next().unwrap();
assert_eq!(rebuilt_cell.data(), Some(&42));
assert!(rebuilt.validate().is_ok());
}
#[test]
fn test_rebuild_drops_ambiguous_data() {
init_tracing();
let vertices = [
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let mut tds: Tds<f64, (), i32, 2> = Tds::empty();
let vertex_keys: Vec<_> = vertices
.iter()
.map(|vertex| tds.insert_vertex_with_mapping(*vertex).unwrap())
.collect();
let duplicate_a = Cell::new(vertex_keys.clone(), Some(42)).unwrap();
let duplicate_b = Cell::new(vertex_keys, Some(42)).unwrap();
tds.insert_cell_with_mapping(duplicate_a).unwrap();
tds.insert_cell_with_mapping(duplicate_b).unwrap();
let rebuild_vertices: Vec<_> = tds
.vertices()
.map(|(_, v)| Vertex::new_with_uuid(*v.point(), v.uuid(), v.data))
.collect();
let cell_data = collect_cell_data(&tds).unwrap();
let kernel = AdaptiveKernel::new();
let mut rebuilt: DelaunayTriangulation<_, (), i32, 2> =
DelaunayTriangulation::with_kernel(&kernel, &rebuild_vertices).unwrap();
restore_cell_data(&mut rebuilt, &cell_data).unwrap();
let (_, rebuilt_cell) = rebuilt.cells().next().unwrap();
assert_eq!(rebuilt_cell.data(), None);
assert!(rebuilt.validate().is_ok());
}
#[test]
fn test_deterministic_repeated_runs() {
init_tracing();
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
vertex!([0.5, 0.5, 0.5]),
];
let config = DelaunayizeConfig::default();
let mut dt1: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::new(&vertices).unwrap();
let outcome1 = delaunayize_by_flips(&mut dt1, config).unwrap();
let mut dt2: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::new(&vertices).unwrap();
let outcome2 = delaunayize_by_flips(&mut dt2, config).unwrap();
assert_eq!(
outcome1.topology_repair.cells_removed,
outcome2.topology_repair.cells_removed
);
assert_eq!(
outcome1.used_fallback_rebuild,
outcome2.used_fallback_rebuild
);
}
}