1#![allow(clippy::needless_range_loop, clippy::ptr_arg)]
2#![allow(dead_code)]
11
12use std::collections::HashMap;
13
14fn 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#[derive(Debug, Clone)]
67pub struct ParamTriMesh {
68 pub positions: Vec<[f64; 3]>,
70 pub uvs: Vec<[f64; 2]>,
72 pub normals: Vec<[f64; 3]>,
74 pub triangles: Vec<[usize; 3]>,
76}
77
78impl ParamTriMesh {
79 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 pub fn vertex_count(&self) -> usize {
91 self.positions.len()
92 }
93
94 pub fn triangle_count(&self) -> usize {
96 self.triangles.len()
97 }
98
99 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 pub fn add_triangle(&mut self, a: usize, b: usize, c: usize) {
110 self.triangles.push([a, b, c]);
111 }
112
113 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); 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
144pub 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 for (t, _tri) in triangles.iter().enumerate() {
161 let base = t * 3;
162 table[base][0] = (base + 1) as i64; table[base][1] = (base + 2) as i64; 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 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 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 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; }
195 }
196 }
197
198 table
199}
200
201pub 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
221pub fn boundary_vertices(n_verts: usize, triangles: &[[usize; 3]]) -> Vec<usize> {
223 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 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
253pub 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
265pub struct LscmParameterization;
271
272impl LscmParameterization {
273 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 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 uvs[pin0] = [0.0, 0.0];
295 uvs[pin1] = [1.0, 0.0];
296
297 for _iter in 0..200 {
299 for vi in 0..n {
300 if vi == pin0 || vi == pin1 {
301 continue;
302 }
303 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 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 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 let e1 = sub3(p1, p0);
363 let e2 = sub3(p2, p0);
364
365 let f1 = sub2(u1, u0);
367 let f2 = sub2(u2, u0);
368
369 let cross = cross3(e1, e2);
371 let area_3d = length3(cross) * 0.5;
372 if area_3d < 1e-15 {
373 continue;
374 }
375
376 let det_f = cross2(f1, f2);
379 if det_f.abs() < 1e-15 {
380 energy += area_3d; continue;
382 }
383 let inv_det = 1.0 / det_f;
384
385 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 let norm_ju = dot3(ju, ju);
399 let norm_jv = dot3(jv, jv);
400 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
410pub struct TutteParameterization;
416
417impl TutteParameterization {
418 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 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 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 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 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
480pub 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
520pub 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
529pub 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
544pub 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 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 while new_uvs.len() < new_positions.len() {
564 new_uvs.push([0.0, 0.0]);
565 }
566
567 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 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 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 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 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 for i in n_orig..n_new {
673 smoothed.push(new_positions[i]);
674 }
675
676 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
687pub 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
736pub 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
774pub fn fill_holes(triangles: &mut Vec<[usize; 3]>, n_verts: usize) {
779 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 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 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 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 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
840pub struct HarmonicMapParameterization;
851
852impl HarmonicMapParameterization {
853 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 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 let cot_weight = |vi: usize, vj: usize| -> f64 {
882 let mut w = 0.0f64;
883 for tri in &mesh.triangles {
884 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 let pk = 3 - pi - pj; 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; }
901 }
902 }
903 w.max(0.0) };
905
906 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#[derive(Debug, Clone)]
941pub struct UvSeam {
942 pub edges: Vec<(usize, usize)>,
944}
945
946impl UvSeam {
947 pub fn new() -> Self {
949 Self { edges: Vec::new() }
950 }
951
952 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 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 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 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 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#[derive(Debug, Clone)]
1016pub struct AtlasChart {
1017 pub vertices: Vec<usize>,
1019 pub uvs: Vec<[f64; 2]>,
1021 pub bbox: (f64, f64, f64, f64),
1023}
1024
1025impl AtlasChart {
1026 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 pub fn width(&self) -> f64 {
1046 self.bbox.2 - self.bbox.0
1047 }
1048
1049 pub fn height(&self) -> f64 {
1051 self.bbox.3 - self.bbox.1
1052 }
1053
1054 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#[derive(Debug, Clone)]
1073pub struct AtlasPackResult {
1074 pub packed_uvs: Vec<[f64; 2]>,
1076 pub atlas_width: f64,
1078 pub atlas_height: f64,
1080 pub placements: Vec<(f64, f64, f64)>,
1082}
1083
1084#[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 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 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
1149pub 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 let angles_3d = triangle_angles_3d(&p);
1170 let angles_uv = triangle_angles_2d(&u);
1172
1173 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
1220pub 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
1233pub 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 let ratio = (area_3d / area_uv).sqrt();
1266 (ratio - 1.0).abs()
1268 })
1269 .collect()
1270}
1271
1272#[derive(Debug, Clone)]
1274pub struct ParamQualityReport {
1275 pub mean_stretch: f64,
1277 pub n_inverted: usize,
1279 pub mean_angle_distortion: f64,
1281 pub mean_isometric: f64,
1283 pub total_uv_area: f64,
1285}
1286
1287pub 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#[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 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 let triangles: Vec<[usize; 3]> = vec![[0, 1, 2], [1, 3, 2]];
1368 let bv = boundary_vertices(4, &triangles);
1369 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 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 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 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 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 #[test]
1441 fn test_harmonic_map_single_triangle() {
1442 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 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 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 #[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)); 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); 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 assert!(
1504 seam.edges.len() >= 3,
1505 "All 3 edges of single tri are boundary seams"
1506 );
1507 }
1508
1509 #[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 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 #[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 #[test]
1552 fn test_angle_distortion_zero_for_identical() {
1553 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 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}