Skip to main content

fast_mesh_ops/
cleanup.rs

1//! Welding vertices and removing degenerate triangles and small components.
2//!
3//! Each operation reads its whole plan from `tx.mesh()`, then writes in this
4//! order: rewired triangles, removed triangles, removed vertices. A second run
5//! on the result finds nothing to do and writes nothing.
6
7use 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/// What [`weld()`] did.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub struct WeldReport {
17    /// Vertices merged into the smallest id of their group.
18    pub merged_vertices: usize,
19    /// Triangles removed because two of their corners merged.
20    pub removed_triangles: usize,
21}
22
23/// Merges vertices at most `tolerance` apart (local units).
24///
25/// Vertices are grouped through chains of pairs within the tolerance; each
26/// group keeps its smallest id at its original position. Triangles are
27/// rewritten to the kept ids, those that thereby get a repeated id are
28/// removed, and so are all vertices no triangle uses afterwards, including
29/// ones that were unused before. Tolerance 0 merges exact duplicates only. A
30/// vertex with a non-finite coordinate is never merged. No vertex is created.
31///
32/// On a mesh with UVs ([`Mesh::uvs`]), a pair also needs equal UVs (within
33/// `1e-6` per component) to merge, so the two sides of a UV seam stay apart
34/// and every kept vertex keeps its own UV. Without UVs nothing changes.
35///
36/// Rewiring can make two triangles share a vertex set, or give an edge a
37/// third triangle; run [`remove_degenerate()`] afterwards to drop duplicates.
38///
39/// # Errors
40/// [`OpsError::InvalidInput`] when `tolerance` is negative, NaN or infinite;
41/// [`OpsError::Store`] when memory runs out.
42pub 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/// Options for [`remove_degenerate()`].
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct DegenerateOptions {
79    /// A triangle is degenerate when its area is at most this times its
80    /// longest edge squared. Default `1e-12`.
81    pub area_epsilon: f64,
82    /// Whether to remove the vertices no triangle uses afterwards. Default
83    /// `true`.
84    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    /// A builder for [`DegenerateOptions`]: every option unset, as
98    /// [`Default`], until set.
99    pub fn builder() -> DegenerateOptionsBuilder {
100        DegenerateOptionsBuilder::default()
101    }
102}
103
104/// A builder for [`DegenerateOptions`], see [`DegenerateOptions::builder`].
105#[derive(Debug, Clone, Copy, Default)]
106pub struct DegenerateOptionsBuilder {
107    options: DegenerateOptions,
108}
109
110impl DegenerateOptionsBuilder {
111    /// A triangle is degenerate when its area is at most this times its
112    /// longest edge squared.
113    pub fn area_epsilon(mut self, area_epsilon: f64) -> Self {
114        self.options.area_epsilon = area_epsilon;
115        self
116    }
117
118    /// Whether to remove the vertices no triangle uses afterwards.
119    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    /// The options as set.
125    pub fn build(self) -> DegenerateOptions {
126        self.options
127    }
128}
129
130/// What [`remove_degenerate()`] did.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
132pub struct DegenerateReport {
133    /// Triangles removed.
134    pub removed_triangles: usize,
135    /// Vertices removed.
136    pub removed_vertices: usize,
137}
138
139/// Removes triangles with a repeated vertex id, triangles whose area is at
140/// most `area_epsilon` times their longest edge squared, and duplicates: of
141/// triangles with the same vertex set, all but the one with the smallest id.
142///
143/// # Errors
144/// [`OpsError::InvalidInput`] when `area_epsilon` is negative, NaN or
145/// infinite; [`OpsError::Store`] when memory runs out.
146pub 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/// Options for [`remove_small_components()`].
185#[derive(Debug, Clone, Copy, PartialEq)]
186pub struct ComponentOptions {
187    /// Remove components with fewer triangles than this.
188    pub min_triangles: Option<usize>,
189    /// Remove components with less area than this (local units squared).
190    pub min_area: Option<f64>,
191    /// Keep the largest component even when it is below a bound. Largest
192    /// means most area; ties go to the most triangles, then the smallest
193    /// triangle id. Default `true`.
194    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    /// A builder for [`ComponentOptions`]: every option unset, as [`Default`],
209    /// until set.
210    pub fn builder() -> ComponentOptionsBuilder {
211        ComponentOptionsBuilder::default()
212    }
213}
214
215/// A builder for [`ComponentOptions`], see [`ComponentOptions::builder`].
216#[derive(Debug, Clone, Copy, Default)]
217pub struct ComponentOptionsBuilder {
218    options: ComponentOptions,
219}
220
221impl ComponentOptionsBuilder {
222    /// Remove components with fewer triangles than this.
223    pub fn min_triangles(mut self, min_triangles: usize) -> Self {
224        self.options.min_triangles = Some(min_triangles);
225        self
226    }
227
228    /// Remove components with less area than this.
229    pub fn min_area(mut self, min_area: f64) -> Self {
230        self.options.min_area = Some(min_area);
231        self
232    }
233
234    /// Keep the largest component even when it is below a bound.
235    pub fn keep_largest(mut self, keep_largest: bool) -> Self {
236        self.options.keep_largest = keep_largest;
237        self
238    }
239
240    /// The options as set.
241    pub fn build(self) -> ComponentOptions {
242        self.options
243    }
244}
245
246/// What [`remove_small_components()`] did.
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
248pub struct RemoveReport {
249    /// Components removed.
250    pub removed_components: usize,
251    /// Triangles removed.
252    pub removed_triangles: usize,
253    /// Vertices removed: those no triangle uses afterwards.
254    pub removed_vertices: usize,
255}
256
257/// Removes the connected components (triangles joined through shared edges
258/// of any kind) that are below any bound that is set, and the vertices left
259/// unused. Triangles with a repeated vertex id belong to no component and stay.
260///
261/// On a mesh with UVs, an edge also joins across a seam (coincident corners,
262/// compared exactly), so the UV islands of one surface count as one component.
263///
264/// # Errors
265/// [`OpsError::InvalidInput`] when no bound is set or `min_area` is negative,
266/// NaN or infinite; [`OpsError::Store`] when memory runs out.
267pub 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    // Components come sorted by their smallest triangle id, so the first of
290    // equals wins the final tie.
291    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
327/// `comps` (sorted by smallest triangle id) with those sharing an edge at
328/// coincident positions merged, in the same order. Such a merged component
329/// counts as closed only if each part is; nothing here reads that.
330fn 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    // Every edge at its welded corners, with the component using it.
343    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    // A root is its set's smallest index, so parts arrive in order.
358    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
379/// The alive vertices no triangle uses once the planned edits are applied:
380/// triangles `removed` (ascending) are gone, and `rewired[i]` (ascending) has
381/// the corners `rewired_to[i]`. Ascending.
382fn 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        // The slits are sewn shut: only the three holes and the square remain.
470        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        // Vertices 0 and 1 are 1e-5 apart; triangle 0 uses both.
478        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        // 166 and 167 repeat an id, 165 is a copy; no vertex is left unused.
523        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        // Triangle 1 lies on a line; vertices 3 and 4 are used only by it.
540        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        // Only small pieces: the largest survives, unless keep_largest is off.
592        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        // Two identical unit squares: equal area and size, the first is kept.
613        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        // Triangles 0 and 1 differ only in vertex 0 vs its copy 4.
689        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        // The stray vertex 3 was unused before and goes too.
697        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        // A, B, C in a row 0.7e-4 apart: |AC| = 1.4e-4 > 1e-4, one group.
708        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}