use crate::vpx::gameitem::primitive::VertexWrapper;
use crate::vpx::obj::VpxFace;
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct MeshValidationResult {
pub vertex_count: usize,
pub face_count: usize,
pub invalid_indices: Vec<(usize, i64)>,
pub zero_normals: Vec<usize>,
pub non_unit_normals: Vec<(usize, f32)>,
pub degenerate_faces: Vec<usize>,
pub boundary_edges: Vec<(i64, i64)>,
pub non_manifold_edges: Vec<(i64, i64)>,
pub inconsistent_winding_faces: Vec<usize>,
}
impl MeshValidationResult {
#[allow(dead_code)]
pub fn is_valid(&self) -> bool {
self.invalid_indices.is_empty()
&& self.zero_normals.is_empty()
&& self.degenerate_faces.is_empty()
}
#[allow(dead_code)]
pub fn is_watertight(&self) -> bool {
self.boundary_edges.is_empty() && self.non_manifold_edges.is_empty()
}
#[allow(dead_code)]
pub fn has_valid_normals(&self) -> bool {
self.zero_normals.is_empty() && self.non_unit_normals.is_empty()
}
#[allow(dead_code)]
pub fn summary(&self) -> String {
let mut issues = Vec::new();
if !self.invalid_indices.is_empty() {
issues.push(format!(
"{} invalid indices (out of bounds)",
self.invalid_indices.len()
));
}
if !self.zero_normals.is_empty() {
issues.push(format!("{} zero-length normals", self.zero_normals.len()));
}
if !self.non_unit_normals.is_empty() {
issues.push(format!("{} non-unit normals", self.non_unit_normals.len()));
}
if !self.degenerate_faces.is_empty() {
issues.push(format!(
"{} degenerate triangles",
self.degenerate_faces.len()
));
}
if !self.boundary_edges.is_empty() {
issues.push(format!(
"{} boundary edges (holes)",
self.boundary_edges.len()
));
}
if !self.non_manifold_edges.is_empty() {
issues.push(format!(
"{} non-manifold edges",
self.non_manifold_edges.len()
));
}
if !self.inconsistent_winding_faces.is_empty() {
issues.push(format!(
"{} faces with inconsistent winding",
self.inconsistent_winding_faces.len()
));
}
if issues.is_empty() {
format!(
"Mesh valid: {} vertices, {} faces",
self.vertex_count, self.face_count
)
} else {
format!(
"Mesh issues ({} vertices, {} faces): {}",
self.vertex_count,
self.face_count,
issues.join(", ")
)
}
}
}
#[allow(dead_code)]
pub fn validate_mesh(vertices: &[VertexWrapper], faces: &[VpxFace]) -> MeshValidationResult {
let mut result = MeshValidationResult {
vertex_count: vertices.len(),
face_count: faces.len(),
..Default::default()
};
for (face_idx, face) in faces.iter().enumerate() {
let vertex_count = vertices.len() as i64;
if face.i0 < 0 || face.i0 >= vertex_count {
result.invalid_indices.push((face_idx, face.i0));
}
if face.i1 < 0 || face.i1 >= vertex_count {
result.invalid_indices.push((face_idx, face.i1));
}
if face.i2 < 0 || face.i2 >= vertex_count {
result.invalid_indices.push((face_idx, face.i2));
}
}
const EPSILON: f32 = 0.0001;
const UNIT_TOLERANCE: f32 = 0.01;
for (idx, wrapper) in vertices.iter().enumerate() {
let v = &wrapper.vertex;
let normal_length = (v.nx * v.nx + v.ny * v.ny + v.nz * v.nz).sqrt();
if normal_length < EPSILON {
result.zero_normals.push(idx);
} else if (normal_length - 1.0).abs() > UNIT_TOLERANCE {
result.non_unit_normals.push((idx, normal_length));
}
}
if result.invalid_indices.is_empty() {
for (face_idx, face) in faces.iter().enumerate() {
let v0 = &vertices[face.i0 as usize].vertex;
let v1 = &vertices[face.i1 as usize].vertex;
let v2 = &vertices[face.i2 as usize].vertex;
let e1 = (v1.x - v0.x, v1.y - v0.y, v1.z - v0.z);
let e2 = (v2.x - v0.x, v2.y - v0.y, v2.z - v0.z);
let cross = (
e1.1 * e2.2 - e1.2 * e2.1,
e1.2 * e2.0 - e1.0 * e2.2,
e1.0 * e2.1 - e1.1 * e2.0,
);
let area_squared = cross.0 * cross.0 + cross.1 * cross.1 + cross.2 * cross.2;
if area_squared < EPSILON * EPSILON {
result.degenerate_faces.push(face_idx);
}
}
}
if result.invalid_indices.is_empty() {
let mut edge_counts: HashMap<(i64, i64), i32> = HashMap::new();
for face in faces {
let edges = [
(face.i0.min(face.i1), face.i0.max(face.i1)),
(face.i1.min(face.i2), face.i1.max(face.i2)),
(face.i2.min(face.i0), face.i2.max(face.i0)),
];
for edge in edges {
*edge_counts.entry(edge).or_insert(0) += 1;
}
}
for (edge, count) in edge_counts {
if count == 1 {
result.boundary_edges.push(edge);
} else if count > 2 {
result.non_manifold_edges.push(edge);
}
}
}
if result.invalid_indices.is_empty() && result.boundary_edges.is_empty() {
let mut directed_edge_faces: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
for (face_idx, face) in faces.iter().enumerate() {
let edges = [(face.i0, face.i1), (face.i1, face.i2), (face.i2, face.i0)];
for edge in edges {
directed_edge_faces.entry(edge).or_default().push(face_idx);
}
}
for ((v0, v1), face_indices) in &directed_edge_faces {
if face_indices.len() > 1 {
for &face_idx in face_indices {
if !result.inconsistent_winding_faces.contains(&face_idx) {
result.inconsistent_winding_faces.push(face_idx);
}
}
}
let reverse = (*v1, *v0);
if !directed_edge_faces.contains_key(&reverse)
&& !result
.boundary_edges
.contains(&((*v0).min(*v1), (*v0).max(*v1)))
{
}
}
}
result
}
#[allow(dead_code)]
pub fn check_normal_consistency(
vertices: &[VertexWrapper],
faces: &[VpxFace],
tolerance: f32,
) -> Vec<usize> {
let mut inconsistent = Vec::new();
for (face_idx, face) in faces.iter().enumerate() {
if face.i0 < 0
|| face.i0 >= vertices.len() as i64
|| face.i1 < 0
|| face.i1 >= vertices.len() as i64
|| face.i2 < 0
|| face.i2 >= vertices.len() as i64
{
continue;
}
let v0 = &vertices[face.i0 as usize].vertex;
let v1 = &vertices[face.i1 as usize].vertex;
let v2 = &vertices[face.i2 as usize].vertex;
let e1 = (v1.x - v0.x, v1.y - v0.y, v1.z - v0.z);
let e2 = (v2.x - v0.x, v2.y - v0.y, v2.z - v0.z);
let face_normal = (
e1.1 * e2.2 - e1.2 * e2.1,
e1.2 * e2.0 - e1.0 * e2.2,
e1.0 * e2.1 - e1.1 * e2.0,
);
let face_normal_len = (face_normal.0 * face_normal.0
+ face_normal.1 * face_normal.1
+ face_normal.2 * face_normal.2)
.sqrt();
if face_normal_len < 0.0001 {
continue; }
let face_normal = (
face_normal.0 / face_normal_len,
face_normal.1 / face_normal_len,
face_normal.2 / face_normal_len,
);
for v in [v0, v1, v2] {
let vertex_normal_len = (v.nx * v.nx + v.ny * v.ny + v.nz * v.nz).sqrt();
if vertex_normal_len < 0.0001 {
continue;
}
let dot = (face_normal.0 * v.nx + face_normal.1 * v.ny + face_normal.2 * v.nz)
/ vertex_normal_len;
if dot < tolerance {
if !inconsistent.contains(&face_idx) {
inconsistent.push(face_idx);
}
break;
}
}
}
inconsistent
}
#[allow(dead_code)]
pub fn check_normal_consistency_gltf(vertices: &[VertexWrapper], faces: &[VpxFace]) -> Vec<usize> {
let mut inconsistent = Vec::new();
for (face_idx, face) in faces.iter().enumerate() {
if face.i0 < 0
|| face.i0 >= vertices.len() as i64
|| face.i1 < 0
|| face.i1 >= vertices.len() as i64
|| face.i2 < 0
|| face.i2 >= vertices.len() as i64
{
continue;
}
let v0 = &vertices[face.i0 as usize].vertex;
let v1 = &vertices[face.i1 as usize].vertex;
let v2 = &vertices[face.i2 as usize].vertex;
let a = [v0.x, v0.z, v0.y];
let b = [v2.x, v2.z, v2.y]; let c = [v1.x, v1.z, v1.y];
let e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let e2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let geo_nx = e1[1] * e2[2] - e1[2] * e2[1];
let geo_ny = e1[2] * e2[0] - e1[0] * e2[2];
let geo_nz = e1[0] * e2[1] - e1[1] * e2[0];
let geo_len = (geo_nx * geo_nx + geo_ny * geo_ny + geo_nz * geo_nz).sqrt();
if geo_len < 0.0001 {
continue; }
let vn = [v0.nx, v0.nz, v0.ny];
let dot = vn[0] * geo_nx + vn[1] * geo_ny + vn[2] * geo_nz;
if dot <= 0.0 {
inconsistent.push(face_idx);
}
}
inconsistent
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vpx::mesh::flippers::build_flipper_mesh;
use crate::vpx::model::Vertex3dNoTex2;
fn make_vertex(x: f32, y: f32, z: f32, nx: f32, ny: f32, nz: f32) -> VertexWrapper {
VertexWrapper::new(
[0u8; 32],
Vertex3dNoTex2 {
x,
y,
z,
nx,
ny,
nz,
tu: 0.0,
tv: 0.0,
},
)
}
#[test]
fn test_valid_triangle() {
let vertices = vec![
make_vertex(0.0, 0.0, 0.0, 0.0, 0.0, 1.0),
make_vertex(1.0, 0.0, 0.0, 0.0, 0.0, 1.0),
make_vertex(0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
];
let faces = vec![VpxFace {
i0: 0,
i1: 1,
i2: 2,
}];
let result = validate_mesh(&vertices, &faces);
assert!(result.invalid_indices.is_empty());
assert!(result.zero_normals.is_empty());
assert!(result.degenerate_faces.is_empty());
}
#[test]
fn test_invalid_index() {
let vertices = vec![
make_vertex(0.0, 0.0, 0.0, 0.0, 0.0, 1.0),
make_vertex(1.0, 0.0, 0.0, 0.0, 0.0, 1.0),
];
let faces = vec![VpxFace {
i0: 0,
i1: 1,
i2: 5, }];
let result = validate_mesh(&vertices, &faces);
assert_eq!(result.invalid_indices.len(), 1);
}
#[test]
fn test_zero_normal() {
let vertices = vec![
make_vertex(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), make_vertex(1.0, 0.0, 0.0, 0.0, 0.0, 1.0),
make_vertex(0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
];
let faces = vec![VpxFace {
i0: 0,
i1: 1,
i2: 2,
}];
let result = validate_mesh(&vertices, &faces);
assert_eq!(result.zero_normals.len(), 1);
assert_eq!(result.zero_normals[0], 0);
}
#[test]
fn test_degenerate_triangle() {
let vertices = vec![
make_vertex(0.0, 0.0, 0.0, 0.0, 0.0, 1.0),
make_vertex(1.0, 0.0, 0.0, 0.0, 0.0, 1.0),
make_vertex(2.0, 0.0, 0.0, 0.0, 0.0, 1.0), ];
let faces = vec![VpxFace {
i0: 0,
i1: 1,
i2: 2,
}];
let result = validate_mesh(&vertices, &faces);
assert_eq!(result.degenerate_faces.len(), 1);
}
#[test]
fn test_watertight_cube() {
let vertices = vec![
make_vertex(0.0, 0.0, 0.0, 0.0, 0.0, -1.0),
make_vertex(1.0, 0.0, 0.0, 0.0, 0.0, -1.0),
make_vertex(1.0, 1.0, 0.0, 0.0, 0.0, -1.0),
make_vertex(0.0, 1.0, 0.0, 0.0, 0.0, -1.0),
make_vertex(0.0, 0.0, 1.0, 0.0, 0.0, 1.0),
make_vertex(1.0, 0.0, 1.0, 0.0, 0.0, 1.0),
make_vertex(1.0, 1.0, 1.0, 0.0, 0.0, 1.0),
make_vertex(0.0, 1.0, 1.0, 0.0, 0.0, 1.0),
];
let faces = vec![
VpxFace {
i0: 0,
i1: 2,
i2: 1,
},
VpxFace {
i0: 0,
i1: 3,
i2: 2,
},
VpxFace {
i0: 4,
i1: 5,
i2: 6,
},
VpxFace {
i0: 4,
i1: 6,
i2: 7,
},
VpxFace {
i0: 0,
i1: 1,
i2: 5,
},
VpxFace {
i0: 0,
i1: 5,
i2: 4,
},
VpxFace {
i0: 2,
i1: 3,
i2: 7,
},
VpxFace {
i0: 2,
i1: 7,
i2: 6,
},
VpxFace {
i0: 0,
i1: 4,
i2: 7,
},
VpxFace {
i0: 0,
i1: 7,
i2: 3,
},
VpxFace {
i0: 1,
i1: 2,
i2: 6,
},
VpxFace {
i0: 1,
i1: 6,
i2: 5,
},
];
let result = validate_mesh(&vertices, &faces);
assert!(
result.is_watertight(),
"Cube should be watertight: {:?}",
result.boundary_edges
);
}
#[test]
fn test_mesh_with_hole() {
let vertices = vec![
make_vertex(0.0, 0.0, 0.0, 0.0, 0.0, 1.0),
make_vertex(1.0, 0.0, 0.0, 0.0, 0.0, 1.0),
make_vertex(1.0, 1.0, 0.0, 0.0, 0.0, 1.0),
make_vertex(0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
];
let faces = vec![
VpxFace {
i0: 0,
i1: 1,
i2: 2,
},
VpxFace {
i0: 0,
i1: 2,
i2: 3,
},
];
let result = validate_mesh(&vertices, &faces);
assert!(!result.is_watertight(), "Plane should have boundary edges");
assert!(!result.boundary_edges.is_empty());
}
#[test]
fn test_flipper_mesh_validation() {
use crate::vpx::gameitem::flipper::Flipper;
use fake::{Fake, Faker};
let mut flipper: Flipper = Faker.fake();
flipper.name = "TestFlipper".to_string();
flipper.is_visible = true;
flipper.base_radius = 21.5;
flipper.end_radius = 13.0;
flipper.flipper_radius_max = 130.0;
flipper.height = 50.0;
flipper.rubber_thickness = Some(7.0);
flipper.rubber_height = Some(19.0);
flipper.rubber_width = Some(24.0);
flipper.start_angle = 121.0;
let (vertices, faces) =
build_flipper_mesh(&flipper, 0.0).expect("Flipper mesh should be generated");
let result = validate_mesh(&vertices, &faces);
assert!(
result.invalid_indices.is_empty(),
"Flipper mesh has invalid indices: {:?}",
result.invalid_indices
);
assert!(
result.degenerate_faces.is_empty(),
"Flipper mesh has degenerate faces: {:?}",
result.degenerate_faces
);
assert!(
result.non_manifold_edges.is_empty(),
"Flipper mesh has non-manifold edges: {:?}",
result.non_manifold_edges
);
let inconsistent_normals = check_normal_consistency(&vertices, &faces, 0.0);
assert!(
inconsistent_normals.len() < faces.len() / 4, "Flipper mesh has too many faces with inconsistent normals: {} out of {} faces",
inconsistent_normals.len(),
faces.len()
);
if !result.zero_normals.is_empty() {
eprintln!(
"Warning: Flipper mesh has {} zero normals",
result.zero_normals.len()
);
}
if !result.non_unit_normals.is_empty() {
eprintln!(
"Warning: Flipper mesh has {} non-unit normals",
result.non_unit_normals.len()
);
}
}
}