forked from 1b-agent/fieldcraft-upstream
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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
24 files changed
+2876
-56
No files matched your search
@@ -0,0 +1,590 @@
|
||||
//! The working tetrahedralization of the boundary recovery: tetrahedra over explicit and implicit
|
||||
//! ([`Lnc`](crate::predicates::Lnc)) vertices, with the operations the recovery needs: point
|
||||
//! location, Bowyer–Watson insertion of Steiner points, vertex stars, edge lookup, and the
|
||||
//! replacement of a set of tetrahedra by another filling the same region.
|
||||
//!
|
||||
//! Layout: per tetrahedron four vertex ids in tet4 order (`det[b − a, c − a, d − a] > 0`, decided
|
||||
//! exactly) and four neighbour ids, neighbour `i` across the face opposite vertex `i`; [`NONE`] on
|
||||
//! the hull. The hull is the bounding box the recovery adds around the surface, so every Steiner
|
||||
//! point is strictly inside it and no ghost tetrahedra are needed.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{BuildHasherDefault, Hasher};
|
||||
|
||||
use crate::delaunay::Triangulation;
|
||||
use crate::predicates::{GenericPoint, Sign, generic_insphere_sos, generic_lex, generic_orient3d};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A multiplicative hasher for small integer keys (vertex triples). The maps it serves are only
|
||||
/// looked up, never iterated, so its order cannot leak into the output.
|
||||
#[derive(Default)]
|
||||
pub(super) struct IdHasher(u64);
|
||||
|
||||
impl Hasher for IdHasher {
|
||||
fn finish(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
for &b in bytes {
|
||||
self.write_u64(u64::from(b));
|
||||
}
|
||||
}
|
||||
|
||||
fn write_u32(&mut self, x: u32) {
|
||||
self.write_u64(u64::from(x));
|
||||
}
|
||||
|
||||
fn write_u64(&mut self, x: u64) {
|
||||
self.0 = (self.0.rotate_left(5) ^ x).wrapping_mul(0x51_7c_c1_b7_27_22_0a_95);
|
||||
}
|
||||
}
|
||||
|
||||
/// A hash map keyed by vertex ids.
|
||||
pub(super) type IdMap<K, V> = HashMap<K, V, BuildHasherDefault<IdHasher>>;
|
||||
|
||||
/// No tetrahedron / no vertex.
|
||||
pub(super) const NONE: u32 = u32::MAX;
|
||||
/// Marks a recycled tetrahedron slot.
|
||||
const DEAD: u32 = u32::MAX - 1;
|
||||
|
||||
pub(super) fn corrupt(what: &str) -> Error {
|
||||
Error::Failed(format!("constrained tetrahedralization is inconsistent: {what}"))
|
||||
}
|
||||
|
||||
/// The face of `vs` opposite slot `k`, ordered so that vertex `k` is on its positive side
|
||||
/// ([`Complex::volume`] of the face plus that vertex is positive).
|
||||
pub(super) fn face_toward(vs: [u32; 4], k: usize) -> [u32; 3] {
|
||||
let [a, b, c, d] = vs;
|
||||
match k {
|
||||
0 => [b, d, c],
|
||||
1 => [a, c, d],
|
||||
2 => [a, d, b],
|
||||
_ => [a, b, c],
|
||||
}
|
||||
}
|
||||
|
||||
/// A triangle's vertices sorted: the key of an unoriented face.
|
||||
pub(super) fn sorted3([a, b, c]: [u32; 3]) -> [u32; 3] {
|
||||
let mut f = [a, b, c];
|
||||
f.sort_unstable();
|
||||
f
|
||||
}
|
||||
|
||||
/// The working tetrahedralization.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct Complex {
|
||||
points: Vec<GenericPoint>,
|
||||
/// A box certainly containing each point (implicit points are not exactly representable).
|
||||
lo: Vec<[f64; 3]>,
|
||||
hi: Vec<[f64; 3]>,
|
||||
/// Rounded coordinates (floating-point estimates only, never decisions).
|
||||
approx: Vec<[f64; 3]>,
|
||||
tets: Vec<[u32; 4]>,
|
||||
adj: Vec<[u32; 4]>,
|
||||
free: Vec<u32>,
|
||||
vertex_tet: Vec<u32>,
|
||||
/// Traversal marks: a tetrahedron is visited in the current traversal when its mark equals
|
||||
/// `epoch`.
|
||||
mark: Vec<u32>,
|
||||
epoch: u32,
|
||||
rng: u64,
|
||||
/// Most tetrahedron slots allowed.
|
||||
max_tets: usize,
|
||||
live: usize,
|
||||
}
|
||||
|
||||
/// Where a point lies ([`Complex::locate`]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Located {
|
||||
/// In the closed tetrahedron and not one of its vertices.
|
||||
Inside(u32),
|
||||
/// Equal to this vertex.
|
||||
Vertex(u32),
|
||||
}
|
||||
|
||||
impl Complex {
|
||||
/// The finite tetrahedra of a Delaunay triangulation of `points` (in the same vertex order),
|
||||
/// whose hull faces get no neighbour.
|
||||
pub(super) fn from_delaunay(tri: &Triangulation, points: Vec<GenericPoint>, max_tets: usize) -> Result<Self> {
|
||||
let mut id = vec![NONE; tri.slots()];
|
||||
let mut tets = Vec::new();
|
||||
for (t, vs) in tri.finite_tets() {
|
||||
if let Some(slot) = id.get_mut(t as usize) {
|
||||
*slot = tets.len() as u32;
|
||||
}
|
||||
tets.push(vs);
|
||||
}
|
||||
if tets.len() > max_tets {
|
||||
return Err(Error::Limit(format!("{} tetrahedra; the limit is {max_tets}", tets.len())));
|
||||
}
|
||||
let mut adj = Vec::with_capacity(tets.len());
|
||||
for (t, _) in tri.finite_tets() {
|
||||
let ns = tri.neighbors(t).ok_or_else(|| corrupt("Delaunay neighbours"))?;
|
||||
adj.push(ns.map(|n| id.get(n as usize).copied().unwrap_or(NONE)));
|
||||
}
|
||||
let (lo, hi) = points.iter().map(GenericPoint::bounds).unzip();
|
||||
let approx = points.iter().map(GenericPoint::approx).collect();
|
||||
let mut cx = Complex {
|
||||
vertex_tet: vec![NONE; points.len()],
|
||||
points,
|
||||
lo,
|
||||
hi,
|
||||
approx,
|
||||
mark: vec![0; tets.len()],
|
||||
live: tets.len(),
|
||||
tets,
|
||||
adj,
|
||||
free: Vec::new(),
|
||||
epoch: 0,
|
||||
rng: 0x9E37_79B9_7F4A_7C15,
|
||||
max_tets,
|
||||
};
|
||||
for (t, vs) in cx.tets.iter().enumerate() {
|
||||
for &v in vs {
|
||||
if let Some(s) = cx.vertex_tet.get_mut(v as usize) {
|
||||
*s = t as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(cx)
|
||||
}
|
||||
|
||||
// ---- read access ----
|
||||
|
||||
pub(super) fn points(&self) -> &[GenericPoint] {
|
||||
&self.points
|
||||
}
|
||||
|
||||
pub(super) fn pt(&self, v: u32) -> Result<&GenericPoint> {
|
||||
self.points.get(v as usize).ok_or_else(|| corrupt("vertex id"))
|
||||
}
|
||||
|
||||
/// Vertex `v`'s coordinates rounded to doubles: for estimates and messages only.
|
||||
pub(super) fn approx(&self, v: u32) -> Result<[f64; 3]> {
|
||||
self.approx.get(v as usize).copied().ok_or_else(|| corrupt("vertex id"))
|
||||
}
|
||||
|
||||
/// The box certainly containing vertex `v`.
|
||||
pub(super) fn bounds(&self, v: u32) -> ([f64; 3], [f64; 3]) {
|
||||
match (self.lo.get(v as usize), self.hi.get(v as usize)) {
|
||||
(Some(&l), Some(&h)) => (l, h),
|
||||
_ => ([f64::NEG_INFINITY; 3], [f64::INFINITY; 3]),
|
||||
}
|
||||
}
|
||||
|
||||
/// The box certainly containing all of `vs`.
|
||||
pub(super) fn bounds_of(&self, vs: &[u32]) -> ([f64; 3], [f64; 3]) {
|
||||
let mut lo = [f64::INFINITY; 3];
|
||||
let mut hi = [f64::NEG_INFINITY; 3];
|
||||
for &v in vs {
|
||||
let (l, h) = self.bounds(v);
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(l[k]);
|
||||
hi[k] = hi[k].max(h[k]);
|
||||
}
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
pub(super) fn slots(&self) -> usize {
|
||||
self.tets.len()
|
||||
}
|
||||
|
||||
pub(super) fn live_count(&self) -> usize {
|
||||
self.live
|
||||
}
|
||||
|
||||
/// The vertices of a live tetrahedron.
|
||||
pub(super) fn tet(&self, t: u32) -> Result<[u32; 4]> {
|
||||
self.tets.get(t as usize).copied().filter(|v| v[0] != DEAD).ok_or_else(|| corrupt("tetrahedron id"))
|
||||
}
|
||||
|
||||
pub(super) fn is_live(&self, t: u32) -> bool {
|
||||
self.tets.get(t as usize).is_some_and(|v| v[0] != DEAD)
|
||||
}
|
||||
|
||||
pub(super) fn adj(&self, t: u32) -> Result<[u32; 4]> {
|
||||
self.tet(t)?;
|
||||
self.adj.get(t as usize).copied().ok_or_else(|| corrupt("tetrahedron id"))
|
||||
}
|
||||
|
||||
/// The live tetrahedra in slot order.
|
||||
pub(super) fn live_tets(&self) -> impl Iterator<Item = (u32, [u32; 4])> + '_ {
|
||||
self.tets.iter().enumerate().filter(|(_, v)| v[0] != DEAD).map(|(t, v)| (t as u32, *v))
|
||||
}
|
||||
|
||||
/// The sign of `det[b − a, c − a, d − a]` (tet4 orientation), exact.
|
||||
pub(super) fn volume(&self, [a, b, c, d]: [u32; 4]) -> Result<Sign> {
|
||||
Ok(-generic_orient3d(self.pt(a)?, self.pt(b)?, self.pt(c)?, self.pt(d)?))
|
||||
}
|
||||
|
||||
/// Whether `x` is inside the perturbed circumsphere of the positive tetrahedron `vs`.
|
||||
pub(super) fn in_sphere(&self, [a, b, c, d]: [u32; 4], x: u32) -> Result<bool> {
|
||||
// `insphere` wants Shewchuk-positive orientation (tet4-negative): swap the first two.
|
||||
Ok(generic_insphere_sos(self.pt(b)?, self.pt(a)?, self.pt(c)?, self.pt(d)?, self.pt(x)?) == Sign::Positive)
|
||||
}
|
||||
|
||||
/// Whether two vertices are the same point (exact).
|
||||
fn same_point(&self, u: u32, v: u32) -> Result<bool> {
|
||||
Ok(generic_lex(self.pt(u)?, self.pt(v)?).is_eq())
|
||||
}
|
||||
|
||||
// ---- traversal marks ----
|
||||
|
||||
/// A new traversal: marks `epoch` (visited) and `epoch + 1` (rejected) are fresh.
|
||||
fn next_epoch(&mut self) {
|
||||
if self.epoch >= u32::MAX - 4 {
|
||||
self.mark.fill(0);
|
||||
self.epoch = 0;
|
||||
}
|
||||
self.epoch += 2;
|
||||
}
|
||||
|
||||
fn visited(&self, t: u32) -> bool {
|
||||
self.mark.get(t as usize) == Some(&self.epoch)
|
||||
}
|
||||
|
||||
fn visit(&mut self, t: u32) {
|
||||
if let Some(m) = self.mark.get_mut(t as usize) {
|
||||
*m = self.epoch;
|
||||
}
|
||||
}
|
||||
|
||||
fn rejected(&self, t: u32) -> bool {
|
||||
self.mark.get(t as usize) == Some(&(self.epoch + 1))
|
||||
}
|
||||
|
||||
fn reject(&mut self, t: u32) {
|
||||
if let Some(m) = self.mark.get_mut(t as usize) {
|
||||
*m = self.epoch + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- stars and edges ----
|
||||
|
||||
/// A live tetrahedron incident to `v`.
|
||||
fn incident(&self, v: u32) -> Result<u32> {
|
||||
let t = self.vertex_tet.get(v as usize).copied().unwrap_or(NONE);
|
||||
if self.tets.get(t as usize).is_some_and(|vs| vs[0] != DEAD && vs.contains(&v)) {
|
||||
return Ok(t);
|
||||
}
|
||||
// The hint is maintained on every allocation, so this scan is a safety net only.
|
||||
self.live_tets().find(|(_, vs)| vs.contains(&v)).map(|(t, _)| t).ok_or_else(|| corrupt("a vertex is in no tetrahedron"))
|
||||
}
|
||||
|
||||
/// The tetrahedra incident to `v` (its star), into `out`.
|
||||
pub(super) fn star(&mut self, v: u32, out: &mut Vec<u32>) -> Result<()> {
|
||||
out.clear();
|
||||
let start = self.incident(v)?;
|
||||
self.next_epoch();
|
||||
self.visit(start);
|
||||
out.push(start);
|
||||
let mut i = 0;
|
||||
while let Some(&t) = out.get(i) {
|
||||
i += 1;
|
||||
let vs = self.tet(t)?;
|
||||
let ns = self.adj(t)?;
|
||||
for (&u, &n) in vs.iter().zip(&ns) {
|
||||
if u == v || n == NONE || self.visited(n) {
|
||||
continue;
|
||||
}
|
||||
self.visit(n);
|
||||
out.push(n);
|
||||
}
|
||||
if out.len() > self.live {
|
||||
return Err(corrupt("a vertex star larger than the mesh"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether `u v` is an edge.
|
||||
pub(super) fn has_edge(&mut self, u: u32, v: u32, scratch: &mut Vec<u32>) -> Result<bool> {
|
||||
self.star(u, scratch)?;
|
||||
for &t in scratch.iter() {
|
||||
if self.tet(t)?.contains(&v) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// The tetrahedra whose (conservative) box meets the box `lo..hi`, connected to the star of
|
||||
/// `v` through such tetrahedra; at most `cap` of them.
|
||||
pub(super) fn tets_near(&mut self, v: u32, lo: [f64; 3], hi: [f64; 3], cap: usize) -> Result<Vec<u32>> {
|
||||
let mut out = Vec::new();
|
||||
self.star(v, &mut out)?;
|
||||
self.next_epoch();
|
||||
for &t in &out {
|
||||
self.visit(t);
|
||||
}
|
||||
let mut i = 0;
|
||||
while let Some(&t) = out.get(i) {
|
||||
i += 1;
|
||||
if out.len() >= cap {
|
||||
break;
|
||||
}
|
||||
for n in self.adj(t)? {
|
||||
if n == NONE || self.visited(n) {
|
||||
continue;
|
||||
}
|
||||
self.visit(n);
|
||||
let (l, h) = self.bounds_of(&self.tet(n)?);
|
||||
if (0..3).all(|k| l[k] <= hi[k] && h[k] >= lo[k]) {
|
||||
out.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// ---- point location ----
|
||||
|
||||
/// The visibility walk towards `p` from `start` (Devillers, Pion, Teillaud 2002), capped at
|
||||
/// the number of tetrahedra, then a scan.
|
||||
pub(super) fn locate(&mut self, p: u32, start: u32) -> Result<Located> {
|
||||
let mut t = if self.is_live(start) { start } else { self.live_tets().next().map(|(t, _)| t).ok_or_else(|| corrupt("empty mesh"))? };
|
||||
let mut prev = NONE;
|
||||
for _ in 0..self.live.saturating_add(16) {
|
||||
let vs = self.tet(t)?;
|
||||
let ns = self.adj(t)?;
|
||||
self.rng ^= self.rng << 13;
|
||||
self.rng ^= self.rng >> 7;
|
||||
self.rng ^= self.rng << 17;
|
||||
let first = (self.rng >> 61) as usize;
|
||||
let mut next = None;
|
||||
for i in 0..4 {
|
||||
let k = (first + i) % 4;
|
||||
let n = ns.get(k).copied().unwrap_or(NONE);
|
||||
if (n != prev || n == NONE) && self.beyond(vs, k, p)? {
|
||||
next = Some(n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
match next {
|
||||
None => return self.inside(vs, t, p),
|
||||
Some(NONE) => return Err(Error::Failed("a Steiner point lies outside the bounding box".into())),
|
||||
Some(n) => {
|
||||
prev = t;
|
||||
t = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The walk may cycle in a non-Delaunay mesh: scan.
|
||||
let live: Vec<(u32, [u32; 4])> = self.live_tets().collect();
|
||||
for (t, vs) in live {
|
||||
let mut inside = true;
|
||||
for k in 0..4 {
|
||||
if self.beyond(vs, k, p)? {
|
||||
inside = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if inside {
|
||||
return self.inside(vs, t, p);
|
||||
}
|
||||
}
|
||||
Err(Error::Failed("a Steiner point is in no tetrahedron".into()))
|
||||
}
|
||||
|
||||
/// Whether `p` is strictly beyond the face of `vs` opposite slot `k`.
|
||||
fn beyond(&self, mut vs: [u32; 4], k: usize, p: u32) -> Result<bool> {
|
||||
if let Some(s) = vs.get_mut(k) {
|
||||
*s = p;
|
||||
}
|
||||
Ok(self.volume(vs)? == Sign::Negative)
|
||||
}
|
||||
|
||||
fn inside(&self, vs: [u32; 4], t: u32, p: u32) -> Result<Located> {
|
||||
for v in vs {
|
||||
if self.same_point(v, p)? {
|
||||
return Ok(Located::Vertex(v));
|
||||
}
|
||||
}
|
||||
Ok(Located::Inside(t))
|
||||
}
|
||||
|
||||
// ---- modification ----
|
||||
|
||||
/// Add a point (not yet in any tetrahedron); its vertex id.
|
||||
pub(super) fn push_point(&mut self, p: GenericPoint) -> Result<u32> {
|
||||
if self.points.len() >= (NONE - 2) as usize {
|
||||
return Err(Error::Limit("too many vertices".into()));
|
||||
}
|
||||
let (l, h) = p.bounds();
|
||||
self.approx.push(p.approx());
|
||||
self.points.push(p);
|
||||
self.lo.push(l);
|
||||
self.hi.push(h);
|
||||
self.vertex_tet.push(NONE);
|
||||
Ok((self.points.len() - 1) as u32)
|
||||
}
|
||||
|
||||
/// Insert vertex `v` (from [`Complex::push_point`]) by Bowyer–Watson with the perturbed
|
||||
/// in-sphere test: the mesh must be Delaunay for that predicate, which it is until the facet
|
||||
/// recovery starts. The walk starts at a tetrahedron incident to `near`. Returns the vertices
|
||||
/// of the deleted tetrahedra (the only ones whose edges can have changed).
|
||||
pub(super) fn insert(&mut self, v: u32, near: u32) -> Result<Vec<u32>> {
|
||||
let start = self.incident(near)?;
|
||||
let seed = match self.locate(v, start)? {
|
||||
Located::Vertex(u) => {
|
||||
let at = self.approx(u)?;
|
||||
return Err(Error::Failed(format!("a Steiner point coincides with vertex {u} at {at:?}")));
|
||||
}
|
||||
Located::Inside(t) => t,
|
||||
};
|
||||
// The cavity: every tetrahedron whose perturbed circumsphere contains v, grown from the
|
||||
// one containing it (which always conflicts: v is in the closed tetrahedron, so strictly
|
||||
// inside its ball).
|
||||
self.next_epoch();
|
||||
let mut cavity = vec![seed];
|
||||
self.visit(seed);
|
||||
let mut boundary: Vec<(u32, usize)> = Vec::new();
|
||||
let mut i = 0;
|
||||
while let Some(&t) = cavity.get(i) {
|
||||
i += 1;
|
||||
for (j, n) in self.adj(t)?.into_iter().enumerate() {
|
||||
if n == NONE || self.rejected(n) {
|
||||
boundary.push((t, j));
|
||||
continue;
|
||||
}
|
||||
if self.visited(n) {
|
||||
continue;
|
||||
}
|
||||
if self.in_sphere(self.tet(n)?, v)? {
|
||||
self.visit(n);
|
||||
cavity.push(n);
|
||||
} else {
|
||||
self.reject(n);
|
||||
boundary.push((t, j));
|
||||
}
|
||||
}
|
||||
if cavity.len() > self.live {
|
||||
return Err(corrupt("a cavity larger than the mesh"));
|
||||
}
|
||||
}
|
||||
let mut new = Vec::with_capacity(boundary.len());
|
||||
for (t, j) in boundary {
|
||||
let mut vs = self.tet(t)?;
|
||||
if let Some(s) = vs.get_mut(j) {
|
||||
*s = v;
|
||||
}
|
||||
if self.volume(vs)? != Sign::Positive {
|
||||
return Err(corrupt("a Bowyer–Watson cavity is not star-shaped from the new point"));
|
||||
}
|
||||
new.push(vs);
|
||||
}
|
||||
let mut touched: Vec<u32> = Vec::with_capacity(4 * cavity.len());
|
||||
for &t in &cavity {
|
||||
touched.extend(self.tet(t)?);
|
||||
}
|
||||
self.replace(&cavity, &new)?;
|
||||
touched.sort_unstable();
|
||||
touched.dedup();
|
||||
Ok(touched)
|
||||
}
|
||||
|
||||
fn alloc(&mut self, vs: [u32; 4]) -> Result<u32> {
|
||||
let t = match self.free.pop() {
|
||||
Some(t) => {
|
||||
*self.tets.get_mut(t as usize).ok_or_else(|| corrupt("free slot"))? = vs;
|
||||
*self.adj.get_mut(t as usize).ok_or_else(|| corrupt("free slot"))? = [NONE; 4];
|
||||
t
|
||||
}
|
||||
None => {
|
||||
if self.tets.len() >= self.max_tets {
|
||||
return Err(Error::Limit(format!("more than {} tetrahedra", self.max_tets)));
|
||||
}
|
||||
self.tets.push(vs);
|
||||
self.adj.push([NONE; 4]);
|
||||
self.mark.push(0);
|
||||
(self.tets.len() - 1) as u32
|
||||
}
|
||||
};
|
||||
for v in vs {
|
||||
if let Some(s) = self.vertex_tet.get_mut(v as usize) {
|
||||
*s = t;
|
||||
}
|
||||
}
|
||||
self.live += 1;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
fn kill(&mut self, t: u32) -> Result<()> {
|
||||
*self.tets.get_mut(t as usize).ok_or_else(|| corrupt("tetrahedron id"))? = [DEAD; 4];
|
||||
self.free.push(t);
|
||||
self.live = self.live.saturating_sub(1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_adj(&mut self, t: u32, i: usize, n: u32) -> Result<()> {
|
||||
*self.adj.get_mut(t as usize).and_then(|a| a.get_mut(i)).ok_or_else(|| corrupt("tetrahedron id"))? = n;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replace the tetrahedra `old` by `new` (positive tet4 tuples) filling the same region: each
|
||||
/// face of a new tetrahedron is either shared with exactly one other new tetrahedron or is a
|
||||
/// face of the region's boundary, where it takes over the outside neighbour. Anything else is
|
||||
/// reported, never patched. The new slot ids.
|
||||
pub(super) fn replace(&mut self, old: &[u32], new: &[[u32; 4]]) -> Result<Vec<u32>> {
|
||||
// Read the outside links before any slot is recycled.
|
||||
// Boundary face → (outside neighbour, its slot back, covered yet).
|
||||
let mut external: IdMap<[u32; 3], (u32, usize, bool)> = IdMap::with_capacity_and_hasher(old.len() * 2, Default::default());
|
||||
self.next_epoch();
|
||||
for &t in old {
|
||||
self.visit(t);
|
||||
}
|
||||
for &t in old {
|
||||
let vs = self.tet(t)?;
|
||||
for (j, n) in self.adj(t)?.into_iter().enumerate() {
|
||||
if n != NONE && self.visited(n) {
|
||||
continue;
|
||||
}
|
||||
let back = if n == NONE { 0 } else { self.adj(n)?.iter().position(|&m| m == t).ok_or_else(|| corrupt("one-sided adjacency"))? };
|
||||
if external.insert(sorted3(face_toward(vs, j)), (n, back, false)).is_some() {
|
||||
return Err(corrupt("a region boundary face is listed twice"));
|
||||
}
|
||||
}
|
||||
}
|
||||
for &t in old {
|
||||
self.kill(t)?;
|
||||
}
|
||||
let mut ids = Vec::with_capacity(new.len());
|
||||
let mut open: IdMap<[u32; 3], (u32, usize)> = IdMap::with_capacity_and_hasher(new.len() * 2, Default::default());
|
||||
let mut matched_external = 0usize;
|
||||
for &vs in new {
|
||||
let t = self.alloc(vs)?;
|
||||
ids.push(t);
|
||||
for k in 0..4 {
|
||||
let key = sorted3(face_toward(vs, k));
|
||||
if let Some(entry) = external.get_mut(&key) {
|
||||
let (n, back, covered) = *entry;
|
||||
if covered {
|
||||
return Err(corrupt("a region boundary face is covered twice"));
|
||||
}
|
||||
entry.2 = true;
|
||||
self.set_adj(t, k, n)?;
|
||||
if n != NONE {
|
||||
self.set_adj(n, back, t)?;
|
||||
}
|
||||
matched_external += 1;
|
||||
} else if let Some((u, l)) = open.remove(&key) {
|
||||
self.set_adj(t, k, u)?;
|
||||
self.set_adj(u, l, t)?;
|
||||
} else {
|
||||
open.insert(key, (t, k));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !open.is_empty() || matched_external != external.len() {
|
||||
return Err(corrupt(&format!(
|
||||
"a retriangulated region does not close: {} unmatched inner faces, {} of {} boundary faces covered",
|
||||
open.len(),
|
||||
matched_external,
|
||||
external.len()
|
||||
)));
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
//! Whether two closed simplices meet properly: their intersection is exactly the simplex spanned
|
||||
//! by their shared vertices (empty when they share none). The gift-wrapping step of the facet
|
||||
//! recovery accepts a new tetrahedron only if it meets every cavity and front triangle properly
|
||||
//! (Diazzi et al. 2023, §4.4 condition ii and App. B), and it must be exact on the coplanar
|
||||
//! configurations CAD boundaries are made of.
|
||||
//!
|
||||
//! # Method
|
||||
//! Two closed convex polytopes `A`, `B` whose relative interiors are disjoint are weakly separated
|
||||
//! by a hyperplane `H` (`A` on one closed side, `B` on the other), and then `A ∩ B = (A ∩ H) ∩
|
||||
//! (B ∩ H)`. The separating hyperplanes form a pointed polyhedral cone (in `(n, c)` space) whose
|
||||
//! extreme rays are hyperplanes through `d` affinely independent points of `A ∪ B` (`d` the
|
||||
//! dimension), so trying the hyperplanes through `d` of the vertices finds one whenever one
|
||||
//! exists. When none exists the relative interiors overlap and the intersection is not a common
|
||||
//! face. When one exists, every one gives the same `A ∩ B`, and the question recurses into `H`
|
||||
//! (one dimension lower) with the vertices on it; a point is the base case. Before that, if all
|
||||
//! of one simplex's vertices are shared, that simplex is a face of the other and they meet
|
||||
//! properly.
|
||||
//!
|
||||
//! Every test is an exact predicate: `orient3d` in space, `orient2d` projected along an axis the
|
||||
//! plane is not parallel to, and coordinate comparison along an axis the line is not
|
||||
//! perpendicular to.
|
||||
|
||||
use crate::predicates::{Axis, GenericPoint, Sign, generic_compare, generic_orient2d, generic_orient3d};
|
||||
use crate::{Error, Result};
|
||||
|
||||
use super::complex::face_toward;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Level {
|
||||
Space,
|
||||
/// A plane, compared by orientation in its projection along this axis.
|
||||
Plane(Axis),
|
||||
/// A line, compared along this axis.
|
||||
Line(Axis),
|
||||
Point,
|
||||
}
|
||||
|
||||
fn pt(p: &[GenericPoint], v: u32) -> Result<&GenericPoint> {
|
||||
p.get(v as usize).ok_or_else(|| Error::Failed("intersection test: vertex id out of range".into()))
|
||||
}
|
||||
|
||||
/// Whether the closed simplices with vertex ids `a` and `b` (each affinely independent, ids of
|
||||
/// distinct points) intersect exactly in the hull of their shared vertices.
|
||||
pub(super) fn meet_properly(p: &[GenericPoint], a: &[u32], b: &[u32]) -> Result<bool> {
|
||||
proper(p, Level::Space, a, b)
|
||||
}
|
||||
|
||||
/// Whether `x` is in the closed positive tetrahedron `t` (Diazzi et al. 2023, Alg. 3).
|
||||
pub(super) fn in_closed_tet(p: &[GenericPoint], t: [u32; 4], x: u32) -> Result<bool> {
|
||||
for k in 0..4 {
|
||||
let [u, v, w] = face_toward(t, k);
|
||||
// The tetrahedron is on the face's positive side (tet4 volume, i.e. orient3d negative).
|
||||
if generic_orient3d(pt(p, u)?, pt(p, v)?, pt(p, w)?, pt(p, x)?) == Sign::Positive {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn subset(x: &[u32], y: &[u32]) -> bool {
|
||||
x.iter().all(|v| y.contains(v))
|
||||
}
|
||||
|
||||
fn proper(p: &[GenericPoint], level: Level, a: &[u32], b: &[u32]) -> Result<bool> {
|
||||
if subset(a, b) || subset(b, a) {
|
||||
return Ok(true);
|
||||
}
|
||||
// Beyond the subset rule, a point holds one vertex, which both sides would share.
|
||||
let k = match level {
|
||||
Level::Space => 3,
|
||||
Level::Plane(_) => 2,
|
||||
Level::Line(_) => 1,
|
||||
Level::Point => return Ok(true),
|
||||
};
|
||||
let mut all: Vec<u32> = a.to_vec();
|
||||
all.extend(b.iter().filter(|v| !a.contains(v)));
|
||||
// Candidate hyperplanes through k of the points, those spanned by one simplex first (they
|
||||
// separate most pairs).
|
||||
let mut candidates: Vec<[u32; 3]> = Vec::new();
|
||||
combinations(&all, k, &mut [0; 3], 0, 0, &mut candidates);
|
||||
candidates.sort_by_key(|h| !(subset(&h[..k], a) || subset(&h[..k], b)));
|
||||
let mut spans = false;
|
||||
for h in &candidates {
|
||||
let h = &h[..k];
|
||||
if let (Level::Space, &[u, v, w]) = (level, h)
|
||||
&& plane_axis(p, [u, v, w])?.is_none()
|
||||
{
|
||||
continue; // collinear: no plane
|
||||
}
|
||||
spans = true;
|
||||
let mut signs: Vec<(u32, Sign)> = Vec::with_capacity(all.len());
|
||||
for &x in &all {
|
||||
signs.push((x, if h.contains(&x) { Sign::Zero } else { side(p, level, h, x)? }));
|
||||
}
|
||||
let sign_of = |v: &u32| signs.iter().find(|(x, _)| x == v).map_or(Sign::Zero, |(_, s)| *s);
|
||||
let has = |set: &[u32], s: Sign| set.iter().any(|v| sign_of(v) == s);
|
||||
let separated = (!has(a, Sign::Negative) && !has(b, Sign::Positive)) || (!has(a, Sign::Positive) && !has(b, Sign::Negative));
|
||||
if !separated {
|
||||
continue;
|
||||
}
|
||||
let on = |set: &[u32]| -> Vec<u32> { set.iter().copied().filter(|v| sign_of(v) == Sign::Zero).collect() };
|
||||
let lower = match (level, h) {
|
||||
(Level::Space, &[u, v, w]) => {
|
||||
Level::Plane(plane_axis(p, [u, v, w])?.ok_or_else(|| Error::Failed("intersection test: degenerate plane".into()))?)
|
||||
}
|
||||
(Level::Plane(_), &[u, v]) => Level::Line(line_axis(p, u, v)?),
|
||||
_ => Level::Point,
|
||||
};
|
||||
return proper(p, lower, &on(a), &on(b));
|
||||
}
|
||||
if !spans && let (Level::Space, &[u, v, ..]) = (level, all.as_slice()) {
|
||||
// All points collinear: compare along their line.
|
||||
return proper(p, Level::Line(line_axis(p, u, v)?), a, b);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// An axis the plane through `u v w` is not parallel to (`None`: the points are collinear).
|
||||
fn plane_axis(p: &[GenericPoint], [u, v, w]: [u32; 3]) -> Result<Option<Axis>> {
|
||||
let (a, b, c) = (pt(p, u)?, pt(p, v)?, pt(p, w)?);
|
||||
Ok(Axis::ALL.into_iter().find(|&ax| !generic_orient2d(a, b, c, ax).is_zero()))
|
||||
}
|
||||
|
||||
/// An axis along which two distinct points differ.
|
||||
fn line_axis(p: &[GenericPoint], u: u32, v: u32) -> Result<Axis> {
|
||||
let (a, b) = (pt(p, u)?, pt(p, v)?);
|
||||
Axis::ALL
|
||||
.into_iter()
|
||||
.find(|&ax| generic_compare(a, b, ax).is_ne())
|
||||
.ok_or_else(|| Error::Failed("intersection test: two vertices are the same point".into()))
|
||||
}
|
||||
|
||||
fn side(p: &[GenericPoint], level: Level, h: &[u32], x: u32) -> Result<Sign> {
|
||||
let px = pt(p, x)?;
|
||||
Ok(match (level, h) {
|
||||
(Level::Space, &[u, v, w]) => generic_orient3d(pt(p, u)?, pt(p, v)?, pt(p, w)?, px),
|
||||
(Level::Plane(ax), &[u, v]) => generic_orient2d(pt(p, u)?, pt(p, v)?, px, ax),
|
||||
(Level::Line(ax), &[u]) => match generic_compare(px, pt(p, u)?, ax) {
|
||||
std::cmp::Ordering::Less => Sign::Negative,
|
||||
std::cmp::Ordering::Equal => Sign::Zero,
|
||||
std::cmp::Ordering::Greater => Sign::Positive,
|
||||
},
|
||||
_ => return Err(Error::Failed("intersection test: malformed hyperplane".into())),
|
||||
})
|
||||
}
|
||||
|
||||
/// The `k`-subsets of `all` (`k ≤ 3`), each in the first `k` slots.
|
||||
fn combinations(all: &[u32], k: usize, cur: &mut [u32; 3], len: usize, from: usize, out: &mut Vec<[u32; 3]>) {
|
||||
if len == k {
|
||||
out.push(*cur);
|
||||
return;
|
||||
}
|
||||
for i in from..all.len() {
|
||||
if let (Some(&v), Some(slot)) = (all.get(i), cur.get_mut(len)) {
|
||||
*slot = v;
|
||||
combinations(all, k, cur, len + 1, i + 1, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
//! Constrained Delaunay tetrahedralization of a closed boundary surface: every input triangle is
|
||||
//! a union of tetrahedron faces, with no tolerance anywhere in the decisions.
|
||||
//!
|
||||
//! # Method
|
||||
//! L. Diazzi, D. Panozzo, A. Vaxman, M. Attene, *Constrained Delaunay Tetrahedrization: A Robust
|
||||
//! and Practical Approach*, ACM TOG 42(6):181, 2023 (arXiv 2309.09805; paper only, the authors'
|
||||
//! code is GPL/LGPL and is not read), on the theory of H. Si, K. Gärtner, *Meshing Piecewise
|
||||
//! Linear Complexes by Constrained Delaunay Tetrahedralizations*, IMR 2005, and J. R. Shewchuk,
|
||||
//! *Constrained Delaunay Tetrahedralizations and Provably Good Boundary Recovery*, IMR 2002 (also
|
||||
//! papers only; TetGen is AGPL).
|
||||
//!
|
||||
//! 1. The Delaunay tetrahedralization ([`crate::delaunay`]) of the surface's vertices and the
|
||||
//! eight corners of a box around them, so every Steiner point lies strictly inside the hull.
|
||||
//! 2. **Segment recovery** ([`segments`], §3.3): every edge of the surface is a segment; a missing
|
||||
//! one is split at an implicit Steiner point (an [`Lnc`](crate::predicates::Lnc) point on the
|
||||
//! original segment, §4.3) placed by Si's rule and inserted by Bowyer–Watson with the same
|
||||
//! perturbed in-sphere test as the Delaunay step, until all segments are edges.
|
||||
//! 3. **Facet recovery** ([`facets`], §3.4, §4.4): every input triangle is a facet; the
|
||||
//! tetrahedra crossing a missing facet are deleted and the two half-cavities refilled by the
|
||||
//! modified gift-wrapping, which builds the constrained Delaunay tetrahedralization of each
|
||||
//! half with exact predicates on implicit points.
|
||||
//! 4. **Inside/outside** (§4.5): a flood fill from the box corners that flips at every facet
|
||||
//! triangle; the orientation of every facet triangle against its inside tetrahedron is then
|
||||
//! checked exactly.
|
||||
//!
|
||||
//! Every decision is an exact predicate ([`crate::predicates`]): `orient3d`, `orient2d` and
|
||||
//! coordinate comparison on explicit and LNC points, and `insphere` with the Devillers–Teillaud
|
||||
//! symbolic perturbation. The result is checked before it is returned: every segment is an edge,
|
||||
//! no edge crosses any facet, every tetrahedron is positive (exactly), and the labelling is
|
||||
//! consistent with the surface's orientation.
|
||||
//!
|
||||
//! # Output
|
||||
//! [`TetMesh`]: the inside tetrahedra in tet4 order, the surface's vertices first (same ids) and
|
||||
//! then the Steiner points, each with its segment and parameter, rounded to doubles; and for each
|
||||
//! boundary tetrahedron face the input triangle it lies on (so the B-rep face tag carries over).
|
||||
//! Rounding a Steiner point to the nearest doubles moves it off its segment by under an ulp and
|
||||
//! can, on a nearly flat tetrahedron, flip it (§5.1). [`round`] then tries the doubles a few ulps
|
||||
//! around such a point for one that keeps every incident tetrahedron positive; [`TetMesh::gate`]
|
||||
//! runs the validity gate on the rounded mesh and any failure left is reported, never hidden.
|
||||
//!
|
||||
//! # Determinism and bounds
|
||||
//! Same surface → same mesh: the Delaunay step is deterministic, segments and facets are
|
||||
//! processed in a fixed order, and hash maps are only looked up. Steiner points, tetrahedra,
|
||||
//! passes and cavities are capped ([`CdtOptions`]); hitting a cap is an [`Error::Limit`].
|
||||
|
||||
mod complex;
|
||||
mod facets;
|
||||
mod meet;
|
||||
mod round;
|
||||
mod segments;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
|
||||
use crate::delaunay::Triangulation;
|
||||
use crate::predicates::{GenericPoint, Point3, Sign};
|
||||
use crate::quality::{GateOptions, GateReport, validity_gate_elements};
|
||||
use crate::surface::{BoundarySurface, Box3};
|
||||
use crate::{Error, Result};
|
||||
|
||||
use complex::{Complex, NONE, corrupt, face_toward, sorted3};
|
||||
use segments::Segment;
|
||||
|
||||
/// Caps on the work of [`tetrahedralize`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct CdtOptions {
|
||||
/// Most Steiner points added on segments. A boundary whose triangles are long next to
|
||||
/// features much closer than their length (needle triangles) needs Steiner points at that
|
||||
/// spacing all along them; the default is well above what the corpus needs (30k at most) and
|
||||
/// stops such input in seconds.
|
||||
pub max_steiner: usize,
|
||||
/// Most tetrahedra (inside and outside, while building).
|
||||
pub max_tets: usize,
|
||||
}
|
||||
|
||||
impl Default for CdtOptions {
|
||||
fn default() -> Self {
|
||||
CdtOptions { max_steiner: 50_000, max_tets: 20_000_000 }
|
||||
}
|
||||
}
|
||||
|
||||
/// A Steiner point: `points[a] + t (points[b] − points[a])` of the surface edge `[a, b]`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct SteinerPoint {
|
||||
pub segment: [u32; 2],
|
||||
pub t: f64,
|
||||
}
|
||||
|
||||
/// A tetrahedron face on the boundary: tetrahedron `tet`'s face opposite its vertex
|
||||
/// `opposite` (0..4) lies on surface triangle `triangle`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BoundaryFace {
|
||||
pub tet: u32,
|
||||
pub opposite: u8,
|
||||
pub triangle: u32,
|
||||
}
|
||||
|
||||
impl BoundaryFace {
|
||||
/// The face's 1-based number in the model's tet4 face order ([`ElementKind::faces`]): the
|
||||
/// face without the opposite vertex.
|
||||
pub fn tet4_face(&self) -> Option<u8> {
|
||||
let k = usize::from(self.opposite);
|
||||
let n = ElementKind::Tet4.faces().iter().position(|f| !f.contains(&k))?;
|
||||
u8::try_from(n + 1).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// What the recovery did.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct CdtStats {
|
||||
/// Tetrahedra of the initial Delaunay tetrahedralization (with the box).
|
||||
pub delaunay_tets: usize,
|
||||
/// Missing subsegments split.
|
||||
pub missing_subsegments: usize,
|
||||
/// Cavities refilled by gift-wrapping.
|
||||
pub cavities: usize,
|
||||
/// Tetrahedra made by gift-wrapping.
|
||||
pub gift_wrapped_tets: usize,
|
||||
/// Tetrahedra in the box at the end.
|
||||
pub box_tets: usize,
|
||||
/// Steiner points moved by a few ulps from their nearest doubles to keep every incident
|
||||
/// tetrahedron positive ([`round`]).
|
||||
pub rounding_adjusted: usize,
|
||||
}
|
||||
|
||||
/// The constrained Delaunay tetrahedralization of a closed surface (see the module docs).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TetMesh {
|
||||
/// The surface's vertices (same ids as [`BoundarySurface::points`]), then the Steiner
|
||||
/// points rounded to doubles.
|
||||
pub points: Vec<[f64; 3]>,
|
||||
/// The Steiner points, for `points[surface vertices..]`.
|
||||
pub steiner: Vec<SteinerPoint>,
|
||||
/// The inside tetrahedra, tet4 order (positive volume in exact arithmetic).
|
||||
pub tets: Vec<[u32; 4]>,
|
||||
/// Every tetrahedron face on the surface, with its input triangle.
|
||||
pub boundary: Vec<BoundaryFace>,
|
||||
pub stats: CdtStats,
|
||||
}
|
||||
|
||||
impl TetMesh {
|
||||
/// The validity gate on the rounded coordinates: every tetrahedron positive, and the volume
|
||||
/// equal to `expected_volume` (the surface's, [`BoundarySurface::volume`]) within 1e-9.
|
||||
pub fn gate(&self, expected_volume: f64) -> Result<GateReport> {
|
||||
let coords = |t: &[u32; 4]| -> Result<[[f64; 3]; 4]> {
|
||||
let p = |v: u32| self.points.get(v as usize).copied().ok_or_else(|| corrupt("output vertex id"));
|
||||
Ok([p(t[0])?, p(t[1])?, p(t[2])?, p(t[3])?])
|
||||
};
|
||||
let elements = self.tets.iter().enumerate().map(|(i, t)| Ok((i as u32 + 1, ElementKind::Tet4, coords(t)?))).collect::<Result<Vec<_>>>()?;
|
||||
validity_gate_elements(elements, &GateOptions { expected_volume: Some(expected_volume), volume_tolerance: 1e-9 })
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of one recovery.
|
||||
pub(crate) struct Recovery {
|
||||
cx: Complex,
|
||||
/// The surface's triangles (the facets), vertex ids as in the complex.
|
||||
triangles: Vec<[u32; 3]>,
|
||||
segments: Vec<Segment>,
|
||||
/// Sorted edge → segment.
|
||||
edge_segment: HashMap<(u32, u32), usize>,
|
||||
/// Recovered facet triangles (sorted vertex ids) → facet.
|
||||
constraint: HashMap<[u32; 3], u32>,
|
||||
/// The surface's vertices are the complex's first `surface` vertices, then 8 box corners,
|
||||
/// then the Steiner points.
|
||||
surface: usize,
|
||||
/// Steiner points in creation order: (segment, t).
|
||||
steiner: Vec<(usize, f64)>,
|
||||
options: CdtOptions,
|
||||
stats: CdtStats,
|
||||
}
|
||||
|
||||
/// The constrained Delaunay tetrahedralization of `surface` (see the module docs).
|
||||
pub fn tetrahedralize(surface: &BoundarySurface, options: &CdtOptions) -> Result<TetMesh> {
|
||||
let mut rec = Recovery::new(surface, options)?;
|
||||
rec.classify_acute()?;
|
||||
rec.recover_segments()?;
|
||||
for f in 0..rec.triangles.len() {
|
||||
rec.recover_facet(f)?;
|
||||
}
|
||||
rec.verify()?;
|
||||
let inside = rec.label()?;
|
||||
rec.output(&inside)
|
||||
}
|
||||
|
||||
impl Recovery {
|
||||
fn new(surface: &BoundarySurface, options: &CdtOptions) -> Result<Self> {
|
||||
let n = surface.points().len();
|
||||
let triangles = surface.triangles().to_vec();
|
||||
if triangles.is_empty() {
|
||||
return Err(Error::Invalid("the surface has no triangles".into()));
|
||||
}
|
||||
let mut points: Vec<[f64; 3]> = surface.points().iter().map(|p| p.map(|x| x + 0.0)).collect();
|
||||
let mut bbox = Box3::EMPTY;
|
||||
for p in &points {
|
||||
bbox.add_point(*p);
|
||||
}
|
||||
let size = (0..3).map(|k| bbox.hi[k] - bbox.lo[k]).fold(0.0, f64::max).max(1.0);
|
||||
for corner in 0..8 {
|
||||
points.push(std::array::from_fn(|k| {
|
||||
let x = if corner >> k & 1 == 0 { bbox.lo[k] - size } else { bbox.hi[k] + size };
|
||||
crate::predicates::snap_to_domain(x) + 0.0
|
||||
}));
|
||||
}
|
||||
let tri = Triangulation::new(&points)?;
|
||||
if tri.dimension() != 3 || (0..points.len() as u32).any(|v| tri.duplicate_of(v).is_some()) {
|
||||
return Err(Error::Invalid("the surface's vertices must be distinct".into()));
|
||||
}
|
||||
let generic = points.iter().map(|&p| Point3::try_from(p).map(GenericPoint::Explicit)).collect::<Result<Vec<_>>>()?;
|
||||
let cx = Complex::from_delaunay(&tri, generic, options.max_tets)?;
|
||||
let mut edges: Vec<(u32, u32)> =
|
||||
triangles.iter().flat_map(|t| (0..3).map(move |k| (t[k].min(t[(k + 1) % 3]), t[k].max(t[(k + 1) % 3])))).collect();
|
||||
edges.sort_unstable();
|
||||
edges.dedup();
|
||||
if let Some(&(a, b)) = edges.iter().find(|(a, b)| a == b || *b as usize >= n) {
|
||||
return Err(Error::Invalid(format!("surface edge {a}–{b} is degenerate or has no vertex")));
|
||||
}
|
||||
let segments: Vec<Segment> = edges.iter().map(|&(a, b)| Segment::new(a, b)).collect();
|
||||
let edge_segment = edges.iter().enumerate().map(|(i, &e)| (e, i)).collect();
|
||||
let stats = CdtStats { delaunay_tets: cx.live_count(), ..CdtStats::default() };
|
||||
Ok(Recovery { cx, triangles, segments, edge_segment, constraint: HashMap::new(), surface: n, steiner: Vec::new(), options: *options, stats })
|
||||
}
|
||||
|
||||
/// Every segment is an edge, no edge crosses a facet, every facet has triangles, every
|
||||
/// tetrahedron is positive.
|
||||
fn verify(&mut self) -> Result<()> {
|
||||
let mut scratch = Vec::new();
|
||||
for s in 0..self.segments.len() {
|
||||
let chain: Vec<u32> = self.segment(s)?.splits.iter().map(|&(_, v)| v).collect();
|
||||
for w in chain.windows(2) {
|
||||
if let &[u, v] = w
|
||||
&& !self.cx.has_edge(u, v, &mut scratch)?
|
||||
{
|
||||
let at = self.cx.approx(u)?;
|
||||
return Err(Error::Failed(format!("a boundary edge near {at:?} was lost during facet recovery")));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut has_faces = vec![false; self.triangles.len()];
|
||||
for &f in self.constraint.values() {
|
||||
if let Some(h) = has_faces.get_mut(f as usize) {
|
||||
*h = true;
|
||||
}
|
||||
}
|
||||
for f in 0..self.triangles.len() {
|
||||
if !has_faces.get(f).copied().unwrap_or(false) || !self.facet_present(f)? {
|
||||
return Err(Error::Failed(format!("input triangle {f} is not a union of tetrahedron faces")));
|
||||
}
|
||||
}
|
||||
let live: Vec<[u32; 4]> = self.cx.live_tets().map(|(_, vs)| vs).collect();
|
||||
for vs in live {
|
||||
if self.cx.volume(vs)? != Sign::Positive {
|
||||
let at = self.cx.approx(vs[0])?;
|
||||
return Err(corrupt(&format!("a tetrahedron near {at:?} is not positive")));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inside (`true`) or outside, per tetrahedron slot: a flood fill from the box corners that
|
||||
/// flips across every facet triangle (Diazzi et al. §4.5), then the exact check that each
|
||||
/// facet triangle has its inside tetrahedron on the side its normal points away from.
|
||||
fn label(&mut self) -> Result<Vec<bool>> {
|
||||
let mut label: Vec<Option<bool>> = vec![None; self.cx.slots()];
|
||||
let corners = self.surface..self.surface + 8;
|
||||
let corner = self.cx.live_tets().find(|(_, vs)| vs.iter().any(|&v| corners.contains(&(v as usize))));
|
||||
let (start, _) = corner.ok_or_else(|| corrupt("no tetrahedron touches the bounding box"))?;
|
||||
if let Some(l) = label.get_mut(start as usize) {
|
||||
*l = Some(false);
|
||||
}
|
||||
let mut queue = vec![start];
|
||||
while let Some(t) = queue.pop() {
|
||||
let inside = label.get(t as usize).copied().flatten().unwrap_or(false);
|
||||
let vs = self.cx.tet(t)?;
|
||||
for (k, nb) in self.cx.adj(t)?.into_iter().enumerate() {
|
||||
let want = inside != self.constraint.contains_key(&sorted3(face_toward(vs, k)));
|
||||
match label.get_mut(nb as usize) {
|
||||
None => {} // the hull
|
||||
Some(l @ None) => {
|
||||
*l = Some(want);
|
||||
queue.push(nb);
|
||||
}
|
||||
Some(Some(got)) if *got != want => {
|
||||
let at = self.cx.approx(vs[k])?;
|
||||
return Err(Error::Failed(format!(
|
||||
"inside/outside labelling is inconsistent near {at:?}: the surface does not bound a volume"
|
||||
)));
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let live: Vec<(u32, [u32; 4])> = self.cx.live_tets().collect();
|
||||
for (t, vs) in live {
|
||||
match label.get(t as usize).copied().flatten() {
|
||||
None => return Err(corrupt("a tetrahedron the flood fill did not reach")),
|
||||
Some(false) => {}
|
||||
Some(true) => {
|
||||
for k in 0..4 {
|
||||
let Some(&f) = self.constraint.get(&sorted3(face_toward(vs, k))) else { continue };
|
||||
let [a, b, c] = *self.triangles.get(f as usize).ok_or_else(|| corrupt("facet id"))?;
|
||||
let x = vs.get(k).copied().unwrap_or(NONE);
|
||||
if self.cx.volume([a, b, c, x])? != Sign::Negative {
|
||||
let at = self.cx.approx(a)?;
|
||||
return Err(Error::Failed(format!("input triangle {f} near {at:?} faces into the solid: the surface is inverted")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(label.into_iter().map(|l| l == Some(true)).collect())
|
||||
}
|
||||
|
||||
fn output(&self, inside: &[bool]) -> Result<TetMesh> {
|
||||
let n = self.surface;
|
||||
// Surface vertices keep their ids; box corners disappear; Steiner points follow.
|
||||
let total = self.cx.points().len();
|
||||
let mut id = vec![NONE; total];
|
||||
let mut points = Vec::with_capacity(n + self.steiner.len());
|
||||
for v in 0..n {
|
||||
if let Some(s) = id.get_mut(v) {
|
||||
*s = v as u32;
|
||||
}
|
||||
points.push(self.cx.approx(v as u32)?);
|
||||
}
|
||||
let mut steiner = Vec::with_capacity(self.steiner.len());
|
||||
for (k, &(s, t)) in self.steiner.iter().enumerate() {
|
||||
let v = n + 8 + k;
|
||||
if let Some(slot) = id.get_mut(v) {
|
||||
*slot = points.len() as u32;
|
||||
}
|
||||
points.push(self.cx.approx(v as u32)?);
|
||||
steiner.push(SteinerPoint { segment: self.segment(s)?.ends, t });
|
||||
}
|
||||
let mut tets = Vec::new();
|
||||
let mut boundary = Vec::new();
|
||||
for (t, vs) in self.cx.live_tets() {
|
||||
if !inside.get(t as usize).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let mapped = vs.map(|v| id.get(v as usize).copied().unwrap_or(NONE));
|
||||
if mapped.contains(&NONE) {
|
||||
return Err(corrupt("an inside tetrahedron uses a bounding-box corner"));
|
||||
}
|
||||
for k in 0..4 {
|
||||
if let Some(&f) = self.constraint.get(&sorted3(face_toward(vs, k))) {
|
||||
boundary.push(BoundaryFace { tet: tets.len() as u32, opposite: k as u8, triangle: f });
|
||||
}
|
||||
}
|
||||
tets.push(mapped);
|
||||
}
|
||||
let mut stats = self.stats;
|
||||
stats.box_tets = self.cx.live_count();
|
||||
stats.rounding_adjusted = round::repair(&mut points, &tets, n)?;
|
||||
Ok(TetMesh { points, steiner, tets, boundary, stats })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Rounding the Steiner points to doubles without inverting tetrahedra.
|
||||
//!
|
||||
//! The exact tetrahedralization is valid, but a Steiner point rounded to its nearest doubles is
|
||||
//! off its segment by up to half an ulp per coordinate, which flips a tetrahedron whose volume is
|
||||
//! below that scale (Diazzi et al. 2023, §5.1, §6.2: the nearly flat ones, typically four Steiner
|
||||
//! points on nearly parallel boundary edges). For each tetrahedron that is not positive after
|
||||
//! rounding, each of its Steiner points tries the doubles within [`REACH`] ulps of its rounded
|
||||
//! coordinates, nearest first, and takes the first with which every tetrahedron around it is
|
||||
//! positive (exact `orient3d` on the doubles). Surface vertices never move. The moves are a few
|
||||
//! ulps, so boundary faces stay on their triangles to rounding precision. The number of passes is
|
||||
//! capped; what is left is for the validity gate to report.
|
||||
//!
|
||||
//! This is the interim answer. The paper's own (§5.1) improves the exact mesh by flips that never
|
||||
//! cross a facet before rounding, which removes most nearly flat tetrahedra instead of nudging
|
||||
//! their vertices; it belongs with the flips of the quality stage.
|
||||
|
||||
use crate::predicates::{Point3, Sign, tet_volume_sign};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// How far, in ulps per coordinate, a rounded Steiner point may move.
|
||||
const REACH: i32 = 2;
|
||||
/// Passes over the tetrahedra that are not positive.
|
||||
const MAX_PASSES: usize = 4;
|
||||
|
||||
fn positive(points: &[[f64; 3]], t: &[u32; 4]) -> Result<bool> {
|
||||
let p = |v: u32| -> Result<Point3> {
|
||||
let c = points.get(v as usize).copied().ok_or_else(|| Error::Failed("rounding: vertex id out of range".into()))?;
|
||||
Point3::try_from(c)
|
||||
};
|
||||
Ok(tet_volume_sign(&p(t[0])?, &p(t[1])?, &p(t[2])?, &p(t[3])?) == Sign::Positive)
|
||||
}
|
||||
|
||||
/// `x` moved by `k` ulps.
|
||||
fn step(mut x: f64, k: i32) -> f64 {
|
||||
for _ in 0..k.unsigned_abs() {
|
||||
x = if k > 0 { x.next_up() } else { x.next_down() };
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
/// Offsets within [`REACH`], nearest (smallest total) first, in a fixed order.
|
||||
fn offsets() -> Vec<[i32; 3]> {
|
||||
let r = -REACH..=REACH;
|
||||
let mut o: Vec<[i32; 3]> = r.clone().flat_map(|a| r.clone().flat_map(move |b| (-REACH..=REACH).map(move |c| [a, b, c]))).collect();
|
||||
o.retain(|d| d != &[0, 0, 0]);
|
||||
o.sort_by_key(|d| (d.iter().map(|k| k.abs()).sum::<i32>(), *d));
|
||||
o
|
||||
}
|
||||
|
||||
/// Move Steiner points (ids `>= surface`) so every tetrahedron is positive where a few-ulp move
|
||||
/// does it. The number of points moved.
|
||||
pub(super) fn repair(points: &mut [[f64; 3]], tets: &[[u32; 4]], surface: usize) -> Result<usize> {
|
||||
let mut bad = Vec::new();
|
||||
for (i, t) in tets.iter().enumerate() {
|
||||
if !positive(points, t)? {
|
||||
bad.push(i);
|
||||
}
|
||||
}
|
||||
if bad.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut around: Vec<Vec<u32>> = vec![Vec::new(); points.len()];
|
||||
for (i, t) in tets.iter().enumerate() {
|
||||
for &v in t {
|
||||
if (v as usize) >= surface
|
||||
&& let Some(a) = around.get_mut(v as usize)
|
||||
{
|
||||
a.push(i as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
let offsets = offsets();
|
||||
let mut moved = vec![false; points.len()];
|
||||
for _ in 0..MAX_PASSES {
|
||||
let mut still = Vec::new();
|
||||
for &i in &bad {
|
||||
let t = *tets.get(i).ok_or_else(|| Error::Failed("rounding: tetrahedron id".into()))?;
|
||||
if positive(points, &t)? {
|
||||
continue;
|
||||
}
|
||||
let mut fixed = false;
|
||||
for v in t.into_iter().filter(|&v| (v as usize) >= surface) {
|
||||
let Some(origin) = points.get(v as usize).copied() else { continue };
|
||||
let incident = around.get(v as usize).cloned().unwrap_or_default();
|
||||
for d in &offsets {
|
||||
let candidate: [f64; 3] = std::array::from_fn(|k| step(origin[k], d[k]));
|
||||
if candidate.iter().any(|x| !crate::predicates::is_admissible(*x)) {
|
||||
continue;
|
||||
}
|
||||
if let Some(p) = points.get_mut(v as usize) {
|
||||
*p = candidate;
|
||||
}
|
||||
let mut ok = true;
|
||||
for &j in &incident {
|
||||
let u = tets.get(j as usize).ok_or_else(|| Error::Failed("rounding: tetrahedron id".into()))?;
|
||||
if !positive(points, u)? {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
fixed = true;
|
||||
if let Some(m) = moved.get_mut(v as usize) {
|
||||
*m = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if let Some(p) = points.get_mut(v as usize) {
|
||||
*p = origin;
|
||||
}
|
||||
}
|
||||
if fixed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !fixed {
|
||||
still.push(i);
|
||||
}
|
||||
}
|
||||
if still.is_empty() || still.len() == bad.len() {
|
||||
break;
|
||||
}
|
||||
bad = still;
|
||||
}
|
||||
Ok(moved.iter().filter(|m| **m).count())
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
//! Segment recovery (Si & Gärtner 2005, as restated by Diazzi et al. 2023, §3.3): while an edge
|
||||
//! of the input surface (a *segment*) is not an edge of the Delaunay tetrahedralization, split
|
||||
//! it at a Steiner point and insert the point (Bowyer–Watson). Each Steiner point is the LNC
|
||||
//! point `a + t (b − a)` of its original segment `a b` (Diazzi et al. §4.2–§4.3): it lies on the
|
||||
//! segment exactly, and only `t` is computed in floating point, so the placement rule may round
|
||||
//! but never moves a point off its segment.
|
||||
//!
|
||||
//! The placement follows Si's reference-point rule. The reference point `r` of a missing
|
||||
//! subsegment `v₁ v₂` is the vertex inside its diametral sphere for which the circle through
|
||||
//! `v₁, v₂, r` is largest. A vertex is *acute* when two segments meet there at less than 90°;
|
||||
//! the rule depends on how many of the original segment's endpoints are acute:
|
||||
//! - none: split where the smaller of the spheres centred at `v₁`, `v₂` through `r` cuts the
|
||||
//! segment, or at the midpoint when both radii exceed half the length;
|
||||
//! - one (`w`): `p` is where the sphere centred at `w` through `r` cuts the segment; keep `p` if
|
||||
//! it is closer to `r` than to the far endpoint `v_o`, else shift it towards the near endpoint
|
||||
//! `v_w` by `|r p|` when that is under half of `|v_w p|`, else take the midpoint of `v_w p`;
|
||||
//! - two: split at the midpoint first, after which each half has one acute endpoint.
|
||||
//!
|
||||
//! Any value that falls outside the open subsegment becomes the midpoint. Termination is Si's
|
||||
//! (the spheres around acute vertices keep splits from cascading); the number of Steiner points
|
||||
//! and of passes is still capped.
|
||||
|
||||
use crate::predicates::{Axis, GenericPoint, Lnc, Point3, generic_orient2d};
|
||||
use crate::vec3::{cross, dot, norm, sub};
|
||||
use crate::{Error, Result};
|
||||
|
||||
use super::Recovery;
|
||||
|
||||
/// Most tetrahedra searched for a reference point.
|
||||
const NEAR_CAP: usize = 20_000;
|
||||
/// Most passes over the segments.
|
||||
const MAX_PASSES: usize = 10_000;
|
||||
|
||||
/// An input segment and its split points.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct Segment {
|
||||
/// The original endpoints (explicit vertices), `ends[0] < ends[1]`.
|
||||
pub ends: [u32; 2],
|
||||
/// `(t, vertex)` along the segment from `ends[0]` (`t = 0`) to `ends[1]` (`t = 1`).
|
||||
pub splits: Vec<(f64, u32)>,
|
||||
/// Whether each original endpoint is acute.
|
||||
pub acute: [bool; 2],
|
||||
}
|
||||
|
||||
impl Segment {
|
||||
pub(super) fn new(a: u32, b: u32) -> Self {
|
||||
let ends = [a.min(b), a.max(b)];
|
||||
Segment { ends, splits: vec![(0.0, ends[0]), (1.0, ends[1])], acute: [false; 2] }
|
||||
}
|
||||
|
||||
/// The vertices from `from` to the other end, in order.
|
||||
pub(super) fn chain_from(&self, from: u32) -> Vec<u32> {
|
||||
let mut v: Vec<u32> = self.splits.iter().map(|&(_, v)| v).collect();
|
||||
if from != self.ends[0] {
|
||||
v.reverse();
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
fn dist(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
norm(sub(a, b))
|
||||
}
|
||||
|
||||
impl Recovery {
|
||||
/// Mark acute vertices: two segments at a vertex make an angle under 90° (floating point:
|
||||
/// this only steers where Steiner points go).
|
||||
pub(super) fn classify_acute(&mut self) -> Result<()> {
|
||||
let n = self.cx.points().len();
|
||||
let mut dirs: Vec<Vec<[f64; 3]>> = vec![Vec::new(); n];
|
||||
for s in &self.segments {
|
||||
let [a, b] = s.ends;
|
||||
let (pa, pb) = (self.cx.approx(a)?, self.cx.approx(b)?);
|
||||
if let Some(d) = dirs.get_mut(a as usize) {
|
||||
d.push(sub(pb, pa));
|
||||
}
|
||||
if let Some(d) = dirs.get_mut(b as usize) {
|
||||
d.push(sub(pa, pb));
|
||||
}
|
||||
}
|
||||
let acute: Vec<bool> = dirs.iter().map(|d| d.iter().enumerate().any(|(i, x)| d.iter().skip(i + 1).any(|y| dot(*x, *y) > 0.0))).collect();
|
||||
for s in &mut self.segments {
|
||||
s.acute = s.ends.map(|v| acute.get(v as usize).copied().unwrap_or(false));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Split missing segments until every one is an edge of the (Delaunay) tetrahedralization.
|
||||
/// Alg. 2 sweeps all segments until none is missing; an insertion only changes edges between
|
||||
/// vertices of the tetrahedra it deletes, so after the first sweep only the segments through
|
||||
/// those vertices are checked again.
|
||||
pub(super) fn recover_segments(&mut self) -> Result<()> {
|
||||
let mut scratch = Vec::new();
|
||||
let mut through: Vec<Vec<u32>> = vec![Vec::new(); self.cx.points().len()];
|
||||
for (s, seg) in self.segments.iter().enumerate() {
|
||||
for v in seg.ends {
|
||||
if let Some(l) = through.get_mut(v as usize) {
|
||||
l.push(s as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut queued = vec![false; self.segments.len()];
|
||||
let mut work: Vec<usize> = (0..self.segments.len()).collect();
|
||||
for _ in 0..MAX_PASSES {
|
||||
if work.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
queued.fill(false);
|
||||
let mut next: Vec<usize> = Vec::new();
|
||||
let mut requeue = |s: usize, next: &mut Vec<usize>| {
|
||||
if let Some(q) = queued.get_mut(s)
|
||||
&& !*q
|
||||
{
|
||||
*q = true;
|
||||
next.push(s);
|
||||
}
|
||||
};
|
||||
for &s in &work {
|
||||
let mut i = 0;
|
||||
while let (Some(&(tu, u)), Some(&(tv, v))) = (self.segment(s)?.splits.get(i), self.segment(s)?.splits.get(i + 1)) {
|
||||
if self.cx.has_edge(u, v, &mut scratch)? {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
self.stats.missing_subsegments += 1;
|
||||
let t = self.split_parameter(s, (tu, u), (tv, v))?;
|
||||
let [a, b] = self.segment(s)?.ends;
|
||||
let (pa, pb) = (explicit(self.cx.pt(a)?)?, explicit(self.cx.pt(b)?)?);
|
||||
let w = self.cx.push_point(GenericPoint::Lnc(Lnc::new(&pa, &pb, t)?))?;
|
||||
through.push(vec![s as u32]);
|
||||
self.steiner.push((s, t));
|
||||
if self.steiner.len() > self.options.max_steiner {
|
||||
let at = self.cx.approx(u)?;
|
||||
return Err(Error::Limit(format!(
|
||||
"more than {} Steiner points on boundary edges (the last near {at:?}): the boundary has features much thinner than its triangles are long; raise the cap or tessellate coarser",
|
||||
self.options.max_steiner
|
||||
)));
|
||||
}
|
||||
let touched = self.cx.insert(w, u)?;
|
||||
if let Some(seg) = self.segments.get_mut(s) {
|
||||
seg.splits.insert(i + 1, (t, w));
|
||||
}
|
||||
requeue(s, &mut next);
|
||||
for x in touched {
|
||||
for &r in through.get(x as usize).map(Vec::as_slice).unwrap_or_default() {
|
||||
requeue(r as usize, &mut next);
|
||||
}
|
||||
}
|
||||
// Both halves wait for the next sweep (Alg. 2 splits each missing segment
|
||||
// once per sweep).
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
work = next;
|
||||
}
|
||||
Err(Error::Limit(format!("segment recovery did not finish in {MAX_PASSES} passes")))
|
||||
}
|
||||
|
||||
pub(super) fn segment(&self, s: usize) -> Result<&Segment> {
|
||||
self.segments.get(s).ok_or_else(|| super::complex::corrupt("segment id"))
|
||||
}
|
||||
|
||||
/// Si's split parameter for the missing subsegment `(tu, u)`–`(tv, v)` of segment `s`.
|
||||
fn split_parameter(&mut self, s: usize, (tu, u): (f64, u32), (tv, v): (f64, u32)) -> Result<f64> {
|
||||
let (ends, acute) = {
|
||||
let seg = self.segment(s)?;
|
||||
(seg.ends, seg.acute)
|
||||
};
|
||||
let [a, b] = ends;
|
||||
let (pa, pb) = (self.cx.approx(a)?, self.cx.approx(b)?);
|
||||
let length = dist(pa, pb);
|
||||
let mid = 0.5 * (tu + tv);
|
||||
let r = self.reference_point(u, v)?;
|
||||
let chosen = match (r, acute) {
|
||||
(None, _) => mid,
|
||||
// Two acute ends: the first split is the midpoint.
|
||||
(_, [true, true]) if tu < 0.5 && tv > 0.5 => 0.5,
|
||||
(Some(r), acute) => {
|
||||
let pr = self.cx.approx(r)?;
|
||||
let (pu, pv) = (self.cx.approx(u)?, self.cx.approx(v)?);
|
||||
match acute {
|
||||
[false, false] => {
|
||||
let (ru, rv) = (dist(pu, pr), dist(pv, pr));
|
||||
let half = 0.5 * dist(pu, pv);
|
||||
if ru > half && rv > half {
|
||||
mid
|
||||
} else if ru <= rv {
|
||||
tu + ru / length
|
||||
} else {
|
||||
tv - rv / length
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// The acute end w: the only one, or the end of the half this
|
||||
// subsegment lies in.
|
||||
let w_is_a = match acute {
|
||||
[true, false] => true,
|
||||
[false, true] => false,
|
||||
_ => tv <= 0.5,
|
||||
};
|
||||
let pw = if w_is_a { pa } else { pb };
|
||||
let radius = dist(pw, pr);
|
||||
let tp = if w_is_a { radius / length } else { 1.0 - radius / length };
|
||||
let (t_near, t_far) = if w_is_a { (tu, tv) } else { (tv, tu) };
|
||||
let at = |t: f64| -> [f64; 3] { std::array::from_fn(|k| pa[k] + t * (pb[k] - pa[k])) };
|
||||
let p = at(tp);
|
||||
if dist(p, pr) < dist(p, at(t_far)) {
|
||||
tp
|
||||
} else {
|
||||
let d = dist(pr, p);
|
||||
let near = dist(at(t_near), p);
|
||||
if d < 0.5 * near { if t_near < tp { tp - d / length } else { tp + d / length } } else { 0.5 * (t_near + tp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let t = if chosen.is_finite() && chosen > tu && chosen < tv { chosen } else { mid };
|
||||
if !(t > tu && t < tv) {
|
||||
let at = self.cx.approx(u)?;
|
||||
return Err(Error::Failed(format!("a missing segment near {at:?} is too short to split further in double precision")));
|
||||
}
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
/// The reference point of the missing subsegment `u v`: the vertex strictly inside its
|
||||
/// diametral sphere with the largest circumcircle through `u, v` (floating point; `None` if
|
||||
/// rounding hides every encroaching vertex). Candidates are the vertices of the tetrahedra
|
||||
/// whose boxes meet the subsegment's box, connected to `u`'s star: they include every
|
||||
/// tetrahedron the subsegment crosses, whose vertices are the ones keeping it from being an
|
||||
/// edge. (On the corpus this picks the same point as searching the whole diametral ball, at a
|
||||
/// fraction of the cost.) A vertex exactly on the open subsegment makes the
|
||||
/// input invalid (a segment through a vertex), which is reported.
|
||||
fn reference_point(&mut self, u: u32, v: u32) -> Result<Option<u32>> {
|
||||
let (pu, pv) = (self.cx.approx(u)?, self.cx.approx(v)?);
|
||||
let c: [f64; 3] = std::array::from_fn(|k| 0.5 * (pu[k] + pv[k]));
|
||||
let r2 = 0.25 * dot(sub(pu, pv), sub(pu, pv));
|
||||
let lo: [f64; 3] = std::array::from_fn(|k| pu[k].min(pv[k]));
|
||||
let hi: [f64; 3] = std::array::from_fn(|k| pu[k].max(pv[k]));
|
||||
let near = self.cx.tets_near(u, lo, hi, NEAR_CAP)?;
|
||||
let mut seen: std::collections::HashSet<u32> = std::collections::HashSet::new();
|
||||
let mut best: Option<(f64, u32)> = None;
|
||||
for t in near {
|
||||
for x in self.cx.tet(t)? {
|
||||
if x == u || x == v || !seen.insert(x) {
|
||||
continue;
|
||||
}
|
||||
let px = self.cx.approx(x)?;
|
||||
let d = sub(px, c);
|
||||
if dot(d, d) >= r2 {
|
||||
continue;
|
||||
}
|
||||
let (ea, eb) = (sub(pu, px), sub(pv, px));
|
||||
let area2 = norm(cross(ea, eb));
|
||||
let radius = if area2 > 0.0 { norm(ea) * norm(eb) * norm(sub(ea, eb)) / (2.0 * area2) } else { f64::INFINITY };
|
||||
if !radius.is_finite() && self.on_segment(u, v, x)? {
|
||||
let at = self.cx.approx(x)?;
|
||||
return Err(Error::Invalid(format!("vertex {x} at {at:?} lies on a boundary edge: the surface is not a valid complex")));
|
||||
}
|
||||
if best.is_none_or(|(b, _)| radius > b) {
|
||||
best = Some((radius, x));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(best.map(|(_, x)| x))
|
||||
}
|
||||
|
||||
/// Whether `x` is exactly collinear with `u v` (it is inside their diametral ball, so then on
|
||||
/// the open segment).
|
||||
fn on_segment(&self, u: u32, v: u32, x: u32) -> Result<bool> {
|
||||
let (a, b, c) = (self.cx.pt(u)?, self.cx.pt(v)?, self.cx.pt(x)?);
|
||||
Ok(Axis::ALL.into_iter().all(|ax| generic_orient2d(a, b, c, ax).is_zero()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The explicit point of an original segment endpoint.
|
||||
fn explicit(p: &GenericPoint) -> Result<Point3> {
|
||||
p.explicit().copied().ok_or_else(|| super::complex::corrupt("a segment endpoint is not an input vertex"))
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
//! Tests of the constrained Delaunay tetrahedralization: exact conformity to every input
|
||||
//! triangle, the enclosed volume, the validity gate, and the classic polyhedra that have no
|
||||
//! tetrahedralization without Steiner points.
|
||||
|
||||
use super::meet::{in_closed_tet, meet_properly};
|
||||
use super::*;
|
||||
use crate::predicates::{GenericPoint, Lnc, Point3};
|
||||
use crate::surface::{BoundarySurface, SurfaceOptions};
|
||||
use crate::vec3::{cross, dot, norm, sub};
|
||||
|
||||
/// A closed surface from points and triangles, each triangle oriented outward from `centre`
|
||||
/// (the shapes here are star-shaped about it).
|
||||
fn surface(points: &[[f64; 3]], triangles: &[[u32; 3]], centre: [f64; 3]) -> BoundarySurface {
|
||||
let oriented: Vec<[u32; 3]> = triangles
|
||||
.iter()
|
||||
.map(|&[a, b, c]| {
|
||||
let (pa, pb, pc) = (points[a as usize], points[b as usize], points[c as usize]);
|
||||
let n = cross(sub(pb, pa), sub(pc, pa));
|
||||
if dot(n, sub(pa, centre)) > 0.0 { [a, b, c] } else { [a, c, b] }
|
||||
})
|
||||
.collect();
|
||||
let faces: Vec<u32> = (0..oriented.len() as u32).collect();
|
||||
BoundarySurface::new(points, &oriented, &faces, &SurfaceOptions::default()).unwrap()
|
||||
}
|
||||
|
||||
/// The checks every result must pass: the gate (validity, volume), every input triangle covered
|
||||
/// by boundary faces of exactly its area, each boundary face on its triangle's plane.
|
||||
fn check(s: &BoundarySurface, m: &TetMesh) {
|
||||
let report = m.gate(s.volume()).unwrap();
|
||||
assert!(report.passed(), "{report:?}");
|
||||
let mut area = vec![0.0; s.triangles().len()];
|
||||
for b in &m.boundary {
|
||||
let t = m.tets[b.tet as usize];
|
||||
let face = face_toward(t, b.opposite as usize).map(|v| m.points[v as usize]);
|
||||
area[b.triangle as usize] += 0.5 * norm(cross(sub(face[1], face[0]), sub(face[2], face[0])));
|
||||
let tri = s.triangles()[b.triangle as usize].map(|v| s.points()[v as usize]);
|
||||
let tn = cross(sub(tri[1], tri[0]), sub(tri[2], tri[0]));
|
||||
for p in face {
|
||||
let d = dot(tn, sub(p, tri[0])) / norm(tn);
|
||||
assert!(d.abs() < 1e-9, "{d}");
|
||||
}
|
||||
}
|
||||
for (i, t) in s.triangles().iter().enumerate() {
|
||||
let p = t.map(|v| s.points()[v as usize]);
|
||||
let want = 0.5 * norm(cross(sub(p[1], p[0]), sub(p[2], p[0])));
|
||||
assert!((area[i] - want).abs() <= 1e-9 * want.max(1e-300), "triangle {i}: {} vs {want}", area[i]);
|
||||
}
|
||||
}
|
||||
|
||||
fn cube() -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
|
||||
let p: Vec<[f64; 3]> = (0..8).map(|i| [(i & 1) as f64, (i >> 1 & 1) as f64, (i >> 2 & 1) as f64]).collect();
|
||||
let quads = [[0, 1, 3, 2], [4, 5, 7, 6], [0, 1, 5, 4], [2, 3, 7, 6], [0, 2, 6, 4], [1, 3, 7, 5]];
|
||||
let t = quads.iter().flat_map(|q| [[q[0], q[1], q[2]], [q[0], q[2], q[3]]]).collect();
|
||||
(p, t)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cube_conforms() {
|
||||
let (p, t) = cube();
|
||||
let s = surface(&p, &t, [0.5; 3]);
|
||||
let m = tetrahedralize(&s, &CdtOptions::default()).unwrap();
|
||||
check(&s, &m);
|
||||
// A square's diagonal is cocircular with the other two corners, so it is not strongly
|
||||
// Delaunay; at most one split per face (its midpoint) recovers it.
|
||||
assert!(m.steiner.len() <= 6, "{:?}", m.steiner);
|
||||
assert_eq!(m.boundary.len(), 12 + 2 * m.steiner.len());
|
||||
}
|
||||
|
||||
/// Schönhardt's twisted prism (Schönhardt 1928): no tetrahedralization without Steiner points.
|
||||
fn schonhardt(twist_deg: f64) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
|
||||
let mut p = Vec::new();
|
||||
for z in 0..2 {
|
||||
for i in 0..3 {
|
||||
let a = (i as f64) * 2.0 * std::f64::consts::PI / 3.0 + if z == 1 { twist_deg.to_radians() } else { 0.0 };
|
||||
p.push([a.cos(), a.sin(), z as f64 * 1.5]);
|
||||
}
|
||||
}
|
||||
let mut t = vec![[0, 1, 2], [3, 4, 5]];
|
||||
for i in 0..3u32 {
|
||||
let j = (i + 1) % 3;
|
||||
// The reflex diagonal: from bottom i to top j (the top is turned towards j).
|
||||
t.push([i, j, 3 + j]);
|
||||
t.push([i, 3 + j, 3 + i]);
|
||||
}
|
||||
(p, t)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schonhardt_prism_gets_steiner_points_and_conforms() {
|
||||
let (p, t) = schonhardt(30.0);
|
||||
let s = surface(&p, &t, [0.0, 0.0, 0.75]);
|
||||
let m = tetrahedralize(&s, &CdtOptions::default()).unwrap();
|
||||
check(&s, &m);
|
||||
assert!(!m.steiner.is_empty(), "Schönhardt's prism has no tetrahedralization without Steiner points");
|
||||
for sp in &m.steiner {
|
||||
assert!(sp.t > 0.0 && sp.t < 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// A UV sphere: many cospherical vertices.
|
||||
fn uv_sphere(rings: usize, sectors: usize) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
|
||||
let mut p = vec![[0.0, 0.0, 1.0], [0.0, 0.0, -1.0]];
|
||||
for r in 1..rings {
|
||||
let th = std::f64::consts::PI * r as f64 / rings as f64;
|
||||
for s in 0..sectors {
|
||||
let ph = 2.0 * std::f64::consts::PI * s as f64 / sectors as f64;
|
||||
p.push([th.sin() * ph.cos(), th.sin() * ph.sin(), th.cos()]);
|
||||
}
|
||||
}
|
||||
let id = |r: usize, s: usize| (2 + (r - 1) * sectors + s % sectors) as u32;
|
||||
let mut t = Vec::new();
|
||||
for s in 0..sectors {
|
||||
t.push([0, id(1, s), id(1, s + 1)]);
|
||||
t.push([1, id(rings - 1, s + 1), id(rings - 1, s)]);
|
||||
for r in 1..rings - 1 {
|
||||
t.push([id(r, s), id(r + 1, s), id(r + 1, s + 1)]);
|
||||
t.push([id(r, s), id(r + 1, s + 1), id(r, s + 1)]);
|
||||
}
|
||||
}
|
||||
(p, t)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sphere_conforms_and_is_deterministic() {
|
||||
let (p, t) = uv_sphere(12, 24);
|
||||
let s = surface(&p, &t, [0.0; 3]);
|
||||
let a = tetrahedralize(&s, &CdtOptions::default()).unwrap();
|
||||
check(&s, &a);
|
||||
let b = tetrahedralize(&s, &CdtOptions::default()).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_are_reported() {
|
||||
let (p, t) = schonhardt(30.0);
|
||||
let s = surface(&p, &t, [0.0, 0.0, 0.75]);
|
||||
let none = CdtOptions { max_steiner: 0, max_tets: 1_000_000 };
|
||||
assert!(matches!(tetrahedralize(&s, &none), Err(Error::Limit(_))));
|
||||
let few = CdtOptions { max_steiner: 1000, max_tets: 10 };
|
||||
assert!(matches!(tetrahedralize(&s, &few), Err(Error::Limit(_))));
|
||||
}
|
||||
|
||||
fn e(x: f64, y: f64, z: f64) -> GenericPoint {
|
||||
GenericPoint::Explicit(Point3::new(x, y, z).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proper_meeting_handles_coplanar_and_shared_cases() {
|
||||
let p = vec![
|
||||
e(0.0, 0.0, 0.0), // 0
|
||||
e(1.0, 0.0, 0.0), // 1
|
||||
e(0.0, 1.0, 0.0), // 2
|
||||
e(0.0, 0.0, 1.0), // 3: tet 0 1 2 3
|
||||
e(1.0, 1.0, 0.0), // 4: beside face 0 1 2, in its plane
|
||||
e(0.2, 0.2, 0.0), // 5: inside face 0 1 2
|
||||
e(0.0, 0.0, -1.0), // 6: below
|
||||
e(0.5, 0.5, 2.0), // 7: above, off the tet
|
||||
e(2.0, 2.0, 2.0), // 8: far
|
||||
e(0.1, 0.1, 0.1), // 9: inside the tet
|
||||
];
|
||||
let tet = [0, 1, 2, 3];
|
||||
let yes = |b: &[u32]| assert!(meet_properly(&p, &tet, b).unwrap(), "{b:?}");
|
||||
let no = |b: &[u32]| assert!(!meet_properly(&p, &tet, b).unwrap(), "{b:?}");
|
||||
yes(&[0, 1, 2]); // a face
|
||||
yes(&[1, 2, 4]); // coplanar, sharing edge 1 2, on the other side
|
||||
yes(&[0, 1, 6]); // shares an edge, below
|
||||
yes(&[3, 7, 8]); // shares a vertex, away
|
||||
yes(&[6, 8, 4]); // disjoint
|
||||
no(&[0, 1, 5]); // coplanar, overlapping the face 0 1 2
|
||||
no(&[1, 2, 5]); // coplanar, overlapping
|
||||
no(&[6, 7, 4]); // passes through the tet
|
||||
no(&[3, 9, 7]); // shares a vertex, enters the tet
|
||||
no(&[5, 6, 8]); // touches the face 0 1 2 at an interior point
|
||||
assert!(in_closed_tet(&p, tet, 9).unwrap());
|
||||
assert!(in_closed_tet(&p, tet, 5).unwrap());
|
||||
assert!(!in_closed_tet(&p, tet, 8).unwrap());
|
||||
// An LNC point on edge 0 3 is on the tet's boundary.
|
||||
let mut q = p.clone();
|
||||
q.push(GenericPoint::Lnc(Lnc::new(&Point3::new(0.0, 0.0, 0.0).unwrap(), &Point3::new(0.0, 0.0, 1.0).unwrap(), 0.3).unwrap()));
|
||||
assert!(in_closed_tet(&q, tet, 10).unwrap());
|
||||
assert!(!meet_properly(&q, &tet, &[10, 6, 8]).unwrap(), "touches edge 0 3 at a non-shared point");
|
||||
// Tetrahedra sharing a face meet properly; overlapping ones do not.
|
||||
assert!(meet_properly(&p, &tet, &[0, 1, 2, 6]).unwrap());
|
||||
assert!(!meet_properly(&p, &tet, &[0, 1, 2, 9]).unwrap());
|
||||
}
|
||||
|
||||
/// Two solids: a wide tetrahedron standing on the big triangle z = 0, and a small one whose top
|
||||
/// vertex is just under that triangle's centre. The Delaunay edge from the big apex to the small
|
||||
/// top pierces the big triangle while all its edges are present: only facet recovery (the
|
||||
/// gift-wrapping of a cavity) restores it.
|
||||
fn pierced() -> (Vec<[f64; 3]>, Vec<[u32; 3]>, Vec<[f64; 3]>) {
|
||||
let p = vec![
|
||||
[-10.0, -10.0, 0.0],
|
||||
[10.0, -10.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
[0.0, 0.0, 5.0],
|
||||
[-1.0, -1.0, -1.0],
|
||||
[1.0, -1.0, -1.0],
|
||||
[0.0, 1.0, -1.0],
|
||||
[0.0, 0.0, -0.01],
|
||||
];
|
||||
let t = vec![[0, 1, 2], [0, 1, 3], [1, 2, 3], [2, 0, 3], [4, 5, 6], [4, 5, 7], [5, 6, 7], [6, 4, 7]];
|
||||
(p, t, vec![[0.0, -3.0, 1.0], [0.0, -0.3, -0.7]])
|
||||
}
|
||||
|
||||
/// Orients each triangle outward from the centre of the shell it belongs to.
|
||||
fn shells(points: &[[f64; 3]], triangles: &[[u32; 3]], centres: &[[f64; 3]], per_shell: usize) -> BoundarySurface {
|
||||
let oriented: Vec<[u32; 3]> = triangles
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &[a, b, c])| {
|
||||
let centre = centres[i / per_shell];
|
||||
let (pa, pb, pc) = (points[a as usize], points[b as usize], points[c as usize]);
|
||||
let n = cross(sub(pb, pa), sub(pc, pa));
|
||||
if dot(n, sub(pa, centre)) > 0.0 { [a, b, c] } else { [a, c, b] }
|
||||
})
|
||||
.collect();
|
||||
let faces: Vec<u32> = (0..oriented.len() as u32).collect();
|
||||
BoundarySurface::new(points, &oriented, &faces, &SurfaceOptions::default()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pierced_facet_is_recovered_by_gift_wrapping() {
|
||||
let (p, t, centres) = pierced();
|
||||
let s = shells(&p, &t, ¢res, 4);
|
||||
assert_eq!(s.shells().len(), 2);
|
||||
let m = tetrahedralize(&s, &CdtOptions::default()).unwrap();
|
||||
check(&s, &m);
|
||||
assert!(m.stats.cavities > 0, "{:?}", m.stats);
|
||||
assert!(m.stats.gift_wrapped_tets > 0);
|
||||
}
|
||||
|
||||
/// A Chazelle-style polyhedron (Chazelle 1984): a block whose top is cut by V-notches along `x`
|
||||
/// and whose bottom is cut by V-notches along `y`, the notch edges almost touching. Its
|
||||
/// tetrahedralizations need many Steiner points.
|
||||
fn chazelle(notches: usize, gap: f64) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
|
||||
let n = 2 * notches;
|
||||
let at = |i: usize| i as f64 / n as f64;
|
||||
// Top height by y index: ridges at even, valleys at odd. Bottom height by x index.
|
||||
let top = |j: usize| if j.is_multiple_of(2) { 1.0 } else { 0.5 + gap / 2.0 };
|
||||
let bottom = |i: usize| if i.is_multiple_of(2) { 0.0 } else { 0.5 - gap / 2.0 };
|
||||
let mut p = Vec::new();
|
||||
let mut id = std::collections::HashMap::new();
|
||||
let mut vertex = |x: usize, y: usize, up: bool, p: &mut Vec<[f64; 3]>| -> u32 {
|
||||
*id.entry((x, y, up)).or_insert_with(|| {
|
||||
p.push([at(x), at(y), if up { top(y) } else { bottom(x) }]);
|
||||
(p.len() - 1) as u32
|
||||
})
|
||||
};
|
||||
let mut t = Vec::new();
|
||||
let quad = |q: [u32; 4], out: [f64; 3], p: &Vec<[f64; 3]>, t: &mut Vec<[u32; 3]>| {
|
||||
for tri in [[q[0], q[1], q[2]], [q[0], q[2], q[3]]] {
|
||||
let (a, b, c) = (p[tri[0] as usize], p[tri[1] as usize], p[tri[2] as usize]);
|
||||
let nrm = cross(sub(b, a), sub(c, a));
|
||||
t.push(if dot(nrm, out) > 0.0 { tri } else { [tri[0], tri[2], tri[1]] });
|
||||
}
|
||||
};
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
let q = [vertex(i, j, true, &mut p), vertex(i + 1, j, true, &mut p), vertex(i + 1, j + 1, true, &mut p), vertex(i, j + 1, true, &mut p)];
|
||||
quad(q, [0.0, 0.0, 1.0], &p, &mut t);
|
||||
let q =
|
||||
[vertex(i, j, false, &mut p), vertex(i + 1, j, false, &mut p), vertex(i + 1, j + 1, false, &mut p), vertex(i, j + 1, false, &mut p)];
|
||||
quad(q, [0.0, 0.0, -1.0], &p, &mut t);
|
||||
}
|
||||
}
|
||||
for k in 0..n {
|
||||
for (fixed, out) in [(0, -1.0), (n, 1.0)] {
|
||||
// Walls x = fixed (along y) and y = fixed (along x).
|
||||
let q = [
|
||||
vertex(fixed, k, false, &mut p),
|
||||
vertex(fixed, k + 1, false, &mut p),
|
||||
vertex(fixed, k + 1, true, &mut p),
|
||||
vertex(fixed, k, true, &mut p),
|
||||
];
|
||||
quad(q, [out, 0.0, 0.0], &p, &mut t);
|
||||
let q = [
|
||||
vertex(k, fixed, false, &mut p),
|
||||
vertex(k + 1, fixed, false, &mut p),
|
||||
vertex(k + 1, fixed, true, &mut p),
|
||||
vertex(k, fixed, true, &mut p),
|
||||
];
|
||||
quad(q, [0.0, out, 0.0], &p, &mut t);
|
||||
}
|
||||
}
|
||||
(p, t)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chazelle_polyhedron_conforms() {
|
||||
let (p, t) = chazelle(3, 0.02);
|
||||
let faces: Vec<u32> = (0..t.len() as u32).collect();
|
||||
let s = BoundarySurface::new(&p, &t, &faces, &SurfaceOptions::default()).unwrap();
|
||||
assert_eq!(s.shells().len(), 1);
|
||||
let m = tetrahedralize(&s, &CdtOptions::default()).unwrap();
|
||||
check(&s, &m);
|
||||
assert!(!m.steiner.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_and_degenerate_surfaces_are_errors() {
|
||||
// A flat "solid" (all points coplanar) bounds no volume.
|
||||
let p = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0]];
|
||||
let t = vec![[0, 1, 2], [1, 3, 2], [0, 2, 1], [1, 2, 3]];
|
||||
let faces = vec![0; 4];
|
||||
if let Ok(s) = BoundarySurface::new(&p, &t, &faces, &SurfaceOptions::default()) {
|
||||
assert!(tetrahedralize(&s, &CdtOptions::default()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_gate_reports_an_inverted_tetrahedron() {
|
||||
let (p, t) = cube();
|
||||
let s = surface(&p, &t, [0.5; 3]);
|
||||
let mut m = tetrahedralize(&s, &CdtOptions::default()).unwrap();
|
||||
m.tets[0].swap(0, 1);
|
||||
let report = m.gate(s.volume()).unwrap();
|
||||
assert_eq!(report.invalid, vec![1]);
|
||||
assert!(report.into_result().is_err());
|
||||
}
|
||||
Reference in new issue
Block a user