1#![forbid(unsafe_code)]
26
27mod report;
28
29pub use report::{GeometricDefect, GeometricHealth};
30
31use axiolid_brep::ExactBRep;
32use axiolid_core::{Point3, Scalar, Tolerance};
33use axiolid_evaluate::{curve, surface};
34use axiolid_topology::Orientation;
35
36#[must_use]
48pub fn geometric_audit(brep: &ExactBRep, tolerance: Tolerance) -> GeometricHealth {
49 let mut health = GeometricHealth::default();
50 check_vertices_on_curves(brep, tolerance, &mut health);
51 check_pcurves_against_curves(brep, tolerance, &mut health);
52 health
53}
54
55const SAMPLES: [Scalar; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
62
63fn follows_in_order(
69 pcurve: &axiolid_curve::Curve2,
70 pcurve_interval: axiolid_core::Interval,
71 support: &axiolid_surface::Surface,
72 curve3: &axiolid_curve::Curve3,
73 edge_interval: axiolid_core::Interval,
74 orientation: Orientation,
75 tolerance: Tolerance,
76) -> Scalar {
77 const DENSE: usize = 16;
78 let (lo, hi) = (
79 edge_interval.start.min(edge_interval.end),
80 edge_interval.start.max(edge_interval.end),
81 );
82 let periodic = matches!(
83 curve3,
84 axiolid_curve::Curve3::Circle(_) | axiolid_curve::Curve3::Ellipse(_)
85 );
86 let closed = match (curve::evaluate3(curve3, lo), curve::evaluate3(curve3, hi)) {
88 (Ok(a), Ok(b)) => distance(a, b) <= tolerance.linear(),
89 _ => false,
90 };
91 let slack = 1e-9 * (1.0 + lo.abs().max(hi.abs()));
92 let mut worst: Scalar = 0.0;
93 let mut previous: Option<Scalar> = None;
94 for i in 0..=DENSE {
95 let sample = i as Scalar / DENSE as Scalar;
96 let Ok(uv) = curve::evaluate2(pcurve, lerp(pcurve_interval, sample)) else {
97 return Scalar::INFINITY;
98 };
99 let Ok(lifted) = surface::evaluate(support, uv.x, uv.y) else {
100 return Scalar::INFINITY;
101 };
102 let Ok(t) = curve::locate3(curve3, lifted, tolerance) else {
103 return Scalar::INFINITY;
104 };
105 let candidates: &[Scalar] = if periodic {
107 &[0.0, 1.0, -1.0, 2.0]
108 } else {
109 &[0.0]
110 };
111 let Some(t) = candidates
112 .iter()
113 .map(|k| t + k * core::f64::consts::TAU)
114 .find(|t| *t >= lo - slack && *t <= hi + slack)
115 else {
116 return Scalar::INFINITY;
117 };
118 let along = (t - edge_interval.start) / (edge_interval.end - edge_interval.start);
120 let along = match orientation {
121 Orientation::Forward => along,
122 Orientation::Reversed => 1.0 - along,
123 };
124 let at_end = |target: Scalar| {
127 (along - target).abs() <= 1e-6 || (closed && (along - (1.0 - target)).abs() <= 1e-6)
128 };
129 if (i == 0 && !at_end(0.0)) || (i == DENSE && !at_end(1.0)) {
130 return Scalar::INFINITY;
131 }
132 if i > 0 && i < DENSE {
133 if let Some(last) = previous {
134 if along < last - 1e-9 {
135 return Scalar::INFINITY;
136 }
137 }
138 previous = Some(along);
139 } else if i == 0 {
140 previous = Some(0.0);
141 }
142 let Ok(on) = curve::evaluate3(curve3, t) else {
143 return Scalar::INFINITY;
144 };
145 worst = worst.max(distance(lifted, on));
146 }
147 worst
148}
149
150fn lerp(interval: axiolid_core::Interval, t: Scalar) -> Scalar {
151 interval.start + (interval.end - interval.start) * t
152}
153
154fn distance(a: Point3, b: Point3) -> Scalar {
155 (a - b).length()
156}
157
158fn check_vertices_on_curves(brep: &ExactBRep, tolerance: Tolerance, health: &mut GeometricHealth) {
160 let topology = brep.topology();
161 for (index, edge) in topology.edges().iter().enumerate() {
162 let Some(curve_id) = edge.curve else { continue };
163 let Some(curve3) = brep.curves3().get(curve_id.index()) else {
164 continue;
165 };
166 let Some(edge_id) = topology.edge_id_at(index) else {
167 continue;
168 };
169 let Some(interval) = brep.edge_interval(edge_id) else {
170 continue;
171 };
172 let vertices = topology.vertices();
173 let (Some(start), Some(end)) = (
174 vertices.get(edge.start.index()),
175 vertices.get(edge.end.index()),
176 ) else {
177 continue;
178 };
179 for (parameter, vertex) in [(interval.start, start), (interval.end, end)] {
180 let Ok(point) = curve::evaluate3(curve3, parameter) else {
181 health.push(GeometricDefect::UnevaluableCurve { edge: index });
182 continue;
183 };
184 let error = distance(point, vertex.position);
185 if error > tolerance.linear() {
186 health.push(GeometricDefect::VertexOffCurve { edge: index, error });
187 }
188 }
189 }
190}
191
192fn check_pcurves_against_curves(
194 brep: &ExactBRep,
195 tolerance: Tolerance,
196 health: &mut GeometricHealth,
197) {
198 let topology = brep.topology();
199 for face in topology.faces() {
200 let Some(surface_id) = face.surface else {
201 continue;
202 };
203 let Some(support) = brep.surfaces().get(surface_id.index()) else {
204 continue;
205 };
206 for bound in &face.bounds {
207 let Some(lp) = topology.loops().get(bound.loop_id.index()) else {
208 continue;
209 };
210 for (use_index, use_) in lp.edges.iter().enumerate() {
211 let Some(pcurve_id) = use_.pcurve else {
212 continue;
213 };
214 let Some(pcurve) = brep.curves2().get(pcurve_id.index()) else {
215 continue;
216 };
217 let Some(pcurve_interval) = brep.pcurve_interval(bound.loop_id, use_index) else {
218 continue;
219 };
220 let Some(edge) = topology.edges().get(use_.edge.index()) else {
221 continue;
222 };
223 let Some(curve_id) = edge.curve else { continue };
224 let Some(curve3) = brep.curves3().get(curve_id.index()) else {
225 continue;
226 };
227 let Some(edge_interval) = brep.edge_interval(use_.edge) else {
228 continue;
229 };
230
231 let mut worst: Scalar = 0.0;
232 for sample in SAMPLES {
233 let along = match use_.orientation {
238 Orientation::Forward => sample,
239 Orientation::Reversed => 1.0 - sample,
240 };
241 let parametric = lerp(pcurve_interval, sample);
242 let spatial = lerp(edge_interval, along);
243
244 let Ok(uv) = curve::evaluate2(pcurve, parametric) else {
245 health.push(GeometricDefect::UnevaluablePcurve {
246 loop_id: bound.loop_id.index(),
247 use_index,
248 });
249 break;
250 };
251 let Ok(lifted) = surface::evaluate(support, uv.x, uv.y) else {
252 health.push(GeometricDefect::UnevaluableSurface {
253 loop_id: bound.loop_id.index(),
254 use_index,
255 });
256 break;
257 };
258 let Ok(expected) = curve::evaluate3(curve3, spatial) else {
259 health.push(GeometricDefect::UnevaluableCurve {
260 edge: use_.edge.index(),
261 });
262 break;
263 };
264 let error = distance(lifted, expected);
269 if error > worst {
270 worst = error;
271 }
272 }
273 if worst > tolerance.linear()
278 && matches!(pcurve, axiolid_curve::Curve2::Implicit(_))
279 {
280 worst = follows_in_order(
281 pcurve,
282 pcurve_interval,
283 support,
284 curve3,
285 edge_interval,
286 use_.orientation,
287 tolerance,
288 );
289 }
290 if worst > tolerance.linear() {
291 health.push(GeometricDefect::PcurveOffCurve {
292 loop_id: bound.loop_id.index(),
293 use_index,
294 error: worst,
295 });
296 }
297 }
298 }
299 }
300}