axiolid_brep_audit/report.rs
1//! What a geometric audit found.
2
3use axiolid_core::Scalar;
4
5/// One geometric inconsistency, with enough detail to locate it.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum GeometricDefect {
8 /// An edge endpoint does not lie on the edge's own 3D curve.
9 VertexOffCurve {
10 /// Index of the offending edge.
11 edge: usize,
12 /// Distance from the vertex to the curve point, in model units.
13 error: Scalar,
14 },
15 /// A pcurve, lifted through its face surface, does not follow the edge.
16 ///
17 /// This is the defect a purely topological audit cannot see: the trim
18 /// curve resolves and the loop closes, but the face boundary runs
19 /// somewhere other than the edge it claims to trim.
20 PcurveOffCurve {
21 /// Index of the owning loop.
22 loop_id: usize,
23 /// Position of the edge use within that loop.
24 use_index: usize,
25 /// Worst sampled deviation, in model units.
26 error: Scalar,
27 },
28 /// An edge's 3D curve could not be evaluated on its stated interval.
29 UnevaluableCurve {
30 /// Index of the offending edge.
31 edge: usize,
32 },
33 /// A pcurve could not be evaluated on its stated interval.
34 UnevaluablePcurve {
35 /// Index of the owning loop.
36 loop_id: usize,
37 /// Position of the edge use within that loop.
38 use_index: usize,
39 },
40 /// A face surface could not be evaluated at a pcurve sample.
41 UnevaluableSurface {
42 /// Index of the owning loop.
43 loop_id: usize,
44 /// Position of the edge use within that loop.
45 use_index: usize,
46 },
47}
48
49/// Result of a geometric audit.
50///
51/// Unlike the topological audit this is tolerance-dependent, so a clean
52/// result is a statement about agreement AT A TOLERANCE, not an absolute one.
53#[derive(Debug, Clone, Default, PartialEq)]
54pub struct GeometricHealth {
55 defects: Vec<GeometricDefect>,
56}
57
58impl GeometricHealth {
59 /// Record one defect.
60 pub(crate) fn push(&mut self, defect: GeometricDefect) {
61 self.defects.push(defect);
62 }
63
64 /// Every defect found, in discovery order.
65 #[must_use]
66 pub fn defects(&self) -> &[GeometricDefect] {
67 &self.defects
68 }
69
70 /// Whether the geometry is consistent at the audited tolerance.
71 #[must_use]
72 pub fn is_consistent(&self) -> bool {
73 self.defects.is_empty()
74 }
75
76 /// The largest positional disagreement found, if any was measured.
77 ///
78 /// Useful for reporting how far off a rejected solid was, rather than
79 /// only that it was rejected.
80 #[must_use]
81 pub fn worst_error(&self) -> Option<Scalar> {
82 self.defects
83 .iter()
84 .filter_map(|defect| match defect {
85 GeometricDefect::VertexOffCurve { error, .. }
86 | GeometricDefect::PcurveOffCurve { error, .. } => Some(*error),
87 _ => None,
88 })
89 .max_by(Scalar::total_cmp)
90 }
91}