forked from 1b-agent/fieldcraft-upstream
mesh: improvement never leaves a tetrahedron floating; the gate refuses one
Deleting a cap tetrahedron (two boundary faces of one B-rep face) gives each of its two other neighbours a boundary face; a neighbour that had three was left with every face on the boundary, joined to the mesh by its vertices alone. On Linux the dome on a plate (cmd::plan::decompose::tests::a_dome_on_a_plate...) got one and the static solve found the stiffness singular. The kernel's tessellation and sphere parameters differ in their last bits between macOS and Linux (SolidCraft, outside this repo); given the same input the tet mesher gives the same mesh on both (checked by replaying Linux's surface and spheres on macOS, which reproduced the floating tetrahedron). The deletion now refuses when a neighbour would float, and the linear validity gate fails a mesh with a tetrahedron that shares no face with the rest, saying so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
2e639336c3
commit
426df4a333
3 files changed
+85
-2
No files matched your search
@@ -1033,6 +1033,17 @@ impl TetComplex {
|
||||
if self.curve(vs[k], vs[l]).is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
// Each neighbour gains a boundary face (both, when they are one tetrahedron); one left
|
||||
// with every face on the boundary would float free of the mesh, joined to it by its
|
||||
// vertices alone (a singular stiffness).
|
||||
for m in [k, l] {
|
||||
let n = ns[m];
|
||||
let gained = if ns[k] == ns[l] { 2 } else { 1 };
|
||||
let open = self.adj(n)?.iter().filter(|&&x| x == NONE).count();
|
||||
if open + gained >= 4 {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
let raw = self.journal.recording.then(|| Box::new(self.snapshot(&[t, ns[k], ns[l]], &vs)));
|
||||
for m in [k, l] {
|
||||
let n = ns[m];
|
||||
|
||||
@@ -559,6 +559,26 @@ pub fn quality_mesh(surface: &BoundarySurface, cdt: &TetMesh, analytic: Vec<Opti
|
||||
}
|
||||
}
|
||||
|
||||
/// The tetrahedra (1-based) that share none of their faces with another one, in a mesh of more
|
||||
/// than one: each is a separate piece of boundary, joined to the mesh by vertices or an edge.
|
||||
fn detached_tets(tets: &[[u32; 4]]) -> Vec<u32> {
|
||||
if tets.len() < 2 {
|
||||
return Vec::new();
|
||||
}
|
||||
let face = |t: &[u32; 4], k: usize| {
|
||||
let mut f = [t[(k + 1) % 4], t[(k + 2) % 4], t[(k + 3) % 4]];
|
||||
f.sort_unstable();
|
||||
f
|
||||
};
|
||||
let mut count: std::collections::BTreeMap<[u32; 3], u32> = std::collections::BTreeMap::new();
|
||||
for t in tets {
|
||||
for k in 0..4 {
|
||||
*count.entry(face(t, k)).or_default() += 1;
|
||||
}
|
||||
}
|
||||
tets.iter().enumerate().filter(|(_, t)| (0..4).all(|k| count.get(&face(t, k)).copied().unwrap_or(0) < 2)).map(|(i, _)| i as u32 + 1).collect()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -576,9 +596,18 @@ pub fn raw_mesh(surface: &BoundarySurface, cdt: &TetMesh) -> Result<QualityMesh>
|
||||
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).
|
||||
/// The validity gate of a linear mesh: every tetrahedron admissible, joined to the others by a
|
||||
/// face (when there are several), 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 detached = detached_tets(tets);
|
||||
if !detached.is_empty() {
|
||||
let list = detached.iter().take(10).map(u32::to_string).collect::<Vec<_>>().join(", ");
|
||||
return Err(Error::Failed(format!(
|
||||
"mesh validity gate: {} tetrahedra share no face with the rest of the mesh ({list}): held by vertices alone, they would make the stiffness singular",
|
||||
detached.len()
|
||||
)));
|
||||
}
|
||||
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])?])
|
||||
|
||||
@@ -454,3 +454,46 @@ mod determinism {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
Reference in new issue
Block a user