1use std::collections::BTreeMap;
8use std::fmt;
9
10use axiolid_core::Tolerance;
11
12use crate::TriangleMeshView;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct MeshHealth {
17 pub positions: usize,
19 pub triangles: usize,
21 pub usable_triangles: usize,
23 pub invalid_indices: usize,
25 pub non_finite_positions: usize,
27 pub degenerate_triangles: usize,
29 pub boundary_edges: usize,
31 pub non_manifold_edges: usize,
33 pub inconsistent_winding_edges: usize,
37 pub first_invalid_index: Option<(usize, u64)>,
39 pub first_non_finite_position: Option<usize>,
41}
42
43impl MeshHealth {
44 pub fn is_surface_usable(&self) -> bool {
46 self.usable_triangles > 0 && self.invalid_indices == 0 && self.non_finite_positions == 0
47 }
48
49 pub fn is_closed_two_manifold(&self) -> bool {
51 self.is_surface_usable()
52 && self.degenerate_triangles == 0
53 && self.boundary_edges == 0
54 && self.non_manifold_edges == 0
55 && self.inconsistent_winding_edges == 0
56 }
57}
58
59#[derive(Debug)]
61pub enum MeshAuditError {
62 CapacityOverflow,
64 Allocation(std::collections::TryReserveError),
66}
67
68impl fmt::Display for MeshAuditError {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 Self::CapacityOverflow => formatter.write_str("mesh audit edge count overflowed"),
72 Self::Allocation(error) => write!(formatter, "mesh audit allocation failed: {error}"),
73 }
74 }
75}
76
77impl std::error::Error for MeshAuditError {
78 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
79 match self {
80 Self::CapacityOverflow => None,
81 Self::Allocation(error) => Some(error),
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy)]
87struct EdgeRecord {
88 low: u64,
89 high: u64,
90 direction: i8,
91}
92
93impl EdgeRecord {
94 const EMPTY: Self = Self {
97 low: 0,
98 high: 0,
99 direction: 0,
100 };
101}
102
103pub const fn audit_mesh_scratch_bytes(triangle_count: usize) -> Option<usize> {
111 match triangle_count.checked_mul(3) {
112 Some(edges) => match edges.checked_mul(std::mem::size_of::<EdgeRecord>()) {
113 Some(bytes) => bytes.checked_mul(2),
114 None => None,
115 },
116 None => None,
117 }
118}
119
120#[derive(Debug, Default)]
121struct EdgeSummary {
122 boundary: usize,
123 non_manifold: usize,
124 inconsistent_winding: usize,
125}
126
127trait EdgeSink {
128 fn record(&mut self, low: u64, high: u64, direction: i8);
129 fn summarize(&mut self) -> EdgeSummary;
130}
131
132#[derive(Debug)]
133struct VecEdgeSink {
134 edges: Vec<EdgeRecord>,
135 scratch: Vec<EdgeRecord>,
138 buckets: usize,
141}
142
143impl VecEdgeSink {
144 fn try_new(triangle_count: usize, positions: usize) -> Result<Self, MeshAuditError> {
145 let count = triangle_count
146 .checked_mul(3)
147 .ok_or(MeshAuditError::CapacityOverflow)?;
148 let mut edges = Vec::new();
149 edges
150 .try_reserve_exact(count)
151 .map_err(MeshAuditError::Allocation)?;
152 let dense = positions <= count.saturating_mul(2).max(1024);
159 let fits = u32::try_from(count).is_ok();
160 let mut scratch = Vec::new();
161 let buckets = if dense && fits {
162 match scratch.try_reserve_exact(count) {
163 Ok(()) => {
164 scratch.resize(count, EdgeRecord::EMPTY);
165 positions
166 }
167 Err(_) => 0,
170 }
171 } else {
172 0
173 };
174 Ok(Self {
175 edges,
176 scratch,
177 buckets,
178 })
179 }
180}
181
182fn counting_sort_edges(edges: &mut Vec<EdgeRecord>, scratch: &mut Vec<EdgeRecord>, buckets: usize) {
195 debug_assert_eq!(scratch.len(), edges.len());
196 let mut counts: Vec<u32> = Vec::new();
197 for pass in 0..2 {
198 counts.clear();
199 counts.resize(buckets + 2, 0);
200 for edge in edges.iter() {
201 let key = if pass == 0 { edge.high } else { edge.low } as usize;
202 counts[key + 1] += 1;
203 }
204 for index in 0..=buckets {
205 counts[index + 1] += counts[index];
206 }
207 for edge in edges.iter() {
208 let key = if pass == 0 { edge.high } else { edge.low } as usize;
209 scratch[counts[key] as usize] = *edge;
210 counts[key] += 1;
211 }
212 std::mem::swap(edges, scratch);
213 }
214}
215
216impl EdgeSink for VecEdgeSink {
217 fn record(&mut self, low: u64, high: u64, direction: i8) {
218 self.edges.push(EdgeRecord {
219 low,
220 high,
221 direction,
222 });
223 }
224
225 fn summarize(&mut self) -> EdgeSummary {
226 if self.buckets > 0 && self.scratch.len() == self.edges.len() {
227 counting_sort_edges(&mut self.edges, &mut self.scratch, self.buckets);
228 } else {
229 self.edges
230 .sort_unstable_by_key(|edge| (edge.low, edge.high));
231 }
232 let mut summary = EdgeSummary::default();
233 let mut start = 0;
234 while start < self.edges.len() {
235 let key = (self.edges[start].low, self.edges[start].high);
236 let mut end = start + 1;
237 let mut winding = i128::from(self.edges[start].direction);
238 while end < self.edges.len() && (self.edges[end].low, self.edges[end].high) == key {
239 winding += i128::from(self.edges[end].direction);
240 end += 1;
241 }
242 match end - start {
243 1 => summary.boundary += 1,
244 2 if winding != 0 => summary.inconsistent_winding += 1,
245 count if count > 2 => summary.non_manifold += 1,
246 _ => {}
247 }
248 start = end;
249 }
250 summary
251 }
252}
253
254#[derive(Debug, Default)]
255struct MapEdgeSink {
256 edges: BTreeMap<(u64, u64), (usize, i128)>,
257}
258
259impl EdgeSink for MapEdgeSink {
260 fn record(&mut self, low: u64, high: u64, direction: i8) {
261 let entry = self.edges.entry((low, high)).or_default();
262 entry.0 = entry.0.saturating_add(1);
263 entry.1 += i128::from(direction);
264 }
265
266 fn summarize(&mut self) -> EdgeSummary {
267 EdgeSummary {
268 boundary: self
269 .edges
270 .values()
271 .filter(|&&(count, _)| count == 1)
272 .count(),
273 non_manifold: self.edges.values().filter(|&&(count, _)| count > 2).count(),
274 inconsistent_winding: self
275 .edges
276 .values()
277 .filter(|&&(count, winding)| count == 2 && winding != 0)
278 .count(),
279 }
280 }
281}
282
283pub fn audit_mesh<M: TriangleMeshView + ?Sized>(mesh: &M, tolerance: Tolerance) -> MeshHealth {
294 match VecEdgeSink::try_new(mesh.triangle_count(), mesh.position_count()) {
295 Ok(edges) => audit_with_edges(mesh, tolerance, edges),
296 Err(_) => audit_with_edges(mesh, tolerance, MapEdgeSink::default()),
297 }
298}
299
300pub fn try_audit_mesh<M: TriangleMeshView + ?Sized>(
305 mesh: &M,
306 tolerance: Tolerance,
307) -> Result<MeshHealth, MeshAuditError> {
308 let edges = VecEdgeSink::try_new(mesh.triangle_count(), mesh.position_count())?;
309 Ok(audit_with_edges(mesh, tolerance, edges))
310}
311
312fn audit_with_edges<M: TriangleMeshView + ?Sized, E: EdgeSink>(
313 mesh: &M,
314 tolerance: Tolerance,
315 mut edges: E,
316) -> MeshHealth {
317 let positions = mesh.position_count();
318 let triangles = mesh.triangle_count();
319 let first_non_finite_position = (0..positions).find(|&index| !mesh.position(index).is_finite());
320 let non_finite_positions = (0..positions)
321 .filter(|&index| !mesh.position(index).is_finite())
322 .count();
323 let mut invalid_indices = 0;
324 let mut first_invalid_index = None;
325 let mut degenerate_triangles = 0;
326 let mut usable_triangles = 0;
327 let squared_double_area_limit = tolerance.linear().powi(4);
328
329 for triangle_index in 0..triangles {
330 let triangle = mesh.triangle(triangle_index);
331 let converted = triangle.map(|source_index| usize::try_from(source_index).ok());
332 let [Some(a_index), Some(b_index), Some(c_index)] = converted else {
333 for source_index in triangle {
334 if usize::try_from(source_index).is_err() {
335 invalid_indices += 1;
336 first_invalid_index.get_or_insert((triangle_index, source_index));
337 }
338 }
339 continue;
340 };
341 let indices = [a_index, b_index, c_index];
342 let mut valid = true;
343 for (corner, &index) in indices.iter().enumerate() {
344 if index >= positions {
345 invalid_indices += 1;
346 first_invalid_index.get_or_insert((triangle_index, triangle[corner]));
347 valid = false;
348 }
349 }
350 if !valid {
351 continue;
352 }
353
354 let [a, b, c] = indices.map(|index| mesh.position(index));
355 if !a.is_finite() || !b.is_finite() || !c.is_finite() {
356 continue;
357 }
358 let squared_double_area = (b - a).cross(c - a).length_squared();
359 if squared_double_area <= squared_double_area_limit {
360 degenerate_triangles += 1;
361 continue;
362 }
363 usable_triangles += 1;
364 for (left, right) in [
365 (triangle[0], triangle[1]),
366 (triangle[1], triangle[2]),
367 (triangle[2], triangle[0]),
368 ] {
369 let direction = if left < right { 1 } else { -1 };
370 edges.record(left.min(right), left.max(right), direction);
371 }
372 }
373
374 let edge_summary = edges.summarize();
375 MeshHealth {
376 positions,
377 triangles,
378 usable_triangles,
379 invalid_indices,
380 non_finite_positions,
381 degenerate_triangles,
382 boundary_edges: edge_summary.boundary,
383 non_manifold_edges: edge_summary.non_manifold,
384 inconsistent_winding_edges: edge_summary.inconsistent_winding,
385 first_invalid_index,
386 first_non_finite_position,
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use crate::TriMesh;
394 use axiolid_core::Point3;
395
396 #[test]
400 fn scratch_bound_charges_two_buffers_of_three_edge_records_per_triangle() {
401 assert_eq!(
402 audit_mesh_scratch_bytes(7),
403 7usize
404 .checked_mul(3)
405 .and_then(|count| count.checked_mul(std::mem::size_of::<EdgeRecord>()))
406 .and_then(|bytes| bytes.checked_mul(2))
407 );
408 let sink = VecEdgeSink::try_new(7, 16).expect("small bounded audit allocation");
409 assert!(sink.edges.capacity() >= 21);
410 }
411
412 #[test]
417 fn counting_sort_matches_comparison_sort() {
418 let buckets = 64usize;
419 let mut seed = 0x9E3779B97F4A7C15u64;
420 let mut next = move || {
421 seed ^= seed << 13;
422 seed ^= seed >> 7;
423 seed ^= seed << 17;
424 seed
425 };
426 let mut edges: Vec<EdgeRecord> = (0..4096)
427 .map(|_| {
428 let a = (next() as usize % buckets) as u64;
429 let b = (next() as usize % buckets) as u64;
430 EdgeRecord {
431 low: a.min(b),
432 high: a.max(b),
433 direction: if next() % 2 == 0 { 1 } else { -1 },
434 }
435 })
436 .collect();
437 let mut expected = edges.clone();
438 expected.sort_unstable_by_key(|e| (e.low, e.high));
439
440 let mut scratch = vec![EdgeRecord::EMPTY; edges.len()];
441 counting_sort_edges(&mut edges, &mut scratch, buckets);
442
443 let keys: Vec<_> = edges.iter().map(|e| (e.low, e.high)).collect();
444 let want: Vec<_> = expected.iter().map(|e| (e.low, e.high)).collect();
445 assert_eq!(
446 keys, want,
447 "counting sort must reproduce the comparison order"
448 );
449 }
450
451 #[test]
455 fn both_sinks_agree_on_a_defective_mesh() {
456 let positions = vec![
459 Point3::new(0.0, 0.0, 0.0),
460 Point3::new(1.0, 0.0, 0.0),
461 Point3::new(0.0, 1.0, 0.0),
462 Point3::new(0.0, 0.0, 1.0),
463 Point3::new(0.0, -1.0, 0.0),
464 ];
465 let indices = vec![0, 1, 2, 0, 1, 3, 0, 1, 4];
466 let mesh = TriMesh::new(positions, indices);
467
468 let tolerance = Tolerance::MILLIMETRE;
469 let fast = VecEdgeSink::try_new(mesh.triangle_count(), mesh.position_count())
470 .expect("fixture allocation");
471 assert!(
472 fast.buckets > 0,
473 "counting sort must be active for this fixture"
474 );
475 let via_counting = audit_with_edges(&mesh, tolerance, fast);
476 let via_map = audit_with_edges(&mesh, tolerance, MapEdgeSink::default());
477 assert_eq!(via_counting, via_map, "sinks must report identical health");
478 }
479}