Skip to main content

oxiphysics_geometry/decimation/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::{HashMap, HashSet};
6
7use super::types::{DecimationMetrics, EdgeFeature, QuadricMatrix, SimpleMesh};
8
9pub(super) fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
10    [
11        a[1] * b[2] - a[2] * b[1],
12        a[2] * b[0] - a[0] * b[2],
13        a[0] * b[1] - a[1] * b[0],
14    ]
15}
16pub(super) fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
17    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
18}
19pub(super) fn length(v: [f64; 3]) -> f64 {
20    dot(v, v).sqrt()
21}
22pub(super) fn normalize(v: [f64; 3]) -> [f64; 3] {
23    let len = length(v);
24    if len < 1e-300 {
25        [0.0, 0.0, 1.0]
26    } else {
27        [v[0] / len, v[1] / len, v[2] / len]
28    }
29}
30/// Compute per-vertex quadric matrices by summing Kp over adjacent triangles.
31pub fn compute_vertex_quadrics(mesh: &SimpleMesh) -> Vec<QuadricMatrix> {
32    let n = mesh.vertex_count();
33    let mut quadrics: Vec<QuadricMatrix> = (0..n).map(|_| QuadricMatrix::zero()).collect();
34    for t_idx in 0..mesh.triangle_count() {
35        let [a, b, c] = mesh.triangles[t_idx];
36        let normal = mesh.triangle_normal(t_idx);
37        let va = mesh.vertices[a];
38        let d = -(normal[0] * va[0] + normal[1] * va[1] + normal[2] * va[2]);
39        let kp = QuadricMatrix::from_plane(normal[0], normal[1], normal[2], d);
40        quadrics[a] = quadrics[a].add(&kp);
41        quadrics[b] = quadrics[b].add(&kp);
42        quadrics[c] = quadrics[c].add(&kp);
43    }
44    quadrics
45}
46/// Compute the optimal collapse target and error for collapsing edge (v1→v2).
47///
48/// Returns `(error, optimal_position)`. The optimal position minimises the
49/// combined quadric error; if the combined Q is not invertible the midpoint is
50/// used instead.
51pub fn edge_collapse_cost(
52    v1: [f64; 3],
53    v2: [f64; 3],
54    q1: &QuadricMatrix,
55    q2: &QuadricMatrix,
56) -> (f64, [f64; 3]) {
57    let combined = q1.add(q2);
58    let mid = [
59        (v1[0] + v2[0]) * 0.5,
60        (v1[1] + v2[1]) * 0.5,
61        (v1[2] + v2[2]) * 0.5,
62    ];
63    let a00 = combined.q[0][0];
64    let a01 = combined.q[0][1];
65    let a02 = combined.q[0][2];
66    let a11 = combined.q[1][1];
67    let a12 = combined.q[1][2];
68    let a22 = combined.q[2][2];
69    let b0 = -combined.q[0][3];
70    let b1 = -combined.q[1][3];
71    let b2 = -combined.q[2][3];
72    let det = a00 * (a11 * a22 - a12 * a12) - a01 * (a01 * a22 - a12 * a02)
73        + a02 * (a01 * a12 - a11 * a02);
74    let opt = if det.abs() > 1e-10 {
75        let inv_det = 1.0 / det;
76        let x = inv_det
77            * (b0 * (a11 * a22 - a12 * a12)
78                + b1 * (a02 * a12 - a01 * a22)
79                + b2 * (a01 * a12 - a11 * a02));
80        let y = inv_det
81            * (b0 * (a12 * a02 - a01 * a22)
82                + b1 * (a00 * a22 - a02 * a02)
83                + b2 * (a01 * a02 - a00 * a12));
84        let z = inv_det
85            * (b0 * (a01 * a12 - a02 * a11)
86                + b1 * (a02 * a01 - a00 * a12)
87                + b2 * (a00 * a11 - a01 * a01));
88        [x, y, z]
89    } else {
90        mid
91    };
92    let error = combined.vertex_error(opt);
93    (error, opt)
94}
95/// Compute the grid cell coordinates for vertex `v` with cell size `grid_size`.
96pub fn cluster_id(v: [f64; 3], grid_size: f64) -> (i64, i64, i64) {
97    (
98        (v[0] / grid_size).floor() as i64,
99        (v[1] / grid_size).floor() as i64,
100        (v[2] / grid_size).floor() as i64,
101    )
102}
103/// Decimate `mesh` by merging all vertices that fall inside the same voxel cell.
104/// Each cluster is represented by the average position of its members.
105/// Triangles with two or more vertices in the same cluster are discarded.
106pub fn vertex_clustering(mesh: &SimpleMesh, grid_size: f64) -> SimpleMesh {
107    let mut cell_to_cluster: HashMap<(i64, i64, i64), usize> = HashMap::new();
108    let mut cluster_sum: Vec<[f64; 3]> = Vec::new();
109    let mut cluster_count: Vec<f64> = Vec::new();
110    let mut vertex_to_cluster: Vec<usize> = Vec::with_capacity(mesh.vertex_count());
111    for &v in &mesh.vertices {
112        let cid = cluster_id(v, grid_size);
113        let idx = cell_to_cluster.entry(cid).or_insert_with(|| {
114            let new_idx = cluster_sum.len();
115            cluster_sum.push([0.0; 3]);
116            cluster_count.push(0.0);
117            new_idx
118        });
119        cluster_sum[*idx][0] += v[0];
120        cluster_sum[*idx][1] += v[1];
121        cluster_sum[*idx][2] += v[2];
122        cluster_count[*idx] += 1.0;
123        vertex_to_cluster.push(*idx);
124    }
125    let mut out = SimpleMesh::new();
126    for i in 0..cluster_sum.len() {
127        let cnt = cluster_count[i];
128        out.add_vertex([
129            cluster_sum[i][0] / cnt,
130            cluster_sum[i][1] / cnt,
131            cluster_sum[i][2] / cnt,
132        ]);
133    }
134    for &[a, b, c] in &mesh.triangles {
135        let ca = vertex_to_cluster[a];
136        let cb = vertex_to_cluster[b];
137        let cc = vertex_to_cluster[c];
138        if ca != cb && cb != cc && ca != cc {
139            out.add_triangle(ca, cb, cc);
140        }
141    }
142    out
143}
144/// Collapse edge (v1, v2): move v1 to `new_pos`, redirect all references to
145/// v2 toward v1, then remove any resulting degenerate (zero-area) triangles.
146/// Returns the index of the surviving vertex (v1, updated in-place).
147pub fn collapse_edge(mesh: &mut SimpleMesh, v1: usize, v2: usize, new_pos: [f64; 3]) -> usize {
148    mesh.vertices[v1] = new_pos;
149    for tri in &mut mesh.triangles {
150        for idx in tri.iter_mut() {
151            if *idx == v2 {
152                *idx = v1;
153            }
154        }
155    }
156    mesh.triangles
157        .retain(|&[a, b, c]| a != b && b != c && a != c);
158    v1
159}
160/// Return all edges that belong to exactly one triangle (boundary edges).
161pub fn find_boundary_edges(mesh: &SimpleMesh) -> Vec<(usize, usize)> {
162    let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
163    for &[a, b, c] in &mesh.triangles {
164        let edges = [
165            (a.min(b), a.max(b)),
166            (b.min(c), b.max(c)),
167            (a.min(c), a.max(c)),
168        ];
169        for e in edges {
170            *edge_count.entry(e).or_insert(0) += 1;
171        }
172    }
173    edge_count
174        .into_iter()
175        .filter(|&(_, count)| count == 1)
176        .map(|(e, _)| e)
177        .collect()
178}
179/// Check whether collapsing edge (v1, v2) preserves manifold topology.
180///
181/// Uses the *link condition*: the collapse is safe if and only if the
182/// intersection of the one-ring neighbourhoods of v1 and v2 equals the
183/// set of vertices shared by triangles that contain both v1 and v2.
184pub fn is_manifold_after_collapse(mesh: &SimpleMesh, v1: usize, v2: usize) -> bool {
185    let mut ring1: HashSet<usize> = HashSet::new();
186    let mut ring2: HashSet<usize> = HashSet::new();
187    let mut shared: HashSet<usize> = HashSet::new();
188    for &[a, b, c] in &mesh.triangles {
189        let verts = [a, b, c];
190        let has_v1 = verts.contains(&v1);
191        let has_v2 = verts.contains(&v2);
192        if has_v1 {
193            for &v in &verts {
194                if v != v1 {
195                    ring1.insert(v);
196                }
197            }
198        }
199        if has_v2 {
200            for &v in &verts {
201                if v != v2 {
202                    ring2.insert(v);
203                }
204            }
205        }
206        if has_v1 && has_v2 {
207            for &v in &verts {
208                if v != v1 && v != v2 {
209                    shared.insert(v);
210                }
211            }
212        }
213    }
214    ring1.remove(&v2);
215    ring2.remove(&v1);
216    let intersection: HashSet<usize> = ring1.intersection(&ring2).copied().collect();
217    intersection == shared
218}
219/// Signed volume of the mesh via the divergence theorem.
220/// Positive for outward-facing normals with CCW winding.
221pub fn mesh_volume(mesh: &SimpleMesh) -> f64 {
222    let mut vol = 0.0;
223    for &[a, b, c] in &mesh.triangles {
224        let va = mesh.vertices[a];
225        let vb = mesh.vertices[b];
226        let vc = mesh.vertices[c];
227        vol += va[0] * (vb[1] * vc[2] - vc[1] * vb[2])
228            + vb[0] * (vc[1] * va[2] - va[1] * vc[2])
229            + vc[0] * (va[1] * vb[2] - vb[1] * va[2]);
230    }
231    vol / 6.0
232}
233/// Area-weighted centroid of the mesh surface.
234pub fn mesh_centroid(mesh: &SimpleMesh) -> [f64; 3] {
235    let mut cx = 0.0;
236    let mut cy = 0.0;
237    let mut cz = 0.0;
238    let mut total_area = 0.0;
239    for t_idx in 0..mesh.triangle_count() {
240        let [a, b, c] = mesh.triangles[t_idx];
241        let va = mesh.vertices[a];
242        let vb = mesh.vertices[b];
243        let vc = mesh.vertices[c];
244        let area = mesh.triangle_area(t_idx);
245        let tcx = (va[0] + vb[0] + vc[0]) / 3.0;
246        let tcy = (va[1] + vb[1] + vc[1]) / 3.0;
247        let tcz = (va[2] + vb[2] + vc[2]) / 3.0;
248        cx += area * tcx;
249        cy += area * tcy;
250        cz += area * tcz;
251        total_area += area;
252    }
253    if total_area < 1e-300 {
254        return [0.0, 0.0, 0.0];
255    }
256    [cx / total_area, cy / total_area, cz / total_area]
257}
258/// Classify every vertex as *smooth* or *feature* based on the maximum dihedral
259/// angle between its adjacent triangle faces.
260///
261/// A vertex is marked as a feature vertex when any pair of its adjacent faces
262/// has a dihedral angle exceeding `threshold_rad`.
263pub fn classify_feature_vertices(mesh: &SimpleMesh, threshold_rad: f64) -> Vec<bool> {
264    let n = mesh.vertex_count();
265    let mut is_feature = vec![false; n];
266    let mut vtris: Vec<Vec<usize>> = vec![Vec::new(); n];
267    for (t_idx, &[a, b, c]) in mesh.triangles.iter().enumerate() {
268        vtris[a].push(t_idx);
269        vtris[b].push(t_idx);
270        vtris[c].push(t_idx);
271    }
272    for v in 0..n {
273        'outer: for &t1 in &vtris[v] {
274            for &t2 in &vtris[v] {
275                if t1 >= t2 {
276                    continue;
277                }
278                let n1 = mesh.triangle_normal(t1);
279                let n2 = mesh.triangle_normal(t2);
280                let cos_ang = dot(n1, n2).clamp(-1.0, 1.0);
281                let angle = cos_ang.acos();
282                if angle > threshold_rad {
283                    is_feature[v] = true;
284                    break 'outer;
285                }
286            }
287        }
288    }
289    is_feature
290}
291/// Simplify `mesh` using QEM edge collapses, preserving feature vertices
292/// (those for which `is_feature[v] == true` will never be the removed vertex).
293///
294/// Reduces until at most `target_triangles` remain or no more safe collapses
295/// are available.
296pub fn simplify_with_features(mesh: &mut SimpleMesh, is_feature: &[bool], target_triangles: usize) {
297    while mesh.triangle_count() > target_triangles {
298        let quadrics = compute_vertex_quadrics(mesh);
299        let mut edge_set: std::collections::HashSet<(usize, usize)> =
300            std::collections::HashSet::new();
301        for &[a, b, c] in &mesh.triangles {
302            edge_set.insert((a.min(b), a.max(b)));
303            edge_set.insert((b.min(c), b.max(c)));
304            edge_set.insert((a.min(c), a.max(c)));
305        }
306        let mut best_cost = f64::MAX;
307        let mut best_v1 = usize::MAX;
308        let mut best_v2 = usize::MAX;
309        let mut best_pos = [0.0f64; 3];
310        for (v1, v2) in &edge_set {
311            let can_remove_v2 = !is_feature.get(*v2).copied().unwrap_or(false);
312            let can_remove_v1 = !is_feature.get(*v1).copied().unwrap_or(false);
313            if !can_remove_v2 && !can_remove_v1 {
314                continue;
315            }
316            let (v_kept, v_rem) = if can_remove_v2 {
317                (*v1, *v2)
318            } else {
319                (*v2, *v1)
320            };
321            let (cost, pos) = edge_collapse_cost(
322                mesh.vertices[v_kept],
323                mesh.vertices[v_rem],
324                &quadrics[v_kept],
325                &quadrics[v_rem],
326            );
327            if cost < best_cost {
328                best_cost = cost;
329                best_v1 = v_kept;
330                best_v2 = v_rem;
331                best_pos = pos;
332            }
333        }
334        if best_v1 == usize::MAX {
335            break;
336        }
337        collapse_edge(mesh, best_v1, best_v2, best_pos);
338    }
339}
340/// Compute the Hausdorff-like error between two meshes (approximate).
341///
342/// For each vertex in `mesh_a`, finds the nearest vertex in `mesh_b` and
343/// records the maximum distance.  This is a one-sided approximation of the
344/// true Hausdorff distance.
345pub fn one_sided_hausdorff(mesh_a: &SimpleMesh, mesh_b: &SimpleMesh) -> f64 {
346    if mesh_a.vertex_count() == 0 || mesh_b.vertex_count() == 0 {
347        return 0.0;
348    }
349    let mut max_dist = 0.0f64;
350    for &va in &mesh_a.vertices {
351        let mut min_d = f64::MAX;
352        for &vb in &mesh_b.vertices {
353            let d = {
354                let dx = va[0] - vb[0];
355                let dy = va[1] - vb[1];
356                let dz = va[2] - vb[2];
357                (dx * dx + dy * dy + dz * dz).sqrt()
358            };
359            if d < min_d {
360                min_d = d;
361            }
362        }
363        if min_d > max_dist {
364            max_dist = min_d;
365        }
366    }
367    max_dist
368}
369/// Compute the average edge length of the mesh.
370pub fn average_edge_length(mesh: &SimpleMesh) -> f64 {
371    let mut total = 0.0;
372    let mut count = 0usize;
373    for &[a, b, c] in &mesh.triangles {
374        for (i, j) in [(a, b), (b, c), (a, c)] {
375            let vi = mesh.vertices[i];
376            let vj = mesh.vertices[j];
377            let d = length([vj[0] - vi[0], vj[1] - vi[1], vj[2] - vi[2]]);
378            total += d;
379            count += 1;
380        }
381    }
382    if count == 0 {
383        0.0
384    } else {
385        total / count as f64
386    }
387}
388/// Compactness ratio: 36π V² / A³  (= 1 for a sphere, < 1 otherwise).
389///
390/// Returns `0.0` if area is effectively zero.
391pub fn mesh_compactness(mesh: &SimpleMesh) -> f64 {
392    let v = mesh_volume(mesh).abs();
393    let a = mesh.surface_area();
394    if a < 1e-300 {
395        return 0.0;
396    }
397    36.0 * std::f64::consts::PI * v * v / (a * a * a)
398}
399/// Vertex clustering decimation: reduces mesh by merging vertices within cells
400/// of a uniform grid, then rebuilding triangles.
401///
402/// The grid cell size `cell_size` controls the level of simplification.
403pub fn vertex_clustering_decimate(mesh: &SimpleMesh, cell_size: f64) -> SimpleMesh {
404    if mesh.vertices.is_empty() || cell_size <= 0.0 {
405        return SimpleMesh::new();
406    }
407    let mut mn = mesh.vertices[0];
408    let mut mx = mesh.vertices[0];
409    for v in &mesh.vertices {
410        for d in 0..3 {
411            if v[d] < mn[d] {
412                mn[d] = v[d];
413            }
414            if v[d] > mx[d] {
415                mx[d] = v[d];
416            }
417        }
418    }
419    let mut cell_to_new_idx: HashMap<[i64; 3], usize> = HashMap::new();
420    let mut vertex_remap: Vec<usize> = Vec::with_capacity(mesh.vertices.len());
421    let mut new_vertices: Vec<[f64; 3]> = Vec::new();
422    for v in &mesh.vertices {
423        let key = [
424            ((v[0] - mn[0]) / cell_size).floor() as i64,
425            ((v[1] - mn[1]) / cell_size).floor() as i64,
426            ((v[2] - mn[2]) / cell_size).floor() as i64,
427        ];
428        let idx = *cell_to_new_idx.entry(key).or_insert_with(|| {
429            let new_idx = new_vertices.len();
430            new_vertices.push(*v);
431            new_idx
432        });
433        vertex_remap.push(idx);
434    }
435    let mut new_tris: Vec<[usize; 3]> = Vec::new();
436    let mut seen: HashSet<[usize; 3]> = HashSet::new();
437    for tri in &mesh.triangles {
438        let a = vertex_remap[tri[0]];
439        let b = vertex_remap[tri[1]];
440        let c = vertex_remap[tri[2]];
441        if a == b || b == c || a == c {
442            continue;
443        }
444        let mut key = [a, b, c];
445        key.sort_unstable();
446        if seen.insert(key) {
447            new_tris.push([a, b, c]);
448        }
449    }
450    SimpleMesh {
451        vertices: new_vertices,
452        triangles: new_tris,
453    }
454}
455/// Classify an edge between two triangles by their dihedral angle.
456///
457/// `n0`, `n1`: outward normals of the two triangles.
458/// `crease_angle_deg`: threshold for classifying as a crease.
459pub fn classify_edge(n0: [f64; 3], n1: [f64; 3], crease_angle_deg: f64) -> EdgeFeature {
460    let cos_angle = dot3(normalize3(n0), normalize3(n1)).clamp(-1.0, 1.0);
461    let angle_deg = cos_angle.acos().to_degrees();
462    if angle_deg > crease_angle_deg {
463        EdgeFeature::Crease
464    } else {
465        EdgeFeature::Smooth
466    }
467}
468/// Feature-aware QEM cost: penalizes collapsing crease or boundary edges.
469///
470/// Returns the base QEM cost multiplied by a feature weight:
471/// - Smooth: weight = 1.0
472/// - Crease: weight = `crease_weight`
473/// - Boundary: weight = `boundary_weight`
474pub fn feature_aware_cost(
475    base_cost: f64,
476    feature: EdgeFeature,
477    crease_weight: f64,
478    boundary_weight: f64,
479) -> f64 {
480    let w = match feature {
481        EdgeFeature::Smooth => 1.0,
482        EdgeFeature::Crease => crease_weight,
483        EdgeFeature::Boundary => boundary_weight,
484    };
485    base_cost * w
486}
487/// Collect decimation statistics before and after simplification.
488pub fn collect_decimation_metrics(
489    original: &SimpleMesh,
490    reduced: &SimpleMesh,
491    n_collapses: usize,
492    total_cost: f64,
493    max_cost: f64,
494) -> DecimationMetrics {
495    DecimationMetrics {
496        original_vertices: original.vertex_count(),
497        original_triangles: original.triangle_count(),
498        reduced_vertices: reduced.vertex_count(),
499        reduced_triangles: reduced.triangle_count(),
500        n_collapses,
501        total_qem_cost: total_cost,
502        max_qem_cost: max_cost,
503    }
504}
505#[inline]
506pub(super) fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
507    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
508}
509#[inline]
510pub(super) fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
511    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
512}
513#[inline]
514pub(super) fn normalize3(v: [f64; 3]) -> [f64; 3] {
515    let len = dot3(v, v).sqrt();
516    if len < 1e-15 {
517        return [0.0, 0.0, 1.0];
518    }
519    [v[0] / len, v[1] / len, v[2] / len]
520}
521#[cfg(test)]
522mod tests {
523    use super::*;
524    use crate::decimation::DecimationStats;
525    use crate::decimation::ProgressiveMesh;
526    fn unit_tetrahedron() -> SimpleMesh {
527        let mut m = SimpleMesh::new();
528        let v0 = m.add_vertex([0.0, 0.0, 0.0]);
529        let v1 = m.add_vertex([1.0, 0.0, 0.0]);
530        let v2 = m.add_vertex([0.0, 1.0, 0.0]);
531        let v3 = m.add_vertex([0.0, 0.0, 1.0]);
532        m.add_triangle(v0, v2, v1);
533        m.add_triangle(v0, v1, v3);
534        m.add_triangle(v1, v2, v3);
535        m.add_triangle(v0, v3, v2);
536        m
537    }
538    fn single_triangle() -> SimpleMesh {
539        let mut m = SimpleMesh::new();
540        let v0 = m.add_vertex([0.0, 0.0, 0.0]);
541        let v1 = m.add_vertex([1.0, 0.0, 0.0]);
542        let v2 = m.add_vertex([0.0, 1.0, 0.0]);
543        m.add_triangle(v0, v1, v2);
544        m
545    }
546    fn unit_cube() -> SimpleMesh {
547        let mut m = SimpleMesh::new();
548        let verts: [[f64; 3]; 8] = [
549            [0.0, 0.0, 0.0],
550            [1.0, 0.0, 0.0],
551            [1.0, 1.0, 0.0],
552            [0.0, 1.0, 0.0],
553            [0.0, 0.0, 1.0],
554            [1.0, 0.0, 1.0],
555            [1.0, 1.0, 1.0],
556            [0.0, 1.0, 1.0],
557        ];
558        for v in &verts {
559            m.add_vertex(*v);
560        }
561        m.add_triangle(0, 2, 1);
562        m.add_triangle(0, 3, 2);
563        m.add_triangle(4, 5, 6);
564        m.add_triangle(4, 6, 7);
565        m.add_triangle(0, 1, 5);
566        m.add_triangle(0, 5, 4);
567        m.add_triangle(2, 3, 7);
568        m.add_triangle(2, 7, 6);
569        m.add_triangle(0, 4, 7);
570        m.add_triangle(0, 7, 3);
571        m.add_triangle(1, 2, 6);
572        m.add_triangle(1, 6, 5);
573        m
574    }
575    #[test]
576    fn test_mesh_new_empty() {
577        let m = SimpleMesh::new();
578        assert_eq!(m.vertex_count(), 0);
579        assert_eq!(m.triangle_count(), 0);
580    }
581    #[test]
582    fn test_add_vertex_returns_index() {
583        let mut m = SimpleMesh::new();
584        let i0 = m.add_vertex([1.0, 2.0, 3.0]);
585        let i1 = m.add_vertex([4.0, 5.0, 6.0]);
586        assert_eq!(i0, 0);
587        assert_eq!(i1, 1);
588        assert_eq!(m.vertex_count(), 2);
589    }
590    #[test]
591    fn test_add_triangle_increments_count() {
592        let mut m = single_triangle();
593        assert_eq!(m.triangle_count(), 1);
594        m.add_triangle(0, 1, 2);
595        assert_eq!(m.triangle_count(), 2);
596    }
597    #[test]
598    fn test_triangle_area_unit_right_triangle() {
599        let m = single_triangle();
600        let area = m.triangle_area(0);
601        assert!((area - 0.5).abs() < 1e-12, "area={area}");
602    }
603    #[test]
604    fn test_surface_area_tetrahedron() {
605        let m = unit_tetrahedron();
606        let expected = 3.0 * 0.5 + 3.0_f64.sqrt() / 2.0;
607        let got = m.surface_area();
608        assert!(
609            (got - expected).abs() < 1e-10,
610            "got={got}, expected={expected}"
611        );
612    }
613    #[test]
614    fn test_triangle_normal_z_up() {
615        let m = single_triangle();
616        let n = m.triangle_normal(0);
617        assert!(n[2] > 0.9, "expected z-up normal, got {:?}", n);
618        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
619        assert!((len - 1.0).abs() < 1e-12, "normal not unit length");
620    }
621    #[test]
622    fn test_triangle_normal_unit_length() {
623        let m = unit_tetrahedron();
624        for i in 0..m.triangle_count() {
625            let n = m.triangle_normal(i);
626            let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
627            assert!((len - 1.0).abs() < 1e-12, "face {i} normal not unit: {len}");
628        }
629    }
630    #[test]
631    fn test_quadric_zero() {
632        let q = QuadricMatrix::zero();
633        for i in 0..4 {
634            for j in 0..4 {
635                assert_eq!(q.q[i][j], 0.0);
636            }
637        }
638    }
639    #[test]
640    fn test_quadric_from_plane_vertex_error_zero_on_plane() {
641        let q = QuadricMatrix::from_plane(0.0, 0.0, 1.0, 0.0);
642        let err = q.vertex_error([3.0, -2.0, 0.0]);
643        assert!(err.abs() < 1e-12, "err={err}");
644    }
645    #[test]
646    fn test_quadric_from_plane_vertex_error_nonzero_off_plane() {
647        let q = QuadricMatrix::from_plane(0.0, 0.0, 1.0, 0.0);
648        let err = q.vertex_error([0.0, 0.0, 1.0]);
649        assert!((err - 1.0).abs() < 1e-12, "err={err}");
650    }
651    #[test]
652    fn test_quadric_add() {
653        let q1 = QuadricMatrix::from_plane(1.0, 0.0, 0.0, 0.0);
654        let q2 = QuadricMatrix::from_plane(0.0, 1.0, 0.0, 0.0);
655        let sum = q1.add(&q2);
656        let err = sum.vertex_error([1.0, 1.0, 0.0]);
657        assert!((err - 2.0).abs() < 1e-12, "err={err}");
658    }
659    #[test]
660    fn test_compute_vertex_quadrics_count() {
661        let m = unit_tetrahedron();
662        let qs = compute_vertex_quadrics(&m);
663        assert_eq!(qs.len(), m.vertex_count());
664    }
665    #[test]
666    fn test_compute_vertex_quadrics_nonzero() {
667        let m = single_triangle();
668        let qs = compute_vertex_quadrics(&m);
669        let total: f64 = qs[0].q.iter().flatten().map(|x| x.abs()).sum();
670        assert!(total > 0.0);
671    }
672    #[test]
673    fn test_edge_collapse_cost_midpoint_fallback() {
674        let q1 = QuadricMatrix::zero();
675        let q2 = QuadricMatrix::zero();
676        let v1 = [0.0, 0.0, 0.0];
677        let v2 = [2.0, 0.0, 0.0];
678        let (error, pos) = edge_collapse_cost(v1, v2, &q1, &q2);
679        assert!(error.abs() < 1e-12);
680        assert!((pos[0] - 1.0).abs() < 1e-12, "pos={:?}", pos);
681    }
682    #[test]
683    fn test_edge_collapse_cost_returns_finite() {
684        let m = unit_tetrahedron();
685        let qs = compute_vertex_quadrics(&m);
686        let (err, pos) = edge_collapse_cost(m.vertices[0], m.vertices[1], &qs[0], &qs[1]);
687        assert!(err.is_finite(), "error not finite");
688        assert!(pos.iter().all(|x| x.is_finite()), "pos not finite");
689    }
690    #[test]
691    fn test_cluster_id_basic() {
692        let id = cluster_id([0.5, 1.5, 2.5], 1.0);
693        assert_eq!(id, (0, 1, 2));
694    }
695    #[test]
696    fn test_cluster_id_negative() {
697        let id = cluster_id([-0.5, -1.5, -2.5], 1.0);
698        assert_eq!(id, (-1, -2, -3));
699    }
700    #[test]
701    fn test_vertex_clustering_reduces_vertices() {
702        let mut m = SimpleMesh::new();
703        let eps = 0.01;
704        let v0 = m.add_vertex([0.0, 0.0, 0.0]);
705        let v1 = m.add_vertex([1.0, 0.0, 0.0]);
706        let v2 = m.add_vertex([0.0, 1.0, 0.0]);
707        let v3 = m.add_vertex([0.0 + eps, 0.0, 0.0]);
708        let v4 = m.add_vertex([1.0 + eps, 0.0, 0.0]);
709        let v5 = m.add_vertex([0.0 + eps, 1.0, 0.0]);
710        m.add_triangle(v0, v1, v2);
711        m.add_triangle(v3, v4, v5);
712        let simplified = vertex_clustering(&m, 0.1);
713        assert!(simplified.vertex_count() < m.vertex_count());
714    }
715    #[test]
716    fn test_vertex_clustering_no_degenerate_triangles() {
717        let mut m = SimpleMesh::new();
718        let v0 = m.add_vertex([0.1, 0.1, 0.1]);
719        let v1 = m.add_vertex([0.2, 0.1, 0.1]);
720        let v2 = m.add_vertex([0.1, 0.2, 0.1]);
721        m.add_triangle(v0, v1, v2);
722        let simplified = vertex_clustering(&m, 1.0);
723        assert_eq!(simplified.triangle_count(), 0);
724    }
725    #[test]
726    fn test_collapse_edge_reduces_triangles() {
727        let mut m = unit_tetrahedron();
728        let before_tris = m.triangle_count();
729        collapse_edge(&mut m, 0, 1, [0.5, 0.0, 0.0]);
730        assert!(m.triangle_count() < before_tris);
731    }
732    #[test]
733    fn test_collapse_edge_no_degenerate_triangles_remain() {
734        let mut m = single_triangle();
735        collapse_edge(&mut m, 0, 1, [0.5, 0.0, 0.0]);
736        for &[a, b, c] in &m.triangles {
737            assert!(a != b && b != c && a != c, "degenerate triangle found");
738        }
739    }
740    #[test]
741    fn test_find_boundary_edges_single_triangle() {
742        let m = single_triangle();
743        let boundary = find_boundary_edges(&m);
744        assert_eq!(boundary.len(), 3);
745    }
746    #[test]
747    fn test_find_boundary_edges_closed_tetrahedron() {
748        let m = unit_tetrahedron();
749        let boundary = find_boundary_edges(&m);
750        assert_eq!(boundary.len(), 0);
751    }
752    #[test]
753    fn test_is_manifold_after_collapse_tetrahedron_edge() {
754        let m = unit_tetrahedron();
755        let result = is_manifold_after_collapse(&m, 0, 1);
756        assert!(result, "manifold check failed on tetrahedron edge");
757    }
758    #[test]
759    fn test_mesh_volume_unit_cube() {
760        let m = unit_cube();
761        let vol = mesh_volume(&m).abs();
762        assert!((vol - 1.0).abs() < 1e-10, "cube volume={vol}");
763    }
764    #[test]
765    fn test_mesh_volume_single_triangle_zero() {
766        let m = single_triangle();
767        let vol = mesh_volume(&m);
768        assert!(vol.abs() < 1e-12, "single triangle volume={vol}");
769    }
770    #[test]
771    fn test_mesh_centroid_single_triangle() {
772        let m = single_triangle();
773        let c = mesh_centroid(&m);
774        assert!((c[0] - 1.0 / 3.0).abs() < 1e-12, "cx={}", c[0]);
775        assert!((c[1] - 1.0 / 3.0).abs() < 1e-12, "cy={}", c[1]);
776        assert!(c[2].abs() < 1e-12, "cz={}", c[2]);
777    }
778    #[test]
779    fn test_mesh_centroid_unit_cube() {
780        let m = unit_cube();
781        let c = mesh_centroid(&m);
782        assert!((c[0] - 0.5).abs() < 1e-10, "cx={}", c[0]);
783        assert!((c[1] - 0.5).abs() < 1e-10, "cy={}", c[1]);
784        assert!((c[2] - 0.5).abs() < 1e-10, "cz={}", c[2]);
785    }
786    #[test]
787    fn test_progressive_mesh_collapse_one_reduces_triangles() {
788        let m = unit_tetrahedron();
789        let before = m.triangle_count();
790        let mut pm = ProgressiveMesh::new(m);
791        let ok = pm.collapse_one();
792        assert!(ok, "collapse_one should succeed on a tetrahedron");
793        assert!(
794            pm.mesh.triangle_count() < before,
795            "triangle count should decrease"
796        );
797    }
798    #[test]
799    fn test_progressive_mesh_decimate_to() {
800        let m = unit_cube();
801        let mut pm = ProgressiveMesh::new(m);
802        pm.decimate_to(4);
803        assert!(
804            pm.mesh.triangle_count() <= 4,
805            "got {}",
806            pm.mesh.triangle_count()
807        );
808    }
809    #[test]
810    fn test_progressive_mesh_history_recorded() {
811        let m = unit_tetrahedron();
812        let mut pm = ProgressiveMesh::new(m);
813        pm.collapse_one();
814        assert_eq!(pm.history.len(), 1);
815        let rec = &pm.history[0];
816        assert!(rec.old_pos.iter().all(|x| x.is_finite()));
817        assert!(rec.new_pos.iter().all(|x| x.is_finite()));
818    }
819    #[test]
820    fn test_progressive_mesh_stops_at_minimum() {
821        let m = single_triangle();
822        let mut pm = ProgressiveMesh::new(m);
823        let ok = pm.collapse_one();
824        assert!(!ok, "should not collapse a 1-triangle mesh");
825    }
826    #[test]
827    fn test_classify_feature_vertices_tetrahedron() {
828        let m = unit_tetrahedron();
829        let feat = classify_feature_vertices(&m, 0.01);
830        assert!(
831            feat.iter().any(|&f| f),
832            "at least one feature vertex expected"
833        );
834    }
835    #[test]
836    fn test_classify_feature_vertices_flat_mesh() {
837        let mut m = SimpleMesh::new();
838        let v0 = m.add_vertex([0.0, 0.0, 0.0]);
839        let v1 = m.add_vertex([1.0, 0.0, 0.0]);
840        let v2 = m.add_vertex([0.0, 1.0, 0.0]);
841        let v3 = m.add_vertex([1.0, 1.0, 0.0]);
842        m.add_triangle(v0, v1, v2);
843        m.add_triangle(v1, v3, v2);
844        let feat = classify_feature_vertices(&m, 0.1);
845        assert!(
846            feat.iter().all(|&f| !f),
847            "flat mesh should have no feature vertices"
848        );
849    }
850    #[test]
851    fn test_simplify_with_features_preserves_feature_vertex() {
852        let m = unit_cube();
853        let n = m.vertex_count();
854        let mut is_feature = vec![false; n];
855        is_feature[0] = true;
856        let mut mesh = m;
857        simplify_with_features(&mut mesh, &is_feature, 2);
858        let v0_used = mesh.triangles.iter().any(|t| t.contains(&0));
859        assert!(v0_used, "feature vertex 0 should survive decimation");
860    }
861    #[test]
862    fn test_simplify_with_features_reduces_triangles() {
863        let m = unit_cube();
864        let is_feature = vec![false; m.vertex_count()];
865        let target = 4;
866        let mut mesh = m;
867        simplify_with_features(&mut mesh, &is_feature, target);
868        assert!(mesh.triangle_count() <= target || mesh.triangle_count() < 12);
869    }
870    #[test]
871    fn test_decimation_stats_reduction_ratio() {
872        let m = unit_cube();
873        let orig_v = m.vertex_count();
874        let orig_t = m.triangle_count();
875        let mut pm = ProgressiveMesh::new(m);
876        pm.decimate_to(6);
877        let stats = DecimationStats::compute(orig_v, orig_t, &pm.mesh, 0.0);
878        assert!(stats.reduction_ratio >= 0.0 && stats.reduction_ratio <= 1.0);
879        assert_eq!(stats.original_triangles, orig_t);
880        assert_eq!(stats.result_triangles, pm.mesh.triangle_count());
881    }
882    #[test]
883    fn test_decimation_stats_no_change() {
884        let m = single_triangle();
885        let stats = DecimationStats::compute(m.vertex_count(), m.triangle_count(), &m, 0.0);
886        assert!((stats.reduction_ratio - 0.0).abs() < 1e-12);
887    }
888    #[test]
889    fn test_one_sided_hausdorff_same_mesh_zero() {
890        let m = single_triangle();
891        let d = one_sided_hausdorff(&m, &m);
892        assert!(d < 1e-12, "same mesh: d={d}");
893    }
894    #[test]
895    fn test_one_sided_hausdorff_offset_mesh() {
896        let mut m1 = SimpleMesh::new();
897        m1.add_vertex([0.0, 0.0, 0.0]);
898        let mut m2 = SimpleMesh::new();
899        m2.add_vertex([3.0, 0.0, 0.0]);
900        let d = one_sided_hausdorff(&m1, &m2);
901        assert!((d - 3.0).abs() < 1e-12, "d={d}");
902    }
903    #[test]
904    fn test_one_sided_hausdorff_empty_returns_zero() {
905        let m = single_triangle();
906        let empty = SimpleMesh::new();
907        let d = one_sided_hausdorff(&m, &empty);
908        assert_eq!(d, 0.0);
909    }
910    #[test]
911    fn test_average_edge_length_unit_right_triangle() {
912        let m = single_triangle();
913        let avg = average_edge_length(&m);
914        let expected = (2.0 + 2.0_f64.sqrt()) / 3.0;
915        assert!((avg - expected).abs() < 1e-10, "avg={avg}");
916    }
917    #[test]
918    fn test_average_edge_length_empty_mesh_zero() {
919        let m = SimpleMesh::new();
920        assert_eq!(average_edge_length(&m), 0.0);
921    }
922    #[test]
923    fn test_mesh_compactness_single_triangle_zero() {
924        let m = single_triangle();
925        let c = mesh_compactness(&m);
926        assert!(c >= 0.0);
927    }
928    #[test]
929    fn test_mesh_compactness_cube_positive() {
930        let m = unit_cube();
931        let c = mesh_compactness(&m);
932        assert!(c > 0.0 && c < 1.0, "compactness={c}");
933    }
934}