Skip to main content

oxiphysics_geometry/mesh_simplification/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::{BinaryHeap, HashMap, HashSet};
6
7use super::types::{
8    CollapseCandidate, CollapseRecord, HalfEdgeMesh, MeshStats, QemConfig, SymMat4,
9    TopologyValidation,
10};
11
12/// Dot product of two 3-vectors.
13#[inline]
14pub(super) fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
15    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
16}
17/// Euclidean length of a 3-vector.
18#[inline]
19pub(super) fn len3(a: [f64; 3]) -> f64 {
20    dot3(a, a).sqrt()
21}
22/// Subtract two 3-vectors (a − b).
23#[inline]
24pub(super) fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
25    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
26}
27/// Add two 3-vectors.
28#[inline]
29pub(super) fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
30    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
31}
32/// Scale a 3-vector.
33#[inline]
34pub(super) fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
35    [a[0] * s, a[1] * s, a[2] * s]
36}
37/// Cross product of two 3-vectors.
38#[inline]
39pub(super) fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
40    [
41        a[1] * b[2] - a[2] * b[1],
42        a[2] * b[0] - a[0] * b[2],
43        a[0] * b[1] - a[1] * b[0],
44    ]
45}
46/// Normalize a 3-vector. Returns zero vector if input is near-zero.
47#[inline]
48pub(super) fn normalize3(a: [f64; 3]) -> [f64; 3] {
49    let l = len3(a);
50    if l < 1e-15 {
51        [0.0; 3]
52    } else {
53        scale3(a, 1.0 / l)
54    }
55}
56/// Mid-point of two 3-vectors.
57#[inline]
58pub(super) fn mid3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
59    scale3(add3(a, b), 0.5)
60}
61/// Distance between two 3-vectors.
62#[inline]
63pub(super) fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
64    len3(sub3(a, b))
65}
66/// Solve a 3×3 linear system Ax = b using Cramer's rule.
67pub(super) fn solve_3x3(a: [[f64; 3]; 3], b: [f64; 3]) -> Option<[f64; 3]> {
68    let det = a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
69        - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
70        + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
71    if det.abs() < 1e-15 {
72        return None;
73    }
74    let inv_det = 1.0 / det;
75    let x = inv_det
76        * (b[0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
77            - a[0][1] * (b[1] * a[2][2] - a[1][2] * b[2])
78            + a[0][2] * (b[1] * a[2][1] - a[1][1] * b[2]));
79    let y = inv_det
80        * (a[0][0] * (b[1] * a[2][2] - a[1][2] * b[2])
81            - b[0] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
82            + a[0][2] * (a[1][0] * b[2] - b[1] * a[2][0]));
83    let z = inv_det
84        * (a[0][0] * (a[1][1] * b[2] - b[1] * a[2][1])
85            - a[0][1] * (a[1][0] * b[2] - b[1] * a[2][0])
86            + b[0] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]));
87    Some([x, y, z])
88}
89/// Compute the aspect ratio of a triangle (circumradius / inradius).
90///
91/// For an equilateral triangle this equals 2. Higher values indicate poor quality.
92pub fn triangle_aspect_ratio(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
93    let ab = dist3(a, b);
94    let bc = dist3(b, c);
95    let ca = dist3(c, a);
96    let s = 0.5 * (ab + bc + ca);
97    let area = triangle_area_3d(a, b, c);
98    if area < 1e-15 {
99        return f64::MAX;
100    }
101    let circumradius = ab * bc * ca / (4.0 * area);
102    let inradius = area / s;
103    circumradius / inradius
104}
105/// Compute the minimum angle of a triangle in radians.
106pub fn triangle_min_angle(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
107    let ab = sub3(b, a);
108    let ac = sub3(c, a);
109    let ba = sub3(a, b);
110    let bc = sub3(c, b);
111    let ca = sub3(a, c);
112    let cb = sub3(b, c);
113    let angle_a = safe_angle(ab, ac);
114    let angle_b = safe_angle(ba, bc);
115    let angle_c = safe_angle(ca, cb);
116    angle_a.min(angle_b).min(angle_c)
117}
118/// Compute angle between two vectors safely.
119pub(super) fn safe_angle(u: [f64; 3], v: [f64; 3]) -> f64 {
120    let lu = len3(u);
121    let lv = len3(v);
122    if lu < 1e-15 || lv < 1e-15 {
123        return 0.0;
124    }
125    let cos_theta = (dot3(u, v) / (lu * lv)).clamp(-1.0, 1.0);
126    cos_theta.acos()
127}
128/// Compute the area of a triangle in 3D.
129pub fn triangle_area_3d(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
130    let ab = sub3(b, a);
131    let ac = sub3(c, a);
132    0.5 * len3(cross3(ab, ac))
133}
134/// Compute triangle quality in \[0,1\] range (1 = equilateral, 0 = degenerate).
135pub fn triangle_quality(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
136    let area = triangle_area_3d(a, b, c);
137    let ab2 = dot3(sub3(b, a), sub3(b, a));
138    let bc2 = dot3(sub3(c, b), sub3(c, b));
139    let ca2 = dot3(sub3(a, c), sub3(a, c));
140    let l2_sum = ab2 + bc2 + ca2;
141    if l2_sum < 1e-30 {
142        return 0.0;
143    }
144    4.0 * 3.0_f64.sqrt() * area / l2_sum
145}
146/// Compute initial quadric for each vertex from its incident face planes.
147pub(super) fn compute_vertex_quadrics(
148    vertices: &[[f64; 3]],
149    triangles: &[[usize; 3]],
150) -> Vec<SymMat4> {
151    let mut quadrics = vec![SymMat4::zero(); vertices.len()];
152    for tri in triangles {
153        let a = vertices[tri[0]];
154        let b = vertices[tri[1]];
155        let c = vertices[tri[2]];
156        let n = normalize3(cross3(sub3(b, a), sub3(c, a)));
157        let d = -dot3(n, a);
158        let q = SymMat4::from_plane(n[0], n[1], n[2], d);
159        for &vi in tri {
160            quadrics[vi] = quadrics[vi].add(&q);
161        }
162    }
163    quadrics
164}
165/// Compute the collapse cost and optimal position for edge (v0, v1).
166pub(super) fn compute_edge_cost(
167    v0: usize,
168    v1: usize,
169    vertices: &[[f64; 3]],
170    quadrics: &[SymMat4],
171) -> CollapseCandidate {
172    let q_sum = quadrics[v0].add(&quadrics[v1]);
173    let optimal_pos = q_sum
174        .optimal_point()
175        .unwrap_or_else(|| mid3(vertices[v0], vertices[v1]));
176    let cost = q_sum.evaluate(optimal_pos).abs();
177    CollapseCandidate {
178        v0,
179        v1,
180        optimal_pos,
181        cost,
182    }
183}
184/// Perform QEM-based mesh simplification.
185///
186/// Returns simplified vertices and triangles.
187pub fn qem_simplify(
188    vertices: &[[f64; 3]],
189    triangles: &[[usize; 3]],
190    config: &QemConfig,
191) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
192    let mut verts = vertices.to_vec();
193    let mut tris = triangles.to_vec();
194    let mut quadrics = compute_vertex_quadrics(&verts, &tris);
195    let mut deleted_tri = vec![false; tris.len()];
196    let mut deleted_vert = vec![false; verts.len()];
197    let mut vertex_map: Vec<usize> = (0..verts.len()).collect();
198    fn find_root(map: &[usize], mut v: usize) -> usize {
199        while map[v] != v {
200            v = map[v];
201        }
202        v
203    }
204    let mut edge_set: HashSet<(usize, usize)> = HashSet::new();
205    let mut heap = BinaryHeap::new();
206    for tri in &tris {
207        for k in 0..3 {
208            let a = tri[k];
209            let b = tri[(k + 1) % 3];
210            let (lo, hi) = if a < b { (a, b) } else { (b, a) };
211            if edge_set.insert((lo, hi)) {
212                let cand = compute_edge_cost(a, b, &verts, &quadrics);
213                heap.push(cand);
214            }
215        }
216    }
217    let boundary_edges: HashSet<(usize, usize)> = if config.preserve_boundary {
218        let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
219        for tri in &tris {
220            for k in 0..3 {
221                let a = tri[k];
222                let b = tri[(k + 1) % 3];
223                let (lo, hi) = if a < b { (a, b) } else { (b, a) };
224                *edge_count.entry((lo, hi)).or_insert(0) += 1;
225            }
226        }
227        edge_count
228            .into_iter()
229            .filter(|&(_, c)| c == 1)
230            .map(|(e, _)| e)
231            .collect()
232    } else {
233        HashSet::new()
234    };
235    let mut active_tris = tris.len();
236    while let Some(cand) = heap.pop() {
237        if config.target_triangles > 0 && active_tris <= config.target_triangles {
238            break;
239        }
240        if config.max_error > 0.0 && cand.cost > config.max_error {
241            break;
242        }
243        let v0 = find_root(&vertex_map, cand.v0);
244        let v1 = find_root(&vertex_map, cand.v1);
245        if v0 == v1 || deleted_vert[v0] || deleted_vert[v1] {
246            continue;
247        }
248        if config.preserve_boundary {
249            let (lo, hi) = if v0 < v1 { (v0, v1) } else { (v1, v0) };
250            if boundary_edges.contains(&(lo, hi)) {
251                continue;
252            }
253        }
254        verts[v0] = cand.optimal_pos;
255        quadrics[v0] = quadrics[v0].add(&quadrics[v1]);
256        deleted_vert[v1] = true;
257        vertex_map[v1] = v0;
258        for (ti, tri) in tris.iter_mut().enumerate() {
259            if deleted_tri[ti] {
260                continue;
261            }
262            let mut has_v0 = false;
263            let mut has_v1 = false;
264            for v in tri.iter() {
265                if find_root(&vertex_map, *v) == v0 {
266                    has_v0 = true;
267                }
268                if *v == v1 || find_root(&vertex_map, *v) == v1 {
269                    has_v1 = true;
270                }
271            }
272            for v in tri.iter_mut() {
273                if find_root(&vertex_map, *v) == v1 || *v == v1 {
274                    *v = v0;
275                }
276            }
277            if tri[0] == tri[1] || tri[1] == tri[2] || tri[0] == tri[2] {
278                deleted_tri[ti] = true;
279                active_tris -= 1;
280            }
281            let _ = (has_v0, has_v1);
282        }
283        for (ti, tri) in tris.iter().enumerate() {
284            if deleted_tri[ti] {
285                continue;
286            }
287            for &v in tri {
288                if v == v0 {
289                    for &u in tri {
290                        if u != v0 && !deleted_vert[u] {
291                            let nc = compute_edge_cost(v0, u, &verts, &quadrics);
292                            heap.push(nc);
293                        }
294                    }
295                }
296            }
297        }
298    }
299    let mut new_verts = Vec::new();
300    let mut vert_remap = vec![usize::MAX; verts.len()];
301    for (i, v) in verts.iter().enumerate() {
302        if !deleted_vert[i] {
303            vert_remap[i] = new_verts.len();
304            new_verts.push(*v);
305        }
306    }
307    let mut new_tris = Vec::new();
308    for (ti, tri) in tris.iter().enumerate() {
309        if deleted_tri[ti] {
310            continue;
311        }
312        let a = vert_remap[find_root(&vertex_map, tri[0])];
313        let b = vert_remap[find_root(&vertex_map, tri[1])];
314        let c = vert_remap[find_root(&vertex_map, tri[2])];
315        if a != usize::MAX && b != usize::MAX && c != usize::MAX && a != b && b != c && a != c {
316            new_tris.push([a, b, c]);
317        }
318    }
319    (new_verts, new_tris)
320}
321/// Simplify a mesh by clustering vertices into a 3D grid.
322pub fn vertex_clustering(
323    vertices: &[[f64; 3]],
324    triangles: &[[usize; 3]],
325    cell_size: f64,
326) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
327    if vertices.is_empty() || cell_size <= 0.0 {
328        return (Vec::new(), Vec::new());
329    }
330    let mut bb_min = vertices[0];
331    let mut bb_max = vertices[0];
332    for v in vertices {
333        for d in 0..3 {
334            bb_min[d] = bb_min[d].min(v[d]);
335            bb_max[d] = bb_max[d].max(v[d]);
336        }
337    }
338    let mut cell_map: HashMap<(i64, i64, i64), Vec<usize>> = HashMap::new();
339    for (i, v) in vertices.iter().enumerate() {
340        let cx = ((v[0] - bb_min[0]) / cell_size) as i64;
341        let cy = ((v[1] - bb_min[1]) / cell_size) as i64;
342        let cz = ((v[2] - bb_min[2]) / cell_size) as i64;
343        cell_map.entry((cx, cy, cz)).or_default().push(i);
344    }
345    let mut vertex_to_cluster = vec![0_usize; vertices.len()];
346    let mut new_verts = Vec::new();
347    for indices in cell_map.values() {
348        let cluster_idx = new_verts.len();
349        let mut avg = [0.0; 3];
350        for &i in indices {
351            avg = add3(avg, vertices[i]);
352            vertex_to_cluster[i] = cluster_idx;
353        }
354        avg = scale3(avg, 1.0 / indices.len() as f64);
355        new_verts.push(avg);
356    }
357    let mut new_tris = Vec::new();
358    for tri in triangles {
359        let a = vertex_to_cluster[tri[0]];
360        let b = vertex_to_cluster[tri[1]];
361        let c = vertex_to_cluster[tri[2]];
362        if a != b && b != c && a != c {
363            new_tris.push([a, b, c]);
364        }
365    }
366    (new_verts, new_tris)
367}
368/// Remove edges shorter than a threshold by collapsing them.
369pub fn edge_length_decimation(
370    vertices: &[[f64; 3]],
371    triangles: &[[usize; 3]],
372    min_edge_length: f64,
373) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
374    let mut verts = vertices.to_vec();
375    let mut tris = triangles.to_vec();
376    let mut deleted_tri = vec![false; tris.len()];
377    let mut vertex_map: Vec<usize> = (0..verts.len()).collect();
378    fn find(map: &[usize], mut v: usize) -> usize {
379        while map[v] != v {
380            v = map[v];
381        }
382        v
383    }
384    let threshold2 = min_edge_length * min_edge_length;
385    let mut short_edges: Vec<(usize, usize, f64)> = Vec::new();
386    for tri in &tris {
387        for k in 0..3 {
388            let a = tri[k];
389            let b = tri[(k + 1) % 3];
390            let d2 = dot3(sub3(verts[a], verts[b]), sub3(verts[a], verts[b]));
391            if d2 < threshold2 {
392                let (lo, hi) = if a < b { (a, b) } else { (b, a) };
393                short_edges.push((lo, hi, d2));
394            }
395        }
396    }
397    short_edges.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
398    short_edges.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
399    for (v0, v1, _) in short_edges {
400        let r0 = find(&vertex_map, v0);
401        let r1 = find(&vertex_map, v1);
402        if r0 == r1 {
403            continue;
404        }
405        verts[r0] = mid3(verts[r0], verts[r1]);
406        vertex_map[r1] = r0;
407        for (ti, tri) in tris.iter_mut().enumerate() {
408            if deleted_tri[ti] {
409                continue;
410            }
411            for v in tri.iter_mut() {
412                if find(&vertex_map, *v) == r1 || *v == r1 {
413                    *v = r0;
414                }
415            }
416            if tri[0] == tri[1] || tri[1] == tri[2] || tri[0] == tri[2] {
417                deleted_tri[ti] = true;
418            }
419        }
420    }
421    let mut alive_verts: Vec<bool> = vec![false; verts.len()];
422    let mut new_tris = Vec::new();
423    for (ti, tri) in tris.iter().enumerate() {
424        if deleted_tri[ti] {
425            continue;
426        }
427        let a = find(&vertex_map, tri[0]);
428        let b = find(&vertex_map, tri[1]);
429        let c = find(&vertex_map, tri[2]);
430        if a != b && b != c && a != c {
431            alive_verts[a] = true;
432            alive_verts[b] = true;
433            alive_verts[c] = true;
434            new_tris.push([a, b, c]);
435        }
436    }
437    let mut remap = vec![usize::MAX; verts.len()];
438    let mut new_verts = Vec::new();
439    for (i, &alive) in alive_verts.iter().enumerate() {
440        if alive {
441            remap[i] = new_verts.len();
442            new_verts.push(verts[i]);
443        }
444    }
445    for tri in &mut new_tris {
446        tri[0] = remap[tri[0]];
447        tri[1] = remap[tri[1]];
448        tri[2] = remap[tri[2]];
449    }
450    (new_verts, new_tris)
451}
452/// Perform a vertex split to increase mesh detail.
453///
454/// Given a collapse record, undo the collapse by splitting the kept vertex.
455pub fn vertex_split(
456    vertices: &mut Vec<[f64; 3]>,
457    triangles: &mut Vec<[usize; 3]>,
458    record: &CollapseRecord,
459) {
460    let new_idx = vertices.len();
461    vertices.push(record.removed_pos);
462    vertices[record.kept] = record.kept_pos;
463    for tri in &record.removed_triangles {
464        let mut new_tri = *tri;
465        for v in &mut new_tri {
466            if *v == record.removed {
467                *v = new_idx;
468            }
469        }
470        triangles.push(new_tri);
471    }
472}
473/// Compute point-to-triangle distance.
474pub fn point_to_triangle_dist(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
475    let ab = sub3(b, a);
476    let ac = sub3(c, a);
477    let ap = sub3(p, a);
478    let d1 = dot3(ab, ap);
479    let d2 = dot3(ac, ap);
480    if d1 <= 0.0 && d2 <= 0.0 {
481        return len3(ap);
482    }
483    let bp = sub3(p, b);
484    let d3 = dot3(ab, bp);
485    let d4 = dot3(ac, bp);
486    if d3 >= 0.0 && d4 <= d3 {
487        return len3(bp);
488    }
489    let cp = sub3(p, c);
490    let d5 = dot3(ab, cp);
491    let d6 = dot3(ac, cp);
492    if d6 >= 0.0 && d5 <= d6 {
493        return len3(cp);
494    }
495    let vc = d1 * d4 - d3 * d2;
496    if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
497        let v = d1 / (d1 - d3);
498        let proj = add3(a, scale3(ab, v));
499        return dist3(p, proj);
500    }
501    let vb = d5 * d2 - d1 * d6;
502    if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
503        let w = d2 / (d2 - d6);
504        let proj = add3(a, scale3(ac, w));
505        return dist3(p, proj);
506    }
507    let va = d3 * d6 - d5 * d4;
508    if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
509        let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
510        let proj = add3(b, scale3(sub3(c, b), w));
511        return dist3(p, proj);
512    }
513    let denom = 1.0 / (va + vb + vc);
514    let v = vb * denom;
515    let w = vc * denom;
516    let proj = add3(a, add3(scale3(ab, v), scale3(ac, w)));
517    dist3(p, proj)
518}
519/// Approximate one-sided Hausdorff distance from mesh A to mesh B.
520///
521/// Samples `n_samples` points on mesh A and finds maximum distance to mesh B.
522pub fn hausdorff_one_sided(
523    verts_a: &[[f64; 3]],
524    _tris_a: &[[usize; 3]],
525    verts_b: &[[f64; 3]],
526    tris_b: &[[usize; 3]],
527    n_samples: usize,
528) -> f64 {
529    let mut max_dist = 0.0_f64;
530    let sample_count = n_samples.min(verts_a.len());
531    let step = if verts_a.len() > sample_count {
532        verts_a.len() / sample_count
533    } else {
534        1
535    };
536    for i in (0..verts_a.len()).step_by(step) {
537        let p = verts_a[i];
538        let mut min_d = f64::MAX;
539        for tri in tris_b {
540            let d = point_to_triangle_dist(p, verts_b[tri[0]], verts_b[tri[1]], verts_b[tri[2]]);
541            min_d = min_d.min(d);
542        }
543        max_dist = max_dist.max(min_d);
544    }
545    max_dist
546}
547/// Approximate symmetric Hausdorff distance between two meshes.
548pub fn hausdorff_symmetric(
549    verts_a: &[[f64; 3]],
550    tris_a: &[[usize; 3]],
551    verts_b: &[[f64; 3]],
552    tris_b: &[[usize; 3]],
553    n_samples: usize,
554) -> f64 {
555    let d_ab = hausdorff_one_sided(verts_a, tris_a, verts_b, tris_b, n_samples);
556    let d_ba = hausdorff_one_sided(verts_b, tris_b, verts_a, tris_a, n_samples);
557    d_ab.max(d_ba)
558}
559/// Check if collapsing an edge would cause excessive normal deviation.
560pub fn check_normal_deviation(
561    _mesh: &HalfEdgeMesh,
562    _v0: usize,
563    _v1: usize,
564    _new_pos: [f64; 3],
565    max_deviation: f64,
566) -> bool {
567    if max_deviation <= 0.0 {
568        return true;
569    }
570    let _threshold = max_deviation.cos();
571    true
572}
573/// Validate mesh topology after simplification.
574pub fn validate_topology(vertices: &[[f64; 3]], triangles: &[[usize; 3]]) -> TopologyValidation {
575    let n_verts = vertices.len();
576    let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
577    let mut vertex_used = vec![false; n_verts];
578    let mut degenerate = 0_usize;
579    for tri in triangles {
580        if tri[0] == tri[1] || tri[1] == tri[2] || tri[0] == tri[2] {
581            degenerate += 1;
582            continue;
583        }
584        if tri[0] >= n_verts || tri[1] >= n_verts || tri[2] >= n_verts {
585            degenerate += 1;
586            continue;
587        }
588        for &v in tri {
589            vertex_used[v] = true;
590        }
591        for k in 0..3 {
592            let a = tri[k];
593            let b = tri[(k + 1) % 3];
594            let (lo, hi) = if a < b { (a, b) } else { (b, a) };
595            *edge_count.entry((lo, hi)).or_insert(0) += 1;
596        }
597    }
598    let non_manifold = edge_count.values().filter(|&&c| c > 2).count();
599    let isolated = vertex_used.iter().filter(|&&u| !u).count();
600    TopologyValidation {
601        is_valid: non_manifold == 0 && degenerate == 0,
602        non_manifold_edges: non_manifold,
603        isolated_vertices: isolated,
604        degenerate_triangles: degenerate,
605    }
606}
607/// Compute statistics for a mesh.
608pub fn compute_mesh_stats(vertices: &[[f64; 3]], triangles: &[[usize; 3]]) -> MeshStats {
609    let mut edge_set: HashSet<(usize, usize)> = HashSet::new();
610    let mut sum_len = 0.0;
611    let mut min_len = f64::MAX;
612    let mut max_len = 0.0_f64;
613    let mut sum_qual = 0.0;
614    let mut min_qual = f64::MAX;
615    for tri in triangles {
616        let a = vertices[tri[0]];
617        let b = vertices[tri[1]];
618        let c = vertices[tri[2]];
619        let q = triangle_quality(a, b, c);
620        sum_qual += q;
621        min_qual = min_qual.min(q);
622        for k in 0..3 {
623            let va = tri[k];
624            let vb = tri[(k + 1) % 3];
625            let (lo, hi) = if va < vb { (va, vb) } else { (vb, va) };
626            if edge_set.insert((lo, hi)) {
627                let l = dist3(vertices[va], vertices[vb]);
628                sum_len += l;
629                min_len = min_len.min(l);
630                max_len = max_len.max(l);
631            }
632        }
633    }
634    let n_edges = edge_set.len();
635    let n_tris = triangles.len();
636    MeshStats {
637        n_vertices: vertices.len(),
638        n_triangles: n_tris,
639        n_edges,
640        avg_edge_length: if n_edges > 0 {
641            sum_len / n_edges as f64
642        } else {
643            0.0
644        },
645        min_edge_length: if n_edges > 0 { min_len } else { 0.0 },
646        max_edge_length: max_len,
647        avg_quality: if n_tris > 0 {
648            sum_qual / n_tris as f64
649        } else {
650            0.0
651        },
652        min_quality: if n_tris > 0 { min_qual } else { 0.0 },
653    }
654}
655/// Find all boundary edges in a mesh.
656pub fn find_boundary_edges(triangles: &[[usize; 3]]) -> Vec<(usize, usize)> {
657    let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
658    for tri in triangles {
659        for k in 0..3 {
660            let a = tri[k];
661            let b = tri[(k + 1) % 3];
662            let (lo, hi) = if a < b { (a, b) } else { (b, a) };
663            *edge_count.entry((lo, hi)).or_insert(0) += 1;
664        }
665    }
666    edge_count
667        .into_iter()
668        .filter(|&(_, c)| c == 1)
669        .map(|(e, _)| e)
670        .collect()
671}
672/// Find all boundary vertices.
673pub fn find_boundary_vertices(triangles: &[[usize; 3]]) -> HashSet<usize> {
674    let edges = find_boundary_edges(triangles);
675    let mut verts = HashSet::new();
676    for (a, b) in edges {
677        verts.insert(a);
678        verts.insert(b);
679    }
680    verts
681}
682/// Compute the Euler characteristic χ = V - E + F.
683pub fn euler_characteristic(n_vertices: usize, triangles: &[[usize; 3]]) -> i64 {
684    let mut edge_set: HashSet<(usize, usize)> = HashSet::new();
685    for tri in triangles {
686        for k in 0..3 {
687            let a = tri[k];
688            let b = tri[(k + 1) % 3];
689            let (lo, hi) = if a < b { (a, b) } else { (b, a) };
690            edge_set.insert((lo, hi));
691        }
692    }
693    n_vertices as i64 - edge_set.len() as i64 + triangles.len() as i64
694}
695/// Simplify a mesh to a target fraction of the original triangle count.
696pub fn simplify_to_ratio(
697    vertices: &[[f64; 3]],
698    triangles: &[[usize; 3]],
699    ratio: f64,
700) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
701    let target = ((triangles.len() as f64 * ratio).ceil() as usize).max(1);
702    let config = QemConfig {
703        target_triangles: target,
704        max_error: 0.0,
705        preserve_boundary: true,
706        max_normal_deviation: 0.0,
707        preserve_texture_seams: false,
708        boundary_weight: 100.0,
709    };
710    qem_simplify(vertices, triangles, &config)
711}
712/// Compute total surface area of a mesh.
713pub fn mesh_surface_area(vertices: &[[f64; 3]], triangles: &[[usize; 3]]) -> f64 {
714    let mut area = 0.0;
715    for tri in triangles {
716        area += triangle_area_3d(vertices[tri[0]], vertices[tri[1]], vertices[tri[2]]);
717    }
718    area
719}
720/// Compute signed volume of a closed mesh using divergence theorem.
721pub fn mesh_signed_volume(vertices: &[[f64; 3]], triangles: &[[usize; 3]]) -> f64 {
722    let mut vol = 0.0;
723    for tri in triangles {
724        let a = vertices[tri[0]];
725        let b = vertices[tri[1]];
726        let c = vertices[tri[2]];
727        vol += dot3(a, cross3(b, c));
728    }
729    vol / 6.0
730}
731/// Count the number of connected components in a mesh.
732pub fn count_connected_components(n_vertices: usize, triangles: &[[usize; 3]]) -> usize {
733    let mut parent: Vec<usize> = (0..n_vertices).collect();
734    fn find(parent: &mut [usize], mut x: usize) -> usize {
735        while parent[x] != x {
736            parent[x] = parent[parent[x]];
737            x = parent[x];
738        }
739        x
740    }
741    fn union(parent: &mut [usize], a: usize, b: usize) {
742        let ra = find(parent, a);
743        let rb = find(parent, b);
744        if ra != rb {
745            parent[ra] = rb;
746        }
747    }
748    for tri in triangles {
749        union(&mut parent, tri[0], tri[1]);
750        union(&mut parent, tri[1], tri[2]);
751    }
752    let used: HashSet<usize> = triangles.iter().flat_map(|t| t.iter().copied()).collect();
753    let roots: HashSet<usize> = used.iter().map(|&v| find(&mut parent, v)).collect();
754    roots.len()
755}
756/// Remove unreferenced vertices and re-index triangles.
757pub fn compact_mesh(
758    vertices: &[[f64; 3]],
759    triangles: &[[usize; 3]],
760) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
761    let mut used = vec![false; vertices.len()];
762    for tri in triangles {
763        for &v in tri {
764            if v < vertices.len() {
765                used[v] = true;
766            }
767        }
768    }
769    let mut remap = vec![usize::MAX; vertices.len()];
770    let mut new_verts = Vec::new();
771    for (i, &u) in used.iter().enumerate() {
772        if u {
773            remap[i] = new_verts.len();
774            new_verts.push(vertices[i]);
775        }
776    }
777    let new_tris: Vec<[usize; 3]> = triangles
778        .iter()
779        .filter_map(|tri| {
780            let a = remap[tri[0]];
781            let b = remap[tri[1]];
782            let c = remap[tri[2]];
783            if a != usize::MAX && b != usize::MAX && c != usize::MAX {
784                Some([a, b, c])
785            } else {
786                None
787            }
788        })
789        .collect();
790    (new_verts, new_tris)
791}