forked from 1b-agent/fieldcraft-upstream
Tet mesh quality: one validity criterion, Delaunay refinement, ODT, Klingner–Shewchuk, tet10
The end-to-end demo met a raw-CDT sliver that the validity gate accepted (exactly positive) and the solver refused. Now: - One validity criterion, `fieldcraft_model::admissible_jacobian` (normalized Jacobian above 1e-12), used by the implicit solver (fem `positive_inverse`), the explicit tet4/hex8 kinematics and the mesh gate, which proves it on the whole element (tet4: the solvers' own Jacobian; curved kinds: a Bernstein floor), so whatever passes the gate passes the solvers. - CDT rounding repaired by flips on the rounded mesh (Diazzi et al. 2023 §5.1) instead of the ±2-ulp nudge. - `fieldcraft-mesh::tet`: a mutable tetrahedral complex whose every change is a checked replacement (positive and admissible tetrahedra, faces matched with opposite orientation) with an exact undo log; edge removal (Klincsek), multi-face removal, Delaunay insertion with boundary faces, edge collapse; a curvature- and thickness-graded size field; constrained Delaunay refinement with encroachment splits; ODT smoothing; the Klingner–Shewchuk schedule; boundary vertices moved onto the exact B-rep surfaces at the end; tet10 with mid-edge nodes on the geometry. - `mesh.tet.generate` gains size, order, quality, quality_target, curvature_angle, grading, max_passes, max_points and reports the CDT's and the mesh's quality; `solve.probe` takes a set and reports its largest von Mises stress. - Oracle cases filleted-bracket-fillet-stress(-coarse): the fillet's peak von Mises stress on tet10 meshes of 2k and 13.5k elements, 213.3 MPa (a four-level study stays within 1.8%); the CDT cases keep the raw tetrahedralization. - Corpus quality columns: all 20 valid parts mesh and pass the gate. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
fed27d816d
commit
9469ac0853
53 files changed
+5880
-278
No files matched your search
@@ -8,43 +8,12 @@
|
||||
//! 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};
|
||||
pub(super) use crate::tet::complex::{IdMap, NONE, face_toward, sorted3};
|
||||
|
||||
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;
|
||||
|
||||
@@ -52,25 +21,6 @@ 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 {
|
||||
|
||||
@@ -34,9 +34,10 @@
|
||||
//! 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.
|
||||
//! can, on a nearly flat tetrahedron, flip it (§5.1); nearly cospherical input also gives
|
||||
//! tetrahedra that are positive yet flat to rounding. [`repair`] removes them by flips that keep
|
||||
//! the boundary (§5.1); [`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
|
||||
@@ -46,11 +47,11 @@
|
||||
mod complex;
|
||||
mod facets;
|
||||
mod meet;
|
||||
mod round;
|
||||
mod repair;
|
||||
mod segments;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub(crate) mod tests;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -122,9 +123,9 @@ pub struct CdtStats {
|
||||
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,
|
||||
/// Tetrahedra that were flat, inverted or inadmissible on the rounded coordinates and were
|
||||
/// removed by flips ([`repair`]).
|
||||
pub repaired: usize,
|
||||
}
|
||||
|
||||
/// The constrained Delaunay tetrahedralization of a closed surface (see the module docs).
|
||||
@@ -355,7 +356,7 @@ impl Recovery {
|
||||
}
|
||||
let mut stats = self.stats;
|
||||
stats.box_tets = self.cx.live_count();
|
||||
stats.rounding_adjusted = round::repair(&mut points, &tets, n)?;
|
||||
stats.repaired = repair::repair(&points, &mut tets, &mut boundary, self.options.max_tets)?;
|
||||
Ok(TetMesh { points, steiner, tets, boundary, stats })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//! Repairing the tetrahedra that rounding to doubles leaves flat, inverted or inadmissible.
|
||||
//!
|
||||
//! The exact tetrahedralization is valid, but its Steiner points are rounded to doubles, which
|
||||
//! can flip a nearly flat tetrahedron (Diazzi et al. 2023, §5.1), and nearly cospherical input
|
||||
//! (a tessellated sphere, a cylinder) gives tetrahedra that are positive yet flat to rounding:
|
||||
//! their normalized Jacobian is below FieldCraft's validity criterion
|
||||
//! ([`fieldcraft_model::admissible_jacobian`]), so a solver would refuse them. Following §5.1,
|
||||
//! such tetrahedra are removed by flips that never touch the boundary, on the rounded
|
||||
//! coordinates and decided by exact `orient3d`: edge removal (which includes the 3-2 and 4-4
|
||||
//! flips) around each of their edges and face removal (the 2-3 flip and multi-face removal)
|
||||
//! across each of their faces, each applied only when it improves the worst tetrahedron it
|
||||
//! changes ([`crate::tet::ops`]). Every boundary edge is held fixed, so every input triangle
|
||||
//! stays a union of tetrahedron faces. The passes are capped; whatever is left is reported by the
|
||||
//! validity gate, never hidden.
|
||||
|
||||
use fieldcraft_model::{normalized_jacobian, tet4_jacobian};
|
||||
|
||||
use super::BoundaryFace;
|
||||
use crate::tet::complex::{EDGES, NONE, TetComplex, VertexKind};
|
||||
use crate::tet::ops::{remove_edge_at, remove_face};
|
||||
use crate::tet::refine::feature_curves;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A tetrahedron is repaired when its normalized Jacobian is at most this (a margin of a
|
||||
/// thousand above the validity criterion, so rounding differences cannot bring it back).
|
||||
const REPAIR_BELOW: f64 = 1e3 * fieldcraft_model::MIN_NORMALIZED_JACOBIAN;
|
||||
/// Passes over the tetrahedra still to repair.
|
||||
const MAX_PASSES: usize = 8;
|
||||
|
||||
fn needs_repair(points: &[[f64; 3]], t: &[u32; 4]) -> bool {
|
||||
let p = |v: u32| points.get(v as usize).copied().unwrap_or([f64::NAN; 3]);
|
||||
!(normalized_jacobian(&tet4_jacobian(&[p(t[0]), p(t[1]), p(t[2]), p(t[3])])) > REPAIR_BELOW)
|
||||
}
|
||||
|
||||
/// Repair the rounded mesh in place; the number of tetrahedra that needed it.
|
||||
pub(super) fn repair(points: &[[f64; 3]], tets: &mut Vec<[u32; 4]>, boundary: &mut Vec<BoundaryFace>, max_tets: usize) -> Result<usize> {
|
||||
let bad = tets.iter().filter(|t| needs_repair(points, t)).count();
|
||||
if bad == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let tags: Vec<(u32, usize, u32)> = boundary.iter().map(|b| (b.tet, usize::from(b.opposite), b.triangle)).collect();
|
||||
let kinds = vec![VertexKind::Corner; points.len()];
|
||||
let mut cx = TetComplex::with_invalid(points, kinds, tets, &tags, &[], points.len(), max_tets.max(tets.len() * 2))?;
|
||||
// The segments: boundary edges between two different input triangles (never by angle).
|
||||
// Edges inside one input triangle (between its pieces) may be flipped; the triangle stays
|
||||
// covered.
|
||||
let (_, segments) = feature_curves(&cx, -2.0)?;
|
||||
for ((a, b), c) in segments {
|
||||
cx.set_curve(a, b, Some(c));
|
||||
}
|
||||
for _ in 0..MAX_PASSES {
|
||||
let targets: Vec<(u32, [u32; 4])> = cx.live_tets().filter(|(_, vs)| needs_repair(points, vs)).collect();
|
||||
if targets.is_empty() {
|
||||
break;
|
||||
}
|
||||
let mut changed = false;
|
||||
for (t, vs) in targets {
|
||||
if !cx.is_live(t) || cx.tet(t)? != vs {
|
||||
continue;
|
||||
}
|
||||
for &(i, j) in &EDGES {
|
||||
if cx.is_live(t) && cx.tet(t)? == vs && remove_edge_at(&mut cx, t, vs[i], vs[j])? {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A flat tetrahedron lying on one input triangle (two of its faces are pieces of
|
||||
// it): delete it; its two other faces cover the same piece of the triangle.
|
||||
if cx.is_live(t) && cx.tet(t)? == vs && cx.delete_boundary_tet(t)? {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if cx.is_live(t) && cx.tet(t)? == vs {
|
||||
for k in 0..4 {
|
||||
if remove_face(&mut cx, t, k)? {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let c = cx.compact()?;
|
||||
if c.source.len() != points.len() || c.source.iter().enumerate().any(|(i, &s)| s as usize != i) {
|
||||
return Err(Error::Failed("repair removed a vertex".into()));
|
||||
}
|
||||
*tets = c.tets;
|
||||
*boundary = c.boundary.into_iter().map(|(tet, k, tag)| BoundaryFace { tet, opposite: k as u8, triangle: tag }).collect();
|
||||
if boundary.iter().any(|b| b.triangle == NONE) {
|
||||
return Err(Error::Failed("repair lost a boundary face's triangle".into()));
|
||||
}
|
||||
Ok(bad)
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
//! 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())
|
||||
}
|
||||
@@ -10,7 +10,7 @@ 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 {
|
||||
pub(crate) fn surface(points: &[[f64; 3]], triangles: &[[u32; 3]], centre: [f64; 3]) -> BoundarySurface {
|
||||
let oriented: Vec<[u32; 3]> = triangles
|
||||
.iter()
|
||||
.map(|&[a, b, c]| {
|
||||
@@ -47,7 +47,7 @@ fn check(s: &BoundarySurface, m: &TetMesh) {
|
||||
}
|
||||
}
|
||||
|
||||
fn cube() -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
|
||||
pub(crate) 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();
|
||||
@@ -98,7 +98,7 @@ fn schonhardt_prism_gets_steiner_points_and_conforms() {
|
||||
}
|
||||
|
||||
/// A UV sphere: many cospherical vertices.
|
||||
fn uv_sphere(rings: usize, sectors: usize) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
|
||||
pub(crate) 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;
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod quad;
|
||||
pub mod quality;
|
||||
pub mod size;
|
||||
pub mod surface;
|
||||
pub mod tet;
|
||||
mod vec3;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
|
||||
@@ -296,6 +296,12 @@ impl Bernstein {
|
||||
Ok((a, b))
|
||||
}
|
||||
|
||||
/// The coefficients of the used slots, in a fixed order shared by every polynomial on the
|
||||
/// same domain (so the coefficients of a vector-valued polynomial's components line up).
|
||||
pub fn coefficients(&self) -> Vec<f64> {
|
||||
self.multis().iter().map(|m| self.get(m)).collect()
|
||||
}
|
||||
|
||||
/// The smallest and largest coefficient: bounds of the polynomial on its domain.
|
||||
pub fn bounds(&self) -> (f64, f64) {
|
||||
self.multis().iter().map(|m| self.get(m)).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), c| (lo.min(c), hi.max(c)))
|
||||
|
||||
@@ -32,7 +32,7 @@ pub(crate) const HEX_CORNERS: [[usize; 3]; 8] = [[0, 0, 0], [1, 0, 0], [1, 1, 0]
|
||||
/// Hex edges in the order of hex20's mid-side nodes.
|
||||
pub(crate) const HEX_EDGES: [(usize, usize); 12] = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)];
|
||||
/// Tet edges in the order of tet10's mid-side nodes.
|
||||
pub(crate) const TET_EDGES: [(usize, usize); 6] = [(0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)];
|
||||
pub const TET_EDGES: [(usize, usize); 6] = [(0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)];
|
||||
|
||||
/// A Jacobian determinant is positive when it exceeds this fraction of its largest magnitude.
|
||||
/// Anything smaller is degenerate to rounding and counts as invalid.
|
||||
@@ -280,15 +280,35 @@ impl Piece {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decide the sign of a Jacobian determinant on the whole reference domain.
|
||||
pub(crate) fn certify(det: Bernstein) -> Result<JacobianCheck> {
|
||||
/// An upper bound of `|∂x/∂ξ_a|` over the whole element for one Jacobian column (a vector
|
||||
/// polynomial in Bernstein form): the longest control vector, since the polynomial stays in the
|
||||
/// convex hull of its control points.
|
||||
fn column_norm_bound(column: &[Bernstein; 3]) -> f64 {
|
||||
let [x, y, z] = column.each_ref().map(Bernstein::coefficients);
|
||||
x.iter().zip(&y).zip(&z).map(|((a, b), c)| (a * a + b * b + c * c).sqrt()).fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
/// The determinant's floor for [`certify`] that proves FieldCraft's validity criterion
|
||||
/// ([`fieldcraft_model::admissible_jacobian`]) on the whole element: with `N` the product of the
|
||||
/// columns' norm bounds, `det J(ξ) > 2 · MIN_NORMALIZED_JACOBIAN · N` everywhere gives
|
||||
/// `det J(ξ) / Π|J_a(ξ)| > MIN_NORMALIZED_JACOBIAN` at every point, so the solvers, which check
|
||||
/// the same criterion at their integration points, accept every element the gate accepts. The
|
||||
/// factor 2 covers the rounding of the solvers' own Jacobian evaluation.
|
||||
pub(crate) fn admissible_floor(columns: &[[Bernstein; 3]; 3]) -> f64 {
|
||||
let n: f64 = columns.iter().map(column_norm_bound).product();
|
||||
2.0 * fieldcraft_model::MIN_NORMALIZED_JACOBIAN * n
|
||||
}
|
||||
|
||||
/// Decide whether a Jacobian determinant exceeds `floor` (and [`RELATIVE_TOLERANCE`] of its
|
||||
/// largest magnitude) on the whole reference domain.
|
||||
pub(crate) fn certify(det: Bernstein, floor: f64) -> Result<JacobianCheck> {
|
||||
let (lo, hi) = det.bounds();
|
||||
let volume = det.integral();
|
||||
let scale = lo.abs().max(hi.abs());
|
||||
if !scale.is_finite() || !volume.is_finite() {
|
||||
if !scale.is_finite() || !volume.is_finite() || !floor.is_finite() {
|
||||
return Err(Error::Invalid("the element's Jacobian is not a finite number".into()));
|
||||
}
|
||||
let tol = RELATIVE_TOLERANCE * scale;
|
||||
let tol = (RELATIVE_TOLERANCE * scale).max(floor);
|
||||
let ratio_of = |v: f64| if scale > 0.0 { v / scale } else { 0.0 };
|
||||
let verts = det.factors().iter().map(|f| (0..=f.dim).map(|k| simplex_vertex(k, 1).map(|x| x as f64)).collect()).collect();
|
||||
let mut stack = vec![Piece::new(det, verts, 0)];
|
||||
|
||||
@@ -14,16 +14,22 @@
|
||||
//! Inputs are hostile: coordinates must be finite and within [`fieldcraft_model::MAX_COORD`],
|
||||
//! and every metric has a defined value on degenerate and inverted elements.
|
||||
//!
|
||||
//! Linear tetrahedra take their validity from the exact `orient3d` ([`crate::predicates`]); the
|
||||
//! other kinds from the floating-point Bernstein certificate, which counts a Jacobian within
|
||||
//! [`jacobian::RELATIVE_TOLERANCE`] of zero as degenerate.
|
||||
//! Validity is FieldCraft's one criterion, shared with the solvers: an element is admissible when
|
||||
//! its normalized Jacobian `det J / Π|∂x/∂ξ_a|` exceeds
|
||||
//! [`fieldcraft_model::MIN_NORMALIZED_JACOBIAN`] ([`fieldcraft_model::admissible_jacobian`]).
|
||||
//! Linear tetrahedra take their orientation from the exact `orient3d` ([`crate::predicates`]) and
|
||||
//! must also pass the criterion on the solvers' own Jacobian (the edges of the input coordinates),
|
||||
//! so the gate and the solvers decide every tet4 alike. The other kinds use the floating-point
|
||||
//! Bernstein certificate, which proves the criterion on the whole element (and counts a Jacobian
|
||||
//! within [`jacobian::RELATIVE_TOLERANCE`] of zero as degenerate); the solvers check it only at
|
||||
//! their integration points, so whatever passes the gate passes the solvers.
|
||||
|
||||
mod bernstein;
|
||||
pub mod jacobian;
|
||||
mod report;
|
||||
pub(crate) mod shape;
|
||||
|
||||
use fieldcraft_model::{ElementKind, check_coordinates};
|
||||
use fieldcraft_model::{ElementKind, admissible_jacobian, check_coordinates, tet4_jacobian};
|
||||
|
||||
pub use jacobian::{JacobianCheck, Validity};
|
||||
pub use report::{
|
||||
@@ -156,15 +162,19 @@ fn checked(kind: ElementKind, xyz: &[[f64; 3]], p: &[[f64; 3]]) -> Result<(Optio
|
||||
{
|
||||
let [a, b, c, d]: [[f64; 3]; 4] = corners(p)?;
|
||||
let volume = shape::det(shape::sub(b, a), shape::sub(c, a), shape::sub(d, a)) / 6.0;
|
||||
// FieldCraft's one criterion, on the solvers' own Jacobian (edges of the input
|
||||
// coordinates); the exact sign settles the orientation of the rest.
|
||||
let admissible = admissible_jacobian(&tet4_jacobian(&corners(xyz)?));
|
||||
let (validity, ratio) = match sign {
|
||||
Sign::Positive => (Validity::Valid, 1.0),
|
||||
Sign::Positive if admissible => (Validity::Valid, 1.0),
|
||||
Sign::Positive | Sign::Zero => (Validity::Invalid, 0.0),
|
||||
Sign::Negative => (Validity::Invalid, -1.0),
|
||||
Sign::Zero => (Validity::Invalid, 0.0),
|
||||
};
|
||||
return Ok((None, JacobianCheck { validity, ratio, volume }));
|
||||
}
|
||||
let jacobian::Jacobian { columns, det } = jacobian::jacobian(kind, p)?;
|
||||
Ok((Some(columns), jacobian::certify(det)?))
|
||||
let floor = jacobian::admissible_floor(&columns);
|
||||
Ok((Some(columns), jacobian::certify(det, floor)?))
|
||||
}
|
||||
|
||||
/// The exact orientation of a tetrahedron (positive for node 4 on the side of face 1-2-3's
|
||||
|
||||
@@ -78,9 +78,12 @@ fn inverted_and_flat_tetrahedra() {
|
||||
assert_eq!(metric(&q, Metric::AspectRatio), f64::INFINITY);
|
||||
assert!(q.metrics.iter().all(|(_, v)| !v.is_nan()));
|
||||
|
||||
// A sliver with an exactly positive orientation is valid, however thin (exact orient3d).
|
||||
// A sliver with an exactly positive orientation but a normalized Jacobian below the one
|
||||
// criterion the solvers share is invalid; a thin one above it is valid.
|
||||
let sliver = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0.3, 0.3, 1e-30]];
|
||||
assert_eq!(element_validity(ElementKind::Tet4, &sliver).unwrap().validity, Validity::Valid);
|
||||
assert_eq!(element_validity(ElementKind::Tet4, &sliver).unwrap().validity, Validity::Invalid);
|
||||
let thin = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0.3, 0.3, 1e-9]];
|
||||
assert_eq!(element_validity(ElementKind::Tet4, &thin).unwrap().validity, Validity::Valid);
|
||||
let below = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0.3, 0.3, -1e-30]];
|
||||
assert_eq!(element_validity(ElementKind::Tet4, &below).unwrap().validity, Validity::Invalid);
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,740 @@
|
||||
//! Where boundary vertices may go: projection onto the B-rep faces and the feature curves
|
||||
//! between them.
|
||||
//!
|
||||
//! The quality stage inserts and moves boundary vertices; each one stays on its B-rep face (or
|
||||
//! feature curve) by projection. [`BoundaryGeometry`] answers the projection. Two sources:
|
||||
//! - [`AnalyticSurface`]: the exact surface of a face when the geometry kernel knows it (plane,
|
||||
//! cylinder, cone, sphere, torus), with closed-form closest points;
|
||||
//! - [`Tessellation`]: the boundary surface's own triangles of that face, the fallback for
|
||||
//! free-form faces (the projection is then accurate to the tessellation's chordal tolerance).
|
||||
//!
|
||||
//! A point on a feature curve between faces `A` and `B` is projected onto the curve's polyline
|
||||
//! in the tessellation, then refined by alternating projections onto `A` and `B` when both are
|
||||
//! analytic (von Neumann's alternating projections converge to the intersection for surfaces
|
||||
//! meeting transversally); the result is kept only when it stays close to the polyline.
|
||||
|
||||
use crate::size::SizeField;
|
||||
use crate::surface::BoundarySurface;
|
||||
|
||||
use super::measure::{self, P, add, cross, dot, norm, scale, sub};
|
||||
|
||||
/// A face's exact surface. Axes are unit vectors.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum AnalyticSurface {
|
||||
Plane {
|
||||
normal: P,
|
||||
point: P,
|
||||
},
|
||||
Cylinder {
|
||||
origin: P,
|
||||
axis: P,
|
||||
radius: f64,
|
||||
},
|
||||
/// Apex, axis pointing into the cone, half-angle in radians.
|
||||
Cone {
|
||||
apex: P,
|
||||
axis: P,
|
||||
half_angle: f64,
|
||||
},
|
||||
Sphere {
|
||||
center: P,
|
||||
radius: f64,
|
||||
},
|
||||
Torus {
|
||||
center: P,
|
||||
axis: P,
|
||||
major: f64,
|
||||
minor: f64,
|
||||
},
|
||||
}
|
||||
|
||||
fn unit(v: P) -> Option<P> {
|
||||
let l = norm(v);
|
||||
(l > 0.0 && l.is_finite()).then(|| scale(v, 1.0 / l))
|
||||
}
|
||||
|
||||
/// `(sin x, cos x)` for `|x| ≤ π` from their Taylor series (to 20 terms, accurate to rounding),
|
||||
/// so the result is the same on every platform (the C library's `sin` and `cos` are not
|
||||
/// correctly rounded and differ between platforms).
|
||||
pub fn sin_cos(x: f64) -> (f64, f64) {
|
||||
let x = x.clamp(-std::f64::consts::PI, std::f64::consts::PI);
|
||||
let (mut s, mut c) = (0.0, 0.0);
|
||||
let (mut ts, mut tc) = (x, 1.0);
|
||||
for k in 0..20 {
|
||||
s += ts;
|
||||
c += tc;
|
||||
let n = 2.0 * k as f64;
|
||||
ts *= -x * x / ((n + 2.0) * (n + 3.0));
|
||||
tc *= -x * x / ((n + 1.0) * (n + 2.0));
|
||||
}
|
||||
(s, c)
|
||||
}
|
||||
|
||||
/// Any unit vector perpendicular to `a`.
|
||||
fn perpendicular(a: P) -> P {
|
||||
let t = if a[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
|
||||
unit(cross(a, t)).unwrap_or([0.0, 0.0, 1.0])
|
||||
}
|
||||
|
||||
impl AnalyticSurface {
|
||||
/// The closest point on the (untrimmed) surface, or `None` where it is not unique (on an
|
||||
/// axis) or the surface's data is not finite.
|
||||
pub fn project(&self, p: P) -> Option<P> {
|
||||
let out = match *self {
|
||||
AnalyticSurface::Plane { normal, point } => {
|
||||
let n = unit(normal)?;
|
||||
sub(p, scale(n, dot(sub(p, point), n)))
|
||||
}
|
||||
AnalyticSurface::Cylinder { origin, axis, radius } => {
|
||||
let a = unit(axis)?;
|
||||
let d = sub(p, origin);
|
||||
let foot = add(origin, scale(a, dot(d, a)));
|
||||
let r = unit(sub(p, foot))?;
|
||||
add(foot, scale(r, radius))
|
||||
}
|
||||
AnalyticSurface::Sphere { center, radius } => add(center, scale(unit(sub(p, center))?, radius)),
|
||||
AnalyticSurface::Cone { apex, axis, half_angle } => {
|
||||
let a = unit(axis)?;
|
||||
let d = sub(p, apex);
|
||||
let along = dot(d, a);
|
||||
let radial = sub(d, scale(a, along));
|
||||
let r = unit(radial).unwrap_or_else(|| perpendicular(a));
|
||||
// The generator through p's half-plane: direction g = cos·a + sin·r.
|
||||
let (s, c) = sin_cos(half_angle);
|
||||
let g = add(scale(a, c), scale(r, s));
|
||||
let t = dot(d, g).max(0.0);
|
||||
add(apex, scale(g, t))
|
||||
}
|
||||
AnalyticSurface::Torus { center, axis, major, minor } => {
|
||||
let a = unit(axis)?;
|
||||
let d = sub(p, center);
|
||||
let radial = sub(d, scale(a, dot(d, a)));
|
||||
let r = unit(radial)?;
|
||||
let ring = add(center, scale(r, major));
|
||||
add(ring, scale(unit(sub(p, ring))?, minor))
|
||||
}
|
||||
};
|
||||
out.iter().all(|x| x.is_finite()).then_some(out)
|
||||
}
|
||||
|
||||
/// The unit normal (up to sign) at a point on the surface, if defined there.
|
||||
pub fn normal_at(&self, q: P) -> Option<P> {
|
||||
let n = match *self {
|
||||
AnalyticSurface::Plane { normal, .. } => normal,
|
||||
AnalyticSurface::Cylinder { origin, axis, .. } => {
|
||||
let a = unit(axis)?;
|
||||
let d = sub(q, origin);
|
||||
sub(d, scale(a, dot(d, a)))
|
||||
}
|
||||
AnalyticSurface::Sphere { center, .. } => sub(q, center),
|
||||
AnalyticSurface::Cone { apex, axis, .. } => {
|
||||
// Normal to both the generator through q and the circle through q.
|
||||
let a = unit(axis)?;
|
||||
let g = sub(q, apex);
|
||||
let tangent = cross(a, g);
|
||||
cross(g, tangent)
|
||||
}
|
||||
AnalyticSurface::Torus { center, axis, major, .. } => {
|
||||
let a = unit(axis)?;
|
||||
let d = sub(q, center);
|
||||
let r = unit(sub(d, scale(a, dot(d, a))))?;
|
||||
sub(q, add(center, scale(r, major)))
|
||||
}
|
||||
};
|
||||
unit(n)
|
||||
}
|
||||
|
||||
/// The smallest radius of curvature at `p` (a cone's depends on the distance to its axis).
|
||||
pub fn radius_at(&self, p: P) -> f64 {
|
||||
match *self {
|
||||
AnalyticSurface::Cone { apex, axis, half_angle } => {
|
||||
let Some(a) = unit(axis) else { return f64::INFINITY };
|
||||
let d = sub(p, apex);
|
||||
let rho = norm(sub(d, scale(a, dot(d, a))));
|
||||
// Normal curvature of the circle of radius rho: cos(α)/rho.
|
||||
rho / sin_cos(half_angle).1.abs().max(1e-12)
|
||||
}
|
||||
AnalyticSurface::Plane { .. } => f64::INFINITY,
|
||||
AnalyticSurface::Cylinder { radius, .. } | AnalyticSurface::Sphere { radius, .. } => radius.abs(),
|
||||
AnalyticSurface::Torus { minor, .. } => minor.abs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// sin 10°: faces meeting at a smaller angle are treated as tangent along their curve.
|
||||
const TRANSVERSAL_SIN: f64 = 0.173_648_177_666_930_35;
|
||||
|
||||
/// The projections the quality stage needs.
|
||||
pub trait BoundaryGeometry {
|
||||
/// The closest point to `p` on B-rep face `face`, near `p` (within a few element sizes).
|
||||
fn project_face(&self, face: u32, p: P) -> Option<P>;
|
||||
/// The closest point to `p` on the feature curve between faces `faces` (equal for a sharp
|
||||
/// edge inside one face).
|
||||
fn project_curve(&self, faces: [u32; 2], p: P) -> Option<P>;
|
||||
}
|
||||
|
||||
/// A bounding-box tree over the boundary's triangles (or its feature segments) for
|
||||
/// closest-point queries, filtered by tag.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Tree {
|
||||
nodes: Vec<Node>,
|
||||
items: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Node {
|
||||
lo: P,
|
||||
hi: P,
|
||||
/// Children (left; right is left + 1) or a leaf over `items[start..end]`.
|
||||
left: Option<usize>,
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
const LEAF: usize = 6;
|
||||
|
||||
impl Tree {
|
||||
fn build(boxes: &[(P, P)]) -> Tree {
|
||||
let mut items: Vec<u32> = (0..boxes.len() as u32).collect();
|
||||
let empty = Node { lo: [0.0; 3], hi: [0.0; 3], left: None, start: 0, end: 0 };
|
||||
let mut nodes = vec![empty];
|
||||
let mut work = vec![(0usize, 0usize, boxes.len())];
|
||||
while let Some((ni, start, end)) = work.pop() {
|
||||
let Some(slice) = items.get_mut(start..end) else { continue };
|
||||
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
|
||||
for &i in slice.iter() {
|
||||
if let Some((a, b)) = boxes.get(i as usize) {
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(a[k]);
|
||||
hi[k] = hi[k].max(b[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut node = Node { lo, hi, left: None, start, end };
|
||||
if end - start > LEAF {
|
||||
let ext = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
|
||||
let axis = if ext[0] >= ext[1] && ext[0] >= ext[2] {
|
||||
0
|
||||
} else if ext[1] >= ext[2] {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let key = |i: &u32| boxes.get(*i as usize).map_or(0.0, |(a, b)| a[axis] + b[axis]);
|
||||
let mid = (end - start) / 2;
|
||||
slice.select_nth_unstable_by(mid, |a, b| key(a).total_cmp(&key(b)).then(a.cmp(b)));
|
||||
let left = nodes.len();
|
||||
nodes.push(empty);
|
||||
nodes.push(empty);
|
||||
node.left = Some(left);
|
||||
work.push((left, start, start + mid));
|
||||
work.push((left + 1, start + mid, end));
|
||||
}
|
||||
if let Some(n) = nodes.get_mut(ni) {
|
||||
*n = node;
|
||||
}
|
||||
}
|
||||
Tree { nodes, items }
|
||||
}
|
||||
|
||||
/// The item nearest to `p` among those `accept` takes, by `distance` (squared, with the
|
||||
/// point and a fourth value carried along), searching boxes closer than the best so far.
|
||||
fn nearest4(&self, p: P, accept: &dyn Fn(u32) -> bool, distance: &dyn Fn(u32) -> (f64, [f64; 4])) -> Option<[f64; 4]> {
|
||||
let box_d2 = |n: &Node| -> f64 {
|
||||
(0..3)
|
||||
.map(|k| {
|
||||
let d = (n.lo[k] - p[k]).max(0.0).max(p[k] - n.hi[k]);
|
||||
d * d
|
||||
})
|
||||
.sum()
|
||||
};
|
||||
let mut best: Option<(f64, [f64; 4])> = None;
|
||||
let mut stack = vec![0usize];
|
||||
let mut steps = 0usize;
|
||||
while let Some(ni) = stack.pop() {
|
||||
steps += 1;
|
||||
if steps > 4 * self.nodes.len() + 16 {
|
||||
break;
|
||||
}
|
||||
let Some(n) = self.nodes.get(ni) else { continue };
|
||||
if best.is_some_and(|(d, _)| box_d2(n) > d) {
|
||||
continue;
|
||||
}
|
||||
match n.left {
|
||||
Some(l) => {
|
||||
let (a, b) = (self.nodes.get(l), self.nodes.get(l + 1));
|
||||
let (da, db) = (a.map_or(f64::INFINITY, box_d2), b.map_or(f64::INFINITY, box_d2));
|
||||
if da <= db {
|
||||
stack.push(l + 1);
|
||||
stack.push(l);
|
||||
} else {
|
||||
stack.push(l);
|
||||
stack.push(l + 1);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
for &i in self.items.get(n.start..n.end).unwrap_or_default() {
|
||||
if !accept(i) {
|
||||
continue;
|
||||
}
|
||||
let (d, q) = distance(i);
|
||||
if best.is_none_or(|(b, _)| d < b) {
|
||||
best = Some((d, q));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
best.map(|(_, q)| q)
|
||||
}
|
||||
}
|
||||
|
||||
/// The closest point on triangle `abc` (Ericson, *Real-Time Collision Detection*, §5.1.5).
|
||||
pub fn closest_on_triangle(p: P, a: P, b: P, c: P) -> P {
|
||||
let (ab, ac, ap) = (sub(b, a), sub(c, a), sub(p, a));
|
||||
let (d1, d2) = (dot(ab, ap), dot(ac, ap));
|
||||
if d1 <= 0.0 && d2 <= 0.0 {
|
||||
return a;
|
||||
}
|
||||
let bp = sub(p, b);
|
||||
let (d3, d4) = (dot(ab, bp), dot(ac, bp));
|
||||
if d3 >= 0.0 && d4 <= d3 {
|
||||
return b;
|
||||
}
|
||||
let vc = d1 * d4 - d3 * d2;
|
||||
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
|
||||
let v = d1 / (d1 - d3);
|
||||
return add(a, scale(ab, v));
|
||||
}
|
||||
let cp = sub(p, c);
|
||||
let (d5, d6) = (dot(ab, cp), dot(ac, cp));
|
||||
if d6 >= 0.0 && d5 <= d6 {
|
||||
return c;
|
||||
}
|
||||
let vb = d5 * d2 - d1 * d6;
|
||||
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
|
||||
let w = d2 / (d2 - d6);
|
||||
return add(a, scale(ac, w));
|
||||
}
|
||||
let va = d3 * d6 - d5 * d4;
|
||||
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
|
||||
let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
||||
return add(b, scale(sub(c, b), w));
|
||||
}
|
||||
let den = 1.0 / (va + vb + vc);
|
||||
let (v, w) = (vb * den, vc * den);
|
||||
let q = add(a, add(scale(ab, v), scale(ac, w)));
|
||||
if q.iter().all(|x| x.is_finite()) { q } else { a }
|
||||
}
|
||||
|
||||
/// The closest point on segment `ab`.
|
||||
pub fn closest_on_segment(p: P, a: P, b: P) -> P {
|
||||
let ab = sub(b, a);
|
||||
let l2 = dot(ab, ab);
|
||||
if !(l2 > 0.0) {
|
||||
return a;
|
||||
}
|
||||
let t = (dot(sub(p, a), ab) / l2).clamp(0.0, 1.0);
|
||||
measure::lerp(a, b, t)
|
||||
}
|
||||
|
||||
/// The boundary surface as projection target, optionally with exact surfaces per face.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tessellation {
|
||||
points: Vec<P>,
|
||||
triangles: Vec<[u32; 3]>,
|
||||
tri_face: Vec<u32>,
|
||||
tri_tree: Tree,
|
||||
/// Feature segments: vertices and the (sorted) face pair.
|
||||
segments: Vec<([u32; 2], [u32; 2])>,
|
||||
seg_tree: Tree,
|
||||
analytic: Vec<Option<AnalyticSurface>>,
|
||||
}
|
||||
|
||||
impl Tessellation {
|
||||
/// The projection target of a boundary surface; `analytic[face]` gives a face's exact
|
||||
/// surface when known.
|
||||
pub fn new(surface: &BoundarySurface, analytic: Vec<Option<AnalyticSurface>>) -> Tessellation {
|
||||
let points = surface.points().to_vec();
|
||||
let triangles = surface.triangles().to_vec();
|
||||
let tri_face = surface.tri_face().to_vec();
|
||||
let pt = |v: u32| points.get(v as usize).copied().unwrap_or([0.0; 3]);
|
||||
let boxes: Vec<(P, P)> = triangles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (a, b, c) = (pt(t[0]), pt(t[1]), pt(t[2]));
|
||||
([0, 1, 2].map(|k| a[k].min(b[k]).min(c[k])), [0, 1, 2].map(|k| a[k].max(b[k]).max(c[k])))
|
||||
})
|
||||
.collect();
|
||||
let tri_tree = Tree::build(&boxes);
|
||||
let face_of = |t: u32| tri_face.get(t as usize).copied().unwrap_or(u32::MAX);
|
||||
let segments: Vec<([u32; 2], [u32; 2])> = surface
|
||||
.feature_edges()
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let (fa, fb) = (face_of(e.triangles[0]), face_of(e.triangles[1]));
|
||||
(e.vertices, [fa.min(fb), fa.max(fb)])
|
||||
})
|
||||
.collect();
|
||||
let seg_boxes: Vec<(P, P)> = segments
|
||||
.iter()
|
||||
.map(|(v, _)| {
|
||||
let (a, b) = (pt(v[0]), pt(v[1]));
|
||||
([0, 1, 2].map(|k| a[k].min(b[k])), [0, 1, 2].map(|k| a[k].max(b[k])))
|
||||
})
|
||||
.collect();
|
||||
let seg_tree = Tree::build(&seg_boxes);
|
||||
Tessellation { points, triangles, tri_face, tri_tree, segments, seg_tree, analytic }
|
||||
}
|
||||
|
||||
fn pt(&self, v: u32) -> P {
|
||||
self.points.get(v as usize).copied().unwrap_or([0.0; 3])
|
||||
}
|
||||
|
||||
/// The exact surface of a face, if known.
|
||||
pub fn analytic(&self, face: u32) -> Option<&AnalyticSurface> {
|
||||
self.analytic.get(face as usize).and_then(|a| a.as_ref())
|
||||
}
|
||||
|
||||
/// The closest point on the face's triangles, and the longest edge of the triangle it is
|
||||
/// on (the local scale of the tessellation).
|
||||
fn on_tessellation(&self, face: u32, p: P) -> Option<(P, f64)> {
|
||||
let accept = |t: u32| self.tri_face.get(t as usize) == Some(&face);
|
||||
let dist = |t: u32| {
|
||||
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
|
||||
let (a, b, c) = (self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2]));
|
||||
let q = closest_on_triangle(p, a, b, c);
|
||||
let d = sub(q, p);
|
||||
let longest = measure::dist(a, b).max(measure::dist(b, c)).max(measure::dist(c, a));
|
||||
(dot(d, d), [q[0], q[1], q[2], longest])
|
||||
};
|
||||
self.tri_tree.nearest4(p, &accept, &dist).map(|r| ([r[0], r[1], r[2]], r[3]))
|
||||
}
|
||||
|
||||
/// The closest point on the curve's polyline, and the length of the segment it is on.
|
||||
fn on_polyline(&self, faces: [u32; 2], p: P) -> Option<(P, f64)> {
|
||||
let key = [faces[0].min(faces[1]), faces[0].max(faces[1])];
|
||||
let accept = |s: u32| self.segments.get(s as usize).is_some_and(|(_, f)| *f == key);
|
||||
let dist = |s: u32| {
|
||||
let v = self.segments.get(s as usize).map_or([0; 2], |(v, _)| *v);
|
||||
let (a, b) = (self.pt(v[0]), self.pt(v[1]));
|
||||
let q = closest_on_segment(p, a, b);
|
||||
let d = sub(q, p);
|
||||
(dot(d, d), [q[0], q[1], q[2], measure::dist(a, b)])
|
||||
};
|
||||
self.seg_tree.nearest4(p, &accept, &dist).map(|r| ([r[0], r[1], r[2]], r[3]))
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundaryGeometry for Tessellation {
|
||||
fn project_face(&self, face: u32, p: P) -> Option<P> {
|
||||
let (near, scale_len) = self.on_tessellation(face, p)?;
|
||||
match self.analytic(face) {
|
||||
// The exact surface's closest point, kept when it is near the tessellation's: the
|
||||
// tessellation deviates from its surface by far less than half its triangles' size,
|
||||
// so a point further away is a projection onto another part of the surface (the far
|
||||
// side of a cylinder) and the tessellation's point is used.
|
||||
Some(s) => {
|
||||
let q = s.project(p)?;
|
||||
let tol = (0.5 * scale_len).max(2.0 * measure::dist(p, near));
|
||||
Some(if measure::dist(q, near) <= tol { q } else { near })
|
||||
}
|
||||
None => Some(near),
|
||||
}
|
||||
}
|
||||
|
||||
fn project_curve(&self, faces: [u32; 2], p: P) -> Option<P> {
|
||||
let (near, seg_len) = self.on_polyline(faces, p)?;
|
||||
let (Some(a), Some(b)) = (self.analytic(faces[0]), self.analytic(faces[1])) else { return Some(near) };
|
||||
if faces[0] == faces[1] {
|
||||
return Some(near);
|
||||
}
|
||||
// Faces meeting tangentially (a blend's edge) define their curve badly: alternating
|
||||
// projections crawl along it. There the polyline lies on both faces to the
|
||||
// tessellation's accuracy and is kept.
|
||||
let (Some(na), Some(nb)) = (a.normal_at(near), b.normal_at(near)) else { return Some(near) };
|
||||
if norm(cross(na, nb)) < TRANSVERSAL_SIN {
|
||||
return Some(near);
|
||||
}
|
||||
let mut q = near;
|
||||
for _ in 0..40 {
|
||||
let Some(qa) = a.project(q) else { return Some(near) };
|
||||
let Some(qb) = b.project(qa) else { return Some(near) };
|
||||
let step = measure::dist(qb, q);
|
||||
q = qb;
|
||||
if step <= 1e-13 * (1.0 + norm(q)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Both surfaces must agree at the result, and it must stay near the polyline.
|
||||
let gap = a.project(q).map_or(f64::INFINITY, |qa| measure::dist(qa, q));
|
||||
let tol = (0.5 * seg_len).max(2.0 * measure::dist(p, near));
|
||||
if gap <= 1e-9 * (1.0 + norm(q)) && measure::dist(q, near) <= tol { Some(q) } else { Some(near) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Tessellation {
|
||||
/// The local thickness at each vertex: the distance to the nearest triangle facing it from
|
||||
/// across the solid (a triangle that does not touch the vertex and whose outward normal
|
||||
/// points back against the vertex's, by more than 120°). Infinite where nothing faces it.
|
||||
/// Thin walls are this thick; elements larger than it would have to be flat.
|
||||
pub fn thickness(&self) -> Vec<f64> {
|
||||
let n = self.points.len();
|
||||
let mut normal = vec![[0.0; 3]; n];
|
||||
let tri_normal: Vec<P> = self
|
||||
.triangles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (a, b, c) = (self.pt(t[0]), self.pt(t[1]), self.pt(t[2]));
|
||||
cross(sub(b, a), sub(c, a))
|
||||
})
|
||||
.collect();
|
||||
for (t, m) in self.triangles.iter().zip(&tri_normal) {
|
||||
for &v in t {
|
||||
if let Some(x) = normal.get_mut(v as usize) {
|
||||
*x = add(*x, *m);
|
||||
}
|
||||
}
|
||||
}
|
||||
(0..n as u32)
|
||||
.map(|v| {
|
||||
let p = self.pt(v);
|
||||
let Some(nv) = normal.get(v as usize).and_then(|m| unit(*m)) else { return f64::INFINITY };
|
||||
let accept = |t: u32| {
|
||||
let Some(tri) = self.triangles.get(t as usize) else { return false };
|
||||
!tri.contains(&v) && tri_normal.get(t as usize).and_then(|m| unit(*m)).is_some_and(|m| dot(m, nv) < -0.5)
|
||||
};
|
||||
let dist = |t: u32| {
|
||||
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
|
||||
let q = closest_on_triangle(p, self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2]));
|
||||
let d = sub(q, p);
|
||||
(dot(d, d), [q[0], q[1], q[2], 0.0])
|
||||
};
|
||||
// Only what lies behind the surface (inside the solid) counts.
|
||||
self.tri_tree.nearest4(p, &accept, &dist).map_or(f64::INFINITY, |r| {
|
||||
let q = [r[0], r[1], r[2]];
|
||||
if dot(sub(q, p), nv) < 0.0 { measure::dist(q, p) } else { f64::INFINITY }
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The curvature of the boundary, sampled at its vertices: for each edge inside one B-rep face,
|
||||
/// the turn of the normal between its two triangles over the distance between their centroids
|
||||
/// (a discrete estimate of the normal curvature across the edge), or the exact surface's radius.
|
||||
/// Returns the smallest radius of curvature at each surface vertex (infinite where flat).
|
||||
pub fn curvature_radii(surface: &BoundarySurface, geometry: &Tessellation) -> Vec<f64> {
|
||||
let pts = surface.points();
|
||||
let tris = surface.triangles();
|
||||
let faces = surface.tri_face();
|
||||
let mut radius = vec![f64::INFINITY; pts.len()];
|
||||
let pt = |v: u32| pts.get(v as usize).copied().unwrap_or([0.0; 3]);
|
||||
let centroid = |t: &[u32; 3]| scale(add(add(pt(t[0]), pt(t[1])), pt(t[2])), 1.0 / 3.0);
|
||||
for (i, (t, nb)) in tris.iter().zip(surface.neighbors()).enumerate() {
|
||||
let face = faces.get(i).copied().unwrap_or(u32::MAX);
|
||||
if let Some(s) = geometry.analytic(face) {
|
||||
for &v in t {
|
||||
if let Some(r) = radius.get_mut(v as usize) {
|
||||
*r = r.min(s.radius_at(pt(v)));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let n = measure::triangle_normal(pt(t[0]), pt(t[1]), pt(t[2]));
|
||||
for (e, &j) in nb.iter().enumerate() {
|
||||
let Some(u) = tris.get(j as usize) else { continue };
|
||||
if j as usize <= i || faces.get(j as usize) != Some(&face) {
|
||||
continue;
|
||||
}
|
||||
let m = measure::triangle_normal(pt(u[0]), pt(u[1]), pt(u[2]));
|
||||
// The chord between the unit normals: the turn angle to second order, and only
|
||||
// correctly rounded operations (the same on every platform).
|
||||
let turn = norm(sub(n, m));
|
||||
let d = measure::dist(centroid(t), centroid(u));
|
||||
if !(turn > 1e-6) || !(d > 0.0) {
|
||||
continue;
|
||||
}
|
||||
let r = d / turn;
|
||||
for v in [t[e], t[(e + 1) % 3]] {
|
||||
if let Some(x) = radius.get_mut(v as usize) {
|
||||
*x = x.min(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
radius
|
||||
}
|
||||
|
||||
/// A size field from a global size and the boundary's curvature, graded: the size at `x` is
|
||||
/// `min(global, min_i (h_i + g |x − x_i|))` over curvature samples `h_i = θ r_i` (θ radians per
|
||||
/// element along a curve of radius `r_i`), stored on a background grid relaxed by fast sweeping
|
||||
/// and interpolated trilinearly.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GradedSize {
|
||||
global: f64,
|
||||
lo: P,
|
||||
cell: f64,
|
||||
dims: [usize; 3],
|
||||
values: Vec<f64>,
|
||||
}
|
||||
|
||||
/// The most nodes of the background grid.
|
||||
pub const MAX_GRID_NODES: usize = 1 << 21;
|
||||
|
||||
impl GradedSize {
|
||||
/// `samples`: points with their local size. `grading`: the largest change of size per unit
|
||||
/// length.
|
||||
pub fn new(global: f64, samples: &[(P, f64)], lo: P, hi: P, grading: f64) -> GradedSize {
|
||||
let diag = measure::dist(lo, hi).max(f64::MIN_POSITIVE);
|
||||
let finest = samples.iter().map(|s| s.1).fold(global, f64::min).max(diag * 1e-4);
|
||||
let mut cell = finest.max(diag / 64.0) * 0.5;
|
||||
let dims_for = |c: f64| [0, 1, 2].map(|k| (((hi[k] - lo[k]) / c).ceil().max(1.0) as usize) + 2);
|
||||
let mut dims = dims_for(cell);
|
||||
while dims.iter().product::<usize>() > MAX_GRID_NODES {
|
||||
cell *= 1.25;
|
||||
dims = dims_for(cell);
|
||||
}
|
||||
let lo = [lo[0] - cell, lo[1] - cell, lo[2] - cell];
|
||||
let n = dims.iter().product::<usize>();
|
||||
let mut values = vec![global; n];
|
||||
let idx = |i: usize, j: usize, k: usize| (k * dims[1] + j) * dims[0] + i;
|
||||
let g = grading.max(0.0);
|
||||
// Seed: each sample's cell corners.
|
||||
for &(x, h) in samples {
|
||||
if !(h > 0.0 && h.is_finite()) {
|
||||
continue;
|
||||
}
|
||||
let c = [0, 1, 2].map(|k| ((x[k] - lo[k]) / cell).floor().max(0.0) as usize);
|
||||
for dk in 0..2 {
|
||||
for dj in 0..2 {
|
||||
for di in 0..2 {
|
||||
let (i, j, k) = (c[0] + di, c[1] + dj, c[2] + dk);
|
||||
if i >= dims[0] || j >= dims[1] || k >= dims[2] {
|
||||
continue;
|
||||
}
|
||||
let node = [lo[0] + i as f64 * cell, lo[1] + j as f64 * cell, lo[2] + k as f64 * cell];
|
||||
let v = h + g * measure::dist(node, x);
|
||||
if let Some(s) = values.get_mut(idx(i, j, k)) {
|
||||
*s = s.min(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fast sweeping: eight sweep orders, each relaxing against the three axis neighbours
|
||||
// behind the sweep direction (and diagonals through them over passes).
|
||||
if g > 0.0 {
|
||||
for _ in 0..2 {
|
||||
for dir in 0..8 {
|
||||
let rev = [dir & 1 != 0, dir & 2 != 0, dir & 4 != 0];
|
||||
for kk in 0..dims[2] {
|
||||
let k = if rev[2] { dims[2] - 1 - kk } else { kk };
|
||||
for jj in 0..dims[1] {
|
||||
let j = if rev[1] { dims[1] - 1 - jj } else { jj };
|
||||
for ii in 0..dims[0] {
|
||||
let i = if rev[0] { dims[0] - 1 - ii } else { ii };
|
||||
let mut best = values.get(idx(i, j, k)).copied().unwrap_or(global);
|
||||
let back = |x: usize, r: bool, n: usize| -> Option<usize> {
|
||||
if r { (x + 1 < n).then_some(x + 1) } else { x.checked_sub(1) }
|
||||
};
|
||||
if let Some(pi) = back(i, rev[0], dims[0]) {
|
||||
best = best.min(values.get(idx(pi, j, k)).copied().unwrap_or(global) + g * cell);
|
||||
}
|
||||
if let Some(pj) = back(j, rev[1], dims[1]) {
|
||||
best = best.min(values.get(idx(i, pj, k)).copied().unwrap_or(global) + g * cell);
|
||||
}
|
||||
if let Some(pk) = back(k, rev[2], dims[2]) {
|
||||
best = best.min(values.get(idx(i, j, pk)).copied().unwrap_or(global) + g * cell);
|
||||
}
|
||||
if let Some(s) = values.get_mut(idx(i, j, k)) {
|
||||
*s = best;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GradedSize { global, lo, cell, dims, values }
|
||||
}
|
||||
|
||||
fn at(&self, i: usize, j: usize, k: usize) -> f64 {
|
||||
let i = i.min(self.dims[0] - 1);
|
||||
let j = j.min(self.dims[1] - 1);
|
||||
let k = k.min(self.dims[2] - 1);
|
||||
self.values.get((k * self.dims[1] + j) * self.dims[0] + i).copied().unwrap_or(self.global)
|
||||
}
|
||||
}
|
||||
|
||||
impl SizeField for GradedSize {
|
||||
fn size(&self, p: [f64; 3]) -> f64 {
|
||||
let f = [0, 1, 2].map(|k| ((p[k] - self.lo[k]) / self.cell).clamp(0.0, (self.dims[k] - 1) as f64));
|
||||
if f.iter().any(|x| !x.is_finite()) {
|
||||
return self.global;
|
||||
}
|
||||
let c = f.map(|x| (x.floor() as usize).min(usize::MAX - 1));
|
||||
let t = [f[0] - c[0] as f64, f[1] - c[1] as f64, f[2] - c[2] as f64];
|
||||
let mut h = 0.0;
|
||||
for dk in 0..2 {
|
||||
for dj in 0..2 {
|
||||
for di in 0..2 {
|
||||
let w = (if di == 1 { t[0] } else { 1.0 - t[0] })
|
||||
* (if dj == 1 { t[1] } else { 1.0 - t[1] })
|
||||
* (if dk == 1 { t[2] } else { 1.0 - t[2] });
|
||||
h += w * self.at(c[0] + di, c[1] + dj, c[2] + dk);
|
||||
}
|
||||
}
|
||||
}
|
||||
if h > 0.0 && h.is_finite() { h.min(self.global) } else { self.global }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn analytic_projections() {
|
||||
let cyl = AnalyticSurface::Cylinder { origin: [0.0; 3], axis: [0.0, 0.0, 2.0], radius: 3.0 };
|
||||
let q = cyl.project([1.0, 1.0, 5.0]).unwrap();
|
||||
assert!((norm([q[0], q[1], 0.0]) - 3.0).abs() < 1e-12 && (q[2] - 5.0).abs() < 1e-12);
|
||||
let s = AnalyticSurface::Sphere { center: [1.0, 0.0, 0.0], radius: 2.0 };
|
||||
assert!((measure::dist(s.project([5.0, 0.0, 0.0]).unwrap(), [3.0, 0.0, 0.0])) < 1e-12);
|
||||
let pl = AnalyticSurface::Plane { normal: [0.0, 0.0, 1.0], point: [0.0, 0.0, 2.0] };
|
||||
assert_eq!(pl.project([1.0, 2.0, 7.0]), Some([1.0, 2.0, 2.0]));
|
||||
let t = AnalyticSurface::Torus { center: [0.0; 3], axis: [0.0, 0.0, 1.0], major: 5.0, minor: 1.0 };
|
||||
let q = t.project([7.0, 0.0, 0.0]).unwrap();
|
||||
assert!(measure::dist(q, [6.0, 0.0, 0.0]) < 1e-12);
|
||||
let cone = AnalyticSurface::Cone { apex: [0.0; 3], axis: [0.0, 0.0, 1.0], half_angle: std::f64::consts::FRAC_PI_4 };
|
||||
let q = cone.project([2.0, 0.0, 0.0]).unwrap();
|
||||
assert!((q[0] - q[2]).abs() < 1e-12 && (q[0] - 1.0).abs() < 1e-12, "{q:?}");
|
||||
assert!(cyl.project([0.0, 0.0, 1.0]).is_none());
|
||||
for x in [-3.0, -1.0, 0.0, 0.3, 1.5, 3.1] {
|
||||
let (s, c) = sin_cos(x);
|
||||
assert!((s - f64::sin(x)).abs() < 1e-15 && (c - f64::cos(x)).abs() < 1e-15, "{x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closest_points() {
|
||||
let (a, b, c) = ([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
|
||||
assert!(measure::dist(closest_on_triangle([0.2, 0.2, 3.0], a, b, c), [0.2, 0.2, 0.0]) < 1e-15);
|
||||
assert_eq!(closest_on_triangle([-1.0, -1.0, 0.0], a, b, c), a);
|
||||
let q = closest_on_triangle([1.0, 1.0, 0.0], a, b, c);
|
||||
assert!(measure::dist(q, [0.5, 0.5, 0.0]) < 1e-15);
|
||||
assert_eq!(closest_on_segment([0.5, 2.0, 0.0], a, b), [0.5, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graded_size_grows_away_from_samples() {
|
||||
let f = GradedSize::new(10.0, &[([0.0; 3], 1.0)], [0.0; 3], [20.0, 20.0, 20.0], 0.5);
|
||||
let near = f.size([0.0; 3]);
|
||||
let mid = f.size([4.0, 0.0, 0.0]);
|
||||
let far = f.size([20.0, 20.0, 20.0]);
|
||||
assert!((1.0..1.6).contains(&near), "{near}");
|
||||
assert!((mid - 3.0).abs() < 0.8, "{mid}");
|
||||
assert_eq!(far, 10.0);
|
||||
assert!(f.size([f64::NAN, 0.0, 0.0]) > 0.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
//! Aggressive mesh improvement: the schedule of Klingner and Shewchuk (IMR 2007, §4.2,
|
||||
//! Figure 4) over smoothing, topological transformations and vertex insertion, monotone in the
|
||||
//! worst quality (the minimum dihedral sine).
|
||||
//!
|
||||
//! Each pass smooths; if that is not enough progress, a topological pass (edge removal,
|
||||
//! multi-face removal, and deletion of flat tetrahedra lying on one B-rep face) follows; if still
|
||||
//! not enough, an insertion pass tries new vertices in the worst tetrahedra (a boundary face's
|
||||
//! barycentre first, then the tetrahedron's, then the midpoints of its boundary edges). An
|
||||
//! insertion is a composite operation: the vertex goes in with a Delaunay cavity, is smoothed,
|
||||
//! and topological passes and smoothing run around it; if the worst of the tetrahedra it made
|
||||
//! or changed is not better than the worst of those it removed, everything is rolled back
|
||||
//! ([`super::complex::Journal`]). Progress is judged on the worst quality and seven thresholded
|
||||
//! means (§4.2); the schedule stops after three passes without progress, or at its pass and
|
||||
//! insertion caps (reported).
|
||||
//!
|
||||
//! The paper chooses the inserted vertex's cavity by an optimal graph cut (§3.4); this
|
||||
//! implementation uses the Delaunay cavity followed by the same smoothing and topological
|
||||
//! repair, which the paper's own composite operation relies on to make insertion pay off.
|
||||
|
||||
use crate::Result;
|
||||
use crate::predicates::Point3;
|
||||
|
||||
use super::complex::{EDGES, IdMap, NONE, Step, TetComplex, VertexKind, Walk, face_toward, sorted4};
|
||||
use super::measure::{self, P};
|
||||
use super::ops::{self, Site, remove_edge_at, remove_face, tet_quality};
|
||||
use super::refine::{Context, MAX_CAVITY};
|
||||
use super::smooth::smooth_vertex;
|
||||
|
||||
/// Settings of the schedule.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ImproveOptions {
|
||||
pub max_passes: usize,
|
||||
pub max_insertions: usize,
|
||||
/// Tetrahedra at or above this quality are left alone by the topological and smoothing
|
||||
/// passes (default sin 40°, the paper's desperation threshold: a mesh whose tetrahedra all
|
||||
/// have dihedral angles between 40° and 140° needs nothing more).
|
||||
pub good: f64,
|
||||
/// Insertion runs every pass while the worst quality is below this (sin 30°: dihedral
|
||||
/// angles between 30° and 150°).
|
||||
pub target: f64,
|
||||
}
|
||||
|
||||
impl Default for ImproveOptions {
|
||||
fn default() -> Self {
|
||||
ImproveOptions { max_passes: 10, max_insertions: 50_000, good: SIN_40, target: 0.5 }
|
||||
}
|
||||
}
|
||||
|
||||
/// What the schedule did.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct ImproveStats {
|
||||
pub passes: usize,
|
||||
pub smoothed: usize,
|
||||
pub topological: usize,
|
||||
pub inserted: usize,
|
||||
/// The worst quality (smallest dihedral sine) after each pass.
|
||||
pub worst: Vec<f64>,
|
||||
pub stopped: Option<String>,
|
||||
}
|
||||
|
||||
/// sin 1°, 5°, 10°, 15°, 25°, 35°, 45° (the thresholds of §4.2), as literals so they are the same
|
||||
/// everywhere.
|
||||
pub const SIN_DEG: [f64; 7] = [
|
||||
0.017_452_406_437_283_51,
|
||||
0.087_155_742_747_658_17,
|
||||
0.173_648_177_666_930_33,
|
||||
0.258_819_045_102_520_74,
|
||||
0.422_618_261_740_699_44,
|
||||
0.573_576_436_351_046,
|
||||
std::f64::consts::FRAC_1_SQRT_2,
|
||||
];
|
||||
|
||||
/// Most tetrahedra one insertion pass tries (the worst first).
|
||||
pub const MAX_INSERTION_LIST: usize = 2000;
|
||||
|
||||
/// sin 40°.
|
||||
pub const SIN_40: f64 = 0.642_787_609_686_539_3;
|
||||
|
||||
/// The worst quality and the seven thresholded means.
|
||||
fn indicators(cx: &TetComplex) -> Result<[f64; 8]> {
|
||||
let mut out = [0.0; 8];
|
||||
out[0] = f64::INFINITY;
|
||||
let mut n = 0usize;
|
||||
for (_, vs) in cx.live_tets() {
|
||||
let q = tet_quality(cx, vs)?;
|
||||
out[0] = out[0].min(q);
|
||||
for (i, s) in SIN_DEG.iter().enumerate() {
|
||||
out[i + 1] += q.min(*s);
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
for m in out.iter_mut().skip(1) {
|
||||
*m /= n.max(1) as f64;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn progressed(now: &[f64; 8], before: &[f64; 8]) -> bool {
|
||||
now[0] > before[0] || now.iter().zip(before).skip(1).any(|(a, b)| *a >= b + 1e-4)
|
||||
}
|
||||
|
||||
/// Smooth every movable vertex of a tetrahedron below `good`. Returns the moves.
|
||||
fn smoothing_pass(cx: &mut TetComplex, ctx: &Context, good: f64) -> Result<usize> {
|
||||
let mut want = vec![false; cx.point_count()];
|
||||
for (_, vs) in cx.live_tets() {
|
||||
if tet_quality(cx, vs)? < good {
|
||||
for v in vs {
|
||||
if let Some(w) = want.get_mut(v as usize) {
|
||||
*w = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut moved = 0;
|
||||
for (v, w) in want.iter().enumerate() {
|
||||
if *w && smooth_vertex(cx, ctx, v as u32)? {
|
||||
moved += 1;
|
||||
}
|
||||
}
|
||||
Ok(moved)
|
||||
}
|
||||
|
||||
/// Delete a tetrahedron capping two boundary faces of one B-rep face that fold by at most 20°
|
||||
/// ([`ops::BOUNDARY_FLIP_COS`]): its dihedral angle at their shared edge is at least 160°, and
|
||||
/// its two other faces cover the same piece of surface.
|
||||
fn delete_flat(cx: &mut TetComplex, t: u32) -> Result<bool> {
|
||||
let vs = cx.tet(t)?;
|
||||
let (ns, tags) = (cx.adj(t)?, cx.tags(t)?);
|
||||
let bnd: Vec<usize> = (0..4).filter(|&k| ns[k] == NONE).collect();
|
||||
let &[i, j] = bnd.as_slice() else { return Ok(false) };
|
||||
if tags[i] != tags[j] {
|
||||
return Ok(false);
|
||||
}
|
||||
let normal = |k: usize| -> Result<P> {
|
||||
let f = face_toward(vs, k);
|
||||
Ok(measure::triangle_normal(cx.xyz(f[0])?, cx.xyz(f[1])?, cx.xyz(f[2])?))
|
||||
};
|
||||
// The inward normals of the two faces of such a cap point nearly the same way.
|
||||
let (a, b) = (normal(i)?, normal(j)?);
|
||||
if !(measure::dot(a, b) >= ops::BOUNDARY_FLIP_COS) {
|
||||
return Ok(false);
|
||||
}
|
||||
cx.delete_boundary_tet(t)
|
||||
}
|
||||
|
||||
/// A flat tetrahedron spanning two boundary faces of different B-rep faces that meet at a
|
||||
/// nearly flat crease (two faces tangent along their curve): its two apexes lie almost on the
|
||||
/// crease, so no flip or move fixes it. Collapse an apex face vertex onto an end of the crease
|
||||
/// edge ([`ops::collapse`]), which takes the tetrahedron away.
|
||||
fn collapse_into_crease(cx: &mut TetComplex, t: u32) -> Result<bool> {
|
||||
let vs = cx.tet(t)?;
|
||||
if tet_quality(cx, vs)? > 0.1 {
|
||||
return Ok(false);
|
||||
}
|
||||
let (ns, tags) = (cx.adj(t)?, cx.tags(t)?);
|
||||
let bnd: Vec<usize> = (0..4).filter(|&k| ns[k] == NONE).collect();
|
||||
let &[i, j] = bnd.as_slice() else { return Ok(false) };
|
||||
if tags[i] == tags[j] {
|
||||
return Ok(false);
|
||||
}
|
||||
// The faces opposite i and j share the edge of the two other vertices; vs[i] and vs[j] are
|
||||
// the apexes (vs[i] lies on face j, vs[j] on face i).
|
||||
let edge: Vec<u32> = (0..4).filter(|&k| k != i && k != j).map(|k| vs[k]).collect();
|
||||
let (ex, longest) = {
|
||||
let x = cx.coords(vs)?;
|
||||
(edge.clone(), measure::edge_range(&x).1)
|
||||
};
|
||||
for apex in [vs[i], vs[j]] {
|
||||
if !matches!(cx.kind(apex), VertexKind::Face(_)) {
|
||||
continue;
|
||||
}
|
||||
for &e in &ex {
|
||||
if ops::collapse(cx, apex, e, 2.0 * longest, 0.0)? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Try to remove each tetrahedron of `list`: its edges, then its faces. Returns the changes.
|
||||
pub fn topological_pass(cx: &mut TetComplex, list: &[u32]) -> Result<usize> {
|
||||
let mut changes = 0;
|
||||
for &t in list {
|
||||
if !cx.is_live(t) {
|
||||
continue;
|
||||
}
|
||||
let vs = cx.tet(t)?;
|
||||
if delete_flat(cx, t)? || collapse_into_crease(cx, t)? {
|
||||
changes += 1;
|
||||
continue;
|
||||
}
|
||||
let mut gone = false;
|
||||
for &(i, j) in &EDGES {
|
||||
if remove_edge_at(cx, t, vs[i], vs[j])? {
|
||||
gone = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if gone {
|
||||
changes += 1;
|
||||
continue;
|
||||
}
|
||||
for k in 0..4 {
|
||||
if remove_face(cx, t, k)? {
|
||||
changes += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
fn below(cx: &TetComplex, good: f64) -> Result<Vec<u32>> {
|
||||
let mut out = Vec::new();
|
||||
for (t, vs) in cx.live_tets() {
|
||||
if tet_quality(cx, vs)? < good {
|
||||
out.push(t);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The quality changes since journal step `len`: the worst quality of the tetrahedra removed
|
||||
/// (at their coordinates before) and of those made or moved (now).
|
||||
fn change_quality(cx: &mut TetComplex, len: usize) -> Result<(f64, f64)> {
|
||||
let steps: Vec<Step> = cx.journal.steps.get(len..).unwrap_or_default().to_vec();
|
||||
// Earliest position of each moved vertex.
|
||||
let mut first: IdMap<u32, Point3> = IdMap::default();
|
||||
for s in &steps {
|
||||
if let Step::Moved { v, from } = s {
|
||||
first.entry(*v).or_insert(*from);
|
||||
}
|
||||
}
|
||||
let old_xyz = |cx: &TetComplex, v: u32| -> Result<P> { first.get(&v).map_or_else(|| cx.xyz(v), |p| Ok(p.coords())) };
|
||||
// Net removed and added tetrahedra by sorted vertices.
|
||||
let mut net: IdMap<[u32; 4], (i32, [u32; 4])> = IdMap::default();
|
||||
let key = sorted4;
|
||||
for s in &steps {
|
||||
match s {
|
||||
Step::Replaced { removed, added, .. } => {
|
||||
for r in removed {
|
||||
net.entry(key(r.vs)).or_insert((0, r.vs)).0 -= 1;
|
||||
}
|
||||
for &vs in added {
|
||||
net.entry(key(vs)).or_insert((0, vs)).0 += 1;
|
||||
}
|
||||
}
|
||||
Step::Deleted { removed, .. } => net.entry(key(removed.vs)).or_insert((0, removed.vs)).0 -= 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let (mut before, mut after) = (f64::INFINITY, f64::INFINITY);
|
||||
let mut keys: Vec<[u32; 4]> = net.keys().copied().collect();
|
||||
keys.sort_unstable();
|
||||
for k in &keys {
|
||||
let Some(&(c, vs)) = net.get(k) else { continue };
|
||||
if c < 0 {
|
||||
let x = [old_xyz(cx, vs[0])?, old_xyz(cx, vs[1])?, old_xyz(cx, vs[2])?, old_xyz(cx, vs[3])?];
|
||||
before = before.min(measure::quality(&x));
|
||||
}
|
||||
}
|
||||
// The tetrahedra made, and those around moved vertices.
|
||||
for k in &keys {
|
||||
if let Some(&(c, vs)) = net.get(k)
|
||||
&& c > 0
|
||||
{
|
||||
after = after.min(tet_quality(cx, vs)?);
|
||||
}
|
||||
}
|
||||
let mut moved: Vec<u32> = first.keys().copied().collect();
|
||||
moved.sort_unstable();
|
||||
for v in moved {
|
||||
if !cx.is_used(v) {
|
||||
continue;
|
||||
}
|
||||
for t in cx.star(v)? {
|
||||
let vs = cx.tet(t)?;
|
||||
if net.get(&key(vs)).is_some_and(|e| e.0 > 0) {
|
||||
continue;
|
||||
}
|
||||
after = after.min(tet_quality(cx, vs)?);
|
||||
let x = [old_xyz(cx, vs[0])?, old_xyz(cx, vs[1])?, old_xyz(cx, vs[2])?, old_xyz(cx, vs[3])?];
|
||||
before = before.min(measure::quality(&x));
|
||||
}
|
||||
}
|
||||
Ok((before, after))
|
||||
}
|
||||
|
||||
/// The composite insertion of §3.5 (see the module docs). Returns whether it was kept.
|
||||
fn attempt_insert(cx: &mut TetComplex, ctx: &Context, p: Point3, kind: VertexKind, site: Site) -> Result<bool> {
|
||||
let was = cx.journal.recording;
|
||||
cx.journal.recording = true;
|
||||
let len = cx.journal.steps.len();
|
||||
let kept = (|| -> Result<bool> {
|
||||
let Some(v) = ops::insert_delaunay(cx, p, kind, site, MAX_CAVITY)? else { return Ok(false) };
|
||||
for _ in 0..4 {
|
||||
smooth_vertex(cx, ctx, v)?;
|
||||
if !cx.is_used(v) {
|
||||
break;
|
||||
}
|
||||
let star = cx.star(v)?;
|
||||
if topological_pass(cx, &star)? == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if cx.is_used(v) {
|
||||
let mut around: Vec<u32> = cx.neighbours(v)?;
|
||||
around.push(v);
|
||||
for u in around {
|
||||
smooth_vertex(cx, ctx, u)?;
|
||||
}
|
||||
}
|
||||
let (before, after) = change_quality(cx, len)?;
|
||||
Ok(after > before && after.is_finite())
|
||||
})();
|
||||
let keep = match kept {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
cx.rollback(len)?;
|
||||
cx.journal.recording = was;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
if !keep {
|
||||
cx.rollback(len)?;
|
||||
}
|
||||
if !was {
|
||||
cx.journal.steps.truncate(len);
|
||||
}
|
||||
cx.journal.recording = was;
|
||||
Ok(keep)
|
||||
}
|
||||
|
||||
/// Try inserting a vertex in each tetrahedron of `list` (§4.2). Returns the insertions kept.
|
||||
fn insertion_pass(cx: &mut TetComplex, ctx: &Context, list: &[u32], budget: usize, good: f64) -> Result<usize> {
|
||||
let mut kept = 0;
|
||||
for &t in list {
|
||||
if kept >= budget {
|
||||
break;
|
||||
}
|
||||
if !cx.is_live(t) {
|
||||
continue;
|
||||
}
|
||||
let vs = cx.tet(t)?;
|
||||
if tet_quality(cx, vs)? >= good {
|
||||
continue;
|
||||
}
|
||||
let ns = cx.adj(t)?;
|
||||
let tags = cx.tags(t)?;
|
||||
let mut done = false;
|
||||
// A tetrahedron lying on two boundary faces that meet at a nearly flat edge (two faces
|
||||
// that are tangent along their curve) is flat however its vertices move: the edge needs
|
||||
// a second tetrahedron. A vertex inside, off the edge's midpoint along the inward
|
||||
// normal, splits the edge's dihedral angle.
|
||||
let bnd: Vec<usize> = (0..4).filter(|&k| ns[k] == NONE).collect();
|
||||
if let &[i, j] = bnd.as_slice() {
|
||||
let edge: Vec<u32> = (0..4).filter(|&k| k != i && k != j).map(|k| vs[k]).collect();
|
||||
if let &[a, b] = edge.as_slice() {
|
||||
let n = |k: usize| -> Result<P> {
|
||||
let f = face_toward(vs, k);
|
||||
Ok(measure::triangle_normal(cx.xyz(f[0])?, cx.xyz(f[1])?, cx.xyz(f[2])?))
|
||||
};
|
||||
let inward = measure::add(n(i)?, n(j)?);
|
||||
let len = measure::norm(inward);
|
||||
let (pa, pb) = (cx.xyz(a)?, cx.xyz(b)?);
|
||||
let l = measure::dist(pa, pb);
|
||||
// Unit inward normals within 25° of each other: |n_i + n_j| > 2 cos 12.5°.
|
||||
if len > 1.952 {
|
||||
let target = measure::add(measure::mid(pa, pb), measure::scale(inward, 0.4 * l / len));
|
||||
if let Ok(p) = Point3::try_from(target)
|
||||
&& let Some(Walk::Inside(at)) = cx.walk(t, &p, 1 << 12)?
|
||||
&& attempt_insert(cx, ctx, p, VertexKind::Interior, Site::Interior(at))?
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Boundary faces' barycentres.
|
||||
for k in 0..4 {
|
||||
if done || ns[k] != NONE || tags[k] == NONE {
|
||||
continue;
|
||||
}
|
||||
let f = face_toward(vs, k);
|
||||
let g = measure::scale(measure::add(measure::add(cx.xyz(f[0])?, cx.xyz(f[1])?), cx.xyz(f[2])?), 1.0 / 3.0);
|
||||
let kind = VertexKind::Face(tags[k]);
|
||||
if let Some(p) = ctx.place(kind, g)
|
||||
&& cx.is_live(t)
|
||||
&& attempt_insert(cx, ctx, p, kind, Site::Face(t, k))?
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
// The barycentre.
|
||||
if !done && cx.is_live(t) && cx.tet(t)? == vs {
|
||||
let g = measure::centroid(&cx.coords(vs)?);
|
||||
if let Ok(p) = Point3::try_from(g)
|
||||
&& attempt_insert(cx, ctx, p, VertexKind::Interior, Site::Interior(t))?
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
// Boundary edges' midpoints.
|
||||
if !done && cx.is_live(t) && cx.tet(t)? == vs {
|
||||
for &(i, j) in &EDGES {
|
||||
let (a, b) = (vs[i], vs[j]);
|
||||
let Some(ring) = cx.edge_ring(a, b)? else { continue };
|
||||
if ring.closed {
|
||||
continue;
|
||||
}
|
||||
let tag = ring
|
||||
.tets
|
||||
.first()
|
||||
.and_then(|&r| {
|
||||
let rv = cx.tet(r).ok()?;
|
||||
let rt = cx.tags(r).ok()?;
|
||||
let rn = cx.adj(r).ok()?;
|
||||
(0..4).find(|&k| rn[k] == NONE && face_toward(rv, k).contains(&a) && face_toward(rv, k).contains(&b)).map(|k| rt[k])
|
||||
})
|
||||
.unwrap_or(NONE);
|
||||
let kind = match cx.curve(a, b) {
|
||||
Some(c) => VertexKind::Curve(c),
|
||||
None if tag != NONE => VertexKind::Face(tag),
|
||||
None => continue,
|
||||
};
|
||||
let m = measure::mid(cx.xyz(a)?, cx.xyz(b)?);
|
||||
if let Some(p) = ctx.place(kind, m)
|
||||
&& attempt_insert(cx, ctx, p, kind, Site::Edge(a, b))?
|
||||
{
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
if !cx.is_live(t) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
kept += usize::from(done);
|
||||
}
|
||||
Ok(kept)
|
||||
}
|
||||
|
||||
/// The improvement schedule (see the module docs). Each pass smooths and runs a topological
|
||||
/// pass over the tetrahedra below the threshold, then an insertion pass when the worst
|
||||
/// tetrahedron did not improve; it stops after three passes without progress (§4.2).
|
||||
pub fn improve(cx: &mut TetComplex, ctx: &Context, o: &ImproveOptions) -> Result<ImproveStats> {
|
||||
let mut stats = ImproveStats::default();
|
||||
stats.smoothed += smoothing_pass(cx, ctx, o.good)?;
|
||||
let list = below(cx, o.good)?;
|
||||
stats.topological += topological_pass(cx, &list)?;
|
||||
let mut failed = 0;
|
||||
// A pass ends where the next begins: its indicators are the next one's starting point.
|
||||
let mut carried: Option<[f64; 8]> = None;
|
||||
while failed < 3 {
|
||||
if stats.passes >= o.max_passes {
|
||||
stats.stopped = Some(format!("improvement stopped after {} passes", o.max_passes));
|
||||
break;
|
||||
}
|
||||
stats.passes += 1;
|
||||
let before = match carried.take() {
|
||||
Some(b) => b,
|
||||
None => indicators(cx)?,
|
||||
};
|
||||
stats.smoothed += smoothing_pass(cx, ctx, o.good)?;
|
||||
let list = below(cx, o.good)?;
|
||||
stats.topological += topological_pass(cx, &list)?;
|
||||
// Insertion when smoothing and flips did not make enough progress (§4.2), or left a
|
||||
// tetrahedron below the target.
|
||||
let mid = indicators(cx)?;
|
||||
if (!progressed(&mid, &before) || mid[0] < o.target) && stats.inserted < o.max_insertions {
|
||||
// The worst 3.5% of the tetrahedra, or (desperation pass, §4.2) every one with a
|
||||
// dihedral angle below 40° or above 140° (sine below sin 40°).
|
||||
let mut scored: Vec<(f64, u32)> = Vec::new();
|
||||
for (t, vs) in cx.live_tets() {
|
||||
scored.push((tet_quality(cx, vs)?, t));
|
||||
}
|
||||
scored.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
|
||||
let list: Vec<u32> = if failed == 1 {
|
||||
scored.iter().filter(|s| s.0 < SIN_40).take(MAX_INSERTION_LIST).map(|s| s.1).collect()
|
||||
} else {
|
||||
let n = (scored.len() * 35).div_ceil(1000).clamp(1, MAX_INSERTION_LIST);
|
||||
scored.iter().take(n).map(|s| s.1).collect()
|
||||
};
|
||||
let budget = o.max_insertions - stats.inserted;
|
||||
stats.inserted += insertion_pass(cx, ctx, &list, budget, o.good)?;
|
||||
}
|
||||
let now = indicators(cx)?;
|
||||
carried = Some(now);
|
||||
stats.worst.push(now[0]);
|
||||
if progressed(&now, &before) {
|
||||
failed = 0;
|
||||
} else {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
if stats.stopped.is_none() && stats.inserted >= o.max_insertions {
|
||||
stats.stopped = Some(format!("improvement stopped at {} inserted vertices", o.max_insertions));
|
||||
}
|
||||
Ok(stats)
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//! Tetrahedron measures for the quality stage, on floating-point coordinates.
|
||||
//!
|
||||
//! Decisions use only `+ − × ÷ √` (correctly rounded in IEEE 754), so they are the same on every
|
||||
//! platform: the minimum sine of the dihedral angles (the improvement objective, Klingner and
|
||||
//! Shewchuk 2007, §2), the circumcentre and the radius–edge ratio (the refinement criterion,
|
||||
//! Shewchuk 1998). Angles in degrees are for reports only.
|
||||
//!
|
||||
//! Sources: J. R. Shewchuk, *What is a good linear finite element? Interpolation, conditioning,
|
||||
//! anisotropy, and quality measures*, 2002 (the dihedral-angle sine formula
|
||||
//! `sin θ_ij = 3 V ℓ_ij / (2 A_k A_l)` and the volume–length and radius-ratio measures);
|
||||
//! B. M. Klingner, J. R. Shewchuk, *Aggressive Tetrahedral Mesh Improvement*, IMR 2007, §2.
|
||||
|
||||
pub(crate) use crate::vec3::{P, add, cross, dist, dot, lerp, mid, norm, scale, sub};
|
||||
|
||||
/// Six times the signed volume (positive in tet4 order).
|
||||
pub fn volume6(x: &[P; 4]) -> f64 {
|
||||
let [a, b, c, d] = *x;
|
||||
dot(sub(b, a), cross(sub(c, a), sub(d, a)))
|
||||
}
|
||||
|
||||
/// The cross product of two edges of the face opposite vertex `k` (its length is twice the
|
||||
/// face's area).
|
||||
fn face_cross(x: &[P; 4], k: usize) -> P {
|
||||
let [a, b, c] = match k {
|
||||
0 => [x[1], x[2], x[3]],
|
||||
1 => [x[0], x[2], x[3]],
|
||||
2 => [x[0], x[1], x[3]],
|
||||
_ => [x[0], x[1], x[2]],
|
||||
};
|
||||
cross(sub(b, a), sub(c, a))
|
||||
}
|
||||
|
||||
/// Twice the area of the face opposite vertex `k`.
|
||||
fn face_area2(x: &[P; 4], k: usize) -> f64 {
|
||||
norm(face_cross(x, k))
|
||||
}
|
||||
|
||||
/// The edges as `(i, j, k, l)`: edge `ij`, and the two other vertices (whose opposite faces meet
|
||||
/// at the edge).
|
||||
const EDGE_FACES: [(usize, usize, usize, usize); 6] = [(0, 1, 2, 3), (0, 2, 1, 3), (0, 3, 1, 2), (1, 2, 0, 3), (1, 3, 0, 2), (2, 3, 0, 1)];
|
||||
|
||||
/// The sines of the six dihedral angles, signed by orientation (all negative for an inverted
|
||||
/// tetrahedron), in [`EDGE_FACES`] order; zeros for a degenerate one.
|
||||
pub fn dihedral_sines(x: &[P; 4]) -> [f64; 6] {
|
||||
let v6 = volume6(x);
|
||||
let areas = [0, 1, 2, 3].map(|k| face_area2(x, k));
|
||||
EDGE_FACES.map(|(i, j, k, l)| {
|
||||
let len = dist(x[i], x[j]);
|
||||
// sin θ = 3 V ℓ / (2 A_k A_l) with V = v6 / 6 and A = area2 / 2.
|
||||
let den = areas[k] * areas[l];
|
||||
let s = v6 * len / den;
|
||||
if s.is_finite() { s } else { 0.0 }
|
||||
})
|
||||
}
|
||||
|
||||
/// The cosines of the six dihedral angles (from the outward face normals).
|
||||
pub fn dihedral_cosines(x: &[P; 4]) -> [f64; 6] {
|
||||
let normal = |k: usize| -> P {
|
||||
let f: Vec<P> = (0..4).filter(|&i| i != k).filter_map(|i| x.get(i).copied()).collect();
|
||||
match f.as_slice() {
|
||||
[a, b, c] => {
|
||||
let n = cross(sub(*b, *a), sub(*c, *a));
|
||||
// Point it away from vertex k.
|
||||
let o = x.get(k).copied().unwrap_or(*a);
|
||||
if dot(n, sub(o, *a)) > 0.0 { scale(n, -1.0) } else { n }
|
||||
}
|
||||
_ => [0.0; 3],
|
||||
}
|
||||
};
|
||||
let n = [0, 1, 2, 3].map(normal);
|
||||
EDGE_FACES.map(|(_, _, k, l)| {
|
||||
let c = -dot(n[k], n[l]) / (norm(n[k]) * norm(n[l]));
|
||||
if c.is_finite() { c.clamp(-1.0, 1.0) } else { 1.0 }
|
||||
})
|
||||
}
|
||||
|
||||
/// The quality objective: the smallest signed dihedral sine (1 is impossible; the regular
|
||||
/// tetrahedron scores √(8/9) ≈ 0.943). Positive for a valid tetrahedron, 0 for a flat one,
|
||||
/// negative for an inverted one (then the sine of largest magnitude, so worse inversions score
|
||||
/// lower). With `sin θ_ij = 6V ℓ_ij / (2A_k · 2A_l)`, the extreme sine is found on squared
|
||||
/// lengths and areas, with one square root.
|
||||
pub fn quality(x: &[P; 4]) -> f64 {
|
||||
let v6 = volume6(x);
|
||||
if v6 == 0.0 || !v6.is_finite() {
|
||||
return 0.0;
|
||||
}
|
||||
let area2sq = [0, 1, 2, 3].map(|k| {
|
||||
let a = face_cross(x, k);
|
||||
dot(a, a)
|
||||
});
|
||||
let mut pick = if v6 > 0.0 { f64::INFINITY } else { 0.0 };
|
||||
for (i, j, k, l) in EDGE_FACES {
|
||||
let e = sub(x[i], x[j]);
|
||||
let r = dot(e, e) / (area2sq[k] * area2sq[l]);
|
||||
pick = if v6 > 0.0 { pick.min(r) } else { pick.max(r) };
|
||||
}
|
||||
let q = v6 * pick.sqrt();
|
||||
if q.is_finite() { q } else { 0.0 }
|
||||
}
|
||||
|
||||
/// The smallest and largest dihedral angle in degrees (reports only).
|
||||
pub fn dihedral_range(x: &[P; 4]) -> (f64, f64) {
|
||||
let (s, c) = (dihedral_sines(x), dihedral_cosines(x));
|
||||
let mut lo = f64::INFINITY;
|
||||
let mut hi = f64::NEG_INFINITY;
|
||||
for (sv, cv) in s.iter().zip(c) {
|
||||
let a = sv.abs().atan2(cv).to_degrees();
|
||||
lo = lo.min(a);
|
||||
hi = hi.max(a);
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
/// The circumcentre, or `None` for a flat tetrahedron.
|
||||
pub fn circumcenter(x: &[P; 4]) -> Option<P> {
|
||||
let [a, b, c, d] = *x;
|
||||
let (b, c, d) = (sub(b, a), sub(c, a), sub(d, a));
|
||||
let den = 2.0 * dot(b, cross(c, d));
|
||||
if den == 0.0 || !den.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let num = add(add(scale(cross(c, d), dot(b, b)), scale(cross(d, b), dot(c, c))), scale(cross(b, c), dot(d, d)));
|
||||
let o = add(a, scale(num, 1.0 / den));
|
||||
o.iter().all(|v| v.is_finite()).then_some(o)
|
||||
}
|
||||
|
||||
/// The circumradius (infinite for a flat tetrahedron).
|
||||
pub fn circumradius(x: &[P; 4]) -> f64 {
|
||||
circumcenter(x).map_or(f64::INFINITY, |o| dist(o, x[0]))
|
||||
}
|
||||
|
||||
/// The shortest and longest edge.
|
||||
pub fn edge_range(x: &[P; 4]) -> (f64, f64) {
|
||||
let mut lo = f64::INFINITY;
|
||||
let mut hi = 0.0_f64;
|
||||
for (i, j, _, _) in EDGE_FACES {
|
||||
let l = x.get(i).zip(x.get(j)).map_or(0.0, |(a, b)| dist(*a, *b));
|
||||
lo = lo.min(l);
|
||||
hi = hi.max(l);
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
/// Circumradius over shortest edge (√6/4 ≈ 0.612 for the regular tetrahedron).
|
||||
pub fn radius_edge(x: &[P; 4]) -> f64 {
|
||||
circumradius(x) / edge_range(x).0
|
||||
}
|
||||
|
||||
/// Three times the inradius over the circumradius, signed (1 for the regular tetrahedron).
|
||||
pub fn radius_ratio(x: &[P; 4]) -> f64 {
|
||||
let v6 = volume6(x);
|
||||
let area2: f64 = (0..4).map(|k| face_area2(x, k)).sum();
|
||||
// r = 3V / A_total = (v6 / 2) / (area2 / 2) = v6 / area2.
|
||||
let r = v6 / area2;
|
||||
let q = 3.0 * r / circumradius(x);
|
||||
if q.is_finite() { q } else { 0.0 }
|
||||
}
|
||||
|
||||
/// The centroid.
|
||||
pub fn centroid(x: &[P; 4]) -> P {
|
||||
let s = x.iter().fold([0.0; 3], |acc, p| add(acc, *p));
|
||||
scale(s, 0.25)
|
||||
}
|
||||
|
||||
/// The circumcentre of a triangle (in its plane), or `None` when degenerate.
|
||||
pub fn triangle_circumcenter(a: P, b: P, c: P) -> Option<P> {
|
||||
let (u, v) = (sub(b, a), sub(c, a));
|
||||
let w = cross(u, v);
|
||||
let den = 2.0 * dot(w, w);
|
||||
if den == 0.0 || !den.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let num = add(scale(cross(w, u), dot(v, v)), scale(cross(v, w), dot(u, u)));
|
||||
let o = add(a, scale(num, 1.0 / den));
|
||||
o.iter().all(|x| x.is_finite()).then_some(o)
|
||||
}
|
||||
|
||||
/// The unit normal of a triangle (zero when degenerate).
|
||||
pub fn triangle_normal(a: P, b: P, c: P) -> P {
|
||||
let n = cross(sub(b, a), sub(c, a));
|
||||
let l = norm(n);
|
||||
if l > 0.0 && l.is_finite() { scale(n, 1.0 / l) } else { [0.0; 3] }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const REGULAR: [P; 4] = [[1.0, 1.0, 1.0], [1.0, -1.0, -1.0], [-1.0, 1.0, -1.0], [-1.0, -1.0, 1.0]];
|
||||
|
||||
fn positive(mut x: [P; 4]) -> [P; 4] {
|
||||
if volume6(&x) < 0.0 {
|
||||
x.swap(0, 1);
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regular_tetrahedron() {
|
||||
let x = positive(REGULAR);
|
||||
assert!((quality(&x) - (8.0f64 / 9.0).sqrt()).abs() < 1e-12, "{}", quality(&x));
|
||||
assert!((radius_ratio(&x) - 1.0).abs() < 1e-12);
|
||||
assert!((radius_edge(&x) - 6f64.sqrt() / 4.0).abs() < 1e-12);
|
||||
let (lo, hi) = dihedral_range(&x);
|
||||
assert!((lo - 70.528_779).abs() < 1e-5 && (hi - lo).abs() < 1e-9, "{lo} {hi}");
|
||||
let o = circumcenter(&x).unwrap();
|
||||
assert!(norm(o) < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_corner_and_inverted() {
|
||||
let x = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||||
assert!(volume6(&x) > 0.0);
|
||||
let (lo, hi) = dihedral_range(&x);
|
||||
assert!((hi - 90.0).abs() < 1e-9 && (lo - 54.735_610).abs() < 1e-5, "{lo} {hi}");
|
||||
let mut y = x;
|
||||
y.swap(1, 2);
|
||||
assert!(quality(&y) < 0.0);
|
||||
let flat = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0]];
|
||||
assert_eq!(quality(&flat), 0.0);
|
||||
assert!(circumcenter(&flat).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sliver_scores_low() {
|
||||
let x = positive([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.01]]);
|
||||
assert!(quality(&x) < 0.05 && quality(&x) > 0.0, "{}", quality(&x));
|
||||
assert!(radius_ratio(&x) < 0.1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
//! Tetrahedral mesh quality: Delaunay refinement to a size field, ODT smoothing and aggressive
|
||||
//! improvement of a constrained Delaunay tetrahedralization ([`crate::cdt`]), then optionally
|
||||
//! quadratic elements with their mid-edge nodes on the geometry.
|
||||
//!
|
||||
//! # Pipeline ([`quality_mesh`], `docs/meshing/literature.md` Part D Step 3)
|
||||
//! 1. **Boundary entities.** The CDT's boundary faces carry their B-rep face; an edge between two
|
||||
//! faces of different B-rep faces, or whose faces turn by more than the feature angle, is a
|
||||
//! feature edge (curve). Vertices are corners (fixed), curve vertices (move along their
|
||||
//! curve), face vertices (move on their face) or interior.
|
||||
//! 2. **Size field** ([`geometry::GradedSize`]): the global size, limited near curved boundary
|
||||
//! to `θ r` (θ radians per element along a curve of radius `r`, from the exact surface or
|
||||
//! estimated from the tessellation) and in thin walls to their thickness
|
||||
//! ([`geometry::Tessellation::thickness`]), graded at a bounded rate.
|
||||
//! 3. **Coarsening** ([`refine::coarsen`]): edge collapses where the boundary recovery's
|
||||
//! Steiner points and the tessellation are denser than the size field.
|
||||
//! 4. **Refinement interleaved with smoothing** (Tournois et al. 2009): rounds of constrained
|
||||
//! Delaunay refinement ([`refine`], Shewchuk 1998, Si 2010) and ODT smoothing ([`smooth`],
|
||||
//! Chen–Xu 2004, Alliez et al. 2005), boundary vertices kept on their faces by projection
|
||||
//! ([`geometry`]).
|
||||
//! 5. **Aggressive improvement** ([`improve`], Klingner–Shewchuk 2007): smoothing, edge
|
||||
//! removal, multi-face removal and vertex insertion, monotone in the worst dihedral sine.
|
||||
//! 6. **Gate and report**: every tetrahedron admissible by FieldCraft's one criterion, the volume
|
||||
//! equal to what the mesh's own boundary encloses; minimum and maximum dihedral angles and
|
||||
//! radius ratios ([`TetQuality`]).
|
||||
//! 7. **Quadratic elements** ([`quadratic`]): mid-edge nodes, projected onto the B-rep faces and
|
||||
//! curves on the boundary; an element that would not be valid keeps straight edges there.
|
||||
//!
|
||||
//! Every loop is bounded (insertions, tetrahedra, passes); a stage that stops early says so in
|
||||
//! the report. Everything is deterministic: fixed orders, no hashing into output, and only
|
||||
//! correctly rounded arithmetic in decisions.
|
||||
|
||||
// The tetrahedron kernels index their four corners and six edges by slot; ranges over them are
|
||||
// clearer than iterator chains (as in the fem and solver crates).
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
pub mod complex;
|
||||
pub mod geometry;
|
||||
pub mod improve;
|
||||
pub mod measure;
|
||||
pub mod ops;
|
||||
pub mod quadratic;
|
||||
pub mod refine;
|
||||
pub mod smooth;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
use crate::cdt::TetMesh;
|
||||
use crate::predicates::Point3;
|
||||
use crate::quality::{GateOptions, GateReport, Metric, Validity, element_quality, enclosed_volume, validity_gate_elements};
|
||||
use crate::surface::BoundarySurface;
|
||||
use crate::{Error, Result};
|
||||
|
||||
use complex::{IdMap, NONE, TetComplex, VertexKind, face_toward, sorted2};
|
||||
use geometry::{AnalyticSurface, GradedSize, Tessellation};
|
||||
use improve::{ImproveOptions, ImproveStats};
|
||||
use refine::{Context, RefineOptions, RefineStats};
|
||||
|
||||
/// Element order.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Order {
|
||||
Linear,
|
||||
Quadratic,
|
||||
}
|
||||
|
||||
/// Settings of [`quality_mesh`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct QualityOptions {
|
||||
/// Target edge length; `None`: a twentieth of the bounding-box diagonal.
|
||||
pub size: Option<f64>,
|
||||
/// Radians per element along curved boundary (0: no curvature refinement).
|
||||
pub curvature_angle: f64,
|
||||
/// Elements across a thin wall: the size is at most the local thickness over this (0: no
|
||||
/// proximity sizing).
|
||||
pub proximity: f64,
|
||||
/// Largest change of size per unit length.
|
||||
pub grading: f64,
|
||||
/// Smallest size, as a fraction of the global size.
|
||||
pub min_size: f64,
|
||||
/// Radius–edge bound of the refinement.
|
||||
pub radius_edge: f64,
|
||||
/// Interleaved refinement and ODT rounds (0: no refinement).
|
||||
pub rounds: usize,
|
||||
/// ODT passes per round.
|
||||
pub odt_passes: usize,
|
||||
/// Run the improvement schedule.
|
||||
pub improve: bool,
|
||||
pub improve_options: ImproveOptions,
|
||||
/// An edge whose two boundary faces turn by more than this many degrees is a feature edge.
|
||||
pub feature_angle: f64,
|
||||
pub order: Order,
|
||||
pub max_tets: usize,
|
||||
pub max_points: usize,
|
||||
}
|
||||
|
||||
impl Default for QualityOptions {
|
||||
fn default() -> Self {
|
||||
QualityOptions {
|
||||
size: None,
|
||||
curvature_angle: std::f64::consts::PI / 8.0,
|
||||
proximity: 1.0,
|
||||
grading: 0.3,
|
||||
min_size: 0.02,
|
||||
radius_edge: 2.0,
|
||||
rounds: 3,
|
||||
odt_passes: 3,
|
||||
improve: true,
|
||||
improve_options: ImproveOptions::default(),
|
||||
feature_angle: 45.0,
|
||||
order: Order::Linear,
|
||||
max_tets: 5_000_000,
|
||||
max_points: 1_000_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Quality statistics of a tetrahedral mesh (corner geometry).
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct TetQuality {
|
||||
pub tets: usize,
|
||||
pub min_dihedral: f64,
|
||||
pub max_dihedral: f64,
|
||||
pub min_radius_ratio: f64,
|
||||
pub mean_radius_ratio: f64,
|
||||
/// Tetrahedra with a dihedral angle below 10° or above 170°.
|
||||
pub slivers: usize,
|
||||
/// Tetrahedra not admissible by FieldCraft's validity criterion.
|
||||
pub inadmissible: usize,
|
||||
}
|
||||
|
||||
/// The quality of tetrahedra given by corner coordinates, with the metrics `mesh.quality`
|
||||
/// reports ([`crate::quality::element_quality`]).
|
||||
pub fn tet_quality_stats(points: &[[f64; 3]], tets: &[[u32; 4]]) -> Result<TetQuality> {
|
||||
let mut s = TetQuality { min_dihedral: 180.0, min_radius_ratio: 1.0, ..TetQuality::default() };
|
||||
let mut sum = 0.0;
|
||||
for t in tets {
|
||||
let p = |v: u32| points.get(v as usize).copied().ok_or_else(|| Error::Failed("quality: vertex id".into()));
|
||||
let q = element_quality(ElementKind::Tet4, &[p(t[0])?, p(t[1])?, p(t[2])?, p(t[3])?])?;
|
||||
let m = |metric: Metric| q.get(metric).unwrap_or(f64::NAN);
|
||||
let (lo, hi, rr) = (m(Metric::MinDihedral), m(Metric::MaxDihedral), m(Metric::RadiusRatio));
|
||||
s.min_dihedral = s.min_dihedral.min(lo);
|
||||
s.max_dihedral = s.max_dihedral.max(hi);
|
||||
s.min_radius_ratio = s.min_radius_ratio.min(rr);
|
||||
sum += rr;
|
||||
s.slivers += usize::from(!(lo >= 10.0 && hi <= 170.0));
|
||||
s.inadmissible += usize::from(q.validity() != Some(Validity::Valid));
|
||||
}
|
||||
s.tets = tets.len();
|
||||
if tets.is_empty() {
|
||||
s.min_dihedral = 0.0;
|
||||
s.min_radius_ratio = 0.0;
|
||||
} else {
|
||||
s.mean_radius_ratio = sum / tets.len() as f64;
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// A boundary face of the output: tetrahedron, the slot of its opposite vertex, B-rep face.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TaggedFace {
|
||||
pub tet: u32,
|
||||
pub opposite: u8,
|
||||
pub face: u32,
|
||||
}
|
||||
|
||||
impl TaggedFace {
|
||||
/// The face's 1-based number in the model's tet4 (and tet10) face order.
|
||||
pub fn element_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 [`quality_mesh`] did.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct QualityReport {
|
||||
/// The constrained Delaunay tetrahedralization it started from.
|
||||
pub input: TetQuality,
|
||||
pub output: TetQuality,
|
||||
pub refine: Vec<RefineStats>,
|
||||
pub odt_moves: usize,
|
||||
/// Edges collapsed where the input was denser than the size field.
|
||||
pub collapsed: usize,
|
||||
/// Boundary vertices that could not be moved onto their B-rep face or curve (the move would
|
||||
/// have inverted a tetrahedron); they stay on the tessellation.
|
||||
pub unsnapped: usize,
|
||||
pub improve: ImproveStats,
|
||||
/// The global size used.
|
||||
pub size: f64,
|
||||
/// Volume of the mesh, and what its boundary encloses.
|
||||
pub volume: f64,
|
||||
pub boundary_volume: f64,
|
||||
/// Quadratic elements whose curved edges had to stay straight to be valid.
|
||||
pub straightened: usize,
|
||||
/// Why a stage stopped early, if one did.
|
||||
pub stopped: Vec<String>,
|
||||
}
|
||||
|
||||
/// The quality mesh.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct QualityMesh {
|
||||
/// Corner vertices, then (quadratic) mid-edge nodes.
|
||||
pub points: Vec<[f64; 3]>,
|
||||
/// Elements in the model's node order (4 or 10 nodes).
|
||||
pub elements: Vec<Vec<u32>>,
|
||||
pub kind: ElementKind,
|
||||
pub boundary: Vec<TaggedFace>,
|
||||
pub report: QualityReport,
|
||||
}
|
||||
|
||||
fn bounds(points: &[[f64; 3]]) -> ([f64; 3], [f64; 3]) {
|
||||
let mut lo = [f64::INFINITY; 3];
|
||||
let mut hi = [f64::NEG_INFINITY; 3];
|
||||
for p in points {
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(p[k]);
|
||||
hi[k] = hi[k].max(p[k]);
|
||||
}
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
/// The size field of a boundary: `global` limited by curvature, graded.
|
||||
pub fn size_field(surface: &BoundarySurface, geometry: &Tessellation, global: f64, o: &QualityOptions) -> GradedSize {
|
||||
let (lo, hi) = bounds(surface.points());
|
||||
let floor = o.min_size * global;
|
||||
let mut samples = Vec::new();
|
||||
if o.curvature_angle > 0.0 {
|
||||
let radii = geometry::curvature_radii(surface, geometry);
|
||||
for (p, r) in surface.points().iter().zip(radii) {
|
||||
let h = (o.curvature_angle * r).max(floor);
|
||||
if h < global {
|
||||
samples.push((*p, h));
|
||||
}
|
||||
}
|
||||
}
|
||||
if o.proximity > 0.0 {
|
||||
// Thin walls: elements no larger than the wall's thickness over `proximity`.
|
||||
for (p, t) in surface.points().iter().zip(geometry.thickness()) {
|
||||
let h = (t / o.proximity).max(floor);
|
||||
if h < global {
|
||||
samples.push((*p, h));
|
||||
}
|
||||
}
|
||||
}
|
||||
GradedSize::new(global, &samples, lo, hi, o.grading)
|
||||
}
|
||||
|
||||
/// The CDT as a [`TetComplex`] with B-rep face tags, feature curves and vertex kinds; and the
|
||||
/// feature curves' face pairs.
|
||||
pub fn complex_from_cdt(surface: &BoundarySurface, cdt: &TetMesh, o: &QualityOptions) -> Result<(TetComplex, Vec<[u32; 2]>)> {
|
||||
let tri_face = surface.tri_face();
|
||||
let tags: Vec<(u32, usize, u32)> =
|
||||
cdt.boundary.iter().map(|b| (b.tet, usize::from(b.opposite), tri_face.get(b.triangle as usize).copied().unwrap_or(NONE))).collect();
|
||||
let kinds = vec![VertexKind::Interior; cdt.points.len()];
|
||||
let mut cx =
|
||||
TetComplex::with_invalid(&cdt.points, kinds, &cdt.tets, &tags, &[], o.max_points.max(cdt.points.len()), o.max_tets.max(cdt.tets.len()))?;
|
||||
let feature_cos = geometry::sin_cos(o.feature_angle.clamp(0.0, 180.0).to_radians()).1;
|
||||
let (table, edges) = refine::feature_curves(&cx, feature_cos)?;
|
||||
for &((a, b), c) in &edges {
|
||||
cx.set_curve(a, b, Some(c));
|
||||
}
|
||||
// Vertex kinds from the boundary faces and curve edges at each vertex.
|
||||
let mut face_tag: Vec<Option<u32>> = vec![None; cdt.points.len()];
|
||||
let mut mixed = vec![false; cdt.points.len()];
|
||||
for (t, vs) in cx.live_tets() {
|
||||
let (ns, tg) = (cx.adj(t)?, cx.tags(t)?);
|
||||
for k in 0..4 {
|
||||
if ns[k] != NONE {
|
||||
continue;
|
||||
}
|
||||
for v in face_toward(vs, k) {
|
||||
let i = v as usize;
|
||||
match face_tag.get(i).copied().flatten() {
|
||||
None => {
|
||||
if let Some(s) = face_tag.get_mut(i) {
|
||||
*s = Some(tg[k]);
|
||||
}
|
||||
}
|
||||
Some(x) if x != tg[k] => {
|
||||
if let Some(m) = mixed.get_mut(i) {
|
||||
*m = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut curve_count: IdMap<u32, Vec<u32>> = IdMap::default();
|
||||
for &((a, b), c) in &edges {
|
||||
curve_count.entry(a).or_default().push(c);
|
||||
curve_count.entry(b).or_default().push(c);
|
||||
}
|
||||
for v in 0..cdt.points.len() as u32 {
|
||||
let kind = match (face_tag.get(v as usize).copied().flatten(), curve_count.get(&v).map(Vec::as_slice)) {
|
||||
(None, _) => VertexKind::Interior,
|
||||
(Some(tag), None) if !mixed.get(v as usize).copied().unwrap_or(true) => VertexKind::Face(tag),
|
||||
(Some(_), Some(&[c1, c2])) if c1 == c2 => VertexKind::Curve(c1),
|
||||
_ => VertexKind::Corner,
|
||||
};
|
||||
cx.set_kind(v, kind);
|
||||
}
|
||||
Ok((cx, table))
|
||||
}
|
||||
|
||||
/// Move every face and curve vertex onto its B-rep face or curve where that keeps the
|
||||
/// tetrahedra around it positive (and, when `guarded`, their worst quality at least the smaller
|
||||
/// of what it was and sin 17.5°, so a finished mesh is not spoilt). Returns the vertices that
|
||||
/// could not move.
|
||||
fn snap_to_geometry(cx: &mut TetComplex, ctx: &Context, guarded: bool) -> Result<usize> {
|
||||
let mut stuck = 0;
|
||||
for v in 0..cx.point_count() as u32 {
|
||||
if !cx.is_used(v) || !matches!(cx.kind(v), VertexKind::Face(_) | VertexKind::Curve(_)) {
|
||||
continue;
|
||||
}
|
||||
let x = cx.xyz(v)?;
|
||||
let Some(p) = ctx.place(cx.kind(v), x) else { continue };
|
||||
if p.coords() == x {
|
||||
continue;
|
||||
}
|
||||
let star = cx.star(v)?;
|
||||
let mut ok = cx.move_is_valid(v, &p, &star)?;
|
||||
if ok && guarded {
|
||||
let before = ops::worst(cx, &star)?;
|
||||
cx.move_point(v, p)?;
|
||||
let after = ops::worst(cx, &star)?;
|
||||
if after < before.min(0.3) {
|
||||
cx.move_point(v, Point3::try_from(x)?)?;
|
||||
ok = false;
|
||||
}
|
||||
} else if ok {
|
||||
cx.move_point(v, p)?;
|
||||
}
|
||||
stuck += usize::from(!ok);
|
||||
}
|
||||
Ok(stuck)
|
||||
}
|
||||
|
||||
/// The volume enclosed by the boundary faces of a mesh ([`enclosed_volume`]).
|
||||
fn boundary_volume(points: &[[f64; 3]], tets: &[[u32; 4]], boundary: &[TaggedFace]) -> Result<f64> {
|
||||
let mut triangles = Vec::with_capacity(boundary.len());
|
||||
for b in boundary {
|
||||
let t = tets.get(b.tet as usize).ok_or_else(|| Error::Failed("boundary face of no tetrahedron".into()))?;
|
||||
// face_toward points into the tetrahedron; outward is its reverse.
|
||||
let f = face_toward(*t, usize::from(b.opposite));
|
||||
triangles.push([f[0] as usize, f[2] as usize, f[1] as usize]);
|
||||
}
|
||||
enclosed_volume(points, &triangles)
|
||||
}
|
||||
|
||||
/// Run the quality pipeline on a constrained Delaunay tetrahedralization of `surface` (see the
|
||||
/// module docs). `analytic[face]` is the exact surface of a B-rep face when known.
|
||||
pub fn quality_mesh(surface: &BoundarySurface, cdt: &TetMesh, analytic: Vec<Option<AnalyticSurface>>, o: &QualityOptions) -> Result<QualityMesh> {
|
||||
let mut report = QualityReport { input: tet_quality_stats(&cdt.points, &cdt.tets)?, ..QualityReport::default() };
|
||||
let geometry = Tessellation::new(surface, analytic);
|
||||
let (lo, hi) = bounds(surface.points());
|
||||
let global = match o.size {
|
||||
Some(h) if h > 0.0 && h.is_finite() => h,
|
||||
Some(h) => return Err(Error::Invalid(format!("size {h} must be a positive number"))),
|
||||
None => measure::dist(lo, hi) / 20.0,
|
||||
};
|
||||
if !(global > 0.0 && global.is_finite()) {
|
||||
return Err(Error::Invalid("the boundary has no extent".into()));
|
||||
}
|
||||
report.size = global;
|
||||
let size = size_field(surface, &geometry, global, o);
|
||||
let (mut cx, curve_faces) = complex_from_cdt(surface, cdt, o)?;
|
||||
// Refinement and improvement keep boundary vertices on the tessellation, a piecewise-flat
|
||||
// surface whose creases they can mesh around; the finished mesh then moves its boundary
|
||||
// vertices onto the exact surfaces (and tet10 puts its mid-edge nodes there). Projecting onto
|
||||
// the exact surfaces while improving leaves vertices on both sides of a blend's tangent
|
||||
// edge (where two faces meet without a crease) too close to flat to separate.
|
||||
let plain = Tessellation::new(surface, Vec::new());
|
||||
let ctx = Context { size: &size, geometry: &plain, curve_faces: &curve_faces };
|
||||
let exact = Context { size: &size, geometry: &geometry, curve_faces: &curve_faces };
|
||||
// Remove what rounding left flat first, so refinement starts from a valid mesh.
|
||||
let flat =
|
||||
|cx: &TetComplex| -> Vec<(u32, [u32; 4])> { cx.live_tets().filter(|(_, vs)| ops::tet_quality(cx, *vs).is_ok_and(|q| q < 1e-6)).collect() };
|
||||
let bad: Vec<u32> = flat(&cx).into_iter().map(|(t, _)| t).collect();
|
||||
if !bad.is_empty() {
|
||||
improve::topological_pass(&mut cx, &bad)?;
|
||||
for (_, vs) in flat(&cx) {
|
||||
for v in vs {
|
||||
smooth::smooth_vertex(&mut cx, &ctx, v)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Coarsen what the boundary recovery made denser than the size field asks for.
|
||||
report.collapsed = refine::coarsen(&mut cx, &ctx, 0.6, 1.4, 0.05, 6)?;
|
||||
let ropt = RefineOptions { radius_edge: o.radius_edge, ..RefineOptions::default() };
|
||||
for _ in 0..o.rounds {
|
||||
let stats = refine::refine(&mut cx, &ctx, &ropt)?;
|
||||
let quiet = stats.interior + stats.boundary == 0;
|
||||
let stopped = stats.stopped.clone();
|
||||
report.refine.push(stats);
|
||||
for _ in 0..o.odt_passes {
|
||||
report.odt_moves += smooth::odt_pass(&mut cx, &ctx)?;
|
||||
}
|
||||
if let Some(s) = stopped {
|
||||
report.stopped.push(s);
|
||||
break;
|
||||
}
|
||||
if quiet {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if o.improve {
|
||||
report.improve = improve::improve(&mut cx, &ctx, &o.improve_options)?;
|
||||
if let Some(s) = &report.improve.stopped {
|
||||
report.stopped.push(s.clone());
|
||||
}
|
||||
}
|
||||
// Put the boundary vertices on the exact surfaces (the tessellation is within its chordal
|
||||
// tolerance of them).
|
||||
report.unsnapped = snap_to_geometry(&mut cx, &exact, true)?;
|
||||
let c = cx.compact()?;
|
||||
let boundary: Vec<TaggedFace> = c.boundary.iter().map(|&(tet, k, face)| TaggedFace { tet, opposite: k as u8, face }).collect();
|
||||
report.output = tet_quality_stats(&c.points, &c.tets)?;
|
||||
report.boundary_volume = boundary_volume(&c.points, &c.tets, &boundary)?;
|
||||
let gate = linear_gate(&c.points, &c.tets, report.boundary_volume)?;
|
||||
report.volume = gate.volume;
|
||||
gate.into_result()?;
|
||||
match o.order {
|
||||
Order::Linear => {
|
||||
Ok(QualityMesh { elements: c.tets.iter().map(|t| t.to_vec()).collect(), points: c.points, kind: ElementKind::Tet4, boundary, report })
|
||||
}
|
||||
Order::Quadratic => {
|
||||
let curve_of: IdMap<(u32, u32), u32> = c.curves.iter().map(|&(e, k)| (sorted2(e.0, e.1), k)).collect();
|
||||
let q = quadratic::tet10(&c.points, &c.tets, &boundary, &curve_of, &curve_faces, &geometry)?;
|
||||
report.straightened = q.straightened;
|
||||
Ok(QualityMesh { points: q.points, elements: q.elements, kind: ElementKind::Tet10, boundary, report })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The constrained Delaunay tetrahedralization itself as a [`QualityMesh`] (no quality stage),
|
||||
/// through the same validity gate: every tetrahedron admissible, the volume equal to what its
|
||||
/// boundary encloses.
|
||||
pub fn raw_mesh(surface: &BoundarySurface, cdt: &TetMesh) -> Result<QualityMesh> {
|
||||
let tri_face = surface.tri_face();
|
||||
let boundary: Vec<TaggedFace> = cdt
|
||||
.boundary
|
||||
.iter()
|
||||
.map(|b| TaggedFace { tet: b.tet, opposite: b.opposite, face: tri_face.get(b.triangle as usize).copied().unwrap_or(NONE) })
|
||||
.collect();
|
||||
let stats = tet_quality_stats(&cdt.points, &cdt.tets)?;
|
||||
let boundary_volume = boundary_volume(&cdt.points, &cdt.tets, &boundary)?;
|
||||
let gate = linear_gate(&cdt.points, &cdt.tets, boundary_volume)?.into_result()?;
|
||||
let report = QualityReport { input: stats, output: stats, volume: gate.volume, boundary_volume, ..QualityReport::default() };
|
||||
Ok(QualityMesh { points: cdt.points.clone(), elements: cdt.tets.iter().map(|t| t.to_vec()).collect(), kind: ElementKind::Tet4, boundary, report })
|
||||
}
|
||||
|
||||
/// The validity gate of a linear mesh: every tetrahedron admissible, and the volume equal to
|
||||
/// what its boundary encloses within 1e-9 (no overlap, no gap).
|
||||
pub fn linear_gate(points: &[[f64; 3]], tets: &[[u32; 4]], boundary_volume: f64) -> Result<GateReport> {
|
||||
let coords = |t: &[u32; 4]| -> Result<[[f64; 3]; 4]> {
|
||||
let p = |v: u32| points.get(v as usize).copied().ok_or_else(|| Error::Failed("gate: vertex id".into()));
|
||||
Ok([p(t[0])?, p(t[1])?, p(t[2])?, p(t[3])?])
|
||||
};
|
||||
let elements = 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(boundary_volume), volume_tolerance: 1e-9 })
|
||||
}
|
||||
@@ -0,0 +1,705 @@
|
||||
//! Local operations on the [`TetComplex`]: edge removal, face removal (2-3 flip and multi-face
|
||||
//! removal), Delaunay vertex insertion with a star-shaped cavity, and the optimal-cavity vertex
|
||||
//! insertion of the improvement stage.
|
||||
//!
|
||||
//! Every operation is a [`TetComplex::replace`], so it either keeps a valid mesh or changes
|
||||
//! nothing; the topological ones are hill-climbing steps that are applied only when the worst new
|
||||
//! tetrahedron is better than the worst one they remove.
|
||||
//!
|
||||
//! Sources:
|
||||
//! - J. R. Shewchuk, *Two Discrete Optimization Algorithms for the Topological Improvement of
|
||||
//! Tetrahedral Meshes*, 2002: edge removal by Klincsek's dynamic programming over the ring of
|
||||
//! the edge (§2, including boundary edges ending in a 2-2 flip), and multi-face removal (§3).
|
||||
//! - B. M. Klingner, J. R. Shewchuk, *Aggressive Tetrahedral Mesh Improvement*, IMR 2007, §3.4:
|
||||
//! vertex insertion whose cavity maximizes the worst new tetrahedron (the cut of Figure 3).
|
||||
//! - A. Bowyer 1981, D. F. Watson 1981; Shewchuk, *Tetrahedral Mesh Generation by Delaunay
|
||||
//! Refinement*, SoCG 1998: Delaunay insertion; the cavity is shrunk until it is star-shaped from
|
||||
//! the new vertex, so a mesh that is no longer exactly Delaunay (after smoothing) stays valid.
|
||||
|
||||
use crate::Result;
|
||||
use crate::predicates::{Point3, Sign};
|
||||
|
||||
use super::complex::{Change, EDGES, IdMap, NONE, TetComplex, VertexKind, face_toward, sorted2, sorted3};
|
||||
use super::measure::{self, P};
|
||||
|
||||
/// The quality objective of a tetrahedron given by vertex ids ([`measure::quality`]).
|
||||
pub fn tet_quality(cx: &TetComplex, vs: [u32; 4]) -> Result<f64> {
|
||||
Ok(measure::quality(&cx.coords(vs)?))
|
||||
}
|
||||
|
||||
/// The worst quality of a set of tetrahedra.
|
||||
pub fn worst(cx: &TetComplex, tets: &[u32]) -> Result<f64> {
|
||||
let mut q = f64::INFINITY;
|
||||
for &t in tets {
|
||||
q = q.min(tet_quality(cx, cx.tet(t)?)?);
|
||||
}
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
/// The largest ring an edge removal considers (Shewchuk 2002 recommends 7; up to 10).
|
||||
pub const MAX_RING: usize = 10;
|
||||
|
||||
/// The cosine of the largest angle (20°) between the normals of two boundary faces on one B-rep
|
||||
/// face that a boundary edge removal (ending in a 2-2 flip) or the deletion of a tetrahedron
|
||||
/// capping them may re-triangulate. The size field holds the faces of a curved B-rep face to
|
||||
/// about 22.5° of turn per element, so faces of one B-rep face fold by about that much, and the
|
||||
/// change moves the boundary by an element's chordal error.
|
||||
pub const BOUNDARY_FLIP_COS: f64 = 0.939_692_620_785_908_4;
|
||||
|
||||
/// Klincsek's table: the best worst quality of a triangulation of the chain `v_i … v_j`
|
||||
/// (with the closing edge `v_i v_j`), and the apex `k` that achieves it.
|
||||
fn klincsek(m: usize, q: &mut dyn FnMut(usize, usize, usize) -> Result<f64>) -> Result<(Vec<f64>, Vec<usize>)> {
|
||||
let mut best = vec![f64::INFINITY; m * m];
|
||||
let mut arg = vec![0usize; m * m];
|
||||
for i in (0..m.saturating_sub(2)).rev() {
|
||||
for j in i + 2..m {
|
||||
let mut b = f64::NEG_INFINITY;
|
||||
let mut a = i + 1;
|
||||
for k in i + 1..j {
|
||||
let sub = best.get(i * m + k).copied().unwrap_or(f64::NEG_INFINITY).min(best.get(k * m + j).copied().unwrap_or(f64::NEG_INFINITY));
|
||||
if sub <= b {
|
||||
continue;
|
||||
}
|
||||
let v = sub.min(q(i, k, j)?);
|
||||
if v > b {
|
||||
b = v;
|
||||
a = k;
|
||||
}
|
||||
}
|
||||
if let Some(s) = best.get_mut(i * m + j) {
|
||||
*s = b;
|
||||
}
|
||||
if let Some(s) = arg.get_mut(i * m + j) {
|
||||
*s = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((best, arg))
|
||||
}
|
||||
|
||||
/// The triangles `(i, k, j)` of the optimal triangulation of `v_i … v_j`.
|
||||
fn triangles(arg: &[usize], m: usize, i: usize, j: usize, out: &mut Vec<(usize, usize, usize)>) {
|
||||
let mut stack = vec![(i, j)];
|
||||
while let Some((i, j)) = stack.pop() {
|
||||
if j < i + 2 {
|
||||
continue;
|
||||
}
|
||||
let k = arg.get(i * m + j).copied().unwrap_or(i + 1);
|
||||
out.push((i, k, j));
|
||||
stack.push((i, k));
|
||||
stack.push((k, j));
|
||||
}
|
||||
}
|
||||
|
||||
/// The two tetrahedra a ring triangle `(v_i, v_k, v_j)` (in ring order) makes with the edge's
|
||||
/// ends: positive when the ring runs as in [`TetComplex::edge_ring`].
|
||||
fn ring_tets(a: u32, b: u32, vi: u32, vk: u32, vj: u32) -> [[u32; 4]; 2] {
|
||||
[[vi, vj, vk, a], [vi, vk, vj, b]]
|
||||
}
|
||||
|
||||
/// Remove edge `ab` if that improves the worst tetrahedron around it (Shewchuk 2002 §2). A
|
||||
/// boundary edge is removed only when its two boundary faces lie on the same B-rep face, are
|
||||
/// nearly coplanar ([`BOUNDARY_FLIP_COS`]) and the edge is not a feature edge; the operation then
|
||||
/// ends in a 2-2 flip of the boundary. Returns whether the mesh changed.
|
||||
pub fn remove_edge(cx: &mut TetComplex, a: u32, b: u32) -> Result<bool> {
|
||||
if cx.curve(a, b).is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(t) = cx.edge_tet(a, b)? else { return Ok(false) };
|
||||
remove_edge_at(cx, t, a, b)
|
||||
}
|
||||
|
||||
/// [`remove_edge`] from a tetrahedron `t` that holds the edge.
|
||||
pub fn remove_edge_at(cx: &mut TetComplex, t: u32, a: u32, b: u32) -> Result<bool> {
|
||||
let vs = cx.tet(t)?;
|
||||
if cx.curve(a, b).is_some() || !vs.contains(&a) || !vs.contains(&b) {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(ring) = cx.edge_ring_at(t, a, b)? else { return Ok(false) };
|
||||
let m = ring.verts.len();
|
||||
if m < 3 || m > MAX_RING + usize::from(!ring.closed) {
|
||||
return Ok(false);
|
||||
}
|
||||
let v = ring.verts.clone();
|
||||
let x = |id: u32| cx.xyz(id);
|
||||
let (xa, xb) = (x(a)?, x(b)?);
|
||||
// An open ring runs from boundary face (a, b, v_0) to (a, b, v_{m−1}): both must lie on one
|
||||
// flat B-rep face, and they become (a, v_0, v_{m−1}) and (b, v_0, v_{m−1}) (a 2-2 flip).
|
||||
let mut new_boundary = Vec::new();
|
||||
if !ring.closed {
|
||||
let (first, last) = (ring.tets.first().copied().unwrap_or(NONE), ring.tets.last().copied().unwrap_or(NONE));
|
||||
let (v0, vl) = (v.first().copied().unwrap_or(NONE), v.last().copied().unwrap_or(NONE));
|
||||
let (Some(t0), Some(t1)) = (boundary_tag(cx, first, [a, b, v0])?, boundary_tag(cx, last, [a, b, vl])?) else { return Ok(false) };
|
||||
let n0 = measure::triangle_normal(xa, xb, x(v0)?);
|
||||
let n1 = measure::triangle_normal(xa, x(vl)?, xb);
|
||||
if t0 != t1 || !(measure::dot(n0, n1) >= BOUNDARY_FLIP_COS) {
|
||||
return Ok(false);
|
||||
}
|
||||
// Oriented into the mesh, as the faces of the new tetrahedra (v_0, v_l, v_k, a) and
|
||||
// (v_0, v_k, v_l, b) opposite v_k.
|
||||
new_boundary.push(([v0, a, vl], t0));
|
||||
new_boundary.push(([v0, vl, b], t0));
|
||||
}
|
||||
let before = worst(cx, &ring.tets)?;
|
||||
let xv = v.iter().map(|&id| x(id)).collect::<Result<Vec<P>>>()?;
|
||||
let mut q = |i: usize, k: usize, j: usize| -> Result<f64> {
|
||||
let (pi, pk, pj) = match (xv.get(i), xv.get(k), xv.get(j)) {
|
||||
(Some(p), Some(q), Some(r)) => (*p, *q, *r),
|
||||
_ => return Ok(f64::NEG_INFINITY),
|
||||
};
|
||||
Ok(measure::quality(&[pi, pj, pk, xa]).min(measure::quality(&[pi, pk, pj, xb])))
|
||||
};
|
||||
// Triangulate the polygon v_0 … v_{m−1}, closed by the edge v_{m−1} v_0.
|
||||
let (best, arg) = klincsek(m, &mut q)?;
|
||||
let total = best.get(m - 1).copied().unwrap_or(f64::NEG_INFINITY);
|
||||
if !(total > before) {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut tris = Vec::new();
|
||||
triangles(&arg, m, 0, m - 1, &mut tris);
|
||||
let mut new = Vec::with_capacity(2 * tris.len());
|
||||
for (i, k, j) in tris {
|
||||
let (vi, vk, vj) = match (v.get(i), v.get(k), v.get(j)) {
|
||||
(Some(p), Some(q), Some(r)) => (*p, *q, *r),
|
||||
_ => return Ok(false),
|
||||
};
|
||||
new.extend(ring_tets(a, b, vi, vk, vj));
|
||||
}
|
||||
let change = Change { old: ring.tets.clone(), new, new_boundary, drop_curves: Vec::new() };
|
||||
Ok(cx.replace(&change)?.is_some())
|
||||
}
|
||||
|
||||
/// The tag of the boundary face of tetrahedron `t` with vertices `face`, if it is one.
|
||||
pub fn boundary_tag(cx: &TetComplex, t: u32, face: [u32; 3]) -> Result<Option<u32>> {
|
||||
if t == NONE {
|
||||
return Ok(None);
|
||||
}
|
||||
let vs = cx.tet(t)?;
|
||||
let key = sorted3(face);
|
||||
let ns = cx.adj(t)?;
|
||||
let tags = cx.tags(t)?;
|
||||
for k in 0..4 {
|
||||
if sorted3(face_toward(vs, k)) == key && ns[k] == NONE {
|
||||
return Ok(Some(tags[k]));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Remove the interior face opposite vertex `k` of tetrahedron `t` by the best multi-face removal
|
||||
/// that includes it (Shewchuk 2002 §3), if that improves the worst tetrahedron. With a single
|
||||
/// face this is the 2-3 flip. Returns whether the mesh changed.
|
||||
pub fn remove_face(cx: &mut TetComplex, t: u32, k: usize) -> Result<bool> {
|
||||
let vs = cx.tet(t)?;
|
||||
let u = cx.adj(t)?.get(k).copied().unwrap_or(NONE);
|
||||
if u == NONE {
|
||||
return Ok(false);
|
||||
}
|
||||
let a = vs[k];
|
||||
let us = cx.tet(u)?;
|
||||
let face = face_toward(vs, k);
|
||||
let b = us.iter().copied().find(|v| !face.contains(v)).unwrap_or(NONE);
|
||||
if b == NONE || cx.curve(a, b).is_some() || cx.edge_tet(a, b)?.is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
// Sandwiched faces between a and b: faces f with tetrahedra (a ∪ f) and (b ∪ f). Grow the
|
||||
// set from the given face across shared edges while every new tetrahedron (a, b, e) on the
|
||||
// ring stays better than the worst removed one; take the best prefix found (a greedy form of
|
||||
// Shewchuk's optimal search, which visits the same candidates).
|
||||
let mut faces: Vec<[u32; 3]> = vec![face];
|
||||
let mut old: Vec<u32> = vec![t, u];
|
||||
let mut best: Option<(f64, Vec<u32>, Vec<[u32; 4]>)> = None;
|
||||
for _round in 0..8 {
|
||||
let removed_q = worst(cx, &old)?;
|
||||
// The boundary edges of the face set (edges in exactly one face), oriented as in the face
|
||||
// seen from a: each gives the new tetrahedron (a, b, e) positive.
|
||||
let mut count: IdMap<(u32, u32), (u32, u32, usize)> = IdMap::default();
|
||||
for f in &faces {
|
||||
for i in 0..3 {
|
||||
let (p, q) = (f[i], f[(i + 1) % 3]);
|
||||
let e = count.entry(sorted2(p, q)).or_insert((p, q, 0));
|
||||
e.2 += 1;
|
||||
}
|
||||
}
|
||||
let mut ring: Vec<(u32, u32)> = count.values().filter(|e| e.2 == 1).map(|e| (e.0, e.1)).collect();
|
||||
ring.sort_unstable();
|
||||
let mut new = Vec::with_capacity(ring.len());
|
||||
let mut q = f64::INFINITY;
|
||||
for &(p, r) in &ring {
|
||||
// face (p, r, x) has a on its positive side, so (p, r, a, b)… orient explicitly.
|
||||
let cand = [p, r, b, a];
|
||||
let cand = if cx.orient(cand)? == Sign::Positive { cand } else { [r, p, b, a] };
|
||||
q = q.min(tet_quality(cx, cand)?);
|
||||
new.push(cand);
|
||||
}
|
||||
if q > removed_q && best.as_ref().is_none_or(|(bq, _, _)| q > *bq) {
|
||||
best = Some((q, old.clone(), new));
|
||||
}
|
||||
// Grow: a sandwiched neighbour face across a ring edge.
|
||||
let mut grown = false;
|
||||
for &(p, r) in &ring {
|
||||
if let Some((f, ta, tb)) = sandwiched_across(cx, a, b, p, r, &faces)? {
|
||||
faces.push(f);
|
||||
old.push(ta);
|
||||
old.push(tb);
|
||||
grown = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !grown {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let Some((_, old, new)) = best else { return Ok(false) };
|
||||
let change = Change { old, new, new_boundary: Vec::new(), drop_curves: Vec::new() };
|
||||
Ok(cx.replace(&change)?.is_some())
|
||||
}
|
||||
|
||||
/// A face `(p, r, x)` sandwiched between `a` and `b` (tetrahedra `a ∪ f` and `b ∪ f` both exist)
|
||||
/// next to edge `pr`, not already in `faces`.
|
||||
fn sandwiched_across(cx: &mut TetComplex, a: u32, b: u32, p: u32, r: u32, faces: &[[u32; 3]]) -> Result<Option<([u32; 3], u32, u32)>> {
|
||||
let Some(ring) = cx.edge_ring(p, r)? else { return Ok(None) };
|
||||
// Tetrahedra around pr containing a, and containing b.
|
||||
for &ta in &ring.tets {
|
||||
let va = cx.tet(ta)?;
|
||||
if !va.contains(&a) {
|
||||
continue;
|
||||
}
|
||||
let x = va.iter().copied().find(|&v| v != a && v != p && v != r).unwrap_or(NONE);
|
||||
if x == NONE || x == b {
|
||||
continue;
|
||||
}
|
||||
let f = [p, r, x];
|
||||
if faces.iter().any(|g| sorted3(*g) == sorted3(f)) {
|
||||
continue;
|
||||
}
|
||||
let k = va.iter().position(|&v| v == a).unwrap_or(0);
|
||||
let tb = cx.adj(ta)?.get(k).copied().unwrap_or(NONE);
|
||||
if tb != NONE && cx.tet(tb)?.contains(&b) {
|
||||
return Ok(Some((f, ta, tb)));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// How an inserted vertex is placed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Site {
|
||||
/// Inside the domain, in (or on) tetrahedron `t`.
|
||||
Interior(u32),
|
||||
/// On boundary face `k` of tetrahedron `t` (the vertex lies on that face's B-rep face).
|
||||
Face(u32, usize),
|
||||
/// On the boundary edge `ab` (a feature edge, which is split, or an edge inside one face).
|
||||
Edge(u32, u32),
|
||||
}
|
||||
|
||||
/// Insert a vertex at `p` with the Delaunay cavity: the tetrahedra whose circumsphere contains
|
||||
/// `p`, grown from the site's tetrahedra across interior faces, then shrunk until every face of
|
||||
/// the cavity sees `p` (star-shaped). A boundary site also removes the boundary faces of the
|
||||
/// cavity on the same B-rep face(s) whose circumcircle holds `p` and that connect to the site
|
||||
/// without crossing a feature edge, and fans the new vertex to their outline. Returns the new
|
||||
/// vertex, or `None` when no valid cavity exists (nothing changes then).
|
||||
pub fn insert_delaunay(cx: &mut TetComplex, p: Point3, kind: VertexKind, site: Site, max_cavity: usize) -> Result<Option<u32>> {
|
||||
insert_delaunay_apart(cx, p, kind, site, max_cavity, 0.0)
|
||||
}
|
||||
|
||||
/// [`insert_delaunay`], refused when a vertex of the cavity lies closer than `min_gap` to `p`
|
||||
/// (the nearest vertex of a Delaunay mesh is a vertex of the cavity), so refinement cannot
|
||||
/// pile vertices onto one spot.
|
||||
pub fn insert_delaunay_apart(cx: &mut TetComplex, p: Point3, kind: VertexKind, site: Site, max_cavity: usize, min_gap: f64) -> Result<Option<u32>> {
|
||||
let (roots, root_faces) = site_roots(cx, site)?;
|
||||
if roots.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
// Grow the conflict region.
|
||||
let mut cavity: Vec<u32> = roots.clone();
|
||||
let mut inset: IdMap<u32, ()> = cavity.iter().map(|&t| (t, ())).collect();
|
||||
let mut i = 0;
|
||||
while let Some(&t) = cavity.get(i) {
|
||||
i += 1;
|
||||
if cavity.len() > max_cavity {
|
||||
return Ok(None);
|
||||
}
|
||||
for n in cx.adj(t)? {
|
||||
if n == NONE || inset.contains_key(&n) {
|
||||
continue;
|
||||
}
|
||||
if cx.in_sphere(cx.tet(n)?, &p)? {
|
||||
inset.insert(n, ());
|
||||
cavity.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
if min_gap > 0.0 {
|
||||
let px = p.coords();
|
||||
for &t in &cavity {
|
||||
for v in cx.tet(t)? {
|
||||
if measure::dist(cx.xyz(v)?, px) < min_gap {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let curve_split = match site {
|
||||
Site::Edge(a, b) => cx.curve(a, b).map(|c| (a, b, c)),
|
||||
_ => None,
|
||||
};
|
||||
// Shrink until star-shaped; boundary faces of the cavity on the site's face(s) are removed
|
||||
// when connected to the site and in conflict.
|
||||
for _ in 0..max_cavity.max(8) {
|
||||
let plan = plan_cavity(cx, &cavity, &p, site, &root_faces)?;
|
||||
match plan {
|
||||
Plan::Ready { new, new_boundary } => {
|
||||
let len = cx.journal.steps.len();
|
||||
let v = cx.add_point(p, kind)?;
|
||||
let new: Vec<[u32; 4]> = new.into_iter().map(|f| [f[0], f[1], f[2], v]).collect();
|
||||
let new_boundary: Vec<([u32; 3], u32)> = new_boundary.into_iter().map(|(e, tag)| ([e.0, e.1, v], tag)).collect();
|
||||
let change =
|
||||
Change { old: cavity.clone(), new, new_boundary, drop_curves: curve_split.map(|(a, b, _)| (a, b)).into_iter().collect() };
|
||||
if cx.replace(&change)?.is_some() {
|
||||
if let Some((a, b, c)) = curve_split {
|
||||
cx.set_curve(a, b, None);
|
||||
cx.set_curve(a, v, Some(c));
|
||||
cx.set_curve(v, b, Some(c));
|
||||
}
|
||||
return Ok(Some(v));
|
||||
}
|
||||
// Undo the vertex.
|
||||
if cx.journal.recording {
|
||||
cx.rollback(len)?;
|
||||
} else {
|
||||
cx.drop_last_point(v)?;
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
Plan::Drop(t) => {
|
||||
if roots.contains(&t) {
|
||||
return Ok(None);
|
||||
}
|
||||
cavity.retain(|&c| c != t);
|
||||
// Keep the cavity connected to the roots.
|
||||
cavity = connected(cx, &cavity, &roots)?;
|
||||
}
|
||||
Plan::Fail => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// The tetrahedra a site must remove, and its boundary faces (as sorted keys) it must remove.
|
||||
fn site_roots(cx: &mut TetComplex, site: Site) -> Result<(Vec<u32>, Vec<[u32; 3]>)> {
|
||||
Ok(match site {
|
||||
Site::Interior(t) => (vec![t], Vec::new()),
|
||||
Site::Face(t, k) => {
|
||||
let vs = cx.tet(t)?;
|
||||
if cx.adj(t)?.get(k) != Some(&NONE) {
|
||||
return Ok((Vec::new(), Vec::new()));
|
||||
}
|
||||
(vec![t], vec![sorted3(face_toward(vs, k))])
|
||||
}
|
||||
Site::Edge(a, b) => {
|
||||
let Some(ring) = cx.edge_ring(a, b)? else { return Ok((Vec::new(), Vec::new())) };
|
||||
if ring.closed {
|
||||
return Ok((Vec::new(), Vec::new()));
|
||||
}
|
||||
let mut faces = Vec::new();
|
||||
if let (Some(&v0), Some(&vl)) = (ring.verts.first(), ring.verts.last()) {
|
||||
faces.push(sorted3([a, b, v0]));
|
||||
faces.push(sorted3([a, b, vl]));
|
||||
}
|
||||
(ring.tets, faces)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
enum Plan {
|
||||
/// New tetrahedra as faces (to be joined to the new vertex) and new boundary edges.
|
||||
Ready {
|
||||
new: Vec<[u32; 3]>,
|
||||
new_boundary: Vec<((u32, u32), u32)>,
|
||||
},
|
||||
/// Drop this tetrahedron from the cavity and try again.
|
||||
Drop(u32),
|
||||
Fail,
|
||||
}
|
||||
|
||||
/// Plan the fill of a cavity from `p`.
|
||||
fn plan_cavity(cx: &TetComplex, cavity: &[u32], p: &Point3, site: Site, root_faces: &[[u32; 3]]) -> Result<Plan> {
|
||||
let inset: IdMap<u32, ()> = cavity.iter().map(|&t| (t, ())).collect();
|
||||
// Boundary faces of cavity tetrahedra: candidates for removal (only for boundary sites).
|
||||
let mut bfaces: Vec<([u32; 3], [u32; 3], u32, u32)> = Vec::new(); // (oriented, sorted, tag, tet)
|
||||
let mut outer: Vec<([u32; 3], u32)> = Vec::new(); // (oriented face, owning tet)
|
||||
for &t in cavity {
|
||||
let vs = cx.tet(t)?;
|
||||
let ns = cx.adj(t)?;
|
||||
let tags = cx.tags(t)?;
|
||||
for k in 0..4 {
|
||||
let n = ns[k];
|
||||
if n != NONE && inset.contains_key(&n) {
|
||||
continue;
|
||||
}
|
||||
let f = face_toward(vs, k);
|
||||
if n == NONE {
|
||||
bfaces.push((f, sorted3(f), tags[k], t));
|
||||
} else {
|
||||
outer.push((f, t));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Removed boundary faces D: for a boundary site, the root faces plus faces connected to them
|
||||
// across non-feature edges, on the same tag, whose circumcircle contains p (approximately:
|
||||
// the diametral sphere of the triangle).
|
||||
let mut removed: Vec<usize> = Vec::new();
|
||||
if !matches!(site, Site::Interior(_)) {
|
||||
let mut queue: Vec<usize> = bfaces.iter().enumerate().filter(|(_, f)| root_faces.contains(&f.1)).map(|(i, _)| i).collect();
|
||||
if queue.len() != root_faces.len() {
|
||||
return Ok(Plan::Fail);
|
||||
}
|
||||
let px = p.coords();
|
||||
let mut taken = vec![false; bfaces.len()];
|
||||
for &i in &queue {
|
||||
if let Some(s) = taken.get_mut(i) {
|
||||
*s = true;
|
||||
}
|
||||
}
|
||||
let mut qi = 0;
|
||||
while let Some(&i) = queue.get(qi) {
|
||||
qi += 1;
|
||||
let Some(&(f, _, tag, _)) = bfaces.get(i) else { continue };
|
||||
for e in 0..3 {
|
||||
let (u, w) = (f[e], f[(e + 1) % 3]);
|
||||
if cx.curve(u, w).is_some() {
|
||||
continue;
|
||||
}
|
||||
for (j, g) in bfaces.iter().enumerate() {
|
||||
if taken.get(j).copied().unwrap_or(true) || g.2 != tag || !(g.0.contains(&u) && g.0.contains(&w)) {
|
||||
continue;
|
||||
}
|
||||
let [ga, gb, gc] = [cx.xyz(g.0[0])?, cx.xyz(g.0[1])?, cx.xyz(g.0[2])?];
|
||||
let Some(o) = measure::triangle_circumcenter(ga, gb, gc) else { continue };
|
||||
let r = measure::dist(o, ga);
|
||||
if measure::dist(o, px) < r {
|
||||
if let Some(s) = taken.get_mut(j) {
|
||||
*s = true;
|
||||
}
|
||||
queue.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
removed = queue;
|
||||
// Vertices strictly inside D would be deleted: only allowed for face vertices of the
|
||||
// same tag that no kept face uses.
|
||||
let mut in_kept: IdMap<u32, ()> = IdMap::default();
|
||||
for (j, g) in bfaces.iter().enumerate() {
|
||||
if !removed.contains(&j) {
|
||||
for v in g.0 {
|
||||
in_kept.insert(v, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
for &(f, _) in &outer {
|
||||
for v in f {
|
||||
in_kept.insert(v, ());
|
||||
}
|
||||
}
|
||||
for &i in &removed {
|
||||
if let Some(g) = bfaces.get(i) {
|
||||
for v in g.0 {
|
||||
if !in_kept.contains_key(&v) && !matches!(cx.kind(v), VertexKind::Face(_) | VertexKind::Interior) {
|
||||
return Ok(Plan::Fail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Every kept face must see p strictly.
|
||||
let mut new = Vec::with_capacity(outer.len() + bfaces.len());
|
||||
for &(f, t) in &outer {
|
||||
if cx.orient_with(f, p)? != Sign::Positive {
|
||||
return Ok(Plan::Drop(t));
|
||||
}
|
||||
new.push(f);
|
||||
}
|
||||
for (j, &(f, _, _, t)) in bfaces.iter().enumerate() {
|
||||
if removed.contains(&j) {
|
||||
continue;
|
||||
}
|
||||
if cx.orient_with(f, p)? != Sign::Positive {
|
||||
return Ok(Plan::Drop(t));
|
||||
}
|
||||
new.push(f);
|
||||
}
|
||||
// The outline of D (edges in exactly one removed face) becomes new boundary, fanned to p.
|
||||
let mut new_boundary = Vec::new();
|
||||
if !removed.is_empty() {
|
||||
let mut count: IdMap<(u32, u32), ((u32, u32), u32, usize)> = IdMap::default();
|
||||
for &i in &removed {
|
||||
let Some(&(f, _, tag, _)) = bfaces.get(i) else { continue };
|
||||
for e in 0..3 {
|
||||
let (u, w) = (f[e], f[(e + 1) % 3]);
|
||||
let c = count.entry(sorted2(u, w)).or_insert(((u, w), tag, 0));
|
||||
c.2 += 1;
|
||||
}
|
||||
}
|
||||
let split = match site {
|
||||
Site::Edge(a, b) => Some(sorted2(a, b)),
|
||||
_ => None,
|
||||
};
|
||||
let mut edges: Vec<((u32, u32), u32)> = count.into_iter().filter(|(k, c)| c.2 == 1 && Some(*k) != split).map(|(_, c)| (c.0, c.1)).collect();
|
||||
edges.sort_unstable();
|
||||
new_boundary = edges;
|
||||
}
|
||||
// A cavity whose tetrahedra all go must still leave something to fill.
|
||||
if new.is_empty() {
|
||||
return Ok(Plan::Fail);
|
||||
}
|
||||
Ok(Plan::Ready { new, new_boundary })
|
||||
}
|
||||
|
||||
/// The tetrahedra of `cavity` connected to the roots through faces inside the cavity.
|
||||
fn connected(cx: &TetComplex, cavity: &[u32], roots: &[u32]) -> Result<Vec<u32>> {
|
||||
let inset: IdMap<u32, ()> = cavity.iter().map(|&t| (t, ())).collect();
|
||||
let mut seen: IdMap<u32, ()> = IdMap::default();
|
||||
let mut out: Vec<u32> = Vec::new();
|
||||
for &r in roots {
|
||||
if inset.contains_key(&r) && seen.insert(r, ()).is_none() {
|
||||
out.push(r);
|
||||
}
|
||||
}
|
||||
let mut i = 0;
|
||||
while let Some(&t) = out.get(i) {
|
||||
i += 1;
|
||||
for n in cx.adj(t)? {
|
||||
if n != NONE && inset.contains_key(&n) && seen.insert(n, ()).is_none() {
|
||||
out.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The edges of a set of tetrahedra, sorted and unique.
|
||||
pub fn edges_of(cx: &TetComplex, tets: &[u32]) -> Result<Vec<(u32, u32)>> {
|
||||
let mut out = Vec::with_capacity(tets.len() * 6);
|
||||
for &t in tets {
|
||||
let vs = cx.tet(t)?;
|
||||
out.extend(EDGES.iter().map(|&(i, j)| sorted2(vs[i], vs[j])));
|
||||
}
|
||||
out.sort_unstable();
|
||||
out.dedup();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Collapse vertex `a` onto its neighbour `b` (edge contraction): every tetrahedron around `a`
|
||||
/// takes `b` in its place and those holding the edge disappear. Allowed when `a` may move to `b`
|
||||
/// without leaving its boundary entity — an interior vertex anywhere, a face vertex along an edge
|
||||
/// inside its B-rep face, a curve vertex along its curve, never a corner — when the boundary
|
||||
/// faces it changes keep their orientation within 30°, every new edge is at most `max_len`, and
|
||||
/// the worst new tetrahedron is at least `min(worst old, floor)`. Returns whether it collapsed.
|
||||
///
|
||||
/// Edge contraction is the coarsening operation of mesh adaptation by local modification (X. Li,
|
||||
/// M. S. Shephard, M. W. Beall, *3D anisotropic mesh adaptation by mesh modification*, CMAME
|
||||
/// 194, 2005); here it removes the boundary recovery's closely spaced Steiner points and dense
|
||||
/// tessellation vertices where the size field asks for larger elements.
|
||||
pub fn collapse(cx: &mut TetComplex, a: u32, b: u32, max_len: f64, floor: f64) -> Result<bool> {
|
||||
let ka = cx.kind(a);
|
||||
let allowed = match ka {
|
||||
VertexKind::Corner => false,
|
||||
VertexKind::Interior => true,
|
||||
VertexKind::Curve(c) => cx.curve(a, b) == Some(c),
|
||||
VertexKind::Face(tag) => {
|
||||
if cx.curve(a, b).is_some() {
|
||||
false
|
||||
} else {
|
||||
match cx.kind(b) {
|
||||
VertexKind::Interior => false,
|
||||
_ => {
|
||||
// The edge must be a boundary edge inside face `tag`.
|
||||
let Some(ring) = cx.edge_ring(a, b)? else { return Ok(false) };
|
||||
if ring.closed {
|
||||
false
|
||||
} else {
|
||||
let ends = [ring.tets.first().copied(), ring.tets.last().copied()];
|
||||
let (v0, vl) = (ring.verts.first().copied().unwrap_or(NONE), ring.verts.last().copied().unwrap_or(NONE));
|
||||
boundary_tag(cx, ends[0].unwrap_or(NONE), [a, b, v0])? == Some(tag)
|
||||
&& boundary_tag(cx, ends[1].unwrap_or(NONE), [a, b, vl])? == Some(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if !allowed || cx.edge_tet(a, b)?.is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
let star = cx.star(a)?;
|
||||
let before = worst(cx, &star)?;
|
||||
let xb = cx.xyz(b)?;
|
||||
let mut new = Vec::with_capacity(star.len());
|
||||
let mut new_boundary = Vec::new();
|
||||
for &t in &star {
|
||||
let vs = cx.tet(t)?;
|
||||
if vs.contains(&b) {
|
||||
continue;
|
||||
}
|
||||
let moved = vs.map(|v| if v == a { b } else { v });
|
||||
for v in moved {
|
||||
if v != b && measure::dist(cx.xyz(v)?, xb) > max_len {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
let (ns, tags) = (cx.adj(t)?, cx.tags(t)?);
|
||||
for k in 0..4 {
|
||||
if ns[k] != NONE || vs[k] == a {
|
||||
continue;
|
||||
}
|
||||
let f = face_toward(vs, k);
|
||||
let g = f.map(|v| if v == a { b } else { v });
|
||||
let n0 = measure::triangle_normal(cx.xyz(f[0])?, cx.xyz(f[1])?, cx.xyz(f[2])?);
|
||||
let n1 = measure::triangle_normal(cx.xyz(g[0])?, cx.xyz(g[1])?, cx.xyz(g[2])?);
|
||||
if !(measure::dot(n0, n1) >= COLLAPSE_NORMAL_COS) {
|
||||
return Ok(false);
|
||||
}
|
||||
new_boundary.push((g, tags[k]));
|
||||
}
|
||||
new.push(moved);
|
||||
}
|
||||
if new.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut after = f64::INFINITY;
|
||||
for vs in &new {
|
||||
after = after.min(tet_quality(cx, *vs)?);
|
||||
}
|
||||
if !(after > 0.0) || after < before.min(floor) {
|
||||
return Ok(false);
|
||||
}
|
||||
// Curve edges at a move to b.
|
||||
let mut drop = Vec::new();
|
||||
let mut add = Vec::new();
|
||||
if let VertexKind::Curve(c) = ka {
|
||||
for u in cx.neighbours(a)? {
|
||||
if cx.curve(a, u) == Some(c) {
|
||||
drop.push((a, u));
|
||||
if u != b {
|
||||
add.push((b, u, c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let change = Change { old: star, new, new_boundary, drop_curves: drop.clone() };
|
||||
if cx.replace(&change)?.is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
for (x, y) in drop {
|
||||
cx.set_curve(x, y, None);
|
||||
}
|
||||
for (x, y, c) in add {
|
||||
cx.set_curve(x, y, Some(c));
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// cos 30°: the largest turn of a boundary face's normal a collapse may cause.
|
||||
const COLLAPSE_NORMAL_COS: f64 = 0.866_025_403_784_438_6;
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Quadratic tetrahedra (tet10): a node on every edge, at the midpoint inside the domain and on
|
||||
//! the geometry on the boundary — projected onto the edge's feature curve, or onto the B-rep face
|
||||
//! of a boundary edge inside one face — so curved boundaries are represented to second order.
|
||||
//!
|
||||
//! A curved edge can make an element invalid (a thin element against a strongly curved face).
|
||||
//! Every element is checked with the certified Jacobian test that the validity gate uses
|
||||
//! ([`crate::quality::element_validity`], which proves FieldCraft's criterion on the whole
|
||||
//! element); an element that fails gets its curved edges straightened (the node goes back to the
|
||||
//! straight midpoint, for every element sharing that edge) and the elements around are checked
|
||||
//! again. Straightening only ever moves nodes back to the linear mesh, which is valid, so the loop
|
||||
//! ends; it is also capped by the number of edges.
|
||||
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
use crate::quality::{Validity, element_validity};
|
||||
use crate::{Error, Result};
|
||||
|
||||
use super::TaggedFace;
|
||||
use super::complex::{IdMap, NONE, face_toward, sorted2};
|
||||
use super::geometry::{BoundaryGeometry, Tessellation};
|
||||
use super::measure;
|
||||
|
||||
/// Tet10 mid-side nodes on edges 1-2, 2-3, 3-1, 1-4, 2-4, 3-4 (the model's node order).
|
||||
use crate::quality::jacobian::TET_EDGES as MID_EDGES;
|
||||
|
||||
/// The quadratic mesh.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Quadratic {
|
||||
pub points: Vec<[f64; 3]>,
|
||||
pub elements: Vec<Vec<u32>>,
|
||||
/// Edges left straight to keep their elements valid.
|
||||
pub straightened: usize,
|
||||
}
|
||||
|
||||
/// Tet10 elements from tet4 ones (see the module docs).
|
||||
pub fn tet10(
|
||||
points: &[[f64; 3]],
|
||||
tets: &[[u32; 4]],
|
||||
boundary: &[TaggedFace],
|
||||
curves: &IdMap<(u32, u32), u32>,
|
||||
curve_faces: &[[u32; 2]],
|
||||
geometry: &Tessellation,
|
||||
) -> Result<Quadratic> {
|
||||
// Boundary edges and the B-rep face they lie on.
|
||||
let mut edge_face: IdMap<(u32, u32), u32> = IdMap::default();
|
||||
for b in boundary {
|
||||
let t = tets.get(b.tet as usize).ok_or_else(|| Error::Failed("tet10: boundary face of no tetrahedron".into()))?;
|
||||
let f = face_toward(*t, usize::from(b.opposite));
|
||||
for i in 0..3 {
|
||||
edge_face.entry(sorted2(f[i], f[(i + 1) % 3])).or_insert(b.face);
|
||||
}
|
||||
}
|
||||
let p = |v: u32| points.get(v as usize).copied().ok_or_else(|| Error::Failed("tet10: vertex id".into()));
|
||||
let mut out = points.to_vec();
|
||||
// Edge -> (node, straight midpoint).
|
||||
let mut node: IdMap<(u32, u32), (u32, [f64; 3])> = IdMap::default();
|
||||
let mut elements = Vec::with_capacity(tets.len());
|
||||
for t in tets {
|
||||
let mut e = t.to_vec();
|
||||
for &(i, j) in &MID_EDGES {
|
||||
let key = sorted2(t[i], t[j]);
|
||||
let id = match node.get(&key) {
|
||||
Some(&(id, _)) => id,
|
||||
None => {
|
||||
let m = measure::mid(p(t[i])?, p(t[j])?);
|
||||
let curved = match curves.get(&key) {
|
||||
Some(&c) => curve_faces.get(c as usize).and_then(|f| geometry.project_curve(*f, m)),
|
||||
None => edge_face.get(&key).and_then(|&f| geometry.project_face(f, m)),
|
||||
};
|
||||
let id = u32::try_from(out.len()).map_err(|_| Error::Limit("too many nodes".into()))?;
|
||||
out.push(curved.filter(|q| q.iter().all(|x| x.is_finite())).unwrap_or(m));
|
||||
node.insert(key, (id, m));
|
||||
id
|
||||
}
|
||||
};
|
||||
e.push(id);
|
||||
}
|
||||
elements.push(e);
|
||||
}
|
||||
// Straighten what makes an element invalid.
|
||||
let coords = |out: &[[f64; 3]], e: &[u32]| -> Result<Vec<[f64; 3]>> {
|
||||
e.iter().map(|&v| out.get(v as usize).copied().ok_or_else(|| Error::Failed("tet10: node id".into()))).collect()
|
||||
};
|
||||
let mut straightened = 0;
|
||||
let mut check: Vec<usize> = (0..elements.len()).collect();
|
||||
let mut edge_elements: IdMap<(u32, u32), Vec<usize>> = IdMap::default();
|
||||
for (n, t) in tets.iter().enumerate() {
|
||||
for &(i, j) in &MID_EDGES {
|
||||
edge_elements.entry(sorted2(t[i], t[j])).or_default().push(n);
|
||||
}
|
||||
}
|
||||
for _ in 0..=node.len() {
|
||||
let mut next = Vec::new();
|
||||
for &n in &check {
|
||||
let Some(e) = elements.get(n) else { continue };
|
||||
if element_validity(ElementKind::Tet10, &coords(&out, e)?)?.validity == Validity::Valid {
|
||||
continue;
|
||||
}
|
||||
let t = tets.get(n).copied().unwrap_or([NONE; 4]);
|
||||
for &(i, j) in &MID_EDGES {
|
||||
let key = sorted2(t[i], t[j]);
|
||||
let Some(&(id, m)) = node.get(&key) else { continue };
|
||||
if out.get(id as usize) != Some(&m) {
|
||||
if let Some(s) = out.get_mut(id as usize) {
|
||||
*s = m;
|
||||
}
|
||||
straightened += 1;
|
||||
next.extend(edge_elements.get(&key).cloned().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
}
|
||||
next.sort_unstable();
|
||||
next.dedup();
|
||||
if next.is_empty() {
|
||||
break;
|
||||
}
|
||||
check = next;
|
||||
}
|
||||
Ok(Quadratic { points: out, elements, straightened })
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
//! Constrained Delaunay refinement to a size field.
|
||||
//!
|
||||
//! A tetrahedron is bad when its radius–edge ratio exceeds a bound `B` (about 2) or its
|
||||
//! circumradius exceeds the local target size; its circumcentre is inserted (Delaunay insertion,
|
||||
//! [`ops::insert_delaunay`]). A circumcentre outside the domain, or inside the diametral sphere of
|
||||
//! a boundary face (it *encroaches* the face), is not inserted; the encroached boundary face is
|
||||
//! split instead, at its circumcentre when that lies inside it and otherwise at the midpoint of
|
||||
//! its longest edge (which splits a feature edge when the edge is one), and every boundary point
|
||||
//! is projected onto its B-rep face or feature curve ([`super::geometry`]).
|
||||
//!
|
||||
//! Sources: J. R. Shewchuk, *Tetrahedral Mesh Generation by Delaunay Refinement*, SoCG 1998
|
||||
//! (radius–edge refinement with encroachment rules, termination for B = 2 and input angles of 90°
|
||||
//! or more); H. Si, *Constrained Delaunay tetrahedral mesh generation and refinement*, FEAD
|
||||
//! 46(1–2), 2010 (refinement of a constrained Delaunay tetrahedralization with small input
|
||||
//! angles). CAD boundaries have small angles, so termination is guaranteed here by bounds
|
||||
//! instead: a boundary face smaller than a fraction of the local size is not split, a
|
||||
//! tetrahedron whose point could not be inserted is not retried, and the number of insertions
|
||||
//! and tetrahedra are capped (the stop is reported).
|
||||
|
||||
use crate::predicates::{Point3, snap_to_domain};
|
||||
use crate::size::SizeField;
|
||||
use crate::{Error, Result};
|
||||
|
||||
use super::complex::{IdMap, NONE, TetComplex, VertexKind, Walk, face_toward, sorted2, sorted4};
|
||||
use super::geometry::BoundaryGeometry;
|
||||
use super::measure::{self, P};
|
||||
use super::ops::{self, Site};
|
||||
|
||||
/// What the boundary insertions project onto.
|
||||
pub struct Context<'a> {
|
||||
pub size: &'a dyn SizeField,
|
||||
pub geometry: &'a dyn BoundaryGeometry,
|
||||
/// The B-rep faces of each feature curve (by curve id).
|
||||
pub curve_faces: &'a [[u32; 2]],
|
||||
}
|
||||
|
||||
impl Context<'_> {
|
||||
/// The local target size, or `fallback` when the field misbehaves.
|
||||
pub fn h(&self, p: P, fallback: f64) -> f64 {
|
||||
let h = self.size.size(p);
|
||||
if h > 0.0 && h.is_finite() { h } else { fallback }
|
||||
}
|
||||
|
||||
/// `p` moved onto the boundary entity of a vertex of `kind`, as an admissible point.
|
||||
pub fn place(&self, kind: VertexKind, p: P) -> Option<Point3> {
|
||||
let q = match kind {
|
||||
VertexKind::Interior => p,
|
||||
VertexKind::Face(f) => self.geometry.project_face(f, p)?,
|
||||
VertexKind::Curve(c) => self.geometry.project_curve(*self.curve_faces.get(c as usize)?, p)?,
|
||||
VertexKind::Corner => return None,
|
||||
};
|
||||
Point3::try_from(q.map(snap_to_domain)).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Refinement settings.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RefineOptions {
|
||||
/// The radius–edge bound `B`.
|
||||
pub radius_edge: f64,
|
||||
/// A tetrahedron is too large when its circumradius exceeds this times the local size (the
|
||||
/// regular tetrahedron with edge `h` has circumradius 0.61 h).
|
||||
pub size_factor: f64,
|
||||
/// Boundary faces whose circumradius is below this times the local size are not split.
|
||||
pub min_face: f64,
|
||||
/// A tetrahedron is refined for its shape (radius–edge ratio) only when its circumradius is
|
||||
/// at least this times the local size; smaller ones are left to smoothing and improvement,
|
||||
/// so badly shaped input near the boundary does not cascade into refinement far below the
|
||||
/// target size.
|
||||
pub min_shape_radius: f64,
|
||||
/// Most vertices inserted.
|
||||
pub max_insertions: usize,
|
||||
}
|
||||
|
||||
impl Default for RefineOptions {
|
||||
fn default() -> Self {
|
||||
RefineOptions { radius_edge: 2.0, size_factor: 0.95, min_face: 0.5, min_shape_radius: 0.5, max_insertions: 2_000_000 }
|
||||
}
|
||||
}
|
||||
|
||||
/// What a refinement did.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct RefineStats {
|
||||
pub interior: usize,
|
||||
pub boundary: usize,
|
||||
pub skipped: usize,
|
||||
/// Why it stopped before every tetrahedron was good, if it did.
|
||||
pub stopped: Option<String>,
|
||||
}
|
||||
|
||||
/// Refinement never puts a vertex closer than this times the local size to another.
|
||||
pub const MIN_GAP: f64 = 0.05;
|
||||
|
||||
/// Most tetrahedra a Delaunay cavity may hold.
|
||||
pub const MAX_CAVITY: usize = 4096;
|
||||
/// Most steps of a point location walk.
|
||||
const MAX_WALK: usize = 1 << 16;
|
||||
|
||||
/// The boundary face of a tetrahedron whose diametral sphere holds `p`, if any (the largest).
|
||||
fn encroached(cx: &TetComplex, tets: &[u32], p: P) -> Result<Option<(u32, usize)>> {
|
||||
let mut best: Option<(f64, u32, usize)> = None;
|
||||
for &t in tets {
|
||||
let vs = cx.tet(t)?;
|
||||
let ns = cx.adj(t)?;
|
||||
for k in 0..4 {
|
||||
if ns[k] != NONE {
|
||||
continue;
|
||||
}
|
||||
let f = face_toward(vs, k);
|
||||
let (a, b, c) = (cx.xyz(f[0])?, cx.xyz(f[1])?, cx.xyz(f[2])?);
|
||||
let Some(o) = measure::triangle_circumcenter(a, b, c) else { continue };
|
||||
let r = measure::dist(o, a);
|
||||
if measure::dist(o, p) < r && best.is_none_or(|(br, _, _)| r > br) {
|
||||
best = Some((r, t, k));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(best.map(|(_, t, k)| (t, k)))
|
||||
}
|
||||
|
||||
/// The tetrahedra whose circumsphere holds `p`, grown from `t` across interior faces.
|
||||
fn conflicts(cx: &TetComplex, t: u32, p: &Point3) -> Result<Vec<u32>> {
|
||||
let mut out = vec![t];
|
||||
let mut seen: IdMap<u32, ()> = IdMap::default();
|
||||
seen.insert(t, ());
|
||||
let mut i = 0;
|
||||
while let Some(&u) = out.get(i) {
|
||||
i += 1;
|
||||
if out.len() > MAX_CAVITY {
|
||||
break;
|
||||
}
|
||||
for n in cx.adj(u)? {
|
||||
if n != NONE && !seen.contains_key(&n) {
|
||||
seen.insert(n, ());
|
||||
if cx.in_sphere(cx.tet(n)?, p)? {
|
||||
out.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Split boundary face `k` of tetrahedron `t`: at its circumcentre when inside it, else at the
|
||||
/// midpoint of its longest edge. Returns the new vertex.
|
||||
pub fn split_face(cx: &mut TetComplex, ctx: &Context, t: u32, k: usize, min_face: f64) -> Result<Option<u32>> {
|
||||
let vs = cx.tet(t)?;
|
||||
if cx.adj(t)?.get(k) != Some(&NONE) {
|
||||
return Ok(None);
|
||||
}
|
||||
let tag = cx.tags(t)?.get(k).copied().unwrap_or(NONE);
|
||||
let f = face_toward(vs, k);
|
||||
let x = [cx.xyz(f[0])?, cx.xyz(f[1])?, cx.xyz(f[2])?];
|
||||
let Some(o) = measure::triangle_circumcenter(x[0], x[1], x[2]) else { return Ok(None) };
|
||||
let r = measure::dist(o, x[0]);
|
||||
let centroid = measure::scale(measure::add(measure::add(x[0], x[1]), x[2]), 1.0 / 3.0);
|
||||
if r < min_face * ctx.h(centroid, r) {
|
||||
return Ok(None);
|
||||
}
|
||||
// Barycentric coordinates of o (signed areas against the face normal).
|
||||
let n = measure::cross(measure::sub(x[1], x[0]), measure::sub(x[2], x[0]));
|
||||
let inside = (0..3).all(|i| {
|
||||
let (a, b) = (x[(i + 1) % 3], x[(i + 2) % 3]);
|
||||
measure::dot(measure::cross(measure::sub(b, a), measure::sub(o, a)), n) >= 0.0
|
||||
});
|
||||
if inside && tag != NONE {
|
||||
// A point inside the diametral sphere of one of the face's feature edges encroaches it:
|
||||
// split the edge instead (Shewchuk 1998).
|
||||
for i in 0..3 {
|
||||
let (u, w) = (f[i], f[(i + 1) % 3]);
|
||||
if cx.curve(u, w).is_some() {
|
||||
let (a, b) = (x[i], x[(i + 1) % 3]);
|
||||
if measure::dist(o, measure::mid(a, b)) < 0.5 * measure::dist(a, b) {
|
||||
return split_edge(cx, ctx, u, w, tag, min_face);
|
||||
}
|
||||
}
|
||||
}
|
||||
let kind = VertexKind::Face(tag);
|
||||
let Some(p) = ctx.place(kind, o) else { return Ok(None) };
|
||||
// The projection may only move the point a little, and never next to a corner.
|
||||
let shortest = (0..3).map(|i| measure::dist(x[i], x[(i + 1) % 3])).fold(f64::INFINITY, f64::min);
|
||||
if measure::dist(p.coords(), o) > 0.5 * r || x.iter().any(|c| measure::dist(*c, p.coords()) < 0.25 * shortest) {
|
||||
return Ok(None);
|
||||
}
|
||||
return ops::insert_delaunay_apart(cx, p, kind, Site::Face(t, k), MAX_CAVITY, MIN_GAP * ctx.h(o, r));
|
||||
}
|
||||
// The longest edge.
|
||||
let (mut u, mut w, mut len) = (f[0], f[1], -1.0);
|
||||
for i in 0..3 {
|
||||
let l = measure::dist(x[i], x[(i + 1) % 3]);
|
||||
if l > len {
|
||||
(u, w, len) = (f[i], f[(i + 1) % 3], l);
|
||||
}
|
||||
}
|
||||
split_edge(cx, ctx, u, w, tag, min_face)
|
||||
}
|
||||
|
||||
/// Split boundary edge `uw` at its midpoint, projected onto its feature curve or onto B-rep face
|
||||
/// `tag`. An edge shorter than `2 · min_face` times the local size is not split.
|
||||
pub fn split_edge(cx: &mut TetComplex, ctx: &Context, u: u32, w: u32, tag: u32, min_face: f64) -> Result<Option<u32>> {
|
||||
let m = measure::mid(cx.xyz(u)?, cx.xyz(w)?);
|
||||
let len = measure::dist(cx.xyz(u)?, cx.xyz(w)?);
|
||||
if len < 2.0 * min_face * ctx.h(m, len) {
|
||||
return Ok(None);
|
||||
}
|
||||
let kind = match cx.curve(u, w) {
|
||||
Some(c) => VertexKind::Curve(c),
|
||||
None if tag != NONE => VertexKind::Face(tag),
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(p) = ctx.place(kind, m) else { return Ok(None) };
|
||||
// The projection may only move the midpoint a little (a quarter of the edge).
|
||||
if measure::dist(p.coords(), m) > 0.25 * len {
|
||||
return Ok(None);
|
||||
}
|
||||
ops::insert_delaunay_apart(cx, p, kind, Site::Edge(u, w), MAX_CAVITY, MIN_GAP * ctx.h(m, len))
|
||||
}
|
||||
|
||||
/// Whether a tetrahedron is bad, and its circumcentre.
|
||||
fn badness(x: &[P; 4], ctx: &Context, o: &RefineOptions) -> Option<P> {
|
||||
let c = measure::circumcenter(x)?;
|
||||
let r = measure::dist(c, x[0]);
|
||||
let (lmin, lmax) = measure::edge_range(x);
|
||||
let h = ctx.h(measure::centroid(x), lmax);
|
||||
((r > o.radius_edge * lmin && r > o.min_shape_radius * h) || r > o.size_factor * h).then_some(c)
|
||||
}
|
||||
|
||||
/// Refine until no tetrahedron is bad (see the module docs).
|
||||
pub fn refine(cx: &mut TetComplex, ctx: &Context, o: &RefineOptions) -> Result<RefineStats> {
|
||||
let mut stats = RefineStats::default();
|
||||
let mut queue: Vec<u32> = cx.live_tets().map(|(t, _)| t).collect();
|
||||
let mut given_up: IdMap<[u32; 4], ()> = IdMap::default();
|
||||
let mut head = 0usize;
|
||||
let mut inserted = 0usize;
|
||||
while head < queue.len() {
|
||||
let t = queue.get(head).copied().unwrap_or(NONE);
|
||||
head += 1;
|
||||
// Keep the queue's memory bounded: drop the processed prefix now and then.
|
||||
if head > 1 << 20 {
|
||||
queue.drain(..head);
|
||||
head = 0;
|
||||
}
|
||||
if !cx.is_live(t) {
|
||||
continue;
|
||||
}
|
||||
let vs = cx.tet(t)?;
|
||||
let key = sorted4(vs);
|
||||
if given_up.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
let x = cx.coords(vs)?;
|
||||
let Some(c) = badness(&x, ctx, o) else { continue };
|
||||
if inserted >= o.max_insertions {
|
||||
stats.stopped = Some(format!("refinement stopped at {} inserted vertices", o.max_insertions));
|
||||
break;
|
||||
}
|
||||
if cx.live_count() + 64 >= cx.max_tets() || cx.point_count() + 64 >= cx.max_points() {
|
||||
stats.stopped = Some(format!("refinement stopped at {} tetrahedra and {} vertices", cx.live_count(), cx.point_count()));
|
||||
break;
|
||||
}
|
||||
let result = match Point3::try_from(c.map(snap_to_domain)) {
|
||||
Err(_) => None,
|
||||
Ok(p) => {
|
||||
// The boundary face the circumcentre lies beyond or encroaches, if any.
|
||||
let (inside, face) = match cx.walk(t, &p, MAX_WALK)? {
|
||||
None => (None, None),
|
||||
Some(Walk::Outside(tb, k)) => (None, Some((tb, k))),
|
||||
Some(Walk::Inside(ti)) => (Some(ti), encroached(cx, &conflicts(cx, ti, &p)?, c)?),
|
||||
};
|
||||
match (inside, face) {
|
||||
(_, Some((tb, k))) => {
|
||||
let v = split_face(cx, ctx, tb, k, o.min_face)?;
|
||||
stats.boundary += usize::from(v.is_some());
|
||||
v
|
||||
}
|
||||
(Some(ti), None) => {
|
||||
let v = ops::insert_delaunay_apart(cx, p, VertexKind::Interior, Site::Interior(ti), MAX_CAVITY, MIN_GAP * ctx.h(c, 1.0))?;
|
||||
stats.interior += usize::from(v.is_some());
|
||||
v
|
||||
}
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
};
|
||||
match result {
|
||||
Some(v) => {
|
||||
inserted += 1;
|
||||
queue.extend(cx.star(v)?);
|
||||
// The bad tetrahedron may survive a boundary split: look at it again.
|
||||
if cx.is_live(t) {
|
||||
queue.push(t);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
stats.skipped += 1;
|
||||
given_up.insert(key, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// The feature curves of a boundary: an edge between two boundary faces is a feature edge when
|
||||
/// their tags differ or their normals turn by more than `feature_cos` (cosine). Returns the
|
||||
/// curve table (face pairs) and the edges with their curve.
|
||||
pub fn feature_curves(cx: &TetComplex, feature_cos: f64) -> Result<(Vec<[u32; 2]>, Vec<((u32, u32), u32)>)> {
|
||||
// Boundary edge -> (tag, oriented normal) of each side.
|
||||
let mut sides: IdMap<(u32, u32), Vec<(u32, P)>> = IdMap::default();
|
||||
for (t, vs) in cx.live_tets() {
|
||||
let ns = cx.adj(t)?;
|
||||
let tags = cx.tags(t)?;
|
||||
for k in 0..4 {
|
||||
if ns[k] != NONE {
|
||||
continue;
|
||||
}
|
||||
let f = face_toward(vs, k);
|
||||
let n = measure::triangle_normal(cx.xyz(f[0])?, cx.xyz(f[1])?, cx.xyz(f[2])?);
|
||||
for i in 0..3 {
|
||||
sides.entry(sorted2(f[i], f[(i + 1) % 3])).or_default().push((tags[k], n));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut keys: Vec<(u32, u32)> = sides.keys().copied().collect();
|
||||
keys.sort_unstable();
|
||||
let mut table: Vec<[u32; 2]> = Vec::new();
|
||||
let mut index: IdMap<[u32; 2], u32> = IdMap::default();
|
||||
let mut edges = Vec::new();
|
||||
for e in keys {
|
||||
let s = sides.get(&e).map(Vec::as_slice).unwrap_or_default();
|
||||
let [(ta, na), (tb, nb)] = s else {
|
||||
return Err(Error::Failed(format!("boundary edge {e:?} has {} faces", s.len())));
|
||||
};
|
||||
if ta != tb || measure::dot(*na, *nb) < feature_cos {
|
||||
let pair = [(*ta).min(*tb), (*ta).max(*tb)];
|
||||
let id = match index.get(&pair) {
|
||||
Some(&id) => id,
|
||||
None => {
|
||||
let id = table.len() as u32;
|
||||
table.push(pair);
|
||||
index.insert(pair, id);
|
||||
id
|
||||
}
|
||||
};
|
||||
edges.push((e, id));
|
||||
}
|
||||
}
|
||||
Ok((table, edges))
|
||||
}
|
||||
|
||||
/// Coarsening: collapse edges shorter than `short` times the local size, shortest (relative)
|
||||
/// first, each onto whichever end it may move to ([`ops::collapse`]); new edges stay below
|
||||
/// `long` times the size, and the worst tetrahedron around does not drop below
|
||||
/// `min(its old worst, floor)`. At most `passes` passes. Returns the collapses.
|
||||
pub fn coarsen(cx: &mut TetComplex, ctx: &Context, short: f64, long: f64, floor: f64, passes: usize) -> Result<usize> {
|
||||
let mut done = 0;
|
||||
for _ in 0..passes {
|
||||
let mut edges: Vec<(f64, u32, u32)> = Vec::new();
|
||||
let mut seen: IdMap<(u32, u32), ()> = IdMap::default();
|
||||
for (_, vs) in cx.live_tets() {
|
||||
for &(i, j) in &super::complex::EDGES {
|
||||
let e = sorted2(vs[i], vs[j]);
|
||||
if seen.insert(e, ()).is_some() {
|
||||
continue;
|
||||
}
|
||||
let (a, b) = (cx.xyz(e.0)?, cx.xyz(e.1)?);
|
||||
let l = measure::dist(a, b);
|
||||
let h = ctx.h(measure::mid(a, b), l);
|
||||
if l < short * h {
|
||||
edges.push((l / h, e.0, e.1));
|
||||
}
|
||||
}
|
||||
}
|
||||
edges.sort_by(|x, y| x.0.total_cmp(&y.0).then(x.1.cmp(&y.1)).then(x.2.cmp(&y.2)));
|
||||
let mut changed = 0;
|
||||
for (_, a, b) in edges {
|
||||
if !cx.is_used(a) || !cx.is_used(b) {
|
||||
continue;
|
||||
}
|
||||
let (pa, pb) = (cx.xyz(a)?, cx.xyz(b)?);
|
||||
let max_len = long * ctx.h(measure::mid(pa, pb), measure::dist(pa, pb));
|
||||
if ops::collapse(cx, a, b, max_len, floor)? || ops::collapse(cx, b, a, max_len, floor)? {
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
done += changed;
|
||||
if changed == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(done)
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
//! Vertex smoothing: optimal Delaunay triangulation (ODT) moves, and optimization-based smoothing
|
||||
//! of the worst tetrahedron around a vertex.
|
||||
//!
|
||||
//! Boundary vertices move on their B-rep face (in its tangent plane, then projected) or along
|
||||
//! their feature curve; corners never move. Every move keeps every tetrahedron around the vertex
|
||||
//! positive (exact `orient3d`), or is not made.
|
||||
//!
|
||||
//! Sources:
|
||||
//! - L. Chen, J. Xu, *Optimal Delaunay triangulations*, J. Comput. Math. 22(2), 2004; L. Chen,
|
||||
//! M. Holst, *Efficient mesh optimization schemes based on Optimal Delaunay Triangulations*,
|
||||
//! CMAME 200, 2011; P. Alliez, D. Cohen-Steiner, M. Yvinec, M. Desbrun, *Variational
|
||||
//! tetrahedral meshing*, ACM TOG 24(3), 2005: the ODT update moves a vertex to the
|
||||
//! volume-weighted average of the circumcentres of its tetrahedra, weighted here by the density
|
||||
//! `h⁻³` of the size field (J. Tournois, C. Wormser, P. Alliez, M. Desbrun, *Interleaving
|
||||
//! Delaunay refinement and optimization for practical isotropic tetrahedron mesh generation*,
|
||||
//! ACM TOG 28(3), 2009, which interleaves these moves with refinement).
|
||||
//! - L. Freitag, M. Jones, P. Plassmann, *An efficient parallel algorithm for mesh smoothing*,
|
||||
//! IMR 1995; L. Freitag, C. Ollivier-Gooch, *Tetrahedral mesh improvement using swapping and
|
||||
//! smoothing*, IJNME 40, 1997; Klingner and Shewchuk 2007, §3.1: nonsmooth optimization of the
|
||||
//! worst quality around a vertex (active set, steepest ascent along the smallest vector in the
|
||||
//! convex hull of the active gradients, line search), applied only when the worst improves.
|
||||
|
||||
use crate::Result;
|
||||
|
||||
use super::complex::{NONE, TetComplex, VertexKind, face_toward};
|
||||
use super::measure::{self, P, add, dot, scale, sub};
|
||||
use super::refine::Context;
|
||||
|
||||
/// The worst quality around `v` with `v` at `p` (the tetrahedra of `star`).
|
||||
fn star_quality(cx: &TetComplex, v: u32, p: P, star: &[u32]) -> Result<f64> {
|
||||
let mut q = f64::INFINITY;
|
||||
for &t in star {
|
||||
let vs = cx.tet(t)?;
|
||||
let mut x = cx.coords(vs)?;
|
||||
for (i, &u) in vs.iter().enumerate() {
|
||||
if u == v
|
||||
&& let Some(s) = x.get_mut(i)
|
||||
{
|
||||
*s = p;
|
||||
}
|
||||
}
|
||||
q = q.min(measure::quality(&x));
|
||||
}
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
/// How `v` may move: `None` for a corner, the identity for an interior vertex, the tangent
|
||||
/// plane's normal for a face vertex, the tangent line for a curve vertex.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Freedom {
|
||||
Free,
|
||||
Plane(P),
|
||||
Line(P),
|
||||
}
|
||||
|
||||
fn freedom(cx: &mut TetComplex, v: u32, star: &[u32]) -> Result<Option<Freedom>> {
|
||||
Ok(match cx.kind(v) {
|
||||
VertexKind::Corner => None,
|
||||
VertexKind::Interior => Some(Freedom::Free),
|
||||
VertexKind::Face(_) => {
|
||||
let mut n = [0.0; 3];
|
||||
for &t in star {
|
||||
let vs = cx.tet(t)?;
|
||||
let ns = cx.adj(t)?;
|
||||
for k in 0..4 {
|
||||
if ns[k] == NONE && vs[k] != v {
|
||||
let f = face_toward(vs, k);
|
||||
let (a, b, c) = (cx.xyz(f[0])?, cx.xyz(f[1])?, cx.xyz(f[2])?);
|
||||
n = add(n, measure::cross(sub(b, a), sub(c, a)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let l = measure::norm(n);
|
||||
if !(l > 0.0) {
|
||||
return Ok(None);
|
||||
}
|
||||
Some(Freedom::Plane(scale(n, 1.0 / l)))
|
||||
}
|
||||
VertexKind::Curve(c) => {
|
||||
let ends: Vec<u32> = cx.neighbours(v)?.into_iter().filter(|&u| cx.curve(v, u) == Some(c)).collect();
|
||||
let [a, b] = ends.as_slice() else { return Ok(None) };
|
||||
let d = sub(cx.xyz(*b)?, cx.xyz(*a)?);
|
||||
let l = measure::norm(d);
|
||||
if !(l > 0.0) {
|
||||
return Ok(None);
|
||||
}
|
||||
Some(Freedom::Line(scale(d, 1.0 / l)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn restrict(d: P, f: Freedom) -> P {
|
||||
match f {
|
||||
Freedom::Free => d,
|
||||
Freedom::Plane(n) => sub(d, scale(n, dot(d, n))),
|
||||
Freedom::Line(t) => scale(t, dot(d, t)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try moving `v` to `target` (restricted to its freedom and placed on its boundary entity),
|
||||
/// at full, half and quarter steps; accept the first that keeps the star valid and its worst
|
||||
/// quality at least `floor`. Returns whether it moved.
|
||||
fn try_move(cx: &mut TetComplex, ctx: &Context, v: u32, star: &[u32], f: Freedom, target: P, floor: f64) -> Result<bool> {
|
||||
let x = cx.xyz(v)?;
|
||||
let d = restrict(sub(target, x), f);
|
||||
for s in [1.0, 0.5, 0.25] {
|
||||
let Some(p) = ctx.place(cx.kind(v), add(x, scale(d, s))) else { continue };
|
||||
if p.coords() == x || !cx.move_is_valid(v, &p, star)? {
|
||||
continue;
|
||||
}
|
||||
if star_quality(cx, v, p.coords(), star)? >= floor {
|
||||
cx.move_point(v, p)?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// The quality a smoothing move may not go below (sin 17.5°) unless the star was already worse.
|
||||
const ODT_FLOOR: f64 = 0.3;
|
||||
|
||||
/// One ODT pass over the vertices (ascending ids) that may move. Returns the number moved.
|
||||
pub fn odt_pass(cx: &mut TetComplex, ctx: &Context) -> Result<usize> {
|
||||
let mut moved = 0;
|
||||
for v in 0..cx.point_count() as u32 {
|
||||
if !cx.is_used(v) || cx.kind(v) == VertexKind::Corner {
|
||||
continue;
|
||||
}
|
||||
let star = cx.star(v)?;
|
||||
let Some(f) = freedom(cx, v, &star)? else { continue };
|
||||
let (mut num, mut den) = ([0.0; 3], 0.0);
|
||||
for &t in &star {
|
||||
let x = cx.coords(cx.tet(t)?)?;
|
||||
let vol = measure::volume6(&x) / 6.0;
|
||||
let g = measure::centroid(&x);
|
||||
let (_, lmax) = measure::edge_range(&x);
|
||||
let c = measure::circumcenter(&x).filter(|c| measure::dist(*c, g) <= lmax).unwrap_or(g);
|
||||
let h = ctx.h(g, lmax);
|
||||
let w = vol / (h * h * h);
|
||||
if !(w > 0.0 && w.is_finite()) {
|
||||
continue;
|
||||
}
|
||||
num = add(num, scale(c, w));
|
||||
den += w;
|
||||
}
|
||||
if !(den > 0.0) {
|
||||
continue;
|
||||
}
|
||||
let target = scale(num, 1.0 / den);
|
||||
let before = star_quality(cx, v, cx.xyz(v)?, &star)?;
|
||||
if try_move(cx, ctx, v, &star, f, target, before.min(ODT_FLOOR))? {
|
||||
moved += 1;
|
||||
}
|
||||
}
|
||||
Ok(moved)
|
||||
}
|
||||
|
||||
/// The smallest vector in the convex hull of `g` (Frank–Wolfe iterations).
|
||||
fn min_norm(g: &[P]) -> P {
|
||||
let Some(&first) = g.first() else { return [0.0; 3] };
|
||||
let mut d = first;
|
||||
for _ in 0..64 {
|
||||
let mut s = first;
|
||||
let mut best = f64::INFINITY;
|
||||
for &gi in g {
|
||||
let v = dot(gi, d);
|
||||
if v < best {
|
||||
best = v;
|
||||
s = gi;
|
||||
}
|
||||
}
|
||||
let e = sub(d, s);
|
||||
let gap = dot(e, d);
|
||||
let ee = dot(e, e);
|
||||
if !(gap > 1e-30) || !(ee > 0.0) {
|
||||
break;
|
||||
}
|
||||
let gamma = (gap / ee).clamp(0.0, 1.0);
|
||||
d = sub(d, scale(e, gamma));
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// The star of a vertex as plain coordinates: per tetrahedron, its four corners and the slot of
|
||||
/// the moving vertex (so a trial position costs no lookups).
|
||||
struct LocalStar {
|
||||
tets: Vec<([P; 4], usize)>,
|
||||
}
|
||||
|
||||
impl LocalStar {
|
||||
fn new(cx: &TetComplex, v: u32, star: &[u32]) -> Result<LocalStar> {
|
||||
let mut tets = Vec::with_capacity(star.len());
|
||||
for &t in star {
|
||||
let vs = cx.tet(t)?;
|
||||
let i = vs.iter().position(|&u| u == v).unwrap_or(0);
|
||||
tets.push((cx.coords(vs)?, i));
|
||||
}
|
||||
Ok(LocalStar { tets })
|
||||
}
|
||||
|
||||
fn quality_at(&self, k: usize, p: P) -> f64 {
|
||||
let Some((x, i)) = self.tets.get(k) else { return f64::NEG_INFINITY };
|
||||
let mut y = *x;
|
||||
if let Some(s) = y.get_mut(*i) {
|
||||
*s = p;
|
||||
}
|
||||
measure::quality(&y)
|
||||
}
|
||||
|
||||
fn worst_at(&self, p: P) -> f64 {
|
||||
(0..self.tets.len()).map(|k| self.quality_at(k, p)).fold(f64::INFINITY, f64::min)
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimization-based smoothing of `v`: raise the worst quality of its star (see the module
|
||||
/// docs). Returns whether it moved.
|
||||
pub fn smooth_vertex(cx: &mut TetComplex, ctx: &Context, v: u32) -> Result<bool> {
|
||||
if !cx.is_used(v) || cx.kind(v) == VertexKind::Corner {
|
||||
return Ok(false);
|
||||
}
|
||||
let star = cx.star(v)?;
|
||||
let Some(f) = freedom(cx, v, &star)? else { return Ok(false) };
|
||||
let local = LocalStar::new(cx, v, &star)?;
|
||||
let x0 = cx.xyz(v)?;
|
||||
// A length scale: the mean edge length from v.
|
||||
let mut len = 0.0;
|
||||
let mut count = 0usize;
|
||||
for (x, i) in &local.tets {
|
||||
for (k, p) in x.iter().enumerate() {
|
||||
if k != *i {
|
||||
len += measure::dist(*p, x0);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let len = len / count.max(1) as f64;
|
||||
if !(len > 0.0 && len.is_finite()) {
|
||||
return Ok(false);
|
||||
}
|
||||
let delta = 1e-6 * len;
|
||||
let mut x = x0;
|
||||
let mut worst = local.worst_at(x);
|
||||
let mut moved = false;
|
||||
for _ in 0..8 {
|
||||
// Qualities and gradients (central differences) of the tetrahedra near the worst.
|
||||
let mut qs = Vec::with_capacity(local.tets.len());
|
||||
for k in 0..local.tets.len() {
|
||||
let q = local.quality_at(k, x);
|
||||
let mut g = [0.0; 3];
|
||||
if q <= worst + 0.1 {
|
||||
for (c, gc) in g.iter_mut().enumerate() {
|
||||
let (mut a, mut b) = (x, x);
|
||||
a[c] += delta;
|
||||
b[c] -= delta;
|
||||
*gc = (local.quality_at(k, a) - local.quality_at(k, b)) / (2.0 * delta);
|
||||
}
|
||||
}
|
||||
qs.push((q, restrict(g, f)));
|
||||
}
|
||||
let active: Vec<P> = qs.iter().filter(|s| s.0 <= worst + 1e-4).map(|s| s.1).collect();
|
||||
let d = min_norm(&active);
|
||||
let dd = dot(d, d);
|
||||
if !(dd > 1e-20) {
|
||||
break;
|
||||
}
|
||||
// The step at which the linear model of an inactive tetrahedron meets the worst, capped
|
||||
// to a fifth of the edge length.
|
||||
let mut alpha = 0.2 * len / dd.sqrt();
|
||||
for &(q, g) in &qs {
|
||||
let gd = dot(g, d);
|
||||
if q > worst + 1e-4 && q <= worst + 0.1 && gd < dd {
|
||||
alpha = alpha.min((q - worst) / (dd - gd));
|
||||
}
|
||||
}
|
||||
let mut accepted = false;
|
||||
for _ in 0..6 {
|
||||
let Some(p) = ctx.place(cx.kind(v), add(x, scale(d, alpha))) else { break };
|
||||
let q = local.worst_at(p.coords());
|
||||
if p.coords() != x && q > worst && cx.move_is_valid(v, &p, &star)? {
|
||||
cx.move_point(v, p)?;
|
||||
x = p.coords();
|
||||
worst = q;
|
||||
accepted = true;
|
||||
moved = true;
|
||||
break;
|
||||
}
|
||||
alpha *= 0.5;
|
||||
}
|
||||
if !accepted {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(moved)
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
use super::complex::*;
|
||||
use super::measure;
|
||||
use super::ops::*;
|
||||
use crate::predicates::Point3;
|
||||
|
||||
/// A cube [0, 1]³ split into six tetrahedra around the diagonal 0–6, boundary faces tagged by
|
||||
/// cube side (0..6).
|
||||
pub(crate) fn cube() -> TetComplex {
|
||||
let p: Vec<[f64; 3]> = (0..8).map(|i| [(i & 1) as f64, ((i >> 1) & 1) as f64, ((i >> 2) & 1) as f64]).collect();
|
||||
// Vertex i = (x, y, z) bits; diagonal 0 → 7.
|
||||
let raw = [[0, 1, 3, 7], [0, 3, 2, 7], [0, 2, 6, 7], [0, 6, 4, 7], [0, 4, 5, 7], [0, 5, 1, 7]];
|
||||
let pts: Vec<Point3> = p.iter().map(|c| Point3::try_from(*c).unwrap()).collect();
|
||||
let tets: Vec<[u32; 4]> = raw
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let t = t.map(|v: usize| v as u32);
|
||||
let x = t.map(|v| pts[v as usize].coords());
|
||||
if measure::volume6(&x) > 0.0 { t } else { [t[1], t[0], t[2], t[3]] }
|
||||
})
|
||||
.collect();
|
||||
let side = |f: [u32; 3]| -> Option<u32> {
|
||||
let c = f.map(|v| p[v as usize]);
|
||||
for axis in 0..3 {
|
||||
for (s, val) in [(0, 0.0), (1, 1.0)] {
|
||||
if c.iter().all(|q| q[axis] == val) {
|
||||
return Some((axis * 2 + s) as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
let mut boundary = Vec::new();
|
||||
for (t, vs) in tets.iter().enumerate() {
|
||||
for k in 0..4 {
|
||||
if let Some(tag) = side(face_toward(*vs, k)) {
|
||||
boundary.push((t as u32, k, tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
let kinds = vec![VertexKind::Corner; 8];
|
||||
TetComplex::new(&p, kinds, &tets, &boundary, &[], 1000, 10_000).unwrap()
|
||||
}
|
||||
|
||||
fn volume(cx: &TetComplex) -> f64 {
|
||||
cx.live_tets().map(|(_, vs)| measure::volume6(&cx.coords(vs).unwrap()) / 6.0).sum()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cube_builds_and_rings() {
|
||||
let mut cx = cube();
|
||||
assert_eq!(cx.live_count(), 6);
|
||||
assert!((volume(&cx) - 1.0).abs() < 1e-15);
|
||||
let ring = cx.edge_ring(0, 7).unwrap().unwrap();
|
||||
assert!(ring.closed);
|
||||
assert_eq!(ring.verts.len(), 6);
|
||||
assert_eq!(ring.tets.len(), 6);
|
||||
let ring = cx.edge_ring(0, 1).unwrap().unwrap();
|
||||
assert!(!ring.closed);
|
||||
assert_eq!(ring.verts.len(), ring.tets.len() + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interior_insertion_and_rollback() {
|
||||
let mut cx = cube();
|
||||
cx.journal.recording = true;
|
||||
let start = cx.journal.steps.len();
|
||||
let p = Point3::try_from([0.5, 0.5, 0.5]).unwrap();
|
||||
let t = match cx.walk(0, &p, 100).unwrap().unwrap() {
|
||||
Walk::Inside(t) => t,
|
||||
w => panic!("{w:?}"),
|
||||
};
|
||||
let v = insert_delaunay(&mut cx, p, VertexKind::Interior, Site::Interior(t), 1000).unwrap().unwrap();
|
||||
assert!(cx.is_used(v));
|
||||
assert!((volume(&cx) - 1.0).abs() < 1e-15);
|
||||
assert!(cx.live_count() > 6);
|
||||
cx.rollback(start).unwrap();
|
||||
cx.validate().unwrap();
|
||||
assert_eq!(cx.live_count(), 6);
|
||||
assert_eq!(cx.point_count(), 8);
|
||||
assert!((volume(&cx) - 1.0).abs() < 1e-15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_face_insertion_keeps_tags() {
|
||||
let mut cx = cube();
|
||||
// A point on the z = 0 side.
|
||||
let p = Point3::try_from([0.3, 0.6, 0.0]).unwrap();
|
||||
let mut site = None;
|
||||
for (t, _) in cx.live_tets().collect::<Vec<_>>() {
|
||||
let vs = cx.tet(t).unwrap();
|
||||
for k in 0..4 {
|
||||
if cx.adj(t).unwrap()[k] == NONE && cx.tags(t).unwrap()[k] == 4 {
|
||||
let f = face_toward(vs, k).map(|v| cx.xyz(v).unwrap());
|
||||
// inside the triangle in xy
|
||||
let s = |a: [f64; 3], b: [f64; 3]| (b[0] - a[0]) * (0.6 - a[1]) - (b[1] - a[1]) * (0.3 - a[0]);
|
||||
let signs = [s(f[0], f[1]), s(f[1], f[2]), s(f[2], f[0])];
|
||||
if signs.iter().all(|x| *x > 0.0) || signs.iter().all(|x| *x < 0.0) {
|
||||
site = Some(Site::Face(t, k));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let v = insert_delaunay(&mut cx, p, VertexKind::Face(4), site.unwrap(), 1000).unwrap().unwrap();
|
||||
assert!(cx.is_used(v));
|
||||
assert!((volume(&cx) - 1.0).abs() < 1e-15);
|
||||
let c = cx.compact().unwrap();
|
||||
let area: f64 = c
|
||||
.boundary
|
||||
.iter()
|
||||
.filter(|b| b.2 == 4)
|
||||
.map(|&(t, k, _)| {
|
||||
let f = face_toward(c.tets[t as usize], k).map(|v| c.points[v as usize]);
|
||||
0.5 * measure::norm(measure::cross(measure::sub(f[1], f[0]), measure::sub(f[2], f[0])))
|
||||
})
|
||||
.sum();
|
||||
assert!((area - 1.0).abs() < 1e-15, "{area}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_and_face_removal_keep_the_mesh_valid() {
|
||||
let mut cx = cube();
|
||||
let p = Point3::try_from([0.5, 0.45, 0.55]).unwrap();
|
||||
let Walk::Inside(t) = cx.walk(0, &p, 100).unwrap().unwrap() else { panic!() };
|
||||
insert_delaunay(&mut cx, p, VertexKind::Interior, Site::Interior(t), 1000).unwrap().unwrap();
|
||||
let mut changed = 0;
|
||||
for _ in 0..3 {
|
||||
let edges = edges_of(&cx, &cx.live_tets().map(|(t, _)| t).collect::<Vec<_>>()).unwrap();
|
||||
for (a, b) in edges {
|
||||
if cx.edge_tet(a, b).unwrap().is_some() && remove_edge(&mut cx, a, b).unwrap() {
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
let tets: Vec<u32> = cx.live_tets().map(|(t, _)| t).collect();
|
||||
for t in tets {
|
||||
for k in 0..4 {
|
||||
if cx.is_live(t) && remove_face(&mut cx, t, k).unwrap() {
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(changed > 0);
|
||||
cx.validate().unwrap();
|
||||
assert!((volume(&cx) - 1.0).abs() < 1e-14);
|
||||
for (_, vs) in cx.live_tets() {
|
||||
assert_eq!(cx.orient(vs).unwrap(), crate::predicates::Sign::Positive);
|
||||
}
|
||||
}
|
||||
|
||||
mod pipeline {
|
||||
use super::super::geometry::AnalyticSurface;
|
||||
use super::super::{Order, QualityOptions, quality_mesh};
|
||||
use crate::cdt::tests::{cube, surface, uv_sphere};
|
||||
use crate::cdt::{CdtOptions, tetrahedralize};
|
||||
use crate::surface::{BoundarySurface, SurfaceOptions};
|
||||
|
||||
/// A 10 mm cube with one B-rep face per side.
|
||||
fn box_surface() -> BoundarySurface {
|
||||
let (p, t) = cube();
|
||||
let p: Vec<[f64; 3]> = p.iter().map(|c| c.map(|x| 10.0 * x)).collect();
|
||||
let s = surface(&p, &t, [5.0; 3]);
|
||||
// One face tag per side: the side's axis and end.
|
||||
let faces: Vec<u32> = s
|
||||
.triangles()
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let c = t.map(|v| s.points()[v as usize]);
|
||||
(0..3).find_map(|k| [0.0, 10.0].iter().position(|&x| c.iter().all(|q| q[k] == x)).map(|e| (2 * k + e) as u32)).unwrap()
|
||||
})
|
||||
.collect();
|
||||
BoundarySurface::new(s.points(), s.triangles(), &faces, &SurfaceOptions::default()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_box_is_refined_improved_and_deterministic() {
|
||||
let s = box_surface();
|
||||
let cdt = tetrahedralize(&s, &CdtOptions::default()).unwrap();
|
||||
let o = QualityOptions { size: Some(2.5), ..QualityOptions::default() };
|
||||
let a = quality_mesh(&s, &cdt, Vec::new(), &o).unwrap();
|
||||
let r = &a.report;
|
||||
assert!(r.output.tets > 100, "{r:?}");
|
||||
assert_eq!(r.output.inadmissible, 0);
|
||||
assert_eq!(r.output.slivers, 0, "{r:?}");
|
||||
assert!(r.output.min_dihedral > 25.0 && r.output.max_dihedral < 155.0, "{r:?}");
|
||||
// Flat faces: the volume is the box's, and each side keeps its tag on its own plane.
|
||||
assert!((r.volume - 1000.0).abs() < 1e-9, "{}", r.volume);
|
||||
for b in &a.boundary {
|
||||
let t: [u32; 4] = a.elements[b.tet as usize].clone().try_into().unwrap();
|
||||
let f = super::super::complex::face_toward(t, b.opposite as usize).map(|v| a.points[v as usize]);
|
||||
let (k, end) = ((b.face / 2) as usize, if b.face % 2 == 0 { 0.0 } else { 10.0 });
|
||||
assert!(f.iter().all(|p| p[k] == end), "face {} at {f:?}", b.face);
|
||||
}
|
||||
// The same input gives the same mesh, bit for bit.
|
||||
let b = quality_mesh(&s, &cdt, Vec::new(), &o).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sphere_keeps_its_boundary_on_the_sphere_to_second_order() {
|
||||
let (p, t) = uv_sphere(10, 20);
|
||||
let p: Vec<[f64; 3]> = p.iter().map(|c| c.map(|x| 5.0 * x)).collect();
|
||||
let s = surface(&p, &t, [0.0; 3]);
|
||||
let faces = vec![0u32; s.triangles().len()];
|
||||
let s = BoundarySurface::new(s.points(), s.triangles(), &faces, &SurfaceOptions::default()).unwrap();
|
||||
let cdt = tetrahedralize(&s, &CdtOptions::default()).unwrap();
|
||||
let sphere = vec![Some(AnalyticSurface::Sphere { center: [0.0; 3], radius: 5.0 })];
|
||||
let o = QualityOptions { size: Some(1.5), order: Order::Quadratic, ..QualityOptions::default() };
|
||||
let m = quality_mesh(&s, &cdt, sphere, &o).unwrap();
|
||||
assert_eq!(m.kind, fieldcraft_model::ElementKind::Tet10);
|
||||
assert_eq!(m.report.straightened, 0);
|
||||
// Every node of a boundary face, corners and mid-edge nodes, lies on the sphere.
|
||||
let mut on = 0;
|
||||
for b in &m.boundary {
|
||||
let e = &m.elements[b.tet as usize];
|
||||
let k = b.opposite as usize;
|
||||
let corners: Vec<usize> = (0..4).filter(|&i| i != k).collect();
|
||||
let mids =
|
||||
[(0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)].iter().enumerate().filter(|(_, (i, j))| *i != k && *j != k).map(|(n, _)| 4 + n);
|
||||
for i in corners.into_iter().chain(mids) {
|
||||
let q = m.points[e[i] as usize];
|
||||
let r = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2]).sqrt();
|
||||
assert!((r - 5.0).abs() < 1e-9, "a boundary node {i} of {e:?} at {q:?}, radius {r}; report {:?}", m.report);
|
||||
on += 1;
|
||||
}
|
||||
}
|
||||
assert!(on > 0);
|
||||
assert!(m.report.output.min_dihedral > 15.0, "{:?}", m.report.output);
|
||||
// Every element passes the certified check of the gate.
|
||||
for e in &m.elements {
|
||||
let xyz: Vec<[f64; 3]> = e.iter().map(|&v| m.points[v as usize]).collect();
|
||||
let v = crate::quality::element_validity(fieldcraft_model::ElementKind::Tet10, &xyz).unwrap();
|
||||
assert_eq!(v.validity, crate::quality::Validity::Valid);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_stop_the_stage_without_panicking() {
|
||||
let s = box_surface();
|
||||
let cdt = tetrahedralize(&s, &CdtOptions::default()).unwrap();
|
||||
for o in [
|
||||
QualityOptions { size: Some(1e-3), max_points: 200, ..QualityOptions::default() },
|
||||
QualityOptions { size: Some(0.5), max_tets: 500, ..QualityOptions::default() },
|
||||
QualityOptions { size: Some(f64::NAN), ..QualityOptions::default() },
|
||||
QualityOptions { size: Some(-1.0), ..QualityOptions::default() },
|
||||
] {
|
||||
match quality_mesh(&s, &cdt, Vec::new(), &o) {
|
||||
Ok(m) => assert!(!m.report.stopped.is_empty(), "{:?}", m.report),
|
||||
Err(e) => assert!(matches!(e, crate::Error::Limit(_) | crate::Error::Invalid(_)), "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,3 +23,18 @@ pub(crate) fn det(a: P, b: P, c: P) -> f64 {
|
||||
pub(crate) fn angle_deg(a: P, b: P) -> f64 {
|
||||
norm(cross(a, b)).atan2(dot(a, b)).to_degrees()
|
||||
}
|
||||
pub(crate) fn add(a: P, b: P) -> P {
|
||||
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
|
||||
}
|
||||
pub(crate) fn scale(a: P, s: f64) -> P {
|
||||
[a[0] * s, a[1] * s, a[2] * s]
|
||||
}
|
||||
pub(crate) fn dist(a: P, b: P) -> f64 {
|
||||
norm(sub(a, b))
|
||||
}
|
||||
pub(crate) fn lerp(a: P, b: P, t: f64) -> P {
|
||||
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
|
||||
}
|
||||
pub(crate) fn mid(a: P, b: P) -> P {
|
||||
[0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])]
|
||||
}
|
||||
Reference in new issue
Block a user