use crate::mesh::MeshBuffers;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct IntegrityReport {
pub vertex_count: usize,
pub face_count: usize,
pub degenerate_faces: Vec<usize>, pub out_of_bounds_indices: Vec<usize>, pub non_manifold_edges: usize,
pub has_nan_positions: bool,
pub has_inf_positions: bool,
}
impl IntegrityReport {
pub fn is_valid(&self) -> bool {
self.degenerate_faces.is_empty()
&& self.out_of_bounds_indices.is_empty()
&& self.non_manifold_edges == 0
&& !self.has_nan_positions
&& !self.has_inf_positions
}
pub fn summary(&self) -> String {
if self.is_valid() {
format!("OK: {} verts, {} faces", self.vertex_count, self.face_count)
} else {
format!(
"INVALID: {} degenerate, {} oob, {} non-manifold, nan={}, inf={}",
self.degenerate_faces.len(),
self.out_of_bounds_indices.len(),
self.non_manifold_edges,
self.has_nan_positions,
self.has_inf_positions
)
}
}
}
pub fn check_integrity(buf: &MeshBuffers) -> IntegrityReport {
let n = buf.positions.len();
let face_count = buf.indices.len() / 3;
let mut has_nan = false;
let mut has_inf = false;
for pos in &buf.positions {
for &c in pos {
if c.is_nan() {
has_nan = true;
}
if c.is_infinite() {
has_inf = true;
}
}
}
let mut degenerate_faces = Vec::new();
let mut out_of_bounds_indices = Vec::new();
let mut edge_count: HashMap<(u32, u32), u32> = HashMap::new();
for (fi, tri) in buf.indices.chunks_exact(3).enumerate() {
let (i0, i1, i2) = (tri[0], tri[1], tri[2]);
if i0 as usize >= n || i1 as usize >= n || i2 as usize >= n {
out_of_bounds_indices.push(fi);
continue;
}
let p0 = buf.positions[i0 as usize];
let p1 = buf.positions[i1 as usize];
let p2 = buf.positions[i2 as usize];
let e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
let e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
let cross_len_sq = {
let cx = e1[1] * e2[2] - e1[2] * e2[1];
let cy = e1[2] * e2[0] - e1[0] * e2[2];
let cz = e1[0] * e2[1] - e1[1] * e2[0];
cx * cx + cy * cy + cz * cz
};
if cross_len_sq < 1e-20 {
degenerate_faces.push(fi);
}
for &(a, b) in &[(i0, i1), (i1, i2), (i2, i0)] {
let key = if a < b { (a, b) } else { (b, a) };
*edge_count.entry(key).or_insert(0) += 1;
}
}
let non_manifold_edges = edge_count.values().filter(|&&c| c > 2).count();
IntegrityReport {
vertex_count: n,
face_count,
degenerate_faces,
out_of_bounds_indices,
non_manifold_edges,
has_nan_positions: has_nan,
has_inf_positions: has_inf,
}
}
pub fn check_index_bounds(buf: &MeshBuffers) -> bool {
let n = buf.positions.len() as u32;
buf.indices.iter().all(|&i| i < n)
}
pub fn check_positions_finite(buf: &MeshBuffers) -> bool {
buf.positions
.iter()
.all(|p| p.iter().all(|c| c.is_finite()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::MeshBuffers as MyMesh;
use oxihuman_morph::engine::MeshBuffers as MB;
use proptest::prelude::*;
fn valid_triangle() -> MyMesh {
MyMesh::from_morph(MB {
positions: 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],
uvs: vec![[0.0, 0.0]; 3],
indices: vec![0, 1, 2],
has_suit: false,
})
}
#[test]
fn valid_mesh_passes() {
let m = valid_triangle();
let report = check_integrity(&m);
assert!(report.is_valid(), "{}", report.summary());
assert_eq!(report.face_count, 1);
assert_eq!(report.vertex_count, 3);
}
#[test]
fn out_of_bounds_index_detected() {
let mut m = valid_triangle();
m.indices.push(0);
m.indices.push(1);
m.indices.push(99); let report = check_integrity(&m);
assert!(!report.out_of_bounds_indices.is_empty());
}
#[test]
fn degenerate_triangle_detected() {
let m = MyMesh::from_morph(MB {
positions: vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0], ],
normals: vec![[0.0, 1.0, 0.0]; 3],
uvs: vec![[0.0, 0.0]; 3],
indices: vec![0, 1, 2],
has_suit: false,
});
let report = check_integrity(&m);
assert!(!report.degenerate_faces.is_empty());
}
#[test]
fn nan_position_detected() {
let m = MyMesh::from_morph(MB {
positions: vec![[f32::NAN, 0.0, 0.0]],
normals: vec![[0.0, 1.0, 0.0]],
uvs: vec![[0.0, 0.0]],
indices: vec![],
has_suit: false,
});
let report = check_integrity(&m);
assert!(report.has_nan_positions);
assert!(!report.is_valid());
}
#[test]
fn index_bounds_fast_path() {
let m = valid_triangle();
assert!(check_index_bounds(&m));
}
#[test]
fn finite_positions_check() {
let m = valid_triangle();
assert!(check_positions_finite(&m));
}
#[test]
fn non_manifold_edge_detected() {
let m = MyMesh::from_morph(MB {
positions: vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, -1.0, 0.0],
[0.0, 0.5, 1.0],
],
normals: vec![[0.0, 0.0, 1.0]; 5],
uvs: vec![[0.0, 0.0]; 5],
indices: vec![0, 1, 2, 0, 1, 3, 0, 1, 4],
has_suit: false,
});
let report = check_integrity(&m);
assert!(report.non_manifold_edges > 0, "expected non-manifold edge");
assert!(!report.is_valid());
}
#[test]
fn real_base_mesh_integrity() {
use crate::normals::compute_normals;
use crate::suit::apply_suit_flag;
use oxihuman_core::parser::obj::parse_obj;
let path = oxihuman_test_utils::base_obj();
if let Ok(src) = std::fs::read_to_string(&path) {
if let Ok(obj) = parse_obj(&src) {
let morph_buf = oxihuman_morph::engine::MeshBuffers {
positions: obj.positions,
normals: obj.normals,
uvs: obj.uvs,
indices: obj.indices,
has_suit: false,
};
let mut mesh = MyMesh::from_morph(morph_buf);
compute_normals(&mut mesh);
apply_suit_flag(&mut mesh);
let report = check_integrity(&mesh);
assert!(
check_index_bounds(&mesh),
"index out of bounds in real mesh"
);
assert!(check_positions_finite(&mesh), "non-finite in real mesh");
assert!(report.out_of_bounds_indices.is_empty());
assert!(!report.has_nan_positions);
}
}
}
proptest! {
#[test]
fn random_triangles_no_false_oob(
i0 in 0u32..100u32,
i1 in 0u32..100u32,
i2 in 0u32..100u32,
n_verts in 100usize..200usize,
) {
let positions = vec![[0.0f32, 0.0, 0.0]; n_verts];
let mesh = MyMesh::from_morph(MB {
positions,
normals: vec![[0.0, 1.0, 0.0]; n_verts],
uvs: vec![[0.0, 0.0]; n_verts],
indices: vec![i0 % n_verts as u32, i1 % n_verts as u32, i2 % n_verts as u32],
has_suit: false,
});
prop_assert!(check_index_bounds(&mesh));
}
#[test]
fn finite_positions_always_pass_finite_check(
x in -1000.0f32..1000.0f32,
y in -1000.0f32..1000.0f32,
z in -1000.0f32..1000.0f32,
) {
let mesh = MyMesh::from_morph(MB {
positions: vec![[x, y, z]],
normals: vec![[0.0, 1.0, 0.0]],
uvs: vec![[0.0, 0.0]],
indices: vec![],
has_suit: false,
});
prop_assert!(check_positions_finite(&mesh));
}
}
}