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 unbalanced: usize,
127 first_unbalanced: Option<(u64, u64)>,
129}
130
131trait EdgeSink {
132 fn record(&mut self, low: u64, high: u64, direction: i8);
133 fn summarize(&mut self) -> EdgeSummary;
134}
135
136#[derive(Debug)]
137struct VecEdgeSink {
138 edges: Vec<EdgeRecord>,
139 scratch: Vec<EdgeRecord>,
142 buckets: usize,
145}
146
147impl VecEdgeSink {
148 fn try_new(triangle_count: usize, positions: usize) -> Result<Self, MeshAuditError> {
149 let count = triangle_count
150 .checked_mul(3)
151 .ok_or(MeshAuditError::CapacityOverflow)?;
152 let mut edges = Vec::new();
153 edges
154 .try_reserve_exact(count)
155 .map_err(MeshAuditError::Allocation)?;
156 let dense = positions <= count.saturating_mul(2).max(1024);
163 let fits = u32::try_from(count).is_ok();
164 let mut scratch = Vec::new();
165 let buckets = if dense && fits {
166 match scratch.try_reserve_exact(count) {
167 Ok(()) => {
168 scratch.resize(count, EdgeRecord::EMPTY);
169 positions
170 }
171 Err(_) => 0,
174 }
175 } else {
176 0
177 };
178 Ok(Self {
179 edges,
180 scratch,
181 buckets,
182 })
183 }
184}
185
186fn counting_sort_edges(edges: &mut Vec<EdgeRecord>, scratch: &mut Vec<EdgeRecord>, buckets: usize) {
199 debug_assert_eq!(scratch.len(), edges.len());
200 let mut counts: Vec<u32> = Vec::new();
201 for pass in 0..2 {
202 counts.clear();
203 counts.resize(buckets + 2, 0);
204 for edge in edges.iter() {
205 let key = if pass == 0 { edge.high } else { edge.low } as usize;
206 counts[key + 1] += 1;
207 }
208 for index in 0..=buckets {
209 counts[index + 1] += counts[index];
210 }
211 for edge in edges.iter() {
212 let key = if pass == 0 { edge.high } else { edge.low } as usize;
213 scratch[counts[key] as usize] = *edge;
214 counts[key] += 1;
215 }
216 std::mem::swap(edges, scratch);
217 }
218}
219
220impl EdgeSink for VecEdgeSink {
221 fn record(&mut self, low: u64, high: u64, direction: i8) {
222 self.edges.push(EdgeRecord {
223 low,
224 high,
225 direction,
226 });
227 }
228
229 fn summarize(&mut self) -> EdgeSummary {
230 if self.buckets > 0 && self.scratch.len() == self.edges.len() {
231 counting_sort_edges(&mut self.edges, &mut self.scratch, self.buckets);
232 } else {
233 self.edges
234 .sort_unstable_by_key(|edge| (edge.low, edge.high));
235 }
236 let mut summary = EdgeSummary::default();
237 let mut start = 0;
238 while start < self.edges.len() {
239 let key = (self.edges[start].low, self.edges[start].high);
240 let mut end = start + 1;
241 let mut winding = i128::from(self.edges[start].direction);
242 while end < self.edges.len() && (self.edges[end].low, self.edges[end].high) == key {
243 winding += i128::from(self.edges[end].direction);
244 end += 1;
245 }
246 if winding != 0 {
247 summary.unbalanced += 1;
248 summary.first_unbalanced.get_or_insert(key);
249 }
250 match end - start {
251 1 => summary.boundary += 1,
252 2 if winding != 0 => summary.inconsistent_winding += 1,
253 count if count > 2 => summary.non_manifold += 1,
254 _ => {}
255 }
256 start = end;
257 }
258 summary
259 }
260}
261
262#[derive(Debug, Default)]
263struct MapEdgeSink {
264 edges: BTreeMap<(u64, u64), (usize, i128)>,
265}
266
267impl EdgeSink for MapEdgeSink {
268 fn record(&mut self, low: u64, high: u64, direction: i8) {
269 let entry = self.edges.entry((low, high)).or_default();
270 entry.0 = entry.0.saturating_add(1);
271 entry.1 += i128::from(direction);
272 }
273
274 fn summarize(&mut self) -> EdgeSummary {
275 let mut unbalanced = self.edges.iter().filter(|(_, &(_, winding))| winding != 0);
276 let first_unbalanced = unbalanced.next().map(|(&key, _)| key);
277 let unbalanced = usize::from(first_unbalanced.is_some()) + unbalanced.count();
278 EdgeSummary {
279 unbalanced,
280 first_unbalanced,
281 boundary: self
282 .edges
283 .values()
284 .filter(|&&(count, _)| count == 1)
285 .count(),
286 non_manifold: self.edges.values().filter(|&&(count, _)| count > 2).count(),
287 inconsistent_winding: self
288 .edges
289 .values()
290 .filter(|&&(count, winding)| count == 2 && winding != 0)
291 .count(),
292 }
293 }
294}
295
296pub fn audit_mesh<M: TriangleMeshView + ?Sized>(mesh: &M, tolerance: Tolerance) -> MeshHealth {
307 audit_mesh_cycle(mesh, tolerance).health
308}
309
310pub fn try_audit_mesh<M: TriangleMeshView + ?Sized>(
315 mesh: &M,
316 tolerance: Tolerance,
317) -> Result<MeshHealth, MeshAuditError> {
318 try_audit_mesh_cycle(mesh, tolerance).map(|audit| audit.health)
319}
320
321#[non_exhaustive]
331#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct MeshCycleHealth {
333 pub health: MeshHealth,
335 pub unbalanced_edges: usize,
337 pub first_unbalanced_edge: Option<(u64, u64)>,
339}
340
341impl MeshCycleHealth {
342 pub fn is_closed_oriented_cycle(&self) -> bool {
349 self.health.is_surface_usable()
350 && self.health.degenerate_triangles == 0
351 && self.unbalanced_edges == 0
352 }
353}
354
355pub fn audit_mesh_cycle<M: TriangleMeshView + ?Sized>(
360 mesh: &M,
361 tolerance: Tolerance,
362) -> MeshCycleHealth {
363 match VecEdgeSink::try_new(mesh.triangle_count(), mesh.position_count()) {
364 Ok(edges) => audit_with_edges(mesh, tolerance, edges),
365 Err(_) => audit_with_edges(mesh, tolerance, MapEdgeSink::default()),
366 }
367}
368
369pub fn try_audit_mesh_cycle<M: TriangleMeshView + ?Sized>(
372 mesh: &M,
373 tolerance: Tolerance,
374) -> Result<MeshCycleHealth, MeshAuditError> {
375 let edges = VecEdgeSink::try_new(mesh.triangle_count(), mesh.position_count())?;
376 Ok(audit_with_edges(mesh, tolerance, edges))
377}
378
379fn audit_with_edges<M: TriangleMeshView + ?Sized, E: EdgeSink>(
380 mesh: &M,
381 tolerance: Tolerance,
382 mut edges: E,
383) -> MeshCycleHealth {
384 let positions = mesh.position_count();
385 let triangles = mesh.triangle_count();
386 let first_non_finite_position = (0..positions).find(|&index| !mesh.position(index).is_finite());
387 let non_finite_positions = (0..positions)
388 .filter(|&index| !mesh.position(index).is_finite())
389 .count();
390 let mut invalid_indices = 0;
391 let mut first_invalid_index = None;
392 let mut degenerate_triangles = 0;
393 let mut usable_triangles = 0;
394 let squared_double_area_limit = tolerance.linear().powi(4);
395
396 for triangle_index in 0..triangles {
397 let triangle = mesh.triangle(triangle_index);
398 let converted = triangle.map(|source_index| usize::try_from(source_index).ok());
399 let [Some(a_index), Some(b_index), Some(c_index)] = converted else {
400 for source_index in triangle {
401 if usize::try_from(source_index).is_err() {
402 invalid_indices += 1;
403 first_invalid_index.get_or_insert((triangle_index, source_index));
404 }
405 }
406 continue;
407 };
408 let indices = [a_index, b_index, c_index];
409 let mut valid = true;
410 for (corner, &index) in indices.iter().enumerate() {
411 if index >= positions {
412 invalid_indices += 1;
413 first_invalid_index.get_or_insert((triangle_index, triangle[corner]));
414 valid = false;
415 }
416 }
417 if !valid {
418 continue;
419 }
420
421 let [a, b, c] = indices.map(|index| mesh.position(index));
422 if !a.is_finite() || !b.is_finite() || !c.is_finite() {
423 continue;
424 }
425 let squared_double_area = (b - a).cross(c - a).length_squared();
426 if squared_double_area <= squared_double_area_limit {
427 degenerate_triangles += 1;
428 continue;
429 }
430 usable_triangles += 1;
431 for (left, right) in [
432 (triangle[0], triangle[1]),
433 (triangle[1], triangle[2]),
434 (triangle[2], triangle[0]),
435 ] {
436 let direction = if left < right { 1 } else { -1 };
437 edges.record(left.min(right), left.max(right), direction);
438 }
439 }
440
441 let edge_summary = edges.summarize();
442 MeshCycleHealth {
443 health: MeshHealth {
444 positions,
445 triangles,
446 usable_triangles,
447 invalid_indices,
448 non_finite_positions,
449 degenerate_triangles,
450 boundary_edges: edge_summary.boundary,
451 non_manifold_edges: edge_summary.non_manifold,
452 inconsistent_winding_edges: edge_summary.inconsistent_winding,
453 first_invalid_index,
454 first_non_finite_position,
455 },
456 unbalanced_edges: edge_summary.unbalanced,
457 first_unbalanced_edge: edge_summary.first_unbalanced,
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::TriMesh;
465 use axiolid_core::Point3;
466
467 #[test]
471 fn scratch_bound_charges_two_buffers_of_three_edge_records_per_triangle() {
472 assert_eq!(
473 audit_mesh_scratch_bytes(7),
474 7usize
475 .checked_mul(3)
476 .and_then(|count| count.checked_mul(std::mem::size_of::<EdgeRecord>()))
477 .and_then(|bytes| bytes.checked_mul(2))
478 );
479 let sink = VecEdgeSink::try_new(7, 16).expect("small bounded audit allocation");
480 assert!(sink.edges.capacity() >= 21);
481 }
482
483 #[test]
488 fn counting_sort_matches_comparison_sort() {
489 let buckets = 64usize;
490 let mut seed = 0x9E3779B97F4A7C15u64;
491 let mut next = move || {
492 seed ^= seed << 13;
493 seed ^= seed >> 7;
494 seed ^= seed << 17;
495 seed
496 };
497 let mut edges: Vec<EdgeRecord> = (0..4096)
498 .map(|_| {
499 let a = (next() as usize % buckets) as u64;
500 let b = (next() as usize % buckets) as u64;
501 EdgeRecord {
502 low: a.min(b),
503 high: a.max(b),
504 direction: if next() % 2 == 0 { 1 } else { -1 },
505 }
506 })
507 .collect();
508 let mut expected = edges.clone();
509 expected.sort_unstable_by_key(|e| (e.low, e.high));
510
511 let mut scratch = vec![EdgeRecord::EMPTY; edges.len()];
512 counting_sort_edges(&mut edges, &mut scratch, buckets);
513
514 let keys: Vec<_> = edges.iter().map(|e| (e.low, e.high)).collect();
515 let want: Vec<_> = expected.iter().map(|e| (e.low, e.high)).collect();
516 assert_eq!(
517 keys, want,
518 "counting sort must reproduce the comparison order"
519 );
520 }
521
522 #[test]
526 fn both_sinks_agree_on_a_defective_mesh() {
527 let positions = vec![
530 Point3::new(0.0, 0.0, 0.0),
531 Point3::new(1.0, 0.0, 0.0),
532 Point3::new(0.0, 1.0, 0.0),
533 Point3::new(0.0, 0.0, 1.0),
534 Point3::new(0.0, -1.0, 0.0),
535 ];
536 let indices = vec![0, 1, 2, 0, 1, 3, 0, 1, 4];
537 let mesh = TriMesh::new(positions, indices);
538
539 let tolerance = Tolerance::MILLIMETRE;
540 let fast = VecEdgeSink::try_new(mesh.triangle_count(), mesh.position_count())
541 .expect("fixture allocation");
542 assert!(
543 fast.buckets > 0,
544 "counting sort must be active for this fixture"
545 );
546 let via_counting = audit_with_edges(&mesh, tolerance, fast);
547 let via_map = audit_with_edges(&mesh, tolerance, MapEdgeSink::default());
548 assert_eq!(via_counting, via_map, "sinks must report identical health");
549 assert!(via_counting.unbalanced_edges > 0);
550 assert!(via_counting.first_unbalanced_edge.is_some());
551 }
552}