Surface: close T-junctions left by independently tessellated faces

A face boundary vertex lying (within the weld distance) on an undivided edge of
the neighbouring face splits that edge's triangle. Only edges no other triangle
uses are looked at, so closed surfaces are unchanged. The half of a crank arm
cut on its symmetry plane tessellates with such a junction where the cut face
meets the top face; it now meshes (tet and swept hex).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 19:37:37 -04:00
1 parent 1c16972cf2
commit 22f1006f71
2 files changed
+106

No files matched your search

+20
View File
@@ -392,3 +392,23 @@ fn orient2d_sign(a: &Point3, b: &Point3, c: &Point3) -> i32 {
let p = |q: &Point3| crate::predicates::Point2::new(q.coords()[0], q.coords()[1]).unwrap();
crate::predicates::orient2d(&p(a), &p(b), &p(c)) as i32
}
/// A vertex of one face on an undivided edge of its neighbour (a T-junction, as independent
/// face tessellations leave where a face is split) is closed by splitting that edge.
#[test]
fn t_junctions_are_closed() {
let (mut p, mut t, mut f) = cube([0.0; 3], 1.0);
// Edge 0–2 (x = 0, z = 0) gets a vertex at its middle on the x = 0 side only.
let m = p.len() as u32;
p.push([0.0, 0.5, 0.0]);
let i = t.iter().position(|x| *x == [0, 6, 2]).unwrap();
t[i] = [0, 6, m];
t.push([m, 6, 2]);
f.push(f[i]);
let (sp, st, sf) = soup(&(p, t, f));
let s = BoundarySurface::new(&sp, &st, &sf, &SurfaceOptions { weld_tolerance: 0.0, weld_distance: 1e-9, feature_angle: 30.0 }).unwrap();
assert!((s.volume() - 1.0).abs() < 1e-12, "{}", s.volume());
assert_eq!(s.triangles().len(), 14);
// Without a weld distance nothing is split, and the open edge is reported.
assert!(BoundarySurface::new(&sp, &st, &sf, &SurfaceOptions { weld_tolerance: 0.0, weld_distance: 0.0, feature_angle: 30.0 }).is_err());
}
+86
View File
@@ -78,9 +78,95 @@ pub(crate) fn weld(positions: &[[f64; 3]], triangles: &[[u32; 3]], relative: f64
source_triangle.push(index(t));
}
}
if tol > 0.0 {
split_t_junctions(&points, &mut out, &mut source_triangle, tol);
}
Ok(Welded { points, triangles: out, source_vertex, source_triangle })
}
/// Rounds of T-junction splitting, and the most open edges looked at.
const T_JUNCTION_ROUNDS: usize = 8;
const MAX_OPEN_EDGES: usize = 100_000;
/// Close T-junctions: where a vertex of one face's boundary lies on an edge of the neighbouring
/// face (within `tol`) that the tessellation did not divide there, split that edge's triangle at
/// it. Only edges no other triangle uses are looked at, so a closed surface is left as it is.
/// The standard repair of tessellations whose faces are triangulated independently (Barequet and
/// Kumar 1997, *Repairing CAD models*).
fn split_t_junctions(points: &[[f64; 3]], triangles: &mut Vec<[u32; 3]>, source: &mut Vec<u32>, tol: f64) {
use std::collections::{BTreeMap, BTreeSet};
for _ in 0..T_JUNCTION_ROUNDS {
let mut used: BTreeMap<(u32, u32), u32> = BTreeMap::new();
for tri in triangles.iter() {
for k in 0..3 {
*used.entry((tri[k], tri[(k + 1) % 3])).or_default() += 1;
}
}
// Edges used once, in one direction only.
let open: BTreeSet<(u32, u32)> = used.iter().filter(|(e, n)| **n == 1 && !used.contains_key(&(e.1, e.0))).map(|(e, _)| *e).collect();
if open.is_empty() || open.len() > MAX_OPEN_EDGES {
return;
}
let ends: BTreeSet<u32> = open.iter().flat_map(|&(a, b)| [a, b]).collect();
let at = |v: u32| points.get(v as usize).copied();
let mut changed = false;
let mut next = Vec::with_capacity(triangles.len() + 8);
let mut next_source = Vec::with_capacity(triangles.len() + 8);
for (tri, &src) in triangles.iter().zip(source.iter()) {
let mut split = None;
for k in 0..3 {
let (a, b, c) = (tri[k], tri[(k + 1) % 3], tri[(k + 2) % 3]);
if !open.contains(&(a, b)) {
continue;
}
let (Some(pa), Some(pb)) = (at(a), at(b)) else { continue };
let ab = sub(pb, pa);
let len2 = dot(ab, ab);
if !(len2 > 0.0) {
continue;
}
let mut on: Vec<(f64, u32)> = ends
.iter()
.filter(|&&v| v != a && v != b && v != c)
.filter_map(|&v| {
let p = at(v)?;
let t = dot(sub(p, pa), ab) / len2;
let q = [pa[0] + ab[0] * t, pa[1] + ab[1] * t, pa[2] + ab[2] * t];
let d = sub(p, q);
let inside = t * len2.sqrt() > tol && (1.0 - t) * len2.sqrt() > tol;
(inside && dot(d, d) <= tol * tol).then_some((t, v))
})
.collect();
if !on.is_empty() {
on.sort_by(|x, y| x.0.total_cmp(&y.0).then(x.1.cmp(&y.1)));
split = Some((a, b, c, on));
break;
}
}
match split {
Some((a, b, c, on)) => {
let mut prev = a;
for (_, v) in on.iter().copied().chain(std::iter::once((1.0, b))) {
next.push([prev, v, c]);
next_source.push(src);
prev = v;
}
changed = true;
}
None => {
next.push(*tri);
next_source.push(src);
}
}
}
*triangles = next;
*source = next_source;
if !changed {
return;
}
}
}
/// Representatives found so far.
enum Lookup {
/// Identical coordinates only (keyed by bits; `+ 0.0` turns -0 into +0).