mesh_repair/
validate.rs

1//! Mesh validation and reporting.
2
3use nalgebra::Point3;
4use tracing::{debug, info, warn};
5
6use crate::Mesh;
7use crate::adjacency::MeshAdjacency;
8use crate::error::{MeshError, MeshResult, ValidationIssue};
9
10/// Validation report for a mesh.
11#[derive(Debug, Clone)]
12pub struct MeshReport {
13    /// Whether the mesh has no boundary edges.
14    pub is_watertight: bool,
15
16    /// Whether all edges have at most 2 adjacent faces.
17    pub is_manifold: bool,
18
19    /// Number of boundary edges (edges with 1 adjacent face).
20    pub boundary_edge_count: usize,
21
22    /// Number of non-manifold edges (edges with >2 adjacent faces).
23    pub non_manifold_edge_count: usize,
24
25    /// Total vertex count.
26    pub vertex_count: usize,
27
28    /// Total face count.
29    pub face_count: usize,
30
31    /// Bounding box as (min_corner, max_corner).
32    pub bounds: Option<(Point3<f64>, Point3<f64>)>,
33
34    /// Dimensions (x, y, z).
35    pub dimensions: Option<(f64, f64, f64)>,
36
37    /// Signed volume of the mesh (positive = outward normals, negative = inside-out).
38    /// Only meaningful for closed (watertight) meshes.
39    pub signed_volume: f64,
40
41    /// Absolute volume of the mesh.
42    pub volume: f64,
43
44    /// Total surface area of the mesh.
45    pub surface_area: f64,
46
47    /// Whether the mesh appears to be inside-out (negative signed volume).
48    pub is_inside_out: bool,
49
50    /// Number of connected components.
51    pub component_count: usize,
52}
53
54impl MeshReport {
55    /// Check if mesh passes basic validity checks.
56    pub fn is_valid(&self) -> bool {
57        self.vertex_count > 0 && self.face_count > 0
58    }
59
60    /// Check if mesh is suitable for 3D printing.
61    ///
62    /// A printable mesh must be:
63    /// - Watertight (no boundary edges)
64    /// - Manifold (no edge shared by more than 2 faces)
65    /// - Not inside-out (normals point outward)
66    pub fn is_printable(&self) -> bool {
67        self.is_watertight && self.is_manifold && !self.is_inside_out
68    }
69
70    /// Check if mesh has correct normal orientation (not inside-out).
71    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
136/// Validate a mesh and return a report.
137pub 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    // Compute volume
147    let signed_volume = mesh.signed_volume();
148    let volume = signed_volume.abs();
149    let is_inside_out = signed_volume < 0.0;
150
151    // Compute surface area
152    let surface_area = mesh.surface_area();
153
154    // Count connected components
155    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    // Log warnings
174    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
197/// Log a summary of mesh validation.
198pub 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/// Options for mesh data validation.
236#[derive(Debug, Clone)]
237pub struct ValidationOptions {
238    /// Whether to reject the mesh on finding invalid data (default: true).
239    /// If false, issues are collected but validation continues.
240    pub reject_on_invalid: bool,
241    /// Maximum number of issues to collect before stopping (default: 100).
242    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    /// Create options that collect all issues without rejecting.
256    pub fn collect_all() -> Self {
257        Self {
258            reject_on_invalid: false,
259            max_issues: 1000,
260        }
261    }
262}
263
264/// Result of mesh data validation.
265#[derive(Debug, Clone)]
266pub struct DataValidationResult {
267    /// List of issues found during validation.
268    pub issues: Vec<ValidationIssue>,
269    /// Number of invalid vertex indices found.
270    pub invalid_index_count: usize,
271    /// Number of NaN coordinates found.
272    pub nan_count: usize,
273    /// Number of infinite coordinates found.
274    pub infinity_count: usize,
275}
276
277impl DataValidationResult {
278    /// Check if validation passed with no issues.
279    pub fn is_valid(&self) -> bool {
280        self.issues.is_empty()
281    }
282
283    /// Get total number of issues found.
284    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
309/// Validate mesh data for invalid indices and coordinates.
310///
311/// This function checks:
312/// - Face indices are within vertex bounds
313/// - Vertex coordinates are not NaN
314/// - Vertex coordinates are not infinite
315///
316/// # Arguments
317/// * `mesh` - The mesh to validate
318/// * `options` - Validation options controlling behavior
319///
320/// # Returns
321/// - `Ok(DataValidationResult)` - Validation completed (check `is_valid()` for result)
322/// - `Err(MeshError)` - Validation failed (only when `reject_on_invalid` is true and issues found)
323///
324/// # Example
325/// ```
326/// use mesh_repair::{Mesh, validate::{validate_mesh_data, ValidationOptions}};
327///
328/// let mesh = Mesh::new();
329/// let result = validate_mesh_data(&mesh, &ValidationOptions::default());
330/// match result {
331///     Ok(validation) => {
332///         if validation.is_valid() {
333///             println!("Mesh data is valid");
334///         } else {
335///             println!("Found {} issues", validation.issue_count());
336///         }
337///     }
338///     Err(e) => println!("Validation failed: {}", e),
339/// }
340/// ```
341pub 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    // Check vertex coordinates for NaN and Infinity
353    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    // Check face indices are within bounds
399    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
444/// Validate mesh data with default options (rejects on first error).
445///
446/// This is a convenience wrapper around `validate_mesh_data` with default options.
447pub 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        // Tetrahedron with outward-facing normals (positive signed volume)
459        let mut mesh = Mesh::new();
460        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0)); // 0
461        mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0)); // 1
462        mesh.vertices.push(Vertex::from_coords(0.5, 0.866025, 0.0)); // 2 (approx sqrt(3)/2)
463        mesh.vertices
464            .push(Vertex::from_coords(0.5, 0.288675, 0.816497)); // 3 (apex)
465
466        // Faces with outward normals (CCW from outside)
467        mesh.faces.push([0, 2, 1]); // Bottom face
468        mesh.faces.push([0, 1, 3]); // Front face
469        mesh.faces.push([1, 2, 3]); // Right face
470        mesh.faces.push([2, 0, 3]); // Left face
471
472        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); // Has boundary edges
508        assert!(report.is_manifold); // No edge has >2 faces (manifold allows boundaries)
509        assert!(!report.is_printable()); // Not printable because not watertight
510        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        // Create a tetrahedron with inverted winding (inside-out)
518        // Same vertices as tetrahedron(), but with flipped faces
519        let mut mesh = Mesh::new();
520        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0)); // 0
521        mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0)); // 1
522        mesh.vertices.push(Vertex::from_coords(0.5, 0.866025, 0.0)); // 2
523        mesh.vertices
524            .push(Vertex::from_coords(0.5, 0.288675, 0.816497)); // 3
525
526        // Inverted winding (swap indices to reverse normal direction)
527        mesh.faces.push([0, 1, 2]); // Bottom face - inverted
528        mesh.faces.push([0, 3, 1]); // Front face - inverted
529        mesh.faces.push([1, 3, 2]); // Right face - inverted
530        mesh.faces.push([2, 3, 0]); // Left face - inverted
531
532        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()); // Inside-out meshes are not printable
538        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    // ==================== Data Validation Tests ====================
557
558    #[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        // Face references vertex 10, but mesh only has 3 vertices
577        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        // Multiple faces with invalid indices
603        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); // 2 in first vertex, 1 in second
652    }
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)); // NaN
699        mesh.vertices
700            .push(Vertex::from_coords(1.0, f64::INFINITY, 0.0)); // Infinity
701        mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0)); // Valid
702        mesh.faces.push([0, 1, 99]); // Invalid index
703
704        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        // Empty mesh has no issues (no vertices or faces to validate)
719        assert!(result.is_valid());
720    }
721
722    #[test]
723    fn test_validation_options_max_issues() {
724        let mut mesh = Mesh::new();
725        // Create a mesh with many invalid indices
726        for i in 0..10 {
727            mesh.vertices.push(Vertex::from_coords(i as f64, 0.0, 0.0));
728        }
729        // Add 20 faces with invalid indices
730        for _ in 0..20 {
731            mesh.faces.push([0, 1, 100]); // Invalid index
732        }
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        // Should stop at max_issues
741        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]); // Invalid indices
755
756        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}