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#[cfg(test)]
56#[inline]
57pub(super) fn vec3_lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
58    [
59        a[0] + t * (b[0] - a[0]),
60        a[1] + t * (b[1] - a[1]),
61        a[2] + t * (b[2] - a[2]),
62    ]
63}
64/// Apply `iters` passes of uniform Laplacian smoothing with step `lambda`.
65///
66/// Each vertex is moved toward the average of its neighbours.
67pub fn laplacian_smooth(mesh: &ProcessMesh, lambda: f64, iters: usize) -> ProcessMesh {
68    let mut out = mesh.clone();
69    let adj = mesh.build_adjacency();
70    for _ in 0..iters {
71        let old = out.verts.clone();
72        for (i, nbrs) in adj.iter().enumerate() {
73            if nbrs.is_empty() {
74                continue;
75            }
76            let mut avg = [0.0_f64; 3];
77            for &j in nbrs {
78                avg = vec3_add(avg, old[j]);
79            }
80            avg = vec3_scale(avg, 1.0 / nbrs.len() as f64);
81            let delta = vec3_sub(avg, old[i]);
82            out.verts[i] = vec3_add(old[i], vec3_scale(delta, lambda));
83        }
84    }
85    out
86}
87/// Apply Taubin λ/μ smoothing to reduce shrinkage.
88///
89/// Alternates a positive step (λ) with a negative step (μ < 0) so that
90/// the mesh volume is approximately preserved.
91pub fn taubin_smooth(mesh: &ProcessMesh, lambda: f64, mu: f64, iters: usize) -> ProcessMesh {
92    let mut out = mesh.clone();
93    let adj = mesh.build_adjacency();
94    let one_step = |verts: &[Vertex], step: f64, a: &[Vec<usize>]| -> Vec<Vertex> {
95        let mut next = verts.to_vec();
96        for (i, nbrs) in a.iter().enumerate() {
97            if nbrs.is_empty() {
98                continue;
99            }
100            let mut avg = [0.0_f64; 3];
101            for &j in nbrs {
102                avg = vec3_add(avg, verts[j]);
103            }
104            avg = vec3_scale(avg, 1.0 / nbrs.len() as f64);
105            let delta = vec3_sub(avg, verts[i]);
106            next[i] = vec3_add(verts[i], vec3_scale(delta, step));
107        }
108        next
109    };
110    for _ in 0..iters {
111        out.verts = one_step(&out.verts, lambda, &adj);
112        out.verts = one_step(&out.verts, mu, &adj);
113    }
114    out
115}
116/// Simple midpoint (butterfly) subdivision: each edge is split and each
117/// triangle becomes four smaller triangles.
118pub fn midpoint_subdivide(mesh: &ProcessMesh) -> ProcessMesh {
119    let mut verts = mesh.verts.clone();
120    let mut faces = Vec::new();
121    let mut edge_mid: HashMap<(usize, usize), usize> = HashMap::new();
122    let mut get_mid = |a: usize, b: usize, v: &mut Vec<Vertex>| -> usize {
123        let key = if a < b { (a, b) } else { (b, a) };
124        *edge_mid.entry(key).or_insert_with(|| {
125            let mid = vec3_scale(vec3_add(v[a], v[b]), 0.5);
126            let idx = v.len();
127            v.push(mid);
128            idx
129        })
130    };
131    for &[a, b, c] in &mesh.faces {
132        let ab = get_mid(a, b, &mut verts);
133        let bc = get_mid(b, c, &mut verts);
134        let ca = get_mid(c, a, &mut verts);
135        faces.push([a, ab, ca]);
136        faces.push([ab, b, bc]);
137        faces.push([ca, bc, c]);
138        faces.push([ab, bc, ca]);
139    }
140    ProcessMesh::new(verts, faces)
141}
142/// Loop subdivision with proper weighting for interior vertices.
143///
144/// Implements Charles Loop's 1987 scheme for smooth subdivision surfaces.
145pub fn loop_subdivide(mesh: &ProcessMesh) -> ProcessMesh {
146    let mut verts = mesh.verts.clone();
147    let mut edge_mid: HashMap<(usize, usize), usize> = HashMap::new();
148    let mut edge_opp: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
149    for &[a, b, c] in &mesh.faces {
150        let edges = [(a, b, c), (b, c, a), (c, a, b)];
151        for (i, j, k) in edges {
152            let key = if i < j { (i, j) } else { (j, i) };
153            edge_opp.entry(key).or_default().push(k);
154        }
155    }
156    let mut get_mid = |a: usize, b: usize, v: &mut Vec<Vertex>| -> usize {
157        let key = if a < b { (a, b) } else { (b, a) };
158        *edge_mid.entry(key).or_insert_with(|| {
159            let opps = edge_opp.get(&key).cloned().unwrap_or_default();
160            let pos = if opps.len() == 2 {
161                let sum_ab = vec3_scale(vec3_add(v[a], v[b]), 3.0 / 8.0);
162                let sum_op = vec3_scale(vec3_add(v[opps[0]], v[opps[1]]), 1.0 / 8.0);
163                vec3_add(sum_ab, sum_op)
164            } else {
165                vec3_scale(vec3_add(v[a], v[b]), 0.5)
166            };
167            let idx = v.len();
168            v.push(pos);
169            idx
170        })
171    };
172    let adj = mesh.build_adjacency();
173    let orig = mesh.verts.clone();
174    let n = orig.len();
175    let mut new_pos = orig.clone();
176    for i in 0..n {
177        let nbrs = &adj[i];
178        let k = nbrs.len() as f64;
179        let beta = if k <= 3.0 {
180            3.0 / 16.0
181        } else {
182            3.0 / (8.0 * k)
183        };
184        let mut sum = [0.0_f64; 3];
185        for &j in nbrs {
186            sum = vec3_add(sum, orig[j]);
187        }
188        new_pos[i] = vec3_add(vec3_scale(orig[i], 1.0 - k * beta), vec3_scale(sum, beta));
189    }
190    let mut faces = Vec::new();
191    for &[a, b, c] in &mesh.faces {
192        let ab = get_mid(a, b, &mut verts);
193        let bc = get_mid(b, c, &mut verts);
194        let ca = get_mid(c, a, &mut verts);
195        faces.push([a, ab, ca]);
196        faces.push([ab, b, bc]);
197        faces.push([ca, bc, c]);
198        faces.push([ab, bc, ca]);
199    }
200    verts[..n].copy_from_slice(&new_pos[..n]);
201    ProcessMesh::new(verts, faces)
202}
203/// Catmull-Clark subdivision adapted for triangle meshes.
204///
205/// For pure triangle meshes this is approximated by computing face centroids,
206/// edge midpoints, and updated vertex positions following the CC weights.
207pub fn catmull_clark_subdivide(mesh: &ProcessMesh) -> ProcessMesh {
208    let nv = mesh.verts.len();
209    let mut new_verts = mesh.verts.clone();
210    let mut face_pts: Vec<Vertex> = Vec::with_capacity(mesh.faces.len());
211    for &[a, b, c] in &mesh.faces {
212        let fp = vec3_scale(
213            vec3_add(vec3_add(mesh.verts[a], mesh.verts[b]), mesh.verts[c]),
214            1.0 / 3.0,
215        );
216        face_pts.push(fp);
217    }
218    let mut edge_map: HashMap<(usize, usize), usize> = HashMap::new();
219    let mut edge_pts: Vec<Vertex> = Vec::new();
220    let mut vert_face_avg = vec![[0.0_f64; 3]; nv];
221    let mut vert_face_cnt = vec![0usize; nv];
222    for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
223        let fp = face_pts[fi];
224        for &v in &[a, b, c] {
225            vert_face_avg[v] = vec3_add(vert_face_avg[v], fp);
226            vert_face_cnt[v] += 1;
227        }
228        for (i, j) in [(a, b), (b, c), (c, a)] {
229            let key = if i < j { (i, j) } else { (j, i) };
230            edge_map.entry(key).or_insert_with(|| {
231                let ep = vec3_scale(vec3_add(mesh.verts[i], mesh.verts[j]), 0.5);
232                let idx = edge_pts.len();
233                edge_pts.push(ep);
234                idx
235            });
236        }
237    }
238    let adj = mesh.build_adjacency();
239    for i in 0..nv {
240        let n = vert_face_cnt[i] as f64;
241        if n == 0.0 {
242            continue;
243        }
244        let f_avg = vec3_scale(vert_face_avg[i], 1.0 / n);
245        let nbrs = &adj[i];
246        let mut e_avg = [0.0_f64; 3];
247        for &j in nbrs {
248            e_avg = vec3_add(e_avg, mesh.verts[j]);
249        }
250        let m = nbrs.len() as f64;
251        e_avg = vec3_scale(e_avg, 1.0 / m.max(1.0));
252        new_verts[i] = vec3_scale(
253            vec3_add(
254                vec3_add(f_avg, vec3_scale(e_avg, 2.0)),
255                vec3_scale(mesh.verts[i], (n - 3.0).max(0.0)),
256            ),
257            1.0 / n,
258        );
259    }
260    let ep_offset = nv;
261    let fp_offset = ep_offset + edge_pts.len();
262    for ep in &edge_pts {
263        new_verts.push(*ep);
264    }
265    for fp in &face_pts {
266        new_verts.push(*fp);
267    }
268    let mut faces = Vec::new();
269    for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
270        let fp_idx = fp_offset + fi;
271        let ab_idx = ep_offset
272            + *edge_map
273                .get(&if a < b { (a, b) } else { (b, a) })
274                .expect("key must exist");
275        let bc_idx = ep_offset
276            + *edge_map
277                .get(&if b < c { (b, c) } else { (c, b) })
278                .expect("key must exist");
279        let ca_idx = ep_offset
280            + *edge_map
281                .get(&if c < a { (c, a) } else { (a, c) })
282                .expect("key must exist");
283        faces.push([a, ab_idx, fp_idx]);
284        faces.push([ab_idx, b, fp_idx]);
285        faces.push([b, bc_idx, fp_idx]);
286        faces.push([bc_idx, c, fp_idx]);
287        faces.push([c, ca_idx, fp_idx]);
288        faces.push([ca_idx, a, fp_idx]);
289    }
290    ProcessMesh::new(new_verts, faces)
291}
292/// Build one quadric per face (the fundamental plane quadric).
293pub(super) fn face_quadrics(mesh: &ProcessMesh) -> Vec<Quadric> {
294    mesh.faces
295        .iter()
296        .map(|&[a, b, c]| {
297            let va = mesh.verts[a];
298            let vb = mesh.verts[b];
299            let vc = mesh.verts[c];
300            let ab = vec3_sub(vb, va);
301            let ac = vec3_sub(vc, va);
302            let n = vec3_normalize(vec3_cross(ab, ac));
303            let d = -vec3_dot(n, va);
304            Quadric::from_plane(n[0], n[1], n[2], d)
305        })
306        .collect()
307}
308/// Build per-vertex quadrics by summing face quadrics for all adjacent faces.
309pub(super) fn vertex_quadrics(mesh: &ProcessMesh) -> Vec<Quadric> {
310    let nv = mesh.verts.len();
311    let fq = face_quadrics(mesh);
312    let mut vq = vec![Quadric::zero(); nv];
313    for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
314        for &v in &[a, b, c] {
315            vq[v] = vq[v].add(&fq[fi]);
316        }
317    }
318    vq
319}
320/// Decimate a mesh to at most `target_faces` triangles using quadric error
321/// metrics with greedy edge collapses.
322pub fn qem_decimate(mesh: &ProcessMesh, target_faces: usize) -> ProcessMesh {
323    if mesh.faces.len() <= target_faces {
324        return mesh.clone();
325    }
326    let nv = mesh.verts.len();
327    let mut verts = mesh.verts.clone();
328    let mut faces = mesh.faces.clone();
329    let mut vq = vertex_quadrics(mesh);
330    let mut removed_verts = vec![false; nv];
331    let mut removed_faces = vec![false; faces.len()];
332    while faces.iter().filter(|&&_f| true).count() - removed_faces.iter().filter(|&&r| r).count()
333        > target_faces
334    {
335        let mut best_cost = f64::INFINITY;
336        let mut best_edge: Option<(usize, usize)> = None;
337        let mut best_pos = [0.0_f64; 3];
338        let mut seen = HashSet::new();
339        for &[a, b, c] in &faces {
340            for (i, j) in [(a, b), (b, c), (c, a)] {
341                let key = if i < j { (i, j) } else { (j, i) };
342                if seen.contains(&key) {
343                    continue;
344                }
345                seen.insert(key);
346                if removed_verts[i] || removed_verts[j] {
347                    continue;
348                }
349                let q = vq[i].add(&vq[j]);
350                let mid = vec3_scale(vec3_add(verts[i], verts[j]), 0.5);
351                let cost = q.evaluate(mid);
352                if cost < best_cost {
353                    best_cost = cost;
354                    best_edge = Some((i, j));
355                    best_pos = mid;
356                }
357            }
358        }
359        if let Some((vi, vj)) = best_edge {
360            verts[vi] = best_pos;
361            vq[vi] = vq[vi].add(&vq[vj]);
362            removed_verts[vj] = true;
363            for (fi, f) in faces.iter_mut().enumerate() {
364                if removed_faces[fi] {
365                    continue;
366                }
367                for idx in f.iter_mut() {
368                    if *idx == vj {
369                        *idx = vi;
370                    }
371                }
372                if f[0] == f[1] || f[1] == f[2] || f[0] == f[2] {
373                    removed_faces[fi] = true;
374                }
375            }
376        } else {
377            break;
378        }
379    }
380    let remap: Vec<usize> = {
381        let mut m = vec![0usize; nv];
382        let mut cnt = 0;
383        for i in 0..nv {
384            if !removed_verts[i] {
385                m[i] = cnt;
386                cnt += 1;
387            }
388        }
389        m
390    };
391    let new_verts: Vec<Vertex> = (0..nv)
392        .filter(|&i| !removed_verts[i])
393        .map(|i| verts[i])
394        .collect();
395    let new_faces: Vec<Face> = faces
396        .iter()
397        .enumerate()
398        .filter(|&(fi, _)| !removed_faces[fi])
399        .map(|(_, &[a, b, c])| [remap[a], remap[b], remap[c]])
400        .collect();
401    ProcessMesh::new(new_verts, new_faces)
402}
403/// Compute quality metrics for a single triangle given its three vertices.
404pub fn triangle_quality(va: Vertex, vb: Vertex, vc: Vertex) -> TriangleQuality {
405    let ab = vec3_sub(vb, va);
406    let ac = vec3_sub(vc, va);
407    let bc = vec3_sub(vc, vb);
408    let la = vec3_len(bc);
409    let lb = vec3_len(ac);
410    let lc = vec3_len(ab);
411    let s = (la + lb + lc) / 2.0;
412    let area2 = vec3_len(vec3_cross(ab, ac));
413    let area = area2 / 2.0;
414    let inradius = if s > 1e-15 { area / s } else { 0.0 };
415    let circumradius = if area > 1e-15 {
416        la * lb * lc / (4.0 * area)
417    } else {
418        f64::INFINITY
419    };
420    let aspect_ratio = if inradius > 1e-15 {
421        circumradius / (2.0 * inradius)
422    } else {
423        f64::INFINITY
424    };
425    let angle_a = if la > 1e-15 && lb > 1e-15 {
426        let cos_a = (lb * lb + lc * lc - la * la) / (2.0 * lb * lc);
427        cos_a.clamp(-1.0, 1.0).acos()
428    } else {
429        0.0
430    };
431    let angle_b = if la > 1e-15 && lc > 1e-15 {
432        let cos_b = (la * la + lc * lc - lb * lb) / (2.0 * la * lc);
433        cos_b.clamp(-1.0, 1.0).acos()
434    } else {
435        0.0
436    };
437    let angle_c = std::f64::consts::PI - angle_a - angle_b;
438    let perim = 2.0 * s;
439    let ideal_area = perim * perim * (3.0_f64.sqrt()) / 36.0;
440    let area_quality = if ideal_area > 1e-15 {
441        (area / ideal_area).min(1.0)
442    } else {
443        0.0
444    };
445    TriangleQuality {
446        aspect_ratio,
447        min_angle: angle_a.min(angle_b).min(angle_c),
448        max_angle: angle_a.max(angle_b).max(angle_c),
449        area_quality,
450    }
451}
452/// Compute aggregate quality statistics for `mesh`.
453pub fn mesh_quality_stats(mesh: &ProcessMesh) -> MeshQualityStats {
454    if mesh.faces.is_empty() {
455        return MeshQualityStats {
456            mean_aspect_ratio: 0.0,
457            max_aspect_ratio: 0.0,
458            min_angle_deg: 0.0,
459            mean_area_quality: 0.0,
460        };
461    }
462    let n = mesh.faces.len() as f64;
463    let mut sum_ar = 0.0;
464    let mut max_ar = 0.0_f64;
465    let mut min_ang = f64::INFINITY;
466    let mut sum_aq = 0.0;
467    for &[a, b, c] in &mesh.faces {
468        let q = triangle_quality(mesh.verts[a], mesh.verts[b], mesh.verts[c]);
469        sum_ar += q.aspect_ratio;
470        max_ar = max_ar.max(q.aspect_ratio);
471        min_ang = min_ang.min(q.min_angle);
472        sum_aq += q.area_quality;
473    }
474    MeshQualityStats {
475        mean_aspect_ratio: sum_ar / n,
476        max_aspect_ratio: max_ar,
477        min_angle_deg: min_ang.to_degrees(),
478        mean_area_quality: sum_aq / n,
479    }
480}
481/// Detect boundary (open) edge loops in `mesh`.
482///
483/// Returns a list of loops, where each loop is an ordered list of vertex
484/// indices forming the boundary.
485pub fn detect_boundary_loops(mesh: &ProcessMesh) -> Vec<Vec<usize>> {
486    let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
487    for &[a, b, c] in &mesh.faces {
488        for (i, j) in [(a, b), (b, c), (c, a)] {
489            *edge_count.entry((i, j)).or_insert(0) += 1;
490        }
491    }
492    let mut boundary_adj: HashMap<usize, usize> = HashMap::new();
493    for &(i, j) in edge_count.keys() {
494        if edge_count.get(&(j, i)).copied().unwrap_or(0) == 0 {
495            boundary_adj.insert(i, j);
496        }
497    }
498    let mut visited = HashSet::new();
499    let mut loops = Vec::new();
500    for &start in boundary_adj.keys() {
501        if visited.contains(&start) {
502            continue;
503        }
504        let mut loop_ = Vec::new();
505        let mut cur = start;
506        loop {
507            if visited.contains(&cur) {
508                break;
509            }
510            visited.insert(cur);
511            loop_.push(cur);
512            match boundary_adj.get(&cur) {
513                Some(&next) => cur = next,
514                None => break,
515            }
516        }
517        if !loop_.is_empty() {
518            loops.push(loop_);
519        }
520    }
521    loops
522}
523/// Fill a single boundary loop with a fan triangulation from its centroid.
524///
525/// Returns the new faces to append.
526pub fn fill_hole_fan(verts: &mut Vec<Vertex>, loop_: &[usize]) -> Vec<Face> {
527    if loop_.len() < 3 {
528        return Vec::new();
529    }
530    let mut centroid = [0.0_f64; 3];
531    for &vi in loop_ {
532        centroid = vec3_add(centroid, verts[vi]);
533    }
534    centroid = vec3_scale(centroid, 1.0 / loop_.len() as f64);
535    let center_idx = verts.len();
536    verts.push(centroid);
537    let mut new_faces = Vec::new();
538    let n = loop_.len();
539    for i in 0..n {
540        new_faces.push([loop_[(i + 1) % n], loop_[i], center_idx]);
541    }
542    new_faces
543}
544/// Repair `mesh` by detecting all boundary loops and filling each with a fan.
545pub fn fill_all_holes(mesh: &ProcessMesh) -> ProcessMesh {
546    let mut out = mesh.clone();
547    loop {
548        let loops = detect_boundary_loops(&out);
549        if loops.is_empty() {
550            break;
551        }
552        for lp in &loops {
553            let new_faces = fill_hole_fan(&mut out.verts, lp);
554            out.faces.extend(new_faces);
555        }
556        if detect_boundary_loops(&out).len() >= loops.len() {
557            break;
558        }
559    }
560    out
561}
562/// Remove non-manifold vertices (vertices shared by more than two disconnected
563/// fans) by duplicating them.
564pub fn fix_non_manifold_vertices(mesh: &ProcessMesh) -> ProcessMesh {
565    let nv = mesh.verts.len();
566    let mut vert_faces: Vec<Vec<usize>> = vec![Vec::new(); nv];
567    for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
568        vert_faces[a].push(fi);
569        vert_faces[b].push(fi);
570        vert_faces[c].push(fi);
571    }
572    let mut new_verts = mesh.verts.clone();
573    let mut new_faces = mesh.faces.clone();
574    for vi in 0..nv {
575        let flist = &vert_faces[vi];
576        if flist.len() < 2 {
577            continue;
578        }
579        let face_set: HashSet<usize> = flist.iter().cloned().collect();
580        let mut visited = HashSet::new();
581        let mut queue = VecDeque::new();
582        queue.push_back(flist[0]);
583        while let Some(fi) = queue.pop_front() {
584            if visited.contains(&fi) {
585                continue;
586            }
587            visited.insert(fi);
588            let [a, b, c] = new_faces[fi];
589            for &fj in &vert_faces[a] {
590                if face_set.contains(&fj) && !visited.contains(&fj) {
591                    queue.push_back(fj);
592                }
593            }
594            for &fj in &vert_faces[b] {
595                if face_set.contains(&fj) && !visited.contains(&fj) {
596                    queue.push_back(fj);
597                }
598            }
599            for &fj in &vert_faces[c] {
600                if face_set.contains(&fj) && !visited.contains(&fj) {
601                    queue.push_back(fj);
602                }
603            }
604        }
605        let extra: Vec<usize> = flist
606            .iter()
607            .filter(|&&f| !visited.contains(&f))
608            .cloned()
609            .collect();
610        if !extra.is_empty() {
611            let new_idx = new_verts.len();
612            new_verts.push(new_verts[vi]);
613            for fi in extra {
614                for idx in new_faces[fi].iter_mut() {
615                    if *idx == vi {
616                        *idx = new_idx;
617                    }
618                }
619            }
620        }
621    }
622    ProcessMesh::new(new_verts, new_faces)
623}
624/// Detect feature edges in `mesh` whose dihedral angle exceeds
625/// `threshold_deg` degrees.
626pub fn detect_feature_edges(mesh: &ProcessMesh, threshold_deg: f64) -> Vec<FeatureEdge> {
627    let thresh = threshold_deg.to_radians();
628    let face_normals: Vec<[f64; 3]> = mesh
629        .faces
630        .iter()
631        .map(|&[a, b, c]| {
632            let ab = vec3_sub(mesh.verts[b], mesh.verts[a]);
633            let ac = vec3_sub(mesh.verts[c], mesh.verts[a]);
634            vec3_normalize(vec3_cross(ab, ac))
635        })
636        .collect();
637    let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
638    for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
639        for (i, j) in [(a, b), (b, c), (c, a)] {
640            let key = if i < j { (i, j) } else { (j, i) };
641            edge_faces.entry(key).or_default().push(fi);
642        }
643    }
644    let mut features = Vec::new();
645    for ((v0, v1), flist) in &edge_faces {
646        if flist.len() != 2 {
647            continue;
648        }
649        let n0 = face_normals[flist[0]];
650        let n1 = face_normals[flist[1]];
651        let cos_a = vec3_dot(n0, n1).clamp(-1.0, 1.0);
652        let angle = cos_a.acos();
653        if angle > thresh {
654            features.push(FeatureEdge {
655                v0: *v0,
656                v1: *v1,
657                dihedral_angle: angle,
658            });
659        }
660    }
661    features
662}
663/// Isotropic remeshing targeting a uniform target edge length.
664///
665/// Performs `iters` rounds of split-long / collapse-short / flip /
666/// Laplacian-smooth.
667pub fn isotropic_remesh(mesh: &ProcessMesh, target_len: f64, iters: usize) -> ProcessMesh {
668    let mut out = mesh.clone();
669    for _ in 0..iters {
670        out = split_long_edges(&out, target_len * 4.0 / 3.0);
671        out = collapse_short_edges(&out, target_len * 4.0 / 5.0);
672        out = laplacian_smooth(&out, 0.5, 1);
673    }
674    out
675}
676/// Split edges longer than `max_len` by inserting midpoints.
677pub fn split_long_edges(mesh: &ProcessMesh, max_len: f64) -> ProcessMesh {
678    let mut verts = mesh.verts.clone();
679    let mut faces = Vec::new();
680    let mut edge_mid: HashMap<(usize, usize), usize> = HashMap::new();
681    let mut get_mid = |a: usize, b: usize, v: &mut Vec<Vertex>| -> Option<usize> {
682        let d = vec3_len(vec3_sub(v[b], v[a]));
683        if d > max_len {
684            let key = if a < b { (a, b) } else { (b, a) };
685            let idx = *edge_mid.entry(key).or_insert_with(|| {
686                let mid = vec3_scale(vec3_add(v[a], v[b]), 0.5);
687                let i = v.len();
688                v.push(mid);
689                i
690            });
691            Some(idx)
692        } else {
693            None
694        }
695    };
696    for &[a, b, c] in &mesh.faces {
697        let ab = get_mid(a, b, &mut verts);
698        let bc = get_mid(b, c, &mut verts);
699        let ca = get_mid(c, a, &mut verts);
700        match (ab, bc, ca) {
701            (None, None, None) => faces.push([a, b, c]),
702            (Some(m), None, None) => {
703                faces.push([a, m, c]);
704                faces.push([m, b, c]);
705            }
706            (None, Some(m), None) => {
707                faces.push([a, b, m]);
708                faces.push([a, m, c]);
709            }
710            (None, None, Some(m)) => {
711                faces.push([a, b, m]);
712                faces.push([m, b, c]);
713            }
714            (Some(ab), Some(bc), None) => {
715                faces.push([a, ab, c]);
716                faces.push([ab, b, bc]);
717                faces.push([ab, bc, c]);
718            }
719            (Some(ab), None, Some(ca)) => {
720                faces.push([a, ab, ca]);
721                faces.push([ab, b, c]);
722                faces.push([ab, c, ca]);
723            }
724            (None, Some(bc), Some(ca)) => {
725                faces.push([a, b, bc]);
726                faces.push([a, bc, ca]);
727                faces.push([ca, bc, c]);
728            }
729            (Some(ab), Some(bc), Some(ca)) => {
730                faces.push([a, ab, ca]);
731                faces.push([ab, b, bc]);
732                faces.push([ca, bc, c]);
733                faces.push([ab, bc, ca]);
734            }
735        }
736    }
737    ProcessMesh::new(verts, faces)
738}
739/// Collapse edges shorter than `min_len` by merging endpoints.
740pub fn collapse_short_edges(mesh: &ProcessMesh, min_len: f64) -> ProcessMesh {
741    let nv = mesh.verts.len();
742    let mut verts = mesh.verts.clone();
743    let mut faces = mesh.faces.clone();
744    let mut remap: Vec<usize> = (0..nv).collect();
745    fn root(r: &mut Vec<usize>, i: usize) -> usize {
746        if r[i] == i {
747            i
748        } else {
749            let p = root(r, r[i]);
750            r[i] = p;
751            p
752        }
753    }
754    let mut collapsed = true;
755    while collapsed {
756        collapsed = false;
757        let mut seen = HashSet::new();
758        for &[a, b, c] in &faces {
759            for (i, j) in [(a, b), (b, c), (c, a)] {
760                let ri = root(&mut remap, i);
761                let rj = root(&mut remap, j);
762                if ri == rj {
763                    continue;
764                }
765                let key = if ri < rj { (ri, rj) } else { (rj, ri) };
766                if seen.contains(&key) {
767                    continue;
768                }
769                seen.insert(key);
770                let d = vec3_len(vec3_sub(verts[rj], verts[ri]));
771                if d < min_len {
772                    verts[ri] = vec3_scale(vec3_add(verts[ri], verts[rj]), 0.5);
773                    remap[rj] = ri;
774                    collapsed = true;
775                }
776            }
777        }
778        for i in 0..nv {
779            let r = root(&mut remap, i);
780            remap[i] = r;
781        }
782        for f in &mut faces {
783            for idx in f.iter_mut() {
784                *idx = remap[*idx];
785            }
786        }
787        faces.retain(|&[a, b, c]| a != b && b != c && a != c);
788    }
789    let active: Vec<bool> = (0..nv).map(|i| remap[i] == i).collect();
790    let new_idx: Vec<usize> = {
791        let mut m = vec![0usize; nv];
792        let mut cnt = 0;
793        for i in 0..nv {
794            if active[i] {
795                m[i] = cnt;
796                cnt += 1;
797            }
798        }
799        m
800    };
801    let new_verts: Vec<Vertex> = (0..nv).filter(|&i| active[i]).map(|i| verts[i]).collect();
802    let new_faces: Vec<Face> = faces
803        .iter()
804        .map(|&[a, b, c]| {
805            let ra = root(&mut remap, a);
806            let rb = root(&mut remap, b);
807            let rc = root(&mut remap, c);
808            [new_idx[ra], new_idx[rb], new_idx[rc]]
809        })
810        .collect();
811    ProcessMesh::new(new_verts, new_faces)
812}
813/// Tutte parameterization: map boundary to a circle, solve harmonic interior.
814///
815/// Uses a simple iterative relaxation (not a direct solver) for portability.
816pub fn tutte_parameterize(mesh: &ProcessMesh, iters: usize) -> UvParameterization {
817    let nv = mesh.verts.len();
818    let loops = detect_boundary_loops(mesh);
819    let boundary: Vec<usize> = loops.into_iter().next().unwrap_or_default();
820    let nb = boundary.len();
821    let mut uv = vec![[0.0_f64; 2]; nv];
822    for (k, &vi) in boundary.iter().enumerate() {
823        let angle = 2.0 * std::f64::consts::PI * k as f64 / nb as f64;
824        uv[vi] = [0.5 + 0.5 * angle.cos(), 0.5 + 0.5 * angle.sin()];
825    }
826    let boundary_set: HashSet<usize> = boundary.iter().cloned().collect();
827    let adj = mesh.build_adjacency();
828    for _ in 0..iters {
829        let old = uv.clone();
830        for i in 0..nv {
831            if boundary_set.contains(&i) {
832                continue;
833            }
834            let nbrs = &adj[i];
835            if nbrs.is_empty() {
836                continue;
837            }
838            let mut sum = [0.0_f64; 2];
839            for &j in nbrs {
840                sum[0] += old[j][0];
841                sum[1] += old[j][1];
842            }
843            let n = nbrs.len() as f64;
844            uv[i] = [sum[0] / n, sum[1] / n];
845        }
846    }
847    let mut out = mesh.clone();
848    out.uvs = Some(uv.clone());
849    UvParameterization { mesh: out, uvs: uv }
850}
851/// Least-Squares Conformal Maps (LSCM) UV parameterization.
852///
853/// Minimises the conformal (angle-preserving) energy over all triangles by solving
854/// the Lévy 2002 sparse linear system.  Two vertices are pinned to break the
855/// rotation/translation ambiguity:
856/// - vertex 0 → UV (0, 0)
857/// - vertex 1 → UV (1, 0)
858///
859/// The free-vertex system `A_free^T A_free x = A_free^T b` is solved with the
860/// [`oxiphysics_fem::parallel_solver::ParallelPcgSolver`].
861///
862/// # Returns
863/// A [`UvParameterization`] with UV coordinates clamped to \[0, 1\] per axis.
864pub fn lscm_parameterize(mesh: &ProcessMesh) -> UvParameterization {
865    use oxiphysics_fem::parallel_solver::{CsrMatrix, ParallelPcgSolver};
866    use std::collections::BTreeMap;
867
868    let nv = mesh.verts.len();
869    let nf = mesh.faces.len();
870
871    // Degenerate cases: fall back to Tutte
872    if nv < 3 || nf == 0 {
873        return tutte_parameterize(mesh, 200);
874    }
875
876    // Each triangle contributes 2 rows (Re and Im of the conformal constraint)
877    // acting on 6 unknowns [u_i, v_i, u_j, v_j, u_k, v_k].
878    // Total rows = 2 * nf, total columns = 2 * nv (u and v interleaved per vertex).
879    //
880    // The DOF ordering: column index = 2*vertex + 0 for u, 2*vertex + 1 for v.
881    //
882    // Two pinned DOFs (columns 0,1 for vertex 0 and columns 2,3 for vertex 1)
883    // are removed from the system and their contributions moved to the RHS.
884    // The pinned values are: u0=0,v0=0, u1=1,v1=0.
885
886    // Pinned vertex indices and values
887    let pin_col = [0usize, 1, 2, 3]; // u0, v0, u1, v1
888    let pin_val = [0.0_f64, 0.0, 1.0, 0.0];
889
890    // Map from original column (0..2*nv) to free column (0..2*(nv-2))
891    let mut col_map = vec![usize::MAX; 2 * nv];
892    let mut free_col = 0usize;
893    for (c, slot) in col_map.iter_mut().enumerate() {
894        if pin_col.contains(&c) {
895            continue;
896        }
897        *slot = free_col;
898        free_col += 1;
899    }
900    let n_free = free_col; // = 2*(nv-2)
901
902    // Accumulate A matrix (as triplets) and rhs b
903    // A is (2*nf) × n_free, but we only need A^T A and A^T b for PCG.
904    // We'll build it row-by-row and form the normal equations.
905
906    // Triplets for A_free: (row, free_col, value)
907    let mut a_triplets: Vec<(usize, usize, f64)> = Vec::with_capacity(12 * nf);
908    // rhs[row] accumulates b = -A_pinned * x_pinned
909    let mut b_full = vec![0.0_f64; 2 * nf];
910
911    for (fi, &[vi, vj, vk]) in mesh.faces.iter().enumerate() {
912        let pi = mesh.verts[vi];
913        let pj = mesh.verts[vj];
914        let pk = mesh.verts[vk];
915
916        // Local 2-D frame for the triangle
917        let d1 = vec3_sub(pj, pi); // p_j - p_i
918        let d2 = vec3_sub(pk, pi); // p_k - p_i
919
920        let x1 = vec3_len(d1); // |d1|
921        if x1 < 1e-14 {
922            continue; // degenerate triangle
923        }
924        let e1 = vec3_scale(d1, 1.0 / x1); // unit edge direction
925        let n_hat = vec3_normalize(vec3_cross(d1, d2));
926        let e2 = vec3_cross(n_hat, e1);
927
928        let x2 = vec3_dot(d2, e1);
929        let y2 = vec3_dot(d2, e2);
930        if y2.abs() < 1e-14 {
931            continue; // degenerate triangle
932        }
933
934        // Per Lévy 2002, the two LSCM constraint rows for this triangle
935        // acting on [u_i, v_i, u_j, v_j, u_k, v_k] are:
936        //   Row Re: [x2 - x1, -y2, -x2, y2,  x1,  0 ] / y2
937        //   Row Im: [y2, x2 - x1, -y2, -x2,   0, x1 ] / y2
938        // (The factor 1/y2 normalises by the triangle "height" in local coords.)
939
940        let inv_y2 = 1.0 / y2;
941        let row_re = 2 * fi;
942        let row_im = 2 * fi + 1;
943
944        // Local coefficient vectors for each DOF index in the 6-vector
945        // local_dofs[0] = global col for u_i, [1] = v_i, [2] = u_j, etc.
946        let local_dofs = [2 * vi, 2 * vi + 1, 2 * vj, 2 * vj + 1, 2 * vk, 2 * vk + 1];
947        let re_coeffs = [
948            (x2 - x1) * inv_y2,
949            -1.0,
950            -x2 * inv_y2,
951            1.0,
952            x1 * inv_y2,
953            0.0,
954        ];
955        let im_coeffs = [
956            1.0,
957            (x2 - x1) * inv_y2,
958            -1.0,
959            -x2 * inv_y2,
960            0.0,
961            x1 * inv_y2,
962        ];
963
964        for k in 0..6 {
965            let gc = local_dofs[k];
966            if let Some(pin_pos) = pin_col.iter().position(|&p| p == gc) {
967                // Pinned DOF: move to RHS
968                b_full[row_re] -= re_coeffs[k] * pin_val[pin_pos];
969                b_full[row_im] -= im_coeffs[k] * pin_val[pin_pos];
970            } else {
971                let fc = col_map[gc];
972                a_triplets.push((row_re, fc, re_coeffs[k]));
973                a_triplets.push((row_im, fc, im_coeffs[k]));
974            }
975        }
976    }
977
978    if n_free == 0 {
979        return tutte_parameterize(mesh, 200);
980    }
981
982    // Build the normal equations A^T A x = A^T b
983    // A^T A is n_free × n_free symmetric positive semi-definite.
984    // Accumulate into a BTreeMap for sparse assembly.
985    let mut ata_map: BTreeMap<(usize, usize), f64> = BTreeMap::new();
986    let mut atb = vec![0.0_f64; n_free];
987
988    for &(row, fc, val) in &a_triplets {
989        atb[fc] += val * b_full[row];
990    }
991
992    // Group triplets by row for efficient A^T A accumulation
993    // For each pair of nonzeros in the same row: (fc_a, val_a) and (fc_b, val_b)
994    // contributes val_a * val_b to ATA[fc_a, fc_b].
995    let mut row_entries: Vec<Vec<(usize, f64)>> = vec![Vec::new(); 2 * nf];
996    for &(row, fc, val) in &a_triplets {
997        row_entries[row].push((fc, val));
998    }
999    for entries in &row_entries {
1000        for &(fc_a, val_a) in entries {
1001            for &(fc_b, val_b) in entries {
1002                *ata_map.entry((fc_a, fc_b)).or_insert(0.0) += val_a * val_b;
1003            }
1004        }
1005    }
1006
1007    // Convert BTreeMap to CSR
1008    let mut row_offsets = vec![0usize; n_free + 1];
1009    for &(r, _) in ata_map.keys() {
1010        row_offsets[r + 1] += 1;
1011    }
1012    for i in 0..n_free {
1013        row_offsets[i + 1] += row_offsets[i];
1014    }
1015    let nnz = ata_map.len();
1016    let mut col_indices_csr = vec![0usize; nnz];
1017    let mut values_csr = vec![0.0_f64; nnz];
1018    let mut row_fill = vec![0usize; n_free];
1019    for (&(r, c), &v) in &ata_map {
1020        let pos = row_offsets[r] + row_fill[r];
1021        col_indices_csr[pos] = c;
1022        values_csr[pos] = v;
1023        row_fill[r] += 1;
1024    }
1025
1026    let ata = CsrMatrix {
1027        nrows: n_free,
1028        ncols: n_free,
1029        row_offsets,
1030        col_indices: col_indices_csr,
1031        values: values_csr,
1032    };
1033
1034    // Solve A^T A x = A^T b  using parallel PCG
1035    let solver = ParallelPcgSolver::new(2000, 1e-8);
1036    let mut x_free = vec![0.0_f64; n_free];
1037    let _stats = solver.solve(&ata, &atb, &mut x_free);
1038
1039    // Reconstruct full UV array
1040    let mut uv = vec![[0.0_f64; 2]; nv];
1041    // Pinned vertices
1042    uv[0] = [0.0, 0.0];
1043    uv[1] = [1.0, 0.0];
1044    // Free vertices
1045    for (v, uv_v) in uv.iter_mut().enumerate() {
1046        let cu = 2 * v;
1047        let cv = 2 * v + 1;
1048        let u_val = if pin_col.contains(&cu) {
1049            let pos = pin_col.iter().position(|&p| p == cu).unwrap_or(0);
1050            pin_val[pos]
1051        } else {
1052            x_free[col_map[cu]]
1053        };
1054        let v_val = if pin_col.contains(&cv) {
1055            let pos = pin_col.iter().position(|&p| p == cv).unwrap_or(1);
1056            pin_val[pos]
1057        } else {
1058            x_free[col_map[cv]]
1059        };
1060        *uv_v = [u_val.clamp(0.0, 1.0), v_val.clamp(0.0, 1.0)];
1061    }
1062
1063    let mut out = mesh.clone();
1064    out.uvs = Some(uv.clone());
1065    UvParameterization { mesh: out, uvs: uv }
1066}
1067/// Generate a simple texture atlas by partitioning the mesh into connected
1068/// components and packing their UV patches into a square atlas.
1069pub fn generate_texture_atlas(mesh: &ProcessMesh, atlas_size: usize) -> Vec<AtlasPatch> {
1070    let _ = atlas_size;
1071    let param = tutte_parameterize(mesh, 100);
1072    let uvs = param.uvs.clone();
1073    let bounds = uvs.iter().fold(
1074        [
1075            f64::INFINITY,
1076            f64::INFINITY,
1077            f64::NEG_INFINITY,
1078            f64::NEG_INFINITY,
1079        ],
1080        |mut b, &[u, v]| {
1081            if u < b[0] {
1082                b[0] = u;
1083            }
1084            if v < b[1] {
1085                b[1] = v;
1086            }
1087            if u > b[2] {
1088                b[2] = u;
1089            }
1090            if v > b[3] {
1091                b[3] = v;
1092            }
1093            b
1094        },
1095    );
1096    vec![AtlasPatch {
1097        group_id: 0,
1098        bounds,
1099        uvs,
1100    }]
1101}
1102/// Test whether a point `p` is inside `mesh` using ray casting.
1103pub fn point_in_mesh(p: [f64; 3], mesh: &ProcessMesh) -> bool {
1104    let dir = vec3_normalize([1.0, 1e-4, 1e-4]);
1105    let mut crossings = 0usize;
1106    for &[a, b, c] in &mesh.faces {
1107        if let Some(_t) =
1108            ray_triangle_intersect(p, dir, mesh.verts[a], mesh.verts[b], mesh.verts[c])
1109        {
1110            crossings += 1;
1111        }
1112    }
1113    crossings % 2 == 1
1114}
1115/// Möller-Trumbore ray-triangle intersection.
1116pub(super) fn ray_triangle_intersect(
1117    orig: [f64; 3],
1118    dir: [f64; 3],
1119    v0: [f64; 3],
1120    v1: [f64; 3],
1121    v2: [f64; 3],
1122) -> Option<f64> {
1123    let e1 = vec3_sub(v1, v0);
1124    let e2 = vec3_sub(v2, v0);
1125    let h = vec3_cross(dir, e2);
1126    let a = vec3_dot(e1, h);
1127    if a.abs() < 1e-15 {
1128        return None;
1129    }
1130    let f = 1.0 / a;
1131    let s = vec3_sub(orig, v0);
1132    let u = f * vec3_dot(s, h);
1133    if !(0.0..=1.0).contains(&u) {
1134        return None;
1135    }
1136    let q = vec3_cross(s, e1);
1137    let v = f * vec3_dot(dir, q);
1138    if v < 0.0 || u + v > 1.0 {
1139        return None;
1140    }
1141    let t = f * vec3_dot(e2, q);
1142    if t > 1e-15 { Some(t) } else { None }
1143}
1144/// Boolean union of two meshes (retains all faces from both).
1145///
1146/// This is a surface-union approximation: faces of mesh A that lie inside B
1147/// are removed, and vice-versa.
1148pub fn mesh_union(a: &ProcessMesh, b: &ProcessMesh) -> BooleanResult {
1149    let offset_b = a.verts.len();
1150    let mut verts = a.verts.clone();
1151    verts.extend_from_slice(&b.verts);
1152    let faces_a: Vec<Face> = a
1153        .faces
1154        .iter()
1155        .filter(|&&[i, j, k]| {
1156            let centroid = vec3_scale(
1157                vec3_add(vec3_add(a.verts[i], a.verts[j]), a.verts[k]),
1158                1.0 / 3.0,
1159            );
1160            !point_in_mesh(centroid, b)
1161        })
1162        .cloned()
1163        .collect();
1164    let faces_b: Vec<Face> = b
1165        .faces
1166        .iter()
1167        .filter(|&&[i, j, k]| {
1168            let centroid = vec3_scale(
1169                vec3_add(vec3_add(b.verts[i], b.verts[j]), b.verts[k]),
1170                1.0 / 3.0,
1171            );
1172            !point_in_mesh(centroid, a)
1173        })
1174        .map(|&[i, j, k]| [i + offset_b, j + offset_b, k + offset_b])
1175        .collect();
1176    let mut faces = faces_a;
1177    faces.extend(faces_b);
1178    let mut result = BooleanResult {
1179        mesh: ProcessMesh::new(verts, faces),
1180        is_exact: false,
1181    };
1182    result.is_exact = result.is_topologically_exact();
1183    result
1184}