mesh_shell/shell/
validation.rs

1//! Shell validation utilities.
2//!
3//! Validates shell meshes to ensure they are suitable for 3D printing.
4
5use tracing::{debug, info, warn};
6
7use mesh_repair::{
8    Mesh, MeshAdjacency, fix_winding_order, remove_degenerate_triangles_enhanced, validate_mesh,
9};
10
11/// Result of shell validation.
12#[derive(Debug, Clone)]
13pub struct ShellValidationResult {
14    /// Whether the shell is watertight (no boundary edges).
15    pub is_watertight: bool,
16    /// Whether the shell is manifold (no edges with >2 faces).
17    pub is_manifold: bool,
18    /// Whether the shell has consistent winding order.
19    pub has_consistent_winding: bool,
20    /// Number of boundary edges (should be 0 for printable shell).
21    pub boundary_edge_count: usize,
22    /// Number of non-manifold edges (should be 0 for printable shell).
23    pub non_manifold_edge_count: usize,
24    /// Total vertex count.
25    pub vertex_count: usize,
26    /// Total face count.
27    pub face_count: usize,
28    /// List of validation issues found.
29    pub issues: Vec<ShellIssue>,
30}
31
32impl ShellValidationResult {
33    /// Check if the shell passes all validation checks.
34    pub fn is_valid(&self) -> bool {
35        self.is_watertight && self.is_manifold && self.has_consistent_winding
36    }
37
38    /// Check if the shell is suitable for 3D printing.
39    pub fn is_printable(&self) -> bool {
40        self.is_watertight && self.is_manifold
41    }
42
43    /// Get the total number of issues found.
44    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/// Issues that can be found during shell validation.
93#[derive(Debug, Clone)]
94pub enum ShellIssue {
95    /// Shell has boundary edges (not watertight).
96    NotWatertight { boundary_edge_count: usize },
97    /// Shell has non-manifold edges.
98    NonManifold { non_manifold_edge_count: usize },
99    /// Shell has inconsistent face winding.
100    InconsistentWinding,
101    /// Shell has zero faces.
102    EmptyShell,
103    /// Shell has degenerate triangles.
104    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
141/// Validate a shell mesh for 3D printing suitability.
142///
143/// Checks:
144/// - Watertightness (no boundary edges)
145/// - Manifoldness (no edges with >2 adjacent faces)
146/// - Consistent winding order
147///
148/// # Arguments
149/// * `shell` - The shell mesh to validate
150///
151/// # Returns
152/// A `ShellValidationResult` with detailed validation information.
153///
154/// # Example
155/// ```
156/// use mesh_repair::Mesh;
157/// use mesh_shell::validate_shell;
158///
159/// let shell = Mesh::new();
160/// let result = validate_shell(&shell);
161/// if result.is_printable() {
162///     println!("Shell is ready for printing!");
163/// } else {
164///     println!("Issues found: {}", result);
165/// }
166/// ```
167pub 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    // Check for empty shell
177    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    // Use mesh-repair's validation to check topology
192    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    // Check watertightness
198    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    // Check manifoldness
210    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    // Check winding consistency
222    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    // Check for degenerate triangles
229    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
259/// Check if the mesh has consistent winding order.
260///
261/// For a valid closed mesh, adjacent faces should have opposite winding
262/// along their shared edge (so normals point consistently outward).
263fn check_winding_consistency(mesh: &Mesh) -> bool {
264    let adjacency = MeshAdjacency::build(&mesh.faces);
265
266    // For each edge, check that the two adjacent faces have opposite winding
267    for (&edge, face_indices) in &adjacency.edge_to_faces {
268        if face_indices.len() != 2 {
269            // Skip boundary or non-manifold edges
270            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        // Find the shared edge orientation in each face
277        let edge_in_a = find_edge_direction(&face_a, edge);
278        let edge_in_b = find_edge_direction(&face_b, edge);
279
280        // For consistent winding, the edge should appear in opposite directions
281        // in the two adjacent faces
282        if edge_in_a == edge_in_b {
283            return false;
284        }
285    }
286
287    true
288}
289
290/// Find the direction of an edge in a face.
291/// Returns true if edge goes v0->v1 in the face's winding order, false if v1->v0.
292fn find_edge_direction(face: &[u32; 3], edge: (u32, u32)) -> bool {
293    let (v0, v1) = edge;
294
295    // Check all three edges of the triangle
296    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; // Forward direction
302        }
303        if a == v1 && b == v0 {
304            return false; // Reverse direction
305        }
306    }
307
308    // Edge not found in face (shouldn't happen with valid adjacency)
309    true
310}
311
312/// Count degenerate triangles in the mesh.
313fn 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/// Result of shell auto-repair operation.
334#[derive(Debug, Clone)]
335pub struct ShellRepairResult {
336    /// Number of degenerate triangles removed.
337    pub degenerate_triangles_removed: usize,
338    /// Number of faces with winding fixed.
339    pub faces_with_winding_fixed: bool,
340    /// Whether repair was successful.
341    pub success: bool,
342    /// Any repair errors encountered.
343    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
373/// Attempt to automatically repair minor issues in a shell mesh.
374///
375/// This function can fix:
376/// - Inconsistent winding order (using flood-fill algorithm)
377/// - Degenerate triangles (removes them)
378///
379/// It cannot fix:
380/// - Non-watertight meshes (holes) - would require hole-filling which may not
381///   be appropriate for all shell geometries
382/// - Non-manifold edges - requires manual intervention
383///
384/// # Arguments
385/// * `shell` - The shell mesh to repair (modified in place)
386///
387/// # Returns
388/// A `ShellRepairResult` with details about repairs performed.
389///
390/// # Example
391/// ```
392/// use mesh_repair::Mesh;
393/// use mesh_shell::{validate_shell, repair_shell};
394///
395/// let mut shell = Mesh::new();
396/// // ... generate shell ...
397/// let repair_result = repair_shell(&mut shell);
398/// if repair_result.success {
399///     println!("{}", repair_result);
400/// }
401/// let validation = validate_shell(&shell);
402/// ```
403pub 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    // 1. Remove degenerate triangles
424    let removed = remove_degenerate_triangles_enhanced(
425        shell, 1e-10,  // area_threshold
426        1000.0, // max_aspect_ratio
427        1e-9,   // min_edge_length
428    );
429    result.degenerate_triangles_removed = removed;
430    if removed > 0 {
431        info!("Removed {} degenerate triangles", removed);
432    }
433
434    // Check if we still have faces after removing degenerate triangles
435    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    // 2. Fix winding order
444    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    // Log summary
461    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
478/// Validate and optionally repair a shell mesh.
479///
480/// This is a convenience function that validates, attempts repair if needed,
481/// and re-validates.
482///
483/// # Arguments
484/// * `shell` - The shell mesh to validate and repair
485/// * `auto_repair` - Whether to attempt automatic repair of minor issues
486///
487/// # Returns
488/// A tuple of (validation result, optional repair result).
489pub fn validate_and_repair_shell(
490    shell: &mut Mesh,
491    auto_repair: bool,
492) -> (ShellValidationResult, Option<ShellRepairResult>) {
493    // Initial validation
494    let initial_validation = validate_shell(shell);
495
496    if initial_validation.is_printable() || !auto_repair {
497        return (initial_validation, None);
498    }
499
500    // Check if we can repair (only certain issues are fixable)
501    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    // Attempt repair
514    let repair_result = repair_shell(shell);
515
516    // Re-validate
517    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        // Faces with consistent outward winding
535        mesh.faces.push([0, 2, 1]); // Bottom
536        mesh.faces.push([0, 1, 3]); // Front
537        mesh.faces.push([1, 2, 3]); // Right
538        mesh.faces.push([2, 0, 3]); // Left
539
540        mesh
541    }
542
543    fn create_open_box() -> Mesh {
544        let mut mesh = Mesh::new();
545
546        // Bottom corners
547        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        // Top corners
552        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        // Bottom (2 triangles)
558        mesh.faces.push([0, 2, 1]);
559        mesh.faces.push([0, 3, 2]);
560        // Front
561        mesh.faces.push([0, 1, 5]);
562        mesh.faces.push([0, 5, 4]);
563        // Back
564        mesh.faces.push([2, 3, 7]);
565        mesh.faces.push([2, 7, 6]);
566        // Left
567        mesh.faces.push([0, 4, 7]);
568        mesh.faces.push([0, 7, 3]);
569        // Right
570        mesh.faces.push([1, 2, 6]);
571        mesh.faces.push([1, 6, 5]);
572        // Top is OPEN
573
574        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        // Add a degenerate triangle (all vertices at same position)
626        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        // Add degenerate triangles
691        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        // Another degenerate - very thin triangle
698        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)); // Nearly collinear
702        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        // Should have removed the degenerate triangles
709        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        // Add a degenerate triangle
756        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        // Repair should have been attempted
765        assert!(repair.is_some());
766        let repair_result = repair.unwrap();
767        assert!(repair_result.degenerate_triangles_removed > 0);
768
769        // Final validation should show the degenerate triangles are gone
770        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}