#![allow(dead_code)]
use std::collections::HashMap;
use crate::mesh::MeshBuffers;
use crate::normals::compute_normals;
#[inline]
fn add3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn scale3(v: [f32; 3], t: f32) -> [f32; 3] {
[v[0] * t, v[1] * t, v[2] * t]
}
#[inline]
fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
pub struct AdvancedRepairConfig {
pub fill_holes: bool,
pub remove_t_junctions: bool,
pub max_hole_edges: usize,
pub tangent_smooth_iters: u32,
}
impl Default for AdvancedRepairConfig {
fn default() -> Self {
Self {
fill_holes: true,
remove_t_junctions: true,
max_hole_edges: 32,
tangent_smooth_iters: 2,
}
}
}
pub struct AdvancedRepairReport {
pub holes_filled: usize,
pub t_junctions_removed: usize,
pub vertices_added: usize,
pub faces_added: usize,
pub is_manifold: bool,
}
pub fn is_manifold_mesh(indices: &[u32]) -> bool {
let mut edge_count: HashMap<(u32, u32), usize> = HashMap::new();
let n = indices.len() / 3;
for f in 0..n {
let v = [indices[f * 3], indices[f * 3 + 1], indices[f * 3 + 2]];
for e in 0..3 {
let a = v[e];
let b = v[(e + 1) % 3];
let key = (a.min(b), a.max(b));
*edge_count.entry(key).or_insert(0) += 1;
}
}
edge_count.values().all(|&c| c == 1 || c == 2)
}
pub fn find_boundary_holes(indices: &[u32]) -> Vec<Vec<u32>> {
let mut edge_dir: HashMap<(u32, u32), Vec<(u32, u32)>> = HashMap::new();
let n = indices.len() / 3;
for f in 0..n {
let v = [indices[f * 3], indices[f * 3 + 1], indices[f * 3 + 2]];
for e in 0..3 {
let a = v[e];
let b = v[(e + 1) % 3];
let key = (a.min(b), a.max(b));
edge_dir.entry(key).or_default().push((a, b));
}
}
let mut next_boundary: HashMap<u32, u32> = HashMap::new();
for dirs in edge_dir.values() {
if dirs.len() == 1 {
let (a, b) = dirs[0];
next_boundary.insert(b, a);
}
}
let mut visited: HashMap<u32, bool> = HashMap::new();
for &start in next_boundary.keys() {
visited.insert(start, false);
}
let mut holes: Vec<Vec<u32>> = Vec::new();
let starts: Vec<u32> = next_boundary.keys().copied().collect();
for start in starts {
if visited.get(&start).copied().unwrap_or(true) {
continue;
}
let mut loop_verts: Vec<u32> = Vec::new();
let mut current = start;
loop {
if visited.get(¤t).copied().unwrap_or(true) {
break;
}
visited.insert(current, true);
loop_verts.push(current);
match next_boundary.get(¤t) {
Some(&next) => current = next,
None => break,
}
}
if !loop_verts.is_empty() {
holes.push(loop_verts);
}
}
holes
}
pub fn fill_hole_fan(positions: &[[f32; 3]], hole_loop: &[u32]) -> Vec<[u32; 3]> {
if hole_loop.len() < 3 {
return Vec::new();
}
let hub = hole_loop[0];
let n = hole_loop.len();
let mut triangles = Vec::with_capacity(n - 2);
for i in 1..n - 1 {
let a = hole_loop[i];
let b = hole_loop[i + 1];
if hub != a && a != b && hub != b {
triangles.push([hub, a, b]);
}
}
let _ = positions; triangles
}
pub fn fill_hole_smooth(
positions: &[[f32; 3]],
hole_loop: &[u32],
smooth_iters: u32,
) -> (Vec<[f32; 3]>, Vec<[u32; 3]>) {
if hole_loop.len() < 3 {
return (Vec::new(), Vec::new());
}
let mut centroid = [0.0_f32; 3];
for &vi in hole_loop {
let p = positions[vi as usize];
centroid = add3(centroid, p);
}
let n = hole_loop.len() as f32;
centroid = scale3(centroid, 1.0 / n);
for _ in 0..smooth_iters {
let mut avg = [0.0_f32; 3];
for &vi in hole_loop {
avg = add3(avg, positions[vi as usize]);
}
avg = scale3(avg, 1.0 / hole_loop.len() as f32);
centroid = add3(scale3(centroid, 0.5), scale3(avg, 0.5));
}
let centroid_idx = positions.len() as u32;
let mut triangles = Vec::with_capacity(hole_loop.len());
let m = hole_loop.len();
for i in 0..m {
let a = hole_loop[i];
let b = hole_loop[(i + 1) % m];
if centroid_idx != a && a != b && centroid_idx != b {
triangles.push([centroid_idx, a, b]);
}
}
(vec![centroid], triangles)
}
pub fn find_t_junctions(
positions: &[[f32; 3]],
indices: &[u32],
eps: f32,
) -> Vec<(usize, u32, u32)> {
let n_verts = positions.len();
let n_faces = indices.len() / 3;
let mut result = Vec::new();
let mut edges: Vec<(u32, u32)> = Vec::new();
for f in 0..n_faces {
let v = [indices[f * 3], indices[f * 3 + 1], indices[f * 3 + 2]];
for e in 0..3 {
let a = v[e];
let b = v[(e + 1) % 3];
edges.push((a, b));
}
}
for vi in 0..n_verts {
let pv = positions[vi];
for &(ea, eb) in &edges {
if ea as usize == vi || eb as usize == vi {
continue;
}
let pa = positions[ea as usize];
let pb = positions[eb as usize];
let ab = sub3(pb, pa);
let av = sub3(pv, pa);
let len_sq = dot3(ab, ab);
if len_sq < 1e-10 {
continue;
}
let t = dot3(av, ab) / len_sq;
if t < eps || t > 1.0 - eps {
continue;
}
let proj = add3(pa, scale3(ab, t));
let diff = sub3(pv, proj);
let dist_sq = dot3(diff, diff);
if dist_sq < eps * eps {
result.push((vi, ea, eb));
}
}
}
result
}
pub fn remove_t_junction(
positions: &[[f32; 3]],
indices: &mut Vec<u32>,
vjunction: usize,
ea: u32,
eb: u32,
) {
let vj = vjunction as u32;
let n_faces = indices.len() / 3;
let mut target_face: Option<usize> = None;
for f in 0..n_faces {
let v = [indices[f * 3], indices[f * 3 + 1], indices[f * 3 + 2]];
for e in 0..3 {
let a = v[e];
let b = v[(e + 1) % 3];
if (a == ea && b == eb) || (a == eb && b == ea) {
target_face = Some(f);
break;
}
}
if target_face.is_some() {
break;
}
}
let Some(f) = target_face else { return };
let v0 = indices[f * 3];
let v1 = indices[f * 3 + 1];
let v2 = indices[f * 3 + 2];
indices.drain(f * 3..f * 3 + 3);
let opposite = [v0, v1, v2]
.iter()
.copied()
.find(|&v| v != ea && v != eb)
.unwrap_or(v0);
let _ = positions;
indices.extend_from_slice(&[ea, vj, opposite]);
indices.extend_from_slice(&[vj, eb, opposite]);
}
pub fn repair_mesh_advanced(
mesh: &MeshBuffers,
cfg: &AdvancedRepairConfig,
) -> (MeshBuffers, AdvancedRepairReport) {
let mut positions = mesh.positions.clone();
let mut indices = mesh.indices.clone();
let mut uvs = mesh.uvs.clone();
let mut holes_filled = 0usize;
let mut t_junctions_removed = 0usize;
let mut vertices_added = 0usize;
let mut faces_added = 0usize;
if cfg.remove_t_junctions {
let junctions = find_t_junctions(&positions, &indices, 1e-4);
for (vi, ea, eb) in &junctions {
remove_t_junction(&positions, &mut indices, *vi, *ea, *eb);
t_junctions_removed += 1;
}
}
if cfg.fill_holes {
let holes = find_boundary_holes(&indices);
for hole in &holes {
if hole.len() > cfg.max_hole_edges {
continue;
}
let (new_verts, new_tris) =
fill_hole_smooth(&positions, hole, cfg.tangent_smooth_iters);
vertices_added += new_verts.len();
faces_added += new_tris.len();
positions.extend_from_slice(&new_verts);
if !uvs.is_empty() {
for _ in &new_verts {
uvs.push([0.0, 0.0]);
}
}
for tri in new_tris {
indices.extend_from_slice(&[tri[0], tri[1], tri[2]]);
}
holes_filled += 1;
}
}
let n_verts = positions.len();
let manifold = is_manifold_mesh(&indices);
let mut result = MeshBuffers {
positions,
normals: vec![[0.0, 0.0, 1.0]; n_verts],
tangents: vec![[1.0, 0.0, 0.0, 1.0]; n_verts],
uvs: if uvs.is_empty() {
vec![[0.0, 0.0]; n_verts]
} else {
uvs
},
indices,
colors: None,
has_suit: mesh.has_suit,
};
compute_normals(&mut result);
let report = AdvancedRepairReport {
holes_filled,
t_junctions_removed,
vertices_added,
faces_added,
is_manifold: manifold,
};
(result, report)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::MeshBuffers;
fn closed_mesh() -> MeshBuffers {
let positions = vec![
[0.0_f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0], [0.5, 0.33, 1.0], ];
let n = positions.len();
let indices = vec![
0u32, 1, 2, 0, 3, 1, 1, 3, 2, 0, 2, 3, ];
MeshBuffers {
positions,
normals: vec![[0.0, 0.0, 1.0]; n],
tangents: vec![[1.0, 0.0, 0.0, 1.0]; n],
uvs: vec![[0.0, 0.0]; n],
indices,
colors: None,
has_suit: false,
}
}
fn open_mesh() -> MeshBuffers {
let positions = vec![
[0.0_f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0], [1.5, 1.0, 0.0], ];
let n = positions.len();
let indices = vec![0u32, 1, 2, 1, 3, 2];
MeshBuffers {
positions,
normals: vec![[0.0, 0.0, 1.0]; n],
tangents: vec![[1.0, 0.0, 0.0, 1.0]; n],
uvs: vec![[0.0, 0.0]; n],
indices,
colors: None,
has_suit: false,
}
}
fn non_manifold_mesh() -> MeshBuffers {
let positions = vec![
[0.0_f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0], [0.5, -1.0, 0.0], [1.5, 0.5, 0.0], ];
let n = positions.len();
let indices = vec![0u32, 1, 2, 0, 1, 3, 0, 1, 4];
MeshBuffers {
positions,
normals: vec![[0.0, 0.0, 1.0]; n],
tangents: vec![[1.0, 0.0, 0.0, 1.0]; n],
uvs: vec![[0.0, 0.0]; n],
indices,
colors: None,
has_suit: false,
}
}
#[test]
fn is_manifold_closed_mesh_returns_true() {
let mesh = closed_mesh();
assert!(is_manifold_mesh(&mesh.indices));
}
#[test]
fn is_manifold_open_mesh_returns_true_boundary_ok() {
let mesh = open_mesh();
assert!(is_manifold_mesh(&mesh.indices));
}
#[test]
fn is_manifold_non_manifold_edge_returns_false() {
let mesh = non_manifold_mesh();
assert!(!is_manifold_mesh(&mesh.indices));
}
#[test]
fn find_boundary_holes_closed_mesh_returns_empty() {
let mesh = closed_mesh();
let holes = find_boundary_holes(&mesh.indices);
assert!(holes.is_empty(), "closed mesh should have no holes");
}
#[test]
fn find_boundary_holes_open_mesh_finds_one_hole() {
let mesh = open_mesh();
let holes = find_boundary_holes(&mesh.indices);
assert_eq!(holes.len(), 1, "open mesh should have exactly 1 hole");
}
#[test]
fn find_boundary_holes_open_mesh_hole_has_correct_vertices() {
let mesh = open_mesh();
let holes = find_boundary_holes(&mesh.indices);
assert_eq!(holes.len(), 1);
let hole = &holes[0];
assert!(hole.len() >= 3, "hole loop should have at least 3 vertices");
}
#[test]
fn fill_hole_fan_produces_valid_indices() {
let positions = vec![[0.0_f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
let hole_loop = vec![0u32, 1, 2];
let tris = fill_hole_fan(&positions, &hole_loop);
assert!(!tris.is_empty());
for tri in &tris {
let [a, b, c] = tri;
assert!(
a != b && b != c && a != c,
"triangle must not be degenerate: {:?}",
tri
);
}
}
#[test]
fn fill_hole_fan_triangle_count() {
let positions: Vec<[f32; 3]> = (0..5)
.map(|i| {
let angle = i as f32 * std::f32::consts::TAU / 5.0;
[angle.cos(), angle.sin(), 0.0]
})
.collect();
let hole_loop: Vec<u32> = (0..5).collect();
let tris = fill_hole_fan(&positions, &hole_loop);
assert_eq!(tris.len(), 3, "pentagon fan = 3 triangles");
}
#[test]
fn fill_hole_smooth_adds_centroid_vertex() {
let positions = vec![[0.0_f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
let hole_loop = vec![0u32, 1, 2];
let (new_verts, new_tris) = fill_hole_smooth(&positions, &hole_loop, 2);
assert_eq!(
new_verts.len(),
1,
"exactly one centroid vertex should be added"
);
assert!(!new_tris.is_empty());
}
#[test]
fn fill_hole_smooth_triangles_form_fan() {
let positions: Vec<[f32; 3]> = (0..4)
.map(|i| {
let angle = i as f32 * std::f32::consts::TAU / 4.0;
[angle.cos(), angle.sin(), 0.0]
})
.collect();
let hole_loop: Vec<u32> = (0..4).collect();
let (_new_verts, new_tris) = fill_hole_smooth(&positions, &hole_loop, 1);
assert_eq!(new_tris.len(), 4);
}
#[test]
fn repair_mesh_advanced_open_mesh_fills_hole() {
let mesh = open_mesh();
let cfg = AdvancedRepairConfig {
fill_holes: true,
remove_t_junctions: false,
max_hole_edges: 32,
tangent_smooth_iters: 1,
};
let (result, report) = repair_mesh_advanced(&mesh, &cfg);
assert!(
report.holes_filled >= 1,
"at least one hole should be filled"
);
assert!(report.faces_added >= 1);
assert!(
result.indices.len() > mesh.indices.len(),
"more triangles after fill"
);
}
#[test]
fn repair_mesh_advanced_no_fill_keeps_same_faces() {
let mesh = open_mesh();
let cfg = AdvancedRepairConfig {
fill_holes: false,
remove_t_junctions: false,
max_hole_edges: 32,
tangent_smooth_iters: 0,
};
let (result, report) = repair_mesh_advanced(&mesh, &cfg);
assert_eq!(report.holes_filled, 0);
assert_eq!(result.indices.len(), mesh.indices.len());
}
#[test]
fn find_t_junctions_no_tjunctions_on_clean_mesh() {
let mesh = closed_mesh();
let junctions = find_t_junctions(&mesh.positions, &mesh.indices, 1e-4);
assert!(
junctions.is_empty(),
"closed tetrahedron has no T-junctions"
);
}
#[test]
fn find_t_junctions_detects_midpoint_junction() {
let positions = vec![
[0.0_f32, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, 1.0, 0.0], [1.0, 0.0, 0.0], ];
let indices = vec![0u32, 1, 2];
let junctions = find_t_junctions(&positions, &indices, 1e-4);
assert!(
junctions.iter().any(|(vi, ea, eb)| {
*vi == 3 && ((*ea == 0 && *eb == 1) || (*ea == 1 && *eb == 0))
}),
"vertex 3 should be detected as T-junction on edge 0-1: {:?}",
junctions
);
}
}