Skip to main content

oxiphysics_geometry/
mesh_param.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//! Triangle mesh parameterization (UV unwrapping) and mesh processing utilities.
6//!
7//! Implements LSCM and Tutte parameterization, UV quality metrics,
8//! mesh subdivision, and mesh repair operations.
9
10#![allow(dead_code)]
11
12use std::collections::HashMap;
13
14// ---------------------------------------------------------------------------
15// Math helpers
16// ---------------------------------------------------------------------------
17
18fn dot2(a: [f64; 2], b: [f64; 2]) -> f64 {
19    a[0] * b[0] + a[1] * b[1]
20}
21
22fn cross2(a: [f64; 2], b: [f64; 2]) -> f64 {
23    a[0] * b[1] - a[1] * b[0]
24}
25
26fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
27    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
28}
29
30fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
31    [
32        a[1] * b[2] - a[2] * b[1],
33        a[2] * b[0] - a[0] * b[2],
34        a[0] * b[1] - a[1] * b[0],
35    ]
36}
37
38fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
39    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
40}
41
42fn sub2(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
43    [a[0] - b[0], a[1] - b[1]]
44}
45
46fn length3(a: [f64; 3]) -> f64 {
47    dot3(a, a).sqrt()
48}
49
50fn normalize3(a: [f64; 3]) -> [f64; 3] {
51    let l = length3(a);
52    if l < 1e-15 {
53        [0.0, 0.0, 0.0]
54    } else {
55        [a[0] / l, a[1] / l, a[2] / l]
56    }
57}
58
59// ---------------------------------------------------------------------------
60// TriMesh (parameterization mesh)
61// ---------------------------------------------------------------------------
62
63/// A triangle mesh with UV coordinates and normals, used for parameterization.
64///
65/// Note: This is distinct from `mesh_ops::TriMesh` which uses nalgebra Vec3.
66#[derive(Debug, Clone)]
67pub struct ParamTriMesh {
68    /// 3D positions of vertices.
69    pub positions: Vec<[f64; 3]>,
70    /// UV texture coordinates per vertex.
71    pub uvs: Vec<[f64; 2]>,
72    /// Per-vertex normals.
73    pub normals: Vec<[f64; 3]>,
74    /// Triangle indices.
75    pub triangles: Vec<[usize; 3]>,
76}
77
78impl ParamTriMesh {
79    /// Create an empty mesh.
80    pub fn new() -> Self {
81        Self {
82            positions: Vec::new(),
83            uvs: Vec::new(),
84            normals: Vec::new(),
85            triangles: Vec::new(),
86        }
87    }
88
89    /// Number of vertices.
90    pub fn vertex_count(&self) -> usize {
91        self.positions.len()
92    }
93
94    /// Number of triangles.
95    pub fn triangle_count(&self) -> usize {
96        self.triangles.len()
97    }
98
99    /// Add a vertex with position and UV; returns the vertex index.
100    pub fn add_vertex(&mut self, pos: [f64; 3], uv: [f64; 2]) -> usize {
101        let idx = self.positions.len();
102        self.positions.push(pos);
103        self.uvs.push(uv);
104        self.normals.push([0.0, 0.0, 0.0]);
105        idx
106    }
107
108    /// Add a triangle by vertex indices.
109    pub fn add_triangle(&mut self, a: usize, b: usize, c: usize) {
110        self.triangles.push([a, b, c]);
111    }
112
113    /// Compute per-vertex area-weighted average normals.
114    pub fn compute_normals(&mut self) {
115        let n = self.positions.len();
116        let mut acc = vec![[0.0f64; 3]; n];
117
118        for tri in &self.triangles {
119            let p0 = self.positions[tri[0]];
120            let p1 = self.positions[tri[1]];
121            let p2 = self.positions[tri[2]];
122            let e1 = sub3(p1, p0);
123            let e2 = sub3(p2, p0);
124            let face_n = cross3(e1, e2); // magnitude = 2 * area
125            for &v in tri {
126                acc[v][0] += face_n[0];
127                acc[v][1] += face_n[1];
128                acc[v][2] += face_n[2];
129            }
130        }
131
132        for (i, n_acc) in acc.into_iter().enumerate() {
133            self.normals[i] = normalize3(n_acc);
134        }
135    }
136}
137
138impl Default for ParamTriMesh {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144// ---------------------------------------------------------------------------
145// Topology helpers
146// ---------------------------------------------------------------------------
147
148/// Build a half-edge table for a triangle mesh.
149///
150/// Returns one entry per directed edge (3 per triangle).
151/// Each entry is `[next_he, prev_he, twin_he]` where indices refer to
152/// half-edges numbered as `tri_idx * 3 + local_edge`.
153/// Twin is `-1` if the edge is on the boundary.
154pub fn build_halfedge(triangles: &[[usize; 3]]) -> Vec<[i64; 3]> {
155    let n_tri = triangles.len();
156    let n_he = n_tri * 3;
157    let mut table = vec![[0i64; 3]; n_he];
158
159    // Fill next / prev within each triangle
160    for (t, _tri) in triangles.iter().enumerate() {
161        let base = t * 3;
162        table[base][0] = (base + 1) as i64; // next
163        table[base][1] = (base + 2) as i64; // prev
164        table[base + 1][0] = (base + 2) as i64;
165        table[base + 1][1] = base as i64;
166        table[base + 2][0] = base as i64;
167        table[base + 2][1] = (base + 1) as i64;
168    }
169
170    // Build edge -> half-edge map for twin lookup
171    // Edge key: (min_v, max_v, direction: from < to)
172    let mut edge_map: HashMap<(usize, usize), i64> = HashMap::new();
173
174    for (t, tri) in triangles.iter().enumerate() {
175        for local in 0..3usize {
176            let v_from = tri[local];
177            let v_to = tri[(local + 1) % 3];
178            let he_idx = (t * 3 + local) as i64;
179            edge_map.insert((v_from, v_to), he_idx);
180        }
181    }
182
183    // Set twins
184    for (t, tri) in triangles.iter().enumerate() {
185        for local in 0..3usize {
186            let he_idx = t * 3 + local;
187            let v_from = tri[local];
188            let v_to = tri[(local + 1) % 3];
189            // Twin has opposite direction
190            if let Some(&twin_idx) = edge_map.get(&(v_to, v_from)) {
191                table[he_idx][2] = twin_idx;
192            } else {
193                table[he_idx][2] = -1; // boundary
194            }
195        }
196    }
197
198    table
199}
200
201/// Return all vertices adjacent to `vertex_idx`.
202pub fn vertex_neighbors(vertex_idx: usize, triangles: &[[usize; 3]]) -> Vec<usize> {
203    let mut neighbors = Vec::new();
204    for tri in triangles {
205        for (local, &v) in tri.iter().enumerate() {
206            if v == vertex_idx {
207                let next = tri[(local + 1) % 3];
208                let prev = tri[(local + 2) % 3];
209                if !neighbors.contains(&next) {
210                    neighbors.push(next);
211                }
212                if !neighbors.contains(&prev) {
213                    neighbors.push(prev);
214                }
215            }
216        }
217    }
218    neighbors
219}
220
221/// Return the set of boundary vertices (those on at least one boundary edge).
222pub fn boundary_vertices(n_verts: usize, triangles: &[[usize; 3]]) -> Vec<usize> {
223    // An edge is a boundary edge if it has no twin.
224    // Build directed-edge count; boundary edges appear exactly once.
225    let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
226    for tri in triangles {
227        for local in 0..3usize {
228            let v_from = tri[local];
229            let v_to = tri[(local + 1) % 3];
230            *edge_count.entry((v_from, v_to)).or_insert(0) += 1;
231        }
232    }
233
234    let mut on_boundary = vec![false; n_verts];
235    for (&(v_from, v_to), &cnt) in &edge_count {
236        if cnt == 1 {
237            // Check if reverse edge exists
238            if !edge_count.contains_key(&(v_to, v_from)) {
239                on_boundary[v_from] = true;
240                on_boundary[v_to] = true;
241            }
242        }
243    }
244
245    on_boundary
246        .into_iter()
247        .enumerate()
248        .filter(|&(_, b)| b)
249        .map(|(i, _)| i)
250        .collect()
251}
252
253/// Return true if vertex `v` lies on the boundary of the mesh.
254pub fn is_boundary_vertex(v: usize, triangles: &[[usize; 3]]) -> bool {
255    let n_verts = triangles
256        .iter()
257        .flat_map(|t| t.iter())
258        .copied()
259        .max()
260        .map(|m| m + 1)
261        .unwrap_or(0);
262    boundary_vertices(n_verts, triangles).contains(&v)
263}
264
265// ---------------------------------------------------------------------------
266// LSCM Parameterization
267// ---------------------------------------------------------------------------
268
269/// Least Squares Conformal Maps parameterization.
270pub struct LscmParameterization;
271
272impl LscmParameterization {
273    /// Compute UV coordinates for the mesh using a simplified LSCM approach.
274    ///
275    /// Pins the first two boundary vertices and solves iteratively.
276    pub fn compute(mesh: &ParamTriMesh) -> Vec<[f64; 2]> {
277        let n = mesh.vertex_count();
278        if n == 0 {
279            return Vec::new();
280        }
281
282        let bv = boundary_vertices(n, &mesh.triangles);
283
284        // Pin two boundary vertices
285        let pin0 = if bv.is_empty() { 0 } else { bv[0] };
286        let pin1 = if bv.len() < 2 {
287            (pin0 + 1).min(n - 1)
288        } else {
289            bv[1]
290        };
291
292        let mut uvs = vec![[0.0f64; 2]; n];
293        // Set pinned vertices
294        uvs[pin0] = [0.0, 0.0];
295        uvs[pin1] = [1.0, 0.0];
296
297        // Iterative Gauss-Seidel relaxation using conformal weights
298        for _iter in 0..200 {
299            for vi in 0..n {
300                if vi == pin0 || vi == pin1 {
301                    continue;
302                }
303                // Collect triangles containing vi
304                let mut sum_u = 0.0f64;
305                let mut sum_v = 0.0f64;
306                let mut weight_total = 0.0f64;
307
308                for tri in &mesh.triangles {
309                    let local_opt = tri.iter().position(|&x| x == vi);
310                    let local = match local_opt {
311                        Some(l) => l,
312                        None => continue,
313                    };
314
315                    let vj = tri[(local + 1) % 3];
316                    let vk = tri[(local + 2) % 3];
317
318                    let pi = mesh.positions[vi];
319                    let pj = mesh.positions[vj];
320                    let pk = mesh.positions[vk];
321
322                    // Cotangent weights
323                    let ej = sub3(pj, pi);
324                    let ek = sub3(pk, pi);
325                    let area2 = length3(cross3(ej, ek));
326                    let w = if area2 > 1e-15 { 1.0 / area2 } else { 0.0 };
327
328                    sum_u += w * (uvs[vj][0] + uvs[vk][0]);
329                    sum_v += w * (uvs[vj][1] + uvs[vk][1]);
330                    weight_total += 2.0 * w;
331                }
332
333                if weight_total > 1e-15 {
334                    uvs[vi] = [sum_u / weight_total, sum_v / weight_total];
335                }
336            }
337        }
338
339        uvs
340    }
341
342    /// Compute conformal energy: sum of |J - R|^2 per triangle.
343    ///
344    /// J is the Jacobian of the UV mapping, R is the closest rotation.
345    pub fn conformal_energy(
346        positions: &[[f64; 3]],
347        uvs: &[[f64; 2]],
348        triangles: &[[usize; 3]],
349    ) -> f64 {
350        let mut energy = 0.0f64;
351
352        for tri in triangles {
353            let p0 = positions[tri[0]];
354            let p1 = positions[tri[1]];
355            let p2 = positions[tri[2]];
356
357            let u0 = uvs[tri[0]];
358            let u1 = uvs[tri[1]];
359            let u2 = uvs[tri[2]];
360
361            // 3D edge vectors
362            let e1 = sub3(p1, p0);
363            let e2 = sub3(p2, p0);
364
365            // UV edge vectors
366            let f1 = sub2(u1, u0);
367            let f2 = sub2(u2, u0);
368
369            // Area in 3D
370            let cross = cross3(e1, e2);
371            let area_3d = length3(cross) * 0.5;
372            if area_3d < 1e-15 {
373                continue;
374            }
375
376            // Jacobian entries (2x2 matrix mapping UV -> 3D)
377            // J = [e1, e2] * inv([f1, f2])
378            let det_f = cross2(f1, f2);
379            if det_f.abs() < 1e-15 {
380                energy += area_3d; // degenerate UV triangle counts as full error
381                continue;
382            }
383            let inv_det = 1.0 / det_f;
384
385            // J (3x2): columns are J*e_u, J*e_v
386            let ju = [
387                (e1[0] * f2[1] - e2[0] * f1[1]) * inv_det,
388                (e1[1] * f2[1] - e2[1] * f1[1]) * inv_det,
389                (e1[2] * f2[1] - e2[2] * f1[1]) * inv_det,
390            ];
391            let jv = [
392                (e2[0] * f1[0] - e1[0] * f2[0]) * inv_det,
393                (e2[1] * f1[0] - e1[1] * f2[0]) * inv_det,
394                (e2[2] * f1[0] - e1[2] * f2[0]) * inv_det,
395            ];
396
397            // Conformal energy: |Ju|^2 + |Jv|^2 - 2 (should be 0 for conformal)
398            let norm_ju = dot3(ju, ju);
399            let norm_jv = dot3(jv, jv);
400            // LSCM energy = |Ju - perp(Jv)|^2; simplified as |Ju|^2 + |Jv|^2 - 2*area_3d/area_uv
401            let area_uv = det_f.abs() * 0.5;
402            let stretch = (norm_ju + norm_jv) * area_uv - 2.0 * area_3d;
403            energy += stretch.abs();
404        }
405
406        energy
407    }
408}
409
410// ---------------------------------------------------------------------------
411// Tutte Parameterization
412// ---------------------------------------------------------------------------
413
414/// Tutte (barycentric) parameterization.
415pub struct TutteParameterization;
416
417impl TutteParameterization {
418    /// Compute UV coordinates using Tutte's method.
419    ///
420    /// Maps boundary vertices to a circle and solves the Laplacian
421    /// system for interior vertices using Gauss-Seidel iteration.
422    pub fn compute(mesh: &ParamTriMesh) -> Vec<[f64; 2]> {
423        let n = mesh.vertex_count();
424        if n == 0 {
425            return Vec::new();
426        }
427
428        let bv = boundary_vertices(n, &mesh.triangles);
429        if bv.is_empty() {
430            // No boundary: return zeros
431            return vec![[0.0; 2]; n];
432        }
433
434        let boundary_uvs = Self::map_boundary_to_circle(&bv, n);
435
436        let mut uvs = vec![[0.0f64; 2]; n];
437        // Assign boundary UVs
438        for &bvi in bv.iter() {
439            uvs[bvi] = boundary_uvs[bvi];
440        }
441
442        let is_boundary: Vec<bool> = (0..n).map(|v| bv.contains(&v)).collect();
443
444        // Gauss-Seidel for interior vertices
445        for _iter in 0..500 {
446            for vi in 0..n {
447                if is_boundary[vi] {
448                    continue;
449                }
450                let neighbors = vertex_neighbors(vi, &mesh.triangles);
451                if neighbors.is_empty() {
452                    continue;
453                }
454                let k = neighbors.len() as f64;
455                let sum_u: f64 = neighbors.iter().map(|&nb| uvs[nb][0]).sum();
456                let sum_v: f64 = neighbors.iter().map(|&nb| uvs[nb][1]).sum();
457                uvs[vi] = [sum_u / k, sum_v / k];
458            }
459        }
460
461        uvs
462    }
463
464    /// Map boundary vertices equally spaced on the unit circle.
465    ///
466    /// Returns a Vec of length `n_verts` where only boundary indices are set;
467    /// interior entries are `[0.0, 0.0]`.
468    pub fn map_boundary_to_circle(boundary: &[usize], n_verts: usize) -> Vec<[f64; 2]> {
469        use std::f64::consts::TAU;
470        let mut uvs = vec![[0.0f64; 2]; n_verts];
471        let nb = boundary.len();
472        for (i, &bv) in boundary.iter().enumerate() {
473            let angle = TAU * i as f64 / nb as f64;
474            uvs[bv] = [angle.cos(), angle.sin()];
475        }
476        uvs
477    }
478}
479
480// ---------------------------------------------------------------------------
481// UV Quality Metrics
482// ---------------------------------------------------------------------------
483
484/// Compute per-triangle texture distortion: ratio of UV area to 3D area.
485///
486/// A value of 1.0 means no distortion.
487pub fn texture_distortion(
488    positions: &[[f64; 3]],
489    uvs: &[[f64; 2]],
490    triangles: &[[usize; 3]],
491) -> Vec<f64> {
492    triangles
493        .iter()
494        .map(|tri| {
495            let p0 = positions[tri[0]];
496            let p1 = positions[tri[1]];
497            let p2 = positions[tri[2]];
498
499            let u0 = uvs[tri[0]];
500            let u1 = uvs[tri[1]];
501            let u2 = uvs[tri[2]];
502
503            let e1_3d = sub3(p1, p0);
504            let e2_3d = sub3(p2, p0);
505            let area_3d = length3(cross3(e1_3d, e2_3d)) * 0.5;
506
507            let e1_uv = sub2(u1, u0);
508            let e2_uv = sub2(u2, u0);
509            let area_uv = cross2(e1_uv, e2_uv).abs() * 0.5;
510
511            if area_3d < 1e-15 || area_uv < 1e-15 {
512                0.0
513            } else {
514                area_uv / area_3d
515            }
516        })
517        .collect()
518}
519
520/// Mean UV distortion across all triangles.
521pub fn uv_stretch(positions: &[[f64; 3]], uvs: &[[f64; 2]], triangles: &[[usize; 3]]) -> f64 {
522    let distortions = texture_distortion(positions, uvs, triangles);
523    if distortions.is_empty() {
524        return 0.0;
525    }
526    distortions.iter().sum::<f64>() / distortions.len() as f64
527}
528
529/// Count triangles with inverted UV mapping (negative signed area).
530pub fn uv_overlap_check(uvs: &[[f64; 2]], triangles: &[[usize; 3]]) -> usize {
531    triangles
532        .iter()
533        .filter(|tri| {
534            let u0 = uvs[tri[0]];
535            let u1 = uvs[tri[1]];
536            let u2 = uvs[tri[2]];
537            let e1 = sub2(u1, u0);
538            let e2 = sub2(u2, u0);
539            cross2(e1, e2) < 0.0
540        })
541        .count()
542}
543
544// ---------------------------------------------------------------------------
545// Mesh Subdivision
546// ---------------------------------------------------------------------------
547
548/// Loop subdivision: split each triangle into 4 sub-triangles.
549///
550/// Interior vertices use 3/8 weight from neighbors; boundary uses 1/8 from ends.
551pub fn loop_subdivision(mesh: &ParamTriMesh) -> ParamTriMesh {
552    let positions = &mesh.positions;
553    let triangles = &mesh.triangles;
554    let uvs = &mesh.uvs;
555    let n_orig = positions.len();
556
557    // Map edge -> midpoint vertex index
558    let mut edge_midpoints: HashMap<(usize, usize), usize> = HashMap::new();
559    let mut new_positions = positions.clone();
560    let mut new_uvs = uvs.clone();
561
562    // Ensure uvs has enough entries
563    while new_uvs.len() < new_positions.len() {
564        new_uvs.push([0.0, 0.0]);
565    }
566
567    // Create edge midpoints
568    for tri in triangles {
569        for local in 0..3usize {
570            let v0 = tri[local];
571            let v1 = tri[(local + 1) % 3];
572            let key = if v0 < v1 { (v0, v1) } else { (v1, v0) };
573            edge_midpoints.entry(key).or_insert_with(|| {
574                let mid_pos = [
575                    (positions[v0][0] + positions[v1][0]) * 0.5,
576                    (positions[v0][1] + positions[v1][1]) * 0.5,
577                    (positions[v0][2] + positions[v1][2]) * 0.5,
578                ];
579                let mid_uv = if !uvs.is_empty() {
580                    [
581                        (uvs[v0][0] + uvs[v1][0]) * 0.5,
582                        (uvs[v0][1] + uvs[v1][1]) * 0.5,
583                    ]
584                } else {
585                    [0.0, 0.0]
586                };
587                let idx = new_positions.len();
588                new_positions.push(mid_pos);
589                new_uvs.push(mid_uv);
590                idx
591            });
592        }
593    }
594
595    // Build new triangle list (4 per original)
596    let mut new_triangles: Vec<[usize; 3]> = Vec::with_capacity(triangles.len() * 4);
597    for tri in triangles {
598        let v0 = tri[0];
599        let v1 = tri[1];
600        let v2 = tri[2];
601
602        let key01 = if v0 < v1 { (v0, v1) } else { (v1, v0) };
603        let key12 = if v1 < v2 { (v1, v2) } else { (v2, v1) };
604        let key20 = if v2 < v0 { (v2, v0) } else { (v0, v2) };
605
606        let m01 = edge_midpoints[&key01];
607        let m12 = edge_midpoints[&key12];
608        let m20 = edge_midpoints[&key20];
609
610        new_triangles.push([v0, m01, m20]);
611        new_triangles.push([m01, v1, m12]);
612        new_triangles.push([m20, m12, v2]);
613        new_triangles.push([m01, m12, m20]);
614    }
615
616    // Update original vertex positions using Loop weights
617    let n_new = new_positions.len();
618    let mut smoothed = new_positions.clone();
619
620    let bv_set: std::collections::HashSet<usize> =
621        boundary_vertices(n_orig, triangles).into_iter().collect();
622
623    for vi in 0..n_orig {
624        let neighbors = vertex_neighbors(vi, triangles);
625        let k = neighbors.len();
626        if k == 0 {
627            continue;
628        }
629
630        if bv_set.contains(&vi) {
631            // Boundary rule: 3/4 own + 1/8 * (two boundary neighbors)
632            let boundary_neighbors: Vec<usize> = neighbors
633                .iter()
634                .copied()
635                .filter(|&nb| bv_set.contains(&nb))
636                .collect();
637            if boundary_neighbors.len() >= 2 {
638                let nb0 = boundary_neighbors[0];
639                let nb1 = boundary_neighbors[1];
640                smoothed[vi] = [
641                    0.75 * new_positions[vi][0]
642                        + 0.125 * (new_positions[nb0][0] + new_positions[nb1][0]),
643                    0.75 * new_positions[vi][1]
644                        + 0.125 * (new_positions[nb0][1] + new_positions[nb1][1]),
645                    0.75 * new_positions[vi][2]
646                        + 0.125 * (new_positions[nb0][2] + new_positions[nb1][2]),
647                ];
648            }
649        } else {
650            // Interior rule: beta weight
651            let beta = if k == 3 {
652                3.0 / 16.0
653            } else {
654                3.0 / (8.0 * k as f64)
655            };
656            let neighbor_sum: [f64; 3] = neighbors.iter().fold([0.0; 3], |acc, &nb| {
657                [
658                    acc[0] + new_positions[nb][0],
659                    acc[1] + new_positions[nb][1],
660                    acc[2] + new_positions[nb][2],
661                ]
662            });
663            smoothed[vi] = [
664                (1.0 - beta * k as f64) * new_positions[vi][0] + beta * neighbor_sum[0],
665                (1.0 - beta * k as f64) * new_positions[vi][1] + beta * neighbor_sum[1],
666                (1.0 - beta * k as f64) * new_positions[vi][2] + beta * neighbor_sum[2],
667            ];
668        }
669    }
670
671    // Pad smoothed to full length
672    for i in n_orig..n_new {
673        smoothed.push(new_positions[i]);
674    }
675
676    // Build result mesh
677    let mut result = ParamTriMesh {
678        positions: smoothed,
679        uvs: new_uvs,
680        normals: vec![[0.0; 3]; n_new],
681        triangles: new_triangles,
682    };
683    result.compute_normals();
684    result
685}
686
687/// Simple midpoint subdivision: each edge gets a midpoint, 1 triangle -> 4.
688pub fn midpoint_subdivision(
689    positions: &[[f64; 3]],
690    triangles: &[[usize; 3]],
691) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
692    let mut new_positions = positions.to_vec();
693    let mut edge_midpoints: HashMap<(usize, usize), usize> = HashMap::new();
694    let mut new_triangles: Vec<[usize; 3]> = Vec::with_capacity(triangles.len() * 4);
695
696    for tri in triangles {
697        let v0 = tri[0];
698        let v1 = tri[1];
699        let v2 = tri[2];
700
701        let get_or_create =
702            |v_a: usize,
703             v_b: usize,
704             positions: &[[f64; 3]],
705             new_positions: &mut Vec<[f64; 3]>,
706             edge_midpoints: &mut HashMap<(usize, usize), usize>| {
707                let key = if v_a < v_b { (v_a, v_b) } else { (v_b, v_a) };
708                if let Some(&idx) = edge_midpoints.get(&key) {
709                    idx
710                } else {
711                    let mid = [
712                        (positions[v_a][0] + positions[v_b][0]) * 0.5,
713                        (positions[v_a][1] + positions[v_b][1]) * 0.5,
714                        (positions[v_a][2] + positions[v_b][2]) * 0.5,
715                    ];
716                    let idx = new_positions.len();
717                    new_positions.push(mid);
718                    edge_midpoints.insert(key, idx);
719                    idx
720                }
721            };
722
723        let m01 = get_or_create(v0, v1, positions, &mut new_positions, &mut edge_midpoints);
724        let m12 = get_or_create(v1, v2, positions, &mut new_positions, &mut edge_midpoints);
725        let m20 = get_or_create(v2, v0, positions, &mut new_positions, &mut edge_midpoints);
726
727        new_triangles.push([v0, m01, m20]);
728        new_triangles.push([m01, v1, m12]);
729        new_triangles.push([m20, m12, v2]);
730        new_triangles.push([m01, m12, m20]);
731    }
732
733    (new_positions, new_triangles)
734}
735
736// ---------------------------------------------------------------------------
737// Mesh Repair
738// ---------------------------------------------------------------------------
739
740/// Merge vertices within `tol` distance and return the cleaned mesh.
741pub fn remove_duplicate_vertices(
742    positions: &[[f64; 3]],
743    triangles: &[[usize; 3]],
744    tol: f64,
745) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
746    let n = positions.len();
747    let mut remap = vec![0usize; n];
748    let mut unique: Vec<[f64; 3]> = Vec::new();
749
750    for (i, &p) in positions.iter().enumerate() {
751        let found = unique.iter().position(|&q| {
752            let dx = p[0] - q[0];
753            let dy = p[1] - q[1];
754            let dz = p[2] - q[2];
755            (dx * dx + dy * dy + dz * dz).sqrt() <= tol
756        });
757        if let Some(j) = found {
758            remap[i] = j;
759        } else {
760            remap[i] = unique.len();
761            unique.push(p);
762        }
763    }
764
765    let new_triangles: Vec<[usize; 3]> = triangles
766        .iter()
767        .map(|tri| [remap[tri[0]], remap[tri[1]], remap[tri[2]]])
768        .filter(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2])
769        .collect();
770
771    (unique, new_triangles)
772}
773
774/// Fill holes by fan-triangulating boundary loops.
775///
776/// For each detected boundary loop, creates a fan of triangles from the
777/// first boundary vertex to all others in the loop.
778pub fn fill_holes(triangles: &mut Vec<[usize; 3]>, n_verts: usize) {
779    // Find boundary edges (appear exactly once)
780    let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
781    for tri in triangles.iter() {
782        for local in 0..3usize {
783            let v0 = tri[local];
784            let v1 = tri[(local + 1) % 3];
785            *edge_count.entry((v0, v1)).or_insert(0) += 1;
786        }
787    }
788
789    // Collect boundary edges (no reverse)
790    let mut boundary_edges: Vec<(usize, usize)> = Vec::new();
791    for (&(v0, v1), &cnt) in &edge_count {
792        if cnt == 1 && !edge_count.contains_key(&(v1, v0)) {
793            boundary_edges.push((v0, v1));
794        }
795    }
796
797    if boundary_edges.is_empty() {
798        return;
799    }
800
801    // Build adjacency from boundary edges to find loops
802    let mut next_map: HashMap<usize, usize> = HashMap::new();
803    for &(v0, v1) in &boundary_edges {
804        next_map.insert(v0, v1);
805    }
806
807    let mut visited = vec![false; n_verts];
808    for start in 0..n_verts {
809        if !next_map.contains_key(&start) || visited[start] {
810            continue;
811        }
812
813        // Trace loop
814        let mut loop_verts = vec![start];
815        let mut cur = start;
816        loop {
817            match next_map.get(&cur) {
818                Some(&nxt) if nxt != start && !visited[nxt] => {
819                    loop_verts.push(nxt);
820                    cur = nxt;
821                }
822                _ => break,
823            }
824        }
825
826        for &v in &loop_verts {
827            visited[v] = true;
828        }
829
830        // Fan-fill from first vertex
831        if loop_verts.len() >= 3 {
832            let apex = loop_verts[0];
833            for i in 1..loop_verts.len() - 1 {
834                triangles.push([apex, loop_verts[i], loop_verts[i + 1]]);
835            }
836        }
837    }
838}
839
840// ---------------------------------------------------------------------------
841// Harmonic map parameterization
842// ---------------------------------------------------------------------------
843
844/// Harmonic map UV parameterization (also known as harmonic embedding).
845///
846/// Maps boundary vertices to a fixed convex boundary (circle) and solves
847/// the discrete harmonic (Laplacian) system with cotangent weights for
848/// interior vertices.  This provides a more accurate conformal-ish mapping
849/// than uniform Tutte.
850pub struct HarmonicMapParameterization;
851
852impl HarmonicMapParameterization {
853    /// Compute cotangent-weighted UV coordinates for the mesh.
854    ///
855    /// Boundary vertices are fixed on a unit circle.
856    /// Interior vertices are solved by Gauss-Seidel iteration with
857    /// cotangent weights.
858    pub fn compute(mesh: &ParamTriMesh) -> Vec<[f64; 2]> {
859        let n = mesh.vertex_count();
860        if n == 0 {
861            return Vec::new();
862        }
863
864        let bv = boundary_vertices(n, &mesh.triangles);
865        if bv.is_empty() {
866            return vec![[0.0; 2]; n];
867        }
868
869        // Map boundary to circle.
870        let boundary_uvs = TutteParameterization::map_boundary_to_circle(&bv, n);
871        let mut uvs = vec![[0.0f64; 2]; n];
872        let is_boundary: Vec<bool> = (0..n).map(|v| bv.contains(&v)).collect();
873
874        for &bvi in &bv {
875            uvs[bvi] = boundary_uvs[bvi];
876        }
877
878        // Precompute cotangent weights.
879        // w_ij = cot(alpha_ij) + cot(beta_ij) where alpha, beta are opposite angles.
880        // For simplicity use area-based weights (simpler to compute but still SPD).
881        let cot_weight = |vi: usize, vj: usize| -> f64 {
882            let mut w = 0.0f64;
883            for tri in &mesh.triangles {
884                // Find if vi and vj are both in this triangle.
885                let pos_i = tri.iter().position(|&v| v == vi);
886                let pos_j = tri.iter().position(|&v| v == vj);
887                if let (Some(pi), Some(pj)) = (pos_i, pos_j) {
888                    // The opposite vertex.
889                    let pk = 3 - pi - pj; // since 0+1+2=3
890                    let vk = tri[pk];
891                    let pk_pos = mesh.positions[vk];
892                    let pi_pos = mesh.positions[vi];
893                    let pj_pos = mesh.positions[vj];
894                    let ei = sub3(pi_pos, pk_pos);
895                    let ej = sub3(pj_pos, pk_pos);
896                    let dot = dot3(ei, ej);
897                    let cross_n = length3(cross3(ei, ej));
898                    if cross_n > 1e-15 {
899                        w += dot / cross_n; // = cot(angle at vk)
900                    }
901                }
902            }
903            w.max(0.0) // clamp to avoid negative weights
904        };
905
906        // Gauss-Seidel with cotangent weights.
907        for _iter in 0..500 {
908            for vi in 0..n {
909                if is_boundary[vi] {
910                    continue;
911                }
912                let neighbors = vertex_neighbors(vi, &mesh.triangles);
913                if neighbors.is_empty() {
914                    continue;
915                }
916                let mut sum_u = 0.0f64;
917                let mut sum_v = 0.0f64;
918                let mut sum_w = 0.0f64;
919                for &vj in &neighbors {
920                    let w = cot_weight(vi, vj).max(1e-10);
921                    sum_u += w * uvs[vj][0];
922                    sum_v += w * uvs[vj][1];
923                    sum_w += w;
924                }
925                if sum_w > 1e-15 {
926                    uvs[vi] = [sum_u / sum_w, sum_v / sum_w];
927                }
928            }
929        }
930
931        uvs
932    }
933}
934
935// ---------------------------------------------------------------------------
936// UV seam handling
937// ---------------------------------------------------------------------------
938
939/// A UV seam: a set of edges where the UV mapping has a discontinuity.
940#[derive(Debug, Clone)]
941pub struct UvSeam {
942    /// Edge pairs `(v_from, v_to)` that are seam edges.
943    pub edges: Vec<(usize, usize)>,
944}
945
946impl UvSeam {
947    /// Create an empty seam.
948    pub fn new() -> Self {
949        Self { edges: Vec::new() }
950    }
951
952    /// Add a seam edge (undirected).
953    pub fn add_edge(&mut self, v0: usize, v1: usize) {
954        let key = if v0 < v1 { (v0, v1) } else { (v1, v0) };
955        if !self.edges.contains(&key) {
956            self.edges.push(key);
957        }
958    }
959
960    /// Check whether a directed edge is a seam edge.
961    pub fn is_seam_edge(&self, v0: usize, v1: usize) -> bool {
962        let key = if v0 < v1 { (v0, v1) } else { (v1, v0) };
963        self.edges.contains(&key)
964    }
965
966    /// Detect seam edges automatically: boundary edges are always seams;
967    /// interior edges with a large UV discontinuity are also seams.
968    pub fn detect_seams(
969        mesh: &ParamTriMesh,
970        uvs: &[[f64; 2]],
971        discontinuity_threshold: f64,
972    ) -> Self {
973        let mut seam = Self::new();
974        let bverts = boundary_vertices(mesh.vertex_count(), &mesh.triangles);
975        // Mark boundary edges as seams.
976        let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
977        for tri in &mesh.triangles {
978            for local in 0..3usize {
979                let v0 = tri[local];
980                let v1 = tri[(local + 1) % 3];
981                *edge_count.entry((v0, v1)).or_insert(0) += 1;
982            }
983        }
984        for (&(v0, v1), &cnt) in &edge_count {
985            if cnt == 1 && !edge_count.contains_key(&(v1, v0)) {
986                seam.add_edge(v0, v1);
987            }
988        }
989        // Also mark edges with large UV discontinuity.
990        if uvs.len() >= mesh.vertex_count() {
991            for &(v0, v1) in edge_count.keys() {
992                let du = uvs[v0][0] - uvs[v1][0];
993                let dv = uvs[v0][1] - uvs[v1][1];
994                if (du * du + dv * dv).sqrt() > discontinuity_threshold {
995                    seam.add_edge(v0, v1);
996                }
997            }
998        }
999        let _ = bverts;
1000        seam
1001    }
1002}
1003
1004impl Default for UvSeam {
1005    fn default() -> Self {
1006        Self::new()
1007    }
1008}
1009
1010// ---------------------------------------------------------------------------
1011// Texture atlas chart
1012// ---------------------------------------------------------------------------
1013
1014/// A single chart (UV island) for texture atlas packing.
1015#[derive(Debug, Clone)]
1016pub struct AtlasChart {
1017    /// Vertex indices belonging to this chart.
1018    pub vertices: Vec<usize>,
1019    /// UV coordinates for these vertices (local to \[0,1\]^2).
1020    pub uvs: Vec<[f64; 2]>,
1021    /// Axis-aligned bounding box in UV space: `(min_u, min_v, max_u, max_v)`.
1022    pub bbox: (f64, f64, f64, f64),
1023}
1024
1025impl AtlasChart {
1026    /// Create a chart from a set of vertex indices and their UV coordinates.
1027    pub fn new(vertices: Vec<usize>, uvs: Vec<[f64; 2]>) -> Self {
1028        let bbox = if uvs.is_empty() {
1029            (0.0, 0.0, 0.0, 0.0)
1030        } else {
1031            let min_u = uvs.iter().map(|uv| uv[0]).fold(f64::INFINITY, f64::min);
1032            let min_v = uvs.iter().map(|uv| uv[1]).fold(f64::INFINITY, f64::min);
1033            let max_u = uvs.iter().map(|uv| uv[0]).fold(f64::NEG_INFINITY, f64::max);
1034            let max_v = uvs.iter().map(|uv| uv[1]).fold(f64::NEG_INFINITY, f64::max);
1035            (min_u, min_v, max_u, max_v)
1036        };
1037        Self {
1038            vertices,
1039            uvs,
1040            bbox,
1041        }
1042    }
1043
1044    /// Width of the chart in UV space.
1045    pub fn width(&self) -> f64 {
1046        self.bbox.2 - self.bbox.0
1047    }
1048
1049    /// Height of the chart in UV space.
1050    pub fn height(&self) -> f64 {
1051        self.bbox.3 - self.bbox.1
1052    }
1053
1054    /// Normalize UVs to the \[0, 1\] range.
1055    pub fn normalize(&mut self) {
1056        let (min_u, min_v, max_u, max_v) = self.bbox;
1057        let du = (max_u - min_u).max(1e-15);
1058        let dv = (max_v - min_v).max(1e-15);
1059        for uv in &mut self.uvs {
1060            uv[0] = (uv[0] - min_u) / du;
1061            uv[1] = (uv[1] - min_v) / dv;
1062        }
1063        self.bbox = (0.0, 0.0, 1.0, 1.0);
1064    }
1065}
1066
1067// ---------------------------------------------------------------------------
1068// Chart packing (texture atlas generation)
1069// ---------------------------------------------------------------------------
1070
1071/// Result of packing multiple charts into a texture atlas.
1072#[derive(Debug, Clone)]
1073pub struct AtlasPackResult {
1074    /// Packed UV coordinates per vertex (indexed by original vertex index).
1075    pub packed_uvs: Vec<[f64; 2]>,
1076    /// Atlas width (normalised, always 1.0 in this implementation).
1077    pub atlas_width: f64,
1078    /// Atlas height (normalised).
1079    pub atlas_height: f64,
1080    /// Per-chart placement: `(offset_u, offset_v, scale)`.
1081    pub placements: Vec<(f64, f64, f64)>,
1082}
1083
1084/// Pack a list of `AtlasChart`s into a texture atlas using a simple row-based packing.
1085///
1086/// Charts are normalized, sorted by height, and placed in rows.
1087#[allow(clippy::too_many_arguments)]
1088pub fn pack_atlas_charts(
1089    charts: &mut Vec<AtlasChart>,
1090    n_verts: usize,
1091    padding: f64,
1092) -> AtlasPackResult {
1093    for chart in charts.iter_mut() {
1094        chart.normalize();
1095    }
1096    // Sort charts by height descending.
1097    charts.sort_by(|a, b| {
1098        b.height()
1099            .partial_cmp(&a.height())
1100            .unwrap_or(std::cmp::Ordering::Equal)
1101    });
1102
1103    let mut packed_uvs = vec![[0.0f64; 2]; n_verts];
1104    let mut placements = Vec::new();
1105
1106    let mut cursor_u = 0.0f64;
1107    let mut cursor_v = 0.0f64;
1108    let mut row_height = 0.0f64;
1109    let scale = 1.0f64 / (charts.len() as f64).sqrt().max(1.0);
1110
1111    for chart in charts.iter() {
1112        let w = (chart.width() * scale + padding).min(1.0);
1113        let h = (chart.height() * scale + padding).min(1.0);
1114
1115        if cursor_u + w > 1.0 {
1116            cursor_v += row_height + padding;
1117            cursor_u = 0.0;
1118            row_height = 0.0;
1119        }
1120
1121        // Place the chart.
1122        let offset_u = cursor_u;
1123        let offset_v = cursor_v;
1124        placements.push((offset_u, offset_v, scale));
1125
1126        for (&vi, &uv) in chart.vertices.iter().zip(chart.uvs.iter()) {
1127            if vi < packed_uvs.len() {
1128                packed_uvs[vi] = [
1129                    offset_u + uv[0] * chart.width() * scale,
1130                    offset_v + uv[1] * chart.height() * scale,
1131                ];
1132            }
1133        }
1134
1135        row_height = row_height.max(h);
1136        cursor_u += w;
1137    }
1138
1139    let atlas_height = cursor_v + row_height;
1140
1141    AtlasPackResult {
1142        packed_uvs,
1143        atlas_width: 1.0,
1144        atlas_height: atlas_height.max(1.0),
1145        placements,
1146    }
1147}
1148
1149// ---------------------------------------------------------------------------
1150// Parameterization quality metrics
1151// ---------------------------------------------------------------------------
1152
1153/// Compute the angle distortion per triangle.
1154///
1155/// Angle distortion is the maximum relative change in angles from 3D to UV space.
1156/// A value of 0 means perfect conformality.
1157pub fn angle_distortion(
1158    positions: &[[f64; 3]],
1159    uvs: &[[f64; 2]],
1160    triangles: &[[usize; 3]],
1161) -> Vec<f64> {
1162    triangles
1163        .iter()
1164        .map(|tri| {
1165            let p = [positions[tri[0]], positions[tri[1]], positions[tri[2]]];
1166            let u = [uvs[tri[0]], uvs[tri[1]], uvs[tri[2]]];
1167
1168            // Compute 3D angles.
1169            let angles_3d = triangle_angles_3d(&p);
1170            // Compute UV angles.
1171            let angles_uv = triangle_angles_2d(&u);
1172
1173            // Max angle difference.
1174            let mut max_diff = 0.0f64;
1175            for i in 0..3 {
1176                let diff = (angles_3d[i] - angles_uv[i]).abs();
1177                if diff > max_diff {
1178                    max_diff = diff;
1179                }
1180            }
1181            max_diff
1182        })
1183        .collect()
1184}
1185
1186fn triangle_angles_3d(p: &[[f64; 3]; 3]) -> [f64; 3] {
1187    let mut angles = [0.0f64; 3];
1188    for i in 0..3 {
1189        let j = (i + 1) % 3;
1190        let k = (i + 2) % 3;
1191        let a = sub3(p[j], p[i]);
1192        let b = sub3(p[k], p[i]);
1193        let la = length3(a);
1194        let lb = length3(b);
1195        if la > 1e-15 && lb > 1e-15 {
1196            let cos_a = (dot3(a, b) / (la * lb)).clamp(-1.0, 1.0);
1197            angles[i] = cos_a.acos();
1198        }
1199    }
1200    angles
1201}
1202
1203fn triangle_angles_2d(u: &[[f64; 2]; 3]) -> [f64; 3] {
1204    let mut angles = [0.0f64; 3];
1205    for i in 0..3 {
1206        let j = (i + 1) % 3;
1207        let k = (i + 2) % 3;
1208        let a = [u[j][0] - u[i][0], u[j][1] - u[i][1]];
1209        let b = [u[k][0] - u[i][0], u[k][1] - u[i][1]];
1210        let la = (a[0] * a[0] + a[1] * a[1]).sqrt();
1211        let lb = (b[0] * b[0] + b[1] * b[1]).sqrt();
1212        if la > 1e-15 && lb > 1e-15 {
1213            let cos_a = ((a[0] * b[0] + a[1] * b[1]) / (la * lb)).clamp(-1.0, 1.0);
1214            angles[i] = cos_a.acos();
1215        }
1216    }
1217    angles
1218}
1219
1220/// Mean angle distortion across all triangles.
1221pub fn mean_angle_distortion(
1222    positions: &[[f64; 3]],
1223    uvs: &[[f64; 2]],
1224    triangles: &[[usize; 3]],
1225) -> f64 {
1226    let dists = angle_distortion(positions, uvs, triangles);
1227    if dists.is_empty() {
1228        return 0.0;
1229    }
1230    dists.iter().sum::<f64>() / dists.len() as f64
1231}
1232
1233/// Compute the isometric distortion: ratio of singular values of the
1234/// Jacobian for each triangle.
1235///
1236/// Returns 0 for triangles with zero area.  A value of 1 means isometric (ideal).
1237pub fn isometric_distortion(
1238    positions: &[[f64; 3]],
1239    uvs: &[[f64; 2]],
1240    triangles: &[[usize; 3]],
1241) -> Vec<f64> {
1242    triangles
1243        .iter()
1244        .map(|tri| {
1245            let p0 = positions[tri[0]];
1246            let p1 = positions[tri[1]];
1247            let p2 = positions[tri[2]];
1248            let u0 = uvs[tri[0]];
1249            let u1 = uvs[tri[1]];
1250            let u2 = uvs[tri[2]];
1251
1252            let e1_3d = sub3(p1, p0);
1253            let e2_3d = sub3(p2, p0);
1254            let e1_uv = [u1[0] - u0[0], u1[1] - u0[1]];
1255            let e2_uv = [u2[0] - u0[0], u2[1] - u0[1]];
1256
1257            let area_3d = length3(cross3(e1_3d, e2_3d)) * 0.5;
1258            let area_uv = (cross2(e1_uv, e2_uv)).abs() * 0.5;
1259
1260            if area_3d < 1e-15 || area_uv < 1e-15 {
1261                return 0.0;
1262            }
1263
1264            // Stretch metric: sqrt(area_3d / area_uv) (or its inverse)
1265            let ratio = (area_3d / area_uv).sqrt();
1266            // Normalize so ideal = 1: return distance from 1.
1267            (ratio - 1.0).abs()
1268        })
1269        .collect()
1270}
1271
1272/// Summary quality report for a UV parameterization.
1273#[derive(Debug, Clone)]
1274pub struct ParamQualityReport {
1275    /// Mean UV stretch (area ratio).
1276    pub mean_stretch: f64,
1277    /// Number of inverted triangles.
1278    pub n_inverted: usize,
1279    /// Mean angle distortion (radians).
1280    pub mean_angle_distortion: f64,
1281    /// Mean isometric distortion.
1282    pub mean_isometric: f64,
1283    /// Total UV area (should be non-zero for a valid parameterization).
1284    pub total_uv_area: f64,
1285}
1286
1287/// Compute a comprehensive quality report for a UV parameterization.
1288pub fn parameterization_quality_report(
1289    positions: &[[f64; 3]],
1290    uvs: &[[f64; 2]],
1291    triangles: &[[usize; 3]],
1292) -> ParamQualityReport {
1293    let mean_stretch = uv_stretch(positions, uvs, triangles);
1294    let n_inverted = uv_overlap_check(uvs, triangles);
1295    let mean_ang = mean_angle_distortion(positions, uvs, triangles);
1296    let iso = isometric_distortion(positions, uvs, triangles);
1297    let mean_iso = if iso.is_empty() {
1298        0.0
1299    } else {
1300        iso.iter().sum::<f64>() / iso.len() as f64
1301    };
1302    let total_uv_area: f64 = triangles
1303        .iter()
1304        .map(|tri| {
1305            let u0 = uvs[tri[0]];
1306            let u1 = uvs[tri[1]];
1307            let u2 = uvs[tri[2]];
1308            let e1 = [u1[0] - u0[0], u1[1] - u0[1]];
1309            let e2 = [u2[0] - u0[0], u2[1] - u0[1]];
1310            cross2(e1, e2).abs() * 0.5
1311        })
1312        .sum();
1313
1314    ParamQualityReport {
1315        mean_stretch,
1316        n_inverted,
1317        mean_angle_distortion: mean_ang,
1318        mean_isometric: mean_iso,
1319        total_uv_area,
1320    }
1321}
1322
1323// ---------------------------------------------------------------------------
1324// Tests
1325// ---------------------------------------------------------------------------
1326
1327#[cfg(test)]
1328mod tests {
1329    use super::*;
1330
1331    fn flat_triangle_mesh() -> ParamTriMesh {
1332        let mut mesh = ParamTriMesh::new();
1333        mesh.add_vertex([0.0, 0.0, 0.0], [0.0, 0.0]);
1334        mesh.add_vertex([1.0, 0.0, 0.0], [1.0, 0.0]);
1335        mesh.add_vertex([0.0, 1.0, 0.0], [0.0, 1.0]);
1336        mesh.add_triangle(0, 1, 2);
1337        mesh
1338    }
1339
1340    #[test]
1341    fn test_trimesh_vertices_and_triangles() {
1342        let mesh = flat_triangle_mesh();
1343        assert_eq!(mesh.vertex_count(), 3);
1344        assert_eq!(mesh.triangle_count(), 1);
1345        assert_eq!(mesh.positions[0], [0.0, 0.0, 0.0]);
1346        assert_eq!(mesh.triangles[0], [0, 1, 2]);
1347    }
1348
1349    #[test]
1350    fn test_compute_normals_flat_triangle_points_up() {
1351        let mut mesh = flat_triangle_mesh();
1352        mesh.compute_normals();
1353        // Normal of a triangle in the XY plane should be +Z = [0, 0, 1]
1354        for i in 0..3 {
1355            let n = mesh.normals[i];
1356            assert!(n[2] > 0.99, "normal.z should be ~1.0, got {}", n[2]);
1357            assert!(n[0].abs() < 1e-10, "normal.x should be ~0, got {}", n[0]);
1358            assert!(n[1].abs() < 1e-10, "normal.y should be ~0, got {}", n[1]);
1359        }
1360    }
1361
1362    #[test]
1363    fn test_boundary_vertices_simple_mesh() {
1364        // A simple strip: two triangles sharing an interior edge.
1365        // Vertices: 0(0,0,0), 1(1,0,0), 2(0,1,0), 3(1,1,0)
1366        // Triangles: [0,1,2], [1,3,2]
1367        let triangles: Vec<[usize; 3]> = vec![[0, 1, 2], [1, 3, 2]];
1368        let bv = boundary_vertices(4, &triangles);
1369        // All 4 vertices are on the boundary of this open patch
1370        assert_eq!(bv.len(), 4, "expected 4 boundary vertices, got {:?}", bv);
1371        for v in 0..4usize {
1372            assert!(bv.contains(&v), "vertex {} should be on boundary", v);
1373        }
1374    }
1375
1376    #[test]
1377    fn test_midpoint_subdivision_one_to_four() {
1378        let positions = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1379        let triangles = vec![[0usize, 1, 2]];
1380        let (new_pos, new_tri) = midpoint_subdivision(&positions, &triangles);
1381        assert_eq!(new_tri.len(), 4, "1 triangle should become 4");
1382        // 3 original + 3 edge midpoints = 6 vertices
1383        assert_eq!(new_pos.len(), 6, "should have 6 vertices after subdivision");
1384    }
1385
1386    #[test]
1387    fn test_uv_overlap_check_zero_for_valid_uv() {
1388        // CCW UV triangle: no overlap
1389        let uvs = vec![[0.0f64, 0.0], [1.0, 0.0], [0.0, 1.0]];
1390        let triangles = vec![[0usize, 1, 2]];
1391        let overlaps = uv_overlap_check(&uvs, &triangles);
1392        assert_eq!(overlaps, 0, "no inverted triangles expected");
1393    }
1394
1395    #[test]
1396    fn test_uv_overlap_check_detects_inverted() {
1397        // CW UV triangle: inverted
1398        let uvs = vec![[0.0f64, 0.0], [0.0, 1.0], [1.0, 0.0]];
1399        let triangles = vec![[0usize, 1, 2]];
1400        let overlaps = uv_overlap_check(&uvs, &triangles);
1401        assert_eq!(overlaps, 1, "one inverted triangle expected");
1402    }
1403
1404    #[test]
1405    fn test_map_boundary_to_circle() {
1406        let boundary = vec![0usize, 1, 2, 3];
1407        let uvs = TutteParameterization::map_boundary_to_circle(&boundary, 4);
1408        for &vi in &boundary {
1409            let u = uvs[vi][0];
1410            let v = uvs[vi][1];
1411            let r = (u * u + v * v).sqrt();
1412            assert!(
1413                (r - 1.0).abs() < 1e-10,
1414                "vertex {} not on unit circle: r={}",
1415                vi,
1416                r
1417            );
1418        }
1419    }
1420
1421    #[test]
1422    fn test_texture_distortion_uniform() {
1423        // A triangle in 3D that matches its UV triangle exactly should have distortion = 1.
1424        // 3D triangle: right triangle with legs = 1 in XY plane -> area = 0.5
1425        // UV triangle: same shape -> area = 0.5
1426        let positions = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1427        let uvs = vec![[0.0f64, 0.0], [1.0, 0.0], [0.0, 1.0]];
1428        let triangles = vec![[0usize, 1, 2]];
1429        let d = texture_distortion(&positions, &uvs, &triangles);
1430        assert_eq!(d.len(), 1);
1431        assert!(
1432            (d[0] - 1.0).abs() < 1e-10,
1433            "expected distortion 1.0, got {}",
1434            d[0]
1435        );
1436    }
1437
1438    // ── Harmonic map parameterization ─────────────────────────────────────────
1439
1440    #[test]
1441    fn test_harmonic_map_single_triangle() {
1442        // Single triangle: 3 boundary vertices -> harmonic should place them on circle.
1443        let mesh = flat_triangle_mesh();
1444        let uvs = HarmonicMapParameterization::compute(&mesh);
1445        assert_eq!(uvs.len(), 3);
1446    }
1447
1448    #[test]
1449    fn test_harmonic_map_grid_mesh() {
1450        // Build a small 2×2 grid.
1451        let mut mesh = ParamTriMesh::new();
1452        for j in 0..3 {
1453            for i in 0..3 {
1454                mesh.add_vertex([i as f64, j as f64, 0.0], [0.0, 0.0]);
1455            }
1456        }
1457        for j in 0..2 {
1458            for i in 0..2 {
1459                let v00 = j * 3 + i;
1460                let v10 = j * 3 + i + 1;
1461                let v01 = (j + 1) * 3 + i;
1462                let v11 = (j + 1) * 3 + i + 1;
1463                mesh.add_triangle(v00, v10, v11);
1464                mesh.add_triangle(v00, v11, v01);
1465            }
1466        }
1467        let uvs = HarmonicMapParameterization::compute(&mesh);
1468        assert_eq!(uvs.len(), 9);
1469        // No UV should be NaN.
1470        for uv in &uvs {
1471            assert!(
1472                uv[0].is_finite() && uv[1].is_finite(),
1473                "UV should be finite"
1474            );
1475        }
1476    }
1477
1478    // ── UV seam handling ──────────────────────────────────────────────────────
1479
1480    #[test]
1481    fn test_uv_seam_add_and_query() {
1482        let mut seam = UvSeam::new();
1483        seam.add_edge(0, 1);
1484        assert!(seam.is_seam_edge(0, 1));
1485        assert!(seam.is_seam_edge(1, 0)); // undirected
1486        assert!(!seam.is_seam_edge(0, 2));
1487    }
1488
1489    #[test]
1490    fn test_uv_seam_no_duplicates() {
1491        let mut seam = UvSeam::new();
1492        seam.add_edge(0, 1);
1493        seam.add_edge(1, 0); // same edge, reversed
1494        assert_eq!(seam.edges.len(), 1, "Should not store duplicate edges");
1495    }
1496
1497    #[test]
1498    fn test_uv_seam_detect_boundary() {
1499        let mesh = flat_triangle_mesh();
1500        let uvs = vec![[0.0f64, 0.0]; 3];
1501        let seam = UvSeam::detect_seams(&mesh, &uvs, 0.5);
1502        // Boundary edges of a single triangle should be seams.
1503        assert!(
1504            seam.edges.len() >= 3,
1505            "All 3 edges of single tri are boundary seams"
1506        );
1507    }
1508
1509    // ── AtlasChart ────────────────────────────────────────────────────────────
1510
1511    #[test]
1512    fn test_atlas_chart_bbox() {
1513        let chart = AtlasChart::new(vec![0, 1, 2], vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
1514        assert!((chart.width() - 1.0).abs() < 1e-10);
1515        assert!((chart.height() - 1.0).abs() < 1e-10);
1516    }
1517
1518    #[test]
1519    fn test_atlas_chart_normalize() {
1520        let mut chart = AtlasChart::new(vec![0, 1, 2], vec![[2.0, 3.0], [4.0, 3.0], [2.0, 5.0]]);
1521        chart.normalize();
1522        // After normalization, UVs should be in [0, 1].
1523        for uv in &chart.uvs {
1524            assert!(uv[0] >= -1e-10 && uv[0] <= 1.0 + 1e-10);
1525            assert!(uv[1] >= -1e-10 && uv[1] <= 1.0 + 1e-10);
1526        }
1527    }
1528
1529    // ── Chart packing ─────────────────────────────────────────────────────────
1530
1531    #[test]
1532    fn test_pack_atlas_charts_basic() {
1533        let mut charts = vec![
1534            AtlasChart::new(vec![0, 1, 2], vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]),
1535            AtlasChart::new(vec![3, 4, 5], vec![[0.0, 0.0], [0.5, 0.0], [0.0, 0.5]]),
1536        ];
1537        let result = pack_atlas_charts(&mut charts, 6, 0.01);
1538        assert_eq!(result.packed_uvs.len(), 6);
1539        assert_eq!(result.placements.len(), 2);
1540    }
1541
1542    #[test]
1543    fn test_pack_atlas_charts_empty() {
1544        let mut charts: Vec<AtlasChart> = Vec::new();
1545        let result = pack_atlas_charts(&mut charts, 0, 0.0);
1546        assert!(result.packed_uvs.is_empty());
1547    }
1548
1549    // ── Parameterization quality metrics ─────────────────────────────────────
1550
1551    #[test]
1552    fn test_angle_distortion_zero_for_identical() {
1553        // When the UV and 3D triangle have the same shape, angle distortion = 0.
1554        let positions = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1555        let uvs = vec![[0.0f64, 0.0], [1.0, 0.0], [0.0, 1.0]];
1556        let triangles = vec![[0usize, 1, 2]];
1557        let d = angle_distortion(&positions, &uvs, &triangles);
1558        assert_eq!(d.len(), 1);
1559        assert!(
1560            d[0] < 1e-8,
1561            "angle distortion should be 0 for same shape, got {}",
1562            d[0]
1563        );
1564    }
1565
1566    #[test]
1567    fn test_mean_angle_distortion_empty() {
1568        let d = mean_angle_distortion(&[], &[], &[]);
1569        assert_eq!(d, 0.0);
1570    }
1571
1572    #[test]
1573    fn test_isometric_distortion_zero_for_same_shape() {
1574        // Same 3D and UV triangle: stretch is 1 -> isometric distortion = 0.
1575        let positions = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1576        let uvs = vec![[0.0f64, 0.0], [1.0, 0.0], [0.0, 1.0]];
1577        let triangles = vec![[0usize, 1, 2]];
1578        let iso = isometric_distortion(&positions, &uvs, &triangles);
1579        assert_eq!(iso.len(), 1);
1580        assert!(
1581            iso[0] < 1e-8,
1582            "isometric distortion should be 0, got {}",
1583            iso[0]
1584        );
1585    }
1586
1587    #[test]
1588    fn test_parameterization_quality_report() {
1589        let positions = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1590        let uvs = vec![[0.0f64, 0.0], [1.0, 0.0], [0.0, 1.0]];
1591        let triangles = vec![[0usize, 1, 2]];
1592        let report = parameterization_quality_report(&positions, &uvs, &triangles);
1593        assert_eq!(report.n_inverted, 0);
1594        assert!(report.total_uv_area > 0.0);
1595        assert!(report.mean_stretch > 0.0);
1596    }
1597
1598    #[test]
1599    fn test_uv_stretch_single_triangle_identity() {
1600        let positions = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1601        let uvs = vec![[0.0f64, 0.0], [1.0, 0.0], [0.0, 1.0]];
1602        let triangles = vec![[0usize, 1, 2]];
1603        let s = uv_stretch(&positions, &uvs, &triangles);
1604        assert!(
1605            (s - 1.0).abs() < 1e-10,
1606            "UV stretch for identity mapping should be 1, got {s}"
1607        );
1608    }
1609}