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.
403#[allow(dead_code)]
404pub fn vertex_clustering_decimate(mesh: &SimpleMesh, cell_size: f64) -> SimpleMesh {
405    if mesh.vertices.is_empty() || cell_size <= 0.0 {
406        return SimpleMesh::new();
407    }
408    let mut mn = mesh.vertices[0];
409    let mut mx = mesh.vertices[0];
410    for v in &mesh.vertices {
411        for d in 0..3 {
412            if v[d] < mn[d] {
413                mn[d] = v[d];
414            }
415            if v[d] > mx[d] {
416                mx[d] = v[d];
417            }
418        }
419    }
420    let mut cell_to_new_idx: HashMap<[i64; 3], usize> = HashMap::new();
421    let mut vertex_remap: Vec<usize> = Vec::with_capacity(mesh.vertices.len());
422    let mut new_vertices: Vec<[f64; 3]> = Vec::new();
423    for v in &mesh.vertices {
424        let key = [
425            ((v[0] - mn[0]) / cell_size).floor() as i64,
426            ((v[1] - mn[1]) / cell_size).floor() as i64,
427            ((v[2] - mn[2]) / cell_size).floor() as i64,
428        ];
429        let idx = *cell_to_new_idx.entry(key).or_insert_with(|| {
430            let new_idx = new_vertices.len();
431            new_vertices.push(*v);
432            new_idx
433        });
434        vertex_remap.push(idx);
435    }
436    let mut new_tris: Vec<[usize; 3]> = Vec::new();
437    let mut seen: HashSet<[usize; 3]> = HashSet::new();
438    for tri in &mesh.triangles {
439        let a = vertex_remap[tri[0]];
440        let b = vertex_remap[tri[1]];
441        let c = vertex_remap[tri[2]];
442        if a == b || b == c || a == c {
443            continue;
444        }
445        let mut key = [a, b, c];
446        key.sort_unstable();
447        if seen.insert(key) {
448            new_tris.push([a, b, c]);
449        }
450    }
451    SimpleMesh {
452        vertices: new_vertices,
453        triangles: new_tris,
454    }
455}
456/// Classify an edge between two triangles by their dihedral angle.
457///
458/// `n0`, `n1`: outward normals of the two triangles.
459/// `crease_angle_deg`: threshold for classifying as a crease.
460#[allow(dead_code)]
461pub fn classify_edge(n0: [f64; 3], n1: [f64; 3], crease_angle_deg: f64) -> EdgeFeature {
462    let cos_angle = dot3(normalize3(n0), normalize3(n1)).clamp(-1.0, 1.0);
463    let angle_deg = cos_angle.acos().to_degrees();
464    if angle_deg > crease_angle_deg {
465        EdgeFeature::Crease
466    } else {
467        EdgeFeature::Smooth
468    }
469}
470/// Feature-aware QEM cost: penalizes collapsing crease or boundary edges.
471///
472/// Returns the base QEM cost multiplied by a feature weight:
473/// - Smooth: weight = 1.0
474/// - Crease: weight = `crease_weight`
475/// - Boundary: weight = `boundary_weight`
476#[allow(dead_code)]
477pub fn feature_aware_cost(
478    base_cost: f64,
479    feature: EdgeFeature,
480    crease_weight: f64,
481    boundary_weight: f64,
482) -> f64 {
483    let w = match feature {
484        EdgeFeature::Smooth => 1.0,
485        EdgeFeature::Crease => crease_weight,
486        EdgeFeature::Boundary => boundary_weight,
487    };
488    base_cost * w
489}
490/// Collect decimation statistics before and after simplification.
491#[allow(dead_code)]
492pub fn collect_decimation_metrics(
493    original: &SimpleMesh,
494    reduced: &SimpleMesh,
495    n_collapses: usize,
496    total_cost: f64,
497    max_cost: f64,
498) -> DecimationMetrics {
499    DecimationMetrics {
500        original_vertices: original.vertex_count(),
501        original_triangles: original.triangle_count(),
502        reduced_vertices: reduced.vertex_count(),
503        reduced_triangles: reduced.triangle_count(),
504        n_collapses,
505        total_qem_cost: total_cost,
506        max_qem_cost: max_cost,
507    }
508}
509#[inline]
510pub(super) fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
511    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
512}
513#[inline]
514pub(super) fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
515    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
516}
517#[inline]
518pub(super) fn normalize3(v: [f64; 3]) -> [f64; 3] {
519    let len = dot3(v, v).sqrt();
520    if len < 1e-15 {
521        return [0.0, 0.0, 1.0];
522    }
523    [v[0] / len, v[1] / len, v[2] / len]
524}
525#[cfg(test)]
526mod tests {
527    use super::*;
528    use crate::decimation::DecimationStats;
529    use crate::decimation::ProgressiveMesh;
530    fn unit_tetrahedron() -> SimpleMesh {
531        let mut m = SimpleMesh::new();
532        let v0 = m.add_vertex([0.0, 0.0, 0.0]);
533        let v1 = m.add_vertex([1.0, 0.0, 0.0]);
534        let v2 = m.add_vertex([0.0, 1.0, 0.0]);
535        let v3 = m.add_vertex([0.0, 0.0, 1.0]);
536        m.add_triangle(v0, v2, v1);
537        m.add_triangle(v0, v1, v3);
538        m.add_triangle(v1, v2, v3);
539        m.add_triangle(v0, v3, v2);
540        m
541    }
542    fn single_triangle() -> SimpleMesh {
543        let mut m = SimpleMesh::new();
544        let v0 = m.add_vertex([0.0, 0.0, 0.0]);
545        let v1 = m.add_vertex([1.0, 0.0, 0.0]);
546        let v2 = m.add_vertex([0.0, 1.0, 0.0]);
547        m.add_triangle(v0, v1, v2);
548        m
549    }
550    fn unit_cube() -> SimpleMesh {
551        let mut m = SimpleMesh::new();
552        let verts: [[f64; 3]; 8] = [
553            [0.0, 0.0, 0.0],
554            [1.0, 0.0, 0.0],
555            [1.0, 1.0, 0.0],
556            [0.0, 1.0, 0.0],
557            [0.0, 0.0, 1.0],
558            [1.0, 0.0, 1.0],
559            [1.0, 1.0, 1.0],
560            [0.0, 1.0, 1.0],
561        ];
562        for v in &verts {
563            m.add_vertex(*v);
564        }
565        m.add_triangle(0, 2, 1);
566        m.add_triangle(0, 3, 2);
567        m.add_triangle(4, 5, 6);
568        m.add_triangle(4, 6, 7);
569        m.add_triangle(0, 1, 5);
570        m.add_triangle(0, 5, 4);
571        m.add_triangle(2, 3, 7);
572        m.add_triangle(2, 7, 6);
573        m.add_triangle(0, 4, 7);
574        m.add_triangle(0, 7, 3);
575        m.add_triangle(1, 2, 6);
576        m.add_triangle(1, 6, 5);
577        m
578    }
579    #[test]
580    fn test_mesh_new_empty() {
581        let m = SimpleMesh::new();
582        assert_eq!(m.vertex_count(), 0);
583        assert_eq!(m.triangle_count(), 0);
584    }
585    #[test]
586    fn test_add_vertex_returns_index() {
587        let mut m = SimpleMesh::new();
588        let i0 = m.add_vertex([1.0, 2.0, 3.0]);
589        let i1 = m.add_vertex([4.0, 5.0, 6.0]);
590        assert_eq!(i0, 0);
591        assert_eq!(i1, 1);
592        assert_eq!(m.vertex_count(), 2);
593    }
594    #[test]
595    fn test_add_triangle_increments_count() {
596        let mut m = single_triangle();
597        assert_eq!(m.triangle_count(), 1);
598        m.add_triangle(0, 1, 2);
599        assert_eq!(m.triangle_count(), 2);
600    }
601    #[test]
602    fn test_triangle_area_unit_right_triangle() {
603        let m = single_triangle();
604        let area = m.triangle_area(0);
605        assert!((area - 0.5).abs() < 1e-12, "area={area}");
606    }
607    #[test]
608    fn test_surface_area_tetrahedron() {
609        let m = unit_tetrahedron();
610        let expected = 3.0 * 0.5 + 3.0_f64.sqrt() / 2.0;
611        let got = m.surface_area();
612        assert!(
613            (got - expected).abs() < 1e-10,
614            "got={got}, expected={expected}"
615        );
616    }
617    #[test]
618    fn test_triangle_normal_z_up() {
619        let m = single_triangle();
620        let n = m.triangle_normal(0);
621        assert!(n[2] > 0.9, "expected z-up normal, got {:?}", n);
622        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
623        assert!((len - 1.0).abs() < 1e-12, "normal not unit length");
624    }
625    #[test]
626    fn test_triangle_normal_unit_length() {
627        let m = unit_tetrahedron();
628        for i in 0..m.triangle_count() {
629            let n = m.triangle_normal(i);
630            let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
631            assert!((len - 1.0).abs() < 1e-12, "face {i} normal not unit: {len}");
632        }
633    }
634    #[test]
635    fn test_quadric_zero() {
636        let q = QuadricMatrix::zero();
637        for i in 0..4 {
638            for j in 0..4 {
639                assert_eq!(q.q[i][j], 0.0);
640            }
641        }
642    }
643    #[test]
644    fn test_quadric_from_plane_vertex_error_zero_on_plane() {
645        let q = QuadricMatrix::from_plane(0.0, 0.0, 1.0, 0.0);
646        let err = q.vertex_error([3.0, -2.0, 0.0]);
647        assert!(err.abs() < 1e-12, "err={err}");
648    }
649    #[test]
650    fn test_quadric_from_plane_vertex_error_nonzero_off_plane() {
651        let q = QuadricMatrix::from_plane(0.0, 0.0, 1.0, 0.0);
652        let err = q.vertex_error([0.0, 0.0, 1.0]);
653        assert!((err - 1.0).abs() < 1e-12, "err={err}");
654    }
655    #[test]
656    fn test_quadric_add() {
657        let q1 = QuadricMatrix::from_plane(1.0, 0.0, 0.0, 0.0);
658        let q2 = QuadricMatrix::from_plane(0.0, 1.0, 0.0, 0.0);
659        let sum = q1.add(&q2);
660        let err = sum.vertex_error([1.0, 1.0, 0.0]);
661        assert!((err - 2.0).abs() < 1e-12, "err={err}");
662    }
663    #[test]
664    fn test_compute_vertex_quadrics_count() {
665        let m = unit_tetrahedron();
666        let qs = compute_vertex_quadrics(&m);
667        assert_eq!(qs.len(), m.vertex_count());
668    }
669    #[test]
670    fn test_compute_vertex_quadrics_nonzero() {
671        let m = single_triangle();
672        let qs = compute_vertex_quadrics(&m);
673        let total: f64 = qs[0].q.iter().flatten().map(|x| x.abs()).sum();
674        assert!(total > 0.0);
675    }
676    #[test]
677    fn test_edge_collapse_cost_midpoint_fallback() {
678        let q1 = QuadricMatrix::zero();
679        let q2 = QuadricMatrix::zero();
680        let v1 = [0.0, 0.0, 0.0];
681        let v2 = [2.0, 0.0, 0.0];
682        let (error, pos) = edge_collapse_cost(v1, v2, &q1, &q2);
683        assert!(error.abs() < 1e-12);
684        assert!((pos[0] - 1.0).abs() < 1e-12, "pos={:?}", pos);
685    }
686    #[test]
687    fn test_edge_collapse_cost_returns_finite() {
688        let m = unit_tetrahedron();
689        let qs = compute_vertex_quadrics(&m);
690        let (err, pos) = edge_collapse_cost(m.vertices[0], m.vertices[1], &qs[0], &qs[1]);
691        assert!(err.is_finite(), "error not finite");
692        assert!(pos.iter().all(|x| x.is_finite()), "pos not finite");
693    }
694    #[test]
695    fn test_cluster_id_basic() {
696        let id = cluster_id([0.5, 1.5, 2.5], 1.0);
697        assert_eq!(id, (0, 1, 2));
698    }
699    #[test]
700    fn test_cluster_id_negative() {
701        let id = cluster_id([-0.5, -1.5, -2.5], 1.0);
702        assert_eq!(id, (-1, -2, -3));
703    }
704    #[test]
705    fn test_vertex_clustering_reduces_vertices() {
706        let mut m = SimpleMesh::new();
707        let eps = 0.01;
708        let v0 = m.add_vertex([0.0, 0.0, 0.0]);
709        let v1 = m.add_vertex([1.0, 0.0, 0.0]);
710        let v2 = m.add_vertex([0.0, 1.0, 0.0]);
711        let v3 = m.add_vertex([0.0 + eps, 0.0, 0.0]);
712        let v4 = m.add_vertex([1.0 + eps, 0.0, 0.0]);
713        let v5 = m.add_vertex([0.0 + eps, 1.0, 0.0]);
714        m.add_triangle(v0, v1, v2);
715        m.add_triangle(v3, v4, v5);
716        let simplified = vertex_clustering(&m, 0.1);
717        assert!(simplified.vertex_count() < m.vertex_count());
718    }
719    #[test]
720    fn test_vertex_clustering_no_degenerate_triangles() {
721        let mut m = SimpleMesh::new();
722        let v0 = m.add_vertex([0.1, 0.1, 0.1]);
723        let v1 = m.add_vertex([0.2, 0.1, 0.1]);
724        let v2 = m.add_vertex([0.1, 0.2, 0.1]);
725        m.add_triangle(v0, v1, v2);
726        let simplified = vertex_clustering(&m, 1.0);
727        assert_eq!(simplified.triangle_count(), 0);
728    }
729    #[test]
730    fn test_collapse_edge_reduces_triangles() {
731        let mut m = unit_tetrahedron();
732        let before_tris = m.triangle_count();
733        collapse_edge(&mut m, 0, 1, [0.5, 0.0, 0.0]);
734        assert!(m.triangle_count() < before_tris);
735    }
736    #[test]
737    fn test_collapse_edge_no_degenerate_triangles_remain() {
738        let mut m = single_triangle();
739        collapse_edge(&mut m, 0, 1, [0.5, 0.0, 0.0]);
740        for &[a, b, c] in &m.triangles {
741            assert!(a != b && b != c && a != c, "degenerate triangle found");
742        }
743    }
744    #[test]
745    fn test_find_boundary_edges_single_triangle() {
746        let m = single_triangle();
747        let boundary = find_boundary_edges(&m);
748        assert_eq!(boundary.len(), 3);
749    }
750    #[test]
751    fn test_find_boundary_edges_closed_tetrahedron() {
752        let m = unit_tetrahedron();
753        let boundary = find_boundary_edges(&m);
754        assert_eq!(boundary.len(), 0);
755    }
756    #[test]
757    fn test_is_manifold_after_collapse_tetrahedron_edge() {
758        let m = unit_tetrahedron();
759        let result = is_manifold_after_collapse(&m, 0, 1);
760        assert!(result, "manifold check failed on tetrahedron edge");
761    }
762    #[test]
763    fn test_mesh_volume_unit_cube() {
764        let m = unit_cube();
765        let vol = mesh_volume(&m).abs();
766        assert!((vol - 1.0).abs() < 1e-10, "cube volume={vol}");
767    }
768    #[test]
769    fn test_mesh_volume_single_triangle_zero() {
770        let m = single_triangle();
771        let vol = mesh_volume(&m);
772        assert!(vol.abs() < 1e-12, "single triangle volume={vol}");
773    }
774    #[test]
775    fn test_mesh_centroid_single_triangle() {
776        let m = single_triangle();
777        let c = mesh_centroid(&m);
778        assert!((c[0] - 1.0 / 3.0).abs() < 1e-12, "cx={}", c[0]);
779        assert!((c[1] - 1.0 / 3.0).abs() < 1e-12, "cy={}", c[1]);
780        assert!(c[2].abs() < 1e-12, "cz={}", c[2]);
781    }
782    #[test]
783    fn test_mesh_centroid_unit_cube() {
784        let m = unit_cube();
785        let c = mesh_centroid(&m);
786        assert!((c[0] - 0.5).abs() < 1e-10, "cx={}", c[0]);
787        assert!((c[1] - 0.5).abs() < 1e-10, "cy={}", c[1]);
788        assert!((c[2] - 0.5).abs() < 1e-10, "cz={}", c[2]);
789    }
790    #[test]
791    fn test_progressive_mesh_collapse_one_reduces_triangles() {
792        let m = unit_tetrahedron();
793        let before = m.triangle_count();
794        let mut pm = ProgressiveMesh::new(m);
795        let ok = pm.collapse_one();
796        assert!(ok, "collapse_one should succeed on a tetrahedron");
797        assert!(
798            pm.mesh.triangle_count() < before,
799            "triangle count should decrease"
800        );
801    }
802    #[test]
803    fn test_progressive_mesh_decimate_to() {
804        let m = unit_cube();
805        let mut pm = ProgressiveMesh::new(m);
806        pm.decimate_to(4);
807        assert!(
808            pm.mesh.triangle_count() <= 4,
809            "got {}",
810            pm.mesh.triangle_count()
811        );
812    }
813    #[test]
814    fn test_progressive_mesh_history_recorded() {
815        let m = unit_tetrahedron();
816        let mut pm = ProgressiveMesh::new(m);
817        pm.collapse_one();
818        assert_eq!(pm.history.len(), 1);
819        let rec = &pm.history[0];
820        assert!(rec.old_pos.iter().all(|x| x.is_finite()));
821        assert!(rec.new_pos.iter().all(|x| x.is_finite()));
822    }
823    #[test]
824    fn test_progressive_mesh_stops_at_minimum() {
825        let m = single_triangle();
826        let mut pm = ProgressiveMesh::new(m);
827        let ok = pm.collapse_one();
828        assert!(!ok, "should not collapse a 1-triangle mesh");
829    }
830    #[test]
831    fn test_classify_feature_vertices_tetrahedron() {
832        let m = unit_tetrahedron();
833        let feat = classify_feature_vertices(&m, 0.01);
834        assert!(
835            feat.iter().any(|&f| f),
836            "at least one feature vertex expected"
837        );
838    }
839    #[test]
840    fn test_classify_feature_vertices_flat_mesh() {
841        let mut m = SimpleMesh::new();
842        let v0 = m.add_vertex([0.0, 0.0, 0.0]);
843        let v1 = m.add_vertex([1.0, 0.0, 0.0]);
844        let v2 = m.add_vertex([0.0, 1.0, 0.0]);
845        let v3 = m.add_vertex([1.0, 1.0, 0.0]);
846        m.add_triangle(v0, v1, v2);
847        m.add_triangle(v1, v3, v2);
848        let feat = classify_feature_vertices(&m, 0.1);
849        assert!(
850            feat.iter().all(|&f| !f),
851            "flat mesh should have no feature vertices"
852        );
853    }
854    #[test]
855    fn test_simplify_with_features_preserves_feature_vertex() {
856        let m = unit_cube();
857        let n = m.vertex_count();
858        let mut is_feature = vec![false; n];
859        is_feature[0] = true;
860        let mut mesh = m;
861        simplify_with_features(&mut mesh, &is_feature, 2);
862        let v0_used = mesh.triangles.iter().any(|t| t.contains(&0));
863        assert!(v0_used, "feature vertex 0 should survive decimation");
864    }
865    #[test]
866    fn test_simplify_with_features_reduces_triangles() {
867        let m = unit_cube();
868        let is_feature = vec![false; m.vertex_count()];
869        let target = 4;
870        let mut mesh = m;
871        simplify_with_features(&mut mesh, &is_feature, target);
872        assert!(mesh.triangle_count() <= target || mesh.triangle_count() < 12);
873    }
874    #[test]
875    fn test_decimation_stats_reduction_ratio() {
876        let m = unit_cube();
877        let orig_v = m.vertex_count();
878        let orig_t = m.triangle_count();
879        let mut pm = ProgressiveMesh::new(m);
880        pm.decimate_to(6);
881        let stats = DecimationStats::compute(orig_v, orig_t, &pm.mesh, 0.0);
882        assert!(stats.reduction_ratio >= 0.0 && stats.reduction_ratio <= 1.0);
883        assert_eq!(stats.original_triangles, orig_t);
884        assert_eq!(stats.result_triangles, pm.mesh.triangle_count());
885    }
886    #[test]
887    fn test_decimation_stats_no_change() {
888        let m = single_triangle();
889        let stats = DecimationStats::compute(m.vertex_count(), m.triangle_count(), &m, 0.0);
890        assert!((stats.reduction_ratio - 0.0).abs() < 1e-12);
891    }
892    #[test]
893    fn test_one_sided_hausdorff_same_mesh_zero() {
894        let m = single_triangle();
895        let d = one_sided_hausdorff(&m, &m);
896        assert!(d < 1e-12, "same mesh: d={d}");
897    }
898    #[test]
899    fn test_one_sided_hausdorff_offset_mesh() {
900        let mut m1 = SimpleMesh::new();
901        m1.add_vertex([0.0, 0.0, 0.0]);
902        let mut m2 = SimpleMesh::new();
903        m2.add_vertex([3.0, 0.0, 0.0]);
904        let d = one_sided_hausdorff(&m1, &m2);
905        assert!((d - 3.0).abs() < 1e-12, "d={d}");
906    }
907    #[test]
908    fn test_one_sided_hausdorff_empty_returns_zero() {
909        let m = single_triangle();
910        let empty = SimpleMesh::new();
911        let d = one_sided_hausdorff(&m, &empty);
912        assert_eq!(d, 0.0);
913    }
914    #[test]
915    fn test_average_edge_length_unit_right_triangle() {
916        let m = single_triangle();
917        let avg = average_edge_length(&m);
918        let expected = (2.0 + 2.0_f64.sqrt()) / 3.0;
919        assert!((avg - expected).abs() < 1e-10, "avg={avg}");
920    }
921    #[test]
922    fn test_average_edge_length_empty_mesh_zero() {
923        let m = SimpleMesh::new();
924        assert_eq!(average_edge_length(&m), 0.0);
925    }
926    #[test]
927    fn test_mesh_compactness_single_triangle_zero() {
928        let m = single_triangle();
929        let c = mesh_compactness(&m);
930        assert!(c >= 0.0);
931    }
932    #[test]
933    fn test_mesh_compactness_cube_positive() {
934        let m = unit_cube();
935        let c = mesh_compactness(&m);
936        assert!(c > 0.0 && c < 1.0, "compactness={c}");
937    }
938}