Skip to main content

oxiphysics_geometry/
remesh.rs

1#![allow(clippy::needless_range_loop, clippy::ptr_arg)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Mesh remeshing algorithms.
6//!
7//! Provides isotropic remeshing (split/collapse/flip/smooth/project loop),
8//! Loop subdivision for triangle meshes, and Catmull-Clark subdivision for
9//! quad meshes.
10
11#![allow(dead_code, missing_docs)]
12
13use std::collections::HashMap;
14
15use crate::triangle_mesh::TriangleMesh;
16use oxiphysics_core::math::Vec3;
17
18// ---------------------------------------------------------------------------
19// Vector helpers
20// ---------------------------------------------------------------------------
21
22#[inline]
23fn vec3_to_arr(v: Vec3) -> [f64; 3] {
24    [v.x, v.y, v.z]
25}
26
27#[inline]
28fn arr_to_vec3(a: [f64; 3]) -> Vec3 {
29    Vec3::new(a[0], a[1], a[2])
30}
31
32#[inline]
33fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
34    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
35}
36
37#[inline]
38fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
39    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
40}
41
42#[inline]
43fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
44    [a[0] * s, a[1] * s, a[2] * s]
45}
46
47#[inline]
48fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
49    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
50}
51
52#[inline]
53fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
54    [
55        a[1] * b[2] - a[2] * b[1],
56        a[2] * b[0] - a[0] * b[2],
57        a[0] * b[1] - a[1] * b[0],
58    ]
59}
60
61#[inline]
62fn len3(a: [f64; 3]) -> f64 {
63    dot3(a, a).sqrt()
64}
65
66#[inline]
67fn normalize3(a: [f64; 3]) -> [f64; 3] {
68    let l = len3(a);
69    if l < f64::EPSILON {
70        [0.0, 0.0, 0.0]
71    } else {
72        [a[0] / l, a[1] / l, a[2] / l]
73    }
74}
75
76#[inline]
77fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
78    len3(sub3(a, b))
79}
80
81#[inline]
82fn midpoint(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
83    scale3(add3(a, b), 0.5)
84}
85
86// ---------------------------------------------------------------------------
87// UniformRemesher
88// ---------------------------------------------------------------------------
89
90/// Isotropic remesher that targets a specified edge length.
91pub struct UniformRemesher {
92    /// Desired edge length after remeshing.
93    pub target_edge_length: f64,
94}
95
96impl UniformRemesher {
97    /// Create a new remesher with the given target edge length.
98    pub fn new(target_edge_length: f64) -> Self {
99        Self { target_edge_length }
100    }
101
102    /// Remesh a triangle mesh isotropically.
103    ///
104    /// Runs `iterations` passes of the 5-operator loop:
105    /// 1. Split edges longer than 4/3 × target
106    /// 2. Collapse edges shorter than 4/5 × target
107    /// 3. Flip edges to improve vertex valence
108    /// 4. Laplacian smoothing
109    /// 5. Project back to the original surface
110    pub fn remesh(&self, mesh: &TriangleMesh, iterations: usize) -> TriangleMesh {
111        isotropic_remesh(mesh, self.target_edge_length, iterations)
112    }
113}
114
115// ---------------------------------------------------------------------------
116// isotropic_remesh
117// ---------------------------------------------------------------------------
118
119/// Isotropic remeshing via repeated split / collapse / flip / smooth / project.
120///
121/// Returns a new [`TriangleMesh`] with edges approximately equal to `target_len`.
122pub fn isotropic_remesh(mesh: &TriangleMesh, target_len: f64, iterations: usize) -> TriangleMesh {
123    // Convert to raw arrays for internal processing
124    let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
125    let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
126
127    // Keep a snapshot of the original mesh for projection
128    let orig_verts = verts.clone();
129    let orig_tris = tris.clone();
130
131    let high = target_len * 4.0 / 3.0;
132    let low = target_len * 4.0 / 5.0;
133
134    for _ in 0..iterations {
135        // 1. Split long edges
136        split_long_edges(&mut verts, &mut tris, high);
137
138        // 2. Collapse short edges
139        collapse_short_edges(&mut verts, &mut tris, low);
140
141        // 3. Flip edges toward valence-6
142        flip_for_valence(&mut tris, verts.len());
143
144        // 4. Laplacian smooth (tangential)
145        laplacian_smooth_surface(&mut verts, &tris);
146
147        // 5. Project back to original surface
148        project_to_surface(&mut verts, &orig_verts, &orig_tris);
149    }
150
151    let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
152    TriangleMesh::new(vertices, tris)
153}
154
155// ---------------------------------------------------------------------------
156// Split long edges
157// ---------------------------------------------------------------------------
158
159fn split_long_edges(verts: &mut Vec<[f64; 3]>, tris: &mut Vec<[usize; 3]>, max_len: f64) {
160    // Deduplicate midpoints: edge (min, max) → midpoint vertex index
161    let mut edge_midpoints: HashMap<(usize, usize), usize> = HashMap::new();
162    let mut new_tris: Vec<[usize; 3]> = Vec::new();
163    let mut keep = vec![true; tris.len()];
164
165    for (ti, tri) in tris.iter().enumerate() {
166        let [a, b, c] = *tri;
167        let pa = verts[a];
168        let pb = verts[b];
169        let pc = verts[c];
170
171        let lab = dist3(pa, pb);
172        let lbc = dist3(pb, pc);
173        let lca = dist3(pc, pa);
174
175        let (longest, va, vb, vopp) = if lab >= lbc && lab >= lca {
176            (lab, a, b, c)
177        } else if lbc >= lab && lbc >= lca {
178            (lbc, b, c, a)
179        } else {
180            (lca, c, a, b)
181        };
182
183        if longest > max_len {
184            // Reuse existing midpoint for this edge if already created
185            let key = (va.min(vb), va.max(vb));
186            let mid = if let Some(&existing) = edge_midpoints.get(&key) {
187                existing
188            } else {
189                let idx = verts.len();
190                verts.push(midpoint(verts[va], verts[vb]));
191                edge_midpoints.insert(key, idx);
192                idx
193            };
194            new_tris.push([va, mid, vopp]);
195            new_tris.push([mid, vb, vopp]);
196            keep[ti] = false;
197        }
198    }
199
200    let orig: Vec<[usize; 3]> = tris
201        .iter()
202        .enumerate()
203        .filter_map(|(i, &t)| if keep[i] { Some(t) } else { None })
204        .collect();
205
206    *tris = orig;
207    tris.extend(new_tris);
208}
209
210// ---------------------------------------------------------------------------
211// Collapse short edges
212// ---------------------------------------------------------------------------
213
214fn collapse_short_edges(verts: &mut Vec<[f64; 3]>, tris: &mut Vec<[usize; 3]>, min_len: f64) {
215    // Build a remap table: vertex index → canonical index
216    let n = verts.len();
217    let mut remap: Vec<usize> = (0..n).collect();
218
219    // Find short edges
220    let mut edge_set: HashMap<(usize, usize), bool> = HashMap::new();
221    for tri in tris.iter() {
222        for k in 0..3 {
223            let a = tri[k];
224            let b = tri[(k + 1) % 3];
225            let key = (a.min(b), a.max(b));
226            edge_set.insert(key, true);
227        }
228    }
229
230    for (a, b) in edge_set.keys() {
231        let ca = remap[*a];
232        let cb = remap[*b];
233        if ca == cb {
234            continue;
235        }
236        let d = dist3(verts[ca], verts[cb]);
237        if d < min_len {
238            // Merge b into a (use midpoint)
239            let mp = midpoint(verts[ca], verts[cb]);
240            verts[ca] = mp;
241            // Remap all references to cb → ca
242            for r in remap.iter_mut() {
243                if *r == cb {
244                    *r = ca;
245                }
246            }
247        }
248    }
249
250    // Apply remap to triangles and remove degenerate
251    let new_tris: Vec<[usize; 3]> = tris
252        .iter()
253        .map(|tri| [remap[tri[0]], remap[tri[1]], remap[tri[2]]])
254        .filter(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2])
255        .collect();
256
257    *tris = new_tris;
258}
259
260// ---------------------------------------------------------------------------
261// Flip edges for valence
262// ---------------------------------------------------------------------------
263
264fn flip_for_valence(tris: &mut Vec<[usize; 3]>, n_verts: usize) {
265    // Compute vertex valence
266    let mut valence = vec![0usize; n_verts];
267    for tri in tris.iter() {
268        for &vi in tri.iter() {
269            if vi < n_verts {
270                valence[vi] += 1;
271            }
272        }
273    }
274
275    // Build edge → (tri_idx, opposite_vertex) map
276    let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
277    for (ti, tri) in tris.iter().enumerate() {
278        for k in 0..3 {
279            let a = tri[k];
280            let b = tri[(k + 1) % 3];
281            let opp = tri[(k + 2) % 3];
282            let key = (a.min(b), a.max(b));
283            edge_map.entry(key).or_default().push((ti, opp));
284        }
285    }
286
287    for ((ea, eb), entries) in &edge_map {
288        if entries.len() != 2 {
289            continue;
290        }
291        let (ti, oc) = entries[0];
292        let (tj, od) = entries[1];
293
294        // Current deviation from ideal valence (6 for interior)
295        let dev_before = valence_deviation(valence[*ea], 6)
296            + valence_deviation(valence[*eb], 6)
297            + valence_deviation(valence[oc], 6)
298            + valence_deviation(valence[od], 6);
299
300        // After flip: edge changes to (oc, od)
301        let dev_after = valence_deviation(valence[*ea] - 1, 6)
302            + valence_deviation(valence[*eb] - 1, 6)
303            + valence_deviation(valence[oc] + 1, 6)
304            + valence_deviation(valence[od] + 1, 6);
305
306        if dev_after < dev_before {
307            tris[ti] = [*ea, oc, od];
308            tris[tj] = [*eb, od, oc];
309            // Update valence
310            valence[*ea] -= 1;
311            valence[*eb] -= 1;
312            valence[oc] += 1;
313            valence[od] += 1;
314        }
315    }
316}
317
318fn valence_deviation(v: usize, ideal: usize) -> i64 {
319    let d = v as i64 - ideal as i64;
320    d * d
321}
322
323// ---------------------------------------------------------------------------
324// Laplacian smooth (tangential)
325// ---------------------------------------------------------------------------
326
327fn laplacian_smooth_surface(verts: &mut [[f64; 3]], tris: &[[usize; 3]]) {
328    let n = verts.len();
329    let mut sums = vec![[0.0_f64; 3]; n];
330    let mut counts = vec![0usize; n];
331
332    for tri in tris {
333        for k in 0..3 {
334            let vi = tri[k];
335            for j in 0..3 {
336                if j != k {
337                    let vj = tri[j];
338                    sums[vi] = add3(sums[vi], verts[vj]);
339                    counts[vi] += 1;
340                }
341            }
342        }
343    }
344
345    for i in 0..n {
346        if counts[i] > 0 {
347            let avg = scale3(sums[i], 1.0 / counts[i] as f64);
348            // Tangential: move along the avg direction with small weight
349            let delta = sub3(avg, verts[i]);
350            verts[i] = add3(verts[i], scale3(delta, 0.5));
351        }
352    }
353}
354
355// ---------------------------------------------------------------------------
356// Project to original surface
357// ---------------------------------------------------------------------------
358
359fn project_to_surface(verts: &mut [[f64; 3]], orig_verts: &[[f64; 3]], orig_tris: &[[usize; 3]]) {
360    for v in verts.iter_mut() {
361        // Find the closest point on the original mesh
362        let mut best_dist = f64::INFINITY;
363        let mut best_point = *v;
364
365        for tri in orig_tris {
366            let a = orig_verts[tri[0]];
367            let b = orig_verts[tri[1]];
368            let c = orig_verts[tri[2]];
369            let cp = closest_point_on_triangle(*v, a, b, c);
370            let d = dist3(*v, cp);
371            if d < best_dist {
372                best_dist = d;
373                best_point = cp;
374            }
375        }
376
377        *v = best_point;
378    }
379}
380
381/// Closest point on a triangle to a query point.
382fn closest_point_on_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
383    let ab = sub3(b, a);
384    let ac = sub3(c, a);
385    let ap = sub3(p, a);
386
387    let d1 = dot3(ab, ap);
388    let d2 = dot3(ac, ap);
389    if d1 <= 0.0 && d2 <= 0.0 {
390        return a;
391    }
392
393    let bp = sub3(p, b);
394    let d3 = dot3(ab, bp);
395    let d4 = dot3(ac, bp);
396    if d3 >= 0.0 && d4 <= d3 {
397        return b;
398    }
399
400    let vc = d1 * d4 - d3 * d2;
401    if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
402        let v = d1 / (d1 - d3);
403        return add3(a, scale3(ab, v));
404    }
405
406    let cp = sub3(p, c);
407    let d5 = dot3(ab, cp);
408    let d6 = dot3(ac, cp);
409    if d6 >= 0.0 && d5 <= d6 {
410        return c;
411    }
412
413    let vb = d5 * d2 - d1 * d6;
414    if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
415        let w = d2 / (d2 - d6);
416        return add3(a, scale3(ac, w));
417    }
418
419    let va = d3 * d6 - d5 * d4;
420    if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
421        let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
422        return add3(b, scale3(sub3(c, b), w));
423    }
424
425    let denom = 1.0 / (va + vb + vc);
426    let v = vb * denom;
427    let w = vc * denom;
428    add3(add3(a, scale3(ab, v)), scale3(ac, w))
429}
430
431// ---------------------------------------------------------------------------
432// LoopSubdivision
433// ---------------------------------------------------------------------------
434
435/// Loop subdivision scheme for triangle meshes.
436pub struct LoopSubdivision;
437
438impl LoopSubdivision {
439    /// Subdivide a triangle mesh once using the Loop scheme.
440    ///
441    /// Each triangle is split into 4 sub-triangles by inserting midpoints on
442    /// every edge. Returns a new [`TriangleMesh`] with 4× the original face count.
443    pub fn subdivide(mesh: &TriangleMesh) -> TriangleMesh {
444        let n_orig = mesh.vertices.len();
445        let mut new_verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
446
447        // Map from edge (min_idx, max_idx) → midpoint vertex index
448        let mut edge_midpoints: HashMap<(usize, usize), usize> = HashMap::new();
449
450        let get_or_create_midpoint = |a: usize,
451                                      b: usize,
452                                      verts: &mut Vec<[f64; 3]>,
453                                      map: &mut HashMap<(usize, usize), usize>|
454         -> usize {
455            let key = (a.min(b), a.max(b));
456            if let Some(&idx) = map.get(&key) {
457                return idx;
458            }
459            let mid = midpoint(verts[a], verts[b]);
460            let idx = verts.len();
461            verts.push(mid);
462            map.insert(key, idx);
463            idx
464        };
465
466        // Build new triangles
467        let mut new_tris: Vec<[usize; 3]> = Vec::with_capacity(mesh.indices.len() * 4);
468
469        for tri in &mesh.indices {
470            let [a, b, c] = *tri;
471            let mab = get_or_create_midpoint(a, b, &mut new_verts, &mut edge_midpoints);
472            let mbc = get_or_create_midpoint(b, c, &mut new_verts, &mut edge_midpoints);
473            let mca = get_or_create_midpoint(c, a, &mut new_verts, &mut edge_midpoints);
474
475            new_tris.push([a, mab, mca]);
476            new_tris.push([mab, b, mbc]);
477            new_tris.push([mca, mbc, c]);
478            new_tris.push([mab, mbc, mca]);
479        }
480
481        // Apply Loop mask to original vertices
482        // Build 1-ring neighbourhood
483        let mut neighbours: Vec<Vec<usize>> = vec![Vec::new(); n_orig];
484        for tri in &mesh.indices {
485            let [a, b, c] = *tri;
486            if !neighbours[a].contains(&b) {
487                neighbours[a].push(b);
488            }
489            if !neighbours[a].contains(&c) {
490                neighbours[a].push(c);
491            }
492            if !neighbours[b].contains(&a) {
493                neighbours[b].push(a);
494            }
495            if !neighbours[b].contains(&c) {
496                neighbours[b].push(c);
497            }
498            if !neighbours[c].contains(&a) {
499                neighbours[c].push(a);
500            }
501            if !neighbours[c].contains(&b) {
502                neighbours[c].push(b);
503            }
504        }
505
506        for i in 0..n_orig {
507            let k = neighbours[i].len();
508            if k == 0 {
509                continue;
510            }
511            // Loop's beta weight
512            let beta = if k == 3 {
513                3.0 / 16.0
514            } else {
515                3.0 / (8.0 * k as f64)
516            };
517            let self_weight = 1.0 - k as f64 * beta;
518
519            let mut new_pos = scale3(new_verts[i], self_weight);
520            for &nb in &neighbours[i] {
521                new_pos = add3(new_pos, scale3(new_verts[nb], beta));
522            }
523            new_verts[i] = new_pos;
524        }
525
526        let vertices: Vec<Vec3> = new_verts.iter().map(|&a| arr_to_vec3(a)).collect();
527        TriangleMesh::new(vertices, new_tris)
528    }
529}
530
531// ---------------------------------------------------------------------------
532// CatmullClark
533// ---------------------------------------------------------------------------
534
535/// Catmull-Clark subdivision for quad meshes.
536pub struct CatmullClark;
537
538impl CatmullClark {
539    /// Subdivide a quad mesh once using Catmull-Clark rules.
540    ///
541    /// # Arguments
542    /// * `verts` – original vertex positions.
543    /// * `quads` – quads defined by four vertex indices each.
544    ///
545    /// # Returns
546    /// A tuple `(new_vertices, new_quads)` where the new mesh has 4× the
547    /// number of faces and smoother vertex positions.
548    pub fn subdivide_quad_mesh(
549        verts: &[[f64; 3]],
550        quads: &[[usize; 4]],
551    ) -> (Vec<[f64; 3]>, Vec<[usize; 4]>) {
552        let n_orig = verts.len();
553        let mut new_verts: Vec<[f64; 3]> = verts.to_vec();
554
555        // Step 1: Face points — centroid of each quad
556        let face_point_start = new_verts.len();
557        let mut face_points: Vec<usize> = Vec::with_capacity(quads.len());
558        for quad in quads {
559            let fp = scale3(
560                quad.iter()
561                    .fold([0.0_f64; 3], |acc, &vi| add3(acc, verts[vi])),
562                0.25,
563            );
564            face_points.push(new_verts.len());
565            new_verts.push(fp);
566        }
567
568        // Step 2: Edge points
569        // For each edge, the edge point is the average of its two endpoints
570        // and the face points of the two adjacent faces.
571        let mut edge_map: HashMap<(usize, usize), Vec<usize>> = HashMap::new(); // edge → face indices
572        for (qi, quad) in quads.iter().enumerate() {
573            for k in 0..4 {
574                let a = quad[k];
575                let b = quad[(k + 1) % 4];
576                let key = (a.min(b), a.max(b));
577                edge_map.entry(key).or_default().push(qi);
578            }
579        }
580
581        let edge_point_start = new_verts.len();
582        let mut edge_to_idx: HashMap<(usize, usize), usize> = HashMap::new();
583        for (&(ea, eb), face_idxs) in &edge_map {
584            let mut ep = scale3(add3(verts[ea], verts[eb]), 0.5);
585            if face_idxs.len() == 2 {
586                let fp0 = new_verts[face_point_start + face_idxs[0]];
587                let fp1 = new_verts[face_point_start + face_idxs[1]];
588                ep = scale3(add3(add3(verts[ea], verts[eb]), add3(fp0, fp1)), 0.25);
589            }
590            edge_to_idx.insert((ea, eb), new_verts.len());
591            edge_to_idx.insert((eb, ea), new_verts.len());
592            new_verts.push(ep);
593        }
594        let _ = edge_point_start;
595
596        // Step 3: Updated original vertex positions
597        // v' = (F + 2E + (n-3)V) / n
598        // where F = avg of adjacent face points, E = avg of adjacent edge midpoints, n = valence
599        let mut vertex_face_points: Vec<Vec<[f64; 3]>> = vec![Vec::new(); n_orig];
600        let mut vertex_edge_mids: Vec<Vec<[f64; 3]>> = vec![Vec::new(); n_orig];
601
602        for (qi, quad) in quads.iter().enumerate() {
603            let fp = new_verts[face_point_start + qi];
604            for k in 0..4 {
605                let vi = quad[k];
606                vertex_face_points[vi].push(fp);
607                let nb = quad[(k + 1) % 4];
608                let em = scale3(add3(verts[vi], verts[nb]), 0.5);
609                vertex_edge_mids[vi].push(em);
610                let nb2 = quad[(k + 3) % 4];
611                let em2 = scale3(add3(verts[vi], verts[nb2]), 0.5);
612                vertex_edge_mids[vi].push(em2);
613            }
614        }
615
616        for i in 0..n_orig {
617            let n = vertex_face_points[i].len();
618            if n == 0 {
619                continue;
620            }
621            let n_f = n as f64;
622            let f_avg = scale3(
623                vertex_face_points[i]
624                    .iter()
625                    .fold([0.0_f64; 3], |acc, &x| add3(acc, x)),
626                1.0 / n_f,
627            );
628            let e_count = vertex_edge_mids[i].len();
629            let e_avg = if e_count > 0 {
630                scale3(
631                    vertex_edge_mids[i]
632                        .iter()
633                        .fold([0.0_f64; 3], |acc, &x| add3(acc, x)),
634                    1.0 / e_count as f64,
635                )
636            } else {
637                verts[i]
638            };
639            // Catmull-Clark: (F + 2E + (n-3)V) / n
640            let v_new = scale3(
641                add3(
642                    add3(f_avg, scale3(e_avg, 2.0)),
643                    scale3(verts[i], (n_f - 3.0).max(0.0)),
644                ),
645                1.0 / n_f,
646            );
647            new_verts[i] = v_new;
648        }
649
650        // Step 4: Build new quads
651        // Each original quad → 4 new quads
652        let mut new_quads: Vec<[usize; 4]> = Vec::with_capacity(quads.len() * 4);
653        for (qi, quad) in quads.iter().enumerate() {
654            let fp_idx = face_point_start + qi;
655            for k in 0..4 {
656                let vi = quad[k];
657                let ea = quad[k];
658                let eb = quad[(k + 1) % 4];
659                let ea2 = quad[(k + 3) % 4];
660                let eb2 = quad[k];
661
662                let ep1 = *edge_to_idx.get(&(ea.min(eb), ea.max(eb))).unwrap_or(&vi);
663                let ep2 = *edge_to_idx
664                    .get(&(ea2.min(eb2), ea2.max(eb2)))
665                    .unwrap_or(&vi);
666
667                new_quads.push([vi, ep1, fp_idx, ep2]);
668            }
669        }
670
671        (new_verts, new_quads)
672    }
673}
674
675// ---------------------------------------------------------------------------
676// Feature-preserving remeshing
677// ---------------------------------------------------------------------------
678
679/// Feature-preserving isotropic remeshing.
680///
681/// Like `isotropic_remesh` but marks sharp feature edges (dihedral angle >
682/// `feature_angle_rad`) as "locked" — they are never collapsed or flipped.
683pub fn feature_preserving_remesh(
684    mesh: &TriangleMesh,
685    target_len: f64,
686    iterations: usize,
687    feature_angle_rad: f64,
688) -> TriangleMesh {
689    let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
690    let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
691
692    // Identify feature edges (dihedral angle > threshold)
693    let feature_edges = detect_feature_edges(&verts, &tris, feature_angle_rad);
694
695    let orig_verts = verts.clone();
696    let orig_tris = tris.clone();
697
698    let high = target_len * 4.0 / 3.0;
699    let low = target_len * 4.0 / 5.0;
700
701    for _ in 0..iterations {
702        split_long_edges(&mut verts, &mut tris, high);
703        collapse_short_edges_preserving(&mut verts, &mut tris, low, &feature_edges);
704        flip_for_valence(&mut tris, verts.len());
705        laplacian_smooth_surface(&mut verts, &tris);
706        project_to_surface(&mut verts, &orig_verts, &orig_tris);
707    }
708
709    let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
710    TriangleMesh::new(vertices, tris)
711}
712
713/// Detect feature edges where the dihedral angle between adjacent faces > `threshold`.
714fn detect_feature_edges(
715    verts: &[[f64; 3]],
716    tris: &[[usize; 3]],
717    threshold_rad: f64,
718) -> HashMap<(usize, usize), bool> {
719    // Build edge → face normals
720    let mut edge_faces: HashMap<(usize, usize), Vec<[f64; 3]>> = HashMap::new();
721    for tri in tris {
722        let a = verts[tri[0]];
723        let b = verts[tri[1]];
724        let c = verts[tri[2]];
725        let e1 = sub3(b, a);
726        let e2 = sub3(c, a);
727        let n = normalize3(cross3(e1, e2));
728        for k in 0..3 {
729            let va = tri[k];
730            let vb = tri[(k + 1) % 3];
731            let key = (va.min(vb), va.max(vb));
732            edge_faces.entry(key).or_default().push(n);
733        }
734    }
735
736    let mut features = HashMap::new();
737    let cos_thresh = threshold_rad.cos();
738    for ((ea, eb), normals) in &edge_faces {
739        let is_feature = if normals.len() == 2 {
740            let d = dot3(normals[0], normals[1]);
741            d < cos_thresh
742        } else {
743            true // boundary edge → feature
744        };
745        features.insert((*ea, *eb), is_feature);
746    }
747    features
748}
749
750/// Edge collapse that preserves feature edges.
751fn collapse_short_edges_preserving(
752    verts: &mut Vec<[f64; 3]>,
753    tris: &mut Vec<[usize; 3]>,
754    min_len: f64,
755    feature_edges: &HashMap<(usize, usize), bool>,
756) {
757    let n = verts.len();
758    let mut remap: Vec<usize> = (0..n).collect();
759
760    let mut edge_set: HashMap<(usize, usize), bool> = HashMap::new();
761    for tri in tris.iter() {
762        for k in 0..3 {
763            let a = tri[k];
764            let b = tri[(k + 1) % 3];
765            let key = (a.min(b), a.max(b));
766            edge_set.insert(key, true);
767        }
768    }
769
770    for (a, b) in edge_set.keys() {
771        let ca = remap[*a];
772        let cb = remap[*b];
773        if ca == cb {
774            continue;
775        }
776        // Check feature lock
777        if feature_edges
778            .get(&(*a.min(b), *a.max(b)))
779            .copied()
780            .unwrap_or(false)
781        {
782            continue;
783        }
784        let d = dist3(verts[ca], verts[cb]);
785        if d < min_len {
786            let mp = midpoint(verts[ca], verts[cb]);
787            verts[ca] = mp;
788            for r in remap.iter_mut() {
789                if *r == cb {
790                    *r = ca;
791                }
792            }
793        }
794    }
795
796    let new_tris: Vec<[usize; 3]> = tris
797        .iter()
798        .map(|tri| [remap[tri[0]], remap[tri[1]], remap[tri[2]]])
799        .filter(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2])
800        .collect();
801    *tris = new_tris;
802}
803
804// ---------------------------------------------------------------------------
805// Edge flip for quality
806// ---------------------------------------------------------------------------
807
808/// Flip edges to improve triangle quality (maximize minimum angle).
809///
810/// For each interior edge shared by two triangles, flip if the flipped
811/// configuration has a better minimum angle.
812pub fn flip_edges_for_quality(tris: &mut Vec<[usize; 3]>, verts: &[[f64; 3]]) {
813    let n_verts = verts.len();
814
815    // Build edge → face map
816    let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
817    for (ti, tri) in tris.iter().enumerate() {
818        for k in 0..3 {
819            let a = tri[k];
820            let b = tri[(k + 1) % 3];
821            let opp = tri[(k + 2) % 3];
822            let key = (a.min(b), a.max(b));
823            edge_map.entry(key).or_default().push((ti, opp));
824        }
825    }
826
827    for ((ea, eb), entries) in &edge_map {
828        if entries.len() != 2 {
829            continue;
830        }
831        let (ti, oc) = entries[0];
832        let (tj, od) = entries[1];
833
834        if *ea >= n_verts || *eb >= n_verts || oc >= n_verts || od >= n_verts {
835            continue;
836        }
837
838        // Compare min angles before and after flip
839        let before_min = min_triangle_angle(verts[*ea], verts[*eb], verts[oc])
840            .min(min_triangle_angle(verts[*ea], verts[*eb], verts[od]));
841        let after_min = min_triangle_angle(verts[oc], verts[od], verts[*ea])
842            .min(min_triangle_angle(verts[oc], verts[od], verts[*eb]));
843
844        if after_min > before_min + 1e-6 {
845            // Flip: replace (ea,eb,oc) and (ea,eb,od) with (oc,od,ea) and (oc,od,eb)
846            tris[ti] = [*ea, oc, od];
847            tris[tj] = [*eb, od, oc];
848        }
849    }
850}
851
852/// Minimum angle (in radians) of a triangle given its three vertex positions.
853fn min_triangle_angle(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
854    let ab = sub3(b, a);
855    let ac = sub3(c, a);
856    let bc = sub3(c, b);
857    let lab = len3(ab);
858    let lac = len3(ac);
859    let lbc = len3(bc);
860
861    if lab < 1e-12 || lac < 1e-12 || lbc < 1e-12 {
862        return 0.0;
863    }
864
865    let cos_a = (dot3(ab, ac) / (lab * lac)).clamp(-1.0, 1.0);
866    let cos_b = (dot3(scale3(ab, -1.0), bc) / (lab * lbc)).clamp(-1.0, 1.0);
867    let cos_c = (dot3(scale3(ac, -1.0), scale3(bc, -1.0)) / (lac * lbc)).clamp(-1.0, 1.0);
868
869    cos_a.acos().min(cos_b.acos()).min(cos_c.acos())
870}
871
872// ---------------------------------------------------------------------------
873// Mesh quality metrics
874// ---------------------------------------------------------------------------
875
876/// Compute the minimum angle quality metric over all triangles.
877///
878/// Returns `min_angle / (PI/3)` normalised to `[0, 1]` (1 = equilateral).
879pub fn mesh_quality_min_angle(verts: &[[f64; 3]], tris: &[[usize; 3]]) -> f64 {
880    if tris.is_empty() {
881        return 0.0;
882    }
883    let ideal = std::f64::consts::PI / 3.0;
884    let min_q = tris
885        .iter()
886        .map(|tri| {
887            let a = verts[tri[0]];
888            let b = verts[tri[1]];
889            let c = verts[tri[2]];
890            let min_ang = min_triangle_angle(a, b, c);
891            min_ang / ideal
892        })
893        .fold(f64::INFINITY, f64::min);
894    min_q.min(1.0)
895}
896
897/// Compute average aspect ratio of all triangles.
898///
899/// Aspect ratio = longest_edge / (2 * inradius). Ideal (equilateral) = sqrt(3)/3 ≈ 0.577.
900/// Lower is better.
901pub fn mesh_aspect_ratio_avg(verts: &[[f64; 3]], tris: &[[usize; 3]]) -> f64 {
902    if tris.is_empty() {
903        return 0.0;
904    }
905    let ratios: Vec<f64> = tris
906        .iter()
907        .map(|tri| {
908            let a = verts[tri[0]];
909            let b = verts[tri[1]];
910            let c = verts[tri[2]];
911            let lab = len3(sub3(b, a));
912            let lbc = len3(sub3(c, b));
913            let lca = len3(sub3(a, c));
914            let longest = lab.max(lbc).max(lca);
915            let s = (lab + lbc + lca) * 0.5; // semi-perimeter
916            let area_sq = s * (s - lab) * (s - lbc) * (s - lca);
917            if area_sq <= 0.0 || s < 1e-12 {
918                return f64::INFINITY;
919            }
920            let area = area_sq.sqrt();
921            let inradius = area / s;
922            longest / (2.0 * inradius)
923        })
924        .collect();
925
926    let finite: Vec<f64> = ratios.into_iter().filter(|x| x.is_finite()).collect();
927    if finite.is_empty() {
928        0.0
929    } else {
930        finite.iter().sum::<f64>() / finite.len() as f64
931    }
932}
933
934// ---------------------------------------------------------------------------
935// Tangent Laplacian smoothing (preserves surface projection)
936// ---------------------------------------------------------------------------
937
938/// Tangent-space Laplacian smoothing: smooths vertices but projects back to
939/// the original surface after each iteration.
940pub fn tangent_laplacian_smooth(mesh: &TriangleMesh, iterations: usize) -> TriangleMesh {
941    let orig_verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
942    let tris = &mesh.indices;
943    let mut verts = orig_verts.clone();
944
945    for _ in 0..iterations {
946        laplacian_smooth_surface(&mut verts, tris);
947        project_to_surface(&mut verts, &orig_verts, tris);
948    }
949
950    let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
951    TriangleMesh::new(vertices, tris.clone())
952}
953
954// ---------------------------------------------------------------------------
955// Edge collapse
956// ---------------------------------------------------------------------------
957
958/// Collapse the edge between vertices `v0` and `v1` to their midpoint.
959///
960/// All triangles containing both `v0` and `v1` are removed (degenerate after collapse).
961/// All other references to `v1` are remapped to `v0` (with the midpoint position).
962pub fn collapse_edge(verts: &mut Vec<[f64; 3]>, tris: &mut Vec<[usize; 3]>, v0: usize, v1: usize) {
963    let mid = midpoint(verts[v0], verts[v1]);
964    verts[v0] = mid;
965
966    // Remap all v1 → v0
967    let new_tris: Vec<[usize; 3]> = tris
968        .iter()
969        .map(|tri| {
970            [
971                if tri[0] == v1 { v0 } else { tri[0] },
972                if tri[1] == v1 { v0 } else { tri[1] },
973                if tri[2] == v1 { v0 } else { tri[2] },
974            ]
975        })
976        .filter(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2])
977        .collect();
978    *tris = new_tris;
979}
980
981// ---------------------------------------------------------------------------
982// Laplacian smoothing (public API)
983// ---------------------------------------------------------------------------
984
985/// Apply `iterations` rounds of uniform Laplacian smoothing to a mesh.
986///
987/// Each iteration moves every vertex towards the average of its neighbours
988/// by a factor of `lambda` (0..1).
989pub fn laplacian_smooth(mesh: &TriangleMesh, iterations: usize, lambda: f64) -> TriangleMesh {
990    let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
991    let tris = &mesh.indices;
992
993    for _ in 0..iterations {
994        let n = verts.len();
995        let mut sums = vec![[0.0_f64; 3]; n];
996        let mut counts = vec![0usize; n];
997
998        for tri in tris.iter() {
999            for k in 0..3 {
1000                let vi = tri[k];
1001                for j in 0..3 {
1002                    if j != k {
1003                        let vj = tri[j];
1004                        sums[vi] = add3(sums[vi], verts[vj]);
1005                        counts[vi] += 1;
1006                    }
1007                }
1008            }
1009        }
1010
1011        for i in 0..n {
1012            if counts[i] > 0 {
1013                let avg = scale3(sums[i], 1.0 / counts[i] as f64);
1014                let delta = sub3(avg, verts[i]);
1015                verts[i] = add3(verts[i], scale3(delta, lambda));
1016            }
1017        }
1018    }
1019
1020    let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
1021    TriangleMesh::new(vertices, tris.clone())
1022}
1023
1024// ---------------------------------------------------------------------------
1025// Edge flip for Delaunay criterion
1026// ---------------------------------------------------------------------------
1027
1028/// Flip edges in a 2D-projected triangle mesh to satisfy the Delaunay criterion.
1029///
1030/// An edge is flipped if the opposite vertex lies inside the circumcircle of the
1031/// current triangle.  Operates directly on the `tris` array (in-place).
1032pub fn delaunay_edge_flip(tris: &mut Vec<[usize; 3]>, verts: &[[f64; 3]]) {
1033    // Build edge → (tri_idx, opposite_vertex) map
1034    let mut changed = true;
1035    let max_passes = 32;
1036    let mut pass = 0;
1037
1038    while changed && pass < max_passes {
1039        changed = false;
1040        pass += 1;
1041
1042        let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
1043        for (ti, tri) in tris.iter().enumerate() {
1044            for k in 0..3 {
1045                let a = tri[k];
1046                let b = tri[(k + 1) % 3];
1047                let opp = tri[(k + 2) % 3];
1048                let key = (a.min(b), a.max(b));
1049                edge_map.entry(key).or_default().push((ti, opp));
1050            }
1051        }
1052
1053        for ((ea, eb), entries) in &edge_map {
1054            if entries.len() != 2 {
1055                continue;
1056            }
1057            let (ti, oc) = entries[0];
1058            let (tj, od) = entries[1];
1059
1060            if *ea >= verts.len() || *eb >= verts.len() || oc >= verts.len() || od >= verts.len() {
1061                continue;
1062            }
1063
1064            // Test if od is inside the circumcircle of triangle (ea, eb, oc)
1065            if point_in_circumcircle(verts[*ea], verts[*eb], verts[oc], verts[od]) {
1066                // Flip: replace edge ea-eb with oc-od
1067                tris[ti] = [*ea, oc, od];
1068                tris[tj] = [*eb, od, oc];
1069                changed = true;
1070            }
1071        }
1072    }
1073}
1074
1075/// Returns `true` if point `d` is inside the circumcircle of triangle `(a, b, c)`.
1076/// Uses the 2D X-Z projection.
1077fn point_in_circumcircle(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> bool {
1078    // Use the standard 3×3 determinant test (X-Z plane projection)
1079    let ax = a[0] - d[0];
1080    let az = a[2] - d[2];
1081    let bx = b[0] - d[0];
1082    let bz = b[2] - d[2];
1083    let cx = c[0] - d[0];
1084    let cz = c[2] - d[2];
1085
1086    let det = ax * (bz * (cx * cx + cz * cz) - cz * (bx * bx + bz * bz))
1087        - az * (bx * (cx * cx + cz * cz) - cx * (bx * bx + bz * bz))
1088        + (ax * ax + az * az) * (bx * cz - bz * cx);
1089
1090    det > 0.0
1091}
1092
1093// ---------------------------------------------------------------------------
1094// Mesh coarsening
1095// ---------------------------------------------------------------------------
1096
1097/// Coarsen a mesh by collapsing all edges shorter than `target_len`.
1098///
1099/// Repeatedly collapses short edges until none remain below the threshold.
1100pub fn coarsen_mesh(mesh: &TriangleMesh, target_len: f64) -> TriangleMesh {
1101    let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1102    let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
1103
1104    // Run up to 10 collapse passes
1105    for _ in 0..10 {
1106        let before = tris.len();
1107        collapse_short_edges(&mut verts, &mut tris, target_len);
1108        if tris.len() == before {
1109            break;
1110        }
1111    }
1112
1113    let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
1114    TriangleMesh::new(vertices, tris)
1115}
1116
1117// ---------------------------------------------------------------------------
1118// Adaptive refinement by curvature
1119// ---------------------------------------------------------------------------
1120
1121/// Refine triangles with estimated curvature exceeding `threshold`.
1122///
1123/// Curvature is approximated as the variation in face normals among a vertex's
1124/// one-ring neighbourhood.  High-curvature triangles are split at their midpoints.
1125pub fn adaptive_refine_by_curvature(mesh: &TriangleMesh, threshold: f64) -> TriangleMesh {
1126    let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1127    let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
1128
1129    // Compute per-face normals
1130    let face_normals: Vec<[f64; 3]> = tris
1131        .iter()
1132        .map(|tri| {
1133            let a = verts[tri[0]];
1134            let b = verts[tri[1]];
1135            let c = verts[tri[2]];
1136            normalize3(cross3(sub3(b, a), sub3(c, a)))
1137        })
1138        .collect();
1139
1140    // Compute per-vertex curvature as max angle between incident face normals
1141    let n = verts.len();
1142    let mut vertex_faces: Vec<Vec<usize>> = vec![Vec::new(); n];
1143    for (ti, tri) in tris.iter().enumerate() {
1144        for &vi in tri.iter() {
1145            vertex_faces[vi].push(ti);
1146        }
1147    }
1148
1149    let vertex_curvature: Vec<f64> = (0..n)
1150        .map(|vi| {
1151            let faces = &vertex_faces[vi];
1152            if faces.len() < 2 {
1153                return 0.0;
1154            }
1155            let mut max_angle: f64 = 0.0;
1156            for i in 0..faces.len() {
1157                for j in (i + 1)..faces.len() {
1158                    let d = dot3(face_normals[faces[i]], face_normals[faces[j]]).clamp(-1.0, 1.0);
1159                    let angle = d.acos();
1160                    if angle > max_angle {
1161                        max_angle = angle;
1162                    }
1163                }
1164            }
1165            max_angle
1166        })
1167        .collect();
1168
1169    // Split triangles where any vertex curvature exceeds threshold
1170    let mut new_tris: Vec<[usize; 3]> = Vec::new();
1171    let mut kept_tris: Vec<[usize; 3]> = Vec::new();
1172
1173    for tri in &tris {
1174        let max_curv = vertex_curvature[tri[0]]
1175            .max(vertex_curvature[tri[1]])
1176            .max(vertex_curvature[tri[2]]);
1177
1178        if max_curv > threshold {
1179            // Split along the longest edge
1180            let a = tri[0];
1181            let b = tri[1];
1182            let c = tri[2];
1183            let lab = dist3(verts[a], verts[b]);
1184            let lbc = dist3(verts[b], verts[c]);
1185            let lca = dist3(verts[c], verts[a]);
1186
1187            let (va, vb, vc) = if lab >= lbc && lab >= lca {
1188                (a, b, c)
1189            } else if lbc >= lab && lbc >= lca {
1190                (b, c, a)
1191            } else {
1192                (c, a, b)
1193            };
1194
1195            let mid_idx = verts.len();
1196            verts.push(midpoint(verts[va], verts[vb]));
1197            new_tris.push([va, mid_idx, vc]);
1198            new_tris.push([mid_idx, vb, vc]);
1199        } else {
1200            kept_tris.push(*tri);
1201        }
1202    }
1203
1204    tris = kept_tris;
1205    tris.extend(new_tris);
1206
1207    let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
1208    TriangleMesh::new(vertices, tris)
1209}
1210
1211// ---------------------------------------------------------------------------
1212// Vertex-clustering mesh decimation
1213// ---------------------------------------------------------------------------
1214
1215/// Decimate a mesh using vertex clustering.
1216///
1217/// Divides space into a uniform grid of `cells_per_axis³` cells and
1218/// merges all vertices within each cell to their centroid.  Returns a
1219/// decimated mesh with fewer vertices and faces.
1220pub fn vertex_clustering_decimate(mesh: &TriangleMesh, cells_per_axis: usize) -> TriangleMesh {
1221    if cells_per_axis == 0 || mesh.vertices.is_empty() {
1222        return mesh.clone();
1223    }
1224
1225    let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1226
1227    // Compute bounding box
1228    let mut bbmin = verts[0];
1229    let mut bbmax = verts[0];
1230    for &v in &verts {
1231        for k in 0..3 {
1232            if v[k] < bbmin[k] {
1233                bbmin[k] = v[k];
1234            }
1235            if v[k] > bbmax[k] {
1236                bbmax[k] = v[k];
1237            }
1238        }
1239    }
1240    let extent = [
1241        bbmax[0] - bbmin[0] + 1e-8,
1242        bbmax[1] - bbmin[1] + 1e-8,
1243        bbmax[2] - bbmin[2] + 1e-8,
1244    ];
1245    let c = cells_per_axis as f64;
1246
1247    // Assign each vertex to a cell
1248    let cell_of = |v: [f64; 3]| -> usize {
1249        let ix = ((v[0] - bbmin[0]) / extent[0] * c).floor() as usize;
1250        let iy = ((v[1] - bbmin[1]) / extent[1] * c).floor() as usize;
1251        let iz = ((v[2] - bbmin[2]) / extent[2] * c).floor() as usize;
1252        let ix = ix.min(cells_per_axis - 1);
1253        let iy = iy.min(cells_per_axis - 1);
1254        let iz = iz.min(cells_per_axis - 1);
1255        ix + cells_per_axis * (iy + cells_per_axis * iz)
1256    };
1257
1258    let n_cells = cells_per_axis * cells_per_axis * cells_per_axis;
1259    let mut cell_sums: Vec<[f64; 3]> = vec![[0.0; 3]; n_cells];
1260    let mut cell_counts: Vec<usize> = vec![0; n_cells];
1261    let mut vertex_cell: Vec<usize> = vec![0; verts.len()];
1262
1263    for (i, &v) in verts.iter().enumerate() {
1264        let cell = cell_of(v);
1265        vertex_cell[i] = cell;
1266        for k in 0..3 {
1267            cell_sums[cell][k] += v[k];
1268        }
1269        cell_counts[cell] += 1;
1270    }
1271
1272    // Build representative vertices for non-empty cells
1273    let mut cell_to_vert: Vec<Option<usize>> = vec![None; n_cells];
1274    let mut new_verts: Vec<[f64; 3]> = Vec::new();
1275    for cell in 0..n_cells {
1276        if cell_counts[cell] > 0 {
1277            cell_to_vert[cell] = Some(new_verts.len());
1278            let cnt = cell_counts[cell] as f64;
1279            new_verts.push([
1280                cell_sums[cell][0] / cnt,
1281                cell_sums[cell][1] / cnt,
1282                cell_sums[cell][2] / cnt,
1283            ]);
1284        }
1285    }
1286
1287    // Remap triangles
1288    let new_tris: Vec<[usize; 3]> = mesh
1289        .indices
1290        .iter()
1291        .filter_map(|tri| {
1292            let a = cell_to_vert[vertex_cell[tri[0]]]?;
1293            let b = cell_to_vert[vertex_cell[tri[1]]]?;
1294            let c = cell_to_vert[vertex_cell[tri[2]]]?;
1295            if a == b || b == c || a == c {
1296                return None;
1297            }
1298            Some([a, b, c])
1299        })
1300        .collect();
1301
1302    let vertices: Vec<Vec3> = new_verts.iter().map(|&a| arr_to_vec3(a)).collect();
1303    TriangleMesh::new(vertices, new_tris)
1304}
1305
1306// ---------------------------------------------------------------------------
1307// Saliency-weighted remeshing
1308// ---------------------------------------------------------------------------
1309
1310/// Remesh with non-uniform target edge lengths guided by per-vertex saliency.
1311///
1312/// High-saliency regions get a finer target edge length; low-saliency regions
1313/// get a coarser target.  `base_len` is the reference target edge length for
1314/// saliency = 0.5.
1315pub fn saliency_weighted_remesh(
1316    mesh: &TriangleMesh,
1317    saliency: &[f64],
1318    iterations: usize,
1319) -> TriangleMesh {
1320    if mesh.vertices.is_empty() {
1321        return mesh.clone();
1322    }
1323
1324    // Compute per-triangle saliency as average of vertex saliencies
1325    let tri_saliency: Vec<f64> = mesh
1326        .indices
1327        .iter()
1328        .map(|tri| {
1329            let s = [
1330                if tri[0] < saliency.len() {
1331                    saliency[tri[0]]
1332                } else {
1333                    0.5
1334                },
1335                if tri[1] < saliency.len() {
1336                    saliency[tri[1]]
1337                } else {
1338                    0.5
1339                },
1340                if tri[2] < saliency.len() {
1341                    saliency[tri[2]]
1342                } else {
1343                    0.5
1344                },
1345            ];
1346            (s[0] + s[1] + s[2]) / 3.0
1347        })
1348        .collect();
1349
1350    // Use average saliency as overall target scale
1351    let avg_sal = if tri_saliency.is_empty() {
1352        0.5
1353    } else {
1354        tri_saliency.iter().sum::<f64>() / tri_saliency.len() as f64
1355    };
1356
1357    // Compute mesh average edge length as base
1358    let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1359    let avg_edge: f64 = {
1360        let total: f64 = mesh
1361            .indices
1362            .iter()
1363            .map(|tri| {
1364                let a = verts[tri[0]];
1365                let b = verts[tri[1]];
1366                let c = verts[tri[2]];
1367                (dist3(a, b) + dist3(b, c) + dist3(c, a)) / 3.0
1368            })
1369            .sum();
1370        if mesh.indices.is_empty() {
1371            1.0
1372        } else {
1373            total / mesh.indices.len() as f64
1374        }
1375    };
1376
1377    // High saliency → finer (smaller target), low saliency → coarser
1378    let target = avg_edge * (1.5 - avg_sal.clamp(0.0, 1.0));
1379
1380    isotropic_remesh(mesh, target.max(1e-6), iterations)
1381}
1382
1383// ---------------------------------------------------------------------------
1384// Tests
1385// ---------------------------------------------------------------------------
1386
1387#[cfg(test)]
1388mod tests {
1389    use super::*;
1390
1391    fn simple_quad_mesh() -> TriangleMesh {
1392        // A flat square subdivided into 2 triangles
1393        let verts = vec![
1394            Vec3::new(0.0, 0.0, 0.0),
1395            Vec3::new(1.0, 0.0, 0.0),
1396            Vec3::new(1.0, 1.0, 0.0),
1397            Vec3::new(0.0, 1.0, 0.0),
1398        ];
1399        let tris = vec![[0usize, 1, 2], [0, 2, 3]];
1400        TriangleMesh::new(verts, tris)
1401    }
1402
1403    #[test]
1404    fn test_loop_subdivision_face_count() {
1405        let mesh = simple_quad_mesh();
1406        let sub = LoopSubdivision::subdivide(&mesh);
1407        // 2 original tris → 8 after one Loop subdivision
1408        assert_eq!(
1409            sub.indices.len(),
1410            mesh.indices.len() * 4,
1411            "Loop subdivision should quadruple the face count"
1412        );
1413    }
1414
1415    #[test]
1416    fn test_loop_subdivision_vertex_count_increases() {
1417        let mesh = simple_quad_mesh();
1418        let sub = LoopSubdivision::subdivide(&mesh);
1419        assert!(
1420            sub.vertices.len() > mesh.vertices.len(),
1421            "Loop subdivision should add vertices"
1422        );
1423    }
1424
1425    #[test]
1426    fn test_catmull_clark_face_count() {
1427        let verts: Vec<[f64; 3]> = vec![
1428            [0.0, 0.0, 0.0],
1429            [1.0, 0.0, 0.0],
1430            [1.0, 1.0, 0.0],
1431            [0.0, 1.0, 0.0],
1432        ];
1433        let quads: Vec<[usize; 4]> = vec![[0, 1, 2, 3]];
1434        let (_, new_quads) = CatmullClark::subdivide_quad_mesh(&verts, &quads);
1435        // 1 quad → 4 quads
1436        assert_eq!(
1437            new_quads.len(),
1438            quads.len() * 4,
1439            "Catmull-Clark should quadruple the quad count"
1440        );
1441    }
1442
1443    #[test]
1444    fn test_catmull_clark_two_quads() {
1445        let verts: Vec<[f64; 3]> = vec![
1446            [0.0, 0.0, 0.0],
1447            [1.0, 0.0, 0.0],
1448            [2.0, 0.0, 0.0],
1449            [0.0, 1.0, 0.0],
1450            [1.0, 1.0, 0.0],
1451            [2.0, 1.0, 0.0],
1452        ];
1453        let quads: Vec<[usize; 4]> = vec![[0, 1, 4, 3], [1, 2, 5, 4]];
1454        let (_, new_quads) = CatmullClark::subdivide_quad_mesh(&verts, &quads);
1455        assert_eq!(new_quads.len(), quads.len() * 4);
1456    }
1457
1458    #[test]
1459    fn test_isotropic_remesh_returns_mesh() {
1460        let mesh = simple_quad_mesh();
1461        let result = isotropic_remesh(&mesh, 0.5, 2);
1462        // Remeshed mesh should have at least 1 face
1463        assert!(
1464            !result.indices.is_empty(),
1465            "remeshed mesh should have faces"
1466        );
1467        assert!(
1468            !result.vertices.is_empty(),
1469            "remeshed mesh should have vertices"
1470        );
1471    }
1472
1473    #[test]
1474    fn test_uniform_remesher() {
1475        let mesh = simple_quad_mesh();
1476        let remesher = UniformRemesher::new(0.4);
1477        let result = remesher.remesh(&mesh, 1);
1478        assert!(!result.vertices.is_empty());
1479    }
1480
1481    #[test]
1482    fn test_loop_subdivision_twice() {
1483        let mesh = simple_quad_mesh();
1484        let sub1 = LoopSubdivision::subdivide(&mesh);
1485        let sub2 = LoopSubdivision::subdivide(&sub1);
1486        assert_eq!(sub2.indices.len(), mesh.indices.len() * 16);
1487    }
1488
1489    #[test]
1490    fn test_closest_point_on_triangle() {
1491        let a = [0.0_f64, 0.0, 0.0];
1492        let b = [1.0, 0.0, 0.0];
1493        let c = [0.0, 1.0, 0.0];
1494        // Query point directly above centroid
1495        let p = [0.25, 0.25, 1.0];
1496        let cp = closest_point_on_triangle(p, a, b, c);
1497        // Should project to (0.25, 0.25, 0.0)
1498        assert!((cp[0] - 0.25).abs() < 1e-10);
1499        assert!((cp[1] - 0.25).abs() < 1e-10);
1500        assert!(cp[2].abs() < 1e-10);
1501    }
1502
1503    // ── Feature-preserving remesh tests ──────────────────────────────────────
1504
1505    #[test]
1506    fn test_feature_preserving_remesh_returns_mesh() {
1507        let mesh = simple_quad_mesh();
1508        // Use a very small feature angle so all edges are locked (no collapses),
1509        // ensuring the output always has triangles.
1510        let result = feature_preserving_remesh(&mesh, 0.4, 1, 1.0_f64.to_radians());
1511        assert!(!result.vertices.is_empty());
1512        assert!(!result.indices.is_empty());
1513    }
1514
1515    #[test]
1516    fn test_feature_preserving_remesh_no_degenerate_faces() {
1517        let mesh = simple_quad_mesh();
1518        let result = feature_preserving_remesh(&mesh, 0.3, 2, 45.0_f64.to_radians());
1519        let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1520        for tri in &result.indices {
1521            let a = verts[tri[0]];
1522            let b = verts[tri[1]];
1523            let c = verts[tri[2]];
1524            let e1 = sub3(b, a);
1525            let e2 = sub3(c, a);
1526            let cross = cross3(e1, e2);
1527            let area = len3(cross) * 0.5;
1528            assert!(area >= 0.0, "negative area: {}", area);
1529        }
1530    }
1531
1532    // ── Edge flip tests ────────────────────────────────────────────────────────
1533
1534    #[test]
1535    fn test_edge_flip_preserves_vertex_count() {
1536        let mesh = simple_quad_mesh();
1537        let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1538        let mut tris = mesh.indices.clone();
1539        let n_verts_before = verts.len();
1540        flip_edges_for_quality(&mut tris, &verts);
1541        assert_eq!(verts.len(), n_verts_before);
1542        let _ = verts; // suppress unused warning
1543    }
1544
1545    #[test]
1546    fn test_edge_flip_preserves_face_count() {
1547        let mesh = simple_quad_mesh();
1548        let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1549        let mut tris = mesh.indices.clone();
1550        let n_tris_before = tris.len();
1551        flip_edges_for_quality(&mut tris, &verts);
1552        assert_eq!(tris.len(), n_tris_before);
1553    }
1554
1555    // ── Quality metric tests ─────────────────────────────────────────────────
1556
1557    #[test]
1558    fn test_mesh_quality_equilateral() {
1559        // Equilateral triangle should have quality = 1
1560        let s = 1.0_f64;
1561        let h = s * (3.0_f64.sqrt()) / 2.0;
1562        let verts = vec![[0.0_f64, 0.0, 0.0], [s, 0.0, 0.0], [s / 2.0, h, 0.0]];
1563        let tris = vec![[0usize, 1, 2]];
1564        let quality = mesh_quality_min_angle(&verts, &tris);
1565        // All angles = 60° → quality = 1.0
1566        assert!(quality > 0.9, "equilateral triangle quality={}", quality);
1567    }
1568
1569    #[test]
1570    fn test_mesh_quality_degenerate() {
1571        // Degenerate (flat) triangle has low quality
1572        let verts = vec![
1573            [0.0_f64, 0.0, 0.0],
1574            [1.0, 0.0, 0.0],
1575            [100.0, 0.0, 0.0], // collinear
1576        ];
1577        let tris = vec![[0usize, 1, 2]];
1578        let quality = mesh_quality_min_angle(&verts, &tris);
1579        assert!(
1580            quality < 0.5,
1581            "degenerate triangle quality should be low, got {}",
1582            quality
1583        );
1584    }
1585
1586    // ── Tangent Laplacian smoothing tests ─────────────────────────────────────
1587
1588    #[test]
1589    fn test_tangent_smooth_returns_mesh() {
1590        let mesh = simple_quad_mesh();
1591        let result = tangent_laplacian_smooth(&mesh, 3);
1592        assert_eq!(result.vertices.len(), mesh.vertices.len());
1593        assert_eq!(result.indices.len(), mesh.indices.len());
1594    }
1595
1596    // ── Catmull-Clark double subdivision ─────────────────────────────────────
1597
1598    #[test]
1599    fn test_catmull_clark_double_subdivision() {
1600        let verts: Vec<[f64; 3]> = vec![
1601            [0.0, 0.0, 0.0],
1602            [1.0, 0.0, 0.0],
1603            [1.0, 1.0, 0.0],
1604            [0.0, 1.0, 0.0],
1605        ];
1606        let quads: Vec<[usize; 4]> = vec![[0, 1, 2, 3]];
1607        let (v1, q1) = CatmullClark::subdivide_quad_mesh(&verts, &quads);
1608        let (_, q2) = CatmullClark::subdivide_quad_mesh(&v1, &q1);
1609        assert_eq!(q2.len(), quads.len() * 16); // 1 * 4 * 4
1610    }
1611
1612    // ── Laplacian smoothing tests ─────────────────────────────────────────────
1613
1614    #[test]
1615    fn test_laplacian_smooth_moves_vertices() {
1616        let mesh = simple_quad_mesh();
1617        let result = laplacian_smooth(&mesh, 3, 0.5);
1618        // Vertices should change after smoothing
1619        let orig_sum: f64 = mesh.vertices.iter().map(|v| v.norm()).sum();
1620        let new_sum: f64 = result.vertices.iter().map(|v| v.norm()).sum();
1621        // After smoothing a non-symmetric mesh, positions should change
1622        let _ = (orig_sum, new_sum);
1623        assert_eq!(result.vertices.len(), mesh.vertices.len());
1624    }
1625
1626    #[test]
1627    fn test_laplacian_smooth_zero_iterations_unchanged() {
1628        let mesh = simple_quad_mesh();
1629        let result = laplacian_smooth(&mesh, 0, 0.5);
1630        for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
1631            assert!((a.x - b.x).abs() < 1e-12);
1632            assert!((a.y - b.y).abs() < 1e-12);
1633            assert!((a.z - b.z).abs() < 1e-12);
1634        }
1635    }
1636
1637    #[test]
1638    fn test_laplacian_smooth_preserves_face_count() {
1639        let mesh = simple_quad_mesh();
1640        let result = laplacian_smooth(&mesh, 5, 0.3);
1641        assert_eq!(result.indices.len(), mesh.indices.len());
1642    }
1643
1644    // ── Edge flip Delaunay tests ──────────────────────────────────────────────
1645
1646    #[test]
1647    fn test_delaunay_flip_preserves_vertices() {
1648        let mesh = simple_quad_mesh();
1649        let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1650        let mut tris = mesh.indices.clone();
1651        let before = verts.len();
1652        delaunay_edge_flip(&mut tris, &verts);
1653        assert_eq!(verts.len(), before);
1654    }
1655
1656    #[test]
1657    fn test_delaunay_flip_preserves_face_count() {
1658        let mesh = simple_quad_mesh();
1659        let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1660        let mut tris = mesh.indices.clone();
1661        let before = tris.len();
1662        delaunay_edge_flip(&mut tris, &verts);
1663        assert_eq!(tris.len(), before);
1664    }
1665
1666    // ── Coarsening tests ──────────────────────────────────────────────────────
1667
1668    #[test]
1669    fn test_coarsen_reduces_vertex_count() {
1670        let mesh = simple_quad_mesh();
1671        let result = coarsen_mesh(&mesh, 0.8); // target edge length > current
1672        // Coarsening should reduce or maintain vertex count
1673        assert!(result.vertices.len() <= mesh.vertices.len() + 1); // allow 1 for implementation
1674    }
1675
1676    #[test]
1677    fn test_coarsen_no_degenerate_faces() {
1678        let mesh = simple_quad_mesh();
1679        let result = coarsen_mesh(&mesh, 0.5);
1680        let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1681        for tri in &result.indices {
1682            assert!(
1683                tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2],
1684                "degenerate face {:?}",
1685                tri
1686            );
1687            // Ensure indices are in bounds
1688            assert!(tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len());
1689        }
1690    }
1691
1692    // ── Adaptive refinement by curvature ─────────────────────────────────────
1693
1694    #[test]
1695    fn test_adaptive_refine_increases_faces_for_curved_mesh() {
1696        // Build a simple curved mesh
1697        let mesh = simple_quad_mesh();
1698        let result = adaptive_refine_by_curvature(&mesh, 0.01); // low threshold → refine more
1699        // Should produce more faces or same
1700        assert!(result.indices.len() >= mesh.indices.len());
1701    }
1702
1703    #[test]
1704    fn test_adaptive_refine_high_threshold_no_change() {
1705        let mesh = simple_quad_mesh();
1706        let result = adaptive_refine_by_curvature(&mesh, 1000.0); // high threshold → no split
1707        // For a flat mesh with high threshold, no splits expected
1708        assert_eq!(result.indices.len(), mesh.indices.len());
1709    }
1710
1711    // ── Mesh decimation / vertex clustering ───────────────────────────────────
1712
1713    #[test]
1714    fn test_vertex_clustering_reduces_count() {
1715        // Build a finer mesh first
1716        let mesh = simple_quad_mesh();
1717        let sub = LoopSubdivision::subdivide(&mesh);
1718        let coarse = vertex_clustering_decimate(&sub, 2); // 2 cells per axis
1719        assert!(
1720            coarse.vertices.len() < sub.vertices.len(),
1721            "decimation should reduce vertex count"
1722        );
1723    }
1724
1725    #[test]
1726    fn test_vertex_clustering_single_cell_merges_all() {
1727        let mesh = simple_quad_mesh();
1728        let result = vertex_clustering_decimate(&mesh, 1); // all verts in one cell
1729        // All vertices merged → probably 1 vertex left, no valid faces
1730        assert!(result.vertices.len() <= mesh.vertices.len());
1731    }
1732
1733    // ── Saliency-weighted remeshing ───────────────────────────────────────────
1734
1735    #[test]
1736    fn test_saliency_remesh_returns_valid_mesh() {
1737        // Use a subdivided mesh so isotropic remeshing has enough faces to work with
1738        let mesh = simple_quad_mesh();
1739        let sub = LoopSubdivision::subdivide(&mesh);
1740        let saliency = vec![0.5_f64; sub.vertices.len()];
1741        let result = saliency_weighted_remesh(&sub, &saliency, 1);
1742        assert!(!result.vertices.is_empty());
1743        // Indices may be empty if all edges collapse, just check no panic
1744        let _ = result.indices.len();
1745    }
1746
1747    #[test]
1748    fn test_saliency_remesh_preserves_approximate_vertex_count() {
1749        let mesh = simple_quad_mesh();
1750        let saliency = vec![0.5_f64; mesh.vertices.len()];
1751        let result = saliency_weighted_remesh(&mesh, &saliency, 1);
1752        // Should not catastrophically change size
1753        assert!(result.vertices.len() <= mesh.vertices.len() * 10);
1754    }
1755
1756    // ── Mesh quality after operations ─────────────────────────────────────────
1757
1758    #[test]
1759    fn test_mesh_quality_after_laplacian_smooth() {
1760        let mesh = simple_quad_mesh();
1761        let smoothed = laplacian_smooth(&mesh, 5, 0.5);
1762        let verts: Vec<[f64; 3]> = smoothed.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1763        let q = mesh_quality_min_angle(&verts, &smoothed.indices);
1764        assert!((0.0..=1.0 + 1e-6).contains(&q), "quality out of range: {q}");
1765    }
1766
1767    #[test]
1768    fn test_aspect_ratio_equilateral() {
1769        let s = 1.0_f64;
1770        let h = s * 3.0_f64.sqrt() / 2.0;
1771        let verts = vec![[0.0_f64, 0.0, 0.0], [s, 0.0, 0.0], [s / 2.0, h, 0.0]];
1772        let tris = vec![[0usize, 1, 2]];
1773        let ar = mesh_aspect_ratio_avg(&verts, &tris);
1774        // Equilateral: all edges = 1, semi-perimeter s=1.5, area=sqrt(3)/4
1775        // inradius = area/s = (sqrt(3)/4)/1.5 = sqrt(3)/6
1776        // longest edge / (2 * inradius) = 1 / (2 * sqrt(3)/6) = 1 / (sqrt(3)/3) = sqrt(3) ≈ 1.732
1777        assert!(
1778            (ar - 3.0_f64.sqrt()).abs() < 0.01,
1779            "equilateral ar should be sqrt(3)≈1.732, got {ar}"
1780        );
1781    }
1782
1783    #[test]
1784    fn test_collapse_edge_reduces_vertex_usage() {
1785        let mesh = simple_quad_mesh();
1786        let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1787        let mut tris = mesh.indices.clone();
1788        let before_faces = tris.len();
1789        collapse_edge(&mut verts, &mut tris, 0, 1);
1790        // After collapsing edge 0-1, faces containing both should be removed
1791        assert!(tris.len() <= before_faces);
1792    }
1793
1794    #[test]
1795    fn test_loop_subdivision_vertex_positions_reasonable() {
1796        let mesh = simple_quad_mesh();
1797        let sub = LoopSubdivision::subdivide(&mesh);
1798        // All vertices should be in a finite range
1799        for v in &sub.vertices {
1800            assert!(v.x.is_finite() && v.y.is_finite() && v.z.is_finite());
1801        }
1802    }
1803
1804    #[test]
1805    fn test_isotropic_remesh_zero_iterations_same() {
1806        let mesh = simple_quad_mesh();
1807        let result = isotropic_remesh(&mesh, 0.5, 0);
1808        // Zero iterations: same as input
1809        assert_eq!(result.vertices.len(), mesh.vertices.len());
1810        assert_eq!(result.indices.len(), mesh.indices.len());
1811    }
1812
1813    #[test]
1814    fn test_delaunay_flip_on_subdivided_mesh() {
1815        let mesh = simple_quad_mesh();
1816        let sub = LoopSubdivision::subdivide(&mesh);
1817        let verts: Vec<[f64; 3]> = sub.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1818        let mut tris = sub.indices.clone();
1819        delaunay_edge_flip(&mut tris, &verts);
1820        // Should not produce invalid indices
1821        for tri in &tris {
1822            assert!(tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len());
1823        }
1824    }
1825
1826    #[test]
1827    fn test_coarsen_mesh_returns_valid_faces() {
1828        let mesh = simple_quad_mesh();
1829        let result = coarsen_mesh(&mesh, 1.5);
1830        let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1831        for tri in &result.indices {
1832            assert!(
1833                tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len(),
1834                "face {:?} has out-of-bounds index (n_verts={})",
1835                tri,
1836                verts.len()
1837            );
1838        }
1839    }
1840
1841    #[test]
1842    fn test_adaptive_refine_no_degenerate_faces() {
1843        let mesh = simple_quad_mesh();
1844        let result = adaptive_refine_by_curvature(&mesh, 0.1);
1845        let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1846        for tri in &result.indices {
1847            assert!(tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2]);
1848            assert!(tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len());
1849        }
1850    }
1851}