Skip to main content

oxiphysics_geometry/decimation/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6use std::collections::{HashMap, HashSet};
7
8/// A record of one progressive mesh simplification step (edge collapse).
9#[derive(Debug, Clone)]
10pub struct EdgeCollapseRecord {
11    /// The vertex that was removed (merged into `target`).
12    pub removed: usize,
13    /// The vertex that `removed` was merged into.
14    pub target: usize,
15    /// The triangles that were deleted by this collapse.
16    pub deleted_triangles: Vec<[usize; 3]>,
17}
18/// Progressive mesh: stores the base (simplified) mesh and a list of refinement records.
19pub struct ProgressiveMeshSimple {
20    /// Current simplified mesh.
21    pub current: SimpleMesh,
22    /// Collapse history (oldest first).
23    pub history: Vec<EdgeCollapseRecord>,
24}
25impl ProgressiveMeshSimple {
26    /// Build a progressive mesh from a base mesh.
27    pub fn new(mesh: SimpleMesh) -> Self {
28        Self {
29            current: mesh,
30            history: Vec::new(),
31        }
32    }
33    /// Perform one vertex-to-vertex edge collapse (vertex `src` → vertex `dst`).
34    ///
35    /// Merges `src` into `dst`, removes all triangles that had both `src` and `dst`,
36    /// and replaces `src` with `dst` in all remaining triangles.
37    pub fn collapse_edge(&mut self, src: usize, dst: usize) {
38        let mut deleted = Vec::new();
39        let mut remaining = Vec::new();
40        for tri in &self.current.triangles {
41            let has_src = tri.contains(&src);
42            let has_dst = tri.contains(&dst);
43            if has_src && has_dst {
44                deleted.push(*tri);
45            } else if has_src {
46                let new_tri = tri.map(|v| if v == src { dst } else { v });
47                remaining.push(new_tri);
48            } else {
49                remaining.push(*tri);
50            }
51        }
52        self.history.push(EdgeCollapseRecord {
53            removed: src,
54            target: dst,
55            deleted_triangles: deleted,
56        });
57        self.current.triangles = remaining;
58    }
59    /// Number of collapses performed.
60    pub fn n_collapses(&self) -> usize {
61        self.history.len()
62    }
63    /// Current triangle count.
64    pub fn n_triangles(&self) -> usize {
65        self.current.triangle_count()
66    }
67}
68/// Progressive mesh: stores the sequence of collapses so the mesh can be
69/// coarsened incrementally and the collapses can be reversed.
70pub struct ProgressiveMesh {
71    /// Current mesh state.
72    pub mesh: SimpleMesh,
73    /// Collapse history (oldest first).
74    pub history: Vec<CollapseRecord>,
75}
76impl ProgressiveMesh {
77    /// Wrap an existing mesh.
78    pub fn new(mesh: SimpleMesh) -> Self {
79        Self {
80            mesh,
81            history: Vec::new(),
82        }
83    }
84    /// Perform one QEM-based edge collapse (cheapest edge) and record it.
85    ///
86    /// Returns `false` when the mesh has ≤ 3 triangles and cannot be reduced
87    /// further.
88    pub fn collapse_one(&mut self) -> bool {
89        if self.mesh.triangle_count() <= 3 {
90            return false;
91        }
92        let quadrics = compute_vertex_quadrics(&self.mesh);
93        let mut edge_set: std::collections::HashSet<(usize, usize)> =
94            std::collections::HashSet::new();
95        for &[a, b, c] in &self.mesh.triangles {
96            edge_set.insert((a.min(b), a.max(b)));
97            edge_set.insert((b.min(c), b.max(c)));
98            edge_set.insert((a.min(c), a.max(c)));
99        }
100        let mut best_cost = f64::MAX;
101        let mut best_v1 = 0usize;
102        let mut best_v2 = 0usize;
103        let mut best_pos = [0.0f64; 3];
104        for (v1, v2) in &edge_set {
105            let (cost, pos) = edge_collapse_cost(
106                self.mesh.vertices[*v1],
107                self.mesh.vertices[*v2],
108                &quadrics[*v1],
109                &quadrics[*v2],
110            );
111            if cost < best_cost {
112                best_cost = cost;
113                best_v1 = *v1;
114                best_v2 = *v2;
115                best_pos = pos;
116            }
117        }
118        let old_pos = self.mesh.vertices[best_v1];
119        let record = CollapseRecord {
120            v_kept: best_v1,
121            v_removed: best_v2,
122            old_pos,
123            new_pos: best_pos,
124        };
125        collapse_edge(&mut self.mesh, best_v1, best_v2, best_pos);
126        self.history.push(record);
127        true
128    }
129    /// Reduce the mesh until it has at most `target_triangles` triangles.
130    pub fn decimate_to(&mut self, target_triangles: usize) {
131        while self.mesh.triangle_count() > target_triangles {
132            if !self.collapse_one() {
133                break;
134            }
135        }
136    }
137}
138/// A record of one edge collapse operation (for progressive meshes / undo).
139#[derive(Debug, Clone)]
140pub struct CollapseRecord {
141    /// The surviving vertex index.
142    pub v_kept: usize,
143    /// The removed vertex index.
144    pub v_removed: usize,
145    /// The position before the collapse.
146    pub old_pos: [f64; 3],
147    /// The new position assigned to `v_kept`.
148    pub new_pos: [f64; 3],
149}
150/// A directed edge in the mesh.
151#[derive(Debug, Clone, Copy, PartialEq)]
152pub struct MeshEdge {
153    /// First vertex index.
154    pub v0: usize,
155    /// Second vertex index.
156    pub v1: usize,
157    /// QEM collapse cost.
158    pub cost: f64,
159}
160/// A min-priority queue of edges sorted by QEM cost.
161pub struct EdgePriorityQueue {
162    pub(super) heap: std::collections::BinaryHeap<MeshEdge>,
163}
164impl EdgePriorityQueue {
165    /// Create an empty queue.
166    pub fn new() -> Self {
167        Self {
168            heap: std::collections::BinaryHeap::new(),
169        }
170    }
171    /// Insert an edge.
172    pub fn push(&mut self, edge: MeshEdge) {
173        self.heap.push(edge);
174    }
175    /// Pop the lowest-cost edge.
176    pub fn pop(&mut self) -> Option<MeshEdge> {
177        self.heap.pop()
178    }
179    /// Number of edges.
180    pub fn len(&self) -> usize {
181        self.heap.len()
182    }
183    /// Whether the queue is empty.
184    pub fn is_empty(&self) -> bool {
185        self.heap.is_empty()
186    }
187    /// Build a priority queue from all unique edges of a mesh with QEM costs.
188    pub fn from_mesh(mesh: &SimpleMesh) -> Self {
189        let mut q = Self::new();
190        let mut seen: HashSet<(usize, usize)> = HashSet::new();
191        for tri in &mesh.triangles {
192            for &(a, b) in &[(tri[0], tri[1]), (tri[1], tri[2]), (tri[0], tri[2])] {
193                let key = if a < b { (a, b) } else { (b, a) };
194                if seen.insert(key) {
195                    let va = mesh.vertices[a];
196                    let vb = mesh.vertices[b];
197                    let cost =
198                        (va[0] - vb[0]).powi(2) + (va[1] - vb[1]).powi(2) + (va[2] - vb[2]).powi(2);
199                    q.push(MeshEdge {
200                        v0: key.0,
201                        v1: key.1,
202                        cost,
203                    });
204                }
205            }
206        }
207        q
208    }
209}
210/// A simple triangle mesh with vertices and index triples.
211#[derive(Clone)]
212pub struct SimpleMesh {
213    /// Vertex positions.
214    pub vertices: Vec<[f64; 3]>,
215    /// Triangle index triples (counter-clockwise winding).
216    pub triangles: Vec<[usize; 3]>,
217}
218impl SimpleMesh {
219    /// Create an empty mesh.
220    pub fn new() -> Self {
221        Self {
222            vertices: Vec::new(),
223            triangles: Vec::new(),
224        }
225    }
226    /// Add a vertex and return its index.
227    pub fn add_vertex(&mut self, v: [f64; 3]) -> usize {
228        let idx = self.vertices.len();
229        self.vertices.push(v);
230        idx
231    }
232    /// Add a triangle by vertex indices.
233    pub fn add_triangle(&mut self, a: usize, b: usize, c: usize) {
234        self.triangles.push([a, b, c]);
235    }
236    /// Number of vertices.
237    pub fn vertex_count(&self) -> usize {
238        self.vertices.len()
239    }
240    /// Number of triangles.
241    pub fn triangle_count(&self) -> usize {
242        self.triangles.len()
243    }
244    /// Outward-facing unit normal of triangle `t_idx`.
245    pub fn triangle_normal(&self, t_idx: usize) -> [f64; 3] {
246        let [a, b, c] = self.triangles[t_idx];
247        let va = self.vertices[a];
248        let vb = self.vertices[b];
249        let vc = self.vertices[c];
250        let ab = [vb[0] - va[0], vb[1] - va[1], vb[2] - va[2]];
251        let ac = [vc[0] - va[0], vc[1] - va[1], vc[2] - va[2]];
252        let n = cross(ab, ac);
253        normalize(n)
254    }
255    /// Area of triangle `t_idx`.
256    pub fn triangle_area(&self, t_idx: usize) -> f64 {
257        let [a, b, c] = self.triangles[t_idx];
258        let va = self.vertices[a];
259        let vb = self.vertices[b];
260        let vc = self.vertices[c];
261        let ab = [vb[0] - va[0], vb[1] - va[1], vb[2] - va[2]];
262        let ac = [vc[0] - va[0], vc[1] - va[1], vc[2] - va[2]];
263        let cp = cross(ab, ac);
264        0.5 * length(cp)
265    }
266    /// Total surface area.
267    pub fn surface_area(&self) -> f64 {
268        (0..self.triangle_count())
269            .map(|i| self.triangle_area(i))
270            .sum()
271    }
272}
273/// 4×4 symmetric quadric matrix used for QEM mesh simplification.
274pub struct QuadricMatrix {
275    /// Row-major 4×4 matrix entries.
276    pub q: [[f64; 4]; 4],
277}
278impl QuadricMatrix {
279    /// Zero matrix.
280    pub fn zero() -> Self {
281        Self { q: [[0.0; 4]; 4] }
282    }
283    /// Fundamental error quadric Kp = pp^T for a plane ax + by + cz + d = 0.
284    /// The plane coefficients must satisfy a² + b² + c² = 1 (unit normal).
285    pub fn from_plane(a: f64, b: f64, c: f64, d: f64) -> Self {
286        let p = [a, b, c, d];
287        let mut q = [[0.0f64; 4]; 4];
288        for i in 0..4 {
289            for j in 0..4 {
290                q[i][j] = p[i] * p[j];
291            }
292        }
293        Self { q }
294    }
295    /// Element-wise sum of two quadrics.
296    pub fn add(&self, other: &Self) -> Self {
297        let mut result = Self::zero();
298        for i in 0..4 {
299            for j in 0..4 {
300                result.q[i][j] = self.q[i][j] + other.q[i][j];
301            }
302        }
303        result
304    }
305    /// Vertex error v^T Q v in homogeneous coordinates \[x, y, z, 1\].
306    pub fn vertex_error(&self, v: [f64; 3]) -> f64 {
307        let h = [v[0], v[1], v[2], 1.0];
308        let mut result = 0.0;
309        for i in 0..4 {
310            for j in 0..4 {
311                result += h[i] * self.q[i][j] * h[j];
312            }
313        }
314        result
315    }
316}
317/// A normal cone: represents the range of normals in a region.
318///
319/// Defined by an axis (average normal) and a half-angle (max deviation).
320#[derive(Debug, Clone, Copy)]
321pub struct NormalCone {
322    /// Axis of the cone (unit vector).
323    pub axis: [f64; 3],
324    /// Half-angle of the cone in radians.
325    pub half_angle: f64,
326}
327impl NormalCone {
328    /// Create a normal cone from a single normal vector.
329    pub fn from_normal(n: [f64; 3]) -> Self {
330        Self {
331            axis: normalize3(n),
332            half_angle: 0.0,
333        }
334    }
335    /// Merge two normal cones into a bounding cone.
336    pub fn merge(&self, other: &NormalCone) -> NormalCone {
337        let axis_new = normalize3(add3(self.axis, other.axis));
338        let angle1 = dot3(self.axis, axis_new).clamp(-1.0, 1.0).acos() + self.half_angle;
339        let angle2 = dot3(other.axis, axis_new).clamp(-1.0, 1.0).acos() + other.half_angle;
340        NormalCone {
341            axis: axis_new,
342            half_angle: angle1.max(angle2),
343        }
344    }
345    /// Check whether a new normal falls within this cone (with tolerance `tol`).
346    pub fn contains(&self, n: [f64; 3], tol: f64) -> bool {
347        let n_hat = normalize3(n);
348        let cos_angle = dot3(self.axis, n_hat).clamp(-1.0, 1.0);
349        let angle = cos_angle.acos();
350        angle <= self.half_angle + tol
351    }
352    /// Convert half-angle to degrees.
353    pub fn half_angle_deg(&self) -> f64 {
354        self.half_angle.to_degrees()
355    }
356}
357/// Feature detection result for an edge.
358#[derive(Debug, Clone, Copy, PartialEq)]
359pub enum EdgeFeature {
360    /// Smooth interior edge.
361    Smooth,
362    /// Sharp crease edge (dihedral angle > threshold).
363    Crease,
364    /// Boundary edge (belongs to only one triangle).
365    Boundary,
366}
367/// Statistics from a decimation run.
368#[derive(Debug, Clone, Default)]
369pub struct DecimationMetrics {
370    /// Original vertex count.
371    pub original_vertices: usize,
372    /// Original triangle count.
373    pub original_triangles: usize,
374    /// Reduced vertex count.
375    pub reduced_vertices: usize,
376    /// Reduced triangle count.
377    pub reduced_triangles: usize,
378    /// Number of edge collapses performed.
379    pub n_collapses: usize,
380    /// Total QEM cost accumulated.
381    pub total_qem_cost: f64,
382    /// Maximum QEM cost in any single collapse.
383    pub max_qem_cost: f64,
384}
385impl DecimationMetrics {
386    /// Compute reduction ratio for vertices \[0, 1\].
387    pub fn vertex_reduction_ratio(&self) -> f64 {
388        if self.original_vertices == 0 {
389            return 0.0;
390        }
391        1.0 - self.reduced_vertices as f64 / self.original_vertices as f64
392    }
393    /// Compute reduction ratio for triangles \[0, 1\].
394    pub fn triangle_reduction_ratio(&self) -> f64 {
395        if self.original_triangles == 0 {
396            return 0.0;
397        }
398        1.0 - self.reduced_triangles as f64 / self.original_triangles as f64
399    }
400    /// Average QEM cost per collapse.
401    pub fn avg_qem_cost(&self) -> f64 {
402        if self.n_collapses == 0 {
403            return 0.0;
404        }
405        self.total_qem_cost / self.n_collapses as f64
406    }
407}
408/// A QEM-based decimator that wraps `SimpleMesh` and provides adaptive
409/// error thresholds, boundary preservation, and feature scoring.
410pub struct QemDecimation {
411    /// The mesh being simplified.
412    pub mesh: SimpleMesh,
413    /// Per-vertex quadric matrices.
414    pub(super) quadrics: Vec<QuadricMatrix>,
415}
416impl QemDecimation {
417    /// Construct a `QemDecimation` from a mesh.  Quadrics are computed once.
418    pub fn new(mesh: SimpleMesh) -> Self {
419        let quadrics = compute_vertex_quadrics(&mesh);
420        Self { mesh, quadrics }
421    }
422    /// Recompute the per-vertex quadric matrices from the current mesh.
423    pub fn recompute_quadrics(&mut self) {
424        self.quadrics = compute_vertex_quadrics(&self.mesh);
425    }
426    /// Compute an *adaptive* QEM error threshold.
427    ///
428    /// The threshold is `scale_factor * avg_edge_length² * mean_curvature_proxy`,
429    /// where the mean curvature proxy is the average edge collapse cost over
430    /// a sample of edges.  This adapts the threshold to mesh scale and feature
431    /// density.
432    ///
433    /// `scale_factor` controls aggressiveness (try 0.01 – 1.0).
434    pub fn compute_error_threshold(&self, scale_factor: f64) -> f64 {
435        let avg_edge = average_edge_length(&self.mesh);
436        if avg_edge < 1e-12 {
437            return 0.0;
438        }
439        let scale_sq = avg_edge * avg_edge;
440        let n_verts = self.mesh.vertex_count();
441        if n_verts < 2 {
442            return scale_factor * scale_sq;
443        }
444        let mut sampled_cost = 0.0f64;
445        let mut sampled_count = 0usize;
446        let step = (n_verts / 32).max(1);
447        for tri in self.mesh.triangles.iter().step_by(step) {
448            let v0 = tri[0];
449            let v1 = tri[1];
450            if v0 >= self.quadrics.len() || v1 >= self.quadrics.len() {
451                continue;
452            }
453            let (cost, _) = edge_collapse_cost(
454                self.mesh.vertices[v0],
455                self.mesh.vertices[v1],
456                &self.quadrics[v0],
457                &self.quadrics[v1],
458            );
459            sampled_cost += cost;
460            sampled_count += 1;
461        }
462        let mean_cost = if sampled_count > 0 {
463            sampled_cost / sampled_count as f64
464        } else {
465            1.0
466        };
467        (scale_factor * scale_sq * mean_cost).max(1e-12)
468    }
469    /// Collapse edges while preserving boundary geometry.
470    ///
471    /// Boundary edges (shared by exactly one triangle) are never collapsed.
472    /// Interior edges with QEM cost below `threshold` are collapsed greedily
473    /// using a priority queue.
474    ///
475    /// Returns the number of collapses performed.
476    pub fn preserve_boundary(&mut self, threshold: f64) -> usize {
477        let boundary_edges = find_boundary_edges(&self.mesh);
478        let boundary_set: HashSet<(usize, usize)> = boundary_edges
479            .iter()
480            .flat_map(|&(a, b)| [(a.min(b), a.max(b)), (a.min(b), a.max(b))])
481            .collect();
482        let mut n_collapses = 0usize;
483        let mut keep_going = true;
484        while keep_going {
485            keep_going = false;
486            self.recompute_quadrics();
487            let mut best: Option<(f64, [f64; 3], usize, usize)> = None;
488            for tri in &self.mesh.triangles {
489                for k in 0..3 {
490                    let v0 = tri[k];
491                    let v1 = tri[(k + 1) % 3];
492                    let key = (v0.min(v1), v0.max(v1));
493                    if boundary_set.contains(&key) {
494                        continue;
495                    }
496                    if v0 >= self.quadrics.len() || v1 >= self.quadrics.len() {
497                        continue;
498                    }
499                    let (cost, opt) = edge_collapse_cost(
500                        self.mesh.vertices[v0],
501                        self.mesh.vertices[v1],
502                        &self.quadrics[v0],
503                        &self.quadrics[v1],
504                    );
505                    if cost <= threshold && best.as_ref().is_none_or(|b| cost < b.0) {
506                        best = Some((cost, opt, v0, v1));
507                    }
508                }
509            }
510            if let Some((_cost, opt, v0, v1)) = best {
511                self.mesh.vertices[v0] = opt;
512                for tri in &mut self.mesh.triangles {
513                    for idx in tri.iter_mut() {
514                        if *idx == v1 {
515                            *idx = v0;
516                        }
517                    }
518                }
519                self.mesh
520                    .triangles
521                    .retain(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2]);
522                n_collapses += 1;
523                keep_going = true;
524            }
525        }
526        n_collapses
527    }
528    /// Compute a feature importance score for each edge in the mesh.
529    ///
530    /// The score is the sum of the dihedral-angle curvature between the two
531    /// incident faces.  High scores indicate sharp creases or geometric
532    /// features that should be preserved.
533    ///
534    /// Returns a `HashMap<(usize, usize), f64>` keyed by the canonical
535    /// (min, max) vertex index pair.
536    pub fn compute_feature_score(&self) -> HashMap<(usize, usize), f64> {
537        let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
538        for (fi, tri) in self.mesh.triangles.iter().enumerate() {
539            for k in 0..3 {
540                let a = tri[k];
541                let b = tri[(k + 1) % 3];
542                let key = (a.min(b), a.max(b));
543                edge_faces.entry(key).or_default().push(fi);
544            }
545        }
546        let mut scores: HashMap<(usize, usize), f64> = HashMap::new();
547        for (&edge, faces) in &edge_faces {
548            if faces.len() != 2 {
549                scores.insert(edge, f64::INFINITY);
550                continue;
551            }
552            let n0 = self.mesh.triangle_normal(faces[0]);
553            let n1 = self.mesh.triangle_normal(faces[1]);
554            let cos_a = (dot3(n0, n1)).clamp(-1.0, 1.0);
555            let score = 1.0 - cos_a;
556            scores.insert(edge, score);
557        }
558        scores
559    }
560}
561/// Statistics about a decimation run.
562#[derive(Debug, Clone)]
563pub struct DecimationStats {
564    /// Vertex count before decimation.
565    pub original_vertices: usize,
566    /// Triangle count before decimation.
567    pub original_triangles: usize,
568    /// Vertex count after decimation.
569    pub result_vertices: usize,
570    /// Triangle count after decimation.
571    pub result_triangles: usize,
572    /// Ratio of removed triangles to original triangle count (0..1).
573    pub reduction_ratio: f64,
574    /// Total QEM error accumulated over all collapses.
575    pub total_qem_error: f64,
576}
577impl DecimationStats {
578    /// Compute stats from before/after snapshots and accumulated error.
579    pub fn compute(
580        original_v: usize,
581        original_t: usize,
582        result: &SimpleMesh,
583        total_qem_error: f64,
584    ) -> Self {
585        let reduction_ratio = if original_t > 0 {
586            1.0 - result.triangle_count() as f64 / original_t as f64
587        } else {
588            0.0
589        };
590        Self {
591            original_vertices: original_v,
592            original_triangles: original_t,
593            result_vertices: result.vertex_count(),
594            result_triangles: result.triangle_count(),
595            reduction_ratio,
596            total_qem_error,
597        }
598    }
599}