Files
OneCAD-Simulation/crates/mesh/src/tet/tests.rs
T
jyvikoandClaude Opus 5.5 4329630943 mesh: the tet objective sees needles; no tet below scaled Jacobian 0.3 on the skill's tet parts
The improvement objective was the smallest dihedral sine, which misses
needles: every dihedral angle fine while a corner is nearly flat, so
the corpus meshes had scaled Jacobians down to 0.01-0.07. The objective
is now the smaller of the dihedral sine and the scaled Jacobian at the
worst corner (the measure analysts check, Knupp 2001), weighted so a
Jacobian of 0.3 (the skill's limit) counts as a 30 degree dihedral
angle (the schedule's target): the Jacobian decides only near that
limit. The existing edge removal, multi-face removal, smoothing and
insertion then remove the needles.

Skill tet10 min SJ: sphere-on-plate 0.092 -> 0.335, filleted-bracket
0.021 -> 0.318 (both pass, nothing marked). Default-size tet10:
thin-ribs 0.240, shallow-groove 0.321, rounded-box 0.311.

The strong-relaxation test relaxes below 0.45: nothing is left below
0.3. Literature note in docs/meshing/literature.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-16 22:11:53 -04:00

510 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}
/// A point of a side's plane past the side's edge is off the face, however large the side's
/// triangles: the fillet box's nodes landed on the untrimmed planes, keeping the sharp
/// corners (#402, #418).
#[test]
fn a_plane_is_trimmed_to_its_face() {
let s = box_surface();
let analytic = (0..6)
.map(|f: usize| {
let (k, e) = (f / 2, f % 2);
let mut normal = [0.0; 3];
normal[k] = if e == 0 { -1.0 } else { 1.0 };
let mut point = [0.0; 3];
point[k] = 10.0 * e as f64;
Some(crate::tet::geometry::AnalyticSurface::Plane { normal, point })
})
.collect();
let t = crate::tet::geometry::Tessellation::new(&s, analytic);
let q = t.project_trimmed(0, [0.5, 11.0, 5.0]).unwrap();
assert!(crate::vec3::dist(q, [0.0, 10.0, 5.0]) < 1e-9, "{q:?}");
let q = t.project_trimmed(0, [0.5, 9.0, 5.0]).unwrap();
assert!(crate::vec3::dist(q, [0.0, 9.0, 5.0]) < 1e-9, "{q:?}");
}
#[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);
}
/// Refinement splitting a face edge whose midpoint lies inside a feature edge's diametral
/// sphere splits the feature edge instead (#303): otherwise each split halves the distance
/// of the face vertices to the curve and they pile up against it.
#[test]
fn a_face_edge_split_that_encroaches_a_curve_splits_the_curve() {
use super::super::complex::{NONE, VertexKind, face_toward};
use super::super::geometry::Tessellation;
use super::super::ops::{Site, insert_delaunay};
use super::super::refine::{Context, MAX_CAVITY, split_edge};
use super::super::{complex_from_cdt, measure};
use crate::predicates::Point3;
use crate::size::UniformSize;
// Whether `p` lies in triangle `f` of the z = 0 plane.
let contains = |f: [[f64; 3]; 3], p: [f64; 3]| {
let side = |a: [f64; 3], b: [f64; 3]| (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
let s = [side(f[0], f[1]), side(f[1], f[2]), side(f[2], f[0])];
s.iter().all(|&x| x >= 0.0) || s.iter().all(|&x| x <= 0.0)
};
let s = box_surface();
let cdt = tetrahedralize(&s, &CdtOptions::default()).unwrap();
let (mut cx, curve_faces) = complex_from_cdt(&s, &cdt, &QualityOptions::default()).unwrap();
let (geometry, size) = (Tessellation::new(&s, Vec::new()), UniformSize(5.0));
let ctx = Context { size: &size, geometry: &geometry, curve_faces: &curve_faces };
// A curve edge on the z = 0 side, and a face vertex beside it on that side.
let ((a, b), _) = cx.curve_edges().into_iter().find(|&((a, b), _)| [a, b].iter().all(|&v| cx.xyz(v).unwrap()[2] == 0.0)).unwrap();
let (xa, xb) = (cx.xyz(a).unwrap(), cx.xyz(b).unwrap());
let len = measure::dist(xa, xb);
let centre = [5.0, 5.0, 0.0];
let mid = measure::mid(xa, xb);
let inward = measure::scale(measure::sub(centre, mid), 0.3 * len / measure::dist(centre, mid));
let q = Point3::try_from(measure::add(mid, inward)).unwrap();
let (t, k) = cx
.live_tets()
.flat_map(|(t, vs)| (0..4).map(move |k| (t, vs, k)))
.find(|&(t, vs, k)| {
cx.adj(t).unwrap()[k] == NONE && cx.tags(t).unwrap()[k] == 4 && contains(face_toward(vs, k).map(|v| cx.xyz(v).unwrap()), q.coords())
})
.map(|(t, _, k)| (t, k))
.unwrap();
let v = insert_delaunay(&mut cx, q, VertexKind::Face(4), Site::Face(t, k), MAX_CAVITY).unwrap().unwrap();
assert!(cx.edge_tet(v, a).unwrap().is_some());
let w = split_edge(&mut cx, &ctx, v, a, 4, 0.0).unwrap().unwrap();
assert!(matches!(cx.kind(w), VertexKind::Curve(_)), "{:?}", cx.kind(w));
assert!(measure::dist(cx.xyz(w).unwrap(), mid) < 1e-9);
cx.validate().unwrap();
}
#[test]
fn a_seeded_edge_gets_the_seeded_spacing_and_the_size_grows_from_it() {
use crate::seeds::EdgeSeed;
use fieldcraft_model::SeedConstraint;
let s = box_surface();
let cdt = tetrahedralize(&s, &CdtOptions::default()).unwrap();
let edge = vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]];
let seed = EdgeSeed { name: "front".into(), points: edge, tolerance: 0.0, count: 16, bias: None, constraint: SeedConstraint::Exact };
let plain = QualityOptions { size: Some(5.0), seeds: vec![EdgeSeed { count: 1, ..seed.clone() }], ..QualityOptions::default() };
let seeded = QualityOptions { seeds: vec![seed], ..plain.clone() };
let a = quality_mesh(&s, &cdt, Vec::new(), &plain).unwrap();
let b = quality_mesh(&s, &cdt, Vec::new(), &seeded).unwrap();
let (na, nb) = (a.report.seeds[0].2, b.report.seeds[0].2);
// Unseeded, the 10 mm edge gets about two 5 mm elements; seeded, about sixteen.
assert!(na <= 4, "{na}");
assert_eq!(nb, 16);
assert_eq!(b.report.seeds[0].1, 16);
assert!(b.report.output.tets > a.report.output.tets && b.report.output.inadmissible == 0);
// Biased: the mesh's nodes on the edge are the seed's.
let bias = Some(fieldcraft_model::Bias { ratio: 4.0, toward: fieldcraft_model::BiasToward::Start });
let biased = QualityOptions { seeds: vec![EdgeSeed { count: 8, bias, ..seeded.seeds[0].clone() }], ..plain.clone() };
let c = quality_mesh(&s, &cdt, Vec::new(), &biased).unwrap();
let mut x: Vec<f64> = c.points.iter().filter(|p| p[1].abs() < 1e-9 && p[2].abs() < 1e-9).map(|p| p[0]).collect();
x.sort_by(f64::total_cmp);
let want: Vec<f64> = biased.seeds[0].fractions(8, false).iter().map(|f| 10.0 * f).collect();
assert_eq!(x.len(), want.len(), "{x:?}");
assert!(x.iter().zip(&want).all(|(a, b)| (a - b).abs() < 1e-9), "{x:?} {want:?}");
}
#[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}"),
}
}
}
}
/// Determinism (#115): the quality mesh is a function of the geometry, the same bit for bit on
/// every platform, run and listing order of the input.
mod determinism {
use crate::cdt::{CdtOptions, tetrahedralize};
use crate::surface::{BoundarySurface, SurfaceOptions};
use crate::tet::canonical::CanonicalSoup;
use crate::tet::{Order, QualityMesh, QualityOptions, quality_mesh};
/// cos and sin of k · 15° from square roots only (correctly rounded everywhere), so the
/// test input itself is the same on every platform.
fn unit_24(k: usize) -> [f64; 2] {
let (s2, s3, s6) = (2f64.sqrt(), 3f64.sqrt(), 6f64.sqrt());
let c = [1.0, (s6 + s2) / 4.0, s3 / 2.0, s2 / 2.0, 0.5, (s6 - s2) / 4.0, 0.0];
// Quadrant q, step j: cos(90° q + 15° j).
let (q, j) = (k / 6 % 4, k % 6);
let (x, y) = (c[j], c[6 - j]);
match q {
0 => [x, y],
1 => [-y, x],
2 => [-x, -y],
_ => [y, -x],
}
}
/// A washer (inner radius 6, outer 15, height 4) as a 24-sided prism ring: faces bottom 0,
/// top 1, inside 2, outside 3; triangles oriented outward.
pub(crate) fn washer() -> (Vec<[f64; 3]>, Vec<[u32; 3]>, Vec<u32>) {
const N: usize = 24;
let mut p = Vec::new();
for (r, z) in [(6.0, 0.0), (15.0, 0.0), (6.0, 4.0), (15.0, 4.0)] {
for k in 0..N {
let [c, s] = unit_24(k);
p.push([r * c, r * s, z]);
}
}
let id = |ring: usize, k: usize| (ring * N + k % N) as u32;
let (mut t, mut f) = (Vec::new(), Vec::new());
for k in 0..N {
// (ring a, ring b): quad a_k b_k b_k+1 a_k+1, outward when listed this way.
for (a, b, tag) in [(1, 0, 0), (2, 3, 1), (0, 2, 2), (3, 1, 3)] {
let q = [id(a, k), id(b, k), id(b, k + 1), id(a, k + 1)];
t.push([q[0], q[1], q[2]]);
t.push([q[0], q[2], q[3]]);
f.extend([tag, tag]);
}
}
(p, t, f)
}
fn mesh(p: &[[f64; 3]], t: &[[u32; 3]], f: &[u32]) -> QualityMesh {
let soup = CanonicalSoup::new(p, t, f).unwrap();
let s = BoundarySurface::new(&soup.positions, &soup.triangles, &soup.tri_face, &SurfaceOptions::default()).unwrap();
assert!(s.volume() > 0.0);
let cdt = tetrahedralize(&s, &CdtOptions::default()).unwrap();
let mut m =
quality_mesh(&s, &cdt, Vec::new(), &QualityOptions { size: Some(3.0), order: Order::Quadratic, ..QualityOptions::default() }).unwrap();
soup.restore_tags(&mut m);
m
}
/// FNV-1a over every coordinate's bits, the elements and the tagged boundary faces.
pub(crate) fn fingerprint(m: &QualityMesh) -> u64 {
let mut h = 0xcbf2_9ce4_8422_2325u64;
let mut eat = |x: u64| {
for b in x.to_le_bytes() {
h = (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3);
}
};
m.points.iter().flatten().for_each(|c| eat(c.to_bits()));
m.elements.iter().flatten().for_each(|&v| eat(u64::from(v)));
m.boundary.iter().for_each(|b| eat(u64::from(b.tet) << 40 | u64::from(b.opposite) << 32 | u64::from(b.face)));
h
}
/// The washer's tet10 mesh has one fingerprint on every platform (CI checks it on Linux
/// x86-64, macOS arm64 and Windows): only correctly rounded arithmetic and the crate's own
/// `atan2` and `cbrt` make its decisions, and no hash map is iterated into it. A change that
/// moves the tet mesh on purpose updates the constant.
#[test]
fn the_quality_mesh_is_the_same_on_every_platform() {
let (p, t, f) = washer();
let m = mesh(&p, &t, &f);
assert!(m.elements.len() > 100, "{:?}", m.report);
// No needles either: the scaled Jacobian analysts check is above the skill's limit.
let sj = m
.elements
.iter()
.map(|e| {
let xyz: Vec<[f64; 3]> = e.iter().map(|&v| m.points[v as usize]).collect();
crate::quality::element_quality(m.kind, &xyz).unwrap().get(crate::quality::Metric::ScaledJacobian).unwrap()
})
.fold(f64::INFINITY, f64::min);
assert!(sj >= 0.3, "min scaled Jacobian {sj}");
assert_eq!(fingerprint(&m), 11_252_377_805_886_396_373, "{} tets, {} nodes", m.elements.len(), m.points.len());
}
/// Listing the triangles, their corners and the vertices in another order, and numbering
/// the faces differently, gives the same mesh with the faces' own tags.
#[test]
fn the_quality_mesh_does_not_depend_on_the_input_order() {
let (p, t, f) = washer();
let a = mesh(&p, &t, &f);
let n = p.len() as u32;
let q: Vec<[f64; 3]> = p.iter().rev().copied().collect();
let u: Vec<[u32; 3]> = t.iter().rev().map(|x| [n - 1 - x[1], n - 1 - x[2], n - 1 - x[0]]).collect();
let g: Vec<u32> = f.iter().rev().map(|&x| 10 + 3 - x).collect();
let b = mesh(&q, &u, &g);
assert_eq!((&a.points, &a.elements), (&b.points, &b.elements));
assert_eq!(a.boundary.len(), b.boundary.len());
for (x, y) in a.boundary.iter().zip(&b.boundary) {
assert_eq!((x.tet, x.opposite, 10 + 3 - x.face), (y.tet, y.opposite, y.face));
}
}
}
/// A cap tetrahedron (two boundary faces of one B-rep face) whose two other neighbours each have
/// three boundary faces: deleting it would leave them joined to nothing by a face, floating free
/// (a singular stiffness: the dome-on-a-plate mesh on Linux, #438), so it stays.
#[test]
fn deleting_a_cap_never_leaves_a_neighbour_floating() {
let p: Vec<[f64; 3]> = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.3, 0.3, -1.0], [1.0, 1.0, 1.0]];
let pts: Vec<Point3> = p.iter().map(|c| Point3::try_from(*c).unwrap()).collect();
let positive = |t: [u32; 4]| if measure::volume6(&t.map(|v| pts[v as usize].coords())) > 0.0 { t } else { [t[1], t[0], t[2], t[3]] };
// The cap, and a tetrahedron on each of its faces 012 and 123.
let tets = [positive([0, 1, 2, 3]), positive([0, 1, 2, 4]), positive([1, 2, 3, 5])];
let key = |f: [u32; 3]| {
let mut f = f;
f.sort_unstable();
f
};
let mut boundary = Vec::new();
for (t, vs) in tets.iter().enumerate() {
for k in 0..4 {
let f = key(face_toward(*vs, k));
let shared = tets.iter().enumerate().any(|(u, ws)| u != t && (0..4).any(|j| key(face_toward(*ws, j)) == f));
if !shared {
boundary.push((t as u32, k, 0));
}
}
}
let mut cx = TetComplex::new(&p, vec![VertexKind::Corner; 6], &tets, &boundary, &[], 100, 100).unwrap();
assert!(!cx.delete_boundary_tet(0).unwrap(), "the cap was deleted, leaving its neighbours floating");
assert_eq!(cx.live_tets().count(), 3);
cx.validate().unwrap();
}
/// The validity gate refuses a tetrahedron that shares no face with the rest of the mesh, even
/// when the volume adds up: it says why instead of leaving the solver to find it singular.
#[test]
fn the_gate_refuses_a_detached_tetrahedron() {
let p = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, 1.0], [2.0, 1.0, 1.0], [1.0, 2.0, 1.0], [1.0, 1.0, 2.0]];
let tets = [[0, 1, 2, 3], [4, 5, 6, 7]];
let v = 2.0 / 6.0;
let e = super::linear_gate(&p, &tets, v).unwrap_err().to_string();
assert!(e.contains("share no face") && e.contains("(1, 2)"), "{e}");
assert!(super::linear_gate(&p, &tets[..1], v / 2.0).is_ok());
}