axiolid_topology/
audit.rs1use std::collections::BTreeMap;
11
12use crate::{BRep, Orientation};
13
14#[derive(Debug, Clone, PartialEq, Eq, Default)]
16#[non_exhaustive]
17pub struct BRepHealth {
18 pub dangling_references: usize,
20 pub empty_loops: usize,
22 pub open_loops: usize,
24 pub faces_without_outer_bound: usize,
26 pub faces_with_multiple_outer_bounds: usize,
28 pub unpaired_edge_uses: usize,
30 pub overused_edges: usize,
32 pub false_closure_claims: usize,
34}
35
36impl BRepHealth {
37 pub fn is_tessellable(&self) -> bool {
45 self.dangling_references == 0
46 && self.empty_loops == 0
47 && self.open_loops == 0
48 && self.faces_without_outer_bound == 0
49 && self.faces_with_multiple_outer_bounds == 0
50 }
51
52 pub fn is_closed_manifold(&self) -> bool {
55 self.is_tessellable()
56 && self.unpaired_edge_uses == 0
57 && self.overused_edges == 0
58 && self.false_closure_claims == 0
59 }
60}
61
62#[must_use]
67pub fn audit_brep<Curve3, Curve2, Surface>(brep: &BRep<Curve3, Curve2, Surface>) -> BRepHealth {
68 let mut health = BRepHealth::default();
69 let vertices = brep.vertices().len();
70 let edges = brep.edges().len();
71 let loops = brep.loops().len();
72 let faces = brep.faces().len();
73 let shells = brep.shells().len();
74
75 for edge in brep.edges() {
76 if edge.start.index() >= vertices || edge.end.index() >= vertices {
77 health.dangling_references += 1;
78 }
79 }
80
81 for lp in brep.loops() {
85 if lp.edges.is_empty() {
86 health.empty_loops += 1;
87 continue;
88 }
89 let mut open = false;
90 for (k, use_) in lp.edges.iter().enumerate() {
91 if use_.edge.index() >= edges {
92 health.dangling_references += 1;
93 open = true;
94 continue;
95 }
96 let next = &lp.edges[(k + 1) % lp.edges.len()];
97 if next.edge.index() >= edges {
98 continue;
99 }
100 let head = endpoints(brep, use_).1;
101 let tail = endpoints(brep, next).0;
102 if head != tail {
103 open = true;
104 }
105 }
106 if open {
107 health.open_loops += 1;
108 }
109 }
110
111 for face in brep.faces() {
112 match face.bounds.iter().filter(|bound| bound.outer).count() {
113 0 => health.faces_without_outer_bound += 1,
114 1 => {}
115 _ => health.faces_with_multiple_outer_bounds += 1,
116 }
117 for bound in &face.bounds {
118 if bound.loop_id.index() >= loops {
119 health.dangling_references += 1;
120 }
121 }
122 }
123
124 for shell in brep.shells() {
130 let mut balance: BTreeMap<usize, i32> = BTreeMap::new();
131 let mut uses: BTreeMap<usize, usize> = BTreeMap::new();
132 for &(face_id, shell_sense) in &shell.faces {
133 if face_id.index() >= faces {
134 health.dangling_references += 1;
135 continue;
136 }
137 let face = &brep.faces()[face_id.index()];
138 let flip = (shell_sense == Orientation::Reversed)
139 ^ (face.orientation == Orientation::Reversed);
140 for bound in &face.bounds {
141 if bound.loop_id.index() >= loops {
142 continue;
143 }
144 for use_ in &brep.loops()[bound.loop_id.index()].edges {
145 if use_.edge.index() >= edges {
146 continue;
147 }
148 let mut forward = use_.orientation == Orientation::Forward;
149 if flip {
150 forward = !forward;
151 }
152 if bound.orientation == Orientation::Reversed {
153 forward = !forward;
154 }
155 *balance.entry(use_.edge.index()).or_default() += if forward { 1 } else { -1 };
156 *uses.entry(use_.edge.index()).or_default() += 1;
157 }
158 }
159 }
160 let unpaired = balance.values().filter(|v| **v != 0).count();
161 let overused = uses.values().filter(|c| **c > 2).count();
162 health.unpaired_edge_uses += unpaired;
163 health.overused_edges += overused;
164 if shell.closed && (unpaired > 0 || overused > 0) {
165 health.false_closure_claims += 1;
166 }
167 }
168 let _ = shells;
169 health
170}
171
172fn endpoints<Curve3, Curve2, Surface>(
174 brep: &BRep<Curve3, Curve2, Surface>,
175 use_: &crate::EdgeUse<Curve2>,
176) -> (crate::VertexId, crate::VertexId) {
177 let edge = &brep.edges()[use_.edge.index()];
178 match use_.orientation {
179 Orientation::Forward => (edge.start, edge.end),
180 _ => (edge.end, edge.start),
181 }
182}