Skip to main content

oxiphysics_geometry/mesh_processing/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::{HashMap, HashSet, VecDeque};
6
7use super::types::{
8    AtlasPatch, BooleanResult, FeatureEdge, MeshQualityStats, ProcessMesh, Quadric,
9    TriangleQuality, UvParameterization,
10};
11
12/// A 3-D vertex position stored as plain `[f64; 3]`.
13pub type Vertex = [f64; 3];
14/// A triangular face represented as three vertex indices.
15pub type Face = [usize; 3];
16/// A 2-D UV coordinate pair.
17pub type Uv = [f64; 2];
18#[inline]
19pub(super) fn vec3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
20    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
21}
22#[inline]
23pub(super) fn vec3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
24    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
25}
26#[inline]
27pub(super) fn vec3_scale(a: [f64; 3], s: f64) -> [f64; 3] {
28    [a[0] * s, a[1] * s, a[2] * s]
29}
30#[inline]
31pub(super) fn vec3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
32    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
33}
34#[inline]
35pub(super) fn vec3_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
36    [
37        a[1] * b[2] - a[2] * b[1],
38        a[2] * b[0] - a[0] * b[2],
39        a[0] * b[1] - a[1] * b[0],
40    ]
41}
42#[inline]
43pub(super) fn vec3_len(a: [f64; 3]) -> f64 {
44    (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
45}
46#[inline]
47pub(super) fn vec3_normalize(a: [f64; 3]) -> [f64; 3] {
48    let l = vec3_len(a);
49    if l < 1e-15 {
50        [0.0, 0.0, 0.0]
51    } else {
52        vec3_scale(a, 1.0 / l)
53    }
54}
55#[inline]
56pub(super) fn vec3_lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
57    vec3_add(vec3_scale(a, 1.0 - t), vec3_scale(b, t))
58}
59/// Apply `iters` passes of uniform Laplacian smoothing with step `lambda`.
60///
61/// Each vertex is moved toward the average of its neighbours.
62pub fn laplacian_smooth(mesh: &ProcessMesh, lambda: f64, iters: usize) -> ProcessMesh {
63    let mut out = mesh.clone();
64    let adj = mesh.build_adjacency();
65    for _ in 0..iters {
66        let old = out.verts.clone();
67        for (i, nbrs) in adj.iter().enumerate() {
68            if nbrs.is_empty() {
69                continue;
70            }
71            let mut avg = [0.0_f64; 3];
72            for &j in nbrs {
73                avg = vec3_add(avg, old[j]);
74            }
75            avg = vec3_scale(avg, 1.0 / nbrs.len() as f64);
76            let delta = vec3_sub(avg, old[i]);
77            out.verts[i] = vec3_add(old[i], vec3_scale(delta, lambda));
78        }
79    }
80    out
81}
82/// Apply Taubin λ/μ smoothing to reduce shrinkage.
83///
84/// Alternates a positive step (λ) with a negative step (μ < 0) so that
85/// the mesh volume is approximately preserved.
86pub fn taubin_smooth(mesh: &ProcessMesh, lambda: f64, mu: f64, iters: usize) -> ProcessMesh {
87    let mut out = mesh.clone();
88    let adj = mesh.build_adjacency();
89    let one_step = |verts: &[Vertex], step: f64, a: &[Vec<usize>]| -> Vec<Vertex> {
90        let mut next = verts.to_vec();
91        for (i, nbrs) in a.iter().enumerate() {
92            if nbrs.is_empty() {
93                continue;
94            }
95            let mut avg = [0.0_f64; 3];
96            for &j in nbrs {
97                avg = vec3_add(avg, verts[j]);
98            }
99            avg = vec3_scale(avg, 1.0 / nbrs.len() as f64);
100            let delta = vec3_sub(avg, verts[i]);
101            next[i] = vec3_add(verts[i], vec3_scale(delta, step));
102        }
103        next
104    };
105    for _ in 0..iters {
106        out.verts = one_step(&out.verts, lambda, &adj);
107        out.verts = one_step(&out.verts, mu, &adj);
108    }
109    out
110}
111/// Simple midpoint (butterfly) subdivision: each edge is split and each
112/// triangle becomes four smaller triangles.
113pub fn midpoint_subdivide(mesh: &ProcessMesh) -> ProcessMesh {
114    let mut verts = mesh.verts.clone();
115    let mut faces = Vec::new();
116    let mut edge_mid: HashMap<(usize, usize), usize> = HashMap::new();
117    let mut get_mid = |a: usize, b: usize, v: &mut Vec<Vertex>| -> usize {
118        let key = if a < b { (a, b) } else { (b, a) };
119        *edge_mid.entry(key).or_insert_with(|| {
120            let mid = vec3_scale(vec3_add(v[a], v[b]), 0.5);
121            let idx = v.len();
122            v.push(mid);
123            idx
124        })
125    };
126    for &[a, b, c] in &mesh.faces {
127        let ab = get_mid(a, b, &mut verts);
128        let bc = get_mid(b, c, &mut verts);
129        let ca = get_mid(c, a, &mut verts);
130        faces.push([a, ab, ca]);
131        faces.push([ab, b, bc]);
132        faces.push([ca, bc, c]);
133        faces.push([ab, bc, ca]);
134    }
135    ProcessMesh::new(verts, faces)
136}
137/// Loop subdivision with proper weighting for interior vertices.
138///
139/// Implements Charles Loop's 1987 scheme for smooth subdivision surfaces.
140pub fn loop_subdivide(mesh: &ProcessMesh) -> ProcessMesh {
141    let mut verts = mesh.verts.clone();
142    let mut edge_mid: HashMap<(usize, usize), usize> = HashMap::new();
143    let mut edge_opp: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
144    for &[a, b, c] in &mesh.faces {
145        let edges = [(a, b, c), (b, c, a), (c, a, b)];
146        for (i, j, k) in edges {
147            let key = if i < j { (i, j) } else { (j, i) };
148            edge_opp.entry(key).or_default().push(k);
149        }
150    }
151    let mut get_mid = |a: usize, b: usize, v: &mut Vec<Vertex>| -> usize {
152        let key = if a < b { (a, b) } else { (b, a) };
153        *edge_mid.entry(key).or_insert_with(|| {
154            let opps = edge_opp.get(&key).cloned().unwrap_or_default();
155            let pos = if opps.len() == 2 {
156                let sum_ab = vec3_scale(vec3_add(v[a], v[b]), 3.0 / 8.0);
157                let sum_op = vec3_scale(vec3_add(v[opps[0]], v[opps[1]]), 1.0 / 8.0);
158                vec3_add(sum_ab, sum_op)
159            } else {
160                vec3_scale(vec3_add(v[a], v[b]), 0.5)
161            };
162            let idx = v.len();
163            v.push(pos);
164            idx
165        })
166    };
167    let adj = mesh.build_adjacency();
168    let orig = mesh.verts.clone();
169    let n = orig.len();
170    let mut new_pos = orig.clone();
171    for i in 0..n {
172        let nbrs = &adj[i];
173        let k = nbrs.len() as f64;
174        let beta = if k <= 3.0 {
175            3.0 / 16.0
176        } else {
177            3.0 / (8.0 * k)
178        };
179        let mut sum = [0.0_f64; 3];
180        for &j in nbrs {
181            sum = vec3_add(sum, orig[j]);
182        }
183        new_pos[i] = vec3_add(vec3_scale(orig[i], 1.0 - k * beta), vec3_scale(sum, beta));
184    }
185    let mut faces = Vec::new();
186    for &[a, b, c] in &mesh.faces {
187        let ab = get_mid(a, b, &mut verts);
188        let bc = get_mid(b, c, &mut verts);
189        let ca = get_mid(c, a, &mut verts);
190        faces.push([a, ab, ca]);
191        faces.push([ab, b, bc]);
192        faces.push([ca, bc, c]);
193        faces.push([ab, bc, ca]);
194    }
195    verts[..n].copy_from_slice(&new_pos[..n]);
196    ProcessMesh::new(verts, faces)
197}
198/// Catmull-Clark subdivision adapted for triangle meshes.
199///
200/// For pure triangle meshes this is approximated by computing face centroids,
201/// edge midpoints, and updated vertex positions following the CC weights.
202pub fn catmull_clark_subdivide(mesh: &ProcessMesh) -> ProcessMesh {
203    let nv = mesh.verts.len();
204    let mut new_verts = mesh.verts.clone();
205    let mut face_pts: Vec<Vertex> = Vec::with_capacity(mesh.faces.len());
206    for &[a, b, c] in &mesh.faces {
207        let fp = vec3_scale(
208            vec3_add(vec3_add(mesh.verts[a], mesh.verts[b]), mesh.verts[c]),
209            1.0 / 3.0,
210        );
211        face_pts.push(fp);
212    }
213    let mut edge_map: HashMap<(usize, usize), usize> = HashMap::new();
214    let mut edge_pts: Vec<Vertex> = Vec::new();
215    let mut vert_face_avg = vec![[0.0_f64; 3]; nv];
216    let mut vert_face_cnt = vec![0usize; nv];
217    for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
218        let fp = face_pts[fi];
219        for &v in &[a, b, c] {
220            vert_face_avg[v] = vec3_add(vert_face_avg[v], fp);
221            vert_face_cnt[v] += 1;
222        }
223        for (i, j) in [(a, b), (b, c), (c, a)] {
224            let key = if i < j { (i, j) } else { (j, i) };
225            edge_map.entry(key).or_insert_with(|| {
226                let ep = vec3_scale(vec3_add(mesh.verts[i], mesh.verts[j]), 0.5);
227                let idx = edge_pts.len();
228                edge_pts.push(ep);
229                idx
230            });
231        }
232    }
233    let adj = mesh.build_adjacency();
234    for i in 0..nv {
235        let n = vert_face_cnt[i] as f64;
236        if n == 0.0 {
237            continue;
238        }
239        let f_avg = vec3_scale(vert_face_avg[i], 1.0 / n);
240        let nbrs = &adj[i];
241        let mut e_avg = [0.0_f64; 3];
242        for &j in nbrs {
243            e_avg = vec3_add(e_avg, mesh.verts[j]);
244        }
245        let m = nbrs.len() as f64;
246        e_avg = vec3_scale(e_avg, 1.0 / m.max(1.0));
247        new_verts[i] = vec3_scale(
248            vec3_add(
249                vec3_add(f_avg, vec3_scale(e_avg, 2.0)),
250                vec3_scale(mesh.verts[i], (n - 3.0).max(0.0)),
251            ),
252            1.0 / n,
253        );
254    }
255    let ep_offset = nv;
256    let fp_offset = ep_offset + edge_pts.len();
257    for ep in &edge_pts {
258        new_verts.push(*ep);
259    }
260    for fp in &face_pts {
261        new_verts.push(*fp);
262    }
263    let mut faces = Vec::new();
264    for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
265        let fp_idx = fp_offset + fi;
266        let ab_idx = ep_offset
267            + *edge_map
268                .get(&if a < b { (a, b) } else { (b, a) })
269                .expect("key must exist");
270        let bc_idx = ep_offset
271            + *edge_map
272                .get(&if b < c { (b, c) } else { (c, b) })
273                .expect("key must exist");
274        let ca_idx = ep_offset
275            + *edge_map
276                .get(&if c < a { (c, a) } else { (a, c) })
277                .expect("key must exist");
278        faces.push([a, ab_idx, fp_idx]);
279        faces.push([ab_idx, b, fp_idx]);
280        faces.push([b, bc_idx, fp_idx]);
281        faces.push([bc_idx, c, fp_idx]);
282        faces.push([c, ca_idx, fp_idx]);
283        faces.push([ca_idx, a, fp_idx]);
284    }
285    ProcessMesh::new(new_verts, faces)
286}
287/// Build one quadric per face (the fundamental plane quadric).
288pub(super) fn face_quadrics(mesh: &ProcessMesh) -> Vec<Quadric> {
289    mesh.faces
290        .iter()
291        .map(|&[a, b, c]| {
292            let va = mesh.verts[a];
293            let vb = mesh.verts[b];
294            let vc = mesh.verts[c];
295            let ab = vec3_sub(vb, va);
296            let ac = vec3_sub(vc, va);
297            let n = vec3_normalize(vec3_cross(ab, ac));
298            let d = -vec3_dot(n, va);
299            Quadric::from_plane(n[0], n[1], n[2], d)
300        })
301        .collect()
302}
303/// Build per-vertex quadrics by summing face quadrics for all adjacent faces.
304pub(super) fn vertex_quadrics(mesh: &ProcessMesh) -> Vec<Quadric> {
305    let nv = mesh.verts.len();
306    let fq = face_quadrics(mesh);
307    let mut vq = vec![Quadric::zero(); nv];
308    for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
309        for &v in &[a, b, c] {
310            vq[v] = vq[v].add(&fq[fi]);
311        }
312    }
313    vq
314}
315/// Decimate a mesh to at most `target_faces` triangles using quadric error
316/// metrics with greedy edge collapses.
317pub fn qem_decimate(mesh: &ProcessMesh, target_faces: usize) -> ProcessMesh {
318    if mesh.faces.len() <= target_faces {
319        return mesh.clone();
320    }
321    let nv = mesh.verts.len();
322    let mut verts = mesh.verts.clone();
323    let mut faces = mesh.faces.clone();
324    let mut vq = vertex_quadrics(mesh);
325    let mut removed_verts = vec![false; nv];
326    let mut removed_faces = vec![false; faces.len()];
327    while faces.iter().filter(|&&_f| true).count() - removed_faces.iter().filter(|&&r| r).count()
328        > target_faces
329    {
330        let mut best_cost = f64::INFINITY;
331        let mut best_edge: Option<(usize, usize)> = None;
332        let mut best_pos = [0.0_f64; 3];
333        let mut seen = HashSet::new();
334        for &[a, b, c] in &faces {
335            for (i, j) in [(a, b), (b, c), (c, a)] {
336                let key = if i < j { (i, j) } else { (j, i) };
337                if seen.contains(&key) {
338                    continue;
339                }
340                seen.insert(key);
341                if removed_verts[i] || removed_verts[j] {
342                    continue;
343                }
344                let q = vq[i].add(&vq[j]);
345                let mid = vec3_scale(vec3_add(verts[i], verts[j]), 0.5);
346                let cost = q.evaluate(mid);
347                if cost < best_cost {
348                    best_cost = cost;
349                    best_edge = Some((i, j));
350                    best_pos = mid;
351                }
352            }
353        }
354        if let Some((vi, vj)) = best_edge {
355            verts[vi] = best_pos;
356            vq[vi] = vq[vi].add(&vq[vj]);
357            removed_verts[vj] = true;
358            for (fi, f) in faces.iter_mut().enumerate() {
359                if removed_faces[fi] {
360                    continue;
361                }
362                for idx in f.iter_mut() {
363                    if *idx == vj {
364                        *idx = vi;
365                    }
366                }
367                if f[0] == f[1] || f[1] == f[2] || f[0] == f[2] {
368                    removed_faces[fi] = true;
369                }
370            }
371        } else {
372            break;
373        }
374    }
375    let remap: Vec<usize> = {
376        let mut m = vec![0usize; nv];
377        let mut cnt = 0;
378        for i in 0..nv {
379            if !removed_verts[i] {
380                m[i] = cnt;
381                cnt += 1;
382            }
383        }
384        m
385    };
386    let new_verts: Vec<Vertex> = (0..nv)
387        .filter(|&i| !removed_verts[i])
388        .map(|i| verts[i])
389        .collect();
390    let new_faces: Vec<Face> = faces
391        .iter()
392        .enumerate()
393        .filter(|&(fi, _)| !removed_faces[fi])
394        .map(|(_, &[a, b, c])| [remap[a], remap[b], remap[c]])
395        .collect();
396    ProcessMesh::new(new_verts, new_faces)
397}
398/// Compute quality metrics for a single triangle given its three vertices.
399pub fn triangle_quality(va: Vertex, vb: Vertex, vc: Vertex) -> TriangleQuality {
400    let ab = vec3_sub(vb, va);
401    let ac = vec3_sub(vc, va);
402    let bc = vec3_sub(vc, vb);
403    let la = vec3_len(bc);
404    let lb = vec3_len(ac);
405    let lc = vec3_len(ab);
406    let s = (la + lb + lc) / 2.0;
407    let area2 = vec3_len(vec3_cross(ab, ac));
408    let area = area2 / 2.0;
409    let inradius = if s > 1e-15 { area / s } else { 0.0 };
410    let circumradius = if area > 1e-15 {
411        la * lb * lc / (4.0 * area)
412    } else {
413        f64::INFINITY
414    };
415    let aspect_ratio = if inradius > 1e-15 {
416        circumradius / (2.0 * inradius)
417    } else {
418        f64::INFINITY
419    };
420    let angle_a = if la > 1e-15 && lb > 1e-15 {
421        let cos_a = (lb * lb + lc * lc - la * la) / (2.0 * lb * lc);
422        cos_a.clamp(-1.0, 1.0).acos()
423    } else {
424        0.0
425    };
426    let angle_b = if la > 1e-15 && lc > 1e-15 {
427        let cos_b = (la * la + lc * lc - lb * lb) / (2.0 * la * lc);
428        cos_b.clamp(-1.0, 1.0).acos()
429    } else {
430        0.0
431    };
432    let angle_c = std::f64::consts::PI - angle_a - angle_b;
433    let perim = 2.0 * s;
434    let ideal_area = perim * perim * (3.0_f64.sqrt()) / 36.0;
435    let area_quality = if ideal_area > 1e-15 {
436        (area / ideal_area).min(1.0)
437    } else {
438        0.0
439    };
440    TriangleQuality {
441        aspect_ratio,
442        min_angle: angle_a.min(angle_b).min(angle_c),
443        max_angle: angle_a.max(angle_b).max(angle_c),
444        area_quality,
445    }
446}
447/// Compute aggregate quality statistics for `mesh`.
448pub fn mesh_quality_stats(mesh: &ProcessMesh) -> MeshQualityStats {
449    if mesh.faces.is_empty() {
450        return MeshQualityStats {
451            mean_aspect_ratio: 0.0,
452            max_aspect_ratio: 0.0,
453            min_angle_deg: 0.0,
454            mean_area_quality: 0.0,
455        };
456    }
457    let n = mesh.faces.len() as f64;
458    let mut sum_ar = 0.0;
459    let mut max_ar = 0.0_f64;
460    let mut min_ang = f64::INFINITY;
461    let mut sum_aq = 0.0;
462    for &[a, b, c] in &mesh.faces {
463        let q = triangle_quality(mesh.verts[a], mesh.verts[b], mesh.verts[c]);
464        sum_ar += q.aspect_ratio;
465        max_ar = max_ar.max(q.aspect_ratio);
466        min_ang = min_ang.min(q.min_angle);
467        sum_aq += q.area_quality;
468    }
469    MeshQualityStats {
470        mean_aspect_ratio: sum_ar / n,
471        max_aspect_ratio: max_ar,
472        min_angle_deg: min_ang.to_degrees(),
473        mean_area_quality: sum_aq / n,
474    }
475}
476/// Detect boundary (open) edge loops in `mesh`.
477///
478/// Returns a list of loops, where each loop is an ordered list of vertex
479/// indices forming the boundary.
480pub fn detect_boundary_loops(mesh: &ProcessMesh) -> Vec<Vec<usize>> {
481    let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
482    for &[a, b, c] in &mesh.faces {
483        for (i, j) in [(a, b), (b, c), (c, a)] {
484            *edge_count.entry((i, j)).or_insert(0) += 1;
485        }
486    }
487    let mut boundary_adj: HashMap<usize, usize> = HashMap::new();
488    for &(i, j) in edge_count.keys() {
489        if edge_count.get(&(j, i)).copied().unwrap_or(0) == 0 {
490            boundary_adj.insert(i, j);
491        }
492    }
493    let mut visited = HashSet::new();
494    let mut loops = Vec::new();
495    for &start in boundary_adj.keys() {
496        if visited.contains(&start) {
497            continue;
498        }
499        let mut loop_ = Vec::new();
500        let mut cur = start;
501        loop {
502            if visited.contains(&cur) {
503                break;
504            }
505            visited.insert(cur);
506            loop_.push(cur);
507            match boundary_adj.get(&cur) {
508                Some(&next) => cur = next,
509                None => break,
510            }
511        }
512        if !loop_.is_empty() {
513            loops.push(loop_);
514        }
515    }
516    loops
517}
518/// Fill a single boundary loop with a fan triangulation from its centroid.
519///
520/// Returns the new faces to append.
521pub fn fill_hole_fan(verts: &mut Vec<Vertex>, loop_: &[usize]) -> Vec<Face> {
522    if loop_.len() < 3 {
523        return Vec::new();
524    }
525    let mut centroid = [0.0_f64; 3];
526    for &vi in loop_ {
527        centroid = vec3_add(centroid, verts[vi]);
528    }
529    centroid = vec3_scale(centroid, 1.0 / loop_.len() as f64);
530    let center_idx = verts.len();
531    verts.push(centroid);
532    let mut new_faces = Vec::new();
533    let n = loop_.len();
534    for i in 0..n {
535        new_faces.push([loop_[(i + 1) % n], loop_[i], center_idx]);
536    }
537    new_faces
538}
539/// Repair `mesh` by detecting all boundary loops and filling each with a fan.
540pub fn fill_all_holes(mesh: &ProcessMesh) -> ProcessMesh {
541    let mut out = mesh.clone();
542    loop {
543        let loops = detect_boundary_loops(&out);
544        if loops.is_empty() {
545            break;
546        }
547        for lp in &loops {
548            let new_faces = fill_hole_fan(&mut out.verts, lp);
549            out.faces.extend(new_faces);
550        }
551        if detect_boundary_loops(&out).len() >= loops.len() {
552            break;
553        }
554    }
555    out
556}
557/// Remove non-manifold vertices (vertices shared by more than two disconnected
558/// fans) by duplicating them.
559pub fn fix_non_manifold_vertices(mesh: &ProcessMesh) -> ProcessMesh {
560    let nv = mesh.verts.len();
561    let mut vert_faces: Vec<Vec<usize>> = vec![Vec::new(); nv];
562    for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
563        vert_faces[a].push(fi);
564        vert_faces[b].push(fi);
565        vert_faces[c].push(fi);
566    }
567    let mut new_verts = mesh.verts.clone();
568    let mut new_faces = mesh.faces.clone();
569    for vi in 0..nv {
570        let flist = &vert_faces[vi];
571        if flist.len() < 2 {
572            continue;
573        }
574        let face_set: HashSet<usize> = flist.iter().cloned().collect();
575        let mut visited = HashSet::new();
576        let mut queue = VecDeque::new();
577        queue.push_back(flist[0]);
578        while let Some(fi) = queue.pop_front() {
579            if visited.contains(&fi) {
580                continue;
581            }
582            visited.insert(fi);
583            let [a, b, c] = new_faces[fi];
584            for &fj in &vert_faces[a] {
585                if face_set.contains(&fj) && !visited.contains(&fj) {
586                    queue.push_back(fj);
587                }
588            }
589            for &fj in &vert_faces[b] {
590                if face_set.contains(&fj) && !visited.contains(&fj) {
591                    queue.push_back(fj);
592                }
593            }
594            for &fj in &vert_faces[c] {
595                if face_set.contains(&fj) && !visited.contains(&fj) {
596                    queue.push_back(fj);
597                }
598            }
599        }
600        let extra: Vec<usize> = flist
601            .iter()
602            .filter(|&&f| !visited.contains(&f))
603            .cloned()
604            .collect();
605        if !extra.is_empty() {
606            let new_idx = new_verts.len();
607            new_verts.push(new_verts[vi]);
608            for fi in extra {
609                for idx in new_faces[fi].iter_mut() {
610                    if *idx == vi {
611                        *idx = new_idx;
612                    }
613                }
614            }
615        }
616    }
617    ProcessMesh::new(new_verts, new_faces)
618}
619/// Detect feature edges in `mesh` whose dihedral angle exceeds
620/// `threshold_deg` degrees.
621pub fn detect_feature_edges(mesh: &ProcessMesh, threshold_deg: f64) -> Vec<FeatureEdge> {
622    let thresh = threshold_deg.to_radians();
623    let face_normals: Vec<[f64; 3]> = mesh
624        .faces
625        .iter()
626        .map(|&[a, b, c]| {
627            let ab = vec3_sub(mesh.verts[b], mesh.verts[a]);
628            let ac = vec3_sub(mesh.verts[c], mesh.verts[a]);
629            vec3_normalize(vec3_cross(ab, ac))
630        })
631        .collect();
632    let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
633    for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
634        for (i, j) in [(a, b), (b, c), (c, a)] {
635            let key = if i < j { (i, j) } else { (j, i) };
636            edge_faces.entry(key).or_default().push(fi);
637        }
638    }
639    let mut features = Vec::new();
640    for ((v0, v1), flist) in &edge_faces {
641        if flist.len() != 2 {
642            continue;
643        }
644        let n0 = face_normals[flist[0]];
645        let n1 = face_normals[flist[1]];
646        let cos_a = vec3_dot(n0, n1).clamp(-1.0, 1.0);
647        let angle = cos_a.acos();
648        if angle > thresh {
649            features.push(FeatureEdge {
650                v0: *v0,
651                v1: *v1,
652                dihedral_angle: angle,
653            });
654        }
655    }
656    features
657}
658/// Isotropic remeshing targeting a uniform target edge length.
659///
660/// Performs `iters` rounds of split-long / collapse-short / flip /
661/// Laplacian-smooth.
662pub fn isotropic_remesh(mesh: &ProcessMesh, target_len: f64, iters: usize) -> ProcessMesh {
663    let mut out = mesh.clone();
664    for _ in 0..iters {
665        out = split_long_edges(&out, target_len * 4.0 / 3.0);
666        out = collapse_short_edges(&out, target_len * 4.0 / 5.0);
667        out = laplacian_smooth(&out, 0.5, 1);
668    }
669    out
670}
671/// Split edges longer than `max_len` by inserting midpoints.
672pub fn split_long_edges(mesh: &ProcessMesh, max_len: f64) -> ProcessMesh {
673    let mut verts = mesh.verts.clone();
674    let mut faces = Vec::new();
675    let mut edge_mid: HashMap<(usize, usize), usize> = HashMap::new();
676    let mut get_mid = |a: usize, b: usize, v: &mut Vec<Vertex>| -> Option<usize> {
677        let d = vec3_len(vec3_sub(v[b], v[a]));
678        if d > max_len {
679            let key = if a < b { (a, b) } else { (b, a) };
680            let idx = *edge_mid.entry(key).or_insert_with(|| {
681                let mid = vec3_scale(vec3_add(v[a], v[b]), 0.5);
682                let i = v.len();
683                v.push(mid);
684                i
685            });
686            Some(idx)
687        } else {
688            None
689        }
690    };
691    for &[a, b, c] in &mesh.faces {
692        let ab = get_mid(a, b, &mut verts);
693        let bc = get_mid(b, c, &mut verts);
694        let ca = get_mid(c, a, &mut verts);
695        match (ab, bc, ca) {
696            (None, None, None) => faces.push([a, b, c]),
697            (Some(m), None, None) => {
698                faces.push([a, m, c]);
699                faces.push([m, b, c]);
700            }
701            (None, Some(m), None) => {
702                faces.push([a, b, m]);
703                faces.push([a, m, c]);
704            }
705            (None, None, Some(m)) => {
706                faces.push([a, b, m]);
707                faces.push([m, b, c]);
708            }
709            (Some(ab), Some(bc), None) => {
710                faces.push([a, ab, c]);
711                faces.push([ab, b, bc]);
712                faces.push([ab, bc, c]);
713            }
714            (Some(ab), None, Some(ca)) => {
715                faces.push([a, ab, ca]);
716                faces.push([ab, b, c]);
717                faces.push([ab, c, ca]);
718            }
719            (None, Some(bc), Some(ca)) => {
720                faces.push([a, b, bc]);
721                faces.push([a, bc, ca]);
722                faces.push([ca, bc, c]);
723            }
724            (Some(ab), Some(bc), Some(ca)) => {
725                faces.push([a, ab, ca]);
726                faces.push([ab, b, bc]);
727                faces.push([ca, bc, c]);
728                faces.push([ab, bc, ca]);
729            }
730        }
731    }
732    ProcessMesh::new(verts, faces)
733}
734/// Collapse edges shorter than `min_len` by merging endpoints.
735pub fn collapse_short_edges(mesh: &ProcessMesh, min_len: f64) -> ProcessMesh {
736    let nv = mesh.verts.len();
737    let mut verts = mesh.verts.clone();
738    let mut faces = mesh.faces.clone();
739    let mut remap: Vec<usize> = (0..nv).collect();
740    fn root(r: &mut Vec<usize>, i: usize) -> usize {
741        if r[i] == i {
742            i
743        } else {
744            let p = root(r, r[i]);
745            r[i] = p;
746            p
747        }
748    }
749    let mut collapsed = true;
750    while collapsed {
751        collapsed = false;
752        let mut seen = HashSet::new();
753        for &[a, b, c] in &faces {
754            for (i, j) in [(a, b), (b, c), (c, a)] {
755                let ri = root(&mut remap, i);
756                let rj = root(&mut remap, j);
757                if ri == rj {
758                    continue;
759                }
760                let key = if ri < rj { (ri, rj) } else { (rj, ri) };
761                if seen.contains(&key) {
762                    continue;
763                }
764                seen.insert(key);
765                let d = vec3_len(vec3_sub(verts[rj], verts[ri]));
766                if d < min_len {
767                    verts[ri] = vec3_scale(vec3_add(verts[ri], verts[rj]), 0.5);
768                    remap[rj] = ri;
769                    collapsed = true;
770                }
771            }
772        }
773        for i in 0..nv {
774            let r = root(&mut remap, i);
775            remap[i] = r;
776        }
777        for f in &mut faces {
778            for idx in f.iter_mut() {
779                *idx = remap[*idx];
780            }
781        }
782        faces.retain(|&[a, b, c]| a != b && b != c && a != c);
783    }
784    let active: Vec<bool> = (0..nv).map(|i| remap[i] == i).collect();
785    let new_idx: Vec<usize> = {
786        let mut m = vec![0usize; nv];
787        let mut cnt = 0;
788        for i in 0..nv {
789            if active[i] {
790                m[i] = cnt;
791                cnt += 1;
792            }
793        }
794        m
795    };
796    let new_verts: Vec<Vertex> = (0..nv).filter(|&i| active[i]).map(|i| verts[i]).collect();
797    let new_faces: Vec<Face> = faces
798        .iter()
799        .map(|&[a, b, c]| {
800            let ra = root(&mut remap, a);
801            let rb = root(&mut remap, b);
802            let rc = root(&mut remap, c);
803            [new_idx[ra], new_idx[rb], new_idx[rc]]
804        })
805        .collect();
806    ProcessMesh::new(new_verts, new_faces)
807}
808/// Tutte parameterization: map boundary to a circle, solve harmonic interior.
809///
810/// Uses a simple iterative relaxation (not a direct solver) for portability.
811pub fn tutte_parameterize(mesh: &ProcessMesh, iters: usize) -> UvParameterization {
812    let nv = mesh.verts.len();
813    let loops = detect_boundary_loops(mesh);
814    let boundary: Vec<usize> = loops.into_iter().next().unwrap_or_default();
815    let nb = boundary.len();
816    let mut uv = vec![[0.0_f64; 2]; nv];
817    for (k, &vi) in boundary.iter().enumerate() {
818        let angle = 2.0 * std::f64::consts::PI * k as f64 / nb as f64;
819        uv[vi] = [0.5 + 0.5 * angle.cos(), 0.5 + 0.5 * angle.sin()];
820    }
821    let boundary_set: HashSet<usize> = boundary.iter().cloned().collect();
822    let adj = mesh.build_adjacency();
823    for _ in 0..iters {
824        let old = uv.clone();
825        for i in 0..nv {
826            if boundary_set.contains(&i) {
827                continue;
828            }
829            let nbrs = &adj[i];
830            if nbrs.is_empty() {
831                continue;
832            }
833            let mut sum = [0.0_f64; 2];
834            for &j in nbrs {
835                sum[0] += old[j][0];
836                sum[1] += old[j][1];
837            }
838            let n = nbrs.len() as f64;
839            uv[i] = [sum[0] / n, sum[1] / n];
840        }
841    }
842    let mut out = mesh.clone();
843    out.uvs = Some(uv.clone());
844    UvParameterization { mesh: out, uvs: uv }
845}
846/// Least-Squares Conformal Map (LSCM) parameterization stub.
847///
848/// For production use, replace with a sparse linear solver.  This stub
849/// initialises the UVs via the Tutte method and returns them.
850pub fn lscm_parameterize(mesh: &ProcessMesh) -> UvParameterization {
851    tutte_parameterize(mesh, 200)
852}
853/// Generate a simple texture atlas by partitioning the mesh into connected
854/// components and packing their UV patches into a square atlas.
855pub fn generate_texture_atlas(mesh: &ProcessMesh, atlas_size: usize) -> Vec<AtlasPatch> {
856    let _ = atlas_size;
857    let param = tutte_parameterize(mesh, 100);
858    let uvs = param.uvs.clone();
859    let bounds = uvs.iter().fold(
860        [
861            f64::INFINITY,
862            f64::INFINITY,
863            f64::NEG_INFINITY,
864            f64::NEG_INFINITY,
865        ],
866        |mut b, &[u, v]| {
867            if u < b[0] {
868                b[0] = u;
869            }
870            if v < b[1] {
871                b[1] = v;
872            }
873            if u > b[2] {
874                b[2] = u;
875            }
876            if v > b[3] {
877                b[3] = v;
878            }
879            b
880        },
881    );
882    vec![AtlasPatch {
883        group_id: 0,
884        bounds,
885        uvs,
886    }]
887}
888/// Test whether a point `p` is inside `mesh` using ray casting.
889pub fn point_in_mesh(p: [f64; 3], mesh: &ProcessMesh) -> bool {
890    let dir = vec3_normalize([1.0, 1e-4, 1e-4]);
891    let mut crossings = 0usize;
892    for &[a, b, c] in &mesh.faces {
893        if let Some(_t) =
894            ray_triangle_intersect(p, dir, mesh.verts[a], mesh.verts[b], mesh.verts[c])
895        {
896            crossings += 1;
897        }
898    }
899    crossings % 2 == 1
900}
901/// Möller-Trumbore ray-triangle intersection.
902pub(super) fn ray_triangle_intersect(
903    orig: [f64; 3],
904    dir: [f64; 3],
905    v0: [f64; 3],
906    v1: [f64; 3],
907    v2: [f64; 3],
908) -> Option<f64> {
909    let e1 = vec3_sub(v1, v0);
910    let e2 = vec3_sub(v2, v0);
911    let h = vec3_cross(dir, e2);
912    let a = vec3_dot(e1, h);
913    if a.abs() < 1e-15 {
914        return None;
915    }
916    let f = 1.0 / a;
917    let s = vec3_sub(orig, v0);
918    let u = f * vec3_dot(s, h);
919    if !(0.0..=1.0).contains(&u) {
920        return None;
921    }
922    let q = vec3_cross(s, e1);
923    let v = f * vec3_dot(dir, q);
924    if v < 0.0 || u + v > 1.0 {
925        return None;
926    }
927    let t = f * vec3_dot(e2, q);
928    if t > 1e-15 { Some(t) } else { None }
929}
930/// Boolean union of two meshes (retains all faces from both).
931///
932/// This is a surface-union approximation: faces of mesh A that lie inside B
933/// are removed, and vice-versa.
934pub fn mesh_union(a: &ProcessMesh, b: &ProcessMesh) -> BooleanResult {
935    let offset_b = a.verts.len();
936    let mut verts = a.verts.clone();
937    verts.extend_from_slice(&b.verts);
938    let faces_a: Vec<Face> = a
939        .faces
940        .iter()
941        .filter(|&&[i, j, k]| {
942            let centroid = vec3_scale(
943                vec3_add(vec3_add(a.verts[i], a.verts[j]), a.verts[k]),
944                1.0 / 3.0,
945            );
946            !point_in_mesh(centroid, b)
947        })
948        .cloned()
949        .collect();
950    let faces_b: Vec<Face> = b
951        .faces
952        .iter()
953        .filter(|&&[i, j, k]| {
954            let centroid = vec3_scale(
955                vec3_add(vec3_add(b.verts[i], b.verts[j]), b.verts[k]),
956                1.0 / 3.0,
957            );
958            !point_in_mesh(centroid, a)
959        })
960        .map(|&[i, j, k]| [i + offset_b, j + offset_b, k + offset_b])
961        .collect();
962    let mut faces = faces_a;
963    faces.extend(faces_b);
964    BooleanResult {
965        mesh: ProcessMesh::new(verts, faces),
966        is_exact: false,
967    }
968}