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

+50 -2
View File
@@ -20,6 +20,7 @@
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`).
@@ -50,7 +51,14 @@ pub fn insphere_sos(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3)
#[cold]
fn insphere_perturbed(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3) -> Sign {
for i in ranked(&[a.0, b.0, c.0, d.0, e.0]) {
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],
@@ -58,7 +66,7 @@ fn insphere_perturbed(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3
3 => [a, b, c, e],
_ => [a, b, c, d],
};
let minor = orient3d(p, q, r, s);
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 };
@@ -67,6 +75,46 @@ fn insphere_perturbed(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3
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]