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}