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 Maps (LSCM) UV parameterization.
847///
848/// Minimises the conformal (angle-preserving) energy over all triangles by solving
849/// the Lévy 2002 sparse linear system.  Two vertices are pinned to break the
850/// rotation/translation ambiguity:
851/// - vertex 0 → UV (0, 0)
852/// - vertex 1 → UV (1, 0)
853///
854/// The free-vertex system `A_free^T A_free x = A_free^T b` is solved with the
855/// [`oxiphysics_fem::parallel_solver::ParallelPcgSolver`].
856///
857/// # Returns
858/// A [`UvParameterization`] with UV coordinates clamped to \[0, 1\] per axis.
859pub fn lscm_parameterize(mesh: &ProcessMesh) -> UvParameterization {
860    use oxiphysics_fem::parallel_solver::{CsrMatrix, ParallelPcgSolver};
861    use std::collections::BTreeMap;
862
863    let nv = mesh.verts.len();
864    let nf = mesh.faces.len();
865
866    // Degenerate cases: fall back to Tutte
867    if nv < 3 || nf == 0 {
868        return tutte_parameterize(mesh, 200);
869    }
870
871    // Each triangle contributes 2 rows (Re and Im of the conformal constraint)
872    // acting on 6 unknowns [u_i, v_i, u_j, v_j, u_k, v_k].
873    // Total rows = 2 * nf, total columns = 2 * nv (u and v interleaved per vertex).
874    //
875    // The DOF ordering: column index = 2*vertex + 0 for u, 2*vertex + 1 for v.
876    //
877    // Two pinned DOFs (columns 0,1 for vertex 0 and columns 2,3 for vertex 1)
878    // are removed from the system and their contributions moved to the RHS.
879    // The pinned values are: u0=0,v0=0, u1=1,v1=0.
880
881    // Pinned vertex indices and values
882    let pin_col = [0usize, 1, 2, 3]; // u0, v0, u1, v1
883    let pin_val = [0.0_f64, 0.0, 1.0, 0.0];
884
885    // Map from original column (0..2*nv) to free column (0..2*(nv-2))
886    let mut col_map = vec![usize::MAX; 2 * nv];
887    let mut free_col = 0usize;
888    for (c, slot) in col_map.iter_mut().enumerate() {
889        if pin_col.contains(&c) {
890            continue;
891        }
892        *slot = free_col;
893        free_col += 1;
894    }
895    let n_free = free_col; // = 2*(nv-2)
896
897    // Accumulate A matrix (as triplets) and rhs b
898    // A is (2*nf) × n_free, but we only need A^T A and A^T b for PCG.
899    // We'll build it row-by-row and form the normal equations.
900
901    // Triplets for A_free: (row, free_col, value)
902    let mut a_triplets: Vec<(usize, usize, f64)> = Vec::with_capacity(12 * nf);
903    // rhs[row] accumulates b = -A_pinned * x_pinned
904    let mut b_full = vec![0.0_f64; 2 * nf];
905
906    for (fi, &[vi, vj, vk]) in mesh.faces.iter().enumerate() {
907        let pi = mesh.verts[vi];
908        let pj = mesh.verts[vj];
909        let pk = mesh.verts[vk];
910
911        // Local 2-D frame for the triangle
912        let d1 = vec3_sub(pj, pi); // p_j - p_i
913        let d2 = vec3_sub(pk, pi); // p_k - p_i
914
915        let x1 = vec3_len(d1); // |d1|
916        if x1 < 1e-14 {
917            continue; // degenerate triangle
918        }
919        let e1 = vec3_scale(d1, 1.0 / x1); // unit edge direction
920        let n_hat = vec3_normalize(vec3_cross(d1, d2));
921        let e2 = vec3_cross(n_hat, e1);
922
923        let x2 = vec3_dot(d2, e1);
924        let y2 = vec3_dot(d2, e2);
925        if y2.abs() < 1e-14 {
926            continue; // degenerate triangle
927        }
928
929        // Per Lévy 2002, the two LSCM constraint rows for this triangle
930        // acting on [u_i, v_i, u_j, v_j, u_k, v_k] are:
931        //   Row Re: [x2 - x1, -y2, -x2, y2,  x1,  0 ] / y2
932        //   Row Im: [y2, x2 - x1, -y2, -x2,   0, x1 ] / y2
933        // (The factor 1/y2 normalises by the triangle "height" in local coords.)
934
935        let inv_y2 = 1.0 / y2;
936        let row_re = 2 * fi;
937        let row_im = 2 * fi + 1;
938
939        // Local coefficient vectors for each DOF index in the 6-vector
940        // local_dofs[0] = global col for u_i, [1] = v_i, [2] = u_j, etc.
941        let local_dofs = [2 * vi, 2 * vi + 1, 2 * vj, 2 * vj + 1, 2 * vk, 2 * vk + 1];
942        let re_coeffs = [
943            (x2 - x1) * inv_y2,
944            -1.0,
945            -x2 * inv_y2,
946            1.0,
947            x1 * inv_y2,
948            0.0,
949        ];
950        let im_coeffs = [
951            1.0,
952            (x2 - x1) * inv_y2,
953            -1.0,
954            -x2 * inv_y2,
955            0.0,
956            x1 * inv_y2,
957        ];
958
959        for k in 0..6 {
960            let gc = local_dofs[k];
961            if let Some(pin_pos) = pin_col.iter().position(|&p| p == gc) {
962                // Pinned DOF: move to RHS
963                b_full[row_re] -= re_coeffs[k] * pin_val[pin_pos];
964                b_full[row_im] -= im_coeffs[k] * pin_val[pin_pos];
965            } else {
966                let fc = col_map[gc];
967                a_triplets.push((row_re, fc, re_coeffs[k]));
968                a_triplets.push((row_im, fc, im_coeffs[k]));
969            }
970        }
971    }
972
973    if n_free == 0 {
974        return tutte_parameterize(mesh, 200);
975    }
976
977    // Build the normal equations A^T A x = A^T b
978    // A^T A is n_free × n_free symmetric positive semi-definite.
979    // Accumulate into a BTreeMap for sparse assembly.
980    let mut ata_map: BTreeMap<(usize, usize), f64> = BTreeMap::new();
981    let mut atb = vec![0.0_f64; n_free];
982
983    for &(row, fc, val) in &a_triplets {
984        atb[fc] += val * b_full[row];
985    }
986
987    // Group triplets by row for efficient A^T A accumulation
988    // For each pair of nonzeros in the same row: (fc_a, val_a) and (fc_b, val_b)
989    // contributes val_a * val_b to ATA[fc_a, fc_b].
990    let mut row_entries: Vec<Vec<(usize, f64)>> = vec![Vec::new(); 2 * nf];
991    for &(row, fc, val) in &a_triplets {
992        row_entries[row].push((fc, val));
993    }
994    for entries in &row_entries {
995        for &(fc_a, val_a) in entries {
996            for &(fc_b, val_b) in entries {
997                *ata_map.entry((fc_a, fc_b)).or_insert(0.0) += val_a * val_b;
998            }
999        }
1000    }
1001
1002    // Convert BTreeMap to CSR
1003    let mut row_offsets = vec![0usize; n_free + 1];
1004    for &(r, _) in ata_map.keys() {
1005        row_offsets[r + 1] += 1;
1006    }
1007    for i in 0..n_free {
1008        row_offsets[i + 1] += row_offsets[i];
1009    }
1010    let nnz = ata_map.len();
1011    let mut col_indices_csr = vec![0usize; nnz];
1012    let mut values_csr = vec![0.0_f64; nnz];
1013    let mut row_fill = vec![0usize; n_free];
1014    for (&(r, c), &v) in &ata_map {
1015        let pos = row_offsets[r] + row_fill[r];
1016        col_indices_csr[pos] = c;
1017        values_csr[pos] = v;
1018        row_fill[r] += 1;
1019    }
1020
1021    let ata = CsrMatrix {
1022        nrows: n_free,
1023        ncols: n_free,
1024        row_offsets,
1025        col_indices: col_indices_csr,
1026        values: values_csr,
1027    };
1028
1029    // Solve A^T A x = A^T b  using parallel PCG
1030    let solver = ParallelPcgSolver::new(2000, 1e-8);
1031    let mut x_free = vec![0.0_f64; n_free];
1032    let _stats = solver.solve(&ata, &atb, &mut x_free);
1033
1034    // Reconstruct full UV array
1035    let mut uv = vec![[0.0_f64; 2]; nv];
1036    // Pinned vertices
1037    uv[0] = [0.0, 0.0];
1038    uv[1] = [1.0, 0.0];
1039    // Free vertices
1040    for (v, uv_v) in uv.iter_mut().enumerate() {
1041        let cu = 2 * v;
1042        let cv = 2 * v + 1;
1043        let u_val = if pin_col.contains(&cu) {
1044            let pos = pin_col.iter().position(|&p| p == cu).unwrap_or(0);
1045            pin_val[pos]
1046        } else {
1047            x_free[col_map[cu]]
1048        };
1049        let v_val = if pin_col.contains(&cv) {
1050            let pos = pin_col.iter().position(|&p| p == cv).unwrap_or(1);
1051            pin_val[pos]
1052        } else {
1053            x_free[col_map[cv]]
1054        };
1055        *uv_v = [u_val.clamp(0.0, 1.0), v_val.clamp(0.0, 1.0)];
1056    }
1057
1058    let mut out = mesh.clone();
1059    out.uvs = Some(uv.clone());
1060    UvParameterization { mesh: out, uvs: uv }
1061}
1062/// Generate a simple texture atlas by partitioning the mesh into connected
1063/// components and packing their UV patches into a square atlas.
1064pub fn generate_texture_atlas(mesh: &ProcessMesh, atlas_size: usize) -> Vec<AtlasPatch> {
1065    let _ = atlas_size;
1066    let param = tutte_parameterize(mesh, 100);
1067    let uvs = param.uvs.clone();
1068    let bounds = uvs.iter().fold(
1069        [
1070            f64::INFINITY,
1071            f64::INFINITY,
1072            f64::NEG_INFINITY,
1073            f64::NEG_INFINITY,
1074        ],
1075        |mut b, &[u, v]| {
1076            if u < b[0] {
1077                b[0] = u;
1078            }
1079            if v < b[1] {
1080                b[1] = v;
1081            }
1082            if u > b[2] {
1083                b[2] = u;
1084            }
1085            if v > b[3] {
1086                b[3] = v;
1087            }
1088            b
1089        },
1090    );
1091    vec![AtlasPatch {
1092        group_id: 0,
1093        bounds,
1094        uvs,
1095    }]
1096}
1097/// Test whether a point `p` is inside `mesh` using ray casting.
1098pub fn point_in_mesh(p: [f64; 3], mesh: &ProcessMesh) -> bool {
1099    let dir = vec3_normalize([1.0, 1e-4, 1e-4]);
1100    let mut crossings = 0usize;
1101    for &[a, b, c] in &mesh.faces {
1102        if let Some(_t) =
1103            ray_triangle_intersect(p, dir, mesh.verts[a], mesh.verts[b], mesh.verts[c])
1104        {
1105            crossings += 1;
1106        }
1107    }
1108    crossings % 2 == 1
1109}
1110/// Möller-Trumbore ray-triangle intersection.
1111pub(super) fn ray_triangle_intersect(
1112    orig: [f64; 3],
1113    dir: [f64; 3],
1114    v0: [f64; 3],
1115    v1: [f64; 3],
1116    v2: [f64; 3],
1117) -> Option<f64> {
1118    let e1 = vec3_sub(v1, v0);
1119    let e2 = vec3_sub(v2, v0);
1120    let h = vec3_cross(dir, e2);
1121    let a = vec3_dot(e1, h);
1122    if a.abs() < 1e-15 {
1123        return None;
1124    }
1125    let f = 1.0 / a;
1126    let s = vec3_sub(orig, v0);
1127    let u = f * vec3_dot(s, h);
1128    if !(0.0..=1.0).contains(&u) {
1129        return None;
1130    }
1131    let q = vec3_cross(s, e1);
1132    let v = f * vec3_dot(dir, q);
1133    if v < 0.0 || u + v > 1.0 {
1134        return None;
1135    }
1136    let t = f * vec3_dot(e2, q);
1137    if t > 1e-15 { Some(t) } else { None }
1138}
1139/// Boolean union of two meshes (retains all faces from both).
1140///
1141/// This is a surface-union approximation: faces of mesh A that lie inside B
1142/// are removed, and vice-versa.
1143pub fn mesh_union(a: &ProcessMesh, b: &ProcessMesh) -> BooleanResult {
1144    let offset_b = a.verts.len();
1145    let mut verts = a.verts.clone();
1146    verts.extend_from_slice(&b.verts);
1147    let faces_a: Vec<Face> = a
1148        .faces
1149        .iter()
1150        .filter(|&&[i, j, k]| {
1151            let centroid = vec3_scale(
1152                vec3_add(vec3_add(a.verts[i], a.verts[j]), a.verts[k]),
1153                1.0 / 3.0,
1154            );
1155            !point_in_mesh(centroid, b)
1156        })
1157        .cloned()
1158        .collect();
1159    let faces_b: Vec<Face> = b
1160        .faces
1161        .iter()
1162        .filter(|&&[i, j, k]| {
1163            let centroid = vec3_scale(
1164                vec3_add(vec3_add(b.verts[i], b.verts[j]), b.verts[k]),
1165                1.0 / 3.0,
1166            );
1167            !point_in_mesh(centroid, a)
1168        })
1169        .map(|&[i, j, k]| [i + offset_b, j + offset_b, k + offset_b])
1170        .collect();
1171    let mut faces = faces_a;
1172    faces.extend(faces_b);
1173    let mut result = BooleanResult {
1174        mesh: ProcessMesh::new(verts, faces),
1175        is_exact: false,
1176    };
1177    result.is_exact = result.is_topologically_exact();
1178    result
1179}