1use nalgebra::Point3;
4use tracing::{debug, info, warn};
5
6use crate::Mesh;
7use crate::adjacency::MeshAdjacency;
8use crate::error::{MeshError, MeshResult, ValidationIssue};
9
10#[derive(Debug, Clone)]
12pub struct MeshReport {
13 pub is_watertight: bool,
15
16 pub is_manifold: bool,
18
19 pub boundary_edge_count: usize,
21
22 pub non_manifold_edge_count: usize,
24
25 pub vertex_count: usize,
27
28 pub face_count: usize,
30
31 pub bounds: Option<(Point3<f64>, Point3<f64>)>,
33
34 pub dimensions: Option<(f64, f64, f64)>,
36
37 pub signed_volume: f64,
40
41 pub volume: f64,
43
44 pub surface_area: f64,
46
47 pub is_inside_out: bool,
49
50 pub component_count: usize,
52}
53
54impl MeshReport {
55 pub fn is_valid(&self) -> bool {
57 self.vertex_count > 0 && self.face_count > 0
58 }
59
60 pub fn is_printable(&self) -> bool {
67 self.is_watertight && self.is_manifold && !self.is_inside_out
68 }
69
70 pub fn has_correct_orientation(&self) -> bool {
72 !self.is_inside_out
73 }
74}
75
76impl std::fmt::Display for MeshReport {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 writeln!(f, "Mesh Report:")?;
79 writeln!(f, " Vertices: {}", self.vertex_count)?;
80 writeln!(f, " Faces: {}", self.face_count)?;
81 writeln!(f, " Components: {}", self.component_count)?;
82
83 if let Some((min, max)) = &self.bounds {
84 writeln!(
85 f,
86 " Bounds: [{:.1}, {:.1}, {:.1}] to [{:.1}, {:.1}, {:.1}]",
87 min.x, min.y, min.z, max.x, max.y, max.z
88 )?;
89 }
90
91 if let Some((dx, dy, dz)) = &self.dimensions {
92 writeln!(f, " Dimensions: {:.1} x {:.1} x {:.1}", dx, dy, dz)?;
93 }
94
95 writeln!(f, " Surface Area: {:.2}", self.surface_area)?;
96 writeln!(
97 f,
98 " Volume: {:.2} (signed: {:.2})",
99 self.volume, self.signed_volume
100 )?;
101
102 writeln!(
103 f,
104 " Watertight: {} (boundary edges: {})",
105 if self.is_watertight { "yes" } else { "NO" },
106 self.boundary_edge_count
107 )?;
108
109 writeln!(
110 f,
111 " Manifold: {} (non-manifold edges: {})",
112 if self.is_manifold { "yes" } else { "NO" },
113 self.non_manifold_edge_count
114 )?;
115
116 writeln!(
117 f,
118 " Orientation: {}",
119 if self.is_inside_out {
120 "INSIDE-OUT"
121 } else {
122 "correct"
123 }
124 )?;
125
126 writeln!(
127 f,
128 " Printable: {}",
129 if self.is_printable() { "yes" } else { "NO" }
130 )?;
131
132 Ok(())
133 }
134}
135
136pub fn validate_mesh(mesh: &Mesh) -> MeshReport {
138 let adjacency = MeshAdjacency::build(&mesh.faces);
139
140 let boundary_edge_count = adjacency.boundary_edge_count();
141 let non_manifold_edge_count = adjacency.non_manifold_edge_count();
142
143 let bounds = mesh.bounds();
144 let dimensions = bounds.map(|(min, max)| (max.x - min.x, max.y - min.y, max.z - min.z));
145
146 let signed_volume = mesh.signed_volume();
148 let volume = signed_volume.abs();
149 let is_inside_out = signed_volume < 0.0;
150
151 let surface_area = mesh.surface_area();
153
154 let component_count = crate::components::find_connected_components(mesh).component_count;
156
157 let report = MeshReport {
158 is_watertight: boundary_edge_count == 0,
159 is_manifold: non_manifold_edge_count == 0,
160 boundary_edge_count,
161 non_manifold_edge_count,
162 vertex_count: mesh.vertex_count(),
163 face_count: mesh.face_count(),
164 bounds,
165 dimensions,
166 signed_volume,
167 volume,
168 surface_area,
169 is_inside_out,
170 component_count,
171 };
172
173 if !report.is_watertight {
175 warn!(
176 "Mesh is not watertight: {} boundary edges",
177 boundary_edge_count
178 );
179 }
180
181 if !report.is_manifold {
182 warn!(
183 "Mesh is not manifold: {} non-manifold edges",
184 non_manifold_edge_count
185 );
186 }
187
188 if report.is_inside_out && report.is_watertight {
189 warn!("Mesh appears to be inside-out (negative signed volume)");
190 }
191
192 debug!("{}", report);
193
194 report
195}
196
197pub fn log_validation(report: &MeshReport) {
199 info!(
200 "Mesh: {} verts, {} faces, {}x{}x{}",
201 report.vertex_count,
202 report.face_count,
203 report
204 .dimensions
205 .map(|d| format!("{:.1}", d.0))
206 .unwrap_or_default(),
207 report
208 .dimensions
209 .map(|d| format!("{:.1}", d.1))
210 .unwrap_or_default(),
211 report
212 .dimensions
213 .map(|d| format!("{:.1}", d.2))
214 .unwrap_or_default(),
215 );
216
217 if report.is_printable() {
218 info!("Mesh is watertight and manifold (printable)");
219 } else {
220 if !report.is_watertight {
221 warn!(
222 "Not watertight: {} boundary edges",
223 report.boundary_edge_count
224 );
225 }
226 if !report.is_manifold {
227 warn!(
228 "Not manifold: {} non-manifold edges",
229 report.non_manifold_edge_count
230 );
231 }
232 }
233}
234
235#[derive(Debug, Clone)]
237pub struct ValidationOptions {
238 pub reject_on_invalid: bool,
241 pub max_issues: usize,
243}
244
245impl Default for ValidationOptions {
246 fn default() -> Self {
247 Self {
248 reject_on_invalid: true,
249 max_issues: 100,
250 }
251 }
252}
253
254impl ValidationOptions {
255 pub fn collect_all() -> Self {
257 Self {
258 reject_on_invalid: false,
259 max_issues: 1000,
260 }
261 }
262}
263
264#[derive(Debug, Clone)]
266pub struct DataValidationResult {
267 pub issues: Vec<ValidationIssue>,
269 pub invalid_index_count: usize,
271 pub nan_count: usize,
273 pub infinity_count: usize,
275}
276
277impl DataValidationResult {
278 pub fn is_valid(&self) -> bool {
280 self.issues.is_empty()
281 }
282
283 pub fn issue_count(&self) -> usize {
285 self.issues.len()
286 }
287}
288
289impl std::fmt::Display for DataValidationResult {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 if self.is_valid() {
292 write!(f, "Data validation passed: no issues found")
293 } else {
294 writeln!(f, "Data validation found {} issue(s):", self.issue_count())?;
295 if self.invalid_index_count > 0 {
296 writeln!(f, " - {} invalid vertex indices", self.invalid_index_count)?;
297 }
298 if self.nan_count > 0 {
299 writeln!(f, " - {} NaN coordinates", self.nan_count)?;
300 }
301 if self.infinity_count > 0 {
302 writeln!(f, " - {} infinite coordinates", self.infinity_count)?;
303 }
304 Ok(())
305 }
306 }
307}
308
309pub fn validate_mesh_data(
342 mesh: &Mesh,
343 options: &ValidationOptions,
344) -> MeshResult<DataValidationResult> {
345 let mut issues = Vec::new();
346 let mut invalid_index_count = 0;
347 let mut nan_count = 0;
348 let mut infinity_count = 0;
349
350 let vertex_count = mesh.vertices.len();
351
352 for (vertex_idx, vertex) in mesh.vertices.iter().enumerate() {
354 if issues.len() >= options.max_issues {
355 break;
356 }
357
358 let coords = [
359 ("x", vertex.position.x),
360 ("y", vertex.position.y),
361 ("z", vertex.position.z),
362 ];
363
364 for (coord_name, value) in coords {
365 if value.is_nan() {
366 nan_count += 1;
367 issues.push(ValidationIssue::NaNCoordinate {
368 vertex_index: vertex_idx,
369 coordinate: coord_name,
370 });
371
372 if options.reject_on_invalid {
373 return Err(MeshError::InvalidCoordinate {
374 vertex_index: vertex_idx,
375 coordinate: coord_name,
376 value,
377 });
378 }
379 } else if value.is_infinite() {
380 infinity_count += 1;
381 issues.push(ValidationIssue::InfiniteCoordinate {
382 vertex_index: vertex_idx,
383 coordinate: coord_name,
384 value,
385 });
386
387 if options.reject_on_invalid {
388 return Err(MeshError::InvalidCoordinate {
389 vertex_index: vertex_idx,
390 coordinate: coord_name,
391 value,
392 });
393 }
394 }
395 }
396 }
397
398 for (face_idx, face) in mesh.faces.iter().enumerate() {
400 if issues.len() >= options.max_issues {
401 break;
402 }
403
404 for &vertex_idx in face {
405 if vertex_idx as usize >= vertex_count {
406 invalid_index_count += 1;
407 issues.push(ValidationIssue::InvalidVertexIndex {
408 face_index: face_idx,
409 vertex_index: vertex_idx,
410 vertex_count,
411 });
412
413 if options.reject_on_invalid {
414 return Err(MeshError::InvalidVertexIndex {
415 face_index: face_idx,
416 vertex_index: vertex_idx,
417 vertex_count,
418 });
419 }
420 }
421 }
422 }
423
424 if !issues.is_empty() {
425 warn!(
426 "Mesh data validation found {} issue(s): {} invalid indices, {} NaN, {} Inf",
427 issues.len(),
428 invalid_index_count,
429 nan_count,
430 infinity_count
431 );
432 } else {
433 debug!("Mesh data validation passed");
434 }
435
436 Ok(DataValidationResult {
437 issues,
438 invalid_index_count,
439 nan_count,
440 infinity_count,
441 })
442}
443
444pub fn validate_mesh_data_strict(mesh: &Mesh) -> MeshResult<()> {
448 validate_mesh_data(mesh, &ValidationOptions::default())?;
449 Ok(())
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use crate::Vertex;
456
457 fn tetrahedron() -> Mesh {
458 let mut mesh = Mesh::new();
460 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::from_coords(0.5, 0.866025, 0.0)); mesh.vertices
464 .push(Vertex::from_coords(0.5, 0.288675, 0.816497)); 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
473 }
474
475 fn single_triangle() -> Mesh {
476 let mut mesh = Mesh::new();
477 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
478 mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
479 mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
480 mesh.faces.push([0, 1, 2]);
481 mesh
482 }
483
484 #[test]
485 fn test_validate_watertight_mesh() {
486 let mesh = tetrahedron();
487 let report = validate_mesh(&mesh);
488
489 assert!(report.is_valid());
490 assert!(report.is_watertight);
491 assert!(report.is_manifold);
492 assert!(report.is_printable());
493 assert_eq!(report.boundary_edge_count, 0);
494 assert_eq!(report.non_manifold_edge_count, 0);
495 assert_eq!(report.component_count, 1);
496 assert!(report.volume > 0.0);
497 assert!(report.surface_area > 0.0);
498 assert!(!report.is_inside_out);
499 }
500
501 #[test]
502 fn test_validate_open_mesh() {
503 let mesh = single_triangle();
504 let report = validate_mesh(&mesh);
505
506 assert!(report.is_valid());
507 assert!(!report.is_watertight); assert!(report.is_manifold); assert!(!report.is_printable()); assert_eq!(report.boundary_edge_count, 3);
511 assert_eq!(report.component_count, 1);
512 assert!(report.surface_area > 0.0);
513 }
514
515 #[test]
516 fn test_validate_inside_out_mesh() {
517 let mut mesh = Mesh::new();
520 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::from_coords(0.5, 0.866025, 0.0)); mesh.vertices
524 .push(Vertex::from_coords(0.5, 0.288675, 0.816497)); mesh.faces.push([0, 1, 2]); mesh.faces.push([0, 3, 1]); mesh.faces.push([1, 3, 2]); mesh.faces.push([2, 3, 0]); let report = validate_mesh(&mesh);
533
534 assert!(report.is_watertight);
535 assert!(report.is_manifold);
536 assert!(report.is_inside_out);
537 assert!(!report.is_printable()); assert!(report.signed_volume < 0.0);
539 }
540
541 #[test]
542 fn test_report_display() {
543 let mesh = tetrahedron();
544 let report = validate_mesh(&mesh);
545 let output = format!("{}", report);
546
547 assert!(output.contains("Vertices: 4"));
548 assert!(output.contains("Faces: 4"));
549 assert!(output.contains("Components: 1"));
550 assert!(output.contains("Watertight: yes"));
551 assert!(output.contains("Surface Area:"));
552 assert!(output.contains("Volume:"));
553 assert!(output.contains("Orientation: correct"));
554 }
555
556 #[test]
559 fn test_validate_valid_mesh_data() {
560 let mesh = tetrahedron();
561 let result = validate_mesh_data(&mesh, &ValidationOptions::default()).unwrap();
562
563 assert!(result.is_valid());
564 assert_eq!(result.issue_count(), 0);
565 assert_eq!(result.invalid_index_count, 0);
566 assert_eq!(result.nan_count, 0);
567 assert_eq!(result.infinity_count, 0);
568 }
569
570 #[test]
571 fn test_validate_invalid_vertex_index_strict() {
572 let mut mesh = Mesh::new();
573 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
574 mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
575 mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
576 mesh.faces.push([0, 1, 10]);
578
579 let result = validate_mesh_data(&mesh, &ValidationOptions::default());
580 assert!(result.is_err());
581
582 match result.unwrap_err() {
583 MeshError::InvalidVertexIndex {
584 face_index,
585 vertex_index,
586 vertex_count,
587 } => {
588 assert_eq!(face_index, 0);
589 assert_eq!(vertex_index, 10);
590 assert_eq!(vertex_count, 3);
591 }
592 e => panic!("Expected InvalidVertexIndex error, got: {:?}", e),
593 }
594 }
595
596 #[test]
597 fn test_validate_invalid_vertex_index_collect() {
598 let mut mesh = Mesh::new();
599 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
600 mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
601 mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
602 mesh.faces.push([0, 1, 10]);
604 mesh.faces.push([0, 20, 2]);
605 mesh.faces.push([30, 1, 2]);
606
607 let result = validate_mesh_data(&mesh, &ValidationOptions::collect_all()).unwrap();
608
609 assert!(!result.is_valid());
610 assert_eq!(result.invalid_index_count, 3);
611 assert_eq!(result.issue_count(), 3);
612 }
613
614 #[test]
615 fn test_validate_nan_coordinate_strict() {
616 let mut mesh = Mesh::new();
617 mesh.vertices.push(Vertex::from_coords(f64::NAN, 0.0, 0.0));
618 mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
619 mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
620 mesh.faces.push([0, 1, 2]);
621
622 let result = validate_mesh_data(&mesh, &ValidationOptions::default());
623 assert!(result.is_err());
624
625 match result.unwrap_err() {
626 MeshError::InvalidCoordinate {
627 vertex_index,
628 coordinate,
629 value,
630 } => {
631 assert_eq!(vertex_index, 0);
632 assert_eq!(coordinate, "x");
633 assert!(value.is_nan());
634 }
635 e => panic!("Expected InvalidCoordinate error, got: {:?}", e),
636 }
637 }
638
639 #[test]
640 fn test_validate_nan_coordinate_collect() {
641 let mut mesh = Mesh::new();
642 mesh.vertices
643 .push(Vertex::from_coords(f64::NAN, f64::NAN, 0.0));
644 mesh.vertices.push(Vertex::from_coords(1.0, f64::NAN, 0.0));
645 mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
646 mesh.faces.push([0, 1, 2]);
647
648 let result = validate_mesh_data(&mesh, &ValidationOptions::collect_all()).unwrap();
649
650 assert!(!result.is_valid());
651 assert_eq!(result.nan_count, 3); }
653
654 #[test]
655 fn test_validate_infinity_coordinate_strict() {
656 let mut mesh = Mesh::new();
657 mesh.vertices
658 .push(Vertex::from_coords(f64::INFINITY, 0.0, 0.0));
659 mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
660 mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
661 mesh.faces.push([0, 1, 2]);
662
663 let result = validate_mesh_data(&mesh, &ValidationOptions::default());
664 assert!(result.is_err());
665
666 match result.unwrap_err() {
667 MeshError::InvalidCoordinate {
668 vertex_index,
669 coordinate,
670 value,
671 } => {
672 assert_eq!(vertex_index, 0);
673 assert_eq!(coordinate, "x");
674 assert!(value.is_infinite());
675 }
676 e => panic!("Expected InvalidCoordinate error, got: {:?}", e),
677 }
678 }
679
680 #[test]
681 fn test_validate_negative_infinity_coordinate() {
682 let mut mesh = Mesh::new();
683 mesh.vertices
684 .push(Vertex::from_coords(0.0, f64::NEG_INFINITY, 0.0));
685 mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
686 mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
687 mesh.faces.push([0, 1, 2]);
688
689 let result = validate_mesh_data(&mesh, &ValidationOptions::collect_all()).unwrap();
690
691 assert!(!result.is_valid());
692 assert_eq!(result.infinity_count, 1);
693 }
694
695 #[test]
696 fn test_validate_multiple_issues_types() {
697 let mut mesh = Mesh::new();
698 mesh.vertices.push(Vertex::from_coords(f64::NAN, 0.0, 0.0)); mesh.vertices
700 .push(Vertex::from_coords(1.0, f64::INFINITY, 0.0)); mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0)); mesh.faces.push([0, 1, 99]); let result = validate_mesh_data(&mesh, &ValidationOptions::collect_all()).unwrap();
705
706 assert!(!result.is_valid());
707 assert_eq!(result.nan_count, 1);
708 assert_eq!(result.infinity_count, 1);
709 assert_eq!(result.invalid_index_count, 1);
710 assert_eq!(result.issue_count(), 3);
711 }
712
713 #[test]
714 fn test_validate_empty_mesh() {
715 let mesh = Mesh::new();
716 let result = validate_mesh_data(&mesh, &ValidationOptions::default()).unwrap();
717
718 assert!(result.is_valid());
720 }
721
722 #[test]
723 fn test_validation_options_max_issues() {
724 let mut mesh = Mesh::new();
725 for i in 0..10 {
727 mesh.vertices.push(Vertex::from_coords(i as f64, 0.0, 0.0));
728 }
729 for _ in 0..20 {
731 mesh.faces.push([0, 1, 100]); }
733
734 let options = ValidationOptions {
735 reject_on_invalid: false,
736 max_issues: 5,
737 };
738 let result = validate_mesh_data(&mesh, &options).unwrap();
739
740 assert_eq!(result.issue_count(), 5);
742 }
743
744 #[test]
745 fn test_validate_mesh_data_strict_passes() {
746 let mesh = tetrahedron();
747 assert!(validate_mesh_data_strict(&mesh).is_ok());
748 }
749
750 #[test]
751 fn test_validate_mesh_data_strict_fails() {
752 let mut mesh = Mesh::new();
753 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
754 mesh.faces.push([0, 1, 2]); assert!(validate_mesh_data_strict(&mesh).is_err());
757 }
758
759 #[test]
760 fn test_data_validation_result_display() {
761 let mut mesh = Mesh::new();
762 mesh.vertices.push(Vertex::from_coords(f64::NAN, 0.0, 0.0));
763 mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
764 mesh.faces.push([0, 1, 99]);
765
766 let result = validate_mesh_data(&mesh, &ValidationOptions::collect_all()).unwrap();
767 let output = format!("{}", result);
768
769 assert!(output.contains("issue"));
770 assert!(output.contains("invalid vertex indices"));
771 assert!(output.contains("NaN"));
772 }
773
774 #[test]
775 fn test_validation_issue_display() {
776 let issue = ValidationIssue::InvalidVertexIndex {
777 face_index: 5,
778 vertex_index: 100,
779 vertex_count: 50,
780 };
781 let output = format!("{}", issue);
782 assert!(output.contains("face 5"));
783 assert!(output.contains("vertex 100"));
784 assert!(output.contains("50 vertices"));
785
786 let issue = ValidationIssue::NaNCoordinate {
787 vertex_index: 3,
788 coordinate: "y",
789 };
790 let output = format!("{}", issue);
791 assert!(output.contains("vertex 3"));
792 assert!(output.contains("NaN"));
793 assert!(output.contains("y"));
794
795 let issue = ValidationIssue::InfiniteCoordinate {
796 vertex_index: 7,
797 coordinate: "z",
798 value: f64::INFINITY,
799 };
800 let output = format!("{}", issue);
801 assert!(output.contains("vertex 7"));
802 assert!(output.contains("infinite"));
803 assert!(output.contains("z"));
804 }
805}