mesh_repair/
error.rs

1// Allow unused_assignments lint for error struct fields that are used in thiserror Display macros
2// but appear as "never read" to the compiler. This is a false positive in newer Rust versions.
3#![allow(unused_assignments)]
4
5//! Error types for mesh operations with rich diagnostics.
6//!
7//! This module provides comprehensive error handling with:
8//! - Machine-readable error codes for programmatic handling
9//! - Rich context (which vertex, which face, what went wrong)
10//! - Recovery suggestions for common issues
11//! - Beautiful terminal display via miette
12//!
13//! # Error Codes
14//!
15//! Each error has a unique code in the format `MESH-XXXX`:
16//! - `MESH-1xxx`: I/O errors (file reading, writing, parsing)
17//! - `MESH-2xxx`: Validation errors (topology, coordinates)
18//! - `MESH-3xxx`: Repair errors (operations that couldn't complete)
19//! - `MESH-4xxx`: Format errors (unsupported or malformed data)
20//!
21//! # Example
22//!
23//! ```rust,ignore
24//! use mesh_repair::{MeshError, ErrorCode};
25//!
26//! let err = MeshError::invalid_vertex_index(5, 100, 50);
27//! println!("Error code: {}", err.code()); // MESH-2001
28//! println!("Recovery: {:?}", err.recovery_suggestion());
29//! ```
30
31use miette::Diagnostic;
32use std::path::PathBuf;
33use thiserror::Error;
34
35/// Result type alias for mesh operations.
36pub type MeshResult<T> = Result<T, MeshError>;
37
38/// Machine-readable error codes for mesh operations.
39///
40/// Codes follow the pattern `MESH-XXXX` where:
41/// - 1xxx = I/O errors
42/// - 2xxx = Validation errors
43/// - 3xxx = Repair errors
44/// - 4xxx = Format errors
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum ErrorCode {
47    // I/O errors (1xxx)
48    /// MESH-1001: Failed to read file
49    IoRead = 1001,
50    /// MESH-1002: Failed to write file
51    IoWrite = 1002,
52    /// MESH-1003: Failed to parse file format
53    ParseError = 1003,
54
55    // Validation errors (2xxx)
56    /// MESH-2001: Face references invalid vertex index
57    InvalidVertexIndex = 2001,
58    /// MESH-2002: Vertex has NaN or Infinity coordinate
59    InvalidCoordinate = 2002,
60    /// MESH-2003: Mesh has no vertices or faces
61    EmptyMesh = 2003,
62    /// MESH-2004: Invalid mesh topology (non-manifold, etc.)
63    InvalidTopology = 2004,
64
65    // Repair errors (3xxx)
66    /// MESH-3001: Repair operation failed
67    RepairFailed = 3001,
68    /// MESH-3002: Hole filling failed
69    HoleFillFailed = 3002,
70    /// MESH-3003: Winding correction failed
71    WindingFailed = 3003,
72    /// MESH-3004: Decimation failed
73    DecimationFailed = 3004,
74    /// MESH-3005: Remeshing failed
75    RemeshingFailed = 3005,
76    /// MESH-3006: Boolean operation failed
77    BooleanFailed = 3006,
78
79    // Format errors (4xxx)
80    /// MESH-4001: Unsupported file format
81    UnsupportedFormat = 4001,
82    /// MESH-4002: Malformed file structure
83    MalformedFile = 4002,
84}
85
86impl ErrorCode {
87    /// Returns the error code as a string in the format `MESH-XXXX`.
88    pub fn as_str(&self) -> &'static str {
89        match self {
90            ErrorCode::IoRead => "MESH-1001",
91            ErrorCode::IoWrite => "MESH-1002",
92            ErrorCode::ParseError => "MESH-1003",
93            ErrorCode::InvalidVertexIndex => "MESH-2001",
94            ErrorCode::InvalidCoordinate => "MESH-2002",
95            ErrorCode::EmptyMesh => "MESH-2003",
96            ErrorCode::InvalidTopology => "MESH-2004",
97            ErrorCode::RepairFailed => "MESH-3001",
98            ErrorCode::HoleFillFailed => "MESH-3002",
99            ErrorCode::WindingFailed => "MESH-3003",
100            ErrorCode::DecimationFailed => "MESH-3004",
101            ErrorCode::RemeshingFailed => "MESH-3005",
102            ErrorCode::BooleanFailed => "MESH-3006",
103            ErrorCode::UnsupportedFormat => "MESH-4001",
104            ErrorCode::MalformedFile => "MESH-4002",
105        }
106    }
107}
108
109impl std::fmt::Display for ErrorCode {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        write!(f, "{}", self.as_str())
112    }
113}
114
115/// Recovery suggestions for mesh errors.
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum RecoverySuggestion {
118    /// Re-export the file from the original software with different settings.
119    ReexportFile { format: Option<String> },
120    /// Run repair operations to fix the issue.
121    RunRepair { operations: Vec<String> },
122    /// Use a different file format.
123    UseDifferentFormat { suggested: Vec<String> },
124    /// Check the original mesh for issues.
125    CheckSourceMesh { checks: Vec<String> },
126    /// Adjust parameters for the operation.
127    AdjustParameters { parameters: Vec<(String, String)> },
128    /// The mesh may be too complex for the operation.
129    SimplifyMesh { target_faces: Option<usize> },
130    /// Manual intervention may be required.
131    ManualIntervention { description: String },
132    /// No automatic recovery available.
133    None,
134}
135
136impl std::fmt::Display for RecoverySuggestion {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        match self {
139            RecoverySuggestion::ReexportFile { format } => {
140                if let Some(fmt) = format {
141                    write!(
142                        f,
143                        "Try re-exporting the mesh as {} from the original software",
144                        fmt
145                    )
146                } else {
147                    write!(f, "Try re-exporting the mesh from the original software")
148                }
149            }
150            RecoverySuggestion::RunRepair { operations } => {
151                write!(f, "Run repair operations: {}", operations.join(", "))
152            }
153            RecoverySuggestion::UseDifferentFormat { suggested } => {
154                write!(f, "Try using a different format: {}", suggested.join(", "))
155            }
156            RecoverySuggestion::CheckSourceMesh { checks } => {
157                write!(f, "Check the source mesh for: {}", checks.join(", "))
158            }
159            RecoverySuggestion::AdjustParameters { parameters } => {
160                let params: Vec<String> = parameters
161                    .iter()
162                    .map(|(k, v)| format!("{} = {}", k, v))
163                    .collect();
164                write!(f, "Try adjusting: {}", params.join(", "))
165            }
166            RecoverySuggestion::SimplifyMesh { target_faces } => {
167                if let Some(target) = target_faces {
168                    write!(f, "Try simplifying the mesh to ~{} faces first", target)
169                } else {
170                    write!(f, "Try simplifying the mesh first using decimation")
171                }
172            }
173            RecoverySuggestion::ManualIntervention { description } => {
174                write!(f, "{}", description)
175            }
176            RecoverySuggestion::None => {
177                write!(f, "No automatic recovery available")
178            }
179        }
180    }
181}
182
183/// Location information for mesh errors.
184#[derive(Debug, Clone)]
185pub enum MeshLocation {
186    /// Error at a specific vertex.
187    Vertex {
188        index: usize,
189        position: Option<[f64; 3]>,
190    },
191    /// Error at a specific face.
192    Face {
193        index: usize,
194        vertices: Option<[u32; 3]>,
195    },
196    /// Error at a specific edge.
197    Edge { vertex_a: usize, vertex_b: usize },
198    /// Error in a file at a specific location.
199    File {
200        path: PathBuf,
201        line: Option<usize>,
202        column: Option<usize>,
203    },
204    /// Error in a region of the mesh.
205    Region {
206        description: String,
207        face_count: usize,
208    },
209    /// No specific location.
210    Unknown,
211}
212
213impl std::fmt::Display for MeshLocation {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        match self {
216            MeshLocation::Vertex { index, position } => {
217                if let Some([x, y, z]) = position {
218                    write!(f, "vertex {} at ({:.3}, {:.3}, {:.3})", index, x, y, z)
219                } else {
220                    write!(f, "vertex {}", index)
221                }
222            }
223            MeshLocation::Face { index, vertices } => {
224                if let Some([a, b, c]) = vertices {
225                    write!(f, "face {} with vertices [{}, {}, {}]", index, a, b, c)
226                } else {
227                    write!(f, "face {}", index)
228                }
229            }
230            MeshLocation::Edge { vertex_a, vertex_b } => {
231                write!(f, "edge between vertices {} and {}", vertex_a, vertex_b)
232            }
233            MeshLocation::File { path, line, column } => {
234                let mut result = path.display().to_string();
235                if let Some(l) = line {
236                    result.push_str(&format!(":{}", l));
237                    if let Some(c) = column {
238                        result.push_str(&format!(":{}", c));
239                    }
240                }
241                write!(f, "{}", result)
242            }
243            MeshLocation::Region {
244                description,
245                face_count,
246            } => {
247                write!(f, "{} ({} faces)", description, face_count)
248            }
249            MeshLocation::Unknown => {
250                write!(f, "unknown location")
251            }
252        }
253    }
254}
255
256/// Errors that can occur during mesh operations.
257///
258/// Each error variant includes:
259/// - A human-readable message
260/// - A machine-readable error code
261/// - Optional location information
262/// - Recovery suggestions when available
263#[derive(Debug, Error, Diagnostic)]
264pub enum MeshError {
265    /// Error reading from a file.
266    #[error("failed to read mesh from {path}")]
267    #[diagnostic(
268        code(mesh::io::read),
269        help("Check that the file exists and is readable. Try: ls -la {}", path.display())
270    )]
271    IoRead {
272        path: PathBuf,
273        #[source]
274        source: std::io::Error,
275    },
276
277    /// Error writing to a file.
278    #[error("failed to write mesh to {path}")]
279    #[diagnostic(
280        code(mesh::io::write),
281        help("Check that the directory exists and is writable")
282    )]
283    IoWrite {
284        path: PathBuf,
285        #[source]
286        source: std::io::Error,
287    },
288
289    /// Error parsing mesh file format.
290    #[error("failed to parse mesh from {path}: {details}")]
291    #[diagnostic(
292        code(mesh::parse::error),
293        help(
294            "The file may be corrupted or in an unsupported format variant. Try re-exporting from the original software."
295        )
296    )]
297    ParseError { path: PathBuf, details: String },
298
299    /// Unsupported file format.
300    #[error("unsupported mesh format: {extension:?}")]
301    #[diagnostic(
302        code(mesh::format::unsupported),
303        help("Supported formats: STL, OBJ, PLY, 3MF, STEP (with 'step' feature)")
304    )]
305    UnsupportedFormat { extension: Option<String> },
306
307    /// Empty mesh (no vertices or faces).
308    #[error("mesh is empty: {details}")]
309    #[diagnostic(
310        code(mesh::validation::empty),
311        help(
312            "The mesh must have at least one vertex and one face. Check that the file was exported correctly."
313        )
314    )]
315    EmptyMesh { details: String },
316
317    /// Invalid mesh topology.
318    #[error("invalid mesh topology: {details}")]
319    #[diagnostic(
320        code(mesh::validation::topology),
321        help(
322            "Try running `mesh repair` to fix topology issues, or use `mesh validate` for a detailed report."
323        )
324    )]
325    InvalidTopology { details: String },
326
327    /// Mesh repair failed.
328    #[error("mesh repair failed: {details}")]
329    #[diagnostic(
330        code(mesh::repair::failed),
331        help("Try running individual repair operations to identify the specific issue.")
332    )]
333    RepairFailed { details: String },
334
335    /// Invalid vertex index in face data.
336    #[error(
337        "invalid vertex index: face {face_index} references vertex {vertex_index}, but mesh only has {vertex_count} vertices"
338    )]
339    #[diagnostic(
340        code(mesh::validation::vertex_index),
341        help(
342            "Run `mesh repair` to remove faces with invalid vertex references, or check the mesh export settings."
343        )
344    )]
345    InvalidVertexIndex {
346        face_index: usize,
347        vertex_index: u32,
348        vertex_count: usize,
349    },
350
351    /// Invalid coordinate value (NaN or Infinity).
352    #[error("invalid coordinate at vertex {vertex_index}: {coordinate} is {value}")]
353    #[diagnostic(
354        code(mesh::validation::coordinate),
355        help(
356            "Check for numerical issues in the source data. This often happens with very small or very large values."
357        )
358    )]
359    InvalidCoordinate {
360        vertex_index: usize,
361        coordinate: &'static str,
362        value: f64,
363    },
364
365    /// Hole filling failed.
366    #[error("hole filling failed: {details}")]
367    #[diagnostic(
368        code(mesh::repair::hole_fill),
369        help(
370            "The hole may be too complex or have self-intersecting boundaries. Try splitting the mesh or filling manually."
371        )
372    )]
373    HoleFillFailed { details: String },
374
375    /// Boolean operation failed.
376    #[error("boolean operation failed: {details}")]
377    #[diagnostic(
378        code(mesh::boolean::failed),
379        help(
380            "Ensure both meshes are watertight and non-self-intersecting. Try running `mesh repair` on both inputs first."
381        )
382    )]
383    BooleanFailed { details: String, operation: String },
384
385    /// Decimation failed.
386    #[error("decimation failed: {details}")]
387    #[diagnostic(
388        code(mesh::decimate::failed),
389        help(
390            "Try a less aggressive target ratio or ensure the mesh has valid topology before decimation."
391        )
392    )]
393    DecimationFailed { details: String },
394
395    /// Remeshing failed.
396    #[error("remeshing failed: {details}")]
397    #[diagnostic(
398        code(mesh::remesh::failed),
399        help("Try adjusting the target edge length or repairing the mesh first.")
400    )]
401    RemeshingFailed { details: String },
402}
403
404impl MeshError {
405    /// Returns the machine-readable error code.
406    pub fn code(&self) -> ErrorCode {
407        match self {
408            MeshError::IoRead { .. } => ErrorCode::IoRead,
409            MeshError::IoWrite { .. } => ErrorCode::IoWrite,
410            MeshError::ParseError { .. } => ErrorCode::ParseError,
411            MeshError::UnsupportedFormat { .. } => ErrorCode::UnsupportedFormat,
412            MeshError::EmptyMesh { .. } => ErrorCode::EmptyMesh,
413            MeshError::InvalidTopology { .. } => ErrorCode::InvalidTopology,
414            MeshError::RepairFailed { .. } => ErrorCode::RepairFailed,
415            MeshError::InvalidVertexIndex { .. } => ErrorCode::InvalidVertexIndex,
416            MeshError::InvalidCoordinate { .. } => ErrorCode::InvalidCoordinate,
417            MeshError::HoleFillFailed { .. } => ErrorCode::HoleFillFailed,
418            MeshError::BooleanFailed { .. } => ErrorCode::BooleanFailed,
419            MeshError::DecimationFailed { .. } => ErrorCode::DecimationFailed,
420            MeshError::RemeshingFailed { .. } => ErrorCode::RemeshingFailed,
421        }
422    }
423
424    /// Returns a recovery suggestion for this error.
425    pub fn recovery_suggestion(&self) -> RecoverySuggestion {
426        match self {
427            MeshError::IoRead { .. } => RecoverySuggestion::CheckSourceMesh {
428                checks: vec!["file exists".into(), "file permissions".into()],
429            },
430            MeshError::IoWrite { .. } => RecoverySuggestion::CheckSourceMesh {
431                checks: vec!["directory exists".into(), "write permissions".into()],
432            },
433            MeshError::ParseError { .. } => RecoverySuggestion::ReexportFile {
434                format: Some("binary STL or OBJ".into()),
435            },
436            MeshError::UnsupportedFormat { .. } => RecoverySuggestion::UseDifferentFormat {
437                suggested: vec!["STL".into(), "OBJ".into(), "PLY".into(), "3MF".into()],
438            },
439            MeshError::EmptyMesh { .. } => RecoverySuggestion::CheckSourceMesh {
440                checks: vec!["mesh has geometry".into(), "correct export settings".into()],
441            },
442            MeshError::InvalidTopology { .. } => RecoverySuggestion::RunRepair {
443                operations: vec!["fix_winding".into(), "remove_degenerate".into()],
444            },
445            MeshError::RepairFailed { .. } => RecoverySuggestion::ManualIntervention {
446                description: "Try running individual repair operations to identify the issue"
447                    .into(),
448            },
449            MeshError::InvalidVertexIndex { .. } => RecoverySuggestion::RunRepair {
450                operations: vec!["validate".into(), "remove_invalid_faces".into()],
451            },
452            MeshError::InvalidCoordinate { .. } => RecoverySuggestion::CheckSourceMesh {
453                checks: vec!["coordinate values".into(), "export precision".into()],
454            },
455            MeshError::HoleFillFailed { .. } => RecoverySuggestion::RunRepair {
456                operations: vec!["fill_holes with max_edges parameter".into()],
457            },
458            MeshError::BooleanFailed { .. } => RecoverySuggestion::RunRepair {
459                operations: vec![
460                    "repair both meshes".into(),
461                    "check for self-intersections".into(),
462                ],
463            },
464            MeshError::DecimationFailed { .. } => RecoverySuggestion::AdjustParameters {
465                parameters: vec![("target_ratio".into(), "try a higher value".into())],
466            },
467            MeshError::RemeshingFailed { .. } => RecoverySuggestion::AdjustParameters {
468                parameters: vec![("target_edge_length".into(), "try a larger value".into())],
469            },
470        }
471    }
472
473    /// Returns location information if available.
474    pub fn location(&self) -> Option<MeshLocation> {
475        match self {
476            MeshError::InvalidVertexIndex { face_index, .. } => Some(MeshLocation::Face {
477                index: *face_index,
478                vertices: None,
479            }),
480            MeshError::InvalidCoordinate { vertex_index, .. } => Some(MeshLocation::Vertex {
481                index: *vertex_index,
482                position: None,
483            }),
484            MeshError::ParseError { path, .. } => Some(MeshLocation::File {
485                path: path.clone(),
486                line: None,
487                column: None,
488            }),
489            MeshError::IoRead { path, .. } => Some(MeshLocation::File {
490                path: path.clone(),
491                line: None,
492                column: None,
493            }),
494            MeshError::IoWrite { path, .. } => Some(MeshLocation::File {
495                path: path.clone(),
496                line: None,
497                column: None,
498            }),
499            _ => None,
500        }
501    }
502
503    // Constructor helpers for common error patterns
504
505    /// Create an IoRead error.
506    pub fn io_read(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
507        MeshError::IoRead {
508            path: path.into(),
509            source,
510        }
511    }
512
513    /// Create an IoWrite error.
514    pub fn io_write(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
515        MeshError::IoWrite {
516            path: path.into(),
517            source,
518        }
519    }
520
521    /// Create a ParseError.
522    pub fn parse_error(path: impl Into<PathBuf>, details: impl Into<String>) -> Self {
523        MeshError::ParseError {
524            path: path.into(),
525            details: details.into(),
526        }
527    }
528
529    /// Create an InvalidVertexIndex error.
530    pub fn invalid_vertex_index(face_index: usize, vertex_index: u32, vertex_count: usize) -> Self {
531        MeshError::InvalidVertexIndex {
532            face_index,
533            vertex_index,
534            vertex_count,
535        }
536    }
537
538    /// Create an InvalidCoordinate error.
539    pub fn invalid_coordinate(vertex_index: usize, coordinate: &'static str, value: f64) -> Self {
540        MeshError::InvalidCoordinate {
541            vertex_index,
542            coordinate,
543            value,
544        }
545    }
546
547    /// Create an EmptyMesh error.
548    pub fn empty_mesh(details: impl Into<String>) -> Self {
549        MeshError::EmptyMesh {
550            details: details.into(),
551        }
552    }
553
554    /// Create an InvalidTopology error.
555    pub fn invalid_topology(details: impl Into<String>) -> Self {
556        MeshError::InvalidTopology {
557            details: details.into(),
558        }
559    }
560
561    /// Create a RepairFailed error.
562    pub fn repair_failed(details: impl Into<String>) -> Self {
563        MeshError::RepairFailed {
564            details: details.into(),
565        }
566    }
567
568    /// Create a HoleFillFailed error.
569    pub fn hole_fill_failed(details: impl Into<String>) -> Self {
570        MeshError::HoleFillFailed {
571            details: details.into(),
572        }
573    }
574
575    /// Create a BooleanFailed error.
576    pub fn boolean_failed(operation: impl Into<String>, details: impl Into<String>) -> Self {
577        MeshError::BooleanFailed {
578            details: details.into(),
579            operation: operation.into(),
580        }
581    }
582
583    /// Create a DecimationFailed error.
584    pub fn decimation_failed(details: impl Into<String>) -> Self {
585        MeshError::DecimationFailed {
586            details: details.into(),
587        }
588    }
589
590    /// Create a RemeshingFailed error.
591    pub fn remeshing_failed(details: impl Into<String>) -> Self {
592        MeshError::RemeshingFailed {
593            details: details.into(),
594        }
595    }
596
597    /// Create an UnsupportedFormat error.
598    pub fn unsupported_format(extension: Option<String>) -> Self {
599        MeshError::UnsupportedFormat { extension }
600    }
601}
602
603/// Validation issues that can be collected during mesh validation.
604///
605/// Unlike `MeshError`, these represent issues that may be warnings rather than errors,
606/// and multiple issues can be collected without stopping validation.
607#[derive(Debug, Clone)]
608pub enum ValidationIssue {
609    /// Face references a vertex index that doesn't exist.
610    InvalidVertexIndex {
611        face_index: usize,
612        vertex_index: u32,
613        vertex_count: usize,
614    },
615    /// Vertex has NaN coordinate.
616    NaNCoordinate {
617        vertex_index: usize,
618        coordinate: &'static str,
619    },
620    /// Vertex has infinite coordinate.
621    InfiniteCoordinate {
622        vertex_index: usize,
623        coordinate: &'static str,
624        value: f64,
625    },
626    /// Degenerate face (zero area).
627    DegenerateFace { face_index: usize, area: f64 },
628    /// Non-manifold edge (shared by more than 2 faces).
629    NonManifoldEdge {
630        vertex_a: usize,
631        vertex_b: usize,
632        face_count: usize,
633    },
634    /// Inconsistent winding order.
635    InconsistentWinding {
636        face_index: usize,
637        neighbor_index: usize,
638    },
639    /// Self-intersection detected.
640    SelfIntersection { face_a: usize, face_b: usize },
641}
642
643impl ValidationIssue {
644    /// Returns a severity level for the issue.
645    pub fn severity(&self) -> IssueSeverity {
646        match self {
647            ValidationIssue::InvalidVertexIndex { .. } => IssueSeverity::Error,
648            ValidationIssue::NaNCoordinate { .. } => IssueSeverity::Error,
649            ValidationIssue::InfiniteCoordinate { .. } => IssueSeverity::Error,
650            ValidationIssue::DegenerateFace { .. } => IssueSeverity::Warning,
651            ValidationIssue::NonManifoldEdge { .. } => IssueSeverity::Warning,
652            ValidationIssue::InconsistentWinding { .. } => IssueSeverity::Warning,
653            ValidationIssue::SelfIntersection { .. } => IssueSeverity::Warning,
654        }
655    }
656
657    /// Returns an error code for programmatic handling.
658    pub fn code(&self) -> &'static str {
659        match self {
660            ValidationIssue::InvalidVertexIndex { .. } => "MESH-2001",
661            ValidationIssue::NaNCoordinate { .. } => "MESH-2002",
662            ValidationIssue::InfiniteCoordinate { .. } => "MESH-2002",
663            ValidationIssue::DegenerateFace { .. } => "MESH-2005",
664            ValidationIssue::NonManifoldEdge { .. } => "MESH-2006",
665            ValidationIssue::InconsistentWinding { .. } => "MESH-2007",
666            ValidationIssue::SelfIntersection { .. } => "MESH-2008",
667        }
668    }
669
670    /// Returns a recovery suggestion.
671    pub fn suggestion(&self) -> &'static str {
672        match self {
673            ValidationIssue::InvalidVertexIndex { .. } => {
674                "Remove faces with invalid vertex references using `mesh repair`"
675            }
676            ValidationIssue::NaNCoordinate { .. } | ValidationIssue::InfiniteCoordinate { .. } => {
677                "Check source data for numerical issues; try re-exporting"
678            }
679            ValidationIssue::DegenerateFace { .. } => {
680                "Run `mesh repair` to remove degenerate faces"
681            }
682            ValidationIssue::NonManifoldEdge { .. } => {
683                "Run `mesh repair` to fix non-manifold edges"
684            }
685            ValidationIssue::InconsistentWinding { .. } => "Run `mesh repair` to fix winding order",
686            ValidationIssue::SelfIntersection { .. } => {
687                "Self-intersections may need manual repair in a 3D editor"
688            }
689        }
690    }
691}
692
693/// Severity levels for validation issues.
694#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
695pub enum IssueSeverity {
696    /// Informational, no action needed.
697    Info,
698    /// Warning, mesh may have issues.
699    Warning,
700    /// Error, mesh is invalid.
701    Error,
702}
703
704impl std::fmt::Display for ValidationIssue {
705    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
706        match self {
707            ValidationIssue::InvalidVertexIndex {
708                face_index,
709                vertex_index,
710                vertex_count,
711            } => {
712                write!(
713                    f,
714                    "face {} references vertex {}, but mesh only has {} vertices",
715                    face_index, vertex_index, vertex_count
716                )
717            }
718            ValidationIssue::NaNCoordinate {
719                vertex_index,
720                coordinate,
721            } => {
722                write!(
723                    f,
724                    "vertex {} has NaN {} coordinate",
725                    vertex_index, coordinate
726                )
727            }
728            ValidationIssue::InfiniteCoordinate {
729                vertex_index,
730                coordinate,
731                value,
732            } => {
733                write!(
734                    f,
735                    "vertex {} has infinite {} coordinate ({})",
736                    vertex_index, coordinate, value
737                )
738            }
739            ValidationIssue::DegenerateFace { face_index, area } => {
740                write!(f, "face {} is degenerate (area: {:.2e})", face_index, area)
741            }
742            ValidationIssue::NonManifoldEdge {
743                vertex_a,
744                vertex_b,
745                face_count,
746            } => {
747                write!(
748                    f,
749                    "edge ({}, {}) is non-manifold (shared by {} faces)",
750                    vertex_a, vertex_b, face_count
751                )
752            }
753            ValidationIssue::InconsistentWinding {
754                face_index,
755                neighbor_index,
756            } => {
757                write!(
758                    f,
759                    "face {} has inconsistent winding with neighbor {}",
760                    face_index, neighbor_index
761                )
762            }
763            ValidationIssue::SelfIntersection { face_a, face_b } => {
764                write!(f, "faces {} and {} self-intersect", face_a, face_b)
765            }
766        }
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773
774    #[test]
775    fn test_error_codes() {
776        let err = MeshError::invalid_vertex_index(5, 100, 50);
777        assert_eq!(err.code(), ErrorCode::InvalidVertexIndex);
778        assert_eq!(err.code().as_str(), "MESH-2001");
779    }
780
781    #[test]
782    fn test_recovery_suggestions() {
783        let err = MeshError::invalid_topology("non-manifold edge");
784        let suggestion = err.recovery_suggestion();
785        match suggestion {
786            RecoverySuggestion::RunRepair { operations } => {
787                assert!(!operations.is_empty());
788            }
789            _ => panic!("Expected RunRepair suggestion"),
790        }
791    }
792
793    #[test]
794    fn test_location_info() {
795        let err = MeshError::invalid_vertex_index(5, 100, 50);
796        let location = err.location();
797        assert!(location.is_some());
798        match location.unwrap() {
799            MeshLocation::Face { index, .. } => {
800                assert_eq!(index, 5);
801            }
802            _ => panic!("Expected Face location"),
803        }
804    }
805
806    #[test]
807    fn test_validation_issue_severity() {
808        let issue = ValidationIssue::DegenerateFace {
809            face_index: 0,
810            area: 0.0,
811        };
812        assert_eq!(issue.severity(), IssueSeverity::Warning);
813
814        let issue = ValidationIssue::InvalidVertexIndex {
815            face_index: 0,
816            vertex_index: 100,
817            vertex_count: 50,
818        };
819        assert_eq!(issue.severity(), IssueSeverity::Error);
820    }
821
822    #[test]
823    fn test_error_display() {
824        let err = MeshError::invalid_vertex_index(5, 100, 50);
825        let display = format!("{}", err);
826        assert!(display.contains("face 5"));
827        assert!(display.contains("vertex 100"));
828        assert!(display.contains("50 vertices"));
829    }
830}