use crate::det_hash::DetHashMap;
use std::collections::BTreeMap;
use crate::sdf_collider::SdfField;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RefineError {
Unfinished {
passes: u32,
tets_left: usize,
},
MarkCountDoesNotMatch {
marks: usize,
tets: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BoundaryFaceError {
NonManifoldFace {
face: [u32; 3],
uses: usize,
},
}
fn tet_edges(vs: [u32; 4]) -> [(u32, u32); 6] {
[
(vs[0], vs[1]),
(vs[0], vs[2]),
(vs[0], vs[3]),
(vs[1], vs[2]),
(vs[1], vs[3]),
(vs[2], vs[3]),
]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Tetrahedron {
pub vertices: [u32; 4],
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct SdfTetMesh {
pub vertices: Vec<[f32; 3]>,
pub tets: Vec<Tetrahedron>,
}
impl SdfTetMesh {
#[must_use]
pub fn vertex_count(&self) -> usize {
self.vertices.len()
}
#[must_use]
pub fn tet_count(&self) -> usize {
self.tets.len()
}
pub fn boundary_faces(&self) -> Result<Vec<[u32; 3]>, BoundaryFaceError> {
let mut uses: BTreeMap<[u32; 3], usize> = BTreeMap::new();
for tet in &self.tets {
let v = tet.vertices;
for face in [
[v[0], v[1], v[2]],
[v[0], v[1], v[3]],
[v[0], v[2], v[3]],
[v[1], v[2], v[3]],
] {
let mut key = face;
key.sort_unstable();
*uses.entry(key).or_insert(0) += 1;
}
}
let mut out = Vec::new();
for (face, count) in uses {
if count > 2 {
return Err(BoundaryFaceError::NonManifoldFace { face, uses: count });
}
if count == 1 {
out.push(face);
}
}
Ok(out)
}
pub fn try_refine_conforming(
&mut self,
max_edge_length: f32,
max_passes: u32,
) -> Result<u32, RefineError> {
assert!(max_edge_length > 0.0, "max_edge_length must be positive");
let limit_sq = max_edge_length * max_edge_length;
let mut midpoints: DetHashMap<(u32, u32), u32> = DetHashMap::default();
let mut passes = 0_u32;
for _ in 0..max_passes {
passes += 1;
let mut changed = false;
let mut new_tets: Vec<Tetrahedron> = Vec::with_capacity(self.tets.len() * 2);
let snapshot = std::mem::take(&mut self.tets);
for tet in &snapshot {
let Some(k) = self.edge_to_split(tet, limit_sq, &midpoints) else {
new_tets.push(*tet);
continue;
};
changed = true;
let (a, b) = tet_edges(tet.vertices)[k];
let mid = self.midpoint_of(a, b, &mut midpoints);
let [first, second] = split_children(tet.vertices, a, b, mid);
new_tets.push(Tetrahedron { vertices: first });
new_tets.push(Tetrahedron { vertices: second });
}
self.tets = new_tets;
if !changed {
return Ok(passes - 1);
}
}
let remaining = self
.tets
.iter()
.filter(|t| self.edge_to_split(t, limit_sq, &midpoints).is_some())
.count();
if remaining == 0 {
return Ok(passes);
}
Err(RefineError::Unfinished {
passes,
tets_left: remaining,
})
}
pub fn try_refine_marked(
&mut self,
marked: &[bool],
max_passes: u32,
) -> Result<u32, RefineError> {
if marked.len() != self.tets.len() {
return Err(RefineError::MarkCountDoesNotMatch {
marks: marked.len(),
tets: self.tets.len(),
});
}
let marked_count = marked.iter().filter(|&&m| m).count();
if marked_count == 0 {
return Ok(0);
}
if max_passes == 0 {
return Err(RefineError::Unfinished {
passes: 0,
tets_left: marked_count,
});
}
const SPLIT_LONGEST: f32 = -1.0;
const PROPAGATE_ONLY: f32 = f32::INFINITY;
let mut midpoints: DetHashMap<(u32, u32), u32> = DetHashMap::default();
let mut passes = 0_u32;
for pass in 0..max_passes {
passes += 1;
let mut changed = false;
let mut new_tets: Vec<Tetrahedron> = Vec::with_capacity(self.tets.len() * 2);
let snapshot = std::mem::take(&mut self.tets);
for (index, tet) in snapshot.iter().enumerate() {
let limit_sq = if pass == 0 && marked[index] {
SPLIT_LONGEST
} else {
PROPAGATE_ONLY
};
let Some(k) = self.edge_to_split(tet, limit_sq, &midpoints) else {
new_tets.push(*tet);
continue;
};
changed = true;
let (a, b) = tet_edges(tet.vertices)[k];
let mid = self.midpoint_of(a, b, &mut midpoints);
let [first, second] = split_children(tet.vertices, a, b, mid);
new_tets.push(Tetrahedron { vertices: first });
new_tets.push(Tetrahedron { vertices: second });
}
self.tets = new_tets;
if !changed {
return Ok(passes - 1);
}
}
let remaining = self
.tets
.iter()
.filter(|t| self.edge_to_split(t, PROPAGATE_ONLY, &midpoints).is_some())
.count();
if remaining == 0 {
return Ok(passes);
}
Err(RefineError::Unfinished {
passes,
tets_left: remaining,
})
}
fn edge_to_split(
&self,
tet: &Tetrahedron,
limit_sq: f32,
midpoints: &DetHashMap<(u32, u32), u32>,
) -> Option<usize> {
let edges = tet_edges(tet.vertices);
let mut best: Option<(usize, f32, (u32, u32))> = None;
for (k, (a, b)) in edges.iter().enumerate() {
let key = (*a.min(b), *a.max(b));
let pa = self.vertices[*a as usize];
let pb = self.vertices[*b as usize];
let dx = pa[0] - pb[0];
let dy = pa[1] - pb[1];
let dz = pa[2] - pb[2];
let len_sq = dx.mul_add(dx, dy.mul_add(dy, dz * dz));
if len_sq <= limit_sq && !midpoints.contains_key(&key) {
continue;
}
let better = match best {
None => true,
Some((_, blen, bkey)) => len_sq > blen || (len_sq == blen && key < bkey),
};
if better {
best = Some((k, len_sq, key));
}
}
best.map(|(k, _, _)| k)
}
fn midpoint_of(&mut self, a: u32, b: u32, midpoints: &mut DetHashMap<(u32, u32), u32>) -> u32 {
let key = (a.min(b), a.max(b));
if let Some(&idx) = midpoints.get(&key) {
return idx;
}
let pa = self.vertices[a as usize];
let pb = self.vertices[b as usize];
let mid = [
0.5 * (pa[0] + pb[0]),
0.5 * (pa[1] + pb[1]),
0.5 * (pa[2] + pb[2]),
];
let idx = u32::try_from(self.vertices.len()).expect("vertex count fits u32");
self.vertices.push(mid);
midpoints.insert(key, idx);
idx
}
#[deprecated(
since = "1.6.0",
note = "use try_refine_conforming: this cannot report an exhausted budget"
)]
pub fn refine_by_max_edge_length(&mut self, max_edge_length: f32, max_passes: u32) -> u32 {
match self.try_refine_conforming(max_edge_length, max_passes) {
Ok(passes) => passes,
Err(RefineError::Unfinished { passes, .. }) => passes,
Err(RefineError::MarkCountDoesNotMatch { .. }) => 0,
}
}
#[must_use]
pub fn max_edge_length(&self) -> f32 {
let mut best = 0.0_f32;
for tet in &self.tets {
let vs = tet.vertices;
let edges: [(u32, u32); 6] = [
(vs[0], vs[1]),
(vs[0], vs[2]),
(vs[0], vs[3]),
(vs[1], vs[2]),
(vs[1], vs[3]),
(vs[2], vs[3]),
];
for (a, b) in edges {
let pa = self.vertices[a as usize];
let pb = self.vertices[b as usize];
let dx = pa[0] - pb[0];
let dy = pa[1] - pb[1];
let dz = pa[2] - pb[2];
let len = dx.mul_add(dx, dy.mul_add(dy, dz * dz)).sqrt();
if len > best {
best = len;
}
}
}
best
}
}
fn split_children(vs: [u32; 4], a: u32, b: u32, mid: u32) -> [[u32; 4]; 2] {
let replace = |from: u32| vs.map(|v| if v == from { mid } else { v });
[replace(b), replace(a)]
}
fn lattice_cells(min: [f32; 3], max: [f32; 3], cell: f32) -> [i32; 3] {
let count = |a: usize| {
let q = (f64::from(max[a]) - f64::from(min[a])) / f64::from(cell);
(q * (1.0 + 1e-6)).floor().clamp(1.0, f64::from(i32::MAX)) as i32
};
[count(0), count(1), count(2)]
}
#[must_use]
pub fn generate<F: SdfField + ?Sized>(
sdf: &F,
min: [f32; 3],
max: [f32; 3],
cell: f32,
) -> SdfTetMesh {
assert!(cell > 0.0, "cell must be positive");
let mut mesh = SdfTetMesh::default();
let mut vertex_index: DetHashMap<(i32, i32, i32), u32> = DetHashMap::default();
let [nx, ny, nz] = lattice_cells(min, max, cell);
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
let corner_ids: [(i32, i32, i32); 8] = [
(ix, iy, iz),
(ix + 1, iy, iz),
(ix + 1, iy + 1, iz),
(ix, iy + 1, iz),
(ix, iy, iz + 1),
(ix + 1, iy, iz + 1),
(ix + 1, iy + 1, iz + 1),
(ix, iy + 1, iz + 1),
];
let corner_positions = corner_ids.map(|(cx, cy, cz)| {
[
min[0] + (cx as f32) * cell,
min[1] + (cy as f32) * cell,
min[2] + (cz as f32) * cell,
]
});
let all_inside = corner_positions
.iter()
.all(|p| sdf.distance(p[0], p[1], p[2]) <= 0.0);
if !all_inside {
continue;
}
let mut cube_verts = [0_u32; 8];
for (slot, id) in corner_ids.iter().enumerate() {
let entry = vertex_index.entry(*id);
let next_index = mesh.vertices.len() as u32;
let idx = *entry.or_insert_with(|| {
mesh.vertices.push(corner_positions[slot]);
next_index
});
cube_verts[slot] = idx;
}
for tet in cube_to_five_tets(cube_verts, cell_parity(ix, iy, iz)) {
push_tet(&mut mesh, tet.vertices);
}
}
}
}
mesh
}
struct WarpedLattice {
origin: [f32; 3],
cell: f32,
dims: [i32; 3],
nodes: Vec<([f32; 3], f32)>,
}
impl WarpedLattice {
fn neighbour_offsets() -> impl Iterator<Item = [i32; 3]> {
(-1..=1).flat_map(move |dz| {
(-1..=1).flat_map(move |dy| {
(-1..=1).filter_map(move |dx: i32| {
if dx == 0 && dy == 0 && dz == 0 {
None
} else {
Some([dx, dy, dz])
}
})
})
})
}
fn build<F: SdfField + ?Sized>(sdf: &F, origin: [f32; 3], cell: f32, dims: [i32; 3]) -> Self {
let counts = [dims[0] + 1, dims[1] + 1, dims[2] + 1];
let total = (counts[0] as usize) * (counts[1] as usize) * (counts[2] as usize);
let mut raw = Vec::with_capacity(total);
for iz in 0..counts[2] {
for iy in 0..counts[1] {
for ix in 0..counts[0] {
let p = corner_pos(origin, cell, ix, iy, iz);
raw.push((p, sdf.distance(p[0], p[1], p[2])));
}
}
}
let snap_distance = SNAP_CELL_FRACTION * cell;
let index =
|id: [i32; 3]| -> usize { ((id[2] * counts[1] + id[1]) * counts[0] + id[0]) as usize };
let mut nodes = raw.clone();
for iz in 0..counts[2] {
for iy in 0..counts[1] {
for ix in 0..counts[0] {
let here = index([ix, iy, iz]);
let (p, s0) = raw[here];
if s0 == 0.0 {
continue;
}
let mut best: Option<(f32, [f32; 3])> = None;
for d in Self::neighbour_offsets() {
let n = [ix + d[0], iy + d[1], iz + d[2]];
if n.iter().zip(counts).any(|(&c, limit)| c < 0 || c >= limit) {
continue;
}
let (q, s1) = raw[index(n)];
if (s0 < 0.0) == (s1 < 0.0) {
continue;
}
let t = s0 / (s0 - s1);
let at = [
p[0] + t * (q[0] - p[0]),
p[1] + t * (q[1] - p[1]),
p[2] + t * (q[2] - p[2]),
];
let dx = at[0] - p[0];
let dy = at[1] - p[1];
let dz = at[2] - p[2];
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist < snap_distance && best.is_none_or(|(b, _)| dist < b) {
best = Some((dist, at));
}
}
if let Some((_, at)) = best {
nodes[here] = (at, 0.0);
}
}
}
}
Self {
origin,
cell,
dims,
nodes,
}
}
fn index(&self, id: [i32; 3]) -> usize {
let counts = [self.dims[0] + 1, self.dims[1] + 1, self.dims[2] + 1];
((id[2] * counts[1] + id[1]) * counts[0] + id[0]) as usize
}
fn position(&self, id: [i32; 3]) -> [f32; 3] {
self.nodes.get(self.index(id)).map_or_else(
|| corner_pos(self.origin, self.cell, id[0], id[1], id[2]),
|n| n.0,
)
}
fn value(&self, id: [i32; 3]) -> f32 {
self.nodes.get(self.index(id)).map_or(0.0, |n| n.1)
}
}
#[must_use]
pub fn generate_marching_tets<F: SdfField + ?Sized>(
sdf: &F,
min: [f32; 3],
max: [f32; 3],
cell: f32,
) -> SdfTetMesh {
assert!(cell > 0.0, "cell must be positive");
let mut mesh = SdfTetMesh::default();
let mut table: DetHashMap<VertexKey, u32> = DetHashMap::default();
let [nx, ny, nz] = lattice_cells(min, max, cell);
let lattice = WarpedLattice::build(sdf, min, cell, [nx, ny, nz]);
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
let corner_ids: [[i32; 3]; 8] = [
[ix, iy, iz],
[ix + 1, iy, iz],
[ix + 1, iy + 1, iz],
[ix, iy + 1, iz],
[ix, iy, iz + 1],
[ix + 1, iy, iz + 1],
[ix + 1, iy + 1, iz + 1],
[ix, iy + 1, iz + 1],
];
let corner_positions: [[f32; 3]; 8] = corner_ids.map(|id| lattice.position(id));
let corner_sdf: [f32; 8] = corner_ids.map(|id| lattice.value(id));
if corner_sdf.iter().all(|&d| d > 0.0) {
continue;
}
for sub_tet in CUBE_FIVE_TETS[cell_parity(ix, iy, iz)] {
let ids = sub_tet.map(|i| corner_ids[i]);
let positions = sub_tet.map(|i| corner_positions[i]);
let sdf_values = sub_tet.map(|i| corner_sdf[i]);
marching_tet_emit(&mut mesh, &mut table, ids, positions, sdf_values);
}
}
}
}
mesh
}
fn corner_pos(min: [f32; 3], cell: f32, ix: i32, iy: i32, iz: i32) -> [f32; 3] {
[
min[0] + (ix as f32) * cell,
min[1] + (iy as f32) * cell,
min[2] + (iz as f32) * cell,
]
}
fn marching_tet_emit(
mesh: &mut SdfTetMesh,
table: &mut DetHashMap<VertexKey, u32>,
ids: [[i32; 3]; 4],
v: [[f32; 3]; 4],
sdf: [f32; 4],
) {
let mut mask: u8 = 0;
for (i, &d) in sdf.iter().enumerate() {
if d < 0.0 {
mask |= 1 << i;
}
}
if mask == 0b0000 {
return;
}
let corner = |mesh: &mut SdfTetMesh, table: &mut DetHashMap<VertexKey, u32>, i: usize| -> u32 {
intern(mesh, table, VertexKey::Corner(ids[i]), v[i])
};
if mask == 0b1111 {
let c = [
corner(mesh, table, 0),
corner(mesh, table, 1),
corner(mesh, table, 2),
corner(mesh, table, 3),
];
push_tet(mesh, c);
return;
}
match mask.count_ones() {
1 => {
let inside = mask.trailing_zeros() as usize;
let others = other_three(inside);
let p = corner(mesh, table, inside);
let e0 = crossing_vertex(mesh, table, ids, v, sdf, inside, others[0]);
let e1 = crossing_vertex(mesh, table, ids, v, sdf, inside, others[1]);
let e2 = crossing_vertex(mesh, table, ids, v, sdf, inside, others[2]);
push_tet(mesh, [p, e0, e1, e2]);
}
3 => {
let outside = (!mask & 0b1111).trailing_zeros() as usize;
let insides = other_three(outside);
let a = corner(mesh, table, insides[0]);
let b = corner(mesh, table, insides[1]);
let c = corner(mesh, table, insides[2]);
let e0 = crossing_vertex(mesh, table, ids, v, sdf, insides[0], outside);
let e1 = crossing_vertex(mesh, table, ids, v, sdf, insides[1], outside);
let e2 = crossing_vertex(mesh, table, ids, v, sdf, insides[2], outside);
for tet in prism_to_tets([a, b, c], [e0, e1, e2]) {
push_tet(mesh, tet);
}
}
2 => {
let mut insides = [0_usize; 2];
let mut outsides = [0_usize; 2];
let mut ip = 0;
let mut op = 0;
for i in 0..4 {
if mask & (1 << i) != 0 {
insides[ip] = i;
ip += 1;
} else {
outsides[op] = i;
op += 1;
}
}
let a = corner(mesh, table, insides[0]);
let b = corner(mesh, table, insides[1]);
let e_a_c = crossing_vertex(mesh, table, ids, v, sdf, insides[0], outsides[0]);
let e_a_d = crossing_vertex(mesh, table, ids, v, sdf, insides[0], outsides[1]);
let e_b_c = crossing_vertex(mesh, table, ids, v, sdf, insides[1], outsides[0]);
let e_b_d = crossing_vertex(mesh, table, ids, v, sdf, insides[1], outsides[1]);
for tet in prism_to_tets([a, e_a_c, e_a_d], [b, e_b_c, e_b_d]) {
push_tet(mesh, tet);
}
}
_ => unreachable!("count 0 and 4 handled above"),
}
}
fn other_three(exclude: usize) -> [usize; 3] {
let mut out = [0_usize; 3];
let mut cursor = 0;
for i in 0..4 {
if i != exclude {
out[cursor] = i;
cursor += 1;
}
}
out
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum VertexKey {
Corner([i32; 3]),
Crossing([i32; 3], [i32; 3]),
}
impl VertexKey {
#[inline]
fn crossing(a: [i32; 3], b: [i32; 3]) -> Self {
if a <= b {
Self::Crossing(a, b)
} else {
Self::Crossing(b, a)
}
}
}
fn intern(
mesh: &mut SdfTetMesh,
table: &mut DetHashMap<VertexKey, u32>,
key: VertexKey,
position: [f32; 3],
) -> u32 {
*table.entry(key).or_insert_with(|| {
let index = mesh.vertices.len() as u32;
mesh.vertices.push(position);
index
})
}
const SNAP_CELL_FRACTION: f32 = 0.30;
fn crossing_vertex(
mesh: &mut SdfTetMesh,
table: &mut DetHashMap<VertexKey, u32>,
ids: [[i32; 3]; 4],
v: [[f32; 3]; 4],
sdf: [f32; 4],
a: usize,
b: usize,
) -> u32 {
let denom = sdf[a] - sdf[b];
let t = if denom.abs() < 1.0e-9 {
0.5
} else {
sdf[a] / denom
};
if t <= 0.0 {
return intern(mesh, table, VertexKey::Corner(ids[a]), v[a]);
}
if t >= 1.0 {
return intern(mesh, table, VertexKey::Corner(ids[b]), v[b]);
}
let position = [
v[a][0] + t * (v[b][0] - v[a][0]),
v[a][1] + t * (v[b][1] - v[a][1]),
v[a][2] + t * (v[b][2] - v[a][2]),
];
intern(mesh, table, VertexKey::crossing(ids[a], ids[b]), position)
}
fn prism_to_tets(bottom: [u32; 3], top: [u32; 3]) -> [[u32; 4]; 3] {
let mut b = bottom;
let mut t = top;
let all = [b[0], b[1], b[2], t[0], t[1], t[2]];
let mut lowest = 0usize;
for (i, v) in all.iter().enumerate() {
if *v < all[lowest] {
lowest = i;
}
}
if lowest >= 3 {
core::mem::swap(&mut b, &mut t);
lowest -= 3;
}
let b = [b[lowest], b[(lowest + 1) % 3], b[(lowest + 2) % 3]];
let t = [t[lowest], t[(lowest + 1) % 3], t[(lowest + 2) % 3]];
if b[1].min(t[2]) < b[2].min(t[1]) {
[
[b[0], b[1], b[2], t[2]],
[b[0], b[1], t[2], t[1]],
[b[0], t[1], t[2], t[0]],
]
} else {
[
[b[0], b[1], b[2], t[1]],
[b[0], t[1], b[2], t[2]],
[b[0], t[1], t[2], t[0]],
]
}
}
fn push_tet(mesh: &mut SdfTetMesh, vertices: [u32; 4]) {
for i in 0..4 {
for j in (i + 1)..4 {
if vertices[i] == vertices[j] {
return;
}
}
}
let p = vertices.map(|v| mesh.vertices[v as usize]);
let signed = tet_signed_volume_x6(p);
if signed == 0.0 {
return;
}
let vertices = if signed < 0.0 {
[vertices[0], vertices[1], vertices[3], vertices[2]]
} else {
vertices
};
mesh.tets.push(Tetrahedron { vertices });
}
fn tet_signed_volume_x6(p: [[f32; 3]; 4]) -> f64 {
let e = |i: usize, k: usize| f64::from(p[i][k]) - f64::from(p[0][k]);
let a = [e(1, 0), e(1, 1), e(1, 2)];
let b = [e(2, 0), e(2, 1), e(2, 2)];
let c = [e(3, 0), e(3, 1), e(3, 2)];
a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
+ a[2] * (b[0] * c[1] - b[1] * c[0])
}
pub(crate) const CUBE_FIVE_TETS: [[[usize; 4]; 5]; 2] = [
[
[0, 1, 2, 5],
[0, 2, 3, 7],
[0, 4, 5, 7],
[2, 5, 6, 7],
[0, 2, 5, 7],
],
[
[0, 1, 3, 4],
[1, 2, 3, 6],
[1, 4, 5, 6],
[3, 4, 6, 7],
[1, 3, 4, 6],
],
];
#[inline]
pub(crate) const fn cell_parity(ix: i32, iy: i32, iz: i32) -> usize {
((ix + iy + iz).rem_euclid(2)) as usize
}
fn cube_to_five_tets(v: [u32; 8], parity: usize) -> [Tetrahedron; 5] {
CUBE_FIVE_TETS[parity].map(|t| Tetrahedron {
vertices: [v[t[0]], v[t[1]], v[t[2]], v[t[3]]],
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf_collider::ClosureSdf;
fn unit_cube() -> ClosureSdf {
ClosureSdf::new(
|x, y, z| {
let ax = x.abs() - 1.0;
let ay = y.abs() - 1.0;
let az = z.abs() - 1.0;
ax.max(ay).max(az)
},
|_x, _y, _z| (1.0, 0.0, 0.0),
)
}
fn ball_sdf() -> ClosureSdf {
ClosureSdf::new(
|x, y, z| (x * x + y * y + z * z).sqrt() - 0.9,
|x, y, z| {
let len = (x * x + y * y + z * z).sqrt().max(1.0e-6);
(x / len, y / len, z / len)
},
)
}
#[test]
fn generate_returns_empty_mesh_outside_interior() {
let sdf = ball_sdf();
let mesh = generate(&sdf, [2.0, 2.0, 2.0], [3.0, 3.0, 3.0], 0.5);
assert!(mesh.tets.is_empty());
assert!(mesh.vertices.is_empty());
}
#[test]
fn generate_produces_tets_inside_cube() {
let sdf = unit_cube();
let mesh = generate(&sdf, [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5], 0.5);
assert!(!mesh.tets.is_empty());
assert_eq!(mesh.tet_count() % 5, 0);
}
#[test]
fn vertices_are_deduplicated() {
let sdf = unit_cube();
let mesh = generate(&sdf, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0], 1.0);
assert!(mesh.vertex_count() <= 27);
}
#[test]
#[should_panic(expected = "cell must be positive")]
fn generate_panics_on_zero_cell() {
let sdf = unit_cube();
let _ = generate(&sdf, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0], 0.0);
}
#[test]
fn tet_count_helper_matches_len() {
let sdf = unit_cube();
let mesh = generate(&sdf, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0], 0.5);
assert_eq!(mesh.tet_count(), mesh.tets.len());
assert_eq!(mesh.vertex_count(), mesh.vertices.len());
}
#[test]
fn marching_tets_produces_more_tets_than_interior_only() {
let sdf = ball_sdf();
let bbox_min = [-1.5, -1.5, -1.5];
let bbox_max = [1.5, 1.5, 1.5];
let cell = 0.5;
let interior = generate(&sdf, bbox_min, bbox_max, cell);
let marching = generate_marching_tets(&sdf, bbox_min, bbox_max, cell);
assert!(marching.tet_count() >= interior.tet_count());
}
#[test]
fn marching_tets_yields_empty_outside_geometry() {
let sdf = ball_sdf();
let mesh = generate_marching_tets(&sdf, [3.0, 3.0, 3.0], [4.0, 4.0, 4.0], 0.5);
assert!(mesh.tets.is_empty());
}
#[test]
fn marching_tets_reproduces_interior_when_boundary_absent() {
let sdf = ball_sdf();
let interior = generate(&sdf, [-0.4, -0.4, -0.4], [0.4, 0.4, 0.4], 0.2);
let marching = generate_marching_tets(&sdf, [-0.4, -0.4, -0.4], [0.4, 0.4, 0.4], 0.2);
assert_eq!(marching.tet_count(), interior.tet_count());
}
#[test]
#[should_panic(expected = "cell must be positive")]
fn marching_tets_panics_on_zero_cell() {
let sdf = ball_sdf();
let _ = generate_marching_tets(&sdf, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0], 0.0);
}
#[test]
fn other_three_excludes_input() {
assert_eq!(other_three(0), [1, 2, 3]);
assert_eq!(other_three(1), [0, 2, 3]);
assert_eq!(other_three(2), [0, 1, 3]);
assert_eq!(other_three(3), [0, 1, 2]);
}
fn one_tet_mesh(side: f32) -> SdfTetMesh {
let mut m = SdfTetMesh::default();
m.vertices.push([0.0, 0.0, 0.0]);
m.vertices.push([side, 0.0, 0.0]);
m.vertices.push([0.0, side, 0.0]);
m.vertices.push([0.0, 0.0, side]);
m.tets.push(Tetrahedron {
vertices: [0, 1, 2, 3],
});
m
}
#[test]
fn refine_reports_an_exhausted_budget_instead_of_a_pass_count() {
let mut m = one_tet_mesh(1.0);
let cramped = m.try_refine_conforming(0.7, 4);
assert!(
matches!(cramped, Err(RefineError::Unfinished { passes: 4, .. })),
"four passes is not enough here and the refiner has to say so: {cramped:?}"
);
assert!(
m.max_edge_length() > 0.7,
"and the mesh is indeed not there yet"
);
let mut m = one_tet_mesh(1.0);
let passes = m
.try_refine_conforming(0.7, 32)
.expect("thirty-two passes finishes a single tetrahedron");
assert!(
m.max_edge_length() <= 0.7,
"a clean finish means the threshold is met"
);
assert!(passes >= 1);
assert!(m.tet_count() >= 2);
}
#[test]
fn refine_stops_when_no_edge_exceeds_threshold() {
let mut m = one_tet_mesh(1.0);
let passes = m
.try_refine_conforming(10.0, 8)
.expect("nothing to do finishes immediately");
assert_eq!(m.tet_count(), 1);
assert_eq!(passes, 0);
}
#[test]
#[should_panic(expected = "max_edge_length must be positive")]
fn refine_panics_on_nonpositive_threshold() {
let mut m = one_tet_mesh(1.0);
let _ = m.try_refine_conforming(0.0, 4);
}
}