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