Skip to main content

oxiphysics_geometry/
remesh.rs

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