1use std::fmt;
4
5use fast_mesh_bvh::Bvh;
6use fast_mesh_store::{Mesh, TriangleId, VertexId, par};
7
8use fast_mesh_query::self_intersects;
9
10use crate::topology::{
11 BoundaryLoop, Component, EdgeMap, components, loops_with, pinch_vertices, point, triangle_area,
12};
13use crate::weld::{check_tolerance, weld_roots};
14use crate::{Result, filled, push, reserve};
15
16#[derive(Clone, Copy)]
18pub struct AnalyzeOptions<'a> {
19 pub weld_tolerance: f32,
22 pub self_intersection: Option<&'a Bvh>,
25 pub max_listed: usize,
28}
29
30impl Default for AnalyzeOptions<'_> {
31 fn default() -> Self {
32 AnalyzeOptions {
33 weld_tolerance: 0.0,
34 self_intersection: None,
35 max_listed: 1000,
36 }
37 }
38}
39
40impl fmt::Debug for AnalyzeOptions<'_> {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 f.debug_struct("AnalyzeOptions")
43 .field("weld_tolerance", &self.weld_tolerance)
44 .field("self_intersection", &self.self_intersection.map(|_| "Bvh"))
45 .field("max_listed", &self.max_listed)
46 .finish()
47 }
48}
49
50impl<'a> AnalyzeOptions<'a> {
51 pub fn builder() -> AnalyzeOptionsBuilder<'a> {
54 AnalyzeOptionsBuilder::default()
55 }
56}
57
58#[derive(Debug, Clone, Default)]
60pub struct AnalyzeOptionsBuilder<'a> {
61 options: AnalyzeOptions<'a>,
62}
63
64impl<'a> AnalyzeOptionsBuilder<'a> {
65 pub fn weld_tolerance(mut self, weld_tolerance: f32) -> Self {
67 self.options.weld_tolerance = weld_tolerance;
68 self
69 }
70
71 pub fn self_intersection(mut self, self_intersection: &'a Bvh) -> Self {
73 self.options.self_intersection = Some(self_intersection);
74 self
75 }
76
77 pub fn max_listed(mut self, max_listed: usize) -> Self {
79 self.options.max_listed = max_listed;
80 self
81 }
82
83 pub fn build(self) -> AnalyzeOptions<'a> {
85 self.options
86 }
87}
88
89#[derive(Debug, Clone, PartialEq)]
91pub struct Analysis {
92 pub loops: Vec<BoundaryLoop>,
95 pub coincident_groups: usize,
97 pub coincident: Vec<Vec<VertexId>>,
99 pub degenerate: usize,
101 pub degenerate_list: Vec<TriangleId>,
103 pub duplicate: usize,
105 pub duplicate_list: Vec<TriangleId>,
107 pub non_manifold_edges: usize,
109 pub non_manifold_edge_list: Vec<(VertexId, VertexId)>,
111 pub non_manifold_vertices: Vec<VertexId>,
113 pub pinch_vertices: Vec<VertexId>,
115 pub components: Vec<Component>,
117 pub orientation_conflicts: usize,
120 pub self_intersects: Option<bool>,
123}
124
125pub(crate) const AREA_EPSILON: f64 = 1e-12;
128
129pub(crate) const PAR_MIN_ANALYZE_TRIS: usize = 2048;
137
138pub fn analyze(mesh: &Mesh, options: &AnalyzeOptions) -> Result<Analysis> {
173 let max = options.max_listed;
174 check_tolerance(options.weld_tolerance)?;
176 let parallel = mesh.triangle_count() >= PAR_MIN_ANALYZE_TRIS;
180 let ((coincident, topology), (scans, self_intersects)) = par::join(
181 parallel,
182 || {
183 par::join(
184 parallel,
185 || groups(&weld_roots(mesh, options.weld_tolerance)?, max),
186 || topology_pass(mesh, max, parallel),
187 )
188 },
189 || {
190 par::join(
191 parallel,
192 || scan_pass(mesh, max),
193 || self_intersection_pass(mesh, options.self_intersection),
194 )
195 },
196 );
197 let (coincident_groups, coincident) = coincident?;
198 let topology = topology?;
199 let (degenerate, degenerate_list, duplicate, duplicate_list) = scans?;
200 let self_intersects = self_intersects?;
201
202 Ok(Analysis {
203 loops: topology.loops,
204 coincident_groups,
205 coincident,
206 degenerate,
207 degenerate_list,
208 duplicate,
209 duplicate_list,
210 non_manifold_edges: topology.non_manifold_edges,
211 non_manifold_edge_list: topology.non_manifold_edge_list,
212 non_manifold_vertices: topology.non_manifold_vertices,
213 pinch_vertices: topology.pinch_vertices,
214 components: topology.components,
215 orientation_conflicts: topology.orientation_conflicts,
216 self_intersects,
217 })
218}
219
220struct Topology {
222 loops: Vec<BoundaryLoop>,
223 non_manifold_edges: usize,
224 non_manifold_edge_list: Vec<(VertexId, VertexId)>,
225 non_manifold_vertices: Vec<VertexId>,
226 pinch_vertices: Vec<VertexId>,
227 components: Vec<Component>,
228 orientation_conflicts: usize,
229}
230
231fn topology_pass(mesh: &Mesh, max: usize, parallel: bool) -> Result<Topology> {
235 let edges = EdgeMap::build(mesh)?;
236 let (loops, (components, rest)) = par::join(
237 parallel,
238 || loops_with(mesh, &edges),
239 || {
240 par::join(
241 parallel,
242 || components(mesh, &edges),
243 || edge_kinds(mesh, &edges, max),
244 )
245 },
246 );
247 let (loops, components) = (loops?, components?);
248 let (pinch_vertices, non_manifold, orientation_conflicts) = rest?;
249 let (non_manifold_edges, non_manifold_edge_list, non_manifold_vertices) = non_manifold;
250 Ok(Topology {
251 loops,
252 non_manifold_edges,
253 non_manifold_edge_list,
254 non_manifold_vertices,
255 pinch_vertices,
256 components,
257 orientation_conflicts,
258 })
259}
260
261type NonManifold = (usize, Vec<(VertexId, VertexId)>, Vec<VertexId>);
263
264fn edge_kinds(
267 mesh: &Mesh,
268 edges: &EdgeMap,
269 max: usize,
270) -> Result<(Vec<VertexId>, NonManifold, usize)> {
271 let pinch_vertices = pinch_vertices(mesh, edges)?;
272 let mut non_manifold_edges = 0;
273 let mut non_manifold_edge_list = Vec::new();
274 let mut on_non_manifold = filled(mesh.vertex_slots(), false)?;
275 let mut orientation_conflicts = 0;
276 for ((a, b), tris) in edges.edges() {
277 match *tris {
278 [_] => {}
279 [s, t] => {
280 if runs_forward(mesh, s, a, b) == runs_forward(mesh, t, a, b) {
281 orientation_conflicts += 1;
282 }
283 }
284 _ => {
285 non_manifold_edges += 1;
286 if non_manifold_edge_list.len() < max {
287 push(&mut non_manifold_edge_list, (a, b))?;
288 }
289 on_non_manifold[a as usize] = true;
290 on_non_manifold[b as usize] = true;
291 }
292 }
293 }
294 let mut non_manifold_vertices = Vec::new();
295 for (v, &on) in on_non_manifold.iter().enumerate() {
296 if on {
297 push(&mut non_manifold_vertices, v as VertexId)?;
298 }
299 }
300 let non_manifold = (
301 non_manifold_edges,
302 non_manifold_edge_list,
303 non_manifold_vertices,
304 );
305 Ok((pinch_vertices, non_manifold, orientation_conflicts))
306}
307
308type Scans = (usize, Vec<TriangleId>, usize, Vec<TriangleId>);
310
311fn scan_pass(mesh: &Mesh, max: usize) -> Result<Scans> {
313 let mut degenerate = 0;
314 let mut degenerate_list = Vec::new();
315 for t in 0..mesh.triangle_slots() as TriangleId {
316 let Some(tri) = mesh.triangle(t) else {
317 continue;
318 };
319 if is_degenerate(mesh, tri, AREA_EPSILON) {
320 degenerate += 1;
321 if degenerate_list.len() < max {
322 push(&mut degenerate_list, t)?;
323 }
324 }
325 }
326 let mut duplicate_list = duplicate_triangles(mesh)?;
327 let duplicate = duplicate_list.len();
328 duplicate_list.truncate(max);
329 Ok((degenerate, degenerate_list, duplicate, duplicate_list))
330}
331
332fn self_intersection_pass(mesh: &Mesh, bvh: Option<&Bvh>) -> Result<Option<bool>> {
334 let Some(bvh) = bvh else {
335 return Ok(None);
336 };
337 Ok(Some(self_intersects(mesh, bvh)?))
338}
339
340pub(crate) fn duplicate_triangles(mesh: &Mesh) -> Result<Vec<TriangleId>> {
343 let mut sets: Vec<([VertexId; 3], TriangleId)> = Vec::new();
346 reserve(&mut sets, mesh.triangle_count())?;
347 for t in 0..mesh.triangle_slots() as TriangleId {
348 if let Some(mut key) = mesh.triangle(t) {
349 key.sort_unstable();
350 push(&mut sets, (key, t))?;
351 }
352 }
353 sets.sort_unstable();
354 let mut out = Vec::new();
355 for w in sets.windows(2) {
356 if w[0].0 == w[1].0 {
357 push(&mut out, w[1].1)?;
358 }
359 }
360 out.sort_unstable();
361 Ok(out)
362}
363
364fn runs_forward(mesh: &Mesh, t: TriangleId, a: VertexId, b: VertexId) -> bool {
366 let [x, y, z] = mesh.triangles()[t as usize];
367 (x, y) == (a, b) || (y, z) == (a, b) || (z, x) == (a, b)
368}
369
370pub(crate) fn is_degenerate(mesh: &Mesh, [a, b, c]: [VertexId; 3], area_epsilon: f64) -> bool {
373 if a == b || b == c || a == c {
374 return true;
375 }
376 let [p, q, r] = [a, b, c].map(|v| point(mesh, v));
377 let d2 = |u: [f64; 3], v: [f64; 3]| (0..3).map(|k| (u[k] - v[k]).powi(2)).sum::<f64>();
378 let longest = d2(p, q).max(d2(q, r)).max(d2(r, p));
379 triangle_area(mesh, [a, b, c]) <= area_epsilon * longest
380}
381
382const UNNUMBERED: usize = usize::MAX;
384
385fn groups(roots: &[VertexId], max: usize) -> Result<(usize, Vec<Vec<VertexId>>)> {
389 let mut index = filled(roots.len(), UNNUMBERED)?;
392 let mut count = 0;
393 let mut out: Vec<Vec<VertexId>> = Vec::new();
394 for (v, &r) in roots.iter().enumerate() {
395 let r = r as usize;
396 if r == v {
397 continue;
398 }
399 if index[r] == UNNUMBERED {
400 index[r] = count;
401 count += 1;
402 if out.len() < max {
403 let mut group = Vec::new();
404 push(&mut group, r as VertexId)?;
405 push(&mut out, group)?;
406 }
407 }
408 if let Some(group) = out.get_mut(index[r]) {
409 push(group, v as VertexId)?;
410 }
411 }
412 Ok((count, out))
413}
414
415#[cfg(test)]
416mod tests {
417 use fast_mesh_bvh::BuildOptions;
418 use fast_mesh_store::StoreError;
419
420 use super::*;
421 use crate::test_util::*;
422 use crate::{OpsError, fail_allocations_after};
423
424 fn run(mesh: &Mesh) -> Analysis {
425 analyze(mesh, &AnalyzeOptions::default()).unwrap()
426 }
427
428 #[test]
429 fn builder_matches_literal() {
430 let mesh = uv_sphere(8, 12, 1.0);
431 let bvh = Bvh::build(&mesh, &BuildOptions::default()).unwrap();
432 let got = AnalyzeOptions::builder()
433 .weld_tolerance(0.5)
434 .self_intersection(&bvh)
435 .max_listed(10)
436 .build();
437 assert_eq!(got.weld_tolerance, 0.5);
438 assert!(got.self_intersection.is_some());
439 assert_eq!(got.max_listed, 10);
440 let default = AnalyzeOptions::builder().build();
441 assert_eq!(default.weld_tolerance, 0.0);
442 assert!(default.self_intersection.is_none());
443 assert_eq!(default.max_listed, 1000);
444 }
445
446 #[test]
447 fn analyze_counts_on_damaged_fixture() {
448 let mesh = damaged_sphere();
449 assert_eq!(mesh.triangle_count(), 170);
450 assert_eq!(mesh.vertex_count(), 94);
451 let a = run(&mesh);
452
453 let mut lens: Vec<usize> = a.loops.iter().map(|l| l.vertices.len()).collect();
455 lens.sort_unstable();
456 assert_eq!(lens, [3, 3, 3, 4, 4, 4, 4, 4]);
457 assert!(a.loops.iter().all(|l| l.clean && l.closed));
458 let seqs: Vec<&[VertexId]> = a.loops.iter().map(|l| l.vertices.as_slice()).collect();
459 assert!(seqs.contains(&&[1, 14, 13][..]));
461 assert!(seqs.contains(&&[10, 23, 86, 22][..]));
463 assert!(seqs.contains(&&[90, 91, 92, 93][..]));
464
465 assert_eq!(a.coincident_groups, 4);
466 assert_eq!(a.coincident, [[10, 86], [25, 87], [31, 88], [52, 89]]);
467 assert_eq!(a.degenerate, 2);
468 assert_eq!(a.degenerate_list, [166, 167]);
469 assert_eq!(a.duplicate, 1);
470 assert_eq!(a.duplicate_list, [165]);
471 assert_eq!(a.non_manifold_edges, 3);
472 assert_eq!(a.non_manifold_edge_list, [(77, 78), (77, 85), (78, 85)]);
473 assert_eq!(a.non_manifold_vertices, [77, 78, 85]);
474 assert!(a.pinch_vertices.is_empty());
475 assert_eq!(a.components.len(), 2);
477 assert_eq!(a.components[0].triangles, (0..166).collect::<Vec<_>>());
478 assert_eq!(a.components[1].triangles, [168, 169]);
479 assert!(a.components.iter().all(|c| !c.closed));
480 assert!((a.components[1].area - 1.0).abs() < 1e-12);
481 assert_eq!(a.orientation_conflicts, 15);
483 assert_eq!(a.self_intersects, None);
484 }
485
486 #[test]
487 fn analyze_clean_sphere_reports_nothing() {
488 let a = run(&uv_sphere(8, 12, 1.0));
489 assert!(a.loops.is_empty());
490 assert_eq!((a.coincident_groups, a.degenerate, a.duplicate), (0, 0, 0));
491 assert_eq!((a.non_manifold_edges, a.orientation_conflicts), (0, 0));
492 assert!(a.non_manifold_vertices.is_empty() && a.pinch_vertices.is_empty());
493 assert_eq!(a.components.len(), 1);
494 assert!(a.components[0].closed);
495 }
496
497 #[test]
498 fn analyze_bowtie_reports_pinch_and_two_clean_holes() {
499 let a = run(&bowtie_holes());
500 assert_eq!(a.pinch_vertices, [12]);
501 assert!(a.non_manifold_vertices.is_empty());
502 assert_eq!(a.non_manifold_edges, 0);
503 let seqs: Vec<&[VertexId]> = a.loops.iter().map(|l| l.vertices.as_slice()).collect();
504 assert_eq!(seqs.len(), 3);
505 assert_eq!(&seqs[1..], [&[6, 11, 12, 7][..], &[12, 17, 18, 13][..]]);
506 assert!(a.loops.iter().all(|l| l.clean && l.closed));
507 assert_eq!(a.components.len(), 1);
508 assert_eq!(a.orientation_conflicts, 0);
509 }
510
511 #[test]
512 fn analyze_fin_reports_non_manifold_vertices_not_pinches() {
513 let a = run(&fin());
514 assert_eq!(a.non_manifold_edges, 1);
515 assert_eq!(a.non_manifold_edge_list, [(0, 1)]);
516 assert_eq!(a.non_manifold_vertices, [0, 1]);
517 assert!(a.pinch_vertices.is_empty());
518 assert!(a.loops.iter().all(|l| !l.clean));
519 }
520
521 #[test]
522 fn analyze_does_not_modify() {
523 let mesh = damaged_sphere();
524 let bvh = Bvh::build(&mesh, &BuildOptions::default()).unwrap();
525 let (revision, before) = (mesh.revision(), parts(&mesh));
526 let options = AnalyzeOptions {
527 self_intersection: Some(&bvh),
528 ..Default::default()
529 };
530 let first = analyze(&mesh, &options).unwrap();
531 assert_eq!(mesh.revision(), revision);
532 assert_eq!(parts(&mesh), before);
533 assert_eq!(analyze(&mesh, &options).unwrap(), first);
534 }
535
536 #[test]
537 fn analyze_self_intersection_flag() {
538 let boxes = crossed_boxes();
539 let sphere = uv_sphere(8, 12, 1.0);
540 for (mesh, want) in [(&boxes, true), (&sphere, false)] {
541 let bvh = Bvh::build(mesh, &BuildOptions::default()).unwrap();
542 let options = AnalyzeOptions {
543 self_intersection: Some(&bvh),
544 ..Default::default()
545 };
546 assert_eq!(analyze(mesh, &options).unwrap().self_intersects, Some(want));
547 assert_eq!(run(mesh).self_intersects, None);
548 }
549 let bvh = Bvh::build(&sphere, &BuildOptions::default()).unwrap();
551 let options = AnalyzeOptions {
552 self_intersection: Some(&bvh),
553 ..Default::default()
554 };
555 assert_eq!(
556 analyze(&boxes, &options),
557 Err(OpsError::Query(fast_mesh_query::QueryError::MeshMismatch))
558 );
559 }
560
561 #[test]
562 fn analyze_degenerate_by_area_and_duplicates_in_any_order() {
563 let positions = [
567 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 0.5, 1e-7,
568 0.0,
569 ];
570 let tris = [
571 [0, 1, 2],
572 [0, 2, 3],
573 [2, 1, 0], [0, 4, 1], [0, 5, 1], [3, 3, 2], [1, 2, 0], ];
579 let indices: Vec<u32> = tris.iter().flatten().copied().collect();
580 let mesh = Mesh::from_buffers(&positions, &indices).unwrap();
581 let a = run(&mesh);
582 assert_eq!(a.degenerate_list, [3, 5]);
583 assert_eq!(a.duplicate_list, [2, 6]);
584 assert_eq!((a.degenerate, a.duplicate), (2, 2));
585 }
586
587 #[test]
588 fn analyze_coincident_with_tolerance() {
589 let positions = [
591 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.2, 0.0, 0.0, 0.5, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, 1.0,
592 0.0,
593 ];
594 let indices = [0, 1, 5, 1, 2, 5, 2, 3, 5, 3, 4, 5];
595 let mesh = Mesh::from_buffers(&positions, &indices).unwrap();
596 let with = |weld_tolerance| {
597 let options = AnalyzeOptions {
598 weld_tolerance,
599 ..Default::default()
600 };
601 analyze(&mesh, &options).unwrap().coincident
602 };
603 assert_eq!(with(0.0), [vec![0, 4]]);
604 assert_eq!(with(0.15), [vec![0, 1, 2, 4]]);
606 assert_eq!(with(0.05), [vec![0, 4]]);
607 assert_eq!(with(1.5), [vec![0, 1, 2, 3, 4, 5]]);
608 for bad in [-1.0, f32::NAN, f32::INFINITY] {
609 let options = AnalyzeOptions {
610 weld_tolerance: bad,
611 ..Default::default()
612 };
613 assert!(matches!(
614 analyze(&mesh, &options),
615 Err(OpsError::InvalidInput(_))
616 ));
617 }
618 }
619
620 #[test]
621 fn analyze_truncates_lists_but_not_counts() {
622 let mesh = damaged_sphere();
623 let options = AnalyzeOptions {
624 max_listed: 1,
625 ..Default::default()
626 };
627 let a = analyze(&mesh, &options).unwrap();
628 assert_eq!((a.coincident_groups, a.coincident), (4, vec![vec![10, 86]]));
629 assert_eq!((a.degenerate, a.degenerate_list), (2, vec![166]));
630 assert_eq!((a.duplicate, a.duplicate_list), (1, vec![165]));
631 assert_eq!(
632 (a.non_manifold_edges, a.non_manifold_edge_list),
633 (3, vec![(77, 78)])
634 );
635 assert_eq!(a.non_manifold_vertices.len(), 3);
636 assert_eq!(a.loops.len(), 8);
637 let none = AnalyzeOptions {
638 max_listed: 0,
639 ..Default::default()
640 };
641 let a = analyze(&mesh, &none).unwrap();
642 assert_eq!((a.degenerate, a.degenerate_list.len()), (2, 0));
643 }
644
645 fn large_damaged() -> Mesh {
650 let sphere = uv_sphere(96, 192, 1.0);
651 let holed = punch_holes(&sphere, &[[1.0, 0.0, 0.0], [0.0, -1.0, 0.0]], 0.2);
652 let every_97th: Vec<TriangleId> = (0..holed.triangle_count() as u32).step_by(97).collect();
653 let (mut positions, mut triangles) =
654 parts(&combine(&[&flip(&holed, &every_97th), &crossed_boxes()]));
655 let copy = (positions.len() / 3) as VertexId;
656 positions.extend_from_within(1500..1503);
657 let t = triangles.iter().position(|t| t.contains(&500)).unwrap();
658 triangles[t] = triangles[t].map(|v| if v == 500 { copy } else { v });
659 triangles.extend([triangles[10], [5, 5, 6]]);
660 mesh_of(&positions, &triangles)
661 }
662
663 #[test]
664 fn analyze_bit_identical() {
665 let mesh = large_damaged();
666 assert!(mesh.triangle_count() >= PAR_MIN_ANALYZE_TRIS);
667 let bvh = Bvh::build(&mesh, &BuildOptions::default()).unwrap();
668 for weld_tolerance in [0.0, 0.02] {
669 let options = AnalyzeOptions {
670 weld_tolerance,
671 self_intersection: Some(&bvh),
672 ..Default::default()
673 };
674 let a = assert_parallel_identical("analyze", || analyze(&mesh, &options).unwrap());
675 assert_eq!(a.self_intersects, Some(true));
676 assert!(a.loops.len() >= 2 && a.orientation_conflicts > 0);
677 assert_eq!((a.duplicate, a.degenerate), (1, 1));
678 assert!(a.coincident_groups > 0);
679 }
680 let other = Bvh::build(&uv_sphere(8, 12, 1.0), &BuildOptions::default()).unwrap();
682 let mut bad = AnalyzeOptions {
683 self_intersection: Some(&other),
684 ..Default::default()
685 };
686 let e = assert_parallel_identical("analyze error", || analyze(&mesh, &bad));
687 assert!(matches!(e, Err(OpsError::Query(_))));
688 bad.weld_tolerance = -1.0;
690 let before = par::parallel_entries();
691 assert!(matches!(
692 analyze(&mesh, &bad),
693 Err(OpsError::InvalidInput(_))
694 ));
695 assert_eq!(par::parallel_entries(), before);
696 }
697
698 #[test]
699 fn analyze_oom_is_clean() {
700 let mesh = damaged_sphere();
701 let bvh = Bvh::build(&mesh, &BuildOptions::default()).unwrap();
702 let options = AnalyzeOptions {
703 self_intersection: Some(&bvh),
704 ..Default::default()
705 };
706 let expected = analyze(&mesh, &options).unwrap();
707 let mut n = 0;
708 loop {
709 fail_allocations_after(Some(n));
710 let got = analyze(&mesh, &options);
711 fail_allocations_after(None);
712 match got {
713 Ok(a) => {
714 assert_eq!(a, expected);
715 break;
716 }
717 Err(e) => assert_eq!(e, OpsError::Store(StoreError::OutOfMemory)),
718 }
719 n += 1;
720 }
721 assert!(n > 10, "only {n} fallible reservations");
722 }
723}