use crate::tessellate::TriMesh;
use crate::vec3;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct MeshValidation {
pub is_watertight: bool,
pub is_consistently_oriented: bool,
pub has_no_degenerate_faces: bool,
pub signed_volume: f64,
pub euler_number: i64,
pub n_connected_components: usize,
}
impl TriMesh {
pub fn validate(&self) -> MeshValidation {
let half_edges = build_half_edge_map(&self.triangles);
let edge_counts = build_edge_counts(&self.triangles);
let is_watertight = check_watertight(&edge_counts);
let is_consistently_oriented = check_consistent_orientation(&half_edges);
let has_no_degenerate_faces = check_no_degenerate(&self.vertices, &self.triangles);
let signed_volume = compute_signed_volume(&self.vertices, &self.triangles);
let euler_number =
compute_euler_number(self.vertices.len(), edge_counts.len(), self.triangles.len());
let n_connected_components =
compute_connected_components(self.vertices.len(), &self.triangles);
MeshValidation {
is_watertight,
is_consistently_oriented,
has_no_degenerate_faces,
signed_volume,
euler_number,
n_connected_components,
}
}
}
fn build_half_edge_map(triangles: &[[usize; 3]]) -> HashMap<(usize, usize), usize> {
let mut map = HashMap::new();
for tri in triangles {
for i in 0..3 {
let a = tri[i];
let b = tri[(i + 1) % 3];
*map.entry((a, b)).or_insert(0) += 1;
}
}
map
}
fn build_edge_counts(triangles: &[[usize; 3]]) -> HashMap<(usize, usize), usize> {
let mut map = HashMap::new();
for tri in triangles {
for i in 0..3 {
let a = tri[i];
let b = tri[(i + 1) % 3];
let key = if a < b { (a, b) } else { (b, a) };
*map.entry(key).or_insert(0) += 1;
}
}
map
}
fn check_watertight(edge_counts: &HashMap<(usize, usize), usize>) -> bool {
edge_counts.values().all(|&count| count == 2)
}
fn check_consistent_orientation(half_edges: &HashMap<(usize, usize), usize>) -> bool {
for &(a, b) in half_edges.keys() {
if !half_edges.contains_key(&(b, a)) {
return false;
}
}
half_edges.values().all(|&count| count == 1)
}
fn check_no_degenerate(vertices: &[[f64; 3]], triangles: &[[usize; 3]]) -> bool {
for tri in triangles {
let v0 = vertices[tri[0]];
let v1 = vertices[tri[1]];
let v2 = vertices[tri[2]];
let area = vec3::length(vec3::cross(vec3::sub(v1, v0), vec3::sub(v2, v0))) * 0.5;
if area <= 1e-20 {
return false;
}
}
true
}
fn compute_signed_volume(vertices: &[[f64; 3]], triangles: &[[usize; 3]]) -> f64 {
let mut volume = 0.0;
for tri in triangles {
let v0 = vertices[tri[0]];
let v1 = vertices[tri[1]];
let v2 = vertices[tri[2]];
volume += vec3::dot(v0, vec3::cross(v1, v2));
}
volume / 6.0
}
fn compute_euler_number(n_vertices: usize, n_edges: usize, n_faces: usize) -> i64 {
n_vertices as i64 - n_edges as i64 + n_faces as i64
}
fn compute_connected_components(n_vertices: usize, triangles: &[[usize; 3]]) -> usize {
if n_vertices == 0 {
return 0;
}
let mut parent: Vec<usize> = (0..n_vertices).collect();
let mut rank = vec![0u32; n_vertices];
fn find(parent: &mut [usize], x: usize) -> usize {
if parent[x] != x {
parent[x] = find(parent, parent[x]);
}
parent[x]
}
fn union(parent: &mut [usize], rank: &mut [u32], a: usize, b: usize) {
let ra = find(parent, a);
let rb = find(parent, b);
if ra == rb {
return;
}
match rank[ra].cmp(&rank[rb]) {
std::cmp::Ordering::Less => parent[ra] = rb,
std::cmp::Ordering::Greater => parent[rb] = ra,
std::cmp::Ordering::Equal => {
parent[rb] = ra;
rank[ra] += 1;
}
}
}
let mut used = vec![false; n_vertices];
for tri in triangles {
used[tri[0]] = true;
used[tri[1]] = true;
used[tri[2]] = true;
union(&mut parent, &mut rank, tri[0], tri[1]);
union(&mut parent, &mut rank, tri[0], tri[2]);
}
let mut roots = std::collections::HashSet::new();
for (i, is_used) in used.iter().enumerate().take(n_vertices) {
if *is_used {
roots.insert(find(&mut parent, i));
}
}
roots.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_triangle_not_watertight() {
let mesh = TriMesh {
vertices: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
normals: vec![[0.0, 0.0, 1.0]; 3],
triangles: vec![[0, 1, 2]],
};
let v = mesh.validate();
assert!(!v.is_watertight);
assert_eq!(v.n_connected_components, 1);
}
#[test]
fn tetrahedron_watertight() {
let mesh = TriMesh {
vertices: vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 0.866, 0.0],
[0.5, 0.289, 0.816],
],
normals: vec![[0.0, 0.0, 1.0]; 4],
triangles: vec![
[0, 2, 1], [0, 1, 3], [1, 2, 3], [2, 0, 3], ],
};
let v = mesh.validate();
assert!(v.is_watertight, "tetrahedron not watertight");
assert!(
v.is_consistently_oriented,
"tetrahedron orientation inconsistent"
);
assert!(
v.has_no_degenerate_faces,
"tetrahedron has degenerate faces"
);
assert_eq!(v.euler_number, 2);
assert_eq!(v.n_connected_components, 1);
assert!(v.signed_volume.abs() > 1e-10, "tetrahedron volume is zero");
}
#[test]
fn empty_mesh() {
let mesh = TriMesh::new();
let v = mesh.validate();
assert!(v.is_watertight); assert!(v.is_consistently_oriented);
assert!(v.has_no_degenerate_faces);
assert!((v.signed_volume).abs() < 1e-20);
assert_eq!(v.n_connected_components, 0);
}
}