forked from 1b-agent/fieldcraft-upstream
* mesh: exact Bowyer-Watson Delaunay tetrahedralization with ghost tets, BRIO/Hilbert order and a verifier Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * mesh: Delaunay benchmark, hash-paired cavity faces, mesh.delaunay.points debug command Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * mesh: simplify Delaunay helpers, share predicates and parsers, halve verifier work 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>
214 lines
9.9 KiB
Rust
214 lines
9.9 KiB
Rust
//! Independent checks of a triangulation: they re-derive every property from the arrays with
|
||
//! the exact predicates; nothing here relies on how the insertion built them.
|
||
|
||
use super::{DEAD, GHOST, NONE, Triangulation, volume_sign};
|
||
use crate::predicates::{Point3, Sign, insphere, insphere_sos};
|
||
use crate::quality::shape::{det, sub};
|
||
use crate::{Error, Result};
|
||
|
||
fn failed<T>(what: String) -> Result<T> {
|
||
Err(Error::Failed(format!("Delaunay verification: {what}")))
|
||
}
|
||
|
||
/// `det[b − a, c − a, d − a] / 6` in floating point (a measure for the volume balance only).
|
||
pub(super) fn volume(p: [&Point3; 4]) -> f64 {
|
||
let [a, b, c, d] = p.map(Point3::coords);
|
||
det(sub(b, a), sub(c, a), sub(d, a)) / 6.0
|
||
}
|
||
|
||
/// Whether `e` is outside the circumsphere of the positive-volume tetrahedron `[a, b, c, d]`:
|
||
/// never inside exactly, and outside with the perturbation.
|
||
fn outside_sphere([a, b, c, d]: [&Point3; 4], e: &Point3) -> bool {
|
||
// `insphere` wants Shewchuk-positive orientation: swap the first two vertices.
|
||
match insphere(b, a, c, d, e) {
|
||
Sign::Positive => false,
|
||
Sign::Negative => true,
|
||
Sign::Zero => insphere_sos(b, a, c, d, e) == Sign::Negative,
|
||
}
|
||
}
|
||
|
||
/// The face of `vs` opposite slot `k`, sorted.
|
||
pub(super) fn face(vs: [u32; 4], k: usize) -> [u32; 3] {
|
||
let mut f = [NONE; 3];
|
||
for (o, (_, &v)) in f.iter_mut().zip(vs.iter().enumerate().filter(|&(i, _)| i != k)) {
|
||
*o = v;
|
||
}
|
||
f.sort_unstable();
|
||
f
|
||
}
|
||
|
||
impl Triangulation {
|
||
fn point_of(&self, v: u32) -> Result<&Point3> {
|
||
match self.points.get(v as usize) {
|
||
Some(p) => Ok(p),
|
||
None => failed(format!("vertex {v} does not exist")),
|
||
}
|
||
}
|
||
|
||
fn points_of(&self, [a, b, c, d]: [u32; 4]) -> Result<[&Point3; 4]> {
|
||
Ok([self.point_of(a)?, self.point_of(b)?, self.point_of(c)?, self.point_of(d)?])
|
||
}
|
||
|
||
fn points_with<'a>(&'a self, vs: [u32; 4], k: usize, p: &'a Point3) -> Result<[&'a Point3; 4]> {
|
||
let mut out = [p; 4];
|
||
for (i, o) in out.iter_mut().enumerate() {
|
||
if i != k {
|
||
*o = self.point_of(vs.get(i).copied().unwrap_or(NONE))?;
|
||
}
|
||
}
|
||
Ok(out)
|
||
}
|
||
|
||
/// The vertex of `o` across its shared face with `t`, and the slot of that face in `o`.
|
||
fn across(&self, t: u32, o: u32) -> Result<([u32; 4], usize)> {
|
||
let Some(ws) = self.tet(o) else { return failed(format!("tetrahedron {t} has a deleted neighbour {o}")) };
|
||
let adj = self.adj.get(o as usize).copied().unwrap_or([NONE; 4]);
|
||
match (adj.iter().position(|&n| n == t), adj.iter().rposition(|&n| n == t)) {
|
||
(Some(k), Some(l)) if k == l => Ok((ws, k)),
|
||
_ => failed(format!("tetrahedra {t} and {o} are not mutual neighbours exactly once")),
|
||
}
|
||
}
|
||
|
||
/// Check the triangulation: adjacency is mutual and matches shared faces, every finite
|
||
/// tetrahedron has positive volume, the hull is convex, every interior face is locally
|
||
/// Delaunay (the opposite vertex is not inside the circumsphere, exactly, and is outside
|
||
/// with the perturbation), and every vertex is in a tetrahedron. As a floating-point sanity
|
||
/// check against a hull covered twice, the finite volume must match the volume the hull faces
|
||
/// enclose to 1e-9. By Delaunay's lemma, local Delaunay faces in a
|
||
/// triangulation of the hull make every circumsphere empty. `O(tetrahedra)`.
|
||
pub fn verify(&self) -> Result<()> {
|
||
let n = self.points.len();
|
||
let mut seen = vec![false; n];
|
||
let (mut finite_volume, mut hull_volume, mut scale) = (0.0f64, 0.0f64, 0.0f64);
|
||
let reference = self.tets.iter().flatten().copied().find(|&v| v < DEAD);
|
||
for (t, &vs) in self.tets.iter().enumerate() {
|
||
if vs[0] == DEAD {
|
||
continue;
|
||
}
|
||
let t = t as u32;
|
||
let adj = self.adj.get(t as usize).copied().unwrap_or([NONE; 4]);
|
||
for (i, &v) in vs.iter().enumerate() {
|
||
if vs.iter().skip(i + 1).any(|&w| w == v) {
|
||
return failed(format!("tetrahedron {t} repeats vertex {v}"));
|
||
}
|
||
if v == GHOST {
|
||
continue;
|
||
}
|
||
if self.duplicate_of(v).is_some() {
|
||
return failed(format!("duplicate point {v} is a vertex"));
|
||
}
|
||
match seen.get_mut(v as usize) {
|
||
Some(s) => *s = true,
|
||
None => return failed(format!("tetrahedron {t} has vertex {v}, which does not exist")),
|
||
}
|
||
}
|
||
// Per face: the neighbour's vertices and the slot of the shared face in it.
|
||
let mut across = [([NONE; 4], 0); 4];
|
||
for (j, (&o, slot)) in adj.iter().zip(&mut across).enumerate() {
|
||
if o == t {
|
||
return failed(format!("tetrahedron {t} is its own neighbour"));
|
||
}
|
||
let (ws, k) = self.across(t, o)?;
|
||
if face(vs, j) != face(ws, k) {
|
||
return failed(format!("tetrahedra {t} and {o} are neighbours without sharing a face"));
|
||
}
|
||
*slot = (ws, k);
|
||
}
|
||
let apex = |j: usize| across.get(j).and_then(|&(ws, k)| ws.get(k).copied()).unwrap_or(NONE);
|
||
match vs.iter().position(|&v| v == GHOST) {
|
||
None => {
|
||
let p = self.points_of(vs)?;
|
||
if volume_sign(p) != Sign::Positive {
|
||
return failed(format!("tetrahedron {t} is flat or inverted"));
|
||
}
|
||
let vol = volume(p);
|
||
finite_volume += vol;
|
||
scale += vol.abs();
|
||
// The test is symmetric in the two tetrahedra: check each face once.
|
||
for (j, &o) in adj.iter().enumerate() {
|
||
let u = apex(j);
|
||
if u != GHOST && t < o && !outside_sphere(p, self.point_of(u)?) {
|
||
return failed(format!("vertex {u} is inside the circumsphere of tetrahedron {t}"));
|
||
}
|
||
}
|
||
}
|
||
Some(g) => {
|
||
let fs = across.get(g).map(|&(ws, _)| ws).unwrap_or([NONE; 4]);
|
||
if fs.contains(&GHOST) {
|
||
return failed(format!("hull face of {t} borders another ghost"));
|
||
}
|
||
let inner = self.point_of(apex(g))?;
|
||
if volume_sign(self.points_with(vs, g, inner)?) != Sign::Negative {
|
||
return failed(format!("ghost tetrahedron {t} faces into the hull"));
|
||
}
|
||
for j in (0..4).filter(|&j| j != g) {
|
||
let apex = self.point_of(apex(j))?;
|
||
match volume_sign(self.points_with(vs, g, apex)?) {
|
||
Sign::Positive => return failed(format!("the hull is not convex at ghost tetrahedron {t}")),
|
||
Sign::Negative => {}
|
||
Sign::Zero => {
|
||
// Coplanar hull faces: the perturbed decision of the finite
|
||
// tetrahedron under this face.
|
||
if !outside_sphere(self.points_of(fs)?, apex) {
|
||
return failed(format!("coplanar hull faces at ghost tetrahedron {t} are not perturbed-Delaunay"));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if let Some(r) = reference {
|
||
hull_volume -= volume(self.points_with(vs, g, self.point_of(r)?)?);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if self.dimension() == 3 {
|
||
for (v, s) in seen.iter().enumerate() {
|
||
let v = v as u32;
|
||
let triangulated = self.duplicate_of(v).is_none();
|
||
if *s != triangulated {
|
||
return failed(format!("vertex {v} is {}in a tetrahedron", if *s { "" } else { "not " }));
|
||
}
|
||
if triangulated && !self.incident_tet(v).and_then(|t| self.tet(t)).is_some_and(|vs| vs.contains(&v)) {
|
||
return failed(format!("vertex {v} has no valid incident tetrahedron"));
|
||
}
|
||
}
|
||
} else if self.tets.iter().any(|vs| vs[0] != DEAD) {
|
||
return failed("a lower-dimensional triangulation has tetrahedra".into());
|
||
}
|
||
if (finite_volume - hull_volume).abs() > 1e-9 * scale {
|
||
return failed(format!("the tetrahedra fill {finite_volume}, the hull encloses {hull_volume}"));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Brute force: no triangulated vertex is inside any circumsphere (exactly) or the perturbed
|
||
/// circumsphere, and none is beyond a hull face. `O(points × tetrahedra)`; for tests.
|
||
pub fn verify_empty_spheres(&self) -> Result<()> {
|
||
let vertices: Vec<u32> = (0..self.points.len() as u32).filter(|&v| self.duplicate_of(v).is_none()).collect();
|
||
for (t, &vs) in self.tets.iter().enumerate() {
|
||
if vs[0] == DEAD {
|
||
continue;
|
||
}
|
||
for &u in &vertices {
|
||
if vs.contains(&u) {
|
||
continue;
|
||
}
|
||
let e = self.point_of(u)?;
|
||
match vs.iter().position(|&v| v == GHOST) {
|
||
None => {
|
||
if !outside_sphere(self.points_of(vs)?, e) {
|
||
return failed(format!("vertex {u} is inside the circumsphere of tetrahedron {t}"));
|
||
}
|
||
}
|
||
Some(g) => {
|
||
if volume_sign(self.points_with(vs, g, e)?) == Sign::Positive {
|
||
return failed(format!("vertex {u} is beyond the hull face of ghost tetrahedron {t}"));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|