1#![allow(clippy::ptr_arg)]
2#![allow(clippy::needless_range_loop)]
3#![allow(dead_code)]
18
19pub type Vertex = [f64; 3];
21
22pub type Face = [usize; 3];
24
25#[inline]
27fn sq_len(v: &[f64; 3]) -> f64 {
28 v[0] * v[0] + v[1] * v[1] + v[2] * v[2]
29}
30
31#[inline]
33fn vec_len(v: &[f64; 3]) -> f64 {
34 sq_len(v).sqrt()
35}
36
37#[inline]
39fn vec_sub(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
40 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
41}
42
43#[inline]
45fn vec_add(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
46 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
47}
48
49#[inline]
51fn vec_scale(v: &[f64; 3], s: f64) -> [f64; 3] {
52 [v[0] * s, v[1] * s, v[2] * s]
53}
54
55#[inline]
57fn dot(a: &[f64; 3], b: &[f64; 3]) -> f64 {
58 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
59}
60
61#[inline]
63fn cross(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
64 [
65 a[1] * b[2] - a[2] * b[1],
66 a[2] * b[0] - a[0] * b[2],
67 a[0] * b[1] - a[1] * b[0],
68 ]
69}
70
71#[inline]
73fn normalize(v: &[f64; 3]) -> [f64; 3] {
74 let len = vec_len(v);
75 if len < 1e-15 {
76 [0.0, 0.0, 0.0]
77 } else {
78 [v[0] / len, v[1] / len, v[2] / len]
79 }
80}
81
82#[inline]
84fn edge_length(a: &[f64; 3], b: &[f64; 3]) -> f64 {
85 vec_len(&vec_sub(a, b))
86}
87
88fn angle_at(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> f64 {
90 let ab = vec_sub(b, a);
91 let ac = vec_sub(c, a);
92 let cos_ang = dot(&ab, &ac) / (vec_len(&ab) * vec_len(&ac)).max(1e-15);
93 cos_ang.clamp(-1.0, 1.0).acos()
94}
95
96pub fn triangle_area(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> f64 {
98 let ab = vec_sub(b, a);
99 let ac = vec_sub(c, a);
100 0.5 * vec_len(&cross(&ab, &ac))
101}
102
103pub fn face_normal(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> [f64; 3] {
105 let ab = vec_sub(b, a);
106 let ac = vec_sub(c, a);
107 normalize(&cross(&ab, &ac))
108}
109
110#[derive(Debug, Clone)]
112pub struct MeshQualityMetrics {
113 pub aspect_ratios: Vec<f64>,
115 pub skewness: Vec<f64>,
117 pub min_angles: Vec<f64>,
119 pub max_angles: Vec<f64>,
121 pub jacobian_quality: Vec<f64>,
123 pub quality_histogram: [usize; 10],
125}
126
127impl MeshQualityMetrics {
128 pub fn compute(vertices: &[Vertex], faces: &[Face]) -> Self {
130 let mut aspect_ratios = Vec::with_capacity(faces.len());
131 let mut skewness = Vec::with_capacity(faces.len());
132 let mut min_angles = Vec::with_capacity(faces.len());
133 let mut max_angles = Vec::with_capacity(faces.len());
134 let mut jacobian_quality = Vec::with_capacity(faces.len());
135 let mut histogram = [0usize; 10];
136
137 for face in faces {
138 let a = &vertices[face[0]];
139 let b = &vertices[face[1]];
140 let c = &vertices[face[2]];
141
142 let lab = edge_length(a, b);
143 let lbc = edge_length(b, c);
144 let lca = edge_length(c, a);
145
146 let l_max = lab.max(lbc).max(lca);
147 let l_min = lab.min(lbc).min(lca).max(1e-15);
148 let ar = l_max / l_min;
149 aspect_ratios.push(ar);
150
151 let ang_a = angle_at(a, b, c);
153 let ang_b = angle_at(b, a, c);
154 let ang_c = angle_at(c, a, b);
155 let min_ang = ang_a.min(ang_b).min(ang_c);
156 let max_ang = ang_a.max(ang_b).max(ang_c);
157 min_angles.push(min_ang);
158 max_angles.push(max_ang);
159
160 let ideal_angle = std::f64::consts::PI / 3.0;
162 let sk = (max_ang - ideal_angle)
163 .abs()
164 .max((ideal_angle - min_ang).abs())
165 / ideal_angle;
166 skewness.push(sk.clamp(0.0, 1.0));
167
168 let area = triangle_area(a, b, c);
170 let ideal_area = (3_f64.sqrt() / 4.0) * l_max * l_max;
171 let jq = if ideal_area > 1e-15 {
172 (area / ideal_area).min(1.0)
173 } else {
174 0.0
175 };
176 jacobian_quality.push(jq);
177
178 let slot = (jq * 9.999) as usize;
180 histogram[slot.min(9)] += 1;
181 }
182
183 Self {
184 aspect_ratios,
185 skewness,
186 min_angles,
187 max_angles,
188 jacobian_quality,
189 quality_histogram: histogram,
190 }
191 }
192
193 pub fn mean_jacobian_quality(&self) -> f64 {
195 if self.jacobian_quality.is_empty() {
196 return 0.0;
197 }
198 self.jacobian_quality.iter().sum::<f64>() / self.jacobian_quality.len() as f64
199 }
200
201 pub fn mean_aspect_ratio(&self) -> f64 {
203 if self.aspect_ratios.is_empty() {
204 return 1.0;
205 }
206 self.aspect_ratios.iter().sum::<f64>() / self.aspect_ratios.len() as f64
207 }
208
209 pub fn mean_skewness(&self) -> f64 {
211 if self.skewness.is_empty() {
212 return 0.0;
213 }
214 self.skewness.iter().sum::<f64>() / self.skewness.len() as f64
215 }
216
217 pub fn global_min_angle_deg(&self) -> f64 {
219 self.min_angles
220 .iter()
221 .cloned()
222 .fold(f64::INFINITY, f64::min)
223 .to_degrees()
224 }
225}
226
227#[derive(Debug, Clone)]
232pub struct LaplacianSmoothing {
233 pub lambda: f64,
235 pub iterations: usize,
237 pub preserve_boundary: bool,
239}
240
241impl LaplacianSmoothing {
242 pub fn new(lambda: f64, iterations: usize) -> Self {
244 Self {
245 lambda,
246 iterations,
247 preserve_boundary: true,
248 }
249 }
250
251 pub fn boundary_vertices(vertices: &[Vertex], faces: &[Face]) -> Vec<bool> {
253 let n = vertices.len();
254 let mut edge_count: std::collections::HashMap<(usize, usize), usize> =
255 std::collections::HashMap::new();
256 for face in faces {
257 for (i, j) in [(face[0], face[1]), (face[1], face[2]), (face[2], face[0])] {
258 let key = (i.min(j), i.max(j));
259 *edge_count.entry(key).or_insert(0) += 1;
260 }
261 }
262 let mut is_boundary = vec![false; n];
263 for ((a, b), count) in &edge_count {
264 if *count == 1 {
265 is_boundary[*a] = true;
266 is_boundary[*b] = true;
267 }
268 }
269 is_boundary
270 }
271
272 pub fn build_adjacency(n_vertices: usize, faces: &[Face]) -> Vec<Vec<usize>> {
274 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n_vertices];
275 for face in faces {
276 for k in 0..3 {
277 let a = face[k];
278 let b = face[(k + 1) % 3];
279 if !adj[a].contains(&b) {
280 adj[a].push(b);
281 }
282 if !adj[b].contains(&a) {
283 adj[b].push(a);
284 }
285 }
286 }
287 adj
288 }
289
290 pub fn smooth(&self, vertices: &[Vertex], faces: &[Face]) -> Vec<Vertex> {
292 let n = vertices.len();
293 let adj = Self::build_adjacency(n, faces);
294 let is_boundary = if self.preserve_boundary {
295 Self::boundary_vertices(vertices, faces)
296 } else {
297 vec![false; n]
298 };
299
300 let mut verts: Vec<Vertex> = vertices.to_vec();
301 for _ in 0..self.iterations {
302 let prev = verts.clone();
303 for i in 0..n {
304 if self.preserve_boundary && is_boundary[i] {
305 continue;
306 }
307 if adj[i].is_empty() {
308 continue;
309 }
310 let mut sum = [0.0f64; 3];
311 for &j in &adj[i] {
312 sum[0] += prev[j][0];
313 sum[1] += prev[j][1];
314 sum[2] += prev[j][2];
315 }
316 let nj = adj[i].len() as f64;
317 let avg = [sum[0] / nj, sum[1] / nj, sum[2] / nj];
318 verts[i][0] = prev[i][0] + self.lambda * (avg[0] - prev[i][0]);
319 verts[i][1] = prev[i][1] + self.lambda * (avg[1] - prev[i][1]);
320 verts[i][2] = prev[i][2] + self.lambda * (avg[2] - prev[i][2]);
321 }
322 }
323 verts
324 }
325
326 pub fn mesh_roughness(vertices: &[Vertex], faces: &[Face]) -> f64 {
328 let adj = Self::build_adjacency(vertices.len(), faces);
329 let mut total = 0.0;
330 let mut count = 0;
331 for (i, v) in vertices.iter().enumerate() {
332 if adj[i].is_empty() {
333 continue;
334 }
335 let mut avg = [0.0f64; 3];
336 for &j in &adj[i] {
337 avg[0] += vertices[j][0];
338 avg[1] += vertices[j][1];
339 avg[2] += vertices[j][2];
340 }
341 let nj = adj[i].len() as f64;
342 avg[0] /= nj;
343 avg[1] /= nj;
344 avg[2] /= nj;
345 total += sq_len(&vec_sub(v, &avg));
346 count += 1;
347 }
348 if count == 0 {
349 0.0
350 } else {
351 total / count as f64
352 }
353 }
354}
355
356#[derive(Debug, Clone)]
362pub struct TaubinSmoothing {
363 pub lambda: f64,
365 pub mu: f64,
367 pub iterations: usize,
369 pub preserve_boundary: bool,
371}
372
373impl TaubinSmoothing {
374 pub fn new(lambda: f64, iterations: usize) -> Self {
378 Self {
379 lambda,
380 mu: -(lambda + 0.01),
381 iterations,
382 preserve_boundary: true,
383 }
384 }
385
386 fn laplacian_step(
388 vertices: &[Vertex],
389 adj: &[Vec<usize>],
390 is_boundary: &[bool],
391 factor: f64,
392 ) -> Vec<Vertex> {
393 let n = vertices.len();
394 let mut result = vertices.to_vec();
395 for i in 0..n {
396 if is_boundary[i] || adj[i].is_empty() {
397 continue;
398 }
399 let mut sum = [0.0f64; 3];
400 for &j in &adj[i] {
401 sum[0] += vertices[j][0];
402 sum[1] += vertices[j][1];
403 sum[2] += vertices[j][2];
404 }
405 let nj = adj[i].len() as f64;
406 let avg = [sum[0] / nj, sum[1] / nj, sum[2] / nj];
407 result[i][0] = vertices[i][0] + factor * (avg[0] - vertices[i][0]);
408 result[i][1] = vertices[i][1] + factor * (avg[1] - vertices[i][1]);
409 result[i][2] = vertices[i][2] + factor * (avg[2] - vertices[i][2]);
410 }
411 result
412 }
413
414 pub fn smooth(&self, vertices: &[Vertex], faces: &[Face]) -> Vec<Vertex> {
416 let n = vertices.len();
417 let adj = LaplacianSmoothing::build_adjacency(n, faces);
418 let is_boundary = if self.preserve_boundary {
419 LaplacianSmoothing::boundary_vertices(vertices, faces)
420 } else {
421 vec![false; n]
422 };
423
424 let mut verts = vertices.to_vec();
425 for _ in 0..self.iterations {
426 verts = Self::laplacian_step(&verts, &adj, &is_boundary, self.lambda);
427 verts = Self::laplacian_step(&verts, &adj, &is_boundary, self.mu);
428 }
429 verts
430 }
431}
432
433#[derive(Debug, Clone)]
438pub struct CotanSmoothing {
439 pub step_size: f64,
441 pub iterations: usize,
443 pub preserve_boundary: bool,
445}
446
447impl CotanSmoothing {
448 pub fn new(step_size: f64, iterations: usize) -> Self {
450 Self {
451 step_size,
452 iterations,
453 preserve_boundary: true,
454 }
455 }
456
457 fn cot(angle: f64) -> f64 {
459 let sin_a = angle.sin();
460 if sin_a.abs() < 1e-15 {
461 0.0
462 } else {
463 angle.cos() / sin_a
464 }
465 }
466
467 pub fn build_cotan_weights(vertices: &[Vertex], faces: &[Face]) -> Vec<Vec<(usize, f64)>> {
469 let n = vertices.len();
470 let mut weights: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
471
472 for face in faces {
473 let (i0, i1, i2) = (face[0], face[1], face[2]);
474 let (a, b, c) = (&vertices[i0], &vertices[i1], &vertices[i2]);
475
476 let alpha = angle_at(a, b, c);
478 let beta = angle_at(b, a, c);
479 let gamma = angle_at(c, a, b);
480
481 let cot_alpha = Self::cot(alpha);
482 let cot_beta = Self::cot(beta);
483 let cot_gamma = Self::cot(gamma);
484
485 weights[i1].push((i2, 0.5 * cot_alpha));
487 weights[i2].push((i1, 0.5 * cot_alpha));
488
489 weights[i0].push((i2, 0.5 * cot_beta));
491 weights[i2].push((i0, 0.5 * cot_beta));
492
493 weights[i0].push((i1, 0.5 * cot_gamma));
495 weights[i1].push((i0, 0.5 * cot_gamma));
496 }
497 weights
498 }
499
500 pub fn smooth(&self, vertices: &[Vertex], faces: &[Face]) -> Vec<Vertex> {
502 let n = vertices.len();
503 let is_boundary = if self.preserve_boundary {
504 LaplacianSmoothing::boundary_vertices(vertices, faces)
505 } else {
506 vec![false; n]
507 };
508 let cotan_weights = Self::build_cotan_weights(vertices, faces);
509 let mut verts = vertices.to_vec();
510
511 for _ in 0..self.iterations {
512 let prev = verts.clone();
513 for i in 0..n {
514 if self.preserve_boundary && is_boundary[i] {
515 continue;
516 }
517 let wsum: f64 = cotan_weights[i].iter().map(|(_, w)| w).sum();
518 if wsum.abs() < 1e-15 {
519 continue;
520 }
521 let mut weighted_sum = [0.0f64; 3];
522 for &(j, w) in &cotan_weights[i] {
523 weighted_sum[0] += w * prev[j][0];
524 weighted_sum[1] += w * prev[j][1];
525 weighted_sum[2] += w * prev[j][2];
526 }
527 let laplacian = [
528 weighted_sum[0] / wsum - prev[i][0],
529 weighted_sum[1] / wsum - prev[i][1],
530 weighted_sum[2] / wsum - prev[i][2],
531 ];
532 verts[i][0] = prev[i][0] + self.step_size * laplacian[0];
533 verts[i][1] = prev[i][1] + self.step_size * laplacian[1];
534 verts[i][2] = prev[i][2] + self.step_size * laplacian[2];
535 }
536 }
537 verts
538 }
539}
540
541#[derive(Debug, Clone)]
550pub struct RemeshingUniform {
551 pub target_edge_length: f64,
553 pub iterations: usize,
555}
556
557impl RemeshingUniform {
558 pub fn new(target_edge_length: f64, iterations: usize) -> Self {
560 Self {
561 target_edge_length,
562 iterations,
563 }
564 }
565
566 pub fn split_long_edges(&self, vertices: &mut Vec<Vertex>, faces: &mut Vec<Face>) {
568 let threshold = (4.0 / 3.0) * self.target_edge_length;
569 let mut i = 0;
570 while i < faces.len() {
571 let face = faces[i];
572 let edges = [
573 (face[0], face[1], face[2]),
574 (face[1], face[2], face[0]),
575 (face[2], face[0], face[1]),
576 ];
577 let mut split_done = false;
578 for (a, b, c) in edges {
579 let va = vertices[a];
580 let vb = vertices[b];
581 if edge_length(&va, &vb) > threshold {
582 let mid = [
584 (va[0] + vb[0]) * 0.5,
585 (va[1] + vb[1]) * 0.5,
586 (va[2] + vb[2]) * 0.5,
587 ];
588 let m = vertices.len();
589 vertices.push(mid);
590 faces[i] = [a, m, c];
592 faces.push([m, b, c]);
593 split_done = true;
594 break;
595 }
596 }
597 if !split_done {
598 i += 1;
599 }
600 }
601 }
602
603 pub fn collapse_short_edges(&self, vertices: &mut Vec<Vertex>, faces: &mut Vec<Face>) {
605 let threshold = (4.0 / 5.0) * self.target_edge_length;
606 let mut collapsed = vec![false; vertices.len()];
607 let mut i = 0;
608 while i < faces.len() {
609 let face = faces[i];
610 if collapsed[face[0]] || collapsed[face[1]] || collapsed[face[2]] {
611 faces.remove(i);
612 continue;
613 }
614 let edges = [(face[0], face[1]), (face[1], face[2]), (face[2], face[0])];
615 let mut did_collapse = false;
616 for (a, b) in edges {
617 if collapsed[a] || collapsed[b] {
618 continue;
619 }
620 if edge_length(&vertices[a], &vertices[b]) < threshold {
621 let mid = [
623 (vertices[a][0] + vertices[b][0]) * 0.5,
624 (vertices[a][1] + vertices[b][1]) * 0.5,
625 (vertices[a][2] + vertices[b][2]) * 0.5,
626 ];
627 vertices[a] = mid;
628 collapsed[b] = true;
629 for f in faces.iter_mut() {
631 for idx in f.iter_mut() {
632 if *idx == b {
633 *idx = a;
634 }
635 }
636 }
637 did_collapse = true;
638 break;
639 }
640 }
641 let f = faces[i];
643 if f[0] == f[1] || f[1] == f[2] || f[0] == f[2] {
644 faces.remove(i);
645 continue;
646 }
647 if !did_collapse {
648 i += 1;
649 }
650 }
651 }
652
653 pub fn remesh_pass(&self, vertices: &mut Vec<Vertex>, faces: &mut Vec<Face>) {
655 self.split_long_edges(vertices, faces);
656 self.collapse_short_edges(vertices, faces);
657 let smoother = LaplacianSmoothing::new(0.5, 1);
659 let smoothed = smoother.smooth(vertices, faces);
660 let n = vertices.len();
661 vertices.copy_from_slice(&smoothed[..n]);
662 }
663}
664
665#[derive(Debug, Clone)]
667pub struct IsotropicRemeshing {
668 pub target_edge_length: f64,
670 pub num_passes: usize,
672}
673
674impl IsotropicRemeshing {
675 pub fn new(target_edge_length: f64, num_passes: usize) -> Self {
677 Self {
678 target_edge_length,
679 num_passes,
680 }
681 }
682
683 pub fn remesh(&self, vertices: &mut Vec<Vertex>, faces: &mut Vec<Face>) {
685 let remesher = RemeshingUniform::new(self.target_edge_length, self.num_passes);
686 for _ in 0..self.num_passes {
687 remesher.split_long_edges(vertices, faces);
688 remesher.collapse_short_edges(vertices, faces);
689 let smoother = LaplacianSmoothing::new(0.2, 2);
692 let smoothed = smoother.smooth(vertices, faces);
693 let len = vertices.len().min(smoothed.len());
694 vertices[..len].copy_from_slice(&smoothed[..len]);
695 }
697 }
698}
699
700#[derive(Debug, Clone)]
706pub struct MeshDecimation {
707 pub target_faces: usize,
709 pub quadrics: Vec<[f64; 16]>,
711}
712
713impl MeshDecimation {
714 pub fn new(vertices: &[Vertex], faces: &[Face], target_faces: usize) -> Self {
716 let quadrics = Self::initialize_quadrics(vertices, faces);
717 Self {
718 target_faces,
719 quadrics,
720 }
721 }
722
723 fn face_plane(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> [f64; 4] {
725 let n = face_normal(a, b, c);
726 let d = -(n[0] * a[0] + n[1] * a[1] + n[2] * a[2]);
727 [n[0], n[1], n[2], d]
728 }
729
730 fn plane_quadric(p: &[f64; 4]) -> [f64; 16] {
732 let (a, b, c, d) = (p[0], p[1], p[2], p[3]);
733 [
734 a * a,
735 a * b,
736 a * c,
737 a * d,
738 a * b,
739 b * b,
740 b * c,
741 b * d,
742 a * c,
743 b * c,
744 c * c,
745 c * d,
746 a * d,
747 b * d,
748 c * d,
749 d * d,
750 ]
751 }
752
753 fn add_quadrics(q1: &[f64; 16], q2: &[f64; 16]) -> [f64; 16] {
755 let mut result = [0.0f64; 16];
756 for i in 0..16 {
757 result[i] = q1[i] + q2[i];
758 }
759 result
760 }
761
762 pub fn initialize_quadrics(vertices: &[Vertex], faces: &[Face]) -> Vec<[f64; 16]> {
764 let mut quadrics = vec![[0.0f64; 16]; vertices.len()];
765 for face in faces {
766 let a = &vertices[face[0]];
767 let b = &vertices[face[1]];
768 let c = &vertices[face[2]];
769 let plane = Self::face_plane(a, b, c);
770 let q = Self::plane_quadric(&plane);
771 for &vi in face.iter() {
772 quadrics[vi] = Self::add_quadrics(&quadrics[vi], &q);
773 }
774 }
775 quadrics
776 }
777
778 pub fn quadric_error(q: &[f64; 16], v: &[f64; 3]) -> f64 {
780 let vh = [v[0], v[1], v[2], 1.0];
782 let mut result = 0.0;
783 for i in 0..4 {
784 for j in 0..4 {
785 result += q[i * 4 + j] * vh[i] * vh[j];
786 }
787 }
788 result.max(0.0)
789 }
790
791 pub fn edge_collapse_cost(&self, vertices: &[Vertex], i: usize, j: usize) -> f64 {
793 let q_combined = Self::add_quadrics(&self.quadrics[i], &self.quadrics[j]);
794 let mid = [
796 (vertices[i][0] + vertices[j][0]) * 0.5,
797 (vertices[i][1] + vertices[j][1]) * 0.5,
798 (vertices[i][2] + vertices[j][2]) * 0.5,
799 ];
800 Self::quadric_error(&q_combined, &mid)
801 }
802
803 pub fn decimate(&mut self, vertices: &mut Vec<Vertex>, faces: &mut Vec<Face>) {
805 while faces.len() > self.target_faces {
806 let mut best_cost = f64::INFINITY;
808 let mut best_face = 0;
809 let mut best_edge = (0usize, 0usize);
810
811 for (fi, face) in faces.iter().enumerate() {
812 for k in 0..3 {
813 let i = face[k];
814 let j = face[(k + 1) % 3];
815 let cost = self.edge_collapse_cost(vertices, i, j);
816 if cost < best_cost {
817 best_cost = cost;
818 best_face = fi;
819 best_edge = (i, j);
820 }
821 }
822 }
823
824 let (va, vb) = best_edge;
825 let mid = [
827 (vertices[va][0] + vertices[vb][0]) * 0.5,
828 (vertices[va][1] + vertices[vb][1]) * 0.5,
829 (vertices[va][2] + vertices[vb][2]) * 0.5,
830 ];
831 vertices[va] = mid;
832 self.quadrics[va] = Self::add_quadrics(&self.quadrics[va], &self.quadrics[vb]);
833
834 faces.remove(best_face);
836
837 for face in faces.iter_mut() {
839 for idx in face.iter_mut() {
840 if *idx == vb {
841 *idx = va;
842 }
843 }
844 }
845
846 faces.retain(|f| f[0] != f[1] && f[1] != f[2] && f[0] != f[2]);
848 }
849 }
850}
851
852#[derive(Debug, Clone)]
859pub struct SubdivisionLoop {
860 pub levels: usize,
862}
863
864impl SubdivisionLoop {
865 pub fn new(levels: usize) -> Self {
867 Self { levels }
868 }
869
870 fn loop_beta(n: usize) -> f64 {
872 if n == 3 {
873 3.0 / 16.0
874 } else {
875 3.0 / (8.0 * n as f64)
876 }
877 }
878
879 pub fn subdivide_once(vertices: &[Vertex], faces: &[Face]) -> (Vec<Vertex>, Vec<Face>) {
883 use std::collections::HashMap;
884
885 let mut new_verts: Vec<Vertex> = vertices.to_vec();
886 let mut edge_mid: HashMap<(usize, usize), usize> = HashMap::new();
887
888 let mut new_faces = Vec::with_capacity(faces.len() * 4);
890 for face in faces {
891 let [i0, i1, i2] = *face;
892 let mut get_mid = |a: usize, b: usize| -> usize {
893 let key = (a.min(b), a.max(b));
894 if let Some(&m) = edge_mid.get(&key) {
895 return m;
896 }
897 let mid = [
900 (vertices[a][0] + vertices[b][0]) * 0.5,
901 (vertices[a][1] + vertices[b][1]) * 0.5,
902 (vertices[a][2] + vertices[b][2]) * 0.5,
903 ];
904 let idx = new_verts.len();
905 new_verts.push(mid);
906 edge_mid.insert(key, idx);
907 idx
908 };
909
910 let m01 = get_mid(i0, i1);
911 let m12 = get_mid(i1, i2);
912 let m20 = get_mid(i2, i0);
913
914 new_faces.push([i0, m01, m20]);
915 new_faces.push([i1, m12, m01]);
916 new_faces.push([i2, m20, m12]);
917 new_faces.push([m01, m12, m20]);
918 }
919
920 let n_orig = vertices.len();
922 let adj = LaplacianSmoothing::build_adjacency(n_orig, faces);
923 for i in 0..n_orig {
924 let n = adj[i].len();
925 if n == 0 {
926 continue;
927 }
928 let beta = Self::loop_beta(n);
929 let mut neighbor_sum = [0.0f64; 3];
930 for &j in &adj[i] {
931 neighbor_sum[0] += vertices[j][0];
932 neighbor_sum[1] += vertices[j][1];
933 neighbor_sum[2] += vertices[j][2];
934 }
935 let w = 1.0 - n as f64 * beta;
936 new_verts[i] = [
937 w * vertices[i][0] + beta * neighbor_sum[0],
938 w * vertices[i][1] + beta * neighbor_sum[1],
939 w * vertices[i][2] + beta * neighbor_sum[2],
940 ];
941 }
942
943 (new_verts, new_faces)
944 }
945
946 pub fn subdivide(&self, vertices: &[Vertex], faces: &[Face]) -> (Vec<Vertex>, Vec<Face>) {
948 let mut v = vertices.to_vec();
949 let mut f = faces.to_vec();
950 for _ in 0..self.levels {
951 let (nv, nf) = Self::subdivide_once(&v, &f);
952 v = nv;
953 f = nf;
954 }
955 (v, f)
956 }
957}
958
959#[derive(Debug, Clone)]
963pub struct MeshBoolean {
964 pub tolerance: f64,
966}
967
968#[derive(Debug, Clone)]
970pub struct BooleanResult {
971 pub vertices: Vec<Vertex>,
973 pub faces: Vec<Face>,
975 pub intersection_count: usize,
977}
978
979impl MeshBoolean {
980 pub fn new(tolerance: f64) -> Self {
982 Self { tolerance }
983 }
984
985 fn point_in_bbox(p: &[f64; 3], mn: &[f64; 3], mx: &[f64; 3]) -> bool {
987 p[0] >= mn[0]
988 && p[0] <= mx[0]
989 && p[1] >= mn[1]
990 && p[1] <= mx[1]
991 && p[2] >= mn[2]
992 && p[2] <= mx[2]
993 }
994
995 pub fn bounding_box(vertices: &[Vertex]) -> ([f64; 3], [f64; 3]) {
997 let mut mn = [f64::INFINITY; 3];
998 let mut mx = [f64::NEG_INFINITY; 3];
999 for v in vertices {
1000 for k in 0..3 {
1001 mn[k] = mn[k].min(v[k]);
1002 mx[k] = mx[k].max(v[k]);
1003 }
1004 }
1005 (mn, mx)
1006 }
1007
1008 pub fn triangle_bbox_overlap(
1010 a0: &[f64; 3],
1011 a1: &[f64; 3],
1012 a2: &[f64; 3],
1013 b0: &[f64; 3],
1014 b1: &[f64; 3],
1015 b2: &[f64; 3],
1016 ) -> bool {
1017 for k in 0..3 {
1018 let amin = a0[k].min(a1[k]).min(a2[k]);
1019 let amax = a0[k].max(a1[k]).max(a2[k]);
1020 let bmin = b0[k].min(b1[k]).min(b2[k]);
1021 let bmax = b0[k].max(b1[k]).max(b2[k]);
1022 if amax < bmin || bmax < amin {
1023 return false;
1024 }
1025 }
1026 true
1027 }
1028
1029 pub fn count_intersections(va: &[Vertex], fa: &[Face], vb: &[Vertex], fb: &[Face]) -> usize {
1031 let mut count = 0;
1032 for ta in fa {
1033 let (a0, a1, a2) = (&va[ta[0]], &va[ta[1]], &va[ta[2]]);
1034 for tb in fb {
1035 let (b0, b1, b2) = (&vb[tb[0]], &vb[tb[1]], &vb[tb[2]]);
1036 if Self::triangle_bbox_overlap(a0, a1, a2, b0, b1, b2) {
1037 count += 1;
1038 }
1039 }
1040 }
1041 count
1042 }
1043
1044 pub fn mesh_union(
1049 &self,
1050 va: &[Vertex],
1051 fa: &[Face],
1052 vb: &[Vertex],
1053 fb: &[Face],
1054 ) -> BooleanResult {
1055 let n_a = va.len();
1056 let mut vertices: Vec<Vertex> = va.to_vec();
1057 vertices.extend_from_slice(vb);
1058 let mut faces: Vec<Face> = fa.to_vec();
1059 for f in fb {
1060 faces.push([f[0] + n_a, f[1] + n_a, f[2] + n_a]);
1061 }
1062 let intersections = Self::count_intersections(va, fa, vb, fb);
1063 BooleanResult {
1064 vertices,
1065 faces,
1066 intersection_count: intersections,
1067 }
1068 }
1069}
1070
1071#[derive(Debug, Clone)]
1076pub struct MeshNormalEstimation {
1077 pub angle_weighted: bool,
1079}
1080
1081impl MeshNormalEstimation {
1082 pub fn new(angle_weighted: bool) -> Self {
1084 Self { angle_weighted }
1085 }
1086
1087 pub fn compute_area_weighted(vertices: &[Vertex], faces: &[Face]) -> Vec<[f64; 3]> {
1089 let n = vertices.len();
1090 let mut normals = vec![[0.0f64; 3]; n];
1091 for face in faces {
1092 let a = &vertices[face[0]];
1093 let b = &vertices[face[1]];
1094 let c = &vertices[face[2]];
1095 let area = triangle_area(a, b, c);
1096 let fn_ = face_normal(a, b, c);
1097 for &vi in face.iter() {
1098 normals[vi][0] += area * fn_[0];
1099 normals[vi][1] += area * fn_[1];
1100 normals[vi][2] += area * fn_[2];
1101 }
1102 }
1103 normals.iter().map(normalize).collect()
1104 }
1105
1106 pub fn compute_angle_weighted(vertices: &[Vertex], faces: &[Face]) -> Vec<[f64; 3]> {
1108 let n = vertices.len();
1109 let mut normals = vec![[0.0f64; 3]; n];
1110 for face in faces {
1111 let (i0, i1, i2) = (face[0], face[1], face[2]);
1112 let a = &vertices[i0];
1113 let b = &vertices[i1];
1114 let c = &vertices[i2];
1115 let fn_ = face_normal(a, b, c);
1116 let angles = [angle_at(a, b, c), angle_at(b, a, c), angle_at(c, a, b)];
1117 for (k, &vi) in face.iter().enumerate() {
1118 let w = angles[k];
1119 normals[vi][0] += w * fn_[0];
1120 normals[vi][1] += w * fn_[1];
1121 normals[vi][2] += w * fn_[2];
1122 }
1123 }
1124 normals.iter().map(normalize).collect()
1125 }
1126
1127 pub fn compute(&self, vertices: &[Vertex], faces: &[Face]) -> Vec<[f64; 3]> {
1129 if self.angle_weighted {
1130 Self::compute_angle_weighted(vertices, faces)
1131 } else {
1132 Self::compute_area_weighted(vertices, faces)
1133 }
1134 }
1135}
1136
1137pub fn equilateral_triangle() -> (Vec<Vertex>, Vec<Face>) {
1139 let h = (3.0_f64.sqrt()) / 2.0;
1140 let vertices = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, h, 0.0]];
1141 let faces = vec![[0, 1, 2]];
1142 (vertices, faces)
1143}
1144
1145pub fn unit_quad() -> (Vec<Vertex>, Vec<Face>) {
1147 let vertices = vec![
1148 [0.0, 0.0, 0.0],
1149 [1.0, 0.0, 0.0],
1150 [1.0, 1.0, 0.0],
1151 [0.0, 1.0, 0.0],
1152 ];
1153 let faces = vec![[0, 1, 2], [0, 2, 3]];
1154 (vertices, faces)
1155}
1156
1157pub fn regular_tetrahedron() -> (Vec<Vertex>, Vec<Face>) {
1159 let vertices = vec![
1160 [1.0, 1.0, 1.0],
1161 [-1.0, -1.0, 1.0],
1162 [-1.0, 1.0, -1.0],
1163 [1.0, -1.0, -1.0],
1164 ];
1165 let faces = vec![[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];
1166 (vertices, faces)
1167}
1168
1169#[cfg(test)]
1170mod tests {
1171 use super::*;
1172
1173 #[test]
1176 fn test_equilateral_aspect_ratio_near_one() {
1177 let (verts, faces) = equilateral_triangle();
1178 let metrics = MeshQualityMetrics::compute(&verts, &faces);
1179 let ar = metrics.aspect_ratios[0];
1180 assert!(
1181 (ar - 1.0).abs() < 1e-10,
1182 "aspect ratio of equilateral = {:.6}",
1183 ar
1184 );
1185 }
1186
1187 #[test]
1188 fn test_equilateral_skewness_near_zero() {
1189 let (verts, faces) = equilateral_triangle();
1190 let metrics = MeshQualityMetrics::compute(&verts, &faces);
1191 let sk = metrics.skewness[0];
1192 assert!(sk < 1e-10, "skewness of equilateral = {:.6}", sk);
1193 }
1194
1195 #[test]
1196 fn test_equilateral_jacobian_near_one() {
1197 let (verts, faces) = equilateral_triangle();
1198 let metrics = MeshQualityMetrics::compute(&verts, &faces);
1199 let jq = metrics.jacobian_quality[0];
1200 assert!(
1201 (jq - 1.0).abs() < 1e-10,
1202 "Jacobian quality of equilateral = {:.6}",
1203 jq
1204 );
1205 }
1206
1207 #[test]
1208 fn test_equilateral_min_angle_60_deg() {
1209 let (verts, faces) = equilateral_triangle();
1210 let metrics = MeshQualityMetrics::compute(&verts, &faces);
1211 let min_ang_deg = metrics.min_angles[0].to_degrees();
1212 assert!(
1213 (min_ang_deg - 60.0).abs() < 1e-8,
1214 "min angle of equilateral = {:.6} deg",
1215 min_ang_deg
1216 );
1217 }
1218
1219 #[test]
1220 fn test_degenerate_face_has_poor_quality() {
1221 let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.0001, 0.0]];
1225 let faces = vec![[0, 1, 2]];
1226 let metrics = MeshQualityMetrics::compute(&verts, &faces);
1227 let min_deg = metrics.min_angles[0].to_degrees();
1229 assert!(
1230 min_deg < 1.0,
1231 "min angle of near-degenerate face should be < 1 deg, got {:.6}",
1232 min_deg
1233 );
1234 }
1235
1236 #[test]
1237 fn test_quality_histogram_sums_to_face_count() {
1238 let (verts, faces) = unit_quad();
1239 let metrics = MeshQualityMetrics::compute(&verts, &faces);
1240 let total: usize = metrics.quality_histogram.iter().sum();
1241 assert_eq!(total, faces.len());
1242 }
1243
1244 #[test]
1245 fn test_mean_jacobian_equilateral() {
1246 let (verts, faces) = equilateral_triangle();
1247 let metrics = MeshQualityMetrics::compute(&verts, &faces);
1248 assert!((metrics.mean_jacobian_quality() - 1.0).abs() < 1e-10);
1249 }
1250
1251 #[test]
1254 fn test_laplacian_smoothing_reduces_roughness() {
1255 let verts = vec![
1259 [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.5, 0.5, 1.0], ];
1265 let faces = vec![[0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4]];
1266 let roughness_before = LaplacianSmoothing::mesh_roughness(&verts, &faces);
1267 let smoother = LaplacianSmoothing::new(0.5, 5);
1268 let smoothed = smoother.smooth(&verts, &faces);
1269 let roughness_after = LaplacianSmoothing::mesh_roughness(&smoothed, &faces);
1270 assert!(
1271 roughness_after < roughness_before,
1272 "roughness before {:.6}, after {:.6}",
1273 roughness_before,
1274 roughness_after
1275 );
1276 }
1277
1278 #[test]
1279 fn test_laplacian_boundary_preserved() {
1280 let (mut verts, faces) = unit_quad();
1281 verts[2][2] = 1.0;
1282 let smoother = LaplacianSmoothing::new(0.5, 10);
1283 let smoothed = smoother.smooth(&verts, &faces);
1284 let is_boundary = LaplacianSmoothing::boundary_vertices(&verts, &faces);
1286 for i in 0..verts.len() {
1287 if is_boundary[i] {
1288 for k in 0..3 {
1289 assert!(
1290 (smoothed[i][k] - verts[i][k]).abs() < 1e-12,
1291 "boundary vertex {i} moved along axis {k}"
1292 );
1293 }
1294 }
1295 }
1296 }
1297
1298 #[test]
1299 fn test_build_adjacency_symmetric() {
1300 let (verts, faces) = unit_quad();
1301 let adj = LaplacianSmoothing::build_adjacency(verts.len(), &faces);
1302 for (i, neighbors) in adj.iter().enumerate() {
1303 for &j in neighbors {
1304 assert!(
1305 adj[j].contains(&i),
1306 "adjacency not symmetric: {i} -> {j} but not {j} -> {i}"
1307 );
1308 }
1309 }
1310 }
1311
1312 #[test]
1315 fn test_taubin_preserves_volume_better_than_laplacian() {
1316 let (mut verts, faces) = unit_quad();
1317 verts[2][2] = 1.0;
1318
1319 let bb_before = MeshBoolean::bounding_box(&verts);
1321 let vol_before = (bb_before.1[0] - bb_before.0[0])
1322 * (bb_before.1[1] - bb_before.0[1])
1323 * (bb_before.1[2] - bb_before.0[2]);
1324
1325 let laplacian_smoother = LaplacianSmoothing::new(0.5, 20);
1326 let laplacian_result = laplacian_smoother.smooth(&verts, &faces);
1327 let bb_laplacian = MeshBoolean::bounding_box(&laplacian_result);
1328 let vol_laplacian = (bb_laplacian.1[0] - bb_laplacian.0[0])
1329 * (bb_laplacian.1[1] - bb_laplacian.0[1])
1330 * (bb_laplacian.1[2] - bb_laplacian.0[2]);
1331
1332 let taubin_smoother = TaubinSmoothing::new(0.5, 20);
1333 let taubin_result = taubin_smoother.smooth(&verts, &faces);
1334 let bb_taubin = MeshBoolean::bounding_box(&taubin_result);
1335 let vol_taubin = (bb_taubin.1[0] - bb_taubin.0[0])
1336 * (bb_taubin.1[1] - bb_taubin.0[1])
1337 * (bb_taubin.1[2] - bb_taubin.0[2]);
1338
1339 let laplacian_loss = (vol_before - vol_laplacian).abs();
1341 let taubin_loss = (vol_before - vol_taubin).abs();
1342 assert!(
1343 taubin_loss <= laplacian_loss + 1e-10,
1344 "Taubin volume loss {:.6} > Laplacian loss {:.6}",
1345 taubin_loss,
1346 laplacian_loss
1347 );
1348 }
1349
1350 #[test]
1353 fn test_loop_subdivision_face_count_times_four() {
1354 let (verts, faces) = equilateral_triangle();
1355 let n_faces_before = faces.len();
1356 let sub = SubdivisionLoop::new(1);
1357 let (_, new_faces) = sub.subdivide(&verts, &faces);
1358 assert_eq!(
1359 new_faces.len(),
1360 n_faces_before * 4,
1361 "expected {} faces, got {}",
1362 n_faces_before * 4,
1363 new_faces.len()
1364 );
1365 }
1366
1367 #[test]
1368 fn test_loop_subdivision_two_levels_face_count() {
1369 let (verts, faces) = equilateral_triangle();
1370 let n_faces_before = faces.len();
1371 let sub = SubdivisionLoop::new(2);
1372 let (_, new_faces) = sub.subdivide(&verts, &faces);
1373 assert_eq!(new_faces.len(), n_faces_before * 16);
1374 }
1375
1376 #[test]
1377 fn test_loop_subdivision_vertex_count_increases() {
1378 let (verts, faces) = equilateral_triangle();
1379 let n_verts_before = verts.len();
1380 let sub = SubdivisionLoop::new(1);
1381 let (new_verts, _) = sub.subdivide(&verts, &faces);
1382 assert!(new_verts.len() > n_verts_before);
1383 }
1384
1385 #[test]
1386 fn test_loop_subdivision_tetrahedron_face_count() {
1387 let (verts, faces) = regular_tetrahedron();
1388 let n_before = faces.len();
1389 let sub = SubdivisionLoop::new(1);
1390 let (_, new_faces) = sub.subdivide(&verts, &faces);
1391 assert_eq!(new_faces.len(), n_before * 4);
1392 }
1393
1394 #[test]
1397 fn test_qem_error_increases_with_more_collapses() {
1398 let (verts, faces) = regular_tetrahedron();
1399 let mut verts1 = verts.clone();
1400 let mut faces1 = faces.clone();
1401 let mut dec1 = MeshDecimation::new(&verts1, &faces1, faces1.len().saturating_sub(1));
1402 dec1.decimate(&mut verts1, &mut faces1);
1403 let total_error1: f64 = verts1
1404 .iter()
1405 .enumerate()
1406 .map(|(i, v)| MeshDecimation::quadric_error(&dec1.quadrics[i], v))
1407 .sum();
1408
1409 let mut verts2 = verts.clone();
1410 let mut faces2 = faces.clone();
1411 let mut dec2 = MeshDecimation::new(&verts2, &faces2, faces2.len().saturating_sub(2));
1412 dec2.decimate(&mut verts2, &mut faces2);
1413 let total_error2: f64 = verts2
1414 .iter()
1415 .enumerate()
1416 .map(|(i, v)| MeshDecimation::quadric_error(&dec2.quadrics[i], v))
1417 .sum();
1418
1419 assert!(
1421 total_error2 >= total_error1 - 1e-10,
1422 "error2 ({:.6}) < error1 ({:.6})",
1423 total_error2,
1424 total_error1
1425 );
1426 }
1427
1428 #[test]
1429 fn test_qem_face_count_decreases() {
1430 let (mut verts, mut faces) = regular_tetrahedron();
1431 let n_before = faces.len();
1432 let mut dec = MeshDecimation::new(&verts, &faces, n_before.saturating_sub(1));
1433 dec.decimate(&mut verts, &mut faces);
1434 assert!(faces.len() <= n_before);
1435 }
1436
1437 #[test]
1438 fn test_qem_quadric_initialization() {
1439 let (verts, faces) = equilateral_triangle();
1440 let quadrics = MeshDecimation::initialize_quadrics(&verts, &faces);
1441 assert_eq!(quadrics.len(), verts.len());
1442 let all_zero = quadrics.iter().all(|q| q.iter().all(|&x| x == 0.0));
1444 assert!(!all_zero);
1445 }
1446
1447 #[test]
1450 fn test_normal_estimation_unit_length() {
1451 let (verts, faces) = equilateral_triangle();
1452 let estimator = MeshNormalEstimation::new(false);
1453 let normals = estimator.compute(&verts, &faces);
1454 for (i, n) in normals.iter().enumerate() {
1455 let len = vec_len(n);
1456 assert!(
1457 (len - 1.0).abs() < 1e-10 || len < 1e-15,
1458 "normal[{i}] length = {:.6}",
1459 len
1460 );
1461 }
1462 }
1463
1464 #[test]
1465 fn test_angle_weighted_normal_unit_length() {
1466 let (verts, faces) = equilateral_triangle();
1467 let normals = MeshNormalEstimation::compute_angle_weighted(&verts, &faces);
1468 for n in &normals {
1469 let len = vec_len(n);
1470 assert!(
1471 (len - 1.0).abs() < 1e-10 || len < 1e-15,
1472 "angle-weighted normal length = {:.6}",
1473 len
1474 );
1475 }
1476 }
1477
1478 #[test]
1479 fn test_normals_point_in_same_hemisphere() {
1480 let (verts, faces) = unit_quad();
1481 let normals = MeshNormalEstimation::compute_area_weighted(&verts, &faces);
1482 let signs: Vec<f64> = normals.iter().map(|n| n[2].signum()).collect();
1484 let all_same = signs.windows(2).all(|w| w[0] == w[1]);
1485 assert!(all_same, "normals point in different hemispheres");
1486 }
1487
1488 #[test]
1489 fn test_normal_estimation_both_methods_consistent() {
1490 let (verts, faces) = equilateral_triangle();
1491 let normals_area = MeshNormalEstimation::compute_area_weighted(&verts, &faces);
1492 let normals_angle = MeshNormalEstimation::compute_angle_weighted(&verts, &faces);
1493 for (na, nb) in normals_area.iter().zip(normals_angle.iter()) {
1495 for k in 0..3 {
1496 assert!(
1497 (na[k] - nb[k]).abs() < 1e-10,
1498 "normals differ at component {k}: {:.6} vs {:.6}",
1499 na[k],
1500 nb[k]
1501 );
1502 }
1503 }
1504 }
1505
1506 #[test]
1509 fn test_triangle_area_equilateral() {
1510 let h = 3.0_f64.sqrt() / 2.0;
1511 let a = [0.0, 0.0, 0.0];
1512 let b = [1.0, 0.0, 0.0];
1513 let c = [0.5, h, 0.0];
1514 let area = triangle_area(&a, &b, &c);
1515 let expected = 3.0_f64.sqrt() / 4.0;
1516 assert!((area - expected).abs() < 1e-12, "area = {:.6}", area);
1517 }
1518
1519 #[test]
1520 fn test_face_normal_orthogonal_to_edges() {
1521 let a = [0.0, 0.0, 0.0];
1522 let b = [1.0, 0.0, 0.0];
1523 let c = [0.0, 1.0, 0.0];
1524 let n = face_normal(&a, &b, &c);
1525 let ab = vec_sub(&b, &a);
1526 let ac = vec_sub(&c, &a);
1527 assert!(dot(&n, &ab).abs() < 1e-12);
1528 assert!(dot(&n, &ac).abs() < 1e-12);
1529 }
1530
1531 #[test]
1532 fn test_cotan_smoothing_produces_valid_vertices() {
1533 let (verts, faces) = unit_quad();
1534 let smoother = CotanSmoothing::new(0.1, 3);
1535 let smoothed = smoother.smooth(&verts, &faces);
1536 assert_eq!(smoothed.len(), verts.len());
1537 for v in &smoothed {
1538 for k in 0..3 {
1539 assert!(v[k].is_finite(), "vertex component[{k}] is not finite");
1540 }
1541 }
1542 }
1543
1544 #[test]
1545 fn test_mesh_boolean_union_vertex_count() {
1546 let (va, fa) = equilateral_triangle();
1547 let (vb, fb) = equilateral_triangle();
1548 let boolean = MeshBoolean::new(1e-10);
1549 let result = boolean.mesh_union(&va, &fa, &vb, &fb);
1550 assert_eq!(result.vertices.len(), va.len() + vb.len());
1551 assert_eq!(result.faces.len(), fa.len() + fb.len());
1552 }
1553
1554 #[test]
1555 fn test_mesh_boolean_face_indices_valid() {
1556 let (va, fa) = unit_quad();
1557 let (vb, fb) = unit_quad();
1558 let boolean = MeshBoolean::new(1e-10);
1559 let result = boolean.mesh_union(&va, &fa, &vb, &fb);
1560 for face in &result.faces {
1561 for &idx in face.iter() {
1562 assert!(
1563 idx < result.vertices.len(),
1564 "face index {idx} >= vertex count {}",
1565 result.vertices.len()
1566 );
1567 }
1568 }
1569 }
1570
1571 #[test]
1572 fn test_isotropic_remeshing_produces_valid_mesh() {
1573 let (mut verts, mut faces) = unit_quad();
1574 let remesher = IsotropicRemeshing::new(0.3, 2);
1575 remesher.remesh(&mut verts, &mut faces);
1576 for face in &faces {
1578 for &idx in face.iter() {
1579 assert!(idx < verts.len());
1580 }
1581 }
1582 }
1583
1584 #[test]
1585 fn test_boundary_vertex_detection_quad() {
1586 let (verts, faces) = unit_quad();
1587 let is_boundary = LaplacianSmoothing::boundary_vertices(&verts, &faces);
1588 let boundary_count = is_boundary.iter().filter(|&&b| b).count();
1590 assert!(boundary_count > 0, "no boundary vertices found");
1591 }
1592
1593 #[test]
1594 fn test_bounding_box_contains_all_vertices() {
1595 let (verts, _) = regular_tetrahedron();
1596 let (mn, mx) = MeshBoolean::bounding_box(&verts);
1597 for v in &verts {
1598 for k in 0..3 {
1599 assert!(
1600 v[k] >= mn[k] - 1e-15 && v[k] <= mx[k] + 1e-15,
1601 "vertex component[{k}] = {:.6} outside bbox [{:.6}, {:.6}]",
1602 v[k],
1603 mn[k],
1604 mx[k]
1605 );
1606 }
1607 }
1608 }
1609
1610 #[test]
1611 fn test_cotan_weights_computed() {
1612 let (verts, faces) = equilateral_triangle();
1613 let weights = CotanSmoothing::build_cotan_weights(&verts, &faces);
1614 assert_eq!(weights.len(), verts.len());
1615 let total_entries: usize = weights.iter().map(|w| w.len()).sum();
1617 assert!(total_entries > 0);
1618 }
1619
1620 #[test]
1621 fn test_loop_beta_valence_3() {
1622 let beta = SubdivisionLoop::loop_beta(3);
1623 assert!((beta - 3.0 / 16.0).abs() < 1e-12);
1624 }
1625
1626 #[test]
1627 fn test_loop_beta_high_valence() {
1628 let beta = SubdivisionLoop::loop_beta(6);
1629 let expected = 3.0 / 48.0;
1630 assert!((beta - expected).abs() < 1e-12);
1631 }
1632}