Skip to main content

fast_mesh_ops/
simplify.rs

1//! Mesh simplification by quadric-error edge collapse.
2//!
3//! [`simplify`] shrinks a mesh toward [`SimplifyOptions::target_triangles`]
4//! by repeatedly collapsing the edge whose collapse adds the least quadric
5//! error (Garland-Heckbert): each vertex holds the sum of the squared
6//! distances to its incident face planes, an edge collapses to the point
7//! minimizing the summed quadrics, and the vertex quadrics merge. Boundary
8//! vertices stay put when [`SimplifyOptions::preserve_boundary`] is set, so
9//! open rims keep their shape. Collapses that would break the link condition
10//! or fold the fan are skipped. The whole plan is computed on working copies
11//! first and committed through one [`Transaction`](fast_mesh_store::Transaction),
12//! which keeps the call atomic under sweep: either every collapse lands or
13//! nothing does.
14//!
15//! UVs ([`Mesh::uvs`](fast_mesh_store::Mesh::uvs)) follow: a survivor takes the
16//! UV at its new position's parameter along the collapsed edge. The quadrics
17//! ignore UVs, so textures can stretch. With `preserve_boundary`, seams (which
18//! are boundaries) stay exactly as they are.
19
20use std::cmp::Ordering;
21use std::collections::BinaryHeap;
22
23use fast_mesh_store::{Transaction, TriangleId, UV_CHANNEL, VertexId};
24
25use crate::{OpsError, Result, filled, push, reserve};
26
27/// The parameter along `a`-`b` of the point nearest to `p`, in `[0, 1]`; the
28/// middle of a zero-length edge.
29fn edge_param(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f32 {
30    let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
31    let len2 = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
32    if len2 > 0.0 {
33        let along = (p[0] - a[0]) * d[0] + (p[1] - a[1]) * d[1] + (p[2] - a[2]) * d[2];
34        (along / len2).clamp(0.0, 1.0) as f32
35    } else {
36        0.5
37    }
38}
39
40/// Options for [`simplify`].
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct SimplifyOptions {
43    /// Stop once this many triangles are alive. Must be at least 1; a target
44    /// at or above the live count leaves the mesh alone.
45    pub target_triangles: usize,
46    /// Keep boundary vertices in place: no edge touching a boundary vertex
47    /// collapses. Default `true`, so open rims keep their shape.
48    pub preserve_boundary: bool,
49}
50
51impl Default for SimplifyOptions {
52    fn default() -> Self {
53        SimplifyOptions {
54            target_triangles: 0,
55            preserve_boundary: true,
56        }
57    }
58}
59
60impl SimplifyOptions {
61    /// A builder for [`SimplifyOptions`].
62    pub fn builder() -> SimplifyOptionsBuilder {
63        SimplifyOptionsBuilder::default()
64    }
65}
66
67/// Builder for [`SimplifyOptions`].
68#[derive(Debug, Clone, Default)]
69pub struct SimplifyOptionsBuilder {
70    options: SimplifyOptions,
71}
72
73impl SimplifyOptionsBuilder {
74    /// Stop once this many triangles are alive. Required: the default 0 is
75    /// rejected by [`simplify`].
76    pub fn target_triangles(mut self, target: usize) -> Self {
77        self.options.target_triangles = target;
78        self
79    }
80
81    /// Keep boundary vertices in place.
82    pub fn preserve_boundary(mut self, preserve: bool) -> Self {
83        self.options.preserve_boundary = preserve;
84        self
85    }
86
87    /// The options as set.
88    pub fn build(self) -> SimplifyOptions {
89        self.options
90    }
91}
92
93/// Outcome of [`simplify`].
94#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
95pub struct SimplifyReport {
96    /// Edge collapses performed.
97    pub collapsed: usize,
98    /// Triangles removed (one per boundary collapse, two per interior one).
99    pub triangles_removed: usize,
100    /// Vertices removed (one per collapse).
101    pub vertices_removed: usize,
102}
103
104/// A working triangle: `id` is its slot in the store.
105struct WorkTri {
106    id: TriangleId,
107    corners: [u32; 3],
108    alive: bool,
109}
110
111/// A collapse candidate, ordered cheapest-first in the max-heap.
112struct HeapEntry {
113    cost: f64,
114    a: u32,
115    b: u32,
116}
117
118impl PartialEq for HeapEntry {
119    fn eq(&self, other: &Self) -> bool {
120        self.cost == other.cost && self.a == other.a && self.b == other.b
121    }
122}
123
124impl Eq for HeapEntry {}
125
126impl PartialOrd for HeapEntry {
127    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
128        Some(self.cmp(other))
129    }
130}
131
132impl Ord for HeapEntry {
133    fn cmp(&self, other: &Self) -> Ordering {
134        // `BinaryHeap` pops the greatest: rank cheaper edges greater, with
135        // the vertex ids breaking ties deterministically.
136        other
137            .cost
138            .partial_cmp(&self.cost)
139            .unwrap_or(Ordering::Equal)
140            .then_with(|| other.a.cmp(&self.a))
141            .then_with(|| other.b.cmp(&self.b))
142    }
143}
144
145/// A symmetric 4x4 quadric as its upper triangle:
146/// `q00 q01 q02 q03 q11 q12 q13 q22 q23 q33`.
147type Quadric = [f64; 10];
148
149fn f64v(p: &[f32; 3]) -> [f64; 3] {
150    [f64::from(p[0]), f64::from(p[1]), f64::from(p[2])]
151}
152
153fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
154    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
155}
156
157fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
158    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
159}
160
161fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
162    [
163        a[1] * b[2] - a[2] * b[1],
164        a[2] * b[0] - a[0] * b[2],
165        a[0] * b[1] - a[1] * b[0],
166    ]
167}
168
169/// The quadric of one face plane: squared distance to it.
170fn face_quadric(p0: [f64; 3], p1: [f64; 3], p2: [f64; 3]) -> Quadric {
171    let n = cross3(sub3(p1, p0), sub3(p2, p0));
172    let len = dot3(n, n).sqrt();
173    if !len.is_finite() || len == 0.0 {
174        return [0.0; 10];
175    }
176    let n = [n[0] / len, n[1] / len, n[2] / len];
177    if !n.iter().all(|c| c.is_finite()) {
178        return [0.0; 10];
179    }
180    let d = -dot3(n, p0);
181    if !d.is_finite() {
182        return [0.0; 10];
183    }
184    let v = [n[0], n[1], n[2], d];
185    let mut q = [0.0; 10];
186    let mut k = 0;
187    for i in 0..4 {
188        for j in i..4 {
189            q[k] = v[i] * v[j];
190            k += 1;
191        }
192    }
193    q
194}
195
196/// The quadric error of `p`: `p^T A p + 2 b^T p + q33`.
197fn quad_error(q: &Quadric, p: [f64; 3]) -> f64 {
198    let [x, y, z] = p;
199    x * x * q[0]
200        + 2.0 * x * y * q[1]
201        + 2.0 * x * z * q[2]
202        + 2.0 * x * q[3]
203        + y * y * q[4]
204        + 2.0 * y * z * q[5]
205        + 2.0 * y * q[6]
206        + z * z * q[7]
207        + 2.0 * z * q[8]
208        + q[9]
209}
210
211/// The point minimizing `q` and its error: the linear solve, or the midpoint
212/// when the 3x3 block is singular or the solve is not finite.
213fn optimal_position(q: &Quadric, a: [f64; 3], b: [f64; 3]) -> ([f64; 3], f64) {
214    let (a00, a01, a02, b0) = (q[0], q[1], q[2], q[3]);
215    let (a11, a12, b1) = (q[4], q[5], q[6]);
216    let (a22, b2) = (q[7], q[8]);
217    let det = a00 * (a11 * a22 - a12 * a12) - a01 * (a01 * a22 - a12 * a02)
218        + a02 * (a01 * a12 - a11 * a02);
219    if det.is_finite() && det != 0.0 {
220        // Adjugate over the determinant (Cramer on `A x = -b`).
221        let inv = [
222            (a11 * a22 - a12 * a12) / det,
223            (a02 * a12 - a01 * a22) / det,
224            (a01 * a12 - a11 * a02) / det,
225            (a11 * a22 - a12 * a12) / det,
226            (a02 * a12 - a01 * a22) / det,
227            (a02 * a01 - a00 * a12) / det,
228            (a01 * a12 - a11 * a02) / det,
229            (a02 * a01 - a00 * a12) / det,
230            (a00 * a11 - a01 * a01) / det,
231        ];
232        // Symmetric: inv01 == inv10 and so on; index the upper triangle.
233        let x = -(inv[0] * b0 + inv[1] * b1 + inv[2] * b2);
234        let y = -(inv[1] * b0 + inv[4] * b1 + inv[5] * b2);
235        let z = -(inv[2] * b0 + inv[5] * b1 + inv[8] * b2);
236        let p = [x, y, z];
237        if p.iter().all(|c| c.is_finite()) {
238            let cost = quad_error(q, p);
239            if cost.is_finite() {
240                return (
241                    p,
242                    if cost < 0.0 && cost > -1e-9 {
243                        0.0
244                    } else {
245                        cost
246                    },
247                );
248            }
249        }
250    }
251    let mid = [
252        (a[0] + b[0]) / 2.0,
253        (a[1] + b[1]) / 2.0,
254        (a[2] + b[2]) / 2.0,
255    ];
256    let cost = quad_error(q, mid);
257    if cost.is_finite() {
258        (mid, cost.max(0.0))
259    } else {
260        // Degenerate quadrics (huge coordinates): fall back to the squared
261        // edge length so the ordering stays finite and shortest-first.
262        let d = sub3(a, b);
263        (mid, dot3(d, d))
264    }
265}
266
267/// The undirected key of the edge `a`-`b`.
268fn key(a: u32, b: u32) -> (u32, u32) {
269    if a < b { (a, b) } else { (b, a) }
270}
271
272/// Whether `corners` carries both `a` and `b`.
273fn carries(corners: &[u32; 3], a: u32, b: u32) -> bool {
274    corners.contains(&a) && corners.contains(&b)
275}
276
277/// The corner of a triangle carrying `(a, b)` that is neither endpoint.
278fn apex_of(corners: &[u32; 3], a: u32, b: u32) -> u32 {
279    if corners[0] != a && corners[0] != b {
280        corners[0]
281    } else if corners[1] != a && corners[1] != b {
282        corners[1]
283    } else {
284        corners[2]
285    }
286}
287
288/// The alive working triangles carrying the edge `(a, b)`.
289fn owners_of(a: u32, b: u32, v2t: &[Vec<u32>], tris: &[WorkTri]) -> Result<Vec<usize>> {
290    let mut owners = Vec::new();
291    reserve(&mut owners, 4)?;
292    for &i in &v2t[a as usize] {
293        let t = &tris[i as usize];
294        if t.alive && carries(&t.corners, a, b) {
295            push(&mut owners, i as usize)?;
296        }
297    }
298    owners.sort_unstable();
299    owners.dedup();
300    Ok(owners)
301}
302
303/// The alive vertices sharing an alive triangle with `v`, without `v` itself.
304fn neighbors_of(v: u32, v2t: &[Vec<u32>], tris: &[WorkTri]) -> Result<Vec<u32>> {
305    let mut out = Vec::new();
306    reserve(&mut out, 16)?;
307    for &i in &v2t[v as usize] {
308        let t = &tris[i as usize];
309        if !t.alive {
310            continue;
311        }
312        if !t.corners.contains(&v) {
313            continue;
314        }
315        for &w in &t.corners {
316            if w != v {
317                push(&mut out, w)?;
318            }
319        }
320    }
321    out.sort_unstable();
322    out.dedup();
323    Ok(out)
324}
325
326fn tri_normal(p0: [f64; 3], p1: [f64; 3], p2: [f64; 3]) -> [f64; 3] {
327    cross3(sub3(p1, p0), sub3(p2, p0))
328}
329
330/// Shrinks the mesh toward `options.target_triangles` by quadric-error edge
331/// collapses (see the module docs). Boundary vertices stay put when
332/// `preserve_boundary` is set. Stops early when no valid collapse is left,
333/// which can leave more triangles than the target on meshes with locked or
334/// non-manifold edges.
335///
336/// A target at or above the live count, and an empty mesh, succeed without
337/// writing. Otherwise at least one collapse runs and the patch is non-empty.
338///
339/// # Errors
340/// [`OpsError::InvalidInput`] for a zero target; [`OpsError::Store`] when
341/// memory runs out.
342pub fn simplify(tx: &mut Transaction, options: &SimplifyOptions) -> Result<SimplifyReport> {
343    if options.target_triangles == 0 {
344        return Err(OpsError::InvalidInput(
345            "simplify target_triangles must be at least 1",
346        ));
347    }
348    let mesh = tx.mesh();
349    let base = mesh.positions();
350    let mut positions: Vec<[f32; 3]> = Vec::new();
351    reserve(&mut positions, base.len())?;
352    positions.extend_from_slice(base);
353    let slots = positions.len();
354    // The working UVs, on a mesh with them.
355    let mut uvs: Option<Vec<f32>> = None;
356    if let Some(from) = mesh.uvs() {
357        let mut copy = Vec::new();
358        reserve(&mut copy, from.len())?;
359        copy.extend_from_slice(from);
360        uvs = Some(copy);
361    }
362    let mut tris: Vec<WorkTri> = Vec::new();
363    reserve(&mut tris, mesh.triangle_slots())?;
364    for t in 0..mesh.triangle_slots() as u32 {
365        let Some(corners) = mesh.triangle(t) else {
366            continue;
367        };
368        if corners[0] == corners[1] || corners[1] == corners[2] || corners[2] == corners[0] {
369            continue;
370        }
371        push(
372            &mut tris,
373            WorkTri {
374                id: t,
375                corners,
376                alive: true,
377            },
378        )?;
379    }
380    let mut live = tris.len();
381    if live <= options.target_triangles {
382        return Ok(SimplifyReport::default());
383    }
384
385    let mut quadrics: Vec<Quadric> = Vec::new();
386    reserve(&mut quadrics, slots)?;
387    quadrics.resize(slots, [0.0; 10]);
388    for tri in &tris {
389        let c = tri.corners;
390        let q = face_quadric(
391            f64v(&positions[c[0] as usize]),
392            f64v(&positions[c[1] as usize]),
393            f64v(&positions[c[2] as usize]),
394        );
395        for &v in &c {
396            for k in 0..10 {
397                quadrics[v as usize][k] += q[k];
398            }
399        }
400    }
401
402    // Boundary vertices: endpoints of edges one live triangle uses.
403    let mut pairs: Vec<(u32, u32)> = Vec::new();
404    reserve(&mut pairs, 3 * tris.len())?;
405    for tri in &tris {
406        let c = tri.corners;
407        for k in 0..3 {
408            push(&mut pairs, key(c[k], c[(k + 1) % 3]))?;
409        }
410    }
411    pairs.sort_unstable();
412    let mut is_boundary = filled(slots, false)?;
413    {
414        let mut start = 0;
415        while start < pairs.len() {
416            let mut end = start + 1;
417            while end < pairs.len() && pairs[end] == pairs[start] {
418                end += 1;
419            }
420            if end - start == 1 {
421                is_boundary[pairs[start].0 as usize] = true;
422                is_boundary[pairs[start].1 as usize] = true;
423            }
424            start = end;
425        }
426    }
427    pairs.dedup();
428
429    let mut v2t: Vec<Vec<u32>> = Vec::new();
430    reserve(&mut v2t, slots)?;
431    v2t.resize_with(slots, Vec::new);
432    for (i, tri) in tris.iter().enumerate() {
433        for &v in &tri.corners {
434            push(&mut v2t[v as usize], i as u32)?;
435        }
436    }
437
438    let mut heap = BinaryHeap::new();
439    heap.try_reserve(pairs.len())
440        .map_err(|_| OpsError::Store(fast_mesh_store::StoreError::OutOfMemory))?;
441    for &(a, b) in &pairs {
442        if options.preserve_boundary && (is_boundary[a as usize] || is_boundary[b as usize]) {
443            continue;
444        }
445        let mut q = [0.0; 10];
446        for k in 0..10 {
447            q[k] = quadrics[a as usize][k] + quadrics[b as usize][k];
448        }
449        let (_, cost) = optimal_position(
450            &q,
451            f64v(&positions[a as usize]),
452            f64v(&positions[b as usize]),
453        );
454        heap.push(HeapEntry { cost, a, b });
455    }
456
457    let mut vlive = filled(slots, true)?;
458    let mut moved = filled(slots, false)?;
459    let mut collapsed = 0usize;
460    // Fresh neighbor lists after each collapse, so candidate edges are
461    // re-armed with current quadrics.
462    let mut scratch: Vec<u32> = Vec::new();
463
464    while live > options.target_triangles {
465        let Some(top) = heap.pop() else {
466            break;
467        };
468        let (a, b) = (top.a, top.b);
469        if !vlive[a as usize] || !vlive[b as usize] {
470            continue;
471        }
472        let owners = owners_of(a, b, &v2t, &tris)?;
473        if owners.is_empty() || owners.len() > 2 {
474            continue;
475        }
476        // Recompute with current quadrics; a stale heap cost re-queues.
477        let mut qsum = [0.0; 10];
478        for k in 0..10 {
479            qsum[k] = quadrics[a as usize][k] + quadrics[b as usize][k];
480        }
481        let pa = f64v(&positions[a as usize]);
482        let pb = f64v(&positions[b as usize]);
483        let (pnew, fresh) = optimal_position(&qsum, pa, pb);
484        if fresh > top.cost + 1e-9 * (1.0 + top.cost.abs()) {
485            heap.push(HeapEntry { cost: fresh, a, b });
486            continue;
487        }
488        // Link condition: the common neighbours must be exactly the apexes
489        // of the owners, or the merge would duplicate triangles.
490        let na = neighbors_of(a, &v2t, &tris)?;
491        let nb = neighbors_of(b, &v2t, &tris)?;
492        let mut apexes = Vec::new();
493        reserve(&mut apexes, owners.len())?;
494        for &o in &owners {
495            push(&mut apexes, apex_of(&tris[o].corners, a, b))?;
496        }
497        apexes.sort_unstable();
498        apexes.dedup();
499        if apexes.len() != owners.len() {
500            continue;
501        }
502        let mut common = Vec::new();
503        reserve(&mut common, na.len().min(nb.len()))?;
504        for &w in &na {
505            if w != a && w != b && nb.binary_search(&w).is_ok() {
506                push(&mut common, w)?;
507            }
508        }
509        if common != apexes {
510            continue;
511        }
512        // Keep the smaller id so repeated runs agree on the survivor.
513        let (keep, drop) = if a < b { (a, b) } else { (b, a) };
514        if !pnew.iter().all(|c| c.is_finite()) {
515            continue;
516        }
517        let pnew32 = [pnew[0] as f32, pnew[1] as f32, pnew[2] as f32];
518        if !pnew32.iter().all(|c| c.is_finite()) {
519            continue;
520        }
521        // No folded fan triangle: every repointed triangle must keep the
522        // sign of its normal and a non-zero area.
523        let mut fan: Vec<usize> = Vec::new();
524        reserve(&mut fan, na.len() + nb.len())?;
525        {
526            let mut seen: Vec<usize> = Vec::new();
527            for &v in &[a, b] {
528                for &i in &v2t[v as usize] {
529                    if !tris[i as usize].alive || owners.contains(&(i as usize)) {
530                        continue;
531                    }
532                    if seen.contains(&(i as usize)) {
533                        continue;
534                    }
535                    push(&mut seen, i as usize)?;
536                    push(&mut fan, i as usize)?;
537                }
538            }
539        }
540        let keep_pos = [
541            f64::from(pnew32[0]),
542            f64::from(pnew32[1]),
543            f64::from(pnew32[2]),
544        ];
545        let mut folds = false;
546        for &i in &fan {
547            let old = tris[i].corners;
548            let pts = [
549                f64v(&positions[old[0] as usize]),
550                f64v(&positions[old[1] as usize]),
551                f64v(&positions[old[2] as usize]),
552            ];
553            let before = tri_normal(pts[0], pts[1], pts[2]);
554            if dot3(before, before) == 0.0 {
555                folds = true;
556                break;
557            }
558            let new = old.map(|v| {
559                if v == keep || v == drop {
560                    keep_pos
561                } else {
562                    f64v(&positions[v as usize])
563                }
564            });
565            let after = tri_normal(new[0], new[1], new[2]);
566            if dot3(after, after) == 0.0 || dot3(after, before) <= 0.0 {
567                folds = true;
568                break;
569            }
570        }
571        if folds {
572            continue;
573        }
574
575        if let Some(uv) = &mut uvs {
576            let f = edge_param(keep_pos, pa, pb);
577            let (ia, ib, ik) = (2 * a as usize, 2 * b as usize, 2 * keep as usize);
578            let mixed = [0, 1].map(|c| uv[ia + c] + f * (uv[ib + c] - uv[ia + c]));
579            uv[ik] = mixed[0];
580            uv[ik + 1] = mixed[1];
581        }
582        quadrics[keep as usize] = qsum;
583        positions[keep as usize] = pnew32;
584        moved[keep as usize] = true;
585        for &o in &owners {
586            tris[o].alive = false;
587        }
588        live -= owners.len();
589        for &i in &fan {
590            let c = tris[i].corners.map(|v| if v == drop { keep } else { v });
591            tris[i].corners = c;
592            push(&mut v2t[keep as usize], i as u32)?;
593        }
594        v2t[drop as usize].clear();
595        vlive[drop as usize] = false;
596        collapsed += 1;
597
598        // Re-arm the survivor's edges with current quadrics.
599        scratch.clear();
600        reserve(&mut scratch, 16)?;
601        for &i in &v2t[keep as usize] {
602            let t = &tris[i as usize];
603            if !t.alive || !t.corners.contains(&keep) {
604                continue;
605            }
606            for &w in &t.corners {
607                if w != keep && vlive[w as usize] && !scratch.contains(&w) {
608                    push(&mut scratch, w)?;
609                }
610            }
611        }
612        scratch.sort_unstable();
613        for &w in scratch.iter() {
614            if options.preserve_boundary && (is_boundary[keep as usize] || is_boundary[w as usize])
615            {
616                continue;
617            }
618            let (lo, hi) = key(keep, w);
619            let mut q = [0.0; 10];
620            for k in 0..10 {
621                q[k] = quadrics[lo as usize][k] + quadrics[hi as usize][k];
622            }
623            let (_, cost) = optimal_position(
624                &q,
625                f64v(&positions[lo as usize]),
626                f64v(&positions[hi as usize]),
627            );
628            heap.push(HeapEntry { cost, a: lo, b: hi });
629        }
630    }
631
632    if collapsed == 0 {
633        return Ok(SimplifyReport::default());
634    }
635
636    // Reserve everything before the first write; past this point only
637    // infallible pushes into reserved capacity remain.
638    let mut set_p: Vec<VertexId> = Vec::new();
639    let mut set_p_to: Vec<[f32; 3]> = Vec::new();
640    let mut set_t: Vec<TriangleId> = Vec::new();
641    let mut set_t_to: Vec<[VertexId; 3]> = Vec::new();
642    let mut del_t: Vec<TriangleId> = Vec::new();
643    let mut del_v: Vec<VertexId> = Vec::new();
644    let mut set_uv: Vec<VertexId> = Vec::new();
645    let mut set_uv_to: Vec<f32> = Vec::new();
646    if let Some(uv) = &uvs {
647        for v in 0..slots as u32 {
648            if vlive[v as usize] && moved[v as usize] {
649                push(&mut set_uv, v)?;
650                let i = 2 * v as usize;
651                reserve(&mut set_uv_to, 2)?;
652                set_uv_to.extend_from_slice(&uv[i..i + 2]);
653            }
654        }
655    }
656    let mut n_moved = 0;
657    let mut n_rewired = 0;
658    let mut n_dead_tri = 0;
659    let mut n_dead_vert = 0;
660    for v in 0..slots as u32 {
661        if !vlive[v as usize] {
662            n_dead_vert += 1;
663        } else if moved[v as usize] && positions[v as usize] != base[v as usize] {
664            n_moved += 1;
665        }
666    }
667    for tri in &tris {
668        if !tri.alive {
669            n_dead_tri += 1;
670        } else {
671            let current = mesh
672                .triangle(tri.id)
673                .expect("simplify: working id must exist");
674            if current != tri.corners {
675                n_rewired += 1;
676            }
677        }
678    }
679    reserve(&mut set_p, n_moved)?;
680    reserve(&mut set_p_to, n_moved)?;
681    reserve(&mut set_t, n_rewired)?;
682    reserve(&mut set_t_to, n_rewired)?;
683    reserve(&mut del_t, n_dead_tri)?;
684    reserve(&mut del_v, n_dead_vert)?;
685    for v in 0..slots as u32 {
686        if !vlive[v as usize] {
687            del_v.push(v);
688        } else if moved[v as usize] && positions[v as usize] != base[v as usize] {
689            set_p.push(v);
690            set_p_to.push(positions[v as usize]);
691        }
692    }
693    for tri in &tris {
694        if !tri.alive {
695            del_t.push(tri.id);
696        } else {
697            let current = mesh
698                .triangle(tri.id)
699                .expect("simplify: working id must exist");
700            if current != tri.corners {
701                set_t.push(tri.id);
702                set_t_to.push(tri.corners);
703            }
704        }
705    }
706    tx.set_positions(&set_p, &set_p_to)?;
707    if uvs.is_some() {
708        tx.write_channel(UV_CHANNEL, &set_uv, &set_uv_to)?;
709    }
710    tx.set_triangles(&set_t, &set_t_to)?;
711    tx.remove_triangles(&del_t)?;
712    tx.remove_vertices(&del_v)?;
713
714    let live_now = tris.iter().filter(|t| t.alive).count();
715    let started = live_now + del_t.len();
716    Ok(SimplifyReport {
717        collapsed,
718        triangles_removed: started - live_now,
719        vertices_removed: del_v.len(),
720    })
721}
722
723#[cfg(test)]
724mod tests {
725    use fast_mesh_store::{Mesh, Transaction, VertexId};
726
727    use super::*;
728    use crate::test_util::{box_mesh, edit, grid, mesh_of, parts, sweep, uv_sphere};
729
730    fn live_count(mesh: &Mesh) -> usize {
731        (0..mesh.triangle_slots() as u32)
732            .filter(|&t| mesh.triangle(t).is_some())
733            .count()
734    }
735
736    fn assert_no_degenerate(mesh: &Mesh) {
737        for t in 0..mesh.triangle_slots() as u32 {
738            if let Some(c) = mesh.triangle(t) {
739                assert!(
740                    c[0] != c[1] && c[1] != c[2] && c[2] != c[0],
741                    "degenerate: {c:?}"
742                );
743                for &v in &c {
744                    assert!(mesh.is_vertex_alive(v as VertexId));
745                }
746            }
747        }
748    }
749
750    #[test]
751    fn decimate_cube_to_target_ratio() {
752        let mut mesh = box_mesh([0.0; 3], [1.0; 3]);
753        assert_eq!(live_count(&mesh), 12);
754        let options = SimplifyOptions::builder().target_triangles(6).build();
755        let (report, empty) = edit(&mut mesh, |tx| simplify(tx, &options));
756        assert!(!empty);
757        assert!(report.collapsed > 0, "expected collapses, got {report:?}");
758        let now = live_count(&mesh);
759        assert!(now <= 6, "expected at most 6 triangles, got {now}");
760        assert!(now > 0, "must keep some triangles");
761        assert_eq!(report.triangles_removed, 12 - now);
762        assert_eq!(report.vertices_removed, report.collapsed);
763        assert_no_degenerate(&mesh);
764    }
765
766    #[test]
767    fn decimate_sphere_to_target_ratio() {
768        let mut mesh = uv_sphere(8, 16, 1.0);
769        let before = live_count(&mesh);
770        assert!(before > 100, "fixture too small: {before}");
771        let target = before / 2;
772        let options = SimplifyOptions::builder().target_triangles(target).build();
773        let (report, empty) = edit(&mut mesh, |tx| simplify(tx, &options));
774        assert!(!empty);
775        assert!(report.collapsed > 0, "expected collapses, got {report:?}");
776        let now = live_count(&mesh);
777        assert!(
778            now <= target,
779            "expected at most {target} triangles, got {now}"
780        );
781        assert!(now > 0, "must keep some triangles");
782        assert_no_degenerate(&mesh);
783    }
784
785    #[test]
786    fn boundary_preserved_when_enabled() {
787        // A 4x4 grid: 25 vertices, 16 on the rim. Locking the rim keeps
788        // every rim vertex alive at its position.
789        let mut mesh = grid(4);
790        let rim: Vec<[f32; 3]> = mesh.positions().to_vec();
791        let options = SimplifyOptions::builder()
792            .target_triangles(16)
793            .preserve_boundary(true)
794            .build();
795        let (report, empty) = edit(&mut mesh, |tx| simplify(tx, &options));
796        assert!(!empty);
797        assert!(report.collapsed > 0, "expected collapses, got {report:?}");
798        assert!(live_count(&mesh) <= 16);
799        let n = 4 + 1;
800        for y in 0..=4 {
801            for x in 0..=4 {
802                if x == 0 || y == 0 || x == 4 || y == 4 {
803                    let v = y * n + x;
804                    assert!(
805                        mesh.is_vertex_alive(v as VertexId),
806                        "rim vertex {v} removed"
807                    );
808                    assert_eq!(mesh.positions()[v as usize], rim[v as usize]);
809                }
810            }
811        }
812        assert_no_degenerate(&mesh);
813    }
814
815    #[test]
816    fn boundary_option_changes_locked_quad() {
817        // Two triangles, every vertex on the rim: locked, nothing collapses;
818        // unlocked, the diagonal collapses.
819        let quad = || {
820            mesh_of(
821                &[0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 4.0, 1.0, 0.0, 0.0, 1.0, 0.0],
822                &[[0, 1, 2], [0, 2, 3]],
823            )
824        };
825        let mut locked = quad();
826        let before = parts(&locked);
827        let (report, empty) = edit(&mut locked, |tx| {
828            simplify(
829                tx,
830                &SimplifyOptions::builder()
831                    .target_triangles(1)
832                    .preserve_boundary(true)
833                    .build(),
834            )
835        });
836        assert_eq!((report.collapsed, empty), (0, true));
837        assert_eq!(parts(&locked), before);
838
839        let mut open = quad();
840        let (report, empty) = edit(&mut open, |tx| {
841            simplify(
842                tx,
843                &SimplifyOptions::builder()
844                    .target_triangles(1)
845                    .preserve_boundary(false)
846                    .build(),
847            )
848        });
849        assert!(!empty);
850        assert!(report.collapsed > 0, "expected collapses, got {report:?}");
851        assert!(live_count(&open) < 2);
852    }
853
854    #[test]
855    fn invalid_target_and_noop_target() {
856        let mut mesh = box_mesh([0.0; 3], [1.0; 3]);
857        {
858            let mut tx = mesh.begin_edit();
859            assert_eq!(
860                simplify(&mut tx, &SimplifyOptions::builder().build()),
861                Err(OpsError::InvalidInput(
862                    "simplify target_triangles must be at least 1"
863                ))
864            );
865        }
866        // Target at/above the count succeeds without writing.
867        let mut mesh = box_mesh([0.0; 3], [1.0; 3]);
868        let before = parts(&mesh);
869        let (report, empty) = edit(&mut mesh, |tx| {
870            simplify(tx, &SimplifyOptions::builder().target_triangles(12).build())
871        });
872        assert_eq!((report.collapsed, empty), (0, true));
873        assert_eq!(parts(&mesh), before);
874        let (report, empty) = edit(&mut mesh, |tx| {
875            simplify(
876                tx,
877                &SimplifyOptions::builder().target_triangles(10_000).build(),
878            )
879        });
880        assert_eq!((report.collapsed, empty), (0, true));
881        assert_eq!(parts(&mesh), before);
882    }
883
884    #[test]
885    fn simplify_sweep_keeps_atomicity() {
886        let n = sweep(grid(4), |tx: &mut Transaction| {
887            simplify(tx, &SimplifyOptions::builder().target_triangles(16).build())?;
888            Ok(())
889        });
890        assert!(n > 0);
891    }
892}