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>
This commit is contained in:
authored and GitHub committed 2026-10-13 18:37:09 -04:00
1 parent e688f6f934
commit aa68bbbcdf
24 files changed
+2876 -56

No files matched your search

+414
View File
@@ -0,0 +1,414 @@
//! Facet recovery (Diazzi et al. 2023, §3.4 and §4.4). Every input triangle is a facet; once all
//! segments are edges, a facet is *missing* when an edge of the tetrahedralization pierces its
//! interior. The tetrahedra with such edges form face-connected *cavities* (each meets the
//! facet's plane inside the facet only, because the facet's edges are mesh edges). Each cavity is
//! deleted and split by the facet's plane into two half-cavities, which are refilled one after
//! the other by the modified gift-wrapping of §4.4 (after Shewchuk 2000, *Sweep algorithms for
//! constructing higher-dimensional constrained Delaunay triangulations*): repeatedly take an
//! open triangle `σ` of the half-cavity's front and join it to the vertex `w` of the half-cavity
//! such that
//! - (i) `w` is on the open side of `σ` (positive volume),
//! - (ii) the tetrahedron meets every cavity, front and constraint triangle properly
//! ([`super::meet`]), and contains no other cavity vertex,
//! - (iii) among those, the perturbed circumsphere of `σ w` contains no other candidate: the
//! candidate that is first in the pencil of spheres through `σ`, with the same symbolic
//! perturbation as the Delaunay tetrahedralization, so both half-cavities induce the same
//! triangulation on the facet.
//!
//! The first half-cavity leaves its triangles on the facet's plane open; the second closes them.
//! Constraint triangles already recovered that lie inside a cavity stay constraints: both of
//! their sides enter the front.
//!
//! Si's cavity expansion with local Delaunay tetrahedralizations (§3.4) is a speed-up the paper
//! uses before falling back to gift-wrapping; gift-wrapping alone is the always-correct path
//! (§4.4: it builds the constrained Delaunay tetrahedralization of any half-cavity that admits
//! one), so it is the one implemented here.
use std::collections::{HashMap, HashSet};
use crate::predicates::{GenericPoint, Sign, generic_orient3d};
use crate::{Error, Result};
use super::Recovery;
use super::complex::{NONE, corrupt, face_toward, sorted3};
use super::meet::{in_closed_tet, meet_properly};
/// Most rounds of finding and refilling the cavities of one facet (one suffices in exact
/// arithmetic).
const MAX_ROUNDS_PER_FACET: usize = 16;
/// A box: `(lo, hi)`.
type Bounds = ([f64; 3], [f64; 3]);
/// The open front of a half-cavity: faces, their boxes, and whether each is still open.
type Front<'a> = (&'a [[u32; 3]], &'a [Bounds], &'a [bool]);
/// An oriented triangle rotated so its smallest id comes first: the key of an oriented face.
fn canon([a, b, c]: [u32; 3]) -> [u32; 3] {
if a <= b && a <= c {
[a, b, c]
} else if b <= a && b <= c {
[b, c, a]
} else {
[c, a, b]
}
}
fn boxes_meet((l1, h1): Bounds, (l2, h2): Bounds) -> bool {
(0..3).all(|k| l1[k] <= h2[k] && l2[k] <= h1[k])
}
/// The side of the facet's plane each vertex is on, cached for one facet.
struct Plane {
corners: [GenericPoint; 3],
side: HashMap<u32, Sign>,
}
impl Recovery {
fn plane(&self, f: usize) -> Result<Plane> {
let tri = *self.triangles.get(f).ok_or_else(|| corrupt("facet id"))?;
let corner = |v: u32| self.cx.pt(v).cloned();
Ok(Plane { corners: [corner(tri[0])?, corner(tri[1])?, corner(tri[2])?], side: HashMap::new() })
}
fn facet_side(&self, plane: &mut Plane, v: u32) -> Result<Sign> {
if let Some(&s) = plane.side.get(&v) {
return Ok(s);
}
let [a, b, c] = &plane.corners;
// tet4 volume sign of (a, b, c, v): positive on the side the facet's normal points to.
let s = -generic_orient3d(a, b, c, self.cx.pt(v)?);
plane.side.insert(v, s);
Ok(s)
}
/// Whether edge `p q` crosses the facet's open interior.
fn pierces(&self, plane: &mut Plane, p: u32, q: u32) -> Result<bool> {
if self.facet_side(plane, p)? * self.facet_side(plane, q)? != Sign::Negative {
return Ok(false);
}
let [a, b, c] = &plane.corners;
let (pp, pq) = (self.cx.pt(p)?, self.cx.pt(q)?);
let s0 = generic_orient3d(pp, pq, a, b);
if s0.is_zero() {
return Ok(false);
}
Ok(generic_orient3d(pp, pq, b, c) == s0 && generic_orient3d(pp, pq, c, a) == s0)
}
fn crosses(&self, plane: &mut Plane, t: u32) -> Result<bool> {
let vs = self.cx.tet(t)?;
for (i, j) in [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] {
if let (Some(&p), Some(&q)) = (vs.get(i), vs.get(j))
&& self.pierces(plane, p, q)?
{
return Ok(true);
}
}
Ok(false)
}
/// The vertices on facet `f`'s boundary: its corners and the Steiner points on its edges.
pub(super) fn facet_ring(&self, f: usize) -> Result<Vec<u32>> {
let tri = *self.triangles.get(f).ok_or_else(|| corrupt("facet id"))?;
let mut ring = Vec::new();
for k in 0..3 {
let (a, b) = (tri[k], tri[(k + 1) % 3]);
let s = *self.edge_segment.get(&(a.min(b), a.max(b))).ok_or_else(|| corrupt("facet edge without a segment"))?;
let chain = self.segment(s)?.chain_from(a);
ring.extend(chain.iter().take(chain.len().saturating_sub(1)));
}
Ok(ring)
}
/// The face-connected sets of tetrahedra crossing facet `f`'s interior. Each contains a
/// tetrahedron incident to a vertex of the facet's boundary `ring`.
fn crossing_cavities(&mut self, plane: &mut Plane, ring: &[u32]) -> Result<Vec<Vec<u32>>> {
let mut cavities = Vec::new();
let mut seen: HashSet<u32> = HashSet::new();
let mut star = Vec::new();
for &v in ring {
self.cx.star(v, &mut star)?;
for &t in &star {
if seen.contains(&t) || !self.crosses(plane, t)? {
continue;
}
seen.insert(t);
let mut cavity = vec![t];
let mut i = 0;
while let Some(&c) = cavity.get(i) {
i += 1;
for n in self.cx.adj(c)? {
if n != NONE && !seen.contains(&n) && self.crosses(plane, n)? {
seen.insert(n);
cavity.push(n);
}
}
}
cavities.push(cavity);
}
}
Ok(cavities)
}
/// Whether facet `f` is a union of mesh triangles: no tetrahedron crosses it.
pub(super) fn facet_present(&mut self, f: usize) -> Result<bool> {
let ring = self.facet_ring(f)?;
let mut plane = self.plane(f)?;
Ok(self.crossing_cavities(&mut plane, &ring)?.is_empty())
}
/// Recover facet `f`, then register its triangles as constraints. Filling one cavity leaves
/// the others untouched (they share no tetrahedron), so one scan finds them all; the scan is
/// repeated until it finds none.
pub(super) fn recover_facet(&mut self, f: usize) -> Result<()> {
let ring = self.facet_ring(f)?;
let mut plane = self.plane(f)?;
for _ in 0..MAX_ROUNDS_PER_FACET {
let cavities = self.crossing_cavities(&mut plane, &ring)?;
if cavities.is_empty() {
return self.register_facet(f, &ring);
}
for cavity in cavities {
self.stats.cavities += 1;
self.fill_cavity(f, &mut plane, &cavity)?;
}
}
Err(Error::Limit(format!("facet {f} was still crossed after {MAX_ROUNDS_PER_FACET} rounds of cavity filling")))
}
/// Record the mesh triangles that make up facet `f` (all three vertices on its boundary).
fn register_facet(&mut self, f: usize, ring: &[u32]) -> Result<()> {
let on: HashSet<u32> = ring.iter().copied().collect();
let mut star = Vec::new();
for &v in ring {
self.cx.star(v, &mut star)?;
for &t in &star {
let vs = self.cx.tet(t)?;
for k in 0..4 {
let face = face_toward(vs, k);
if face.contains(&v) && face.iter().all(|u| on.contains(u)) {
let key = sorted3(face);
match self.constraint.insert(key, f as u32) {
Some(g) if g != f as u32 => {
return Err(Error::Failed(format!("input triangles {g} and {f} overlap")));
}
_ => {}
}
}
}
}
}
Ok(())
}
/// Delete the cavity and refill its two halves by gift-wrapping.
fn fill_cavity(&mut self, f: usize, plane: &mut Plane, cavity: &[u32]) -> Result<()> {
let inside: HashSet<u32> = cavity.iter().copied().collect();
// Per half: the boundary faces (cavity on their positive side) and the constraint faces
// inside it, and the other sides of those constraints.
let (mut fixed_up, mut fixed_down) = (Vec::new(), Vec::new());
let (mut back_up, mut back_down) = (Vec::new(), Vec::new());
let mut vertices: Vec<u32> = Vec::new();
for &t in cavity {
let vs = self.cx.tet(t)?;
vertices.extend(vs);
for (j, n) in self.cx.adj(t)?.into_iter().enumerate() {
let face = face_toward(vs, j);
let interior = n != NONE && inside.contains(&n);
if interior && (n < t || !self.constraint.contains_key(&sorted3(face))) {
continue;
}
let mut pos = false;
let mut neg = false;
for v in face {
match self.facet_side(plane, v)? {
Sign::Positive => pos = true,
Sign::Negative => neg = true,
Sign::Zero => {}
}
}
let (fixed, back) = match (pos, neg) {
(true, false) => (&mut fixed_up, &mut back_up),
(false, true) => (&mut fixed_down, &mut back_down),
_ => {
let at = self.cx.approx(face[0])?;
return Err(Error::Failed(format!("facet {f}: a cavity face near {at:?} is not on one side of the facet's plane")));
}
};
fixed.push(face);
if interior {
// A recovered constraint inside the cavity: both sides are open.
let [a, b, c] = face;
back.push([a, c, b]);
}
}
}
vertices.sort_unstable();
vertices.dedup();
let mut sides = Vec::with_capacity(vertices.len());
for &v in &vertices {
sides.push(self.facet_side(plane, v)?);
}
let above: Vec<u32> = vertices.iter().zip(&sides).filter(|(_, s)| **s != Sign::Negative).map(|(v, _)| *v).collect();
let below: Vec<u32> = vertices.iter().zip(&sides).filter(|(_, s)| **s != Sign::Positive).map(|(v, _)| *v).collect();
let on_plane = |v: u32| plane.side.get(&v) == Some(&Sign::Zero);
let front_up = fixed_up.iter().chain(&back_up).copied().collect();
let (mut new, open) = self.gift_wrap(f, front_up, &fixed_up, &above, Some(&on_plane))?;
if open.iter().any(|face| !face.iter().all(|&v| on_plane(v))) {
return Err(Error::Failed(format!("facet {f}: the upper half-cavity was left open off the facet")));
}
// The facet's triangles made by the upper half bound the lower one.
fixed_down.extend(open.iter().copied());
let front_down = fixed_down.iter().chain(&back_down).copied().collect();
let (lower, rest) = self.gift_wrap(f, front_down, &fixed_down, &below, None)?;
if !rest.is_empty() {
return Err(Error::Failed(format!("facet {f}: the lower half-cavity did not close")));
}
new.extend(lower);
let used: HashSet<u32> = new.iter().flatten().copied().collect();
if let Some(&lost) = vertices.iter().find(|v| !used.contains(v)) {
let at = self.cx.approx(lost)?;
return Err(Error::Failed(format!("facet {f}: refilling a cavity lost vertex {lost} at {at:?}")));
}
self.stats.gift_wrapped_tets += new.len();
self.cx.replace(cavity, &new)?;
Ok(())
}
/// Fill a half-cavity from its open `front` faces (each with the unfilled region on its
/// positive side), using only `vertices`. `fixed` are the triangles bounding it, which no new
/// tetrahedron may cross. Faces on the facet's plane are left open when `skip` says so. The new
/// tetrahedra and the faces left open.
fn gift_wrap(
&mut self,
f: usize,
front: Vec<[u32; 3]>,
fixed: &[[u32; 3]],
vertices: &[u32],
skip: Option<&dyn Fn(u32) -> bool>,
) -> Result<(Vec<[u32; 4]>, Vec<[u32; 3]>)> {
let mut faces: Vec<[u32; 3]> = Vec::with_capacity(front.len() * 2);
let mut boxes: Vec<([f64; 3], [f64; 3])> = Vec::with_capacity(front.len() * 2);
let mut alive: Vec<bool> = Vec::new();
let mut index: HashMap<[u32; 3], usize> = HashMap::new();
let mut queue: Vec<usize> = Vec::new();
for face in front {
if index.insert(canon(face), faces.len()).is_some() {
return Err(corrupt("a cavity face is listed twice"));
}
queue.push(faces.len());
faces.push(face);
boxes.push(self.cx.bounds_of(&face));
alive.push(true);
}
queue.reverse();
let fixed_boxes: Vec<([u32; 3], Bounds)> = fixed.iter().map(|&t| (t, self.cx.bounds_of(&t))).collect();
let cap = 64 * (faces.len() + vertices.len()) + 64;
let mut tets: Vec<[u32; 4]> = Vec::new();
while let Some(i) = queue.pop() {
if !alive.get(i).copied().unwrap_or(false) {
continue;
}
let sigma = *faces.get(i).ok_or_else(|| corrupt("front index"))?;
if let Some(on) = skip
&& sigma.iter().all(|&v| on(v))
{
continue;
}
let w = self.apex(f, sigma, (&faces, &boxes, &alive), &fixed_boxes, vertices)?;
let tet = [sigma[0], sigma[1], sigma[2], w];
tets.push(tet);
if tets.len() > cap {
return Err(Error::Limit(format!("facet {f}: gift-wrapping a cavity made more than {cap} tetrahedra")));
}
if let Some(a) = alive.get_mut(i) {
*a = false;
}
index.remove(&canon(sigma));
for k in 0..3 {
let g = face_toward(tet, k);
if let Some(j) = index.remove(&canon(g)) {
if let Some(a) = alive.get_mut(j) {
*a = false;
}
continue;
}
let r = [g[0], g[2], g[1]];
if index.insert(canon(r), faces.len()).is_some() {
let at = self.cx.approx(r[0])?;
return Err(Error::Failed(format!("facet {f}: gift-wrapping overlapped itself near {at:?}")));
}
queue.push(faces.len());
faces.push(r);
boxes.push(self.cx.bounds_of(&r));
alive.push(true);
}
}
let open: Vec<[u32; 3]> = faces.iter().zip(&alive).filter(|(_, a)| **a).map(|(f, _)| *f).collect();
Ok((tets, open))
}
/// The apex for front face `sigma`: conditions (i)–(iii) of the module docs.
fn apex(&mut self, f: usize, sigma: [u32; 3], front: Front<'_>, fixed: &[([u32; 3], Bounds)], vertices: &[u32]) -> Result<u32> {
let mut candidates = Vec::new();
for &w in vertices {
if !sigma.contains(&w) && self.cx.volume([sigma[0], sigma[1], sigma[2], w])? == Sign::Positive {
candidates.push(w);
}
}
let mut excluded = vec![false; candidates.len()];
for _ in 0..candidates.len() {
let mut best: Option<(usize, u32)> = None;
for (k, &w) in candidates.iter().enumerate() {
if excluded.get(k).copied().unwrap_or(true) {
continue;
}
best = match best {
None => Some((k, w)),
Some((bk, b)) => {
if self.cx.in_sphere([sigma[0], sigma[1], sigma[2], b], w)? {
Some((k, w))
} else {
Some((bk, b))
}
}
};
}
let Some((k, w)) = best else { break };
if self.valid(sigma, w, front, fixed, vertices)? {
return Ok(w);
}
if let Some(e) = excluded.get_mut(k) {
*e = true;
}
}
let at = self.cx.approx(sigma[0])?;
Err(Error::Failed(format!("facet {f}: gift-wrapping found no valid apex for a cavity face near {at:?}")))
}
/// Condition (ii): the tetrahedron `sigma w` meets every front and fixed triangle properly
/// and contains no other cavity vertex.
fn valid(&self, sigma: [u32; 3], w: u32, (faces, boxes, alive): Front<'_>, fixed: &[([u32; 3], Bounds)], vertices: &[u32]) -> Result<bool> {
let tet = [sigma[0], sigma[1], sigma[2], w];
let bx = self.cx.bounds_of(&tet);
let points = self.cx.points();
for &u in vertices {
if !tet.contains(&u) && boxes_meet(bx, self.cx.bounds(u)) && in_closed_tet(points, tet, u)? {
return Ok(false);
}
}
for ((face, fb), _) in faces.iter().zip(boxes).zip(alive).filter(|(_, a)| **a) {
if boxes_meet(bx, *fb) && !meet_properly(points, &tet, face)? {
return Ok(false);
}
}
for (face, fb) in fixed {
if boxes_meet(bx, *fb) && !meet_properly(points, &tet, face)? {
return Ok(false);
}
}
Ok(true)
}
}