1use std::collections::HashMap;
10
11#[inline]
16fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
17 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
18}
19
20#[inline]
21fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
22 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
23}
24
25#[inline]
26fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
27 [a[0] * s, a[1] * s, a[2] * s]
28}
29
30#[inline]
31fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
32 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
33}
34
35#[inline]
36fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
37 [
38 a[1] * b[2] - a[2] * b[1],
39 a[2] * b[0] - a[0] * b[2],
40 a[0] * b[1] - a[1] * b[0],
41 ]
42}
43
44#[inline]
45fn len3(a: [f64; 3]) -> f64 {
46 dot3(a, a).sqrt()
47}
48
49#[inline]
50fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
51 len3(sub3(a, b))
52}
53
54#[inline]
55fn normalize3(a: [f64; 3]) -> [f64; 3] {
56 let l = len3(a);
57 if l < f64::EPSILON {
58 [0.0, 0.0, 0.0]
59 } else {
60 [a[0] / l, a[1] / l, a[2] / l]
61 }
62}
63
64fn angle_at(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
66 let ab = normalize3(sub3(b, a));
67 let ac = normalize3(sub3(c, a));
68 dot3(ab, ac).clamp(-1.0, 1.0).acos()
69}
70
71pub struct TetrahedralMesh {
77 pub vertices: Vec<[f64; 3]>,
79 pub tets: Vec<[usize; 4]>,
81}
82
83impl TetrahedralMesh {
84 pub fn new(vertices: Vec<[f64; 3]>, tets: Vec<[usize; 4]>) -> Self {
86 Self { vertices, tets }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq)]
96pub struct MeshQuality {
97 pub aspect_ratio: f64,
99 pub min_angle_deg: f64,
101 pub max_angle_deg: f64,
103 pub skewness: f64,
105 pub jacobian: f64,
107}
108
109impl MeshQuality {
110 pub fn is_acceptable(&self) -> bool {
114 self.aspect_ratio < 5.0 && self.skewness < 0.9 && self.min_angle_deg > 5.0
115 }
116
117 pub fn is_degenerate(&self) -> bool {
119 self.jacobian.abs() < 1e-10
120 }
121}
122
123pub fn compute_tet_quality(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> MeshQuality {
132 let e01 = sub3(v1, v0);
134 let e02 = sub3(v2, v0);
135 let e03 = sub3(v3, v0);
136 let e12 = sub3(v2, v1);
137 let e13 = sub3(v3, v1);
138 let e23 = sub3(v3, v2);
139
140 let edge_lengths = [
142 len3(e01),
143 len3(e02),
144 len3(e03),
145 len3(e12),
146 len3(e13),
147 len3(e23),
148 ];
149
150 let min_len = edge_lengths.iter().cloned().fold(f64::INFINITY, f64::min);
151 let max_len = edge_lengths.iter().cloned().fold(0.0_f64, f64::max);
152
153 let aspect_ratio = if min_len < f64::EPSILON {
154 f64::INFINITY
155 } else {
156 max_len / min_len
157 };
158
159 let jac = dot3(e01, cross3(e02, e03));
161 let mean_edge = edge_lengths.iter().sum::<f64>() / 6.0;
163 let ideal_jac = mean_edge * mean_edge * mean_edge * (2.0_f64).sqrt();
164 let scaled_jac = if ideal_jac < f64::EPSILON {
167 0.0
168 } else {
169 jac.abs() / ideal_jac
170 };
171
172 let n012 = normalize3(cross3(e01, e02));
179 let n013 = normalize3(cross3(e03, e01));
180 let n023 = normalize3(cross3(e02, e03));
181 let n123 = normalize3(cross3(e12, e13));
182
183 let dihedral_angle = |na: [f64; 3], nb: [f64; 3]| -> f64 {
185 let cos_a = dot3(na, nb).clamp(-1.0, 1.0);
186 std::f64::consts::PI - cos_a.acos()
188 };
189
190 let dihedrals = [
198 dihedral_angle(n012, n013),
199 dihedral_angle(n012, n023),
200 dihedral_angle(n013, n023),
201 dihedral_angle(n012, n123),
202 dihedral_angle(n013, n123),
203 dihedral_angle(n023, n123),
204 ];
205
206 let rad_to_deg = 180.0 / std::f64::consts::PI;
207 let min_angle_deg = dihedrals.iter().cloned().fold(f64::INFINITY, f64::min) * rad_to_deg;
208 let max_angle_deg = dihedrals.iter().cloned().fold(0.0_f64, f64::max) * rad_to_deg;
209
210 let ideal_dihedral = 70.529;
213 let skewness = ((max_angle_deg - ideal_dihedral) / (180.0 - ideal_dihedral)).clamp(0.0, 1.0);
214
215 MeshQuality {
216 aspect_ratio,
217 min_angle_deg,
218 max_angle_deg,
219 skewness,
220 jacobian: scaled_jac,
221 }
222}
223
224pub fn compute_tri_quality(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> MeshQuality {
233 let a = dist3(v1, v2);
234 let b = dist3(v0, v2);
235 let c = dist3(v0, v1);
236
237 let min_len = a.min(b).min(c);
238 let max_len = a.max(b).max(c);
239
240 let aspect_ratio = if min_len < f64::EPSILON {
241 f64::INFINITY
242 } else {
243 max_len / min_len
244 };
245
246 let angle_a = angle_at(v0, v1, v2);
247 let angle_b = angle_at(v1, v0, v2);
248 let angle_c = angle_at(v2, v0, v1);
249
250 let rad_to_deg = 180.0 / std::f64::consts::PI;
251 let angles_deg = [
252 angle_a * rad_to_deg,
253 angle_b * rad_to_deg,
254 angle_c * rad_to_deg,
255 ];
256
257 let min_angle_deg = angles_deg.iter().cloned().fold(f64::INFINITY, f64::min);
258 let max_angle_deg = angles_deg.iter().cloned().fold(0.0_f64, f64::max);
259
260 let ideal = 60.0;
262 let skewness = ((max_angle_deg - ideal) / (180.0 - ideal)).clamp(0.0, 1.0);
263
264 let area = {
267 let cross = cross3(sub3(v1, v0), sub3(v2, v0));
268 len3(cross) * 0.5
269 };
270 let denom = a * b;
271 let jacobian = if denom < f64::EPSILON {
272 0.0
273 } else {
274 (2.0 * area / denom).clamp(-1.0, 1.0)
275 };
276
277 MeshQuality {
278 aspect_ratio,
279 min_angle_deg,
280 max_angle_deg,
281 skewness,
282 jacobian,
283 }
284}
285
286pub fn repair_degenerate_tets(mesh: &mut TetrahedralMesh) -> usize {
295 let threshold = 1e-12_f64;
296 let before = mesh.tets.len();
297 mesh.tets.retain(|tet| {
298 let v0 = mesh.vertices[tet[0]];
299 let v1 = mesh.vertices[tet[1]];
300 let v2 = mesh.vertices[tet[2]];
301 let v3 = mesh.vertices[tet[3]];
302 let e01 = sub3(v1, v0);
303 let e02 = sub3(v2, v0);
304 let e03 = sub3(v3, v0);
305 let vol = dot3(e01, cross3(e02, e03)).abs() / 6.0;
306 vol > threshold
307 });
308 before - mesh.tets.len()
309}
310
311pub fn smooth_laplacian(positions: &mut [[f64; 3]], tets: &[[usize; 4]], iterations: usize) {
325 let n = positions.len();
326 let mut tet_count = vec![0usize; n];
328 for tet in tets {
329 for &vi in tet.iter() {
330 tet_count[vi] += 1;
331 }
332 }
333
334 for _ in 0..iterations {
335 let mut sums = vec![[0.0_f64; 3]; n];
336 let mut counts = vec![0usize; n];
337
338 for tet in tets {
339 for k in 0..4 {
340 let vi = tet[k];
341 for (j, &vj) in tet.iter().enumerate() {
343 if j != k {
344 sums[vi] = add3(sums[vi], positions[vj]);
345 counts[vi] += 1;
346 }
347 }
348 }
349 }
350
351 for i in 0..n {
352 if counts[i] > 0 && tet_count[i] >= 4 {
354 let avg = scale3(sums[i], 1.0 / counts[i] as f64);
355 positions[i] = avg;
356 }
357 }
358 }
359}
360
361pub fn flip_edges_2d(triangles: &mut [[usize; 3]], positions: &[[f64; 3]]) -> usize {
374 let mut flips = 0usize;
375 let mut changed = true;
376
377 while changed {
378 changed = false;
379
380 let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
382 for (ti, tri) in triangles.iter().enumerate() {
383 for k in 0..3 {
384 let a = tri[k];
385 let b = tri[(k + 1) % 3];
386 let opp = tri[(k + 2) % 3]; let key = (a.min(b), a.max(b));
388 edge_map.entry(key).or_default().push((ti, opp));
389 }
390 }
391
392 for ((_ea, _eb), tris) in &edge_map {
393 if tris.len() != 2 {
394 continue; }
396 let (ti, opp_i) = tris[0];
397 let (tj, opp_j) = tris[1];
398
399 let ea = _ea;
401 let eb = _eb;
402
403 let pa = positions[*ea];
404 let pb = positions[*eb];
405 let pc = positions[opp_i];
406 let pd = positions[opp_j];
407
408 if in_circumcircle_2d(pa, pb, pc, pd) {
410 let new_t0 = [*ea, opp_i, opp_j];
412 let new_t1 = [*eb, opp_j, opp_i];
413
414 if signed_area_2d(
416 positions[new_t0[0]],
417 positions[new_t0[1]],
418 positions[new_t0[2]],
419 ) > 0.0
420 && signed_area_2d(
421 positions[new_t1[0]],
422 positions[new_t1[1]],
423 positions[new_t1[2]],
424 ) > 0.0
425 {
426 triangles[ti] = new_t0;
427 triangles[tj] = new_t1;
428 flips += 1;
429 changed = true;
430 break; }
432 }
433 }
434 }
435
436 flips
437}
438
439fn signed_area_2d(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
441 0.5 * ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]))
442}
443
444fn in_circumcircle_2d(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> bool {
448 let ax = a[0] - d[0];
449 let ay = a[1] - d[1];
450 let bx = b[0] - d[0];
451 let by = b[1] - d[1];
452 let cx = c[0] - d[0];
453 let cy = c[1] - d[1];
454
455 let det = ax * (by * (cx * cx + cy * cy) - cy * (bx * bx + by * by))
456 - ay * (bx * (cx * cx + cy * cy) - cx * (bx * bx + by * by))
457 + (ax * ax + ay * ay) * (bx * cy - by * cx);
458
459 det > 0.0
460}
461
462pub fn tet_jacobian_matrix(
471 v0: [f64; 3],
472 v1: [f64; 3],
473 v2: [f64; 3],
474 v3: [f64; 3],
475) -> [[f64; 3]; 3] {
476 let e01 = sub3(v1, v0);
477 let e02 = sub3(v2, v0);
478 let e03 = sub3(v3, v0);
479 [e01, e02, e03]
480}
481
482pub fn tet_jacobian_det(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
486 let j = tet_jacobian_matrix(v0, v1, v2, v3);
487 dot3(j[0], cross3(j[1], j[2]))
489}
490
491pub fn tet_scaled_jacobian(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
497 let j = tet_jacobian_matrix(v0, v1, v2, v3);
498 let det = dot3(j[0], cross3(j[1], j[2]));
499
500 let l0 = len3(j[0]);
501 let l1 = len3(j[1]);
502 let l2 = len3(j[2]);
503
504 let denom = l0 * l1 * l2;
505 if denom < f64::EPSILON {
506 return 0.0;
507 }
508
509 let raw = det / denom;
512 raw / (2.0_f64).sqrt()
513}
514
515pub fn tet_min_scaled_jacobian(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
520 let sj0 = tet_scaled_jacobian(v0, v1, v2, v3);
523 let sj1 = tet_scaled_jacobian(v1, v2, v0, v3);
524 let sj2 = tet_scaled_jacobian(v2, v0, v1, v3);
525 let sj3 = tet_scaled_jacobian(v3, v1, v0, v2);
526 sj0.min(sj1).min(sj2).min(sj3)
527}
528
529pub fn tet_condition_number_quality(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
539 let j = tet_jacobian_matrix(v0, v1, v2, v3);
540 let det = dot3(j[0], cross3(j[1], j[2]));
541
542 if det.abs() < f64::EPSILON {
543 return 0.0;
544 }
545
546 let mut frob_j = 0.0;
548 for row in &j {
549 frob_j += dot3(*row, *row);
550 }
551 frob_j = frob_j.sqrt();
552
553 let inv_det = 1.0 / det;
555 let j_inv = [
556 scale3(cross3(j[1], j[2]), inv_det),
557 scale3(cross3(j[2], j[0]), inv_det),
558 scale3(cross3(j[0], j[1]), inv_det),
559 ];
560
561 let mut frob_jinv = 0.0;
563 for row in &j_inv {
564 frob_jinv += dot3(*row, *row);
565 }
566 frob_jinv = frob_jinv.sqrt();
567
568 let kappa = frob_j * frob_jinv;
569 let ideal_kappa = 3.0_f64.sqrt();
572 (ideal_kappa / kappa).clamp(0.0, 1.0)
573}
574
575pub struct QualityHistogram {
581 pub bin_edges: Vec<f64>,
583 pub counts: Vec<usize>,
585 pub total: usize,
587}
588
589impl QualityHistogram {
590 pub fn from_values(values: &[f64], n_bins: usize) -> Self {
594 assert!(n_bins > 0, "n_bins must be > 0");
595 let mut counts = vec![0usize; n_bins];
596 let bin_width = 1.0 / n_bins as f64;
597 let mut bin_edges = Vec::with_capacity(n_bins + 1);
598 for i in 0..=n_bins {
599 bin_edges.push(i as f64 * bin_width);
600 }
601
602 for &v in values {
603 let bin = ((v / bin_width) as usize).min(n_bins - 1);
604 counts[bin] += 1;
605 }
606
607 Self {
608 bin_edges,
609 counts,
610 total: values.len(),
611 }
612 }
613
614 pub fn fractions(&self) -> Vec<f64> {
616 if self.total == 0 {
617 return vec![0.0; self.counts.len()];
618 }
619 self.counts
620 .iter()
621 .map(|&c| c as f64 / self.total as f64)
622 .collect()
623 }
624
625 pub fn peak_bin(&self) -> usize {
627 self.counts
628 .iter()
629 .enumerate()
630 .max_by_key(|(_, c)| **c)
631 .map(|(i, _)| i)
632 .unwrap_or(0)
633 }
634}
635
636pub fn find_worst_elements(qualities: &[f64], threshold: f64) -> Vec<usize> {
645 let mut bad: Vec<(usize, f64)> = qualities
646 .iter()
647 .enumerate()
648 .filter(|(_, q)| *q < &threshold)
649 .map(|(i, q)| (i, *q))
650 .collect();
651 bad.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
652 bad.iter().map(|&(i, _)| i).collect()
653}
654
655pub struct QualityStats {
657 pub min: f64,
659 pub max: f64,
661 pub mean: f64,
663 pub std_dev: f64,
665 pub n_poor: usize,
667}
668
669impl QualityStats {
670 pub fn compute(values: &[f64]) -> Self {
672 if values.is_empty() {
673 return Self {
674 min: 0.0,
675 max: 0.0,
676 mean: 0.0,
677 std_dev: 0.0,
678 n_poor: 0,
679 };
680 }
681 let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
682 let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
683 let mean = values.iter().sum::<f64>() / values.len() as f64;
684 let variance =
685 values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
686 let std_dev = variance.sqrt();
687 let n_poor = values.iter().filter(|&&v| v < 0.3).count();
688 Self {
689 min,
690 max,
691 mean,
692 std_dev,
693 n_poor,
694 }
695 }
696}
697
698pub fn mesh_scaled_jacobian_qualities(mesh: &TetrahedralMesh) -> Vec<f64> {
700 mesh.tets
701 .iter()
702 .map(|tet| {
703 let v0 = mesh.vertices[tet[0]];
704 let v1 = mesh.vertices[tet[1]];
705 let v2 = mesh.vertices[tet[2]];
706 let v3 = mesh.vertices[tet[3]];
707 tet_min_scaled_jacobian(v0, v1, v2, v3)
708 })
709 .collect()
710}
711
712pub fn mesh_condition_number_qualities(mesh: &TetrahedralMesh) -> Vec<f64> {
714 mesh.tets
715 .iter()
716 .map(|tet| {
717 let v0 = mesh.vertices[tet[0]];
718 let v1 = mesh.vertices[tet[1]];
719 let v2 = mesh.vertices[tet[2]];
720 let v3 = mesh.vertices[tet[3]];
721 tet_condition_number_quality(v0, v1, v2, v3)
722 })
723 .collect()
724}
725
726pub fn tet_dihedral_angle_extremes(
734 v0: [f64; 3],
735 v1: [f64; 3],
736 v2: [f64; 3],
737 v3: [f64; 3],
738) -> (f64, f64) {
739 let q = compute_tet_quality(v0, v1, v2, v3);
740 (q.min_angle_deg, q.max_angle_deg)
741}
742
743pub fn tri_angle_extremes(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> (f64, f64) {
747 let q = compute_tri_quality(v0, v1, v2);
748 (q.min_angle_deg, q.max_angle_deg)
749}
750
751pub fn line_aspect_ratio(_v0: [f64; 3], _v1: [f64; 3]) -> f64 {
757 1.0
758}
759
760pub fn quad_aspect_ratio(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
764 let d1 = dist3(v0, v2);
766 let d2 = dist3(v1, v3);
767 let min_d = d1.min(d2);
768 let max_d = d1.max(d2);
769 if min_d < f64::EPSILON {
770 f64::INFINITY
771 } else {
772 max_d / min_d
773 }
774}
775
776pub fn wedge_aspect_ratio(
780 v0: [f64; 3],
781 v1: [f64; 3],
782 v2: [f64; 3],
783 v3: [f64; 3],
784 v4: [f64; 3],
785 v5: [f64; 3],
786) -> f64 {
787 let edges = [
788 dist3(v0, v1),
789 dist3(v1, v2),
790 dist3(v2, v0), dist3(v3, v4),
792 dist3(v4, v5),
793 dist3(v5, v3), dist3(v0, v3),
795 dist3(v1, v4),
796 dist3(v2, v5), ];
798 let min_e = edges.iter().cloned().fold(f64::INFINITY, f64::min);
799 let max_e = edges.iter().cloned().fold(0.0_f64, f64::max);
800 if min_e < f64::EPSILON {
801 f64::INFINITY
802 } else {
803 max_e / min_e
804 }
805}
806
807pub fn hex_aspect_ratio(
811 v0: [f64; 3],
812 v1: [f64; 3],
813 v2: [f64; 3],
814 v3: [f64; 3],
815 v4: [f64; 3],
816 v5: [f64; 3],
817 v6: [f64; 3],
818 v7: [f64; 3],
819) -> f64 {
820 let edges = [
821 dist3(v0, v1),
822 dist3(v1, v2),
823 dist3(v2, v3),
824 dist3(v3, v0), dist3(v4, v5),
826 dist3(v5, v6),
827 dist3(v6, v7),
828 dist3(v7, v4), dist3(v0, v4),
830 dist3(v1, v5),
831 dist3(v2, v6),
832 dist3(v3, v7), ];
834 let min_e = edges.iter().cloned().fold(f64::INFINITY, f64::min);
835 let max_e = edges.iter().cloned().fold(0.0_f64, f64::max);
836 if min_e < f64::EPSILON {
837 f64::INFINITY
838 } else {
839 max_e / min_e
840 }
841}
842
843pub fn quality_guided_smoothing(
855 positions: &mut [[f64; 3]],
856 tets: &[[usize; 4]],
857 iterations: usize,
858) -> usize {
859 let n = positions.len();
860 let mut tet_count = vec![0usize; n];
861 for tet in tets {
862 for &vi in tet.iter() {
863 tet_count[vi] += 1;
864 }
865 }
866
867 let mut total_accepted = 0usize;
868
869 for _ in 0..iterations {
870 let mut sums = vec![[0.0_f64; 3]; n];
871 let mut counts = vec![0usize; n];
872
873 for tet in tets.iter() {
874 for k in 0..4 {
875 let vi = tet[k];
876 for (j, &vj) in tet.iter().enumerate() {
877 if j != k {
878 sums[vi] = add3(sums[vi], positions[vj]);
879 counts[vi] += 1;
880 }
881 }
882 }
883 }
884
885 for i in 0..n {
886 if counts[i] == 0 || tet_count[i] < 2 {
887 continue;
888 }
889 let new_pos = scale3(sums[i], 1.0 / counts[i] as f64);
890
891 let min_q_before = tets
893 .iter()
894 .filter(|t| t.contains(&i))
895 .map(|t| {
896 let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
897 tet_scaled_jacobian(vv[0], vv[1], vv[2], vv[3]).abs()
898 })
899 .fold(f64::INFINITY, f64::min);
900
901 let old_pos = positions[i];
903 positions[i] = new_pos;
904
905 let min_q_after = tets
906 .iter()
907 .filter(|t| t.contains(&i))
908 .map(|t| {
909 let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
910 tet_scaled_jacobian(vv[0], vv[1], vv[2], vv[3]).abs()
911 })
912 .fold(f64::INFINITY, f64::min);
913
914 if min_q_after >= min_q_before {
915 total_accepted += 1; } else {
917 positions[i] = old_pos; }
919 }
920 }
921
922 total_accepted
923}
924
925pub fn minimum_angle_optimization(
937 positions: &mut [[f64; 3]],
938 triangles: &[[usize; 3]],
939 iterations: usize,
940) -> usize {
941 let n = positions.len();
942
943 let mut tri_count = vec![0usize; n];
945 for tri in triangles {
946 for &vi in tri.iter() {
947 tri_count[vi] += 1;
948 }
949 }
950
951 let mut total_accepted = 0usize;
952
953 for _ in 0..iterations {
954 for i in 0..n {
955 if tri_count[i] < 2 {
956 continue; }
958
959 let mut sum = [0.0_f64; 3];
961 let mut cnt = 0usize;
962 for tri in triangles.iter().filter(|t| t.contains(&i)) {
963 for &j in tri.iter() {
964 if j != i {
965 sum = add3(sum, positions[j]);
966 cnt += 1;
967 }
968 }
969 }
970 if cnt == 0 {
971 continue;
972 }
973 let new_pos = scale3(sum, 1.0 / cnt as f64);
974
975 let min_before = triangles
977 .iter()
978 .filter(|t| t.contains(&i))
979 .map(|t| {
980 let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
981 compute_tri_quality(vv[0], vv[1], vv[2]).min_angle_deg
982 })
983 .fold(f64::INFINITY, f64::min);
984
985 let old_pos = positions[i];
986 positions[i] = new_pos;
987
988 let min_after = triangles
989 .iter()
990 .filter(|t| t.contains(&i))
991 .map(|t| {
992 let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
993 compute_tri_quality(vv[0], vv[1], vv[2]).min_angle_deg
994 })
995 .fold(f64::INFINITY, f64::min);
996
997 if min_after >= min_before {
998 total_accepted += 1;
999 } else {
1000 positions[i] = old_pos;
1001 }
1002 }
1003 }
1004
1005 total_accepted
1006}
1007
1008#[cfg(test)]
1013mod tests {
1014 use super::*;
1015 use std::f64::consts::PI;
1016
1017 fn unit_tet_vertices() -> ([f64; 3], [f64; 3], [f64; 3], [f64; 3]) {
1019 let v0 = [1.0, 1.0, 1.0];
1021 let v1 = [-1.0, 1.0, -1.0];
1022 let v2 = [1.0, -1.0, -1.0];
1023 let v3 = [-1.0, -1.0, 1.0];
1024 (v0, v1, v2, v3)
1025 }
1026
1027 #[test]
1028 fn test_unit_tet_aspect_ratio() {
1029 let (v0, v1, v2, v3) = unit_tet_vertices();
1030 let q = compute_tet_quality(v0, v1, v2, v3);
1031 assert!(
1033 (q.aspect_ratio - 1.0).abs() < 1e-10,
1034 "aspect_ratio={}, expected 1.0",
1035 q.aspect_ratio
1036 );
1037 }
1038
1039 #[test]
1040 fn test_unit_tet_jacobian_positive() {
1041 let (v0, v1, v2, v3) = unit_tet_vertices();
1042 let q = compute_tet_quality(v0, v1, v2, v3);
1043 assert!(
1044 q.jacobian > 0.0,
1045 "jacobian should be positive, got {}",
1046 q.jacobian
1047 );
1048 }
1049
1050 #[test]
1051 fn test_degenerate_tet_detected() {
1052 let v0 = [0.0, 0.0, 0.0];
1054 let v1 = [1.0, 0.0, 0.0];
1055 let v2 = [0.0, 1.0, 0.0];
1056 let v3 = [0.5, 0.5, 0.0]; let q = compute_tet_quality(v0, v1, v2, v3);
1058 assert!(
1059 q.is_degenerate(),
1060 "coplanar tet should be degenerate, jacobian={}",
1061 q.jacobian
1062 );
1063 }
1064
1065 #[test]
1066 fn test_repair_degenerate_tets() {
1067 let verts = vec![
1068 [0.0_f64, 0.0, 0.0],
1069 [1.0, 0.0, 0.0],
1070 [0.0, 1.0, 0.0],
1071 [0.0, 0.0, 1.0],
1072 [0.5, 0.5, 0.0], ];
1074 let tets = vec![
1075 [0usize, 1, 2, 3], [0, 1, 2, 4], ];
1078 let mut mesh = TetrahedralMesh::new(verts, tets);
1079 let removed = repair_degenerate_tets(&mut mesh);
1080 assert_eq!(removed, 1, "expected 1 degenerate tet removed");
1081 assert_eq!(mesh.tets.len(), 1);
1082 }
1083
1084 #[test]
1085 fn test_equilateral_tri_quality() {
1086 let h = (3.0_f64).sqrt() / 2.0;
1088 let v0 = [0.0, 0.0, 0.0];
1089 let v1 = [1.0, 0.0, 0.0];
1090 let v2 = [0.5, h, 0.0];
1091 let q = compute_tri_quality(v0, v1, v2);
1092 assert!(
1093 (q.aspect_ratio - 1.0).abs() < 1e-10,
1094 "aspect_ratio={}, expected 1.0",
1095 q.aspect_ratio
1096 );
1097 assert!(
1098 (q.min_angle_deg - 60.0).abs() < 1e-6,
1099 "min_angle={}, expected 60°",
1100 q.min_angle_deg
1101 );
1102 assert!(
1103 (q.skewness).abs() < 1e-6,
1104 "skewness={}, expected 0",
1105 q.skewness
1106 );
1107 }
1108
1109 #[test]
1110 fn test_right_triangle_quality() {
1111 let v0 = [0.0, 0.0, 0.0];
1113 let v1 = [3.0, 0.0, 0.0];
1114 let v2 = [0.0, 4.0, 0.0];
1115 let q = compute_tri_quality(v0, v1, v2);
1116 assert!(
1117 (q.max_angle_deg - 90.0).abs() < 1e-6,
1118 "max_angle={}, expected 90°",
1119 q.max_angle_deg
1120 );
1121 assert!(
1122 q.aspect_ratio > 1.0,
1123 "aspect_ratio should be > 1 for non-equilateral"
1124 );
1125 }
1126
1127 #[test]
1128 fn test_smooth_laplacian_runs() {
1129 let mut positions: Vec<[f64; 3]> = vec![
1130 [0.0, 0.0, 0.0],
1131 [1.0, 0.0, 0.0],
1132 [0.0, 1.0, 0.0],
1133 [0.0, 0.0, 1.0],
1134 [1.0, 1.0, 1.0],
1135 ];
1136 let tets = vec![[0usize, 1, 2, 3], [1, 2, 3, 4]];
1137 smooth_laplacian(&mut positions, &tets, 2);
1138 for p in &positions {
1140 assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
1141 }
1142 }
1143
1144 #[test]
1145 fn test_flip_edges_2d_no_flip_on_delaunay() {
1146 let positions: Vec<[f64; 3]> = 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 mut tris = vec![[0usize, 1, 2], [0, 2, 3]];
1154 let flips = flip_edges_2d(&mut tris, &positions);
1155 let _ = flips;
1157 assert_eq!(tris.len(), 2, "triangle count must stay the same");
1158 }
1159
1160 #[test]
1161 fn test_tet_quality_angles_in_range() {
1162 let (v0, v1, v2, v3) = unit_tet_vertices();
1163 let q = compute_tet_quality(v0, v1, v2, v3);
1164 assert!(q.min_angle_deg > 0.0 && q.min_angle_deg < 180.0);
1165 assert!(q.max_angle_deg > 0.0 && q.max_angle_deg < 180.0);
1166 assert!(q.min_angle_deg <= q.max_angle_deg);
1167 }
1168
1169 #[test]
1170 fn test_degenerate_tri_quality() {
1171 let v0 = [0.0, 0.0, 0.0];
1173 let v1 = [1.0, 0.0, 0.0];
1174 let v2 = [2.0, 0.0, 0.0];
1175 let q = compute_tri_quality(v0, v1, v2);
1176 assert!(
1177 q.jacobian.abs() < 1e-10,
1178 "collinear triangle jacobian should be ~0, got {}",
1179 q.jacobian
1180 );
1181 }
1182
1183 #[test]
1184 fn test_pi_constant_used() {
1185 let half_pi_deg = PI / 2.0 * 180.0 / PI;
1187 assert!((half_pi_deg - 90.0).abs() < 1e-10);
1188 }
1189
1190 #[test]
1193 fn test_tet_jacobian_det_positive() {
1194 let (v0, v1, v2, v3) = unit_tet_vertices();
1195 let det = tet_jacobian_det(v0, v1, v2, v3);
1196 assert!(
1197 det.abs() > 1e-10,
1198 "regular tet should have non-zero Jacobian det"
1199 );
1200 }
1201
1202 #[test]
1203 fn test_tet_jacobian_det_degenerate() {
1204 let v0 = [0.0, 0.0, 0.0];
1205 let v1 = [1.0, 0.0, 0.0];
1206 let v2 = [0.0, 1.0, 0.0];
1207 let v3 = [0.5, 0.5, 0.0]; let det = tet_jacobian_det(v0, v1, v2, v3);
1209 assert!(
1210 det.abs() < 1e-10,
1211 "coplanar tet should have zero Jacobian det"
1212 );
1213 }
1214
1215 #[test]
1216 fn test_tet_jacobian_matrix() {
1217 let v0 = [0.0, 0.0, 0.0];
1218 let v1 = [1.0, 0.0, 0.0];
1219 let v2 = [0.0, 1.0, 0.0];
1220 let v3 = [0.0, 0.0, 1.0];
1221 let j = tet_jacobian_matrix(v0, v1, v2, v3);
1222 assert!((j[0][0] - 1.0).abs() < 1e-10);
1223 assert!((j[1][1] - 1.0).abs() < 1e-10);
1224 assert!((j[2][2] - 1.0).abs() < 1e-10);
1225 }
1226
1227 #[test]
1230 fn test_scaled_jacobian_regular_tet() {
1231 let (v0, v1, v2, v3) = unit_tet_vertices();
1232 let sj = tet_scaled_jacobian(v0, v1, v2, v3);
1233 assert!(
1234 sj > 0.0,
1235 "regular tet should have positive scaled jacobian, got {sj}"
1236 );
1237 }
1238
1239 #[test]
1240 fn test_scaled_jacobian_degenerate() {
1241 let v0 = [0.0, 0.0, 0.0];
1242 let v1 = [1.0, 0.0, 0.0];
1243 let v2 = [0.0, 1.0, 0.0];
1244 let v3 = [0.5, 0.5, 0.0];
1245 let sj = tet_scaled_jacobian(v0, v1, v2, v3);
1246 assert!(sj.abs() < 1e-10);
1247 }
1248
1249 #[test]
1250 fn test_min_scaled_jacobian_regular_tet() {
1251 let (v0, v1, v2, v3) = unit_tet_vertices();
1252 let msj = tet_min_scaled_jacobian(v0, v1, v2, v3);
1253 assert!(
1254 msj > 0.0,
1255 "min scaled jacobian should be positive for regular tet"
1256 );
1257 }
1258
1259 #[test]
1260 fn test_min_scaled_jacobian_right_angle_tet() {
1261 let v0 = [0.0, 0.0, 0.0];
1262 let v1 = [1.0, 0.0, 0.0];
1263 let v2 = [0.0, 1.0, 0.0];
1264 let v3 = [0.0, 0.0, 1.0];
1265 let msj = tet_min_scaled_jacobian(v0, v1, v2, v3);
1266 assert!(msj > 0.0);
1267 }
1268
1269 #[test]
1272 fn test_condition_number_quality_regular_tet() {
1273 let (v0, v1, v2, v3) = unit_tet_vertices();
1274 let q = tet_condition_number_quality(v0, v1, v2, v3);
1275 assert!(
1276 q > 0.3,
1277 "regular tet should have positive condition number quality, got {q}"
1278 );
1279 }
1280
1281 #[test]
1282 fn test_condition_number_quality_degenerate() {
1283 let v0 = [0.0, 0.0, 0.0];
1284 let v1 = [1.0, 0.0, 0.0];
1285 let v2 = [0.0, 1.0, 0.0];
1286 let v3 = [0.5, 0.5, 0.0];
1287 let q = tet_condition_number_quality(v0, v1, v2, v3);
1288 assert!(q.abs() < 1e-10, "degenerate tet should have zero quality");
1289 }
1290
1291 #[test]
1292 fn test_condition_number_quality_bounded() {
1293 let v0 = [0.0, 0.0, 0.0];
1294 let v1 = [1.0, 0.0, 0.0];
1295 let v2 = [0.0, 1.0, 0.0];
1296 let v3 = [0.0, 0.0, 1.0];
1297 let q = tet_condition_number_quality(v0, v1, v2, v3);
1298 assert!(
1299 (0.0..=1.0).contains(&q),
1300 "quality should be in [0,1], got {q}"
1301 );
1302 }
1303
1304 #[test]
1307 fn test_quality_histogram_basic() {
1308 let values = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
1309 let hist = QualityHistogram::from_values(&values, 5);
1310 assert_eq!(hist.counts.len(), 5);
1311 assert_eq!(hist.total, 10);
1312 let total_count: usize = hist.counts.iter().sum();
1313 assert_eq!(total_count, 10);
1314 }
1315
1316 #[test]
1317 fn test_quality_histogram_single_bin() {
1318 let values = vec![0.5, 0.6, 0.7];
1319 let hist = QualityHistogram::from_values(&values, 1);
1320 assert_eq!(hist.counts, vec![3]);
1321 }
1322
1323 #[test]
1324 fn test_quality_histogram_fractions() {
1325 let values = vec![0.1, 0.2, 0.7, 0.8];
1326 let hist = QualityHistogram::from_values(&values, 2);
1327 let fracs = hist.fractions();
1328 assert_eq!(fracs.len(), 2);
1329 assert!((fracs.iter().sum::<f64>() - 1.0).abs() < 1e-10);
1330 }
1331
1332 #[test]
1333 fn test_quality_histogram_peak_bin() {
1334 let values = vec![0.1, 0.15, 0.12, 0.8, 0.85];
1335 let hist = QualityHistogram::from_values(&values, 5);
1336 let peak = hist.peak_bin();
1337 assert!(peak < 5);
1338 }
1339
1340 #[test]
1341 fn test_quality_histogram_empty() {
1342 let hist = QualityHistogram::from_values(&[], 5);
1343 assert_eq!(hist.total, 0);
1344 assert!(hist.counts.iter().all(|&c| c == 0));
1345 }
1346
1347 #[test]
1350 fn test_find_worst_elements_all_good() {
1351 let qualities = vec![0.8, 0.9, 0.7, 0.85];
1352 let worst = find_worst_elements(&qualities, 0.3);
1353 assert!(worst.is_empty());
1354 }
1355
1356 #[test]
1357 fn test_find_worst_elements_some_bad() {
1358 let qualities = vec![0.8, 0.1, 0.9, 0.2, 0.7];
1359 let worst = find_worst_elements(&qualities, 0.3);
1360 assert_eq!(worst.len(), 2);
1361 assert_eq!(worst[0], 1); assert_eq!(worst[1], 3); }
1365
1366 #[test]
1367 fn test_find_worst_elements_empty() {
1368 let worst = find_worst_elements(&[], 0.5);
1369 assert!(worst.is_empty());
1370 }
1371
1372 #[test]
1375 fn test_quality_stats_basic() {
1376 let values = vec![0.1, 0.2, 0.5, 0.8, 0.9];
1377 let stats = QualityStats::compute(&values);
1378 assert!((stats.min - 0.1).abs() < 1e-10);
1379 assert!((stats.max - 0.9).abs() < 1e-10);
1380 assert!((stats.mean - 0.5).abs() < 1e-10);
1381 assert!(stats.std_dev > 0.0);
1382 assert_eq!(stats.n_poor, 2); }
1384
1385 #[test]
1386 fn test_quality_stats_empty() {
1387 let stats = QualityStats::compute(&[]);
1388 assert_eq!(stats.n_poor, 0);
1389 assert!((stats.mean).abs() < 1e-10);
1390 }
1391
1392 #[test]
1393 fn test_quality_stats_uniform() {
1394 let values = vec![0.5, 0.5, 0.5, 0.5];
1395 let stats = QualityStats::compute(&values);
1396 assert!((stats.std_dev).abs() < 1e-10);
1397 assert!((stats.mean - 0.5).abs() < 1e-10);
1398 }
1399
1400 #[test]
1403 fn test_mesh_scaled_jacobian_qualities() {
1404 let verts = vec![
1405 [0.0, 0.0, 0.0],
1406 [1.0, 0.0, 0.0],
1407 [0.0, 1.0, 0.0],
1408 [0.0, 0.0, 1.0],
1409 ];
1410 let tets = vec![[0, 1, 2, 3]];
1411 let mesh = TetrahedralMesh::new(verts, tets);
1412 let qualities = mesh_scaled_jacobian_qualities(&mesh);
1413 assert_eq!(qualities.len(), 1);
1414 assert!(qualities[0].is_finite());
1415 }
1416
1417 #[test]
1418 fn test_mesh_condition_number_qualities() {
1419 let verts = vec![
1420 [0.0, 0.0, 0.0],
1421 [1.0, 0.0, 0.0],
1422 [0.0, 1.0, 0.0],
1423 [0.0, 0.0, 1.0],
1424 ];
1425 let tets = vec![[0, 1, 2, 3]];
1426 let mesh = TetrahedralMesh::new(verts, tets);
1427 let qualities = mesh_condition_number_qualities(&mesh);
1428 assert_eq!(qualities.len(), 1);
1429 assert!(qualities[0] > 0.0);
1430 }
1431
1432 #[test]
1433 fn test_mesh_quality_pipeline() {
1434 let (v0, v1, v2, v3) = unit_tet_vertices();
1436 let mesh = TetrahedralMesh::new(
1437 vec![
1438 v0,
1439 v1,
1440 v2,
1441 v3,
1442 [10.0, 0.0, 0.0],
1443 [11.0, 0.0, 0.0],
1444 [10.0, 1.0, 0.0],
1445 [10.0, 0.0, 0.001],
1446 ],
1447 vec![[0, 1, 2, 3], [4, 5, 6, 7]],
1448 );
1449 let qualities = mesh_scaled_jacobian_qualities(&mesh);
1450 assert_eq!(qualities.len(), 2);
1451
1452 let stats = QualityStats::compute(&qualities);
1453 assert!(stats.min <= stats.max);
1454
1455 let hist = QualityHistogram::from_values(&qualities, 5);
1456 assert_eq!(hist.total, 2);
1457 }
1458
1459 #[test]
1462 fn test_tet_dihedral_extremes_regular() {
1463 let (v0, v1, v2, v3) = unit_tet_vertices();
1464 let (min_d, max_d) = tet_dihedral_angle_extremes(v0, v1, v2, v3);
1465 assert!(min_d > 0.0 && min_d < 180.0);
1466 assert!(max_d > 0.0 && max_d < 180.0);
1467 assert!(min_d <= max_d);
1468 }
1469
1470 #[test]
1471 fn test_tri_angle_extremes_equilateral() {
1472 let h = (3.0_f64).sqrt() / 2.0;
1473 let (min_a, max_a) = tri_angle_extremes([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, h, 0.0]);
1474 assert!(
1475 (min_a - 60.0).abs() < 1e-6,
1476 "min angle should be 60°, got {min_a}"
1477 );
1478 assert!(
1479 (max_a - 60.0).abs() < 1e-6,
1480 "max angle should be 60°, got {max_a}"
1481 );
1482 }
1483
1484 #[test]
1485 fn test_tri_angle_extremes_right_triangle() {
1486 let (min_a, max_a) = tri_angle_extremes([0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 4.0, 0.0]);
1487 assert!((max_a - 90.0).abs() < 1e-6);
1488 assert!(min_a < 90.0);
1489 }
1490
1491 #[test]
1494 fn test_line_aspect_ratio() {
1495 let ar = line_aspect_ratio([0.0; 3], [1.0, 0.0, 0.0]);
1496 assert!((ar - 1.0).abs() < 1e-12);
1497 }
1498
1499 #[test]
1500 fn test_quad_aspect_ratio_square() {
1501 let ar = quad_aspect_ratio(
1503 [0.0, 0.0, 0.0],
1504 [1.0, 0.0, 0.0],
1505 [1.0, 1.0, 0.0],
1506 [0.0, 1.0, 0.0],
1507 );
1508 assert!(
1509 (ar - 1.0).abs() < 1e-10,
1510 "square aspect ratio should be 1, got {ar}"
1511 );
1512 }
1513
1514 #[test]
1515 fn test_quad_aspect_ratio_rectangle() {
1516 let ar = quad_aspect_ratio(
1519 [0.0, 0.0, 0.0],
1520 [4.0, 0.0, 0.0],
1521 [4.0, 1.0, 0.0],
1522 [0.0, 1.0, 0.0],
1523 );
1524 assert!(
1526 (ar - 1.0).abs() < 1e-10,
1527 "rectangle has equal diagonals, AR=1, got {ar}"
1528 );
1529 }
1530
1531 #[test]
1532 fn test_wedge_aspect_ratio_regular() {
1533 let h = (3.0_f64).sqrt() / 2.0;
1535 let ar = wedge_aspect_ratio(
1536 [0.0, 0.0, 0.0],
1537 [1.0, 0.0, 0.0],
1538 [0.5, h, 0.0],
1539 [0.0, 0.0, 1.0],
1540 [1.0, 0.0, 1.0],
1541 [0.5, h, 1.0],
1542 );
1543 assert!(ar >= 1.0, "wedge AR should be >= 1");
1544 assert!(
1546 (ar - 1.0).abs() < 1e-10,
1547 "regular prism should have AR=1, got {ar}"
1548 );
1549 }
1550
1551 #[test]
1552 fn test_hex_aspect_ratio_unit_cube() {
1553 let ar = hex_aspect_ratio(
1554 [0.0, 0.0, 0.0],
1555 [1.0, 0.0, 0.0],
1556 [1.0, 1.0, 0.0],
1557 [0.0, 1.0, 0.0],
1558 [0.0, 0.0, 1.0],
1559 [1.0, 0.0, 1.0],
1560 [1.0, 1.0, 1.0],
1561 [0.0, 1.0, 1.0],
1562 );
1563 assert!(
1564 (ar - 1.0).abs() < 1e-10,
1565 "unit cube should have AR=1, got {ar}"
1566 );
1567 }
1568
1569 #[test]
1570 fn test_hex_aspect_ratio_stretched() {
1571 let ar = hex_aspect_ratio(
1572 [0.0, 0.0, 0.0],
1573 [5.0, 0.0, 0.0],
1574 [5.0, 1.0, 0.0],
1575 [0.0, 1.0, 0.0],
1576 [0.0, 0.0, 1.0],
1577 [5.0, 0.0, 1.0],
1578 [5.0, 1.0, 1.0],
1579 [0.0, 1.0, 1.0],
1580 );
1581 assert!(
1582 (ar - 5.0).abs() < 1e-10,
1583 "stretched hex should have AR=5, got {ar}"
1584 );
1585 }
1586
1587 #[test]
1590 fn test_quality_guided_smoothing_runs() {
1591 let mut positions: Vec<[f64; 3]> = vec![
1592 [0.0, 0.0, 0.0],
1593 [1.0, 0.0, 0.0],
1594 [0.0, 1.0, 0.0],
1595 [0.0, 0.0, 1.0],
1596 [1.0, 1.0, 1.0],
1597 ];
1598 let tets = vec![[0usize, 1, 2, 3], [1, 2, 3, 4]];
1599 let accepted = quality_guided_smoothing(&mut positions, &tets, 2);
1600 let _ = accepted;
1602 for p in &positions {
1603 assert!(p.iter().all(|v| v.is_finite()));
1604 }
1605 }
1606
1607 #[test]
1608 fn test_quality_guided_smoothing_does_not_degrade() {
1609 let (v0, v1, v2, v3) = unit_tet_vertices();
1611 let mut positions = vec![v0, v1, v2, v3];
1612 let tets = vec![[0usize, 1, 2, 3]];
1613 let q_before =
1614 tet_min_scaled_jacobian(positions[0], positions[1], positions[2], positions[3]);
1615 quality_guided_smoothing(&mut positions, &tets, 3);
1616 let q_after =
1617 tet_min_scaled_jacobian(positions[0], positions[1], positions[2], positions[3]);
1618 assert!(
1619 q_after >= q_before - 1e-10,
1620 "quality should not degrade: before={q_before}, after={q_after}"
1621 );
1622 }
1623
1624 #[test]
1627 fn test_minimum_angle_optimization_runs() {
1628 let h = (3.0_f64).sqrt() / 2.0;
1629 let mut positions: Vec<[f64; 3]> = vec![
1630 [0.0, 0.0, 0.0],
1631 [1.0, 0.0, 0.0],
1632 [0.5, h, 0.0],
1633 [1.5, h, 0.0],
1634 ];
1635 let tris = vec![[0usize, 1, 2], [1, 3, 2]];
1636 let accepted = minimum_angle_optimization(&mut positions, &tris, 3);
1637 let _ = accepted;
1638 for p in &positions {
1639 assert!(p.iter().all(|v| v.is_finite()));
1640 }
1641 }
1642
1643 #[test]
1644 fn test_minimum_angle_optimization_equilateral_unchanged() {
1645 let h = (3.0_f64).sqrt() / 2.0;
1647 let v0 = [0.0, 0.0, 0.0];
1648 let v1 = [1.0, 0.0, 0.0];
1649 let v2 = [0.5, h, 0.0];
1650 let v3 = [1.5, h, 0.0];
1651 let mut positions = vec![v0, v1, v2, v3];
1652 let tris = vec![[0usize, 1, 2], [1, 3, 2]];
1653 minimum_angle_optimization(&mut positions, &tris, 5);
1654 for p in &positions {
1656 assert!(p.iter().all(|v| v.is_finite()));
1657 }
1658 }
1659}