1use tracing::{debug, info, warn};
6
7use mesh_repair::{
8 Mesh, MeshAdjacency, fix_winding_order, remove_degenerate_triangles_enhanced, validate_mesh,
9};
10
11#[derive(Debug, Clone)]
13pub struct ShellValidationResult {
14 pub is_watertight: bool,
16 pub is_manifold: bool,
18 pub has_consistent_winding: bool,
20 pub boundary_edge_count: usize,
22 pub non_manifold_edge_count: usize,
24 pub vertex_count: usize,
26 pub face_count: usize,
28 pub issues: Vec<ShellIssue>,
30}
31
32impl ShellValidationResult {
33 pub fn is_valid(&self) -> bool {
35 self.is_watertight && self.is_manifold && self.has_consistent_winding
36 }
37
38 pub fn is_printable(&self) -> bool {
40 self.is_watertight && self.is_manifold
41 }
42
43 pub fn issue_count(&self) -> usize {
45 self.issues.len()
46 }
47}
48
49impl std::fmt::Display for ShellValidationResult {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 writeln!(f, "Shell Validation Result:")?;
52 writeln!(f, " Vertices: {}", self.vertex_count)?;
53 writeln!(f, " Faces: {}", self.face_count)?;
54 writeln!(
55 f,
56 " Watertight: {} (boundary edges: {})",
57 if self.is_watertight { "yes" } else { "NO" },
58 self.boundary_edge_count
59 )?;
60 writeln!(
61 f,
62 " Manifold: {} (non-manifold edges: {})",
63 if self.is_manifold { "yes" } else { "NO" },
64 self.non_manifold_edge_count
65 )?;
66 writeln!(
67 f,
68 " Consistent winding: {}",
69 if self.has_consistent_winding {
70 "yes"
71 } else {
72 "NO"
73 }
74 )?;
75 writeln!(
76 f,
77 " Printable: {}",
78 if self.is_printable() { "yes" } else { "NO" }
79 )?;
80
81 if !self.issues.is_empty() {
82 writeln!(f, " Issues ({}):", self.issues.len())?;
83 for issue in &self.issues {
84 writeln!(f, " - {}", issue)?;
85 }
86 }
87
88 Ok(())
89 }
90}
91
92#[derive(Debug, Clone)]
94pub enum ShellIssue {
95 NotWatertight { boundary_edge_count: usize },
97 NonManifold { non_manifold_edge_count: usize },
99 InconsistentWinding,
101 EmptyShell,
103 DegenerateTriangles { count: usize },
105}
106
107impl std::fmt::Display for ShellIssue {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 ShellIssue::NotWatertight {
111 boundary_edge_count,
112 } => {
113 write!(
114 f,
115 "Shell is not watertight ({} boundary edges)",
116 boundary_edge_count
117 )
118 }
119 ShellIssue::NonManifold {
120 non_manifold_edge_count,
121 } => {
122 write!(
123 f,
124 "Shell is not manifold ({} non-manifold edges)",
125 non_manifold_edge_count
126 )
127 }
128 ShellIssue::InconsistentWinding => {
129 write!(f, "Shell has inconsistent face winding order")
130 }
131 ShellIssue::EmptyShell => {
132 write!(f, "Shell is empty (no faces)")
133 }
134 ShellIssue::DegenerateTriangles { count } => {
135 write!(f, "Shell has {} degenerate triangles", count)
136 }
137 }
138 }
139}
140
141pub fn validate_shell(shell: &Mesh) -> ShellValidationResult {
168 info!(
169 "Validating shell mesh ({} vertices, {} faces)",
170 shell.vertex_count(),
171 shell.face_count()
172 );
173
174 let mut issues = Vec::new();
175
176 if shell.faces.is_empty() {
178 issues.push(ShellIssue::EmptyShell);
179 return ShellValidationResult {
180 is_watertight: false,
181 is_manifold: false,
182 has_consistent_winding: false,
183 boundary_edge_count: 0,
184 non_manifold_edge_count: 0,
185 vertex_count: shell.vertex_count(),
186 face_count: 0,
187 issues,
188 };
189 }
190
191 let mesh_report = validate_mesh(shell);
193
194 let boundary_edge_count = mesh_report.boundary_edge_count;
195 let non_manifold_edge_count = mesh_report.non_manifold_edge_count;
196
197 let is_watertight = boundary_edge_count == 0;
199 if !is_watertight {
200 issues.push(ShellIssue::NotWatertight {
201 boundary_edge_count,
202 });
203 warn!(
204 "Shell is not watertight: {} boundary edges",
205 boundary_edge_count
206 );
207 }
208
209 let is_manifold = non_manifold_edge_count == 0;
211 if !is_manifold {
212 issues.push(ShellIssue::NonManifold {
213 non_manifold_edge_count,
214 });
215 warn!(
216 "Shell is not manifold: {} non-manifold edges",
217 non_manifold_edge_count
218 );
219 }
220
221 let has_consistent_winding = check_winding_consistency(shell);
223 if !has_consistent_winding {
224 issues.push(ShellIssue::InconsistentWinding);
225 warn!("Shell has inconsistent winding order");
226 }
227
228 let degenerate_count = count_degenerate_triangles(shell);
230 if degenerate_count > 0 {
231 issues.push(ShellIssue::DegenerateTriangles {
232 count: degenerate_count,
233 });
234 warn!("Shell has {} degenerate triangles", degenerate_count);
235 }
236
237 let result = ShellValidationResult {
238 is_watertight,
239 is_manifold,
240 has_consistent_winding,
241 boundary_edge_count,
242 non_manifold_edge_count,
243 vertex_count: shell.vertex_count(),
244 face_count: shell.face_count(),
245 issues,
246 };
247
248 if result.is_printable() {
249 info!("Shell validation passed - mesh is printable");
250 } else {
251 warn!("Shell validation found {} issue(s)", result.issue_count());
252 }
253
254 debug!("{}", result);
255
256 result
257}
258
259fn check_winding_consistency(mesh: &Mesh) -> bool {
264 let adjacency = MeshAdjacency::build(&mesh.faces);
265
266 for (&edge, face_indices) in &adjacency.edge_to_faces {
268 if face_indices.len() != 2 {
269 continue;
271 }
272
273 let face_a = mesh.faces[face_indices[0] as usize];
274 let face_b = mesh.faces[face_indices[1] as usize];
275
276 let edge_in_a = find_edge_direction(&face_a, edge);
278 let edge_in_b = find_edge_direction(&face_b, edge);
279
280 if edge_in_a == edge_in_b {
283 return false;
284 }
285 }
286
287 true
288}
289
290fn find_edge_direction(face: &[u32; 3], edge: (u32, u32)) -> bool {
293 let (v0, v1) = edge;
294
295 for i in 0..3 {
297 let a = face[i];
298 let b = face[(i + 1) % 3];
299
300 if a == v0 && b == v1 {
301 return true; }
303 if a == v1 && b == v0 {
304 return false; }
306 }
307
308 true
310}
311
312fn count_degenerate_triangles(mesh: &Mesh) -> usize {
314 const DEGENERATE_THRESHOLD: f64 = 1e-10;
315
316 mesh.faces
317 .iter()
318 .filter(|face| {
319 let v0 = &mesh.vertices[face[0] as usize].position;
320 let v1 = &mesh.vertices[face[1] as usize].position;
321 let v2 = &mesh.vertices[face[2] as usize].position;
322
323 let edge1 = v1 - v0;
324 let edge2 = v2 - v0;
325 let cross = edge1.cross(&edge2);
326 let area = cross.norm() / 2.0;
327
328 area < DEGENERATE_THRESHOLD
329 })
330 .count()
331}
332
333#[derive(Debug, Clone)]
335pub struct ShellRepairResult {
336 pub degenerate_triangles_removed: usize,
338 pub faces_with_winding_fixed: bool,
340 pub success: bool,
342 pub errors: Vec<String>,
344}
345
346impl std::fmt::Display for ShellRepairResult {
347 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348 if self.success {
349 writeln!(f, "Shell repair completed successfully:")?;
350 if self.degenerate_triangles_removed > 0 {
351 writeln!(
352 f,
353 " - Removed {} degenerate triangles",
354 self.degenerate_triangles_removed
355 )?;
356 }
357 if self.faces_with_winding_fixed {
358 writeln!(f, " - Fixed winding order")?;
359 }
360 if self.degenerate_triangles_removed == 0 && !self.faces_with_winding_fixed {
361 writeln!(f, " - No repairs needed")?;
362 }
363 } else {
364 writeln!(f, "Shell repair failed:")?;
365 for error in &self.errors {
366 writeln!(f, " - {}", error)?;
367 }
368 }
369 Ok(())
370 }
371}
372
373pub fn repair_shell(shell: &mut Mesh) -> ShellRepairResult {
404 info!(
405 "Attempting to repair shell mesh ({} vertices, {} faces)",
406 shell.vertex_count(),
407 shell.face_count()
408 );
409
410 let mut result = ShellRepairResult {
411 degenerate_triangles_removed: 0,
412 faces_with_winding_fixed: false,
413 success: true,
414 errors: Vec::new(),
415 };
416
417 if shell.faces.is_empty() {
418 result.success = false;
419 result.errors.push("Cannot repair empty shell".to_string());
420 return result;
421 }
422
423 let removed = remove_degenerate_triangles_enhanced(
425 shell, 1e-10, 1000.0, 1e-9, );
429 result.degenerate_triangles_removed = removed;
430 if removed > 0 {
431 info!("Removed {} degenerate triangles", removed);
432 }
433
434 if shell.faces.is_empty() {
436 result.success = false;
437 result
438 .errors
439 .push("All faces were degenerate - shell is now empty".to_string());
440 return result;
441 }
442
443 let pre_winding_check = check_winding_consistency(shell);
445 if !pre_winding_check {
446 match fix_winding_order(shell) {
447 Ok(()) => {
448 result.faces_with_winding_fixed = true;
449 info!("Fixed winding order");
450 }
451 Err(e) => {
452 result
453 .errors
454 .push(format!("Failed to fix winding order: {}", e));
455 warn!("Failed to fix winding order: {}", e);
456 }
457 }
458 }
459
460 if result.success {
462 let total_repairs = result.degenerate_triangles_removed
463 + (if result.faces_with_winding_fixed {
464 1
465 } else {
466 0
467 });
468 if total_repairs > 0 {
469 info!("Shell repair completed: {} repairs applied", total_repairs);
470 } else {
471 debug!("Shell repair: no repairs needed");
472 }
473 }
474
475 result
476}
477
478pub fn validate_and_repair_shell(
490 shell: &mut Mesh,
491 auto_repair: bool,
492) -> (ShellValidationResult, Option<ShellRepairResult>) {
493 let initial_validation = validate_shell(shell);
495
496 if initial_validation.is_printable() || !auto_repair {
497 return (initial_validation, None);
498 }
499
500 let can_repair = initial_validation.issues.iter().any(|issue| {
502 matches!(
503 issue,
504 ShellIssue::InconsistentWinding | ShellIssue::DegenerateTriangles { .. }
505 )
506 });
507
508 if !can_repair {
509 debug!("Shell has issues that cannot be auto-repaired");
510 return (initial_validation, None);
511 }
512
513 let repair_result = repair_shell(shell);
515
516 let final_validation = validate_shell(shell);
518
519 (final_validation, Some(repair_result))
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525 use mesh_repair::Vertex;
526
527 fn create_watertight_tetrahedron() -> Mesh {
528 let mut mesh = Mesh::new();
529 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
530 mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
531 mesh.vertices.push(Vertex::from_coords(5.0, 10.0, 0.0));
532 mesh.vertices.push(Vertex::from_coords(5.0, 5.0, 10.0));
533
534 mesh.faces.push([0, 2, 1]); mesh.faces.push([0, 1, 3]); mesh.faces.push([1, 2, 3]); mesh.faces.push([2, 0, 3]); mesh
541 }
542
543 fn create_open_box() -> Mesh {
544 let mut mesh = Mesh::new();
545
546 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
548 mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
549 mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 0.0));
550 mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 0.0));
551 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 10.0));
553 mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 10.0));
554 mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 10.0));
555 mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 10.0));
556
557 mesh.faces.push([0, 2, 1]);
559 mesh.faces.push([0, 3, 2]);
560 mesh.faces.push([0, 1, 5]);
562 mesh.faces.push([0, 5, 4]);
563 mesh.faces.push([2, 3, 7]);
565 mesh.faces.push([2, 7, 6]);
566 mesh.faces.push([0, 4, 7]);
568 mesh.faces.push([0, 7, 3]);
569 mesh.faces.push([1, 2, 6]);
571 mesh.faces.push([1, 6, 5]);
572 mesh
575 }
576
577 #[test]
578 fn test_validate_watertight_shell() {
579 let shell = create_watertight_tetrahedron();
580 let result = validate_shell(&shell);
581
582 assert!(result.is_watertight);
583 assert!(result.is_manifold);
584 assert!(result.is_printable());
585 assert_eq!(result.boundary_edge_count, 0);
586 assert_eq!(result.non_manifold_edge_count, 0);
587 }
588
589 #[test]
590 fn test_validate_open_shell() {
591 let shell = create_open_box();
592 let result = validate_shell(&shell);
593
594 assert!(!result.is_watertight);
595 assert!(result.is_manifold);
596 assert!(!result.is_printable());
597 assert!(result.boundary_edge_count > 0);
598 assert!(
599 result
600 .issues
601 .iter()
602 .any(|i| matches!(i, ShellIssue::NotWatertight { .. }))
603 );
604 }
605
606 #[test]
607 fn test_validate_empty_shell() {
608 let shell = Mesh::new();
609 let result = validate_shell(&shell);
610
611 assert!(!result.is_valid());
612 assert!(!result.is_printable());
613 assert!(
614 result
615 .issues
616 .iter()
617 .any(|i| matches!(i, ShellIssue::EmptyShell))
618 );
619 }
620
621 #[test]
622 fn test_validate_shell_with_degenerate_triangles() {
623 let mut mesh = create_watertight_tetrahedron();
624
625 let idx = mesh.vertices.len() as u32;
627 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
628 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
629 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
630 mesh.faces.push([idx, idx + 1, idx + 2]);
631
632 let result = validate_shell(&mesh);
633
634 assert!(
635 result
636 .issues
637 .iter()
638 .any(|i| matches!(i, ShellIssue::DegenerateTriangles { .. }))
639 );
640 }
641
642 #[test]
643 fn test_shell_validation_result_display() {
644 let shell = create_watertight_tetrahedron();
645 let result = validate_shell(&shell);
646 let output = format!("{}", result);
647
648 assert!(output.contains("Vertices:"));
649 assert!(output.contains("Faces:"));
650 assert!(output.contains("Watertight: yes"));
651 assert!(output.contains("Manifold: yes"));
652 assert!(output.contains("Printable: yes"));
653 }
654
655 #[test]
656 fn test_shell_issue_display() {
657 let issue = ShellIssue::NotWatertight {
658 boundary_edge_count: 4,
659 };
660 let output = format!("{}", issue);
661 assert!(output.contains("watertight"));
662 assert!(output.contains("4"));
663
664 let issue = ShellIssue::NonManifold {
665 non_manifold_edge_count: 2,
666 };
667 let output = format!("{}", issue);
668 assert!(output.contains("manifold"));
669 assert!(output.contains("2"));
670
671 let issue = ShellIssue::InconsistentWinding;
672 let output = format!("{}", issue);
673 assert!(output.contains("winding"));
674
675 let issue = ShellIssue::EmptyShell;
676 let output = format!("{}", issue);
677 assert!(output.contains("empty"));
678
679 let issue = ShellIssue::DegenerateTriangles { count: 5 };
680 let output = format!("{}", issue);
681 assert!(output.contains("degenerate"));
682 assert!(output.contains("5"));
683 }
684
685 #[test]
686 fn test_repair_shell_removes_degenerate_triangles() {
687 let mut mesh = create_watertight_tetrahedron();
688 let original_face_count = mesh.faces.len();
689
690 let idx = mesh.vertices.len() as u32;
692 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
693 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
694 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
695 mesh.faces.push([idx, idx + 1, idx + 2]);
696
697 let idx = mesh.vertices.len() as u32;
699 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
700 mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
701 mesh.vertices.push(Vertex::from_coords(5.0, 0.0000001, 0.0)); mesh.faces.push([idx, idx + 1, idx + 2]);
703
704 let repair_result = repair_shell(&mut mesh);
705
706 assert!(repair_result.success);
707 assert!(repair_result.degenerate_triangles_removed > 0);
708 assert!(mesh.faces.len() <= original_face_count + 2);
710 }
711
712 #[test]
713 fn test_repair_shell_result_display() {
714 let result = ShellRepairResult {
715 degenerate_triangles_removed: 3,
716 faces_with_winding_fixed: true,
717 success: true,
718 errors: vec![],
719 };
720
721 let output = format!("{}", result);
722 assert!(output.contains("Removed 3 degenerate triangles"));
723 assert!(output.contains("Fixed winding order"));
724 assert!(output.contains("successfully"));
725 }
726
727 #[test]
728 fn test_repair_shell_result_with_errors() {
729 let result = ShellRepairResult {
730 degenerate_triangles_removed: 0,
731 faces_with_winding_fixed: false,
732 success: false,
733 errors: vec!["Test error".to_string()],
734 };
735
736 let output = format!("{}", result);
737 assert!(output.contains("failed"));
738 assert!(output.contains("Test error"));
739 }
740
741 #[test]
742 fn test_validate_and_repair_shell_no_repair() {
743 let mut mesh = create_watertight_tetrahedron();
744
745 let (validation, repair) = validate_and_repair_shell(&mut mesh, false);
746
747 assert!(validation.is_printable());
748 assert!(repair.is_none());
749 }
750
751 #[test]
752 fn test_validate_and_repair_shell_with_repair() {
753 let mut mesh = create_watertight_tetrahedron();
754
755 let idx = mesh.vertices.len() as u32;
757 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
758 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
759 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
760 mesh.faces.push([idx, idx + 1, idx + 2]);
761
762 let (validation, repair) = validate_and_repair_shell(&mut mesh, true);
763
764 assert!(repair.is_some());
766 let repair_result = repair.unwrap();
767 assert!(repair_result.degenerate_triangles_removed > 0);
768
769 let has_degenerate_issue = validation
771 .issues
772 .iter()
773 .any(|i| matches!(i, ShellIssue::DegenerateTriangles { .. }));
774 assert!(
775 !has_degenerate_issue,
776 "Degenerate triangles should have been removed"
777 );
778 }
779
780 #[test]
781 fn test_repair_clean_shell() {
782 let mut mesh = create_watertight_tetrahedron();
783
784 let repair_result = repair_shell(&mut mesh);
785
786 assert!(repair_result.success);
787 assert_eq!(repair_result.degenerate_triangles_removed, 0);
788 assert!(!repair_result.faces_with_winding_fixed);
789 assert!(repair_result.errors.is_empty());
790 }
791}