1#![allow(unused_assignments)]
4
5use miette::Diagnostic;
14use thiserror::Error;
15
16pub type ShellResult<T> = Result<T, ShellError>;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum ShellErrorCode {
27 EmptyMesh = 1001,
29 InvalidParams = 1002,
31
32 GridTooLarge = 2001,
34 SdfFailed = 2002,
36 IsosurfaceFailed = 2003,
38
39 TagTransferFailed = 3001,
41 ShellGenerationFailed = 3002,
43 RimGenerationFailed = 3003,
45}
46
47impl ShellErrorCode {
48 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#[derive(Debug, Clone, PartialEq)]
71pub enum ShellRecoverySuggestion {
72 ReduceGridResolution {
74 current: [usize; 3],
75 suggested: [usize; 3],
76 },
77 UseAdaptiveGrid,
79 RepairInputMesh,
81 AdjustThickness { current: f64, suggested: f64 },
83 SimplifyMesh { target_faces: usize },
85 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#[derive(Debug, Error, Diagnostic)]
128pub enum ShellError {
129 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[error("mesh operation failed: {0}")]
218 #[diagnostic(code(shell::mesh::error))]
219 MeshError(#[from] mesh_repair::MeshError),
220}
221
222impl ShellError {
223 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 pub fn recovery_suggestion(&self) -> ShellRecoverySuggestion {
240 match self {
241 ShellError::EmptyMesh => ShellRecoverySuggestion::RepairInputMesh,
242 ShellError::GridTooLarge { dims, max, .. } => {
243 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 pub fn empty_mesh() -> Self {
274 ShellError::EmptyMesh
275 }
276
277 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 pub fn empty_isosurface() -> Self {
288 ShellError::EmptyIsosurface
289 }
290
291 pub fn tag_transfer_failed(details: impl Into<String>) -> Self {
293 ShellError::TagTransferFailed {
294 details: details.into(),
295 }
296 }
297
298 pub fn shell_generation_failed(details: impl Into<String>) -> Self {
300 ShellError::ShellGenerationFailed {
301 details: details.into(),
302 suggestion: None,
303 }
304 }
305
306 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 pub fn sdf_failed(details: impl Into<String>) -> Self {
319 ShellError::SdfFailed {
320 details: details.into(),
321 grid_dims: None,
322 }
323 }
324
325 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 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 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 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 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}