1use std::collections::HashMap;
10
11fn 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#[derive(Debug, Clone)]
60pub struct ParamTriMesh {
61 pub positions: Vec<[f64; 3]>,
63 pub uvs: Vec<[f64; 2]>,
65 pub normals: Vec<[f64; 3]>,
67 pub triangles: Vec<[usize; 3]>,
69}
70
71impl ParamTriMesh {
72 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 pub fn vertex_count(&self) -> usize {
84 self.positions.len()
85 }
86
87 pub fn triangle_count(&self) -> usize {
89 self.triangles.len()
90 }
91
92 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 pub fn add_triangle(&mut self, a: usize, b: usize, c: usize) {
103 self.triangles.push([a, b, c]);
104 }
105
106 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); 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
137pub 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 for (t, _tri) in triangles.iter().enumerate() {
154 let base = t * 3;
155 table[base][0] = (base + 1) as i64; table[base][1] = (base + 2) as i64; 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 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 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 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; }
188 }
189 }
190
191 table
192}
193
194pub 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
214pub fn boundary_vertices(n_verts: usize, triangles: &[[usize; 3]]) -> Vec<usize> {
216 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 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
246pub 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
258pub struct LscmParameterization;
264
265impl LscmParameterization {
266 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 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 uvs[pin0] = [0.0, 0.0];
288 uvs[pin1] = [1.0, 0.0];
289
290 for _iter in 0..200 {
292 for vi in 0..n {
293 if vi == pin0 || vi == pin1 {
294 continue;
295 }
296 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 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 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 let e1 = sub3(p1, p0);
356 let e2 = sub3(p2, p0);
357
358 let f1 = sub2(u1, u0);
360 let f2 = sub2(u2, u0);
361
362 let cross = cross3(e1, e2);
364 let area_3d = length3(cross) * 0.5;
365 if area_3d < 1e-15 {
366 continue;
367 }
368
369 let det_f = cross2(f1, f2);
372 if det_f.abs() < 1e-15 {
373 energy += area_3d; continue;
375 }
376 let inv_det = 1.0 / det_f;
377
378 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 let norm_ju = dot3(ju, ju);
392 let norm_jv = dot3(jv, jv);
393 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
403pub struct TutteParameterization;
409
410impl TutteParameterization {
411 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 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 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 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 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
473pub 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
513pub 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
522pub 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
537pub 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 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 while new_uvs.len() < new_positions.len() {
557 new_uvs.push([0.0, 0.0]);
558 }
559
560 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 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 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 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 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 for pos in new_positions.iter().take(n_new).skip(n_orig) {
666 smoothed.push(*pos);
667 }
668
669 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
680pub 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
729pub 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
767pub fn fill_holes(triangles: &mut Vec<[usize; 3]>, n_verts: usize) {
772 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 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 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 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 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
833pub struct HarmonicMapParameterization;
844
845impl HarmonicMapParameterization {
846 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 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 let cot_weight = |vi: usize, vj: usize| -> f64 {
875 let mut w = 0.0f64;
876 for tri in &mesh.triangles {
877 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 let pk = 3 - pi - pj; 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; }
894 }
895 }
896 w.max(0.0) };
898
899 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#[derive(Debug, Clone)]
934pub struct UvSeam {
935 pub edges: Vec<(usize, usize)>,
937}
938
939impl UvSeam {
940 pub fn new() -> Self {
942 Self { edges: Vec::new() }
943 }
944
945 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 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 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 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 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#[derive(Debug, Clone)]
1009pub struct AtlasChart {
1010 pub vertices: Vec<usize>,
1012 pub uvs: Vec<[f64; 2]>,
1014 pub bbox: (f64, f64, f64, f64),
1016}
1017
1018impl AtlasChart {
1019 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 pub fn width(&self) -> f64 {
1039 self.bbox.2 - self.bbox.0
1040 }
1041
1042 pub fn height(&self) -> f64 {
1044 self.bbox.3 - self.bbox.1
1045 }
1046
1047 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#[derive(Debug, Clone)]
1066pub struct AtlasPackResult {
1067 pub packed_uvs: Vec<[f64; 2]>,
1069 pub atlas_width: f64,
1071 pub atlas_height: f64,
1073 pub placements: Vec<(f64, f64, f64)>,
1075}
1076
1077pub 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 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 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
1141pub 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 let angles_3d = triangle_angles_3d(&p);
1162 let angles_uv = triangle_angles_2d(&u);
1164
1165 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
1212pub 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
1225pub 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 let ratio = (area_3d / area_uv).sqrt();
1258 (ratio - 1.0).abs()
1260 })
1261 .collect()
1262}
1263
1264#[derive(Debug, Clone)]
1266pub struct ParamQualityReport {
1267 pub mean_stretch: f64,
1269 pub n_inverted: usize,
1271 pub mean_angle_distortion: f64,
1273 pub mean_isometric: f64,
1275 pub total_uv_area: f64,
1277}
1278
1279pub 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#[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 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 let triangles: Vec<[usize; 3]> = vec![[0, 1, 2], [1, 3, 2]];
1360 let bv = boundary_vertices(4, &triangles);
1361 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 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 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 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 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 #[test]
1433 fn test_harmonic_map_single_triangle() {
1434 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 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 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 #[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)); 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); 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 assert!(
1496 seam.edges.len() >= 3,
1497 "All 3 edges of single tri are boundary seams"
1498 );
1499 }
1500
1501 #[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 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 #[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 #[test]
1544 fn test_angle_distortion_zero_for_identical() {
1545 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 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}