Skip to main content

oxiphysics_geometry/
mesh_param.rs

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