axiolid_heal/diagnose.rs
1//! Producing a [`Diagnosis`] from a mesh (#73).
2//!
3//! # Why this exists
4//!
5//! `Diagnosis` and `DefectKind` were vocabulary: complete, well-named, and
6//! produced by nothing. `audit_mesh` counts defects but returns only counts,
7//! so a caller learned *how many* boundary edges a mesh had and never *which*.
8//! A repair cannot act on a count, and a report that cannot name what it
9//! found is not auditable.
10//!
11//! # Counts versus locations
12//!
13//! Where the audit knows a location it is recorded in `Defect::element`;
14//! where it knows only a count, the defect carries the count in `detail` and
15//! leaves `element` empty rather than inventing an index. Self-intersection
16//! is the one class that always has a location, because it is computed here
17//! rather than counted by the audit.
18
19use axiolid_core::Tolerance;
20use axiolid_mesh::{audit_mesh, TriangleMeshView};
21
22use crate::diagnosis::{Defect, DefectKind, Diagnosis};
23use crate::intersect::self_intersections;
24
25/// Diagnose a triangle mesh, reporting every defect class it exhibits.
26///
27/// Defects are returned in a deterministic order: structural classes in the
28/// audit's own order first, then self-intersecting pairs in sorted order. The
29/// result is empty exactly when the mesh is clean.
30#[must_use]
31pub fn diagnose<M: TriangleMeshView + ?Sized>(mesh: &M, tolerance: Tolerance) -> Diagnosis {
32 let health = audit_mesh(mesh, tolerance);
33 let mut defects = Vec::new();
34
35 if health.non_manifold_edges != 0 {
36 defects.push(counted(
37 DefectKind::NonManifoldEdge,
38 health.non_manifold_edges,
39 "edges used by other than two faces",
40 ));
41 }
42 if health.inconsistent_winding_edges != 0 {
43 defects.push(counted(
44 DefectKind::InconsistentOrientation,
45 health.inconsistent_winding_edges,
46 "edges whose adjacent faces disagree on direction",
47 ));
48 }
49 if health.boundary_edges != 0 {
50 defects.push(counted(
51 DefectKind::OpenShell,
52 health.boundary_edges,
53 "edges used by exactly one face",
54 ));
55 }
56 if health.degenerate_triangles != 0 {
57 defects.push(counted(
58 DefectKind::DegenerateElement,
59 health.degenerate_triangles,
60 "triangles with zero area",
61 ));
62 }
63
64 for pair in self_intersections(mesh) {
65 defects.push(Defect {
66 kind: DefectKind::SelfIntersection,
67 element: Some(pair.first),
68 detail: Some(format!("crosses triangle {}", pair.second)),
69 });
70 }
71
72 Diagnosis { defects }
73}
74
75/// A defect the audit can count but not locate.
76fn counted(kind: DefectKind, count: usize, what: &str) -> Defect {
77 Defect {
78 kind,
79 element: None,
80 detail: Some(format!("{count} {what}")),
81 }
82}