Files
OneCAD-Simulation/crates/mesh/src/predicates/sos.rs
T
aa68bbbcdf Constrained Delaunay tetrahedralization (boundary recovery) and mesh.tet.generate (M2) (#29)
* predicates: LNC points, generic insphere and its perturbed form (Diazzi et al. 2023 §4.3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mesh: constrained Delaunay tetrahedralization (segment recovery with LNC Steiner points, gift-wrapping facet recovery, labelling)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* cdt: rounding repair, mesh.tet.generate, corpus CDT column; review cleanups (shared perturbation rule, one facet scan, cached coordinates, worklist segment recovery)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mesh.tet.generate is ready in the catalogue; corpus table with the CDT column

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:37:09 -04:00

143 lines
6.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Symbolic perturbation of the in-sphere and in-circle tests (Devillers & Teillaud 2011, after
//! Edelsbrunner & Mücke's Simulation of Simplicity).
//!
//! `insphere(a, b, c, d, e)` is the 5×5 determinant with rows `[p, |p|², 1]` (translate by `e`
//! and expand along the last column to see it). Lift every point by an infinitesimal
//! `|p|² + ε^k(p)`, where `k(p)` is the rank of `p` in decreasing lexicographic order
//! (`(x, y, z)`; the largest point gets the largest perturbation `ε¹`). The determinant is linear
//! in the lift column, so the perturbed value is
//!
//! `insphere + Σ_p ε^k(p) · C_p`, `C_p = (-1)^(row(p) + 4) · orient3d(the other four points)`
//!
//! (the cofactor of `p`'s lift is a determinant with rows `[q, 1]`, which is `orient3d` of the
//! other points in their order). Its sign is the sign of the first nonzero term in order of
//! decreasing rank. Lifting by these weights is a regular triangulation of the same points, so
//! the decisions are consistent across calls, and a Bowyer–Watson cavity stays a star-shaped
//! ball. When `a, b, c, d` is a proper tetrahedron the term of `e` (`-orient3d(a, b, c, d)`) is
//! nonzero, so the result is never zero.
//!
//! The 2D version replaces `orient3d` by `orient2d` and the 5×5 by the 4×4 determinant.
use std::cmp::Ordering;
use super::indirect::{Axis, GenericPoint, generic_compare, generic_insphere, generic_orient3d};
use super::{Point2, Point3, Sign, incircle, insphere, orient2d, orient3d};
/// Lexicographic order on coordinates (total: `f64::total_cmp`).
fn lex<const N: usize>(p: &[f64; N], q: &[f64; N]) -> Ordering {
p.iter().zip(q).map(|(a, b)| a.total_cmp(b)).find(|o| o.is_ne()).unwrap_or(Ordering::Equal)
}
/// Argument positions sorted by decreasing lexicographic order of their points, ties (equal
/// points, which a triangulation never passes) by position.
fn ranked<const N: usize, const M: usize>(points: &[[f64; N]; M]) -> [usize; M] {
let mut order: [usize; M] = std::array::from_fn(|i| i);
order.sort_by(|&i, &j| match (points.get(i), points.get(j)) {
(Some(p), Some(q)) => lex(q, p).then(i.cmp(&j)),
_ => i.cmp(&j),
});
order
}
/// [`insphere`] with symbolic perturbation: never [`Sign::Zero`] when `a, b, c, d` is not
/// coplanar. Points must be pairwise distinct for the perturbation to be consistent (a
/// Delaunay triangulation never passes duplicates); with all five points coplanar it returns
/// `Zero`.
#[inline]
pub fn insphere_sos(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3) -> Sign {
let exact = insphere(a, b, c, d, e);
if exact.is_zero() { insphere_perturbed(a, b, c, d, e) } else { exact }
}
#[cold]
fn insphere_perturbed(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3) -> Sign {
perturbed_cofactors([a, b, c, d, e], ranked(&[a.0, b.0, c.0, d.0, e.0]), orient3d)
}
/// The sign of the first nonzero lift cofactor, in rank order (`order`: argument positions by
/// decreasing rank). The single statement of the perturbation rule for every point type.
fn perturbed_cofactors<P>(points: [&P; 5], order: [usize; 5], orient: impl Fn(&P, &P, &P, &P) -> Sign) -> Sign {
let [a, b, c, d, e] = points;
for i in order {
let [p, q, r, s] = match i {
0 => [b, c, d, e],
1 => [a, c, d, e],
2 => [a, b, d, e],
3 => [a, b, c, e],
_ => [a, b, c, d],
};
let minor = orient(p, q, r, s);
if !minor.is_zero() {
// Row i + 1 (1-based), lift column 4: cofactor sign (-1)^(i + 5).
return if i % 2 == 1 { minor } else { -minor };
}
}
Sign::Zero
}
/// The lexicographic order of two points' exact coordinates (`x`, then `y`, then `z`): the rank
/// order of the perturbation, for explicit and implicit points alike. On explicit points it is
/// the order [`insphere_sos`] uses (callers admit `+0.0`, never `-0.0`).
pub fn generic_lex(p: &GenericPoint, q: &GenericPoint) -> Ordering {
if let (Some(p), Some(q)) = (p.explicit(), q.explicit()) {
return lex(&p.coords(), &q.coords());
}
Axis::ALL.into_iter().map(|a| generic_compare(p, q, a)).find(|o| o.is_ne()).unwrap_or(Ordering::Equal)
}
/// [`insphere_sos`] on explicit or implicit points: the same lift by lexicographic rank, so a
/// triangulation built with [`insphere_sos`] stays consistent when implicit points are added.
/// Never [`Sign::Zero`] when `a, b, c, d` is not coplanar; points must be pairwise distinct.
pub fn generic_insphere_sos(a: &GenericPoint, b: &GenericPoint, c: &GenericPoint, d: &GenericPoint, e: &GenericPoint) -> Sign {
if let (Some(a), Some(b), Some(c), Some(d), Some(e)) = (a.explicit(), b.explicit(), c.explicit(), d.explicit(), e.explicit()) {
return insphere_sos(a, b, c, d, e);
}
let exact = generic_insphere(a, b, c, d, e);
if !exact.is_zero() {
return exact;
}
let points = [a, b, c, d, e];
// Positions by decreasing rank, ties by position: an insertion sort, which needs no total
// order guarantee from the comparator to stay in bounds.
let mut order = [0usize, 1, 2, 3, 4];
for i in 1..order.len() {
let mut j = i;
while j > 0 {
let (Some(&x), Some(&y)) = (order.get(j - 1), order.get(j)) else { break };
let (Some(px), Some(py)) = (points.get(x), points.get(y)) else { break };
if generic_lex(py, px) != Ordering::Greater {
break;
}
order.swap(j - 1, j);
j -= 1;
}
}
perturbed_cofactors(points, order, generic_orient3d)
}
/// [`incircle`] with symbolic perturbation: never [`Sign::Zero`] when `a, b, c` is not
/// collinear. Same conventions as [`insphere_sos`].
#[inline]
pub fn incircle_sos(a: &Point2, b: &Point2, c: &Point2, d: &Point2) -> Sign {
let exact = incircle(a, b, c, d);
if exact.is_zero() { incircle_perturbed(a, b, c, d) } else { exact }
}
#[cold]
fn incircle_perturbed(a: &Point2, b: &Point2, c: &Point2, d: &Point2) -> Sign {
for i in ranked(&[a.0, b.0, c.0, d.0]) {
let [p, q, r] = match i {
0 => [b, c, d],
1 => [a, c, d],
2 => [a, b, d],
_ => [a, b, c],
};
let minor = orient2d(p, q, r);
if !minor.is_zero() {
// Row i + 1 (1-based), lift column 3: cofactor sign (-1)^(i + 4).
return if i % 2 == 0 { minor } else { -minor };
}
}
Sign::Zero
}