use crate::core::collections::{
CavityBoundaryBuffer, CellKeyBuffer, CellSecondaryMap, FacetToCellsMap, FastHashMap,
FastHashSet, FastHasher, MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer,
};
use crate::core::facet::FacetHandle;
use crate::core::traits::data_type::DataType;
use crate::core::triangulation_data_structure::{CellKey, Tds, VertexKey};
use crate::core::util::canonical_points::{sorted_cell_points, sorted_facet_points_with_extra};
use crate::geometry::kernel::Kernel;
use crate::geometry::point::Point;
use crate::geometry::traits::coordinate::{CoordinateConversionError, CoordinateScalar};
use std::hash::{Hash, Hasher};
#[cfg(debug_assertions)]
#[derive(Debug, Clone, Copy)]
struct ConflictDebugConfig {
log_conflict: bool,
progress_enabled: bool,
progress_every: usize,
}
#[cfg(debug_assertions)]
fn conflict_debug_config() -> &'static ConflictDebugConfig {
static CONFIG: std::sync::OnceLock<ConflictDebugConfig> = std::sync::OnceLock::new();
CONFIG.get_or_init(|| ConflictDebugConfig {
log_conflict: std::env::var_os("DELAUNAY_DEBUG_CONFLICT").is_some(),
progress_enabled: std::env::var_os("DELAUNAY_DEBUG_CONFLICT_PROGRESS").is_some(),
progress_every: std::env::var("DELAUNAY_DEBUG_CONFLICT_PROGRESS_EVERY")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|value| *value > 0)
.unwrap_or(5000),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocateResult {
InsideCell(CellKey),
OnFacet(CellKey, u8), OnEdge(CellKey),
OnVertex(VertexKey),
Outside,
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum LocateError {
#[error("Cannot locate in empty triangulation")]
EmptyTriangulation,
#[error("Invalid cell reference: {cell_key:?}")]
InvalidCell {
cell_key: CellKey,
},
#[error("Predicate error: {source}")]
PredicateError {
#[from]
source: CoordinateConversionError,
},
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum ConflictError {
#[error("Invalid starting cell: {cell_key:?}")]
InvalidStartCell {
cell_key: CellKey,
},
#[error("Predicate error: {source}")]
PredicateError {
#[from]
source: CoordinateConversionError,
},
#[error("Failed to access required data for cell {cell_key:?}: {message}")]
CellDataAccessFailed {
cell_key: CellKey,
message: String,
},
#[error(
"Non-manifold facet detected: facet {facet_hash:#x} shared by {cell_count} conflict cells (expected ≤2)"
)]
NonManifoldFacet {
facet_hash: u64,
cell_count: usize,
},
#[error(
"Ridge fan detected: {facet_count} facets share ridge with {ridge_vertex_count} vertices (indicates degenerate geometry requiring perturbation)"
)]
RidgeFan {
facet_count: usize,
ridge_vertex_count: usize,
extra_cells: Vec<CellKey>,
},
#[error(
"Disconnected cavity boundary: visited {visited} of {total} boundary facets (indicates degenerate geometry requiring perturbation)"
)]
DisconnectedBoundary {
visited: usize,
total: usize,
disconnected_cells: Vec<CellKey>,
},
#[error(
"Open cavity boundary: ridge with {ridge_vertex_count} vertices is incident to {facet_count} boundary facets (expected 2; indicates degenerate geometry requiring perturbation)"
)]
OpenBoundary {
facet_count: usize,
ridge_vertex_count: usize,
open_cell: CellKey,
},
}
#[derive(Debug, Clone)]
struct RidgeInfo {
ridge_vertex_count: usize,
facet_count: usize,
first_facet: usize,
second_facet: Option<usize>,
extra_facets: Vec<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocateFallbackReason {
CycleDetected,
StepLimit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocateFallback {
pub reason: LocateFallbackReason,
pub steps: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocateStats {
pub start_cell: CellKey,
pub used_hint: bool,
pub walk_steps: usize,
pub fallback: Option<LocateFallback>,
}
impl LocateStats {
#[must_use]
pub const fn fell_back_to_scan(&self) -> bool {
self.fallback.is_some()
}
}
pub fn locate<K, U, V, const D: usize>(
tds: &Tds<K::Scalar, U, V, D>,
kernel: &K,
point: &Point<K::Scalar, D>,
hint: Option<CellKey>,
) -> Result<LocateResult, LocateError>
where
K: Kernel<D>,
U: DataType,
V: DataType,
{
locate_with_stats(tds, kernel, point, hint).map(|(result, _stats)| result)
}
pub fn locate_with_stats<K, U, V, const D: usize>(
tds: &Tds<K::Scalar, U, V, D>,
kernel: &K,
point: &Point<K::Scalar, D>,
hint: Option<CellKey>,
) -> Result<(LocateResult, LocateStats), LocateError>
where
K: Kernel<D>,
U: DataType,
V: DataType,
{
const MAX_STEPS: usize = 10000;
if tds.number_of_cells() == 0 {
return Err(LocateError::EmptyTriangulation);
}
let (start_cell, used_hint) = match hint {
Some(key) if tds.contains_cell(key) => (key, true),
_ => (
tds.cell_keys()
.next()
.ok_or(LocateError::EmptyTriangulation)?,
false,
),
};
let mut stats = LocateStats {
start_cell,
used_hint,
walk_steps: 0,
fallback: None,
};
let mut current_cell = start_cell;
let mut visited: FastHashSet<CellKey> = FastHashSet::default();
for step in 0..MAX_STEPS {
stats.walk_steps = step + 1;
if !visited.insert(current_cell) {
stats.fallback = Some(LocateFallback {
reason: LocateFallbackReason::CycleDetected,
steps: stats.walk_steps,
});
let result = locate_by_scan(tds, kernel, point)?;
return Ok((result, stats));
}
let cell = tds.get_cell(current_cell).ok_or(LocateError::InvalidCell {
cell_key: current_cell,
})?;
let facet_count = cell.number_of_vertices();
let mut found_outside_facet = false;
for facet_idx in 0..facet_count {
if is_point_outside_facet(tds, kernel, current_cell, facet_idx, point)? == Some(true) {
if let Some(neighbor_key) = cell
.neighbors()
.and_then(|neighbors| neighbors.get(facet_idx))
.and_then(|&opt_key| opt_key)
{
current_cell = neighbor_key;
found_outside_facet = true;
break;
}
return Ok((LocateResult::Outside, stats));
}
}
if !found_outside_facet {
return Ok((LocateResult::InsideCell(current_cell), stats));
}
}
stats.fallback = Some(LocateFallback {
reason: LocateFallbackReason::StepLimit,
steps: stats.walk_steps,
});
let result = locate_by_scan(tds, kernel, point)?;
Ok((result, stats))
}
pub(crate) fn locate_by_scan<K, U, V, const D: usize>(
tds: &Tds<K::Scalar, U, V, D>,
kernel: &K,
point: &Point<K::Scalar, D>,
) -> Result<LocateResult, LocateError>
where
K: Kernel<D>,
U: DataType,
V: DataType,
{
for (cell_key, cell) in tds.cells() {
let mut found_outside_facet = false;
let facet_count = cell.number_of_vertices();
for facet_idx in 0..facet_count {
if is_point_outside_facet(tds, kernel, cell_key, facet_idx, point)? == Some(true) {
found_outside_facet = true;
break;
}
}
if !found_outside_facet {
return Ok(LocateResult::InsideCell(cell_key));
}
}
Ok(LocateResult::Outside)
}
fn is_point_outside_facet<K, U, V, const D: usize>(
tds: &Tds<K::Scalar, U, V, D>,
kernel: &K,
cell_key: CellKey,
facet_idx: usize,
query_point: &Point<K::Scalar, D>,
) -> Result<Option<bool>, LocateError>
where
K: Kernel<D>,
U: DataType,
V: DataType,
{
let cell = tds
.get_cell(cell_key)
.ok_or(LocateError::InvalidCell { cell_key })?;
let cell_vertex_keys = cell.vertices();
if cell_vertex_keys.len() != D + 1 {
return Ok(None); }
if facet_idx > D {
return Ok(None); }
let opposite_key = cell_vertex_keys[facet_idx];
let Some(opposite_point) = tds.get_vertex_by_key(opposite_key).map(|v| *v.point()) else {
return Ok(None); };
let facet_keys: SmallBuffer<VertexKey, MAX_PRACTICAL_DIMENSION_SIZE> = cell_vertex_keys
.iter()
.enumerate()
.filter(|&(i, _)| i != facet_idx)
.map(|(_, &vk)| vk)
.collect();
let Some(canonical_cell) = sorted_facet_points_with_extra(tds, &facet_keys, opposite_point)
else {
return Ok(None);
};
let cell_orientation = kernel.orientation(&canonical_cell)?;
let mut query_simplex = canonical_cell;
let last = query_simplex.len() - 1;
query_simplex[last] = *query_point;
let query_orientation = kernel.orientation(&query_simplex)?;
Ok(Some(cell_orientation * query_orientation < 0))
}
#[expect(
clippy::too_many_lines,
reason = "function is long due to complex locate logic and should be split when refactoring"
)]
pub fn find_conflict_region<K, U, V, const D: usize>(
tds: &Tds<K::Scalar, U, V, D>,
kernel: &K,
point: &Point<K::Scalar, D>,
start_cell: CellKey,
) -> Result<CellKeyBuffer, ConflictError>
where
K: Kernel<D>,
U: DataType,
V: DataType,
{
#[cfg(debug_assertions)]
let debug_config = conflict_debug_config();
#[cfg(debug_assertions)]
let start_time = std::time::Instant::now();
#[cfg(debug_assertions)]
let mut visited_count = 0usize;
#[cfg(debug_assertions)]
let mut conflict_count = 0usize;
#[cfg(debug_assertions)]
let mut neighbor_enqueued = 0usize;
if !tds.contains_cell(start_cell) {
return Err(ConflictError::InvalidStartCell {
cell_key: start_cell,
});
}
let mut conflict_cells = CellKeyBuffer::new();
let mut queue = CellKeyBuffer::new();
queue.push(start_cell);
let mut visited = CellSecondaryMap::new();
while let Some(cell_key) = queue.pop() {
if visited.contains_key(cell_key) {
continue;
}
visited.insert(cell_key, ());
#[cfg(debug_assertions)]
{
visited_count = visited_count.saturating_add(1);
if debug_config.progress_enabled
&& visited_count.is_multiple_of(debug_config.progress_every)
{
tracing::debug!(
visited_count,
conflict_count,
queue_len = queue.len(),
neighbor_enqueued,
elapsed = ?start_time.elapsed(),
"find_conflict_region: progress"
);
}
}
let cell = tds
.get_cell(cell_key)
.ok_or_else(|| ConflictError::CellDataAccessFailed {
cell_key,
message: "Cell vanished during BFS traversal".to_string(),
})?;
let simplex_points =
sorted_cell_points(tds, cell).ok_or_else(|| ConflictError::CellDataAccessFailed {
cell_key,
message: format!("Failed to resolve all {} cell vertices", D + 1),
})?;
if simplex_points.len() != D + 1 {
return Err(ConflictError::CellDataAccessFailed {
cell_key,
message: format!("Expected {} vertices, got {}", D + 1, simplex_points.len()),
});
}
#[cfg(debug_assertions)]
if debug_config.log_conflict {
tracing::debug!(
cell_key = ?cell_key,
vertex_keys = ?cell.vertices(),
simplex_len = simplex_points.len(),
query_point = ?point,
"find_conflict_region: in_sphere input"
);
}
let sign = match kernel.in_sphere(&simplex_points, point) {
Ok(value) => value,
Err(err) => {
#[cfg(debug_assertions)]
if debug_config.log_conflict {
tracing::debug!(
cell_key = ?cell_key,
vertex_keys = ?cell.vertices(),
query_point = ?point,
error = ?err,
"find_conflict_region: in_sphere failed"
);
}
return Err(err.into());
}
};
#[cfg(debug_assertions)]
if debug_config.log_conflict {
tracing::debug!(
cell_key = ?cell_key,
sign,
in_conflict = sign >= 0,
"find_conflict_region: in_sphere classification"
);
}
if sign >= 0 {
conflict_cells.push(cell_key);
#[cfg(debug_assertions)]
{
conflict_count = conflict_count.saturating_add(1);
}
if let Some(neighbors) = cell.neighbors() {
for &neighbor_opt in neighbors {
if let Some(neighbor_key) = neighbor_opt
&& !visited.contains_key(neighbor_key)
{
queue.push(neighbor_key);
#[cfg(debug_assertions)]
{
neighbor_enqueued = neighbor_enqueued.saturating_add(1);
}
}
}
}
} else {
#[cfg(debug_assertions)]
if debug_config.log_conflict {
let neighbor_keys: SmallBuffer<Option<CellKey>, MAX_PRACTICAL_DIMENSION_SIZE> =
cell.neighbors()
.map(|ns| ns.iter().copied().collect())
.unwrap_or_default();
tracing::debug!(
cell_key = ?cell_key,
vertex_keys = ?cell.vertices(),
sign,
neighbors = ?neighbor_keys,
"find_conflict_region: BFS boundary (non-conflict)"
);
}
}
}
#[cfg(debug_assertions)]
if debug_config.progress_enabled || debug_config.log_conflict {
tracing::debug!(
visited_count,
conflict_cells = conflict_cells.len(),
neighbor_enqueued,
elapsed = ?start_time.elapsed(),
"find_conflict_region: summary"
);
}
Ok(conflict_cells)
}
#[cfg(debug_assertions)]
pub fn verify_conflict_region_completeness<K, U, V, const D: usize>(
tds: &Tds<K::Scalar, U, V, D>,
kernel: &K,
point: &Point<K::Scalar, D>,
bfs_conflict_cells: &CellKeyBuffer,
) -> usize
where
K: Kernel<D>,
U: DataType,
V: DataType,
{
let bfs_set: FastHashSet<CellKey> = bfs_conflict_cells.iter().copied().collect();
let mut missed_count = 0usize;
let mut brute_force_count = 0usize;
let mut malformed_cells = 0usize;
let mut predicate_errors = 0usize;
for (cell_key, cell) in tds.cells() {
let Some(simplex_points) = sorted_cell_points(tds, cell) else {
malformed_cells += 1;
tracing::debug!(
cell_key = ?cell_key,
vertex_keys = ?cell.vertices(),
"verify_conflict_region: skipping malformed cell (sorted_cell_points returned None)"
);
continue;
};
if simplex_points.len() != D + 1 {
malformed_cells += 1;
continue;
}
let Ok(sign) = kernel.in_sphere(&simplex_points, point) else {
predicate_errors += 1;
tracing::debug!(
cell_key = ?cell_key,
vertex_keys = ?cell.vertices(),
"verify_conflict_region: in_sphere predicate failed"
);
continue;
};
if sign >= 0 {
brute_force_count += 1;
if !bfs_set.contains(&cell_key) {
missed_count += 1;
let (neighbor_in_bfs, neighbor_total, neighbor_none) =
cell.neighbors().map_or((0, 0, 0), |neighbors| {
let total = neighbors.len();
let none_count = neighbors.iter().filter(|n| n.is_none()).count();
let in_bfs = neighbors
.iter()
.filter_map(|n| *n)
.filter(|nk| bfs_set.contains(nk))
.count();
(in_bfs, total, none_count)
});
let reachability = if neighbor_in_bfs > 0 {
"REACHABLE_BUT_REJECTED"
} else {
"UNREACHABLE"
};
tracing::warn!(
cell_key = ?cell_key,
vertex_keys = ?cell.vertices(),
sign,
reachability,
neighbor_in_bfs,
neighbor_total,
neighbor_none,
bfs_conflict_len = bfs_conflict_cells.len(),
brute_force_conflict_so_far = brute_force_count,
"verify_conflict_region: BFS MISSED conflicting cell"
);
}
}
}
if missed_count > 0 || malformed_cells > 0 || predicate_errors > 0 {
tracing::warn!(
bfs_conflict = bfs_conflict_cells.len(),
brute_force_conflict = brute_force_count,
missed = missed_count,
malformed_cells,
predicate_errors,
query_point = ?point,
"verify_conflict_region: INCOMPLETE — missed cells or evaluation failures"
);
} else {
tracing::debug!(
bfs_conflict = bfs_conflict_cells.len(),
brute_force_conflict = brute_force_count,
"verify_conflict_region: conflict region is COMPLETE"
);
}
missed_count
}
#[expect(
clippy::too_many_lines,
reason = "Long function; keep boundary extraction logic in one place for clarity"
)]
pub fn extract_cavity_boundary<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
conflict_cells: &CellKeyBuffer,
) -> Result<CavityBoundaryBuffer, ConflictError>
where
T: CoordinateScalar,
U: DataType,
V: DataType,
{
if conflict_cells.is_empty() {
return Ok(CavityBoundaryBuffer::new());
}
#[cfg(debug_assertions)]
let detail_enabled = std::env::var_os("DELAUNAY_DEBUG_CAVITY").is_some();
#[cfg(debug_assertions)]
let start_time = std::time::Instant::now();
#[cfg(debug_assertions)]
let mut boundary_facet_count = 0usize;
#[cfg(debug_assertions)]
let mut internal_facet_count = 0usize;
#[cfg(debug_assertions)]
if detail_enabled {
tracing::debug!(
conflict_cells = conflict_cells.len(),
"extract_cavity_boundary: start"
);
}
let conflict_set: FastHashSet<CellKey> = conflict_cells.iter().copied().collect();
let mut facet_to_conflict: FacetToCellsMap = FacetToCellsMap::default();
let mut facet_hash_to_vkeys: FastHashMap<
u64,
SmallBuffer<VertexKey, MAX_PRACTICAL_DIMENSION_SIZE>,
> = FastHashMap::default();
for &cell_key in &conflict_set {
let cell = tds
.get_cell(cell_key)
.ok_or(ConflictError::InvalidStartCell { cell_key })?;
let facet_count = cell.number_of_vertices(); for facet_idx in 0..facet_count {
let mut facet_vkeys = SmallBuffer::<VertexKey, MAX_PRACTICAL_DIMENSION_SIZE>::new();
for (i, &vkey) in cell.vertices().iter().enumerate() {
if i != facet_idx {
facet_vkeys.push(vkey);
}
}
facet_vkeys.sort_unstable();
let mut hasher = FastHasher::default();
for &vkey in &facet_vkeys {
vkey.hash(&mut hasher);
}
let facet_hash = hasher.finish();
facet_hash_to_vkeys.entry(facet_hash).or_insert(facet_vkeys);
let facet_idx_u8 =
u8::try_from(facet_idx).map_err(|_| ConflictError::CellDataAccessFailed {
cell_key,
message: format!("Facet index {facet_idx} exceeds u8::MAX"),
})?;
facet_to_conflict
.entry(facet_hash)
.or_default()
.push(FacetHandle::new(cell_key, facet_idx_u8));
}
}
let mut boundary_facets = CavityBoundaryBuffer::new();
let mut ridge_map: FastHashMap<u64, RidgeInfo> = FastHashMap::default();
for (facet_hash, cell_facet_pairs) in &facet_to_conflict {
match cell_facet_pairs.as_slice() {
[handle] => {
let cell_key = handle.cell_key();
let facet_idx_u8 = handle.facet_index();
let boundary_facet_idx = boundary_facets.len();
boundary_facets.push(FacetHandle::new(cell_key, facet_idx_u8));
#[cfg(debug_assertions)]
{
boundary_facet_count = boundary_facet_count.saturating_add(1);
}
let facet_vkeys = facet_hash_to_vkeys.get(facet_hash).ok_or_else(|| {
ConflictError::CellDataAccessFailed {
cell_key,
message: format!(
"Missing canonical vertex keys for facet hash {:#x}",
*facet_hash
),
}
})?;
if facet_vkeys.len() >= 2 {
let ridge_vertex_count = facet_vkeys.len() - 1;
for ridge_idx in 0..facet_vkeys.len() {
let mut ridge_hasher = FastHasher::default();
for (i, &vkey) in facet_vkeys.iter().enumerate() {
if i != ridge_idx {
vkey.hash(&mut ridge_hasher);
}
}
let ridge_hash = ridge_hasher.finish();
ridge_map
.entry(ridge_hash)
.and_modify(|info| {
info.facet_count += 1;
if info.second_facet.is_none() {
info.second_facet = Some(boundary_facet_idx);
} else {
info.extra_facets.push(boundary_facet_idx);
}
})
.or_insert(RidgeInfo {
ridge_vertex_count,
facet_count: 1,
first_facet: boundary_facet_idx,
second_facet: None,
extra_facets: Vec::new(),
});
}
}
}
[_, _] => {
#[cfg(debug_assertions)]
{
internal_facet_count = internal_facet_count.saturating_add(1);
}
}
many => {
#[cfg(debug_assertions)]
if detail_enabled {
tracing::debug!(
facet_hash = *facet_hash,
cell_count = many.len(),
conflict_cells = conflict_cells.len(),
boundary_facet_count,
internal_facet_count,
elapsed = ?start_time.elapsed(),
"extract_cavity_boundary: non-manifold facet"
);
}
return Err(ConflictError::NonManifoldFacet {
facet_hash: *facet_hash,
cell_count: many.len(),
});
}
}
}
#[cfg(debug_assertions)]
if detail_enabled {
tracing::debug!(
conflict_cells = conflict_cells.len(),
facet_entries = facet_to_conflict.len(),
boundary_facets = boundary_facets.len(),
internal_facets = internal_facet_count,
elapsed = ?start_time.elapsed(),
"extract_cavity_boundary: facet classification summary"
);
}
#[cfg(debug_assertions)]
if detail_enabled {
let mut ridge_facet_one = 0usize;
let mut ridge_facet_two = 0usize;
let mut ridge_facet_many = 0usize;
let mut ridge_vertex_count_min = usize::MAX;
let mut ridge_vertex_count_max = 0usize;
for info in ridge_map.values() {
ridge_vertex_count_min = ridge_vertex_count_min.min(info.ridge_vertex_count);
ridge_vertex_count_max = ridge_vertex_count_max.max(info.ridge_vertex_count);
match info.facet_count {
1 => ridge_facet_one += 1,
2 => ridge_facet_two += 1,
_ => ridge_facet_many += 1,
}
}
if ridge_map.is_empty() {
ridge_vertex_count_min = 0;
}
tracing::debug!(
conflict_cells = conflict_cells.len(),
boundary_facets = boundary_facets.len(),
ridge_count = ridge_map.len(),
ridge_facet_one,
ridge_facet_two,
ridge_facet_many,
ridge_vertex_count_min,
ridge_vertex_count_max,
"extract_cavity_boundary: ridge summary"
);
}
if !boundary_facets.is_empty() {
let boundary_len = boundary_facets.len();
let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); boundary_len];
for info in ridge_map.values() {
if info.facet_count == 1 {
#[cfg(debug_assertions)]
if detail_enabled {
tracing::debug!(
facet_count = info.facet_count,
ridge_vertex_count = info.ridge_vertex_count,
boundary_facets = boundary_facets.len(),
ridge_count = ridge_map.len(),
elapsed = ?start_time.elapsed(),
"extract_cavity_boundary: open boundary ridge"
);
}
let open_cell = boundary_facets
.get(info.first_facet)
.ok_or_else(|| ConflictError::CellDataAccessFailed {
cell_key: CellKey::default(),
message: format!(
"OpenBoundary: boundary_facets missing first_facet index {} \
(boundary_facets.len()={})",
info.first_facet,
boundary_facets.len(),
),
})
.map(FacetHandle::cell_key)?;
return Err(ConflictError::OpenBoundary {
facet_count: info.facet_count,
ridge_vertex_count: info.ridge_vertex_count,
open_cell,
});
}
if info.facet_count >= 3 {
#[cfg(debug_assertions)]
if detail_enabled {
tracing::debug!(
facet_count = info.facet_count,
ridge_vertex_count = info.ridge_vertex_count,
extra_facets = info.extra_facets.len(),
boundary_facets = boundary_facets.len(),
ridge_count = ridge_map.len(),
elapsed = ?start_time.elapsed(),
"extract_cavity_boundary: ridge fan"
);
}
debug_assert!(
info.extra_facets
.iter()
.all(|&fi| fi < boundary_facets.len()),
"RidgeFan extra_facets index out of bounds: extra_facets={:?}, boundary_facets.len()={}",
info.extra_facets,
boundary_facets.len(),
);
let mut seen = FastHashSet::<CellKey>::default();
let mut extra_cells: Vec<CellKey> = Vec::new();
for &fi in &info.extra_facets {
let ck = boundary_facets
.get(fi)
.ok_or_else(|| ConflictError::CellDataAccessFailed {
cell_key: CellKey::default(),
message: format!(
"RidgeFan extra_facets index {fi} out of bounds \
(boundary_facets.len()={})",
boundary_facets.len()
),
})?
.cell_key();
if seen.insert(ck) {
extra_cells.push(ck);
}
}
return Err(ConflictError::RidgeFan {
facet_count: info.facet_count,
ridge_vertex_count: info.ridge_vertex_count,
extra_cells,
});
}
let a = info.first_facet;
let b = info.second_facet.ok_or_else(|| {
let fallback_cell_key = boundary_facets.first().map_or_else(
|| {
CellKey::default()
},
FacetHandle::cell_key,
);
let cell_key = boundary_facets
.get(a)
.map_or(fallback_cell_key, FacetHandle::cell_key);
ConflictError::CellDataAccessFailed {
cell_key,
message: "RidgeInfo missing second_facet when facet_count == 2".to_string(),
}
})?;
adjacency[a].push(b);
adjacency[b].push(a);
}
let mut visited = vec![false; boundary_len];
let mut stack = vec![0usize];
visited[0] = true;
let mut visited_count = 1usize;
while let Some(cur) = stack.pop() {
for &n in &adjacency[cur] {
if !visited[n] {
visited[n] = true;
visited_count += 1;
stack.push(n);
}
}
}
if visited_count != boundary_len {
#[cfg(debug_assertions)]
if detail_enabled {
tracing::debug!(
visited = visited_count,
total = boundary_len,
boundary_facets = boundary_facets.len(),
elapsed = ?start_time.elapsed(),
"extract_cavity_boundary: disconnected boundary"
);
}
let mut seen = FastHashSet::<CellKey>::default();
let disconnected_cells: Vec<CellKey> = boundary_facets
.iter()
.enumerate()
.filter(|(i, _)| !visited[*i])
.map(|(_, fh)| fh.cell_key())
.filter(|ck| seen.insert(*ck))
.collect();
return Err(ConflictError::DisconnectedBoundary {
visited: visited_count,
total: boundary_len,
disconnected_cells,
});
}
}
#[cfg(debug_assertions)]
if detail_enabled {
tracing::debug!(
conflict_cells = conflict_cells.len(),
boundary_facets = boundary_facets.len(),
internal_facets = internal_facet_count,
ridge_count = ridge_map.len(),
elapsed = ?start_time.elapsed(),
"extract_cavity_boundary: boundary connectivity validated"
);
}
Ok(boundary_facets)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::cell::Cell;
use crate::geometry::kernel::{FastKernel, RobustKernel};
use crate::geometry::traits::coordinate::Coordinate;
use crate::prelude::DelaunayTriangulation;
use crate::vertex;
use slotmap::KeyData;
#[test]
fn test_orientation_logic_manual() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let cell_key = dt.tds().cell_keys().next().unwrap();
let cell = dt.tds().get_cell(cell_key).unwrap();
let cell_points: Vec<Point<f64, 2>> = cell
.vertices()
.iter()
.map(|&vkey| *dt.tds().get_vertex_by_key(vkey).unwrap().point())
.collect();
println!("Cell vertices: {cell_points:?}");
let cell_orientation = kernel.orientation(&cell_points).unwrap();
println!("Cell orientation: {cell_orientation}");
let query_inside = Point::new([0.3, 0.3]);
for facet_idx in 0..3 {
let result =
is_point_outside_facet(dt.tds(), &kernel, cell_key, facet_idx, &query_inside);
let is_outside = result.unwrap() == Some(true);
println!("Facet {facet_idx} (opposite to vertex {facet_idx}): is_outside={is_outside}");
assert!(
!is_outside,
"Point inside triangle should not be outside facet {facet_idx}"
);
}
let query_outside = Point::new([2.0, 2.0]);
let mut found_outside_facet = false;
for facet_idx in 0..3 {
let result =
is_point_outside_facet(dt.tds(), &kernel, cell_key, facet_idx, &query_outside);
let is_outside = result.unwrap() == Some(true);
println!("Outside point - Facet {facet_idx}: is_outside={is_outside}");
if is_outside {
found_outside_facet = true;
}
}
assert!(
found_outside_facet,
"Point outside triangle should be outside at least one facet"
);
}
#[test]
fn test_locate_empty_triangulation() {
let tds: Tds<f64, (), (), 3> = Tds::empty();
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.0, 0.0, 0.0]);
let result = locate(&tds, &kernel, &point, None);
assert!(matches!(result, Err(LocateError::EmptyTriangulation)));
}
#[test]
fn test_locate_point_inside_2d() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.3, 0.3]);
let result = locate(dt.tds(), &kernel, &point, None);
match result {
Ok(LocateResult::InsideCell(cell_key)) => {
assert!(dt.tds().contains_cell(cell_key));
}
_ => panic!("Expected point to be inside a cell, got {result:?}"),
}
}
#[test]
fn test_locate_point_inside_3d() {
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 dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.25, 0.25, 0.25]);
let result = locate(dt.tds(), &kernel, &point, None);
match result {
Ok(LocateResult::InsideCell(cell_key)) => {
assert!(dt.tds().contains_cell(cell_key));
}
_ => panic!("Expected point to be inside a cell, got {result:?}"),
}
}
#[test]
fn test_locate_point_outside_2d() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let point = Point::new([10.0, 10.0]);
let result = locate(dt.tds(), &kernel, &point, None);
assert!(matches!(result, Ok(LocateResult::Outside)));
}
#[test]
fn test_locate_point_outside_3d() {
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 dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let point = Point::new([2.0, 2.0, 2.0]);
let result = locate(dt.tds(), &kernel, &point, None);
assert!(matches!(result, Ok(LocateResult::Outside)));
}
#[test]
fn test_locate_with_hint_cell() {
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 dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let hint_cell = dt.tds().cell_keys().next().unwrap();
let point = Point::new([0.25, 0.25, 0.25]);
let result = locate(dt.tds(), &kernel, &point, Some(hint_cell));
assert!(matches!(result, Ok(LocateResult::InsideCell(_))));
}
#[test]
fn test_locate_with_robust_kernel() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = RobustKernel::<f64>::default();
let point = Point::new([0.3, 0.3]);
let result = locate(dt.tds(), &kernel, &point, None);
assert!(matches!(result, Ok(LocateResult::InsideCell(_))));
}
#[test]
fn test_locate_with_stats_falls_back_on_cycle() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let cell_key = dt.tds().cell_keys().next().unwrap();
let cell = dt.tds_mut().get_cell_by_key_mut(cell_key).unwrap();
let mut neighbors = crate::core::collections::NeighborBuffer::<Option<CellKey>>::new();
neighbors.resize(3, Some(cell_key));
cell.neighbors = Some(neighbors);
let point = Point::new([10.0, 10.0]);
let (result, stats) = locate_with_stats(dt.tds(), &kernel, &point, None).unwrap();
assert!(matches!(result, LocateResult::Outside));
assert!(stats.fell_back_to_scan());
assert!(!stats.used_hint);
assert_eq!(stats.start_cell, cell_key);
assert_eq!(stats.walk_steps, 2);
assert!(matches!(
stats.fallback,
Some(LocateFallback {
reason: LocateFallbackReason::CycleDetected,
steps: 2,
})
));
}
#[test]
fn test_is_point_outside_facet_inside() {
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 dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let cell_key = dt.tds().cell_keys().next().unwrap();
let point = Point::new([0.25, 0.25, 0.25]);
for facet_idx in 0..4 {
let result = is_point_outside_facet(dt.tds(), &kernel, cell_key, facet_idx, &point);
assert!(matches!(result, Ok(Some(false) | None)));
}
}
#[test]
fn test_is_point_outside_facet_outside() {
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 dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let cell_key = dt.tds().cell_keys().next().unwrap();
let point = Point::new([2.0, 2.0, 2.0]);
let mut found_outside = false;
for facet_idx in 0..4 {
if matches!(
is_point_outside_facet(dt.tds(), &kernel, cell_key, facet_idx, &point),
Ok(Some(true))
) {
found_outside = true;
break;
}
}
assert!(
found_outside,
"Expected point to be outside at least one facet"
);
}
#[test]
fn test_locate_near_boundary() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = RobustKernel::<f64>::default();
let point = Point::new([0.01, 0.01]);
let result = locate(dt.tds(), &kernel, &point, None);
match result {
Ok(LocateResult::InsideCell(_) | LocateResult::OnEdge(_)) => { }
other => panic!("Expected inside or on edge, got {other:?}"),
}
}
macro_rules! test_locate_dimension {
($dim:literal, $inside_point:expr, $($coords:expr),+ $(,)?) => {{
let vertices: Vec<_> = vec![
$(vertex!($coords)),+
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let point = Point::new($inside_point);
let result = locate(dt.tds(), &kernel, &point, None);
assert!(
matches!(result, Ok(LocateResult::InsideCell(_))),
"Expected point to be inside a cell in {}-simplex",
$dim
);
}};
}
#[test]
fn test_locate_2d() {
test_locate_dimension!(2, [0.3, 0.3], [0.0, 0.0], [1.0, 0.0], [0.0, 1.0],);
}
#[test]
fn test_locate_3d() {
test_locate_dimension!(
3,
[0.25, 0.25, 0.25],
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
);
}
#[test]
fn test_locate_4d() {
test_locate_dimension!(
4,
[0.2, 0.2, 0.2, 0.2],
[0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
);
}
#[test]
fn test_locate_5d() {
test_locate_dimension!(
5,
[0.15, 0.15, 0.15, 0.15, 0.15],
[0.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
);
}
macro_rules! test_find_conflict_region_dimension {
($dim:literal, $inside_point:expr, $($coords:expr),+ $(,)?) => {{
let vertices: Vec<_> = vec![
$(vertex!($coords)),+
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let start_cell = dt.tds().cell_keys().next().unwrap();
let point = Point::new($inside_point);
let conflict_cells = find_conflict_region(dt.tds(), &kernel, &point, start_cell).unwrap();
assert_eq!(
conflict_cells.len(),
1,
"Expected 1 cell in conflict for point inside single {}-simplex",
$dim
);
}};
}
#[test]
fn test_find_conflict_region_2d() {
test_find_conflict_region_dimension!(2, [0.3, 0.3], [0.0, 0.0], [1.0, 0.0], [0.0, 1.0],);
}
#[test]
fn test_find_conflict_region_3d() {
test_find_conflict_region_dimension!(
3,
[0.25, 0.25, 0.25],
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
);
}
#[test]
fn test_find_conflict_region_4d() {
test_find_conflict_region_dimension!(
4,
[0.2, 0.2, 0.2, 0.2],
[0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
);
}
#[test]
fn test_find_conflict_region_5d() {
test_find_conflict_region_dimension!(
5,
[0.15, 0.15, 0.15, 0.15, 0.15],
[0.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
);
}
#[test]
fn test_find_conflict_region_outside_point() {
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 dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let start_cell = dt.tds().cell_keys().next().unwrap();
let point = Point::new([10.0, 10.0, 10.0]);
let conflict_cells = find_conflict_region(dt.tds(), &kernel, &point, start_cell).unwrap();
assert_eq!(
conflict_cells.len(),
0,
"Expected 0 cells in conflict for point far outside"
);
}
#[test]
fn test_find_conflict_region_invalid_start_cell() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let invalid_cell = CellKey::from(KeyData::from_ffi(999_999));
let point = Point::new([0.3, 0.3]);
let result = find_conflict_region(dt.tds(), &kernel, &point, invalid_cell);
assert!(
matches!(result, Err(ConflictError::InvalidStartCell { .. })),
"Expected InvalidStartCell error"
);
}
#[test]
fn test_find_conflict_region_with_robust_kernel() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = RobustKernel::<f64>::default();
let start_cell = dt.tds().cell_keys().next().unwrap();
let point = Point::new([0.3, 0.3]);
let conflict_cells = find_conflict_region(dt.tds(), &kernel, &point, start_cell).unwrap();
assert_eq!(
conflict_cells.len(),
1,
"Robust kernel should also find 1 cell in conflict"
);
}
macro_rules! test_cavity_boundary_dimension {
($dim:literal, $expected_facets:expr, $($coords:expr),+ $(,)?) => {{
let vertices: Vec<_> = vec![
$(vertex!($coords)),+
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let start_cell = dt.tds().cell_keys().next().unwrap();
let mut conflict_cells = CellKeyBuffer::new();
conflict_cells.push(start_cell);
let boundary = extract_cavity_boundary(dt.tds(), &conflict_cells).unwrap();
assert_eq!(
boundary.len(),
$expected_facets,
"Expected {} boundary facets for single {}-simplex",
$expected_facets,
$dim
);
}};
}
#[test]
fn test_extract_cavity_boundary_2d() {
test_cavity_boundary_dimension!(2, 3, [0.0, 0.0], [1.0, 0.0], [0.0, 1.0],);
}
#[test]
fn test_extract_cavity_boundary_3d() {
test_cavity_boundary_dimension!(
3,
4,
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
);
}
#[test]
fn test_extract_cavity_boundary_4d() {
test_cavity_boundary_dimension!(
4,
5,
[0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
);
}
#[test]
fn test_extract_cavity_boundary_5d() {
test_cavity_boundary_dimension!(
5,
6,
[0.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
);
}
#[test]
fn test_extract_cavity_boundary_empty_conflict() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let conflict_cells = CellKeyBuffer::new();
let boundary = extract_cavity_boundary(dt.tds(), &conflict_cells).unwrap();
assert_eq!(
boundary.len(),
0,
"Expected 0 boundary facets for empty conflict region"
);
}
#[test]
fn test_locate_with_stats_invalid_hint_uses_arbitrary_start_cell() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.25, 0.25]);
let invalid_hint = CellKey::from(KeyData::from_ffi(999_999));
let expected_start = dt.tds().cell_keys().next().unwrap();
let (result, stats) =
locate_with_stats(dt.tds(), &kernel, &point, Some(invalid_hint)).unwrap();
assert!(matches!(result, LocateResult::InsideCell(_)));
assert_eq!(stats.start_cell, expected_start);
assert!(!stats.used_hint);
assert!(!stats.fell_back_to_scan());
}
#[test]
fn test_locate_by_scan_inside_and_outside() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let expected_cell = dt.tds().cell_keys().next().unwrap();
let inside = Point::new([0.2, 0.2]);
let outside = Point::new([3.0, 3.0]);
assert_eq!(
locate_by_scan(dt.tds(), &kernel, &inside).unwrap(),
LocateResult::InsideCell(expected_cell)
);
assert_eq!(
locate_by_scan(dt.tds(), &kernel, &outside).unwrap(),
LocateResult::Outside
);
}
#[test]
fn test_extract_cavity_boundary_rejects_nonmanifold_facet_2d() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let origin = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let x_axis = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let y_axis = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let upper_right = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap();
let top_apex = tds.insert_vertex_with_mapping(vertex!([0.5, 1.5])).unwrap();
let first_cell = tds
.insert_cell_with_mapping(Cell::new(vec![origin, x_axis, y_axis], None).unwrap())
.unwrap();
let second_cell = tds
.insert_cell_with_mapping(Cell::new(vec![origin, x_axis, upper_right], None).unwrap())
.unwrap();
let third_cell = tds
.insert_cell_with_mapping(Cell::new(vec![origin, x_axis, top_apex], None).unwrap())
.unwrap();
let mut conflict_cells = CellKeyBuffer::new();
conflict_cells.push(first_cell);
conflict_cells.push(second_cell);
conflict_cells.push(third_cell);
let err = extract_cavity_boundary(&tds, &conflict_cells).unwrap_err();
assert!(matches!(
err,
ConflictError::NonManifoldFacet { cell_count: 3, .. }
));
}
#[test]
fn test_extract_cavity_boundary_detects_disconnected_boundary_2d() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let left_origin = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let left_x = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let left_y = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let right_origin = tds.insert_vertex_with_mapping(vertex!([3.0, 0.0])).unwrap();
let right_x = tds.insert_vertex_with_mapping(vertex!([4.0, 0.0])).unwrap();
let right_y = tds.insert_vertex_with_mapping(vertex!([3.0, 1.0])).unwrap();
let left = tds
.insert_cell_with_mapping(Cell::new(vec![left_origin, left_x, left_y], None).unwrap())
.unwrap();
let right = tds
.insert_cell_with_mapping(
Cell::new(vec![right_origin, right_x, right_y], None).unwrap(),
)
.unwrap();
let mut conflict_cells = CellKeyBuffer::new();
conflict_cells.push(left);
conflict_cells.push(right);
let err = extract_cavity_boundary(&tds, &conflict_cells).unwrap_err();
match err {
ConflictError::DisconnectedBoundary {
visited,
total,
disconnected_cells,
} => {
assert!(visited < total);
assert_eq!(total, 6);
assert!(
!disconnected_cells.is_empty(),
"disconnected_cells should be non-empty"
);
for ck in &disconnected_cells {
assert!(
tds.contains_cell(*ck),
"disconnected cell key {ck:?} should be present in the TDS"
);
}
}
other => panic!("Expected DisconnectedBoundary, got {other:?}"),
}
}
#[test]
fn test_locate_with_stats_valid_hint_marks_used_hint() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.2, 0.2]);
let hint = dt.tds().cell_keys().next().unwrap();
let (result, stats) = locate_with_stats(dt.tds(), &kernel, &point, Some(hint)).unwrap();
assert!(matches!(result, LocateResult::InsideCell(_)));
assert_eq!(stats.start_cell, hint);
assert!(stats.used_hint);
assert!(!stats.fell_back_to_scan());
}
#[test]
fn test_locate_by_scan_empty_returns_outside() {
let tds: Tds<f64, (), (), 2> = Tds::empty();
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.0, 0.0]);
assert_eq!(
locate_by_scan(&tds, &kernel, &point).unwrap(),
LocateResult::Outside
);
}
#[test]
fn test_extract_cavity_boundary_invalid_conflict_cell_key() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let invalid = CellKey::from(KeyData::from_ffi(424_242));
let mut conflict_cells = CellKeyBuffer::new();
conflict_cells.push(invalid);
let err = extract_cavity_boundary(dt.tds(), &conflict_cells).unwrap_err();
assert!(matches!(
err,
ConflictError::InvalidStartCell { cell_key } if cell_key == invalid
));
}
#[test]
fn test_is_point_outside_facet_underdimensioned_cell_returns_none() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let cell_key = tds
.insert_cell_with_mapping(Cell::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
{
let cell = tds.get_cell_by_key_mut(cell_key).unwrap();
cell.clear_vertex_keys();
cell.push_vertex_key(v0);
cell.push_vertex_key(v1);
}
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.3, 0.3]);
let result = is_point_outside_facet(&tds, &kernel, cell_key, 0, &point);
assert!(
matches!(result, Ok(None)),
"underdimensioned cell should return Ok(None), got {result:?}"
);
}
#[test]
fn test_is_point_outside_facet_unresolvable_opposite_vertex_returns_none() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let cell_key = tds
.insert_cell_with_mapping(Cell::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
let missing = VertexKey::from(KeyData::from_ffi(999_999));
{
let cell = tds.get_cell_by_key_mut(cell_key).unwrap();
cell.clear_vertex_keys();
cell.push_vertex_key(missing); cell.push_vertex_key(v1);
cell.push_vertex_key(v2);
}
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.3, 0.3]);
let result = is_point_outside_facet(&tds, &kernel, cell_key, 0, &point);
assert!(
matches!(result, Ok(None)),
"unresolvable opposite vertex should return Ok(None), got {result:?}"
);
}
#[test]
fn test_is_point_outside_facet_degenerate_cell_missing_vertex_returns_none() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let cell_key = tds
.insert_cell_with_mapping(Cell::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
let existing_vertices = tds.get_cell(cell_key).unwrap().vertices().to_vec();
let missing = VertexKey::from(KeyData::from_ffi(999_999));
{
let cell = tds.get_cell_by_key_mut(cell_key).unwrap();
cell.clear_vertex_keys();
cell.push_vertex_key(existing_vertices[0]);
cell.push_vertex_key(existing_vertices[1]);
cell.push_vertex_key(missing);
}
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.3_f64, 0.3_f64]);
let result = is_point_outside_facet(&tds, &kernel, cell_key, 0, &point);
assert!(
matches!(result, Ok(None)),
"degenerate cell with missing vertex should return Ok(None), got {result:?}"
);
}
#[test]
fn test_find_conflict_region_vanished_neighbor_returns_cell_data_access_failed() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let cell_key = tds
.insert_cell_with_mapping(Cell::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
let ghost = CellKey::from(KeyData::from_ffi(777_777));
{
let cell = tds.get_cell_by_key_mut(cell_key).unwrap();
let buf = cell.ensure_neighbors_buffer_mut();
buf[0] = Some(ghost);
}
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.2, 0.2]);
let result = find_conflict_region(&tds, &kernel, &point, cell_key);
assert!(
matches!(
result,
Err(ConflictError::CellDataAccessFailed { cell_key: ck, .. }) if ck == ghost
),
"expected CellDataAccessFailed for vanished neighbor, got {result:?}"
);
}
#[test]
fn test_find_conflict_region_underdimensioned_cell_returns_cell_data_access_failed() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let cell_key = tds
.insert_cell_with_mapping(Cell::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
{
let cell = tds.get_cell_by_key_mut(cell_key).unwrap();
cell.clear_vertex_keys();
cell.push_vertex_key(v0);
cell.push_vertex_key(v1);
}
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.3, 0.3]);
let result = find_conflict_region(&tds, &kernel, &point, cell_key);
assert!(
matches!(
result,
Err(ConflictError::CellDataAccessFailed { cell_key: ck, .. }) if ck == cell_key
),
"expected CellDataAccessFailed for underdimensioned cell, got {result:?}"
);
}
#[test]
fn test_find_conflict_region_degenerate_cell_returns_cell_data_access_failed() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let cell_key = tds
.insert_cell_with_mapping(Cell::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
let existing_vertices = tds.get_cell(cell_key).unwrap().vertices().to_vec();
let missing = VertexKey::from(KeyData::from_ffi(999_999));
{
let cell = tds.get_cell_by_key_mut(cell_key).unwrap();
cell.clear_vertex_keys();
cell.push_vertex_key(existing_vertices[0]);
cell.push_vertex_key(existing_vertices[1]);
cell.push_vertex_key(missing);
}
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.3_f64, 0.3_f64]);
let result = find_conflict_region(&tds, &kernel, &point, cell_key);
assert!(
matches!(result, Err(ConflictError::CellDataAccessFailed { cell_key: ck, .. }) if ck == cell_key),
"expected CellDataAccessFailed for degenerate cell, got {result:?}"
);
}
#[test]
fn test_locate_with_stats_none_hint_picks_arbitrary_start_cell() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.25_f64, 0.25_f64]);
let (result, stats) = locate_with_stats(dt.tds(), &kernel, &point, None).unwrap();
assert!(matches!(result, LocateResult::InsideCell(_)));
assert!(!stats.used_hint, "None hint should set used_hint = false");
assert!(!stats.fell_back_to_scan());
}
#[cfg(debug_assertions)]
macro_rules! test_verify_conflict_region_complete_dimension {
($dim:literal, $inside_point:expr, $($coords:expr),+ $(,)?) => {{
let vertices: Vec<_> = vec![
$(vertex!($coords)),+
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let start_cell = dt.tds().cell_keys().next().unwrap();
let point = Point::new($inside_point);
let conflict_cells = find_conflict_region(dt.tds(), &kernel, &point, start_cell).unwrap();
assert!(
!conflict_cells.is_empty(),
"BFS should find at least 1 conflict cell for interior point in {}D",
$dim
);
let missed = verify_conflict_region_completeness(
dt.tds(),
&kernel,
&point,
&conflict_cells,
);
assert_eq!(
missed, 0,
"BFS conflict region should be complete for interior point in {}D",
$dim
);
}};
}
#[cfg(debug_assertions)]
#[test]
fn test_verify_conflict_region_completeness_2d() {
test_verify_conflict_region_complete_dimension!(
2,
[0.3, 0.3],
[0.0, 0.0],
[1.0, 0.0],
[0.0, 1.0],
);
}
#[cfg(debug_assertions)]
#[test]
fn test_verify_conflict_region_completeness_3d() {
test_verify_conflict_region_complete_dimension!(
3,
[0.25, 0.25, 0.25],
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
);
}
#[cfg(debug_assertions)]
#[test]
fn test_verify_conflict_region_completeness_4d() {
test_verify_conflict_region_complete_dimension!(
4,
[0.2, 0.2, 0.2, 0.2],
[0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
);
}
#[cfg(debug_assertions)]
#[test]
fn test_verify_conflict_region_completeness_5d() {
test_verify_conflict_region_complete_dimension!(
5,
[0.15, 0.15, 0.15, 0.15, 0.15],
[0.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
);
}
#[cfg(debug_assertions)]
#[test]
fn test_verify_conflict_region_completeness_empty_bfs_detects_missed() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let point = Point::new([0.3, 0.3]);
let empty_bfs = CellKeyBuffer::new();
let missed = verify_conflict_region_completeness(dt.tds(), &kernel, &point, &empty_bfs);
assert!(
missed > 0,
"Empty BFS result should detect missed conflict cells"
);
}
#[cfg(debug_assertions)]
#[test]
fn test_verify_conflict_region_completeness_outside_point_zero_missed() {
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 dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let start_cell = dt.tds().cell_keys().next().unwrap();
let point = Point::new([10.0, 10.0, 10.0]);
let conflict_cells = find_conflict_region(dt.tds(), &kernel, &point, start_cell).unwrap();
assert!(conflict_cells.is_empty());
let missed =
verify_conflict_region_completeness(dt.tds(), &kernel, &point, &conflict_cells);
assert_eq!(missed, 0, "Outside point should produce zero missed cells");
}
#[cfg(debug_assertions)]
#[test]
fn test_verify_conflict_region_completeness_truncated_multi_cell_detects_missed() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([4.0, 0.0]),
vertex!([4.0, 3.0]),
vertex!([0.0, 3.0]),
];
let dt = DelaunayTriangulation::new(&vertices).unwrap();
let kernel = FastKernel::<f64>::new();
let start_cell = dt.tds().cell_keys().next().unwrap();
let point = Point::new([2.0, 1.5]);
let full_conflict = find_conflict_region(dt.tds(), &kernel, &point, start_cell).unwrap();
assert!(
full_conflict.len() >= 2,
"Expected ≥2 conflict cells for center query in 2-triangle mesh, got {}",
full_conflict.len()
);
let mut truncated = CellKeyBuffer::new();
truncated.push(full_conflict[0]);
let missed = verify_conflict_region_completeness(dt.tds(), &kernel, &point, &truncated);
assert!(
missed >= 1,
"Truncated BFS should detect at least 1 missed conflict cell, got {missed}"
);
}
#[test]
fn test_extract_cavity_boundary_rejects_ridge_fan_2d() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let center = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let axis_x = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let axis_y = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let axis_neg_x = tds
.insert_vertex_with_mapping(vertex!([-1.0, 0.0]))
.unwrap();
let axis_neg_y = tds
.insert_vertex_with_mapping(vertex!([0.0, -1.0]))
.unwrap();
let far_x = tds.insert_vertex_with_mapping(vertex!([2.0, 0.0])).unwrap();
let far_y = tds.insert_vertex_with_mapping(vertex!([0.0, 2.0])).unwrap();
let first_cell = tds
.insert_cell_with_mapping(Cell::new(vec![center, axis_x, axis_y], None).unwrap())
.unwrap();
let second_cell = tds
.insert_cell_with_mapping(
Cell::new(vec![center, axis_neg_x, axis_neg_y], None).unwrap(),
)
.unwrap();
let third_cell = tds
.insert_cell_with_mapping(Cell::new(vec![center, far_x, far_y], None).unwrap())
.unwrap();
let mut conflict_cells = CellKeyBuffer::new();
conflict_cells.push(first_cell);
conflict_cells.push(second_cell);
conflict_cells.push(third_cell);
match extract_cavity_boundary(&tds, &conflict_cells).unwrap_err() {
ConflictError::RidgeFan {
facet_count,
ridge_vertex_count,
extra_cells,
} => {
assert!(facet_count >= 3);
assert_eq!(ridge_vertex_count, 1);
assert!(
!extra_cells.is_empty() && extra_cells.len() <= facet_count - 2,
"deduped extra_cells should be non-empty and not exceed facet_count - 2; got {} vs {}",
extra_cells.len(),
facet_count - 2
);
let mut seen = FastHashSet::default();
for ck in &extra_cells {
assert!(
tds.contains_cell(*ck),
"extra cell key {ck:?} should be present in the TDS"
);
assert!(seen.insert(*ck), "duplicate key {ck:?} in extra_cells");
}
}
other => panic!("Expected RidgeFan, got {other:?}"),
}
}
}