Skip to main content

ogeom_mesh/
simplify.rs

1//! Decimating a triangulation.
2//!
3//! A mesh fine enough for a mass property is far finer than one needed to draw,
4//! and a mesh fine enough to draw a whole assembly is far finer than one needed
5//! for the bolt in the corner of it. Decimation is how one tessellation serves
6//! both without being computed twice.
7//!
8//! # The error is measured, not hoped for
9//!
10//! Collapsing an edge moves the surface. *How far* it moves is what decides
11//! whether the collapse is worth making, so every candidate carries the squared
12//! distance from the merged vertex to the planes of every face that met there:
13//! the quadric error metric of Garland and Heckbert. Summing plane distances
14//! this way costs one small symmetric matrix per vertex and makes the choice a
15//! comparison rather than a guess.
16//!
17//! The result reports the worst error it accepted. A decimation that returned
18//! only a smaller mesh would be one nothing downstream could decide to trust.
19//!
20//! # What it will not touch
21//!
22//! A vertex on a boundary stays. The alternative is a constraint plane that
23//! makes boundary collapses expensive but possible, and "expensive but
24//! possible" means the outline of a sheet body creeps inward as the mesh
25//! coarsens, which is exactly the thing a caller would not think to check.
26//! Holding the boundary exactly is a stronger promise and a simpler one.
27//!
28//! A collapse that would turn a triangle inside out is refused for the same
29//! reason: a fold is not a small error, it is a mesh that no longer bounds what
30//! it did.
31//!
32//! # Verified
33//!
34//! The quadric bounds nothing on its own: it is a sum over planes, and the
35//! nearest point of the source need not be on any of them. [`Target::Verified`]
36//! makes a collapse only where the points it moves stay within the tolerance
37//! of the other mesh and its triangles meet no others but at shared corners,
38//! then measures the result against the source both ways, pins what fails
39//! and decimates again; see [`Deviation`] for what it reports.
40
41use std::cmp::Reverse;
42use std::collections::BinaryHeap;
43
44use ogeom_core::{FastMap, FastSet};
45use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
46use ogeom_math::Point;
47use ogeom_topo::Triangulation;
48
49mod checked;
50mod verified;
51
52/// How far to decimate.
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum Target {
55    /// Stop once the mesh is down to this many triangles, whatever the error.
56    Triangles(usize),
57    /// Collapse only while the error stays within this distance.
58    ///
59    /// The honest option: the caller says how wrong the mesh may be and gets
60    /// however few triangles that allows, rather than naming a count and
61    /// discovering the error afterwards.
62    Error(f64),
63    /// Collapse within `tolerance`, then measure the result against the
64    /// source both ways and hold whatever the measurement refuses.
65    ///
66    /// Each source vertex is measured to the result, and each result vertex
67    /// and triangle centre to the source. A collapse is made only where
68    /// every point it moves stays within `tolerance`, and only where its
69    /// triangles meet no other triangle but at the corners they share, so a
70    /// mesh that bounded a solid still does. Where a point is farther than
71    /// `tolerance` all the same, which rounding alone can cause, the source
72    /// vertices behind it are pinned and the decimation is run again from
73    /// the source, until at least `share` of the points on each side are
74    /// within `tolerance`. The measurement is reported in
75    /// [`Simplified::deviation`].
76    ///
77    /// With `max_triangles`, a result still larger than that is decimated
78    /// further, pins released, and the deviation reported is the one
79    /// measured on what came out, however far past `tolerance` it is.
80    Verified {
81        /// How far, as a distance, a point may be from the other mesh.
82        tolerance: f64,
83        /// The fraction of points on each side that must be within
84        /// `tolerance`, in `(0, 1]`; 0.999 is a scan's usual demand.
85        share: f64,
86        /// At most this many triangles, whatever the deviation.
87        max_triangles: Option<usize>,
88    },
89}
90
91/// The distance between a decimated mesh and its source, measured both ways.
92///
93/// The points measured are the source's vertices, against the result, and
94/// the result's vertices and triangle centres, against the source; each
95/// distance is to the nearest point on the other mesh's triangles.
96#[derive(Debug, Clone, Copy, PartialEq)]
97pub struct Deviation {
98    /// The smaller of the two sides' fractions of points within the
99    /// tolerance.
100    pub within_share: f64,
101    /// The distance within which the asked share of points lies, on the
102    /// worse of the two sides.
103    pub distance_at_share: f64,
104    /// The point farthest from the other mesh, on either side, and its
105    /// distance.
106    pub furthest: (Point, f64),
107    /// The root mean square distance over every point measured, both sides.
108    pub rms: f64,
109    /// How many source vertices the measurement pinned, counted before a
110    /// `max_triangles` released them.
111    pub pinned: usize,
112    /// How many decimations were run, each from the source.
113    pub rounds: usize,
114}
115
116/// What decimation produced.
117#[derive(Debug, Clone)]
118pub struct Simplified {
119    /// The decimated mesh.
120    pub mesh: Triangulation,
121    /// The worst error accepted, as a distance.
122    ///
123    /// Zero when nothing was collapsed. Every vertex of the result is within
124    /// this of the surface the original described.
125    pub error: f64,
126    /// How many edge collapses were made.
127    pub collapsed: usize,
128    /// Whether the target was reached.
129    ///
130    /// A mesh can run out of *valid* collapses before it runs out of triangles
131    /// (every remaining edge is on a boundary or would fold something), and
132    /// then the result is as small as it can safely be rather than as small as
133    /// was asked for. Reported rather than passed off as success.
134    pub target_met: bool,
135    /// With [`Target::Verified`], what was measured; `None` otherwise.
136    ///
137    /// [`Simplified::error`] is then [`Deviation::furthest`]'s distance.
138    pub deviation: Option<Deviation>,
139    /// For each vertex of the source, the vertex of [`Simplified::mesh`]
140    /// it was merged into or kept as; `None` for one no triangle of the
141    /// result uses, as a vertex no triangle of the source used. The source
142    /// vertices of one result vertex are those its collapses merged.
143    pub result_vertex: Vec<Option<u32>>,
144}
145
146/// Decimate a mesh.
147///
148/// # Errors
149///
150/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the target is
151/// not a positive count or a positive distance, a share is not in `(0, 1]`, or
152/// the mesh names a vertex it does not have;
153/// [`OgeomError::Numeric`](ogeom_core::OgeomError::Numeric) with
154/// [`Target::Verified`] if a vertex of a triangle is not finite;
155/// [`OgeomError::Range`](ogeom_core::OgeomError::Range) if the mesh has more
156/// than `u32::MAX` triangles.
157pub fn simplify(mesh: &Triangulation, target: Target, tol: Tolerances) -> OgeomResult<Simplified> {
158    match target {
159        Target::Verified { tolerance, .. } if !tolerance.is_finite() || tolerance <= 0.0 => {
160            ogeom_bail!(Construction, "a tolerance of {tolerance} is not a distance");
161        }
162        Target::Verified { share, .. } if !(share > 0.0 && share <= 1.0) => {
163            ogeom_bail!(
164                Construction,
165                "a share of {share} is not a fraction of the points"
166            );
167        }
168        Target::Verified {
169            max_triangles: Some(0),
170            ..
171        } => {
172            ogeom_bail!(Construction, "a mesh of no triangles describes nothing");
173        }
174        Target::Triangles(0) => {
175            ogeom_bail!(Construction, "a mesh of no triangles describes nothing");
176        }
177        Target::Error(e) if !e.is_finite() || e <= 0.0 => {
178            ogeom_bail!(Construction, "an error budget of {e} is not a distance");
179        }
180        _ => {}
181    }
182    if u32::try_from(mesh.triangles.len()).is_err() {
183        ogeom_bail!(Range, "a mesh of more than four billion triangles");
184    }
185    for triangle in &mesh.triangles {
186        for index in triangle {
187            if *index as usize >= mesh.positions.len() {
188                ogeom_bail!(
189                    Construction,
190                    "a triangle names vertex {index}, and the mesh has {}",
191                    mesh.positions.len()
192                );
193            }
194        }
195    }
196
197    let (budget, floor) = match target {
198        Target::Error(e) => (e * e, 1),
199        Target::Triangles(n) => (f64::MAX, n),
200        Target::Verified {
201            tolerance,
202            share,
203            max_triangles,
204        } => return verified::simplify(mesh, tolerance, share, max_triangles, tol),
205    };
206    let mut state = State::new(mesh, tol);
207
208    // Within reach the queue makes the collapses the scan makes, sooner.
209    let (worst, collapsed) = if mesh.positions.iter().all(|p| within_reach(*p)) {
210        state.decimate_by_queue(budget, floor)
211    } else {
212        state.decimate_by_scan(budget, floor)
213    };
214
215    let target_met = match target {
216        Target::Triangles(n) => state.live_triangles() <= n,
217        // Every collapse made was inside the budget, and the loop stops only
218        // when no remaining one is.
219        Target::Error(_) | Target::Verified { .. } => true,
220    };
221    let (mesh, renumbered) = state.harvest();
222    Ok(Simplified {
223        mesh,
224        error: worst.max(0.0).sqrt(),
225        collapsed,
226        target_met,
227        deviation: None,
228        result_vertex: state.result_vertex(&renumbered),
229    })
230}
231
232/// Whether every coordinate of `p` is at most 2^100 in size. A quadric
233/// summed over at most 2^32 triangles of such points, weighed at such a
234/// point, stays below 2^645 term by term, so no cost overflows to infinity
235/// or turns NaN, and costs compare as numbers.
236fn within_reach(p: Point) -> bool {
237    const REACH: f64 = 1_267_650_600_228_229_401_496_703_205_376.0; // 2^100
238    [p.x, p.y, p.z].iter().all(|c| c.abs() <= REACH)
239}
240
241/// A collapse [`State::cheapest`] would make, as the queue holds it:
242/// cheapest first, ties to the edge the scan of live triangles meets
243/// first.
244#[derive(Debug, Clone, Copy)]
245struct Candidate {
246    cost: f64,
247    /// Three times the index of the first live triangle holding the edge,
248    /// plus the edge's slot in it.
249    place: u64,
250    /// The edge's ends, as that triangle walks it.
251    from: u32,
252    to: u32,
253}
254
255impl Candidate {
256    /// Whether two candidates are the same collapse at the same cost.
257    fn same(&self, other: &Self) -> bool {
258        self.cost.to_bits() == other.cost.to_bits()
259            && self.place == other.place
260            && self.from == other.from
261            && self.to == other.to
262    }
263}
264
265impl PartialEq for Candidate {
266    fn eq(&self, other: &Self) -> bool {
267        self.cmp(other) == core::cmp::Ordering::Equal
268    }
269}
270
271impl Eq for Candidate {}
272
273impl PartialOrd for Candidate {
274    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
275        Some(self.cmp(other))
276    }
277}
278
279impl Ord for Candidate {
280    /// By cost, as [`State::cheapest`] compares them (costs within reach
281    /// are numbers, so this is a total order), then by place.
282    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
283        self.cost
284            .partial_cmp(&other.cost)
285            .unwrap_or(core::cmp::Ordering::Equal)
286            .then(self.place.cmp(&other.place))
287    }
288}
289
290/// A 4x4 symmetric quadric, as its upper triangle.
291///
292/// `v^T Q v` is the sum of squared distances from `v` to a set of planes, which
293/// is what makes these addable: the error of merging two vertices is the error
294/// of one plus the error of the other.
295#[derive(Debug, Clone, Copy, Default)]
296struct Quadric([f64; 10]);
297
298impl Quadric {
299    /// The quadric of one plane `ax + by + cz + d = 0`, with a unit normal.
300    fn of_plane(a: f64, b: f64, c: f64, d: f64) -> Self {
301        Self([
302            a * a,
303            a * b,
304            a * c,
305            a * d,
306            b * b,
307            b * c,
308            b * d,
309            c * c,
310            c * d,
311            d * d,
312        ])
313    }
314
315    fn add(&mut self, other: &Self) {
316        for (a, b) in self.0.iter_mut().zip(other.0) {
317            *a += b;
318        }
319    }
320
321    /// The total weight of the planes summed in: each plane's unit normal
322    /// puts its weight once on the diagonal.
323    fn weight(&self) -> f64 {
324        self.0[0] + self.0[4] + self.0[7]
325    }
326
327    /// The squared distance this quadric assigns to a point.
328    fn at(&self, p: Point) -> f64 {
329        let [q00, q01, q02, q03, q11, q12, q13, q22, q23, q33] = self.0;
330        let (x, y, z) = (p.x, p.y, p.z);
331        q00 * x * x
332            + 2.0 * q01 * x * y
333            + 2.0 * q02 * x * z
334            + 2.0 * q03 * x
335            + q11 * y * y
336            + 2.0 * q12 * y * z
337            + 2.0 * q13 * y
338            + q22 * z * z
339            + 2.0 * q23 * z
340            + q33
341    }
342}
343
344/// The mesh mid-decimation.
345struct State {
346    positions: Vec<Point>,
347    triangles: Vec<[u32; 3]>,
348    /// Whether each triangle is still there.
349    live: Vec<bool>,
350    quadrics: Vec<Quadric>,
351    /// Vertices that must not move: the ones on a boundary.
352    pinned: Vec<bool>,
353    /// Further vertices that must not move: the ones a verified decimation's
354    /// measurement held.
355    held: Vec<bool>,
356    /// The vertex each vertex was merged into; itself while it survives.
357    merged_into: Vec<u32>,
358    /// Which triangles touch each vertex, live or not; empty for a vertex
359    /// no triangle names or one merged away.
360    around: Vec<Vec<u32>>,
361    tol: Tolerances,
362    remaining: usize,
363}
364
365impl State {
366    fn new(mesh: &Triangulation, tol: Tolerances) -> Self {
367        let mut quadrics = vec![Quadric::default(); mesh.positions.len()];
368        let mut around: Vec<Vec<u32>> = vec![Vec::new(); mesh.positions.len()];
369        let mut uses: FastMap<(u32, u32), usize> = FastMap::default();
370
371        for (i, triangle) in mesh.triangles.iter().enumerate() {
372            let [a, b, c] = triangle.map(|v| mesh.positions[v as usize]);
373            let normal = (b - a).cross(c - a);
374            let length = normal.magnitude();
375            if length > tol.confusion() {
376                let unit = normal * (1.0 / length);
377                // Weighted by area, so a large flat region is not outvoted by a
378                // cluster of slivers describing the same plane.
379                let plane = Quadric::of_plane(unit.x, unit.y, unit.z, -unit.dot(a.to_vector()));
380                let mut weighted = plane;
381                for value in &mut weighted.0 {
382                    *value *= length;
383                }
384                for v in triangle {
385                    quadrics[*v as usize].add(&weighted);
386                }
387            }
388            for v in triangle {
389                #[allow(clippy::cast_possible_truncation)]
390                around[*v as usize].push(i as u32);
391            }
392            for k in 0..3 {
393                let (x, y) = (triangle[k], triangle[(k + 1) % 3]);
394                *uses.entry((x.min(y), x.max(y))).or_default() += 1;
395            }
396        }
397
398        // A boundary edge is used once. Both its ends are held.
399        let mut pinned = vec![false; mesh.positions.len()];
400        for ((a, b), count) in uses {
401            if count != 2 {
402                pinned[a as usize] = true;
403                pinned[b as usize] = true;
404            }
405        }
406
407        Self {
408            positions: mesh.positions.clone(),
409            triangles: mesh.triangles.clone(),
410            live: vec![true; mesh.triangles.len()],
411            quadrics,
412            pinned,
413            held: vec![false; mesh.positions.len()],
414            #[allow(clippy::cast_possible_truncation)]
415            merged_into: (0..mesh.positions.len() as u32).collect(),
416            around,
417            tol,
418            remaining: mesh.triangles.len(),
419        }
420    }
421
422    const fn live_triangles(&self) -> usize {
423        self.remaining
424    }
425
426    /// The cheapest collapse still worth making, within a squared-error budget,
427    /// ties to the edge met first walking the live triangles in order.
428    ///
429    /// The whole mesh is walked at every call. [`State::decimate_by_queue`]
430    /// takes the same collapses in the same order from a queue.
431    fn cheapest(&self, budget: f64) -> Option<(f64, u32, u32, Point)> {
432        let mut best: Option<(f64, u32, u32, Point)> = None;
433        let mut seen: FastSet<(u32, u32)> = FastSet::default();
434        for (i, triangle) in self.triangles.iter().enumerate() {
435            if !self.live[i] {
436                continue;
437            }
438            for k in 0..3 {
439                let (a, b) = (triangle[k], triangle[(k + 1) % 3]);
440                let key = (a.min(b), a.max(b));
441                if !seen.insert(key) {
442                    continue;
443                }
444                // Either end pinned means the edge is on a boundary, or leads
445                // to one. Holding both ends holds the outline exactly.
446                if self.pinned[a as usize] || self.pinned[b as usize] {
447                    continue;
448                }
449                let at = Point::from_vector(
450                    (self.positions[a as usize].to_vector()
451                        + self.positions[b as usize].to_vector())
452                        * 0.5,
453                );
454                let mut merged = self.quadrics[a as usize];
455                merged.add(&self.quadrics[b as usize]);
456                let cost = merged.at(at).max(0.0);
457                if cost > budget {
458                    continue;
459                }
460                if best.is_some_and(|(current, ..)| cost >= current) {
461                    continue;
462                }
463                if self.would_fold(a, b, at) || !self.keeps_manifold(a, b) {
464                    continue;
465                }
466                best = Some((cost, a, b, at));
467            }
468        }
469        best
470    }
471
472    /// Collapse the cheapest edge within `budget`, by [`State::cheapest`],
473    /// while more than `floor` triangles remain: the worst cost accepted
474    /// and how many collapses were made.
475    fn decimate_by_scan(&mut self, budget: f64, floor: usize) -> (f64, usize) {
476        let mut worst = 0.0_f64;
477        let mut collapsed = 0;
478        while self.live_triangles() > floor {
479            let Some((cost, from, to, at)) = self.cheapest(budget) else {
480                break;
481            };
482            self.collapse(from, to, at);
483            worst = worst.max(cost);
484            collapsed += 1;
485        }
486        (worst, collapsed)
487    }
488
489    /// [`State::decimate_by_scan`] from a queue: the same collapses in the
490    /// same order, each found without walking the whole mesh.
491    ///
492    /// The queue holds every collapse [`State::cheapest`] would weigh, at
493    /// the weight it would give it at this step, and stale entries
494    /// besides. A collapse
495    /// changes the weighing only of the edges with an end among the
496    /// vertices about either end of the collapsed edge, before or after,
497    /// or the merged vertex itself: their cost, the first live triangle
498    /// holding them, and whether they fold or break the manifold read
499    /// nothing else. Those are weighed again and queued. An entry is taken
500    /// only where its edge weighs the same when it comes up, so the first
501    /// taken is the cheapest edge, ties to the one the scan meets first,
502    /// which is what the scan takes. The queue is built afresh once it
503    /// holds more than three entries a live triangle, half of them stale
504    /// at least.
505    fn decimate_by_queue(&mut self, budget: f64, floor: usize) -> (f64, usize) {
506        let mut worst = 0.0_f64;
507        let mut collapsed = 0;
508        let mut queue = self.queue(budget);
509        let mut fresh: FastSet<(u32, u32)> = FastSet::default();
510        while self.live_triangles() > floor {
511            let Some(Reverse(taken)) = queue.pop() else {
512                break;
513            };
514            let Some((now, at)) = self.candidate(taken.from, taken.to, budget) else {
515                continue;
516            };
517            if !now.same(&taken) {
518                continue;
519            }
520            let mut near = self.ring(now.from);
521            near.extend(self.ring(now.to));
522            self.collapse(now.from, now.to, at);
523            near.extend(self.ring(now.to));
524            near.push(now.to);
525            near.sort_unstable();
526            near.dedup();
527            fresh.clear();
528            for &v in &near {
529                for w in self.ring(v) {
530                    if fresh.insert((v.min(w), v.max(w)))
531                        && let Some((again, _)) = self.candidate(v, w, budget)
532                    {
533                        queue.push(Reverse(again));
534                    }
535                }
536            }
537            worst = worst.max(now.cost);
538            collapsed += 1;
539            // Each live triangle holds three edge sides and each live edge
540            // two: past three entries a triangle, at least half are stale.
541            if queue.len() > 3 * self.live_triangles() {
542                queue = self.queue(budget);
543            }
544        }
545        (worst, collapsed)
546    }
547
548    /// Every collapse [`State::cheapest`] would weigh now, queued.
549    fn queue(&self, budget: f64) -> BinaryHeap<Reverse<Candidate>> {
550        let mut queue = BinaryHeap::new();
551        let mut seen: FastSet<(u32, u32)> = FastSet::default();
552        for (i, triangle) in self.triangles.iter().enumerate() {
553            if !self.live[i] {
554                continue;
555            }
556            for k in 0..3 {
557                let (a, b) = (triangle[k], triangle[(k + 1) % 3]);
558                if seen.insert((a.min(b), a.max(b)))
559                    && let Some((c, _)) = self.candidate(a, b, budget)
560                {
561                    queue.push(Reverse(c));
562                }
563            }
564        }
565        queue
566    }
567
568    /// The collapse of the edge between `a` and `b` as [`State::cheapest`]
569    /// weighs it, and the point it merges at: `None` where no live triangle
570    /// holds the edge, an end is pinned, the cost is past `budget`, or the
571    /// collapse would fold or break the manifold.
572    fn candidate(&self, a: u32, b: u32, budget: f64) -> Option<(Candidate, Point)> {
573        if self.pinned[a as usize] || self.pinned[b as usize] {
574            return None;
575        }
576        let mut first: Option<(u64, u32, u32)> = None;
577        for &index in &self.around[a as usize] {
578            if !self.live[index as usize] {
579                continue;
580            }
581            let t = self.triangles[index as usize];
582            for k in 0..3 {
583                let (x, y) = (t[k], t[(k + 1) % 3]);
584                if (x == a && y == b) || (x == b && y == a) {
585                    let place = 3 * u64::from(index) + k as u64;
586                    if first.is_none_or(|f| place < f.0) {
587                        first = Some((place, x, y));
588                    }
589                }
590            }
591        }
592        let (place, from, to) = first?;
593        let at = Point::from_vector(
594            (self.positions[from as usize].to_vector() + self.positions[to as usize].to_vector())
595                * 0.5,
596        );
597        let mut merged = self.quadrics[from as usize];
598        merged.add(&self.quadrics[to as usize]);
599        let cost = merged.at(at).max(0.0);
600        if cost > budget || self.would_fold(from, to, at) || !self.keeps_manifold(from, to) {
601            return None;
602        }
603        Some((
604            Candidate {
605                cost,
606                place,
607                from,
608                to,
609            },
610            at,
611        ))
612    }
613
614    /// Whether merging two vertices would turn any surviving triangle over.
615    ///
616    /// A fold is not a small error. It is a mesh that no longer bounds what it
617    /// did, and no error metric measures that: the merged point can sit
618    /// exactly on every plane and still put the triangle back to front.
619    fn would_fold(&self, from: u32, to: u32, at: Point) -> bool {
620        for vertex in [from, to] {
621            for &index in &self.around[vertex as usize] {
622                if !self.live[index as usize] {
623                    continue;
624                }
625                let triangle = self.triangles[index as usize];
626                // Triangles containing both vanish with the collapse.
627                if triangle.contains(&from) && triangle.contains(&to) {
628                    continue;
629                }
630                let before = self.normal_of(triangle, None);
631                let after = self.normal_of(triangle, Some((from, to, at)));
632                let (Some(before), Some(after)) = (before, after) else {
633                    return true;
634                };
635                if before.dot(after) <= 0.0 {
636                    return true;
637                }
638            }
639        }
640        false
641    }
642
643    /// Whether merging two vertices keeps the faces about them a manifold:
644    /// the vertices they both neighbour are those across the edge between
645    /// them, and no two faces would be left on the same three vertices.
646    ///
647    /// A third neighbour in common joins two edges into one with four
648    /// faces; two faces on the same three vertices, as when a tetrahedron
649    /// collapses, close over each other. Neither is a coarser surface.
650    fn keeps_manifold(&self, a: u32, b: u32) -> bool {
651        let mut across = Vec::new();
652        for &index in &self.around[a as usize] {
653            let t = self.triangles[index as usize];
654            if self.live[index as usize] && t.contains(&b) {
655                across.extend(t.iter().filter(|&&w| w != a && w != b));
656            }
657        }
658        across.sort_unstable();
659        across.dedup();
660        let (ring_a, ring_b) = (self.ring(a), self.ring(b));
661        let common = ring_a.iter().filter(|w| ring_b.binary_search(w).is_ok());
662        if !common.copied().eq(across.iter().copied()) {
663            return false;
664        }
665        let face = |v: u32, x: u32, y: u32| {
666            self.around[v as usize].iter().any(|&index| {
667                let t = self.triangles[index as usize];
668                self.live[index as usize] && t.contains(&x) && t.contains(&y)
669            })
670        };
671        !(across.len() == 2 && face(a, across[0], across[1]) && face(b, across[0], across[1]))
672    }
673
674    /// A triangle's normal, optionally with one collapse applied.
675    fn normal_of(
676        &self,
677        triangle: [u32; 3],
678        collapse: Option<(u32, u32, Point)>,
679    ) -> Option<ogeom_math::Vector> {
680        let at = |v: u32| match collapse {
681            Some((from, to, p)) if v == from || v == to => p,
682            _ => self.positions[v as usize],
683        };
684        let (a, b, c) = (at(triangle[0]), at(triangle[1]), at(triangle[2]));
685        let normal = (b - a).cross(c - a);
686        if normal.magnitude() <= self.tol.confusion() {
687            return None;
688        }
689        Some(normal)
690    }
691
692    /// Merge two vertices at a point.
693    fn collapse(&mut self, from: u32, to: u32, at: Point) {
694        self.positions[to as usize] = at;
695        self.merged_into[from as usize] = to;
696        let mut merged = self.quadrics[from as usize];
697        merged.add(&self.quadrics[to as usize]);
698        self.quadrics[to as usize] = merged;
699
700        let touching = std::mem::take(&mut self.around[from as usize]);
701        let mut emptied = Vec::new();
702        for index in touching {
703            if !self.live[index as usize] {
704                continue;
705            }
706            let triangle = &mut self.triangles[index as usize];
707            for v in triangle.iter_mut() {
708                if *v == from {
709                    *v = to;
710                }
711            }
712            // A triangle naming one vertex twice has no area left.
713            let [a, b, c] = *triangle;
714            if a == b || b == c || c == a {
715                self.live[index as usize] = false;
716                self.remaining -= 1;
717                emptied.extend([a, b, c]);
718            } else {
719                self.around[to as usize].push(index);
720            }
721        }
722        // The vertices about a triangle that went drop it, the order of the
723        // rest kept, so that a vertex that has taken in many others does
724        // not walk their dead triangles at every look.
725        emptied.sort_unstable();
726        emptied.dedup();
727        for v in emptied {
728            let live = &self.live;
729            self.around[v as usize].retain(|&t| live[t as usize]);
730        }
731    }
732
733    /// The surviving mesh, with unused vertices dropped, and each surviving
734    /// vertex's number in it.
735    fn harvest(&self) -> (Triangulation, FastMap<u32, u32>) {
736        let mut out = Triangulation::new();
737        let mut moved: FastMap<u32, u32> = FastMap::default();
738        for (index, triangle) in self.triangles.iter().enumerate() {
739            if !self.live[index] {
740                continue;
741            }
742            let mut mapped = [0_u32; 3];
743            for (slot, v) in mapped.iter_mut().zip(triangle) {
744                *slot = *moved.entry(*v).or_insert_with(|| {
745                    #[allow(clippy::cast_possible_truncation)]
746                    let fresh = out.positions.len() as u32;
747                    out.positions.push(self.positions[*v as usize]);
748                    fresh
749                });
750            }
751            out.triangles.push(mapped);
752        }
753        // Normals are recomputed from the surviving geometry rather than
754        // carried over: a merged vertex's old normal described a surface that
755        // is no longer there.
756        out.normals = vec![ogeom_math::Vector::ZERO; out.positions.len()];
757        for triangle in &out.triangles {
758            let [a, b, c] = triangle.map(|v| out.positions[v as usize]);
759            let normal = (b - a).cross(c - a);
760            for v in triangle {
761                out.normals[*v as usize] += normal;
762            }
763        }
764        for normal in &mut out.normals {
765            let length = normal.magnitude();
766            if length > self.tol.confusion() {
767                *normal *= 1.0 / length;
768            }
769        }
770        out.parameters = vec![(0.0, 0.0); out.positions.len()];
771        out.deflection_met = false;
772        (out, moved)
773    }
774
775    /// The vertex a vertex of the source ended up as.
776    fn survivor(&self, mut v: u32) -> u32 {
777        while self.merged_into[v as usize] != v {
778            v = self.merged_into[v as usize];
779        }
780        v
781    }
782
783    /// For each source vertex, the number in the harvested mesh of the
784    /// vertex it ended up as, given each survivor's number there.
785    fn result_vertex(&self, renumbered: &FastMap<u32, u32>) -> Vec<Option<u32>> {
786        let mut root = self.merged_into.clone();
787        for v in 0..root.len() {
788            let mut r = root[v];
789            while root[r as usize] != r {
790                r = root[r as usize];
791            }
792            let mut w = v;
793            while root[w] != r {
794                let up = root[w] as usize;
795                root[w] = r;
796                w = up;
797            }
798        }
799        root.iter().map(|r| renumbered.get(r).copied()).collect()
800    }
801
802    /// Whether an edge may collapse at all.
803    fn free(&self, a: u32, b: u32) -> bool {
804        !(self.pinned[a as usize]
805            || self.pinned[b as usize]
806            || self.held[a as usize]
807            || self.held[b as usize])
808    }
809
810    /// The cost of collapsing an edge to its midpoint, as a squared distance:
811    /// the mean of the squared distances to the planes merged in, weighted as
812    /// they were summed.
813    ///
814    /// Divided by the weight so that it does not grow with the size of the
815    /// triangles, which [`Target::Verified`] needs to set a budget in the
816    /// units of its tolerance. A mean can hide one far plane, so the budget
817    /// only orders and filters; the measurement afterwards decides.
818    fn mean_cost(&self, a: u32, b: u32) -> (f64, Point) {
819        let at = Point::from_vector(
820            (self.positions[a as usize].to_vector() + self.positions[b as usize].to_vector()) * 0.5,
821        );
822        let mut merged = self.quadrics[a as usize];
823        merged.add(&self.quadrics[b as usize]);
824        let cost = merged.at(at).max(0.0);
825        let weight = merged.weight();
826        (if weight > 0.0 { cost / weight } else { cost }, at)
827    }
828
829    /// The vertices sharing a live triangle with `v`, in ascending order.
830    fn ring(&self, v: u32) -> Vec<u32> {
831        let mut out = Vec::new();
832        for &index in &self.around[v as usize] {
833            if self.live[index as usize] {
834                out.extend(self.triangles[index as usize].iter().filter(|&&w| w != v));
835            }
836        }
837        out.sort_unstable();
838        out.dedup();
839        out
840    }
841}
842
843#[cfg(test)]
844#[allow(clippy::unwrap_used)]
845mod tests {
846    use super::*;
847    use crate::{Deflection, triangulate};
848    use ogeom_algo::{make_box, make_sphere};
849    use ogeom_math::Frame;
850    use ogeom_topo::Model;
851
852    const T: Tolerances = Tolerances::millimetres();
853
854    fn sphere(chord: f64) -> Triangulation {
855        let mut model = Model::new();
856        let built = make_sphere(&mut model, Frame::WORLD, 10.0, T).unwrap();
857        triangulate(
858            &model,
859            &built.shape,
860            Deflection {
861                chord,
862                ..Deflection::default()
863            },
864            T,
865        )
866        .unwrap()
867    }
868
869    #[test]
870    fn decimating_a_sphere_keeps_it_a_sphere_to_the_error_it_reports() {
871        // The property that matters: the result says how far it moved, and
872        // every vertex of it really is within that of the original surface.
873        let mesh = sphere(0.02);
874        let before = mesh.triangle_count();
875        let done = simplify(&mesh, Target::Triangles(before / 4), T).unwrap();
876
877        assert!(done.collapsed > 0);
878        assert!(
879            done.mesh.triangle_count() < before,
880            "nothing was removed: {} of {before}",
881            done.mesh.triangle_count()
882        );
883        for p in &done.mesh.positions {
884            let off = (p.to_vector().magnitude() - 10.0).abs();
885            assert!(
886                off <= done.error + 1e-9,
887                "a vertex is {off} off the sphere, but the reported error is {}",
888                done.error
889            );
890        }
891    }
892
893    #[test]
894    fn a_tighter_error_budget_removes_less() {
895        let mesh = sphere(0.02);
896        let loose = simplify(&mesh, Target::Error(0.5), T).unwrap();
897        let tight = simplify(&mesh, Target::Error(0.01), T).unwrap();
898
899        assert!(
900            tight.mesh.triangle_count() >= loose.mesh.triangle_count(),
901            "a tighter budget should keep more: {} against {}",
902            tight.mesh.triangle_count(),
903            loose.mesh.triangle_count()
904        );
905        assert!(
906            tight.error <= 0.01 + 1e-12,
907            "over budget at {}",
908            tight.error
909        );
910        assert!(loose.error <= 0.5 + 1e-12);
911        assert!(tight.target_met && loose.target_met);
912    }
913
914    #[test]
915    fn the_mesh_stays_closed() {
916        // A collapse that opened a hole would be a decimation that changed what
917        // the mesh bounds, which is a different thing from making it coarser.
918        let mesh = sphere(0.05);
919        assert!(mesh.is_closed());
920        let done = simplify(&mesh, Target::Triangles(mesh.triangle_count() / 2), T).unwrap();
921        assert!(
922            done.mesh.is_closed(),
923            "decimation opened the mesh after {} collapses",
924            done.collapsed
925        );
926        assert!(done.mesh.volume() > 0.0, "and it turned inside out");
927    }
928
929    #[test]
930    fn a_collapse_that_would_join_two_edges_into_one_is_not_made() {
931        // A double pyramid on a triangle with two of its corners close
932        // together: merging those two leaves each pole's three faces on two
933        // vertices, every edge of the pair under four faces.
934        let mut mesh = Triangulation::new();
935        mesh.positions = vec![
936            Point::new(0.0, 0.0, 0.0),
937            Point::new(0.01, 0.0, 0.0),
938            Point::new(0.0, 1.0, 0.0),
939            Point::new(0.0, 0.3, 1.0),
940            Point::new(0.0, 0.3, -1.0),
941        ];
942        mesh.triangles = vec![
943            [3, 0, 1],
944            [3, 1, 2],
945            [3, 2, 0],
946            [4, 1, 0],
947            [4, 2, 1],
948            [4, 0, 2],
949        ];
950        for target in [Target::Triangles(4), Target::Error(1.0)] {
951            let done = simplify(&mesh, target, T).unwrap();
952            let mut uses: FastMap<(u32, u32), usize> = FastMap::default();
953            let mut sets = FastSet::default();
954            for t in &done.mesh.triangles {
955                let mut set = *t;
956                set.sort_unstable();
957                assert!(sets.insert(set), "two faces on {set:?} after {target:?}");
958                for k in 0..3 {
959                    let (a, b) = (t[k], t[(k + 1) % 3]);
960                    *uses.entry((a.min(b), a.max(b))).or_default() += 1;
961                }
962            }
963            assert!(
964                uses.values().all(|&n| n == 2),
965                "an edge without two faces after {target:?}: {uses:?}"
966            );
967            assert!(done.mesh.volume() > 0.0);
968        }
969    }
970
971    #[test]
972    fn a_boundary_is_held_exactly() {
973        // A sheet body's outline must not creep inward as the mesh coarsens,
974        // and "expensive but possible" would let it.
975        let mut model = Model::new();
976        let solid = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 4.0), T).unwrap();
977        let face = ogeom_topo::explore_unique(&model, &solid.shape, ogeom_topo::ShapeType::Face)
978            .unwrap()[0]
979            .clone();
980        let sheet = crate::triangulate_face(
981            &model,
982            &face,
983            Deflection {
984                chord: 0.05,
985                ..Deflection::default()
986            },
987            T,
988        )
989        .unwrap();
990
991        let outline = |m: &Triangulation| {
992            let mut low = f64::MAX;
993            let mut high = f64::MIN;
994            for p in &m.positions {
995                low = low.min(p.x);
996                high = high.max(p.x);
997            }
998            (low, high)
999        };
1000        let before = outline(&sheet);
1001        let done = simplify(&sheet, Target::Triangles(2), T).unwrap();
1002        let after = outline(&done.mesh);
1003        assert!(
1004            (before.0 - after.0).abs() < 1e-12 && (before.1 - after.1).abs() < 1e-12,
1005            "the outline moved from {before:?} to {after:?}"
1006        );
1007    }
1008
1009    #[test]
1010    fn a_target_that_cannot_be_reached_is_reported_rather_than_claimed() {
1011        // Every edge of a single triangle is a boundary edge, so there is
1012        // nothing to collapse. Saying the target was met would be a lie that
1013        // costs a caller the chance to try something else.
1014        let mut mesh = Triangulation::new();
1015        mesh.positions = vec![
1016            Point::ORIGIN,
1017            Point::new(1.0, 0.0, 0.0),
1018            Point::new(0.0, 1.0, 0.0),
1019        ];
1020        mesh.triangles = vec![[0, 1, 2]];
1021        let done = simplify(&mesh, Target::Triangles(1), T).unwrap();
1022        assert_eq!(done.collapsed, 0);
1023        assert_eq!(done.mesh.triangle_count(), 1);
1024    }
1025
1026    #[test]
1027    fn a_target_that_describes_nothing_is_refused() {
1028        let mesh = sphere(0.2);
1029        assert!(simplify(&mesh, Target::Triangles(0), T).is_err());
1030        assert!(simplify(&mesh, Target::Error(0.0), T).is_err());
1031        assert!(simplify(&mesh, Target::Error(-1.0), T).is_err());
1032        assert!(simplify(&mesh, Target::Error(f64::NAN), T).is_err());
1033
1034        let mut broken = Triangulation::new();
1035        broken.positions = vec![Point::ORIGIN];
1036        broken.triangles = vec![[0, 1, 2]];
1037        assert!(simplify(&broken, Target::Triangles(1), T).is_err());
1038    }
1039
1040    /// A torus of `n` by `m` grid squares, each cut into two triangles,
1041    /// its tube's radius waved by `wave`; with `open`, one ring of squares
1042    /// left out, so the grid has a border to hold.
1043    fn torus_grid(n: usize, m: usize, wave: f64, open: bool) -> Triangulation {
1044        use core::f64::consts::TAU;
1045        let mut grid = Triangulation::new();
1046        for i in 0..n {
1047            for j in 0..m {
1048                #[allow(clippy::cast_precision_loss)]
1049                let (u, v) = (TAU * i as f64 / n as f64, TAU * j as f64 / m as f64);
1050                let r = wave.mul_add((3.0 * u).sin() * (2.0 * v).cos(), 5.0);
1051                let rho = r.mul_add(v.cos(), 20.0);
1052                grid.positions
1053                    .push(Point::new(rho * u.cos(), rho * u.sin(), r * v.sin()));
1054            }
1055        }
1056        #[allow(clippy::cast_possible_truncation)]
1057        let at = |i: usize, j: usize| ((i % n) * m + (j % m)) as u32;
1058        for i in 0..(if open { n - 1 } else { n }) {
1059            for j in 0..m {
1060                let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
1061                grid.triangles.push([a, b, c]);
1062                grid.triangles.push([a, c, d]);
1063            }
1064        }
1065        grid
1066    }
1067
1068    #[test]
1069    fn the_queue_makes_the_collapses_the_scan_makes() {
1070        // The queue only finds the scan's choice sooner: on an even grid,
1071        // where many edges cost the same to the bit and ties go to the
1072        // edge met first, on one with a border, and on a mesher's sphere,
1073        // both make the same collapses to the bit.
1074        let meshes = [
1075            torus_grid(40, 20, 0.0, false),
1076            torus_grid(40, 20, 0.5, false),
1077            torus_grid(40, 20, 0.5, true),
1078            sphere(0.05),
1079        ];
1080        for mesh in &meshes {
1081            // A budget the halving reached, so a run held to it collapses
1082            // some edges and stops on the budget, not the count.
1083            let halved = State::new(mesh, T).decimate_by_scan(f64::MAX, mesh.triangles.len() / 2);
1084            for (budget, floor) in [
1085                (f64::MAX, mesh.triangles.len() / 2),
1086                (f64::MAX, mesh.triangles.len() / 10),
1087                (halved.0, 1),
1088            ] {
1089                let mut scan = State::new(mesh, T);
1090                let mut queue = State::new(mesh, T);
1091                let by_scan = scan.decimate_by_scan(budget, floor);
1092                let by_queue = queue.decimate_by_queue(budget, floor);
1093                assert!(by_scan.1 > 0);
1094                assert_eq!(by_scan.0.to_bits(), by_queue.0.to_bits());
1095                assert_eq!(by_scan.1, by_queue.1);
1096                assert_eq!(scan.triangles, queue.triangles);
1097                assert_eq!(scan.live, queue.live);
1098                assert_eq!(scan.merged_into, queue.merged_into);
1099                for (a, b) in scan.positions.iter().zip(&queue.positions) {
1100                    assert_eq!(
1101                        [a.x, a.y, a.z].map(f64::to_bits),
1102                        [b.x, b.y, b.z].map(f64::to_bits)
1103                    );
1104                }
1105            }
1106        }
1107    }
1108}