mesh_shell/
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 shell operations with rich diagnostics.
6//!
7//! This module provides comprehensive error handling with:
8//! - Machine-readable error codes for programmatic handling
9//! - Rich context (grid dimensions, operation parameters)
10//! - Recovery suggestions for common issues
11//! - Beautiful terminal display via miette
12
13use miette::Diagnostic;
14use thiserror::Error;
15
16/// Result type alias for shell operations.
17pub type ShellResult<T> = Result<T, ShellError>;
18
19/// Machine-readable error codes for shell operations.
20///
21/// Codes follow the pattern `SHELL-XXXX` where:
22/// - 1xxx = Input validation errors
23/// - 2xxx = Computation errors
24/// - 3xxx = Output generation errors
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum ShellErrorCode {
27    /// SHELL-1001: Input mesh is empty
28    EmptyMesh = 1001,
29    /// SHELL-1002: Invalid parameters
30    InvalidParams = 1002,
31
32    /// SHELL-2001: SDF grid too large
33    GridTooLarge = 2001,
34    /// SHELL-2002: SDF computation failed
35    SdfFailed = 2002,
36    /// SHELL-2003: Isosurface extraction failed
37    IsosurfaceFailed = 2003,
38
39    /// SHELL-3001: Tag transfer failed
40    TagTransferFailed = 3001,
41    /// SHELL-3002: Shell generation failed
42    ShellGenerationFailed = 3002,
43    /// SHELL-3003: Rim generation failed
44    RimGenerationFailed = 3003,
45}
46
47impl ShellErrorCode {
48    /// Returns the error code as a string in the format `SHELL-XXXX`.
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            ShellErrorCode::EmptyMesh => "SHELL-1001",
52            ShellErrorCode::InvalidParams => "SHELL-1002",
53            ShellErrorCode::GridTooLarge => "SHELL-2001",
54            ShellErrorCode::SdfFailed => "SHELL-2002",
55            ShellErrorCode::IsosurfaceFailed => "SHELL-2003",
56            ShellErrorCode::TagTransferFailed => "SHELL-3001",
57            ShellErrorCode::ShellGenerationFailed => "SHELL-3002",
58            ShellErrorCode::RimGenerationFailed => "SHELL-3003",
59        }
60    }
61}
62
63impl std::fmt::Display for ShellErrorCode {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        write!(f, "{}", self.as_str())
66    }
67}
68
69/// Recovery suggestions for shell errors.
70#[derive(Debug, Clone, PartialEq)]
71pub enum ShellRecoverySuggestion {
72    /// Reduce grid resolution.
73    ReduceGridResolution {
74        current: [usize; 3],
75        suggested: [usize; 3],
76    },
77    /// Use adaptive grid.
78    UseAdaptiveGrid,
79    /// Repair input mesh first.
80    RepairInputMesh,
81    /// Adjust shell thickness.
82    AdjustThickness { current: f64, suggested: f64 },
83    /// Simplify input mesh.
84    SimplifyMesh { target_faces: usize },
85    /// No specific suggestion.
86    None,
87}
88
89impl std::fmt::Display for ShellRecoverySuggestion {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match self {
92            ShellRecoverySuggestion::ReduceGridResolution { current, suggested } => {
93                write!(
94                    f,
95                    "Reduce grid resolution from {:?} to {:?}",
96                    current, suggested
97                )
98            }
99            ShellRecoverySuggestion::UseAdaptiveGrid => {
100                write!(f, "Enable adaptive grid resolution for better performance")
101            }
102            ShellRecoverySuggestion::RepairInputMesh => {
103                write!(f, "Run `mesh repair` on the input mesh first")
104            }
105            ShellRecoverySuggestion::AdjustThickness { current, suggested } => {
106                write!(
107                    f,
108                    "Adjust thickness from {:.2}mm to {:.2}mm",
109                    current, suggested
110                )
111            }
112            ShellRecoverySuggestion::SimplifyMesh { target_faces } => {
113                write!(
114                    f,
115                    "Simplify mesh to ~{} faces using decimation",
116                    target_faces
117                )
118            }
119            ShellRecoverySuggestion::None => {
120                write!(f, "No specific suggestion available")
121            }
122        }
123    }
124}
125
126/// Errors that can occur during shell operations.
127#[derive(Debug, Error, Diagnostic)]
128pub enum ShellError {
129    /// Input mesh is empty.
130    #[error("input mesh is empty")]
131    #[diagnostic(
132        code(shell::input::empty),
133        help(
134            "The input mesh must have at least one vertex and one face. Check that the mesh was loaded correctly."
135        )
136    )]
137    EmptyMesh,
138
139    /// SDF grid would be too large.
140    #[error("SDF grid too large: {dims:?} = {total} voxels exceeds limit of {max}")]
141    #[diagnostic(
142        code(shell::grid::too_large),
143        help("Reduce the grid resolution or use adaptive grid mode. Consider: --voxel-size {}",
144             (*max as f64).powf(1.0/3.0).ceil() as usize)
145    )]
146    GridTooLarge {
147        dims: [usize; 3],
148        total: usize,
149        max: usize,
150    },
151
152    /// Isosurface extraction failed.
153    #[error("isosurface extraction produced empty mesh")]
154    #[diagnostic(
155        code(shell::isosurface::empty),
156        help(
157            "The shell thickness may be larger than the mesh dimensions, or the mesh may have degenerate geometry. Try reducing thickness or repairing the mesh."
158        )
159    )]
160    EmptyIsosurface,
161
162    /// Tag transfer failed.
163    #[error("failed to transfer vertex tags: {details}")]
164    #[diagnostic(
165        code(shell::tags::transfer_failed),
166        help(
167            "Tag transfer requires a well-formed source mesh. Ensure the input mesh has valid vertex indices."
168        )
169    )]
170    TagTransferFailed { details: String },
171
172    /// Shell generation failed.
173    #[error("shell generation failed: {details}")]
174    #[diagnostic(
175        code(shell::generation::failed),
176        help("{}", suggestion.as_ref().map(|s| s.to_string()).unwrap_or_else(|| "Try repairing the input mesh or adjusting shell parameters.".to_string()))
177    )]
178    ShellGenerationFailed {
179        details: String,
180        suggestion: Option<ShellRecoverySuggestion>,
181    },
182
183    /// SDF computation failed.
184    #[error("SDF computation failed: {details}")]
185    #[diagnostic(
186        code(shell::sdf::failed),
187        help(
188            "The mesh may have degenerate triangles or non-manifold geometry. Run `mesh repair` first."
189        )
190    )]
191    SdfFailed {
192        details: String,
193        grid_dims: Option<[usize; 3]>,
194    },
195
196    /// Invalid parameters.
197    #[error("invalid shell parameters: {details}")]
198    #[diagnostic(
199        code(shell::params::invalid),
200        help("Check parameter values: thickness > 0, voxel_size > 0, etc.")
201    )]
202    InvalidParams {
203        details: String,
204        param_name: Option<String>,
205        param_value: Option<String>,
206    },
207
208    /// Rim generation failed.
209    #[error("rim generation failed: {details}")]
210    #[diagnostic(
211        code(shell::rim::failed),
212        help("Rim generation requires open boundaries. Ensure the shell has valid open edges.")
213    )]
214    RimGenerationFailed { details: String },
215
216    /// Underlying mesh error.
217    #[error("mesh operation failed: {0}")]
218    #[diagnostic(code(shell::mesh::error))]
219    MeshError(#[from] mesh_repair::MeshError),
220}
221
222impl ShellError {
223    /// Returns the machine-readable error code.
224    pub fn code(&self) -> ShellErrorCode {
225        match self {
226            ShellError::EmptyMesh => ShellErrorCode::EmptyMesh,
227            ShellError::GridTooLarge { .. } => ShellErrorCode::GridTooLarge,
228            ShellError::EmptyIsosurface => ShellErrorCode::IsosurfaceFailed,
229            ShellError::TagTransferFailed { .. } => ShellErrorCode::TagTransferFailed,
230            ShellError::ShellGenerationFailed { .. } => ShellErrorCode::ShellGenerationFailed,
231            ShellError::SdfFailed { .. } => ShellErrorCode::SdfFailed,
232            ShellError::InvalidParams { .. } => ShellErrorCode::InvalidParams,
233            ShellError::RimGenerationFailed { .. } => ShellErrorCode::RimGenerationFailed,
234            ShellError::MeshError(_) => ShellErrorCode::ShellGenerationFailed,
235        }
236    }
237
238    /// Returns a recovery suggestion for this error.
239    pub fn recovery_suggestion(&self) -> ShellRecoverySuggestion {
240        match self {
241            ShellError::EmptyMesh => ShellRecoverySuggestion::RepairInputMesh,
242            ShellError::GridTooLarge { dims, max, .. } => {
243                // Calculate suggested dimensions that would fit within max
244                let scale = (*max as f64 / (dims[0] * dims[1] * dims[2]) as f64).powf(1.0 / 3.0);
245                let suggested = [
246                    ((dims[0] as f64 * scale) as usize).max(1),
247                    ((dims[1] as f64 * scale) as usize).max(1),
248                    ((dims[2] as f64 * scale) as usize).max(1),
249                ];
250                ShellRecoverySuggestion::ReduceGridResolution {
251                    current: *dims,
252                    suggested,
253                }
254            }
255            ShellError::EmptyIsosurface => ShellRecoverySuggestion::AdjustThickness {
256                current: 0.0,
257                suggested: 1.0,
258            },
259            ShellError::TagTransferFailed { .. } => ShellRecoverySuggestion::RepairInputMesh,
260            ShellError::ShellGenerationFailed { suggestion, .. } => suggestion
261                .clone()
262                .unwrap_or(ShellRecoverySuggestion::RepairInputMesh),
263            ShellError::SdfFailed { .. } => ShellRecoverySuggestion::RepairInputMesh,
264            ShellError::InvalidParams { .. } => ShellRecoverySuggestion::None,
265            ShellError::RimGenerationFailed { .. } => ShellRecoverySuggestion::RepairInputMesh,
266            ShellError::MeshError(_) => ShellRecoverySuggestion::RepairInputMesh,
267        }
268    }
269
270    // Constructor helpers
271
272    /// Create an empty mesh error.
273    pub fn empty_mesh() -> Self {
274        ShellError::EmptyMesh
275    }
276
277    /// Create a grid too large error.
278    pub fn grid_too_large(dims: [usize; 3], max: usize) -> Self {
279        ShellError::GridTooLarge {
280            dims,
281            total: dims[0] * dims[1] * dims[2],
282            max,
283        }
284    }
285
286    /// Create an empty isosurface error.
287    pub fn empty_isosurface() -> Self {
288        ShellError::EmptyIsosurface
289    }
290
291    /// Create a tag transfer failed error.
292    pub fn tag_transfer_failed(details: impl Into<String>) -> Self {
293        ShellError::TagTransferFailed {
294            details: details.into(),
295        }
296    }
297
298    /// Create a shell generation failed error.
299    pub fn shell_generation_failed(details: impl Into<String>) -> Self {
300        ShellError::ShellGenerationFailed {
301            details: details.into(),
302            suggestion: None,
303        }
304    }
305
306    /// Create a shell generation failed error with suggestion.
307    pub fn shell_generation_failed_with_suggestion(
308        details: impl Into<String>,
309        suggestion: ShellRecoverySuggestion,
310    ) -> Self {
311        ShellError::ShellGenerationFailed {
312            details: details.into(),
313            suggestion: Some(suggestion),
314        }
315    }
316
317    /// Create an SDF failed error.
318    pub fn sdf_failed(details: impl Into<String>) -> Self {
319        ShellError::SdfFailed {
320            details: details.into(),
321            grid_dims: None,
322        }
323    }
324
325    /// Create an SDF failed error with grid dimensions.
326    pub fn sdf_failed_with_grid(details: impl Into<String>, grid_dims: [usize; 3]) -> Self {
327        ShellError::SdfFailed {
328            details: details.into(),
329            grid_dims: Some(grid_dims),
330        }
331    }
332
333    /// Create an invalid params error.
334    pub fn invalid_params(details: impl Into<String>) -> Self {
335        ShellError::InvalidParams {
336            details: details.into(),
337            param_name: None,
338            param_value: None,
339        }
340    }
341
342    /// Create an invalid params error with param info.
343    pub fn invalid_param(
344        param_name: impl Into<String>,
345        param_value: impl Into<String>,
346        details: impl Into<String>,
347    ) -> Self {
348        ShellError::InvalidParams {
349            details: details.into(),
350            param_name: Some(param_name.into()),
351            param_value: Some(param_value.into()),
352        }
353    }
354
355    /// Create a rim generation failed error.
356    pub fn rim_generation_failed(details: impl Into<String>) -> Self {
357        ShellError::RimGenerationFailed {
358            details: details.into(),
359        }
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_error_codes() {
369        let err = ShellError::empty_mesh();
370        assert_eq!(err.code(), ShellErrorCode::EmptyMesh);
371        assert_eq!(err.code().as_str(), "SHELL-1001");
372    }
373
374    #[test]
375    fn test_grid_too_large_suggestion() {
376        let err = ShellError::grid_too_large([200, 200, 200], 1_000_000);
377        let suggestion = err.recovery_suggestion();
378        match suggestion {
379            ShellRecoverySuggestion::ReduceGridResolution { current, suggested } => {
380                assert_eq!(current, [200, 200, 200]);
381                // Suggested should be smaller
382                assert!(suggested[0] < 200 || suggested[1] < 200 || suggested[2] < 200);
383            }
384            _ => panic!("Expected ReduceGridResolution suggestion"),
385        }
386    }
387
388    #[test]
389    fn test_error_display() {
390        let err = ShellError::grid_too_large([100, 100, 100], 500_000);
391        let display = format!("{}", err);
392        assert!(display.contains("1000000 voxels"));
393        assert!(display.contains("500000"));
394    }
395
396    #[test]
397    fn test_from_mesh_error() {
398        let mesh_err = mesh_repair::MeshError::empty_mesh("test");
399        let shell_err: ShellError = mesh_err.into();
400        assert!(matches!(shell_err, ShellError::MeshError(_)));
401    }
402}