1#![allow(unused_assignments)]
4
5use miette::Diagnostic;
32use std::path::PathBuf;
33use thiserror::Error;
34
35pub type MeshResult<T> = Result<T, MeshError>;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum ErrorCode {
47 IoRead = 1001,
50 IoWrite = 1002,
52 ParseError = 1003,
54
55 InvalidVertexIndex = 2001,
58 InvalidCoordinate = 2002,
60 EmptyMesh = 2003,
62 InvalidTopology = 2004,
64
65 RepairFailed = 3001,
68 HoleFillFailed = 3002,
70 WindingFailed = 3003,
72 DecimationFailed = 3004,
74 RemeshingFailed = 3005,
76 BooleanFailed = 3006,
78
79 UnsupportedFormat = 4001,
82 MalformedFile = 4002,
84}
85
86impl ErrorCode {
87 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#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum RecoverySuggestion {
118 ReexportFile { format: Option<String> },
120 RunRepair { operations: Vec<String> },
122 UseDifferentFormat { suggested: Vec<String> },
124 CheckSourceMesh { checks: Vec<String> },
126 AdjustParameters { parameters: Vec<(String, String)> },
128 SimplifyMesh { target_faces: Option<usize> },
130 ManualIntervention { description: String },
132 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#[derive(Debug, Clone)]
185pub enum MeshLocation {
186 Vertex {
188 index: usize,
189 position: Option<[f64; 3]>,
190 },
191 Face {
193 index: usize,
194 vertices: Option<[u32; 3]>,
195 },
196 Edge { vertex_a: usize, vertex_b: usize },
198 File {
200 path: PathBuf,
201 line: Option<usize>,
202 column: Option<usize>,
203 },
204 Region {
206 description: String,
207 face_count: usize,
208 },
209 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#[derive(Debug, Error, Diagnostic)]
264pub enum MeshError {
265 #[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("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("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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 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 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 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 pub fn empty_mesh(details: impl Into<String>) -> Self {
549 MeshError::EmptyMesh {
550 details: details.into(),
551 }
552 }
553
554 pub fn invalid_topology(details: impl Into<String>) -> Self {
556 MeshError::InvalidTopology {
557 details: details.into(),
558 }
559 }
560
561 pub fn repair_failed(details: impl Into<String>) -> Self {
563 MeshError::RepairFailed {
564 details: details.into(),
565 }
566 }
567
568 pub fn hole_fill_failed(details: impl Into<String>) -> Self {
570 MeshError::HoleFillFailed {
571 details: details.into(),
572 }
573 }
574
575 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 pub fn decimation_failed(details: impl Into<String>) -> Self {
585 MeshError::DecimationFailed {
586 details: details.into(),
587 }
588 }
589
590 pub fn remeshing_failed(details: impl Into<String>) -> Self {
592 MeshError::RemeshingFailed {
593 details: details.into(),
594 }
595 }
596
597 pub fn unsupported_format(extension: Option<String>) -> Self {
599 MeshError::UnsupportedFormat { extension }
600 }
601}
602
603#[derive(Debug, Clone)]
608pub enum ValidationIssue {
609 InvalidVertexIndex {
611 face_index: usize,
612 vertex_index: u32,
613 vertex_count: usize,
614 },
615 NaNCoordinate {
617 vertex_index: usize,
618 coordinate: &'static str,
619 },
620 InfiniteCoordinate {
622 vertex_index: usize,
623 coordinate: &'static str,
624 value: f64,
625 },
626 DegenerateFace { face_index: usize, area: f64 },
628 NonManifoldEdge {
630 vertex_a: usize,
631 vertex_b: usize,
632 face_count: usize,
633 },
634 InconsistentWinding {
636 face_index: usize,
637 neighbor_index: usize,
638 },
639 SelfIntersection { face_a: usize, face_b: usize },
641}
642
643impl ValidationIssue {
644 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
695pub enum IssueSeverity {
696 Info,
698 Warning,
700 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}