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