1use fast_mesh_store::{Mesh, Transaction, TriangleId, VertexId};
8
9use crate::analyze::{AREA_EPSILON, duplicate_triangles, is_degenerate};
10use crate::topology::{Component, EdgeMap, UnionFind, components};
11use crate::weld::{weld_roots, weld_roots_keeping_seams};
12use crate::{OpsError, Result, filled, push, reserve};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub struct WeldReport {
17 pub merged_vertices: usize,
19 pub removed_triangles: usize,
21}
22
23pub fn weld(tx: &mut Transaction, tolerance: f32) -> Result<WeldReport> {
43 let mesh = tx.mesh();
44 let roots = weld_roots_keeping_seams(mesh, tolerance, mesh.uvs())?;
45 let mut report = WeldReport::default();
46 for (v, &r) in roots.iter().enumerate() {
47 if r as usize != v {
48 report.merged_vertices += 1;
49 }
50 }
51 let repeats = |[a, b, c]: [VertexId; 3]| a == b || b == c || a == c;
52 let (mut rewired, mut rewired_to, mut collapsed) = (Vec::new(), Vec::new(), Vec::new());
53 for t in 0..mesh.triangle_slots() as TriangleId {
54 let Some(tri) = mesh.triangle(t) else {
55 continue;
56 };
57 let new = tri.map(|v| roots[v as usize]);
58 if new == tri {
59 continue;
60 }
61 if repeats(new) && !repeats(tri) {
62 push(&mut collapsed, t)?;
63 } else {
64 push(&mut rewired, t)?;
65 push(&mut rewired_to, new)?;
66 }
67 }
68 report.removed_triangles = collapsed.len();
69 let unused = unused_vertices(mesh, &collapsed, &rewired, &rewired_to)?;
70 tx.set_triangles(&rewired, &rewired_to)?;
71 tx.remove_triangles(&collapsed)?;
72 tx.remove_vertices(&unused)?;
73 Ok(report)
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct DegenerateOptions {
79 pub area_epsilon: f64,
82 pub remove_unused_vertices: bool,
85}
86
87impl Default for DegenerateOptions {
88 fn default() -> Self {
89 Self {
90 area_epsilon: AREA_EPSILON,
91 remove_unused_vertices: true,
92 }
93 }
94}
95
96impl DegenerateOptions {
97 pub fn builder() -> DegenerateOptionsBuilder {
100 DegenerateOptionsBuilder::default()
101 }
102}
103
104#[derive(Debug, Clone, Copy, Default)]
106pub struct DegenerateOptionsBuilder {
107 options: DegenerateOptions,
108}
109
110impl DegenerateOptionsBuilder {
111 pub fn area_epsilon(mut self, area_epsilon: f64) -> Self {
114 self.options.area_epsilon = area_epsilon;
115 self
116 }
117
118 pub fn remove_unused_vertices(mut self, remove_unused_vertices: bool) -> Self {
120 self.options.remove_unused_vertices = remove_unused_vertices;
121 self
122 }
123
124 pub fn build(self) -> DegenerateOptions {
126 self.options
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
132pub struct DegenerateReport {
133 pub removed_triangles: usize,
135 pub removed_vertices: usize,
137}
138
139pub fn remove_degenerate(
147 tx: &mut Transaction,
148 options: &DegenerateOptions,
149) -> Result<DegenerateReport> {
150 if !(options.area_epsilon >= 0.0 && options.area_epsilon.is_finite()) {
151 return Err(OpsError::InvalidInput(
152 "area_epsilon must be finite and not negative",
153 ));
154 }
155 let mesh = tx.mesh();
156 let mut doomed: Vec<TriangleId> = Vec::new();
157 for t in 0..mesh.triangle_slots() as TriangleId {
158 if mesh
159 .triangle(t)
160 .is_some_and(|tri| is_degenerate(mesh, tri, options.area_epsilon))
161 {
162 push(&mut doomed, t)?;
163 }
164 }
165 for t in duplicate_triangles(mesh)? {
166 push(&mut doomed, t)?;
167 }
168 doomed.sort_unstable();
169 doomed.dedup();
170 let unused = if options.remove_unused_vertices {
171 unused_vertices(mesh, &doomed, &[], &[])?
172 } else {
173 Vec::new()
174 };
175 tx.remove_triangles(&doomed)?;
176 tx.remove_vertices(&unused)?;
177 let removed_vertices = unused.len();
178 Ok(DegenerateReport {
179 removed_triangles: doomed.len(),
180 removed_vertices,
181 })
182}
183
184#[derive(Debug, Clone, Copy, PartialEq)]
186pub struct ComponentOptions {
187 pub min_triangles: Option<usize>,
189 pub min_area: Option<f64>,
191 pub keep_largest: bool,
195}
196
197impl Default for ComponentOptions {
198 fn default() -> Self {
199 Self {
200 min_triangles: None,
201 min_area: None,
202 keep_largest: true,
203 }
204 }
205}
206
207impl ComponentOptions {
208 pub fn builder() -> ComponentOptionsBuilder {
211 ComponentOptionsBuilder::default()
212 }
213}
214
215#[derive(Debug, Clone, Copy, Default)]
217pub struct ComponentOptionsBuilder {
218 options: ComponentOptions,
219}
220
221impl ComponentOptionsBuilder {
222 pub fn min_triangles(mut self, min_triangles: usize) -> Self {
224 self.options.min_triangles = Some(min_triangles);
225 self
226 }
227
228 pub fn min_area(mut self, min_area: f64) -> Self {
230 self.options.min_area = Some(min_area);
231 self
232 }
233
234 pub fn keep_largest(mut self, keep_largest: bool) -> Self {
236 self.options.keep_largest = keep_largest;
237 self
238 }
239
240 pub fn build(self) -> ComponentOptions {
242 self.options
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
248pub struct RemoveReport {
249 pub removed_components: usize,
251 pub removed_triangles: usize,
253 pub removed_vertices: usize,
255}
256
257pub fn remove_small_components(
268 tx: &mut Transaction,
269 options: &ComponentOptions,
270) -> Result<RemoveReport> {
271 if options.min_triangles.is_none() && options.min_area.is_none() {
272 return Err(OpsError::InvalidInput(
273 "remove_small_components needs min_triangles or min_area",
274 ));
275 }
276 if let Some(a) = options.min_area
277 && !(a >= 0.0 && a.is_finite())
278 {
279 return Err(OpsError::InvalidInput(
280 "min_area must be finite and not negative",
281 ));
282 }
283 let mesh = tx.mesh();
284 let edges = EdgeMap::build(mesh)?;
285 let mut comps = components(mesh, &edges)?;
286 if mesh.uvs().is_some() {
287 comps = joined_across_seams(mesh, &edges, comps)?;
288 }
289 let mut largest: Option<usize> = None;
292 for (i, c) in comps.iter().enumerate() {
293 let better = largest.is_none_or(|j| {
294 let l = &comps[j];
295 (c.area, c.triangles.len()) > (l.area, l.triangles.len())
296 });
297 if better {
298 largest = Some(i);
299 }
300 }
301 let mut doomed: Vec<TriangleId> = Vec::new();
302 let mut removed_components = 0;
303 for (i, c) in comps.iter().enumerate() {
304 if options.keep_largest && largest == Some(i) {
305 continue;
306 }
307 let small = options.min_triangles.is_some_and(|n| c.triangles.len() < n)
308 || options.min_area.is_some_and(|a| c.area < a);
309 if small {
310 removed_components += 1;
311 reserve(&mut doomed, c.triangles.len())?;
312 doomed.extend_from_slice(&c.triangles);
313 }
314 }
315 doomed.sort_unstable();
316 let unused = unused_vertices(mesh, &doomed, &[], &[])?;
317 tx.remove_triangles(&doomed)?;
318 tx.remove_vertices(&unused)?;
319 let removed_vertices = unused.len();
320 Ok(RemoveReport {
321 removed_components,
322 removed_triangles: doomed.len(),
323 removed_vertices,
324 })
325}
326
327fn joined_across_seams(
331 mesh: &Mesh,
332 edges: &EdgeMap,
333 comps: Vec<Component>,
334) -> Result<Vec<Component>> {
335 let roots = weld_roots(mesh, 0.0)?;
336 let mut owner = filled(mesh.triangle_slots(), u32::MAX)?;
337 for (i, c) in comps.iter().enumerate() {
338 for &t in &c.triangles {
339 owner[t as usize] = i as u32;
340 }
341 }
342 let mut keyed: Vec<((VertexId, VertexId), u32)> = Vec::new();
344 reserve(&mut keyed, edges.len())?;
345 for ((a, b), tris) in edges.edges() {
346 let (ra, rb) = (roots[a as usize], roots[b as usize]);
347 keyed.push(((ra.min(rb), ra.max(rb)), owner[tris[0] as usize]));
348 }
349 keyed.sort_unstable();
350 let mut uf = UnionFind::new();
351 uf.reset(comps.len())?;
352 for w in keyed.windows(2) {
353 if w[0].0 == w[1].0 {
354 uf.union(w[0].1, w[1].1);
355 }
356 }
357 let mut merged: Vec<Component> = Vec::new();
359 let mut slot = filled(comps.len(), u32::MAX)?;
360 for (i, c) in comps.into_iter().enumerate() {
361 let root = uf.find(i as u32) as usize;
362 if slot[root] == u32::MAX {
363 slot[root] = merged.len() as u32;
364 push(&mut merged, c)?;
365 } else {
366 let m = &mut merged[slot[root] as usize];
367 reserve(&mut m.triangles, c.triangles.len())?;
368 m.triangles.extend_from_slice(&c.triangles);
369 m.area += c.area;
370 m.closed &= c.closed;
371 }
372 }
373 for m in &mut merged {
374 m.triangles.sort_unstable();
375 }
376 Ok(merged)
377}
378
379fn unused_vertices(
383 mesh: &Mesh,
384 removed: &[TriangleId],
385 rewired: &[TriangleId],
386 rewired_to: &[[VertexId; 3]],
387) -> Result<Vec<VertexId>> {
388 let mut used = filled(mesh.vertex_slots(), false)?;
389 for t in 0..mesh.triangle_slots() as TriangleId {
390 let Some(mut tri) = mesh.triangle(t) else {
391 continue;
392 };
393 if removed.binary_search(&t).is_ok() {
394 continue;
395 }
396 if let Ok(i) = rewired.binary_search(&t) {
397 tri = rewired_to[i];
398 }
399 for v in tri {
400 used[v as usize] = true;
401 }
402 }
403 let mut unused = Vec::new();
404 for v in 0..mesh.vertex_slots() as VertexId {
405 if mesh.is_vertex_alive(v) && !used[v as usize] {
406 push(&mut unused, v)?;
407 }
408 }
409 Ok(unused)
410}
411
412#[cfg(test)]
413mod tests {
414 use fast_mesh_store::{History, Mesh};
415
416 use super::*;
417 use crate::analyze::{AnalyzeOptions, analyze};
418 use crate::test_util::*;
419
420 #[test]
421 fn builders_match_literals() {
422 assert_eq!(
423 DegenerateOptions::builder()
424 .area_epsilon(1e-6)
425 .remove_unused_vertices(false)
426 .build(),
427 DegenerateOptions {
428 area_epsilon: 1e-6,
429 remove_unused_vertices: false,
430 }
431 );
432 assert_eq!(
433 DegenerateOptions::builder().build(),
434 DegenerateOptions::default()
435 );
436 assert_eq!(
437 ComponentOptions::builder()
438 .min_triangles(10)
439 .min_area(2.0)
440 .keep_largest(false)
441 .build(),
442 ComponentOptions {
443 min_triangles: Some(10),
444 min_area: Some(2.0),
445 keep_largest: false,
446 }
447 );
448 assert_eq!(
449 ComponentOptions::builder().build(),
450 ComponentOptions::default()
451 );
452 }
453
454 #[test]
455 fn weld_merges_duplicates_and_drops_collapsed() {
456 let mut mesh = damaged_sphere();
457 let (r, _) = edit(&mut mesh, |tx| weld(tx, 0.0));
458 assert_eq!(
459 r,
460 WeldReport {
461 merged_vertices: 4,
462 removed_triangles: 0
463 }
464 );
465 assert_eq!(mesh.vertex_count(), 90);
466 assert_eq!(mesh.triangle_count(), 170);
467 let a = analyze(&mesh, &AnalyzeOptions::default()).unwrap();
468 assert_eq!(a.coincident_groups, 0);
469 let mut lens: Vec<usize> = a.loops.iter().map(|l| l.vertices.len()).collect();
471 lens.sort_unstable();
472 assert_eq!(lens, [3, 3, 3, 4]);
473 }
474
475 #[test]
476 fn weld_removes_triangles_that_collapse() {
477 let mut mesh = Mesh::from_buffers(
479 &[0.0, 0.0, 0.0, 1e-5, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0],
480 &[0, 1, 2, 1, 3, 2],
481 )
482 .unwrap();
483 let (r, _) = edit(&mut mesh, |tx| weld(tx, 1e-4));
484 assert_eq!(
485 r,
486 WeldReport {
487 merged_vertices: 1,
488 removed_triangles: 1
489 }
490 );
491 assert_eq!(mesh.triangle_count(), 1);
492 assert_eq!(mesh.triangle(1), Some([0, 3, 2]));
493 assert!(!mesh.is_vertex_alive(1));
494 }
495
496 #[test]
497 fn weld_tolerance_merges_near() {
498 let positions = [0.0, 0.0, 0.0, 1e-5, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0];
499 let mut mesh = Mesh::from_buffers(&positions, &[0, 3, 2, 1, 3, 2]).unwrap();
500 let (r, empty) = edit(&mut mesh, |tx| weld(tx, 1e-6));
501 assert_eq!((r.merged_vertices, empty), (0, true));
502 let (r, _) = edit(&mut mesh, |tx| weld(tx, 1e-4));
503 assert_eq!(r.merged_vertices, 1);
504 assert_eq!(mesh.triangle(1), Some([0, 3, 2]));
505 }
506
507 #[test]
508 fn weld_rejects_bad_tolerance() {
509 let mut mesh = damaged_sphere();
510 let mut tx = mesh.begin_edit();
511 for t in [-1.0, f32::NAN, f32::INFINITY] {
512 assert!(matches!(weld(&mut tx, t), Err(OpsError::InvalidInput(_))));
513 }
514 }
515
516 #[test]
517 fn degenerate_and_duplicates_removed() {
518 let mut mesh = damaged_sphere();
519 let (r, _) = edit(&mut mesh, |tx| {
520 remove_degenerate(tx, &DegenerateOptions::default())
521 });
522 assert_eq!(
524 r,
525 DegenerateReport {
526 removed_triangles: 3,
527 removed_vertices: 0
528 }
529 );
530 assert_eq!(mesh.triangle_count(), 167);
531 assert!(!mesh.is_triangle_alive(165));
532 assert!(!mesh.is_triangle_alive(166) && !mesh.is_triangle_alive(167));
533 let a = analyze(&mesh, &AnalyzeOptions::default()).unwrap();
534 assert_eq!((a.degenerate, a.duplicate, a.non_manifold_edges), (0, 0, 0));
535 }
536
537 #[test]
538 fn degenerate_removes_slivers_and_unused_vertices() {
539 let positions = [
541 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 2.0, 0.0, 0.0, 3.0, 0.0, 0.0,
542 ];
543 let mut mesh = Mesh::from_buffers(&positions, &[0, 1, 2, 1, 3, 4]).unwrap();
544 let keep = DegenerateOptions {
545 remove_unused_vertices: false,
546 ..Default::default()
547 };
548 let (r, _) = edit(&mut mesh, |tx| remove_degenerate(tx, &keep));
549 assert_eq!((r.removed_triangles, r.removed_vertices), (1, 0));
550 let (r, _) = edit(&mut mesh, |tx| {
551 remove_degenerate(tx, &DegenerateOptions::default())
552 });
553 assert_eq!((r.removed_triangles, r.removed_vertices), (0, 2));
554 assert_eq!((mesh.vertex_count(), mesh.triangle_count()), (3, 1));
555 }
556
557 #[test]
558 fn degenerate_rejects_bad_epsilon() {
559 let mut mesh = damaged_sphere();
560 let mut tx = mesh.begin_edit();
561 let o = DegenerateOptions {
562 area_epsilon: -1.0,
563 ..Default::default()
564 };
565 assert!(matches!(
566 remove_degenerate(&mut tx, &o),
567 Err(OpsError::InvalidInput(_))
568 ));
569 }
570
571 #[test]
572 fn small_components_removed_largest_kept() {
573 let mut mesh = damaged_sphere();
574 let o = ComponentOptions {
575 min_triangles: Some(10),
576 ..Default::default()
577 };
578 let (r, _) = edit(&mut mesh, |tx| remove_small_components(tx, &o));
579 assert_eq!(
580 r,
581 RemoveReport {
582 removed_components: 1,
583 removed_triangles: 2,
584 removed_vertices: 4
585 }
586 );
587 assert_eq!(mesh.triangle_count(), 168);
588 let a = analyze(&mesh, &AnalyzeOptions::default()).unwrap();
589 assert_eq!(a.components.len(), 1);
590
591 let pieces = || combine(&[&grid(1), &grid(2), &grid(3)]);
593 let o = ComponentOptions {
594 min_triangles: Some(100),
595 ..Default::default()
596 };
597 let mut m = pieces();
598 let (r, _) = edit(&mut m, |tx| remove_small_components(tx, &o));
599 assert_eq!((r.removed_components, m.triangle_count()), (2, 18));
600 let mut m = pieces();
601 let o = ComponentOptions {
602 keep_largest: false,
603 ..o
604 };
605 let (r, _) = edit(&mut m, |tx| remove_small_components(tx, &o));
606 assert_eq!((r.removed_components, m.triangle_count()), (3, 0));
607 assert_eq!(m.vertex_count(), 0);
608 }
609
610 #[test]
611 fn small_components_by_area_and_largest_tie() {
612 let mut m = combine(&[&grid(1), &grid(1)]);
614 let o = ComponentOptions {
615 min_area: Some(5.0),
616 ..Default::default()
617 };
618 let (r, _) = edit(&mut m, |tx| remove_small_components(tx, &o));
619 assert_eq!(r.removed_components, 1);
620 assert!(m.is_triangle_alive(0) && !m.is_triangle_alive(2));
621 }
622
623 #[test]
624 fn no_bound_is_invalid_input() {
625 let mut mesh = damaged_sphere();
626 let mut tx = mesh.begin_edit();
627 assert!(matches!(
628 remove_small_components(&mut tx, &ComponentOptions::default()),
629 Err(OpsError::InvalidInput(_))
630 ));
631 let o = ComponentOptions {
632 min_area: Some(f64::NAN),
633 ..Default::default()
634 };
635 assert!(matches!(
636 remove_small_components(&mut tx, &o),
637 Err(OpsError::InvalidInput(_))
638 ));
639 }
640
641 #[test]
642 fn idempotent() {
643 let mut mesh = damaged_sphere();
644 let comp = ComponentOptions {
645 min_triangles: Some(10),
646 ..Default::default()
647 };
648 let dg = DegenerateOptions::default();
649 edit(&mut mesh, |tx| weld(tx, 0.0));
650 edit(&mut mesh, |tx| remove_degenerate(tx, &dg));
651 edit(&mut mesh, |tx| remove_small_components(tx, &comp));
652 let (r, empty) = edit(&mut mesh, |tx| weld(tx, 0.0));
653 assert_eq!((r, empty), (WeldReport::default(), true));
654 let (r, empty) = edit(&mut mesh, |tx| remove_degenerate(tx, &dg));
655 assert_eq!((r, empty), (DegenerateReport::default(), true));
656 let (r, empty) = edit(&mut mesh, |tx| remove_small_components(tx, &comp));
657 assert_eq!((r, empty), (RemoveReport::default(), true));
658 }
659
660 #[test]
661 fn undo_restores() {
662 let mut mesh = damaged_sphere();
663 let before = parts(&mesh);
664 let mut history = History::new(1 << 24);
665 let comp = ComponentOptions {
666 min_triangles: Some(10),
667 ..Default::default()
668 };
669 for step in 0..3 {
670 let mut tx = mesh.begin_edit();
671 match step {
672 0 => drop(weld(&mut tx, 0.0).unwrap()),
673 1 => drop(remove_degenerate(&mut tx, &DegenerateOptions::default()).unwrap()),
674 _ => drop(remove_small_components(&mut tx, &comp).unwrap()),
675 }
676 let patch = tx.commit().unwrap();
677 assert!(!patch.is_empty());
678 history.push(&mut mesh, patch).unwrap();
679 }
680 assert_ne!(parts(&mesh), before);
681 while history.undo(&mut mesh).unwrap().is_some() {}
682 assert_eq!(parts(&mesh), before);
683 assert_eq!(mesh.vertex_count(), 94);
684 }
685
686 #[test]
687 fn weld_into_duplicates_then_remove_degenerate() {
688 let positions = [
690 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 5.0, 5.0, 5.0, 0.0, 0.0, 0.0,
691 ];
692 let mut mesh = Mesh::from_buffers(&positions, &[0, 1, 2, 4, 1, 2]).unwrap();
693 let (r, _) = edit(&mut mesh, |tx| weld(tx, 0.0));
694 assert_eq!(r.merged_vertices, 1);
695 assert_eq!(mesh.triangle_count(), 2);
696 assert_eq!(mesh.vertex_count(), 3);
698 let (r, _) = edit(&mut mesh, |tx| {
699 remove_degenerate(tx, &DegenerateOptions::default())
700 });
701 assert_eq!(r.removed_triangles, 1);
702 assert!(mesh.is_triangle_alive(0) && !mesh.is_triangle_alive(1));
703 }
704
705 #[test]
706 fn weld_chains_near_points_and_keeps_the_smallest_in_place() {
707 let positions = [
709 0.0, 0.0, 0.0, 0.7e-4, 0.0, 0.0, 1.4e-4, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0,
710 ];
711 let mut mesh = Mesh::from_buffers(&positions, &[0, 3, 4, 1, 3, 4, 2, 3, 4]).unwrap();
712 let kept = mesh.positions()[0];
713 let (r, _) = edit(&mut mesh, |tx| weld(tx, 1e-4));
714 assert_eq!(r.merged_vertices, 2);
715 assert_eq!(mesh.positions()[0], kept);
716 assert_eq!(mesh.vertex_count(), 3);
717 assert_eq!(mesh.triangle(2), Some([0, 3, 4]));
718 }
719
720 #[test]
721 fn weld_removes_preexisting_unused_vertices() {
722 let positions = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 9.0, 9.0, 9.0];
723 let mut mesh = Mesh::from_buffers(&positions, &[0, 1, 2]).unwrap();
724 let (r, empty) = edit(&mut mesh, |tx| weld(tx, 0.0));
725 assert_eq!((r, empty), (WeldReport::default(), false));
726 assert_eq!(mesh.vertex_count(), 3);
727 let (_, empty) = edit(&mut mesh, |tx| weld(tx, 0.0));
728 assert!(empty);
729 }
730
731 #[test]
732 fn out_of_memory_is_reported_and_writes_nothing() {
733 let comp = ComponentOptions {
734 min_triangles: Some(10),
735 ..Default::default()
736 };
737 assert!(sweep(damaged_sphere(), |tx| weld(tx, 0.0).map(drop)) > 5);
738 assert!(
739 sweep(damaged_sphere(), |tx| remove_degenerate(
740 tx,
741 &DegenerateOptions::default()
742 )
743 .map(drop))
744 > 5
745 );
746 assert!(
747 sweep(damaged_sphere(), |tx| remove_small_components(tx, &comp)
748 .map(drop))
749 > 5
750 );
751 }
752}