1#![allow(clippy::needless_range_loop, clippy::ptr_arg)]
2#![allow(dead_code, missing_docs)]
12
13use std::collections::HashMap;
14
15use crate::triangle_mesh::TriangleMesh;
16use oxiphysics_core::math::Vec3;
17
18#[inline]
23fn vec3_to_arr(v: Vec3) -> [f64; 3] {
24 [v.x, v.y, v.z]
25}
26
27#[inline]
28fn arr_to_vec3(a: [f64; 3]) -> Vec3 {
29 Vec3::new(a[0], a[1], a[2])
30}
31
32#[inline]
33fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
34 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
35}
36
37#[inline]
38fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
39 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
40}
41
42#[inline]
43fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
44 [a[0] * s, a[1] * s, a[2] * s]
45}
46
47#[inline]
48fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
49 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
50}
51
52#[inline]
53fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
54 [
55 a[1] * b[2] - a[2] * b[1],
56 a[2] * b[0] - a[0] * b[2],
57 a[0] * b[1] - a[1] * b[0],
58 ]
59}
60
61#[inline]
62fn len3(a: [f64; 3]) -> f64 {
63 dot3(a, a).sqrt()
64}
65
66#[inline]
67fn normalize3(a: [f64; 3]) -> [f64; 3] {
68 let l = len3(a);
69 if l < f64::EPSILON {
70 [0.0, 0.0, 0.0]
71 } else {
72 [a[0] / l, a[1] / l, a[2] / l]
73 }
74}
75
76#[inline]
77fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
78 len3(sub3(a, b))
79}
80
81#[inline]
82fn midpoint(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
83 scale3(add3(a, b), 0.5)
84}
85
86pub struct UniformRemesher {
92 pub target_edge_length: f64,
94}
95
96impl UniformRemesher {
97 pub fn new(target_edge_length: f64) -> Self {
99 Self { target_edge_length }
100 }
101
102 pub fn remesh(&self, mesh: &TriangleMesh, iterations: usize) -> TriangleMesh {
111 isotropic_remesh(mesh, self.target_edge_length, iterations)
112 }
113}
114
115pub fn isotropic_remesh(mesh: &TriangleMesh, target_len: f64, iterations: usize) -> TriangleMesh {
123 let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
125 let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
126
127 let orig_verts = verts.clone();
129 let orig_tris = tris.clone();
130
131 let high = target_len * 4.0 / 3.0;
132 let low = target_len * 4.0 / 5.0;
133
134 for _ in 0..iterations {
135 split_long_edges(&mut verts, &mut tris, high);
137
138 collapse_short_edges(&mut verts, &mut tris, low);
140
141 flip_for_valence(&mut tris, verts.len());
143
144 laplacian_smooth_surface(&mut verts, &tris);
146
147 project_to_surface(&mut verts, &orig_verts, &orig_tris);
149 }
150
151 let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
152 TriangleMesh::new(vertices, tris)
153}
154
155fn split_long_edges(verts: &mut Vec<[f64; 3]>, tris: &mut Vec<[usize; 3]>, max_len: f64) {
160 let mut edge_midpoints: HashMap<(usize, usize), usize> = HashMap::new();
162 let mut new_tris: Vec<[usize; 3]> = Vec::new();
163 let mut keep = vec![true; tris.len()];
164
165 for (ti, tri) in tris.iter().enumerate() {
166 let [a, b, c] = *tri;
167 let pa = verts[a];
168 let pb = verts[b];
169 let pc = verts[c];
170
171 let lab = dist3(pa, pb);
172 let lbc = dist3(pb, pc);
173 let lca = dist3(pc, pa);
174
175 let (longest, va, vb, vopp) = if lab >= lbc && lab >= lca {
176 (lab, a, b, c)
177 } else if lbc >= lab && lbc >= lca {
178 (lbc, b, c, a)
179 } else {
180 (lca, c, a, b)
181 };
182
183 if longest > max_len {
184 let key = (va.min(vb), va.max(vb));
186 let mid = if let Some(&existing) = edge_midpoints.get(&key) {
187 existing
188 } else {
189 let idx = verts.len();
190 verts.push(midpoint(verts[va], verts[vb]));
191 edge_midpoints.insert(key, idx);
192 idx
193 };
194 new_tris.push([va, mid, vopp]);
195 new_tris.push([mid, vb, vopp]);
196 keep[ti] = false;
197 }
198 }
199
200 let orig: Vec<[usize; 3]> = tris
201 .iter()
202 .enumerate()
203 .filter_map(|(i, &t)| if keep[i] { Some(t) } else { None })
204 .collect();
205
206 *tris = orig;
207 tris.extend(new_tris);
208}
209
210fn collapse_short_edges(verts: &mut Vec<[f64; 3]>, tris: &mut Vec<[usize; 3]>, min_len: f64) {
215 let n = verts.len();
217 let mut remap: Vec<usize> = (0..n).collect();
218
219 let mut edge_set: HashMap<(usize, usize), bool> = HashMap::new();
221 for tri in tris.iter() {
222 for k in 0..3 {
223 let a = tri[k];
224 let b = tri[(k + 1) % 3];
225 let key = (a.min(b), a.max(b));
226 edge_set.insert(key, true);
227 }
228 }
229
230 for (a, b) in edge_set.keys() {
231 let ca = remap[*a];
232 let cb = remap[*b];
233 if ca == cb {
234 continue;
235 }
236 let d = dist3(verts[ca], verts[cb]);
237 if d < min_len {
238 let mp = midpoint(verts[ca], verts[cb]);
240 verts[ca] = mp;
241 for r in remap.iter_mut() {
243 if *r == cb {
244 *r = ca;
245 }
246 }
247 }
248 }
249
250 let new_tris: Vec<[usize; 3]> = tris
252 .iter()
253 .map(|tri| [remap[tri[0]], remap[tri[1]], remap[tri[2]]])
254 .filter(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2])
255 .collect();
256
257 *tris = new_tris;
258}
259
260fn flip_for_valence(tris: &mut Vec<[usize; 3]>, n_verts: usize) {
265 let mut valence = vec![0usize; n_verts];
267 for tri in tris.iter() {
268 for &vi in tri.iter() {
269 if vi < n_verts {
270 valence[vi] += 1;
271 }
272 }
273 }
274
275 let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
277 for (ti, tri) in tris.iter().enumerate() {
278 for k in 0..3 {
279 let a = tri[k];
280 let b = tri[(k + 1) % 3];
281 let opp = tri[(k + 2) % 3];
282 let key = (a.min(b), a.max(b));
283 edge_map.entry(key).or_default().push((ti, opp));
284 }
285 }
286
287 for ((ea, eb), entries) in &edge_map {
288 if entries.len() != 2 {
289 continue;
290 }
291 let (ti, oc) = entries[0];
292 let (tj, od) = entries[1];
293
294 let dev_before = valence_deviation(valence[*ea], 6)
296 + valence_deviation(valence[*eb], 6)
297 + valence_deviation(valence[oc], 6)
298 + valence_deviation(valence[od], 6);
299
300 let dev_after = valence_deviation(valence[*ea] - 1, 6)
302 + valence_deviation(valence[*eb] - 1, 6)
303 + valence_deviation(valence[oc] + 1, 6)
304 + valence_deviation(valence[od] + 1, 6);
305
306 if dev_after < dev_before {
307 tris[ti] = [*ea, oc, od];
308 tris[tj] = [*eb, od, oc];
309 valence[*ea] -= 1;
311 valence[*eb] -= 1;
312 valence[oc] += 1;
313 valence[od] += 1;
314 }
315 }
316}
317
318fn valence_deviation(v: usize, ideal: usize) -> i64 {
319 let d = v as i64 - ideal as i64;
320 d * d
321}
322
323fn laplacian_smooth_surface(verts: &mut [[f64; 3]], tris: &[[usize; 3]]) {
328 let n = verts.len();
329 let mut sums = vec![[0.0_f64; 3]; n];
330 let mut counts = vec![0usize; n];
331
332 for tri in tris {
333 for k in 0..3 {
334 let vi = tri[k];
335 for j in 0..3 {
336 if j != k {
337 let vj = tri[j];
338 sums[vi] = add3(sums[vi], verts[vj]);
339 counts[vi] += 1;
340 }
341 }
342 }
343 }
344
345 for i in 0..n {
346 if counts[i] > 0 {
347 let avg = scale3(sums[i], 1.0 / counts[i] as f64);
348 let delta = sub3(avg, verts[i]);
350 verts[i] = add3(verts[i], scale3(delta, 0.5));
351 }
352 }
353}
354
355fn project_to_surface(verts: &mut [[f64; 3]], orig_verts: &[[f64; 3]], orig_tris: &[[usize; 3]]) {
360 for v in verts.iter_mut() {
361 let mut best_dist = f64::INFINITY;
363 let mut best_point = *v;
364
365 for tri in orig_tris {
366 let a = orig_verts[tri[0]];
367 let b = orig_verts[tri[1]];
368 let c = orig_verts[tri[2]];
369 let cp = closest_point_on_triangle(*v, a, b, c);
370 let d = dist3(*v, cp);
371 if d < best_dist {
372 best_dist = d;
373 best_point = cp;
374 }
375 }
376
377 *v = best_point;
378 }
379}
380
381fn closest_point_on_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
383 let ab = sub3(b, a);
384 let ac = sub3(c, a);
385 let ap = sub3(p, a);
386
387 let d1 = dot3(ab, ap);
388 let d2 = dot3(ac, ap);
389 if d1 <= 0.0 && d2 <= 0.0 {
390 return a;
391 }
392
393 let bp = sub3(p, b);
394 let d3 = dot3(ab, bp);
395 let d4 = dot3(ac, bp);
396 if d3 >= 0.0 && d4 <= d3 {
397 return b;
398 }
399
400 let vc = d1 * d4 - d3 * d2;
401 if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
402 let v = d1 / (d1 - d3);
403 return add3(a, scale3(ab, v));
404 }
405
406 let cp = sub3(p, c);
407 let d5 = dot3(ab, cp);
408 let d6 = dot3(ac, cp);
409 if d6 >= 0.0 && d5 <= d6 {
410 return c;
411 }
412
413 let vb = d5 * d2 - d1 * d6;
414 if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
415 let w = d2 / (d2 - d6);
416 return add3(a, scale3(ac, w));
417 }
418
419 let va = d3 * d6 - d5 * d4;
420 if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
421 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
422 return add3(b, scale3(sub3(c, b), w));
423 }
424
425 let denom = 1.0 / (va + vb + vc);
426 let v = vb * denom;
427 let w = vc * denom;
428 add3(add3(a, scale3(ab, v)), scale3(ac, w))
429}
430
431pub struct LoopSubdivision;
437
438impl LoopSubdivision {
439 pub fn subdivide(mesh: &TriangleMesh) -> TriangleMesh {
444 let n_orig = mesh.vertices.len();
445 let mut new_verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
446
447 let mut edge_midpoints: HashMap<(usize, usize), usize> = HashMap::new();
449
450 let get_or_create_midpoint = |a: usize,
451 b: usize,
452 verts: &mut Vec<[f64; 3]>,
453 map: &mut HashMap<(usize, usize), usize>|
454 -> usize {
455 let key = (a.min(b), a.max(b));
456 if let Some(&idx) = map.get(&key) {
457 return idx;
458 }
459 let mid = midpoint(verts[a], verts[b]);
460 let idx = verts.len();
461 verts.push(mid);
462 map.insert(key, idx);
463 idx
464 };
465
466 let mut new_tris: Vec<[usize; 3]> = Vec::with_capacity(mesh.indices.len() * 4);
468
469 for tri in &mesh.indices {
470 let [a, b, c] = *tri;
471 let mab = get_or_create_midpoint(a, b, &mut new_verts, &mut edge_midpoints);
472 let mbc = get_or_create_midpoint(b, c, &mut new_verts, &mut edge_midpoints);
473 let mca = get_or_create_midpoint(c, a, &mut new_verts, &mut edge_midpoints);
474
475 new_tris.push([a, mab, mca]);
476 new_tris.push([mab, b, mbc]);
477 new_tris.push([mca, mbc, c]);
478 new_tris.push([mab, mbc, mca]);
479 }
480
481 let mut neighbours: Vec<Vec<usize>> = vec![Vec::new(); n_orig];
484 for tri in &mesh.indices {
485 let [a, b, c] = *tri;
486 if !neighbours[a].contains(&b) {
487 neighbours[a].push(b);
488 }
489 if !neighbours[a].contains(&c) {
490 neighbours[a].push(c);
491 }
492 if !neighbours[b].contains(&a) {
493 neighbours[b].push(a);
494 }
495 if !neighbours[b].contains(&c) {
496 neighbours[b].push(c);
497 }
498 if !neighbours[c].contains(&a) {
499 neighbours[c].push(a);
500 }
501 if !neighbours[c].contains(&b) {
502 neighbours[c].push(b);
503 }
504 }
505
506 for i in 0..n_orig {
507 let k = neighbours[i].len();
508 if k == 0 {
509 continue;
510 }
511 let beta = if k == 3 {
513 3.0 / 16.0
514 } else {
515 3.0 / (8.0 * k as f64)
516 };
517 let self_weight = 1.0 - k as f64 * beta;
518
519 let mut new_pos = scale3(new_verts[i], self_weight);
520 for &nb in &neighbours[i] {
521 new_pos = add3(new_pos, scale3(new_verts[nb], beta));
522 }
523 new_verts[i] = new_pos;
524 }
525
526 let vertices: Vec<Vec3> = new_verts.iter().map(|&a| arr_to_vec3(a)).collect();
527 TriangleMesh::new(vertices, new_tris)
528 }
529}
530
531pub struct CatmullClark;
537
538impl CatmullClark {
539 pub fn subdivide_quad_mesh(
549 verts: &[[f64; 3]],
550 quads: &[[usize; 4]],
551 ) -> (Vec<[f64; 3]>, Vec<[usize; 4]>) {
552 let n_orig = verts.len();
553 let mut new_verts: Vec<[f64; 3]> = verts.to_vec();
554
555 let face_point_start = new_verts.len();
557 let mut face_points: Vec<usize> = Vec::with_capacity(quads.len());
558 for quad in quads {
559 let fp = scale3(
560 quad.iter()
561 .fold([0.0_f64; 3], |acc, &vi| add3(acc, verts[vi])),
562 0.25,
563 );
564 face_points.push(new_verts.len());
565 new_verts.push(fp);
566 }
567
568 let mut edge_map: HashMap<(usize, usize), Vec<usize>> = HashMap::new(); for (qi, quad) in quads.iter().enumerate() {
573 for k in 0..4 {
574 let a = quad[k];
575 let b = quad[(k + 1) % 4];
576 let key = (a.min(b), a.max(b));
577 edge_map.entry(key).or_default().push(qi);
578 }
579 }
580
581 let edge_point_start = new_verts.len();
582 let mut edge_to_idx: HashMap<(usize, usize), usize> = HashMap::new();
583 for (&(ea, eb), face_idxs) in &edge_map {
584 let mut ep = scale3(add3(verts[ea], verts[eb]), 0.5);
585 if face_idxs.len() == 2 {
586 let fp0 = new_verts[face_point_start + face_idxs[0]];
587 let fp1 = new_verts[face_point_start + face_idxs[1]];
588 ep = scale3(add3(add3(verts[ea], verts[eb]), add3(fp0, fp1)), 0.25);
589 }
590 edge_to_idx.insert((ea, eb), new_verts.len());
591 edge_to_idx.insert((eb, ea), new_verts.len());
592 new_verts.push(ep);
593 }
594 let _ = edge_point_start;
595
596 let mut vertex_face_points: Vec<Vec<[f64; 3]>> = vec![Vec::new(); n_orig];
600 let mut vertex_edge_mids: Vec<Vec<[f64; 3]>> = vec![Vec::new(); n_orig];
601
602 for (qi, quad) in quads.iter().enumerate() {
603 let fp = new_verts[face_point_start + qi];
604 for k in 0..4 {
605 let vi = quad[k];
606 vertex_face_points[vi].push(fp);
607 let nb = quad[(k + 1) % 4];
608 let em = scale3(add3(verts[vi], verts[nb]), 0.5);
609 vertex_edge_mids[vi].push(em);
610 let nb2 = quad[(k + 3) % 4];
611 let em2 = scale3(add3(verts[vi], verts[nb2]), 0.5);
612 vertex_edge_mids[vi].push(em2);
613 }
614 }
615
616 for i in 0..n_orig {
617 let n = vertex_face_points[i].len();
618 if n == 0 {
619 continue;
620 }
621 let n_f = n as f64;
622 let f_avg = scale3(
623 vertex_face_points[i]
624 .iter()
625 .fold([0.0_f64; 3], |acc, &x| add3(acc, x)),
626 1.0 / n_f,
627 );
628 let e_count = vertex_edge_mids[i].len();
629 let e_avg = if e_count > 0 {
630 scale3(
631 vertex_edge_mids[i]
632 .iter()
633 .fold([0.0_f64; 3], |acc, &x| add3(acc, x)),
634 1.0 / e_count as f64,
635 )
636 } else {
637 verts[i]
638 };
639 let v_new = scale3(
641 add3(
642 add3(f_avg, scale3(e_avg, 2.0)),
643 scale3(verts[i], (n_f - 3.0).max(0.0)),
644 ),
645 1.0 / n_f,
646 );
647 new_verts[i] = v_new;
648 }
649
650 let mut new_quads: Vec<[usize; 4]> = Vec::with_capacity(quads.len() * 4);
653 for (qi, quad) in quads.iter().enumerate() {
654 let fp_idx = face_point_start + qi;
655 for k in 0..4 {
656 let vi = quad[k];
657 let ea = quad[k];
658 let eb = quad[(k + 1) % 4];
659 let ea2 = quad[(k + 3) % 4];
660 let eb2 = quad[k];
661
662 let ep1 = *edge_to_idx.get(&(ea.min(eb), ea.max(eb))).unwrap_or(&vi);
663 let ep2 = *edge_to_idx
664 .get(&(ea2.min(eb2), ea2.max(eb2)))
665 .unwrap_or(&vi);
666
667 new_quads.push([vi, ep1, fp_idx, ep2]);
668 }
669 }
670
671 (new_verts, new_quads)
672 }
673}
674
675pub fn feature_preserving_remesh(
684 mesh: &TriangleMesh,
685 target_len: f64,
686 iterations: usize,
687 feature_angle_rad: f64,
688) -> TriangleMesh {
689 let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
690 let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
691
692 let feature_edges = detect_feature_edges(&verts, &tris, feature_angle_rad);
694
695 let orig_verts = verts.clone();
696 let orig_tris = tris.clone();
697
698 let high = target_len * 4.0 / 3.0;
699 let low = target_len * 4.0 / 5.0;
700
701 for _ in 0..iterations {
702 split_long_edges(&mut verts, &mut tris, high);
703 collapse_short_edges_preserving(&mut verts, &mut tris, low, &feature_edges);
704 flip_for_valence(&mut tris, verts.len());
705 laplacian_smooth_surface(&mut verts, &tris);
706 project_to_surface(&mut verts, &orig_verts, &orig_tris);
707 }
708
709 let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
710 TriangleMesh::new(vertices, tris)
711}
712
713fn detect_feature_edges(
715 verts: &[[f64; 3]],
716 tris: &[[usize; 3]],
717 threshold_rad: f64,
718) -> HashMap<(usize, usize), bool> {
719 let mut edge_faces: HashMap<(usize, usize), Vec<[f64; 3]>> = HashMap::new();
721 for tri in tris {
722 let a = verts[tri[0]];
723 let b = verts[tri[1]];
724 let c = verts[tri[2]];
725 let e1 = sub3(b, a);
726 let e2 = sub3(c, a);
727 let n = normalize3(cross3(e1, e2));
728 for k in 0..3 {
729 let va = tri[k];
730 let vb = tri[(k + 1) % 3];
731 let key = (va.min(vb), va.max(vb));
732 edge_faces.entry(key).or_default().push(n);
733 }
734 }
735
736 let mut features = HashMap::new();
737 let cos_thresh = threshold_rad.cos();
738 for ((ea, eb), normals) in &edge_faces {
739 let is_feature = if normals.len() == 2 {
740 let d = dot3(normals[0], normals[1]);
741 d < cos_thresh
742 } else {
743 true };
745 features.insert((*ea, *eb), is_feature);
746 }
747 features
748}
749
750fn collapse_short_edges_preserving(
752 verts: &mut Vec<[f64; 3]>,
753 tris: &mut Vec<[usize; 3]>,
754 min_len: f64,
755 feature_edges: &HashMap<(usize, usize), bool>,
756) {
757 let n = verts.len();
758 let mut remap: Vec<usize> = (0..n).collect();
759
760 let mut edge_set: HashMap<(usize, usize), bool> = HashMap::new();
761 for tri in tris.iter() {
762 for k in 0..3 {
763 let a = tri[k];
764 let b = tri[(k + 1) % 3];
765 let key = (a.min(b), a.max(b));
766 edge_set.insert(key, true);
767 }
768 }
769
770 for (a, b) in edge_set.keys() {
771 let ca = remap[*a];
772 let cb = remap[*b];
773 if ca == cb {
774 continue;
775 }
776 if feature_edges
778 .get(&(*a.min(b), *a.max(b)))
779 .copied()
780 .unwrap_or(false)
781 {
782 continue;
783 }
784 let d = dist3(verts[ca], verts[cb]);
785 if d < min_len {
786 let mp = midpoint(verts[ca], verts[cb]);
787 verts[ca] = mp;
788 for r in remap.iter_mut() {
789 if *r == cb {
790 *r = ca;
791 }
792 }
793 }
794 }
795
796 let new_tris: Vec<[usize; 3]> = tris
797 .iter()
798 .map(|tri| [remap[tri[0]], remap[tri[1]], remap[tri[2]]])
799 .filter(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2])
800 .collect();
801 *tris = new_tris;
802}
803
804pub fn flip_edges_for_quality(tris: &mut Vec<[usize; 3]>, verts: &[[f64; 3]]) {
813 let n_verts = verts.len();
814
815 let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
817 for (ti, tri) in tris.iter().enumerate() {
818 for k in 0..3 {
819 let a = tri[k];
820 let b = tri[(k + 1) % 3];
821 let opp = tri[(k + 2) % 3];
822 let key = (a.min(b), a.max(b));
823 edge_map.entry(key).or_default().push((ti, opp));
824 }
825 }
826
827 for ((ea, eb), entries) in &edge_map {
828 if entries.len() != 2 {
829 continue;
830 }
831 let (ti, oc) = entries[0];
832 let (tj, od) = entries[1];
833
834 if *ea >= n_verts || *eb >= n_verts || oc >= n_verts || od >= n_verts {
835 continue;
836 }
837
838 let before_min = min_triangle_angle(verts[*ea], verts[*eb], verts[oc])
840 .min(min_triangle_angle(verts[*ea], verts[*eb], verts[od]));
841 let after_min = min_triangle_angle(verts[oc], verts[od], verts[*ea])
842 .min(min_triangle_angle(verts[oc], verts[od], verts[*eb]));
843
844 if after_min > before_min + 1e-6 {
845 tris[ti] = [*ea, oc, od];
847 tris[tj] = [*eb, od, oc];
848 }
849 }
850}
851
852fn min_triangle_angle(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
854 let ab = sub3(b, a);
855 let ac = sub3(c, a);
856 let bc = sub3(c, b);
857 let lab = len3(ab);
858 let lac = len3(ac);
859 let lbc = len3(bc);
860
861 if lab < 1e-12 || lac < 1e-12 || lbc < 1e-12 {
862 return 0.0;
863 }
864
865 let cos_a = (dot3(ab, ac) / (lab * lac)).clamp(-1.0, 1.0);
866 let cos_b = (dot3(scale3(ab, -1.0), bc) / (lab * lbc)).clamp(-1.0, 1.0);
867 let cos_c = (dot3(scale3(ac, -1.0), scale3(bc, -1.0)) / (lac * lbc)).clamp(-1.0, 1.0);
868
869 cos_a.acos().min(cos_b.acos()).min(cos_c.acos())
870}
871
872pub fn mesh_quality_min_angle(verts: &[[f64; 3]], tris: &[[usize; 3]]) -> f64 {
880 if tris.is_empty() {
881 return 0.0;
882 }
883 let ideal = std::f64::consts::PI / 3.0;
884 let min_q = tris
885 .iter()
886 .map(|tri| {
887 let a = verts[tri[0]];
888 let b = verts[tri[1]];
889 let c = verts[tri[2]];
890 let min_ang = min_triangle_angle(a, b, c);
891 min_ang / ideal
892 })
893 .fold(f64::INFINITY, f64::min);
894 min_q.min(1.0)
895}
896
897pub fn mesh_aspect_ratio_avg(verts: &[[f64; 3]], tris: &[[usize; 3]]) -> f64 {
902 if tris.is_empty() {
903 return 0.0;
904 }
905 let ratios: Vec<f64> = tris
906 .iter()
907 .map(|tri| {
908 let a = verts[tri[0]];
909 let b = verts[tri[1]];
910 let c = verts[tri[2]];
911 let lab = len3(sub3(b, a));
912 let lbc = len3(sub3(c, b));
913 let lca = len3(sub3(a, c));
914 let longest = lab.max(lbc).max(lca);
915 let s = (lab + lbc + lca) * 0.5; let area_sq = s * (s - lab) * (s - lbc) * (s - lca);
917 if area_sq <= 0.0 || s < 1e-12 {
918 return f64::INFINITY;
919 }
920 let area = area_sq.sqrt();
921 let inradius = area / s;
922 longest / (2.0 * inradius)
923 })
924 .collect();
925
926 let finite: Vec<f64> = ratios.into_iter().filter(|x| x.is_finite()).collect();
927 if finite.is_empty() {
928 0.0
929 } else {
930 finite.iter().sum::<f64>() / finite.len() as f64
931 }
932}
933
934pub fn tangent_laplacian_smooth(mesh: &TriangleMesh, iterations: usize) -> TriangleMesh {
941 let orig_verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
942 let tris = &mesh.indices;
943 let mut verts = orig_verts.clone();
944
945 for _ in 0..iterations {
946 laplacian_smooth_surface(&mut verts, tris);
947 project_to_surface(&mut verts, &orig_verts, tris);
948 }
949
950 let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
951 TriangleMesh::new(vertices, tris.clone())
952}
953
954pub fn collapse_edge(verts: &mut Vec<[f64; 3]>, tris: &mut Vec<[usize; 3]>, v0: usize, v1: usize) {
963 let mid = midpoint(verts[v0], verts[v1]);
964 verts[v0] = mid;
965
966 let new_tris: Vec<[usize; 3]> = tris
968 .iter()
969 .map(|tri| {
970 [
971 if tri[0] == v1 { v0 } else { tri[0] },
972 if tri[1] == v1 { v0 } else { tri[1] },
973 if tri[2] == v1 { v0 } else { tri[2] },
974 ]
975 })
976 .filter(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2])
977 .collect();
978 *tris = new_tris;
979}
980
981pub fn laplacian_smooth(mesh: &TriangleMesh, iterations: usize, lambda: f64) -> TriangleMesh {
990 let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
991 let tris = &mesh.indices;
992
993 for _ in 0..iterations {
994 let n = verts.len();
995 let mut sums = vec![[0.0_f64; 3]; n];
996 let mut counts = vec![0usize; n];
997
998 for tri in tris.iter() {
999 for k in 0..3 {
1000 let vi = tri[k];
1001 for j in 0..3 {
1002 if j != k {
1003 let vj = tri[j];
1004 sums[vi] = add3(sums[vi], verts[vj]);
1005 counts[vi] += 1;
1006 }
1007 }
1008 }
1009 }
1010
1011 for i in 0..n {
1012 if counts[i] > 0 {
1013 let avg = scale3(sums[i], 1.0 / counts[i] as f64);
1014 let delta = sub3(avg, verts[i]);
1015 verts[i] = add3(verts[i], scale3(delta, lambda));
1016 }
1017 }
1018 }
1019
1020 let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
1021 TriangleMesh::new(vertices, tris.clone())
1022}
1023
1024pub fn delaunay_edge_flip(tris: &mut Vec<[usize; 3]>, verts: &[[f64; 3]]) {
1033 let mut changed = true;
1035 let max_passes = 32;
1036 let mut pass = 0;
1037
1038 while changed && pass < max_passes {
1039 changed = false;
1040 pass += 1;
1041
1042 let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
1043 for (ti, tri) in tris.iter().enumerate() {
1044 for k in 0..3 {
1045 let a = tri[k];
1046 let b = tri[(k + 1) % 3];
1047 let opp = tri[(k + 2) % 3];
1048 let key = (a.min(b), a.max(b));
1049 edge_map.entry(key).or_default().push((ti, opp));
1050 }
1051 }
1052
1053 for ((ea, eb), entries) in &edge_map {
1054 if entries.len() != 2 {
1055 continue;
1056 }
1057 let (ti, oc) = entries[0];
1058 let (tj, od) = entries[1];
1059
1060 if *ea >= verts.len() || *eb >= verts.len() || oc >= verts.len() || od >= verts.len() {
1061 continue;
1062 }
1063
1064 if point_in_circumcircle(verts[*ea], verts[*eb], verts[oc], verts[od]) {
1066 tris[ti] = [*ea, oc, od];
1068 tris[tj] = [*eb, od, oc];
1069 changed = true;
1070 }
1071 }
1072 }
1073}
1074
1075fn point_in_circumcircle(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> bool {
1078 let ax = a[0] - d[0];
1080 let az = a[2] - d[2];
1081 let bx = b[0] - d[0];
1082 let bz = b[2] - d[2];
1083 let cx = c[0] - d[0];
1084 let cz = c[2] - d[2];
1085
1086 let det = ax * (bz * (cx * cx + cz * cz) - cz * (bx * bx + bz * bz))
1087 - az * (bx * (cx * cx + cz * cz) - cx * (bx * bx + bz * bz))
1088 + (ax * ax + az * az) * (bx * cz - bz * cx);
1089
1090 det > 0.0
1091}
1092
1093pub fn coarsen_mesh(mesh: &TriangleMesh, target_len: f64) -> TriangleMesh {
1101 let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1102 let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
1103
1104 for _ in 0..10 {
1106 let before = tris.len();
1107 collapse_short_edges(&mut verts, &mut tris, target_len);
1108 if tris.len() == before {
1109 break;
1110 }
1111 }
1112
1113 let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
1114 TriangleMesh::new(vertices, tris)
1115}
1116
1117pub fn adaptive_refine_by_curvature(mesh: &TriangleMesh, threshold: f64) -> TriangleMesh {
1126 let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1127 let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
1128
1129 let face_normals: Vec<[f64; 3]> = tris
1131 .iter()
1132 .map(|tri| {
1133 let a = verts[tri[0]];
1134 let b = verts[tri[1]];
1135 let c = verts[tri[2]];
1136 normalize3(cross3(sub3(b, a), sub3(c, a)))
1137 })
1138 .collect();
1139
1140 let n = verts.len();
1142 let mut vertex_faces: Vec<Vec<usize>> = vec![Vec::new(); n];
1143 for (ti, tri) in tris.iter().enumerate() {
1144 for &vi in tri.iter() {
1145 vertex_faces[vi].push(ti);
1146 }
1147 }
1148
1149 let vertex_curvature: Vec<f64> = (0..n)
1150 .map(|vi| {
1151 let faces = &vertex_faces[vi];
1152 if faces.len() < 2 {
1153 return 0.0;
1154 }
1155 let mut max_angle: f64 = 0.0;
1156 for i in 0..faces.len() {
1157 for j in (i + 1)..faces.len() {
1158 let d = dot3(face_normals[faces[i]], face_normals[faces[j]]).clamp(-1.0, 1.0);
1159 let angle = d.acos();
1160 if angle > max_angle {
1161 max_angle = angle;
1162 }
1163 }
1164 }
1165 max_angle
1166 })
1167 .collect();
1168
1169 let mut new_tris: Vec<[usize; 3]> = Vec::new();
1171 let mut kept_tris: Vec<[usize; 3]> = Vec::new();
1172
1173 for tri in &tris {
1174 let max_curv = vertex_curvature[tri[0]]
1175 .max(vertex_curvature[tri[1]])
1176 .max(vertex_curvature[tri[2]]);
1177
1178 if max_curv > threshold {
1179 let a = tri[0];
1181 let b = tri[1];
1182 let c = tri[2];
1183 let lab = dist3(verts[a], verts[b]);
1184 let lbc = dist3(verts[b], verts[c]);
1185 let lca = dist3(verts[c], verts[a]);
1186
1187 let (va, vb, vc) = if lab >= lbc && lab >= lca {
1188 (a, b, c)
1189 } else if lbc >= lab && lbc >= lca {
1190 (b, c, a)
1191 } else {
1192 (c, a, b)
1193 };
1194
1195 let mid_idx = verts.len();
1196 verts.push(midpoint(verts[va], verts[vb]));
1197 new_tris.push([va, mid_idx, vc]);
1198 new_tris.push([mid_idx, vb, vc]);
1199 } else {
1200 kept_tris.push(*tri);
1201 }
1202 }
1203
1204 tris = kept_tris;
1205 tris.extend(new_tris);
1206
1207 let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
1208 TriangleMesh::new(vertices, tris)
1209}
1210
1211pub fn vertex_clustering_decimate(mesh: &TriangleMesh, cells_per_axis: usize) -> TriangleMesh {
1221 if cells_per_axis == 0 || mesh.vertices.is_empty() {
1222 return mesh.clone();
1223 }
1224
1225 let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1226
1227 let mut bbmin = verts[0];
1229 let mut bbmax = verts[0];
1230 for &v in &verts {
1231 for k in 0..3 {
1232 if v[k] < bbmin[k] {
1233 bbmin[k] = v[k];
1234 }
1235 if v[k] > bbmax[k] {
1236 bbmax[k] = v[k];
1237 }
1238 }
1239 }
1240 let extent = [
1241 bbmax[0] - bbmin[0] + 1e-8,
1242 bbmax[1] - bbmin[1] + 1e-8,
1243 bbmax[2] - bbmin[2] + 1e-8,
1244 ];
1245 let c = cells_per_axis as f64;
1246
1247 let cell_of = |v: [f64; 3]| -> usize {
1249 let ix = ((v[0] - bbmin[0]) / extent[0] * c).floor() as usize;
1250 let iy = ((v[1] - bbmin[1]) / extent[1] * c).floor() as usize;
1251 let iz = ((v[2] - bbmin[2]) / extent[2] * c).floor() as usize;
1252 let ix = ix.min(cells_per_axis - 1);
1253 let iy = iy.min(cells_per_axis - 1);
1254 let iz = iz.min(cells_per_axis - 1);
1255 ix + cells_per_axis * (iy + cells_per_axis * iz)
1256 };
1257
1258 let n_cells = cells_per_axis * cells_per_axis * cells_per_axis;
1259 let mut cell_sums: Vec<[f64; 3]> = vec![[0.0; 3]; n_cells];
1260 let mut cell_counts: Vec<usize> = vec![0; n_cells];
1261 let mut vertex_cell: Vec<usize> = vec![0; verts.len()];
1262
1263 for (i, &v) in verts.iter().enumerate() {
1264 let cell = cell_of(v);
1265 vertex_cell[i] = cell;
1266 for k in 0..3 {
1267 cell_sums[cell][k] += v[k];
1268 }
1269 cell_counts[cell] += 1;
1270 }
1271
1272 let mut cell_to_vert: Vec<Option<usize>> = vec![None; n_cells];
1274 let mut new_verts: Vec<[f64; 3]> = Vec::new();
1275 for cell in 0..n_cells {
1276 if cell_counts[cell] > 0 {
1277 cell_to_vert[cell] = Some(new_verts.len());
1278 let cnt = cell_counts[cell] as f64;
1279 new_verts.push([
1280 cell_sums[cell][0] / cnt,
1281 cell_sums[cell][1] / cnt,
1282 cell_sums[cell][2] / cnt,
1283 ]);
1284 }
1285 }
1286
1287 let new_tris: Vec<[usize; 3]> = mesh
1289 .indices
1290 .iter()
1291 .filter_map(|tri| {
1292 let a = cell_to_vert[vertex_cell[tri[0]]]?;
1293 let b = cell_to_vert[vertex_cell[tri[1]]]?;
1294 let c = cell_to_vert[vertex_cell[tri[2]]]?;
1295 if a == b || b == c || a == c {
1296 return None;
1297 }
1298 Some([a, b, c])
1299 })
1300 .collect();
1301
1302 let vertices: Vec<Vec3> = new_verts.iter().map(|&a| arr_to_vec3(a)).collect();
1303 TriangleMesh::new(vertices, new_tris)
1304}
1305
1306pub fn saliency_weighted_remesh(
1316 mesh: &TriangleMesh,
1317 saliency: &[f64],
1318 iterations: usize,
1319) -> TriangleMesh {
1320 if mesh.vertices.is_empty() {
1321 return mesh.clone();
1322 }
1323
1324 let tri_saliency: Vec<f64> = mesh
1326 .indices
1327 .iter()
1328 .map(|tri| {
1329 let s = [
1330 if tri[0] < saliency.len() {
1331 saliency[tri[0]]
1332 } else {
1333 0.5
1334 },
1335 if tri[1] < saliency.len() {
1336 saliency[tri[1]]
1337 } else {
1338 0.5
1339 },
1340 if tri[2] < saliency.len() {
1341 saliency[tri[2]]
1342 } else {
1343 0.5
1344 },
1345 ];
1346 (s[0] + s[1] + s[2]) / 3.0
1347 })
1348 .collect();
1349
1350 let avg_sal = if tri_saliency.is_empty() {
1352 0.5
1353 } else {
1354 tri_saliency.iter().sum::<f64>() / tri_saliency.len() as f64
1355 };
1356
1357 let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1359 let avg_edge: f64 = {
1360 let total: f64 = mesh
1361 .indices
1362 .iter()
1363 .map(|tri| {
1364 let a = verts[tri[0]];
1365 let b = verts[tri[1]];
1366 let c = verts[tri[2]];
1367 (dist3(a, b) + dist3(b, c) + dist3(c, a)) / 3.0
1368 })
1369 .sum();
1370 if mesh.indices.is_empty() {
1371 1.0
1372 } else {
1373 total / mesh.indices.len() as f64
1374 }
1375 };
1376
1377 let target = avg_edge * (1.5 - avg_sal.clamp(0.0, 1.0));
1379
1380 isotropic_remesh(mesh, target.max(1e-6), iterations)
1381}
1382
1383#[cfg(test)]
1388mod tests {
1389 use super::*;
1390
1391 fn simple_quad_mesh() -> TriangleMesh {
1392 let verts = vec![
1394 Vec3::new(0.0, 0.0, 0.0),
1395 Vec3::new(1.0, 0.0, 0.0),
1396 Vec3::new(1.0, 1.0, 0.0),
1397 Vec3::new(0.0, 1.0, 0.0),
1398 ];
1399 let tris = vec![[0usize, 1, 2], [0, 2, 3]];
1400 TriangleMesh::new(verts, tris)
1401 }
1402
1403 #[test]
1404 fn test_loop_subdivision_face_count() {
1405 let mesh = simple_quad_mesh();
1406 let sub = LoopSubdivision::subdivide(&mesh);
1407 assert_eq!(
1409 sub.indices.len(),
1410 mesh.indices.len() * 4,
1411 "Loop subdivision should quadruple the face count"
1412 );
1413 }
1414
1415 #[test]
1416 fn test_loop_subdivision_vertex_count_increases() {
1417 let mesh = simple_quad_mesh();
1418 let sub = LoopSubdivision::subdivide(&mesh);
1419 assert!(
1420 sub.vertices.len() > mesh.vertices.len(),
1421 "Loop subdivision should add vertices"
1422 );
1423 }
1424
1425 #[test]
1426 fn test_catmull_clark_face_count() {
1427 let verts: Vec<[f64; 3]> = vec![
1428 [0.0, 0.0, 0.0],
1429 [1.0, 0.0, 0.0],
1430 [1.0, 1.0, 0.0],
1431 [0.0, 1.0, 0.0],
1432 ];
1433 let quads: Vec<[usize; 4]> = vec![[0, 1, 2, 3]];
1434 let (_, new_quads) = CatmullClark::subdivide_quad_mesh(&verts, &quads);
1435 assert_eq!(
1437 new_quads.len(),
1438 quads.len() * 4,
1439 "Catmull-Clark should quadruple the quad count"
1440 );
1441 }
1442
1443 #[test]
1444 fn test_catmull_clark_two_quads() {
1445 let verts: Vec<[f64; 3]> = vec![
1446 [0.0, 0.0, 0.0],
1447 [1.0, 0.0, 0.0],
1448 [2.0, 0.0, 0.0],
1449 [0.0, 1.0, 0.0],
1450 [1.0, 1.0, 0.0],
1451 [2.0, 1.0, 0.0],
1452 ];
1453 let quads: Vec<[usize; 4]> = vec![[0, 1, 4, 3], [1, 2, 5, 4]];
1454 let (_, new_quads) = CatmullClark::subdivide_quad_mesh(&verts, &quads);
1455 assert_eq!(new_quads.len(), quads.len() * 4);
1456 }
1457
1458 #[test]
1459 fn test_isotropic_remesh_returns_mesh() {
1460 let mesh = simple_quad_mesh();
1461 let result = isotropic_remesh(&mesh, 0.5, 2);
1462 assert!(
1464 !result.indices.is_empty(),
1465 "remeshed mesh should have faces"
1466 );
1467 assert!(
1468 !result.vertices.is_empty(),
1469 "remeshed mesh should have vertices"
1470 );
1471 }
1472
1473 #[test]
1474 fn test_uniform_remesher() {
1475 let mesh = simple_quad_mesh();
1476 let remesher = UniformRemesher::new(0.4);
1477 let result = remesher.remesh(&mesh, 1);
1478 assert!(!result.vertices.is_empty());
1479 }
1480
1481 #[test]
1482 fn test_loop_subdivision_twice() {
1483 let mesh = simple_quad_mesh();
1484 let sub1 = LoopSubdivision::subdivide(&mesh);
1485 let sub2 = LoopSubdivision::subdivide(&sub1);
1486 assert_eq!(sub2.indices.len(), mesh.indices.len() * 16);
1487 }
1488
1489 #[test]
1490 fn test_closest_point_on_triangle() {
1491 let a = [0.0_f64, 0.0, 0.0];
1492 let b = [1.0, 0.0, 0.0];
1493 let c = [0.0, 1.0, 0.0];
1494 let p = [0.25, 0.25, 1.0];
1496 let cp = closest_point_on_triangle(p, a, b, c);
1497 assert!((cp[0] - 0.25).abs() < 1e-10);
1499 assert!((cp[1] - 0.25).abs() < 1e-10);
1500 assert!(cp[2].abs() < 1e-10);
1501 }
1502
1503 #[test]
1506 fn test_feature_preserving_remesh_returns_mesh() {
1507 let mesh = simple_quad_mesh();
1508 let result = feature_preserving_remesh(&mesh, 0.4, 1, 1.0_f64.to_radians());
1511 assert!(!result.vertices.is_empty());
1512 assert!(!result.indices.is_empty());
1513 }
1514
1515 #[test]
1516 fn test_feature_preserving_remesh_no_degenerate_faces() {
1517 let mesh = simple_quad_mesh();
1518 let result = feature_preserving_remesh(&mesh, 0.3, 2, 45.0_f64.to_radians());
1519 let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1520 for tri in &result.indices {
1521 let a = verts[tri[0]];
1522 let b = verts[tri[1]];
1523 let c = verts[tri[2]];
1524 let e1 = sub3(b, a);
1525 let e2 = sub3(c, a);
1526 let cross = cross3(e1, e2);
1527 let area = len3(cross) * 0.5;
1528 assert!(area >= 0.0, "negative area: {}", area);
1529 }
1530 }
1531
1532 #[test]
1535 fn test_edge_flip_preserves_vertex_count() {
1536 let mesh = simple_quad_mesh();
1537 let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1538 let mut tris = mesh.indices.clone();
1539 let n_verts_before = verts.len();
1540 flip_edges_for_quality(&mut tris, &verts);
1541 assert_eq!(verts.len(), n_verts_before);
1542 let _ = verts; }
1544
1545 #[test]
1546 fn test_edge_flip_preserves_face_count() {
1547 let mesh = simple_quad_mesh();
1548 let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1549 let mut tris = mesh.indices.clone();
1550 let n_tris_before = tris.len();
1551 flip_edges_for_quality(&mut tris, &verts);
1552 assert_eq!(tris.len(), n_tris_before);
1553 }
1554
1555 #[test]
1558 fn test_mesh_quality_equilateral() {
1559 let s = 1.0_f64;
1561 let h = s * (3.0_f64.sqrt()) / 2.0;
1562 let verts = vec![[0.0_f64, 0.0, 0.0], [s, 0.0, 0.0], [s / 2.0, h, 0.0]];
1563 let tris = vec![[0usize, 1, 2]];
1564 let quality = mesh_quality_min_angle(&verts, &tris);
1565 assert!(quality > 0.9, "equilateral triangle quality={}", quality);
1567 }
1568
1569 #[test]
1570 fn test_mesh_quality_degenerate() {
1571 let verts = vec![
1573 [0.0_f64, 0.0, 0.0],
1574 [1.0, 0.0, 0.0],
1575 [100.0, 0.0, 0.0], ];
1577 let tris = vec![[0usize, 1, 2]];
1578 let quality = mesh_quality_min_angle(&verts, &tris);
1579 assert!(
1580 quality < 0.5,
1581 "degenerate triangle quality should be low, got {}",
1582 quality
1583 );
1584 }
1585
1586 #[test]
1589 fn test_tangent_smooth_returns_mesh() {
1590 let mesh = simple_quad_mesh();
1591 let result = tangent_laplacian_smooth(&mesh, 3);
1592 assert_eq!(result.vertices.len(), mesh.vertices.len());
1593 assert_eq!(result.indices.len(), mesh.indices.len());
1594 }
1595
1596 #[test]
1599 fn test_catmull_clark_double_subdivision() {
1600 let verts: Vec<[f64; 3]> = vec![
1601 [0.0, 0.0, 0.0],
1602 [1.0, 0.0, 0.0],
1603 [1.0, 1.0, 0.0],
1604 [0.0, 1.0, 0.0],
1605 ];
1606 let quads: Vec<[usize; 4]> = vec![[0, 1, 2, 3]];
1607 let (v1, q1) = CatmullClark::subdivide_quad_mesh(&verts, &quads);
1608 let (_, q2) = CatmullClark::subdivide_quad_mesh(&v1, &q1);
1609 assert_eq!(q2.len(), quads.len() * 16); }
1611
1612 #[test]
1615 fn test_laplacian_smooth_moves_vertices() {
1616 let mesh = simple_quad_mesh();
1617 let result = laplacian_smooth(&mesh, 3, 0.5);
1618 let orig_sum: f64 = mesh.vertices.iter().map(|v| v.norm()).sum();
1620 let new_sum: f64 = result.vertices.iter().map(|v| v.norm()).sum();
1621 let _ = (orig_sum, new_sum);
1623 assert_eq!(result.vertices.len(), mesh.vertices.len());
1624 }
1625
1626 #[test]
1627 fn test_laplacian_smooth_zero_iterations_unchanged() {
1628 let mesh = simple_quad_mesh();
1629 let result = laplacian_smooth(&mesh, 0, 0.5);
1630 for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
1631 assert!((a.x - b.x).abs() < 1e-12);
1632 assert!((a.y - b.y).abs() < 1e-12);
1633 assert!((a.z - b.z).abs() < 1e-12);
1634 }
1635 }
1636
1637 #[test]
1638 fn test_laplacian_smooth_preserves_face_count() {
1639 let mesh = simple_quad_mesh();
1640 let result = laplacian_smooth(&mesh, 5, 0.3);
1641 assert_eq!(result.indices.len(), mesh.indices.len());
1642 }
1643
1644 #[test]
1647 fn test_delaunay_flip_preserves_vertices() {
1648 let mesh = simple_quad_mesh();
1649 let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1650 let mut tris = mesh.indices.clone();
1651 let before = verts.len();
1652 delaunay_edge_flip(&mut tris, &verts);
1653 assert_eq!(verts.len(), before);
1654 }
1655
1656 #[test]
1657 fn test_delaunay_flip_preserves_face_count() {
1658 let mesh = simple_quad_mesh();
1659 let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1660 let mut tris = mesh.indices.clone();
1661 let before = tris.len();
1662 delaunay_edge_flip(&mut tris, &verts);
1663 assert_eq!(tris.len(), before);
1664 }
1665
1666 #[test]
1669 fn test_coarsen_reduces_vertex_count() {
1670 let mesh = simple_quad_mesh();
1671 let result = coarsen_mesh(&mesh, 0.8); assert!(result.vertices.len() <= mesh.vertices.len() + 1); }
1675
1676 #[test]
1677 fn test_coarsen_no_degenerate_faces() {
1678 let mesh = simple_quad_mesh();
1679 let result = coarsen_mesh(&mesh, 0.5);
1680 let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1681 for tri in &result.indices {
1682 assert!(
1683 tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2],
1684 "degenerate face {:?}",
1685 tri
1686 );
1687 assert!(tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len());
1689 }
1690 }
1691
1692 #[test]
1695 fn test_adaptive_refine_increases_faces_for_curved_mesh() {
1696 let mesh = simple_quad_mesh();
1698 let result = adaptive_refine_by_curvature(&mesh, 0.01); assert!(result.indices.len() >= mesh.indices.len());
1701 }
1702
1703 #[test]
1704 fn test_adaptive_refine_high_threshold_no_change() {
1705 let mesh = simple_quad_mesh();
1706 let result = adaptive_refine_by_curvature(&mesh, 1000.0); assert_eq!(result.indices.len(), mesh.indices.len());
1709 }
1710
1711 #[test]
1714 fn test_vertex_clustering_reduces_count() {
1715 let mesh = simple_quad_mesh();
1717 let sub = LoopSubdivision::subdivide(&mesh);
1718 let coarse = vertex_clustering_decimate(&sub, 2); assert!(
1720 coarse.vertices.len() < sub.vertices.len(),
1721 "decimation should reduce vertex count"
1722 );
1723 }
1724
1725 #[test]
1726 fn test_vertex_clustering_single_cell_merges_all() {
1727 let mesh = simple_quad_mesh();
1728 let result = vertex_clustering_decimate(&mesh, 1); assert!(result.vertices.len() <= mesh.vertices.len());
1731 }
1732
1733 #[test]
1736 fn test_saliency_remesh_returns_valid_mesh() {
1737 let mesh = simple_quad_mesh();
1739 let sub = LoopSubdivision::subdivide(&mesh);
1740 let saliency = vec![0.5_f64; sub.vertices.len()];
1741 let result = saliency_weighted_remesh(&sub, &saliency, 1);
1742 assert!(!result.vertices.is_empty());
1743 let _ = result.indices.len();
1745 }
1746
1747 #[test]
1748 fn test_saliency_remesh_preserves_approximate_vertex_count() {
1749 let mesh = simple_quad_mesh();
1750 let saliency = vec![0.5_f64; mesh.vertices.len()];
1751 let result = saliency_weighted_remesh(&mesh, &saliency, 1);
1752 assert!(result.vertices.len() <= mesh.vertices.len() * 10);
1754 }
1755
1756 #[test]
1759 fn test_mesh_quality_after_laplacian_smooth() {
1760 let mesh = simple_quad_mesh();
1761 let smoothed = laplacian_smooth(&mesh, 5, 0.5);
1762 let verts: Vec<[f64; 3]> = smoothed.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1763 let q = mesh_quality_min_angle(&verts, &smoothed.indices);
1764 assert!((0.0..=1.0 + 1e-6).contains(&q), "quality out of range: {q}");
1765 }
1766
1767 #[test]
1768 fn test_aspect_ratio_equilateral() {
1769 let s = 1.0_f64;
1770 let h = s * 3.0_f64.sqrt() / 2.0;
1771 let verts = vec![[0.0_f64, 0.0, 0.0], [s, 0.0, 0.0], [s / 2.0, h, 0.0]];
1772 let tris = vec![[0usize, 1, 2]];
1773 let ar = mesh_aspect_ratio_avg(&verts, &tris);
1774 assert!(
1778 (ar - 3.0_f64.sqrt()).abs() < 0.01,
1779 "equilateral ar should be sqrt(3)≈1.732, got {ar}"
1780 );
1781 }
1782
1783 #[test]
1784 fn test_collapse_edge_reduces_vertex_usage() {
1785 let mesh = simple_quad_mesh();
1786 let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1787 let mut tris = mesh.indices.clone();
1788 let before_faces = tris.len();
1789 collapse_edge(&mut verts, &mut tris, 0, 1);
1790 assert!(tris.len() <= before_faces);
1792 }
1793
1794 #[test]
1795 fn test_loop_subdivision_vertex_positions_reasonable() {
1796 let mesh = simple_quad_mesh();
1797 let sub = LoopSubdivision::subdivide(&mesh);
1798 for v in &sub.vertices {
1800 assert!(v.x.is_finite() && v.y.is_finite() && v.z.is_finite());
1801 }
1802 }
1803
1804 #[test]
1805 fn test_isotropic_remesh_zero_iterations_same() {
1806 let mesh = simple_quad_mesh();
1807 let result = isotropic_remesh(&mesh, 0.5, 0);
1808 assert_eq!(result.vertices.len(), mesh.vertices.len());
1810 assert_eq!(result.indices.len(), mesh.indices.len());
1811 }
1812
1813 #[test]
1814 fn test_delaunay_flip_on_subdivided_mesh() {
1815 let mesh = simple_quad_mesh();
1816 let sub = LoopSubdivision::subdivide(&mesh);
1817 let verts: Vec<[f64; 3]> = sub.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1818 let mut tris = sub.indices.clone();
1819 delaunay_edge_flip(&mut tris, &verts);
1820 for tri in &tris {
1822 assert!(tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len());
1823 }
1824 }
1825
1826 #[test]
1827 fn test_coarsen_mesh_returns_valid_faces() {
1828 let mesh = simple_quad_mesh();
1829 let result = coarsen_mesh(&mesh, 1.5);
1830 let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1831 for tri in &result.indices {
1832 assert!(
1833 tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len(),
1834 "face {:?} has out-of-bounds index (n_verts={})",
1835 tri,
1836 verts.len()
1837 );
1838 }
1839 }
1840
1841 #[test]
1842 fn test_adaptive_refine_no_degenerate_faces() {
1843 let mesh = simple_quad_mesh();
1844 let result = adaptive_refine_by_curvature(&mesh, 0.1);
1845 let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
1846 for tri in &result.indices {
1847 assert!(tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2]);
1848 assert!(tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len());
1849 }
1850 }
1851}