1#![allow(clippy::needless_range_loop, clippy::ptr_arg)]
2#![allow(dead_code, missing_docs)]
11
12use std::collections::HashMap;
13
14#[inline]
19fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
20 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
21}
22
23#[inline]
24fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
25 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
26}
27
28#[inline]
29fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
30 [a[0] * s, a[1] * s, a[2] * s]
31}
32
33#[inline]
34fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
35 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
36}
37
38#[inline]
39fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
40 [
41 a[1] * b[2] - a[2] * b[1],
42 a[2] * b[0] - a[0] * b[2],
43 a[0] * b[1] - a[1] * b[0],
44 ]
45}
46
47#[inline]
48fn len3(a: [f64; 3]) -> f64 {
49 dot3(a, a).sqrt()
50}
51
52#[inline]
53fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
54 len3(sub3(a, b))
55}
56
57#[inline]
58fn normalize3(a: [f64; 3]) -> [f64; 3] {
59 let l = len3(a);
60 if l < f64::EPSILON {
61 [0.0, 0.0, 0.0]
62 } else {
63 [a[0] / l, a[1] / l, a[2] / l]
64 }
65}
66
67fn angle_at(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
69 let ab = normalize3(sub3(b, a));
70 let ac = normalize3(sub3(c, a));
71 dot3(ab, ac).clamp(-1.0, 1.0).acos()
72}
73
74pub struct TetrahedralMesh {
80 pub vertices: Vec<[f64; 3]>,
82 pub tets: Vec<[usize; 4]>,
84}
85
86impl TetrahedralMesh {
87 pub fn new(vertices: Vec<[f64; 3]>, tets: Vec<[usize; 4]>) -> Self {
89 Self { vertices, tets }
90 }
91}
92
93#[derive(Debug, Clone, PartialEq)]
99pub struct MeshQuality {
100 pub aspect_ratio: f64,
102 pub min_angle_deg: f64,
104 pub max_angle_deg: f64,
106 pub skewness: f64,
108 pub jacobian: f64,
110}
111
112impl MeshQuality {
113 pub fn is_acceptable(&self) -> bool {
117 self.aspect_ratio < 5.0 && self.skewness < 0.9 && self.min_angle_deg > 5.0
118 }
119
120 pub fn is_degenerate(&self) -> bool {
122 self.jacobian.abs() < 1e-10
123 }
124}
125
126pub fn compute_tet_quality(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> MeshQuality {
135 let e01 = sub3(v1, v0);
137 let e02 = sub3(v2, v0);
138 let e03 = sub3(v3, v0);
139 let e12 = sub3(v2, v1);
140 let e13 = sub3(v3, v1);
141 let e23 = sub3(v3, v2);
142
143 let edge_lengths = [
145 len3(e01),
146 len3(e02),
147 len3(e03),
148 len3(e12),
149 len3(e13),
150 len3(e23),
151 ];
152
153 let min_len = edge_lengths.iter().cloned().fold(f64::INFINITY, f64::min);
154 let max_len = edge_lengths.iter().cloned().fold(0.0_f64, f64::max);
155
156 let aspect_ratio = if min_len < f64::EPSILON {
157 f64::INFINITY
158 } else {
159 max_len / min_len
160 };
161
162 let jac = dot3(e01, cross3(e02, e03));
164 let mean_edge = edge_lengths.iter().sum::<f64>() / 6.0;
166 let ideal_jac = mean_edge * mean_edge * mean_edge * (2.0_f64).sqrt();
167 let scaled_jac = if ideal_jac < f64::EPSILON {
170 0.0
171 } else {
172 jac.abs() / ideal_jac
173 };
174
175 let n012 = normalize3(cross3(e01, e02));
182 let n013 = normalize3(cross3(e03, e01));
183 let n023 = normalize3(cross3(e02, e03));
184 let n123 = normalize3(cross3(e12, e13));
185
186 let dihedral_angle = |na: [f64; 3], nb: [f64; 3]| -> f64 {
188 let cos_a = dot3(na, nb).clamp(-1.0, 1.0);
189 std::f64::consts::PI - cos_a.acos()
191 };
192
193 let dihedrals = [
201 dihedral_angle(n012, n013),
202 dihedral_angle(n012, n023),
203 dihedral_angle(n013, n023),
204 dihedral_angle(n012, n123),
205 dihedral_angle(n013, n123),
206 dihedral_angle(n023, n123),
207 ];
208
209 let rad_to_deg = 180.0 / std::f64::consts::PI;
210 let min_angle_deg = dihedrals.iter().cloned().fold(f64::INFINITY, f64::min) * rad_to_deg;
211 let max_angle_deg = dihedrals.iter().cloned().fold(0.0_f64, f64::max) * rad_to_deg;
212
213 let ideal_dihedral = 70.529;
216 let skewness = ((max_angle_deg - ideal_dihedral) / (180.0 - ideal_dihedral)).clamp(0.0, 1.0);
217
218 MeshQuality {
219 aspect_ratio,
220 min_angle_deg,
221 max_angle_deg,
222 skewness,
223 jacobian: scaled_jac,
224 }
225}
226
227pub fn compute_tri_quality(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> MeshQuality {
236 let a = dist3(v1, v2);
237 let b = dist3(v0, v2);
238 let c = dist3(v0, v1);
239
240 let min_len = a.min(b).min(c);
241 let max_len = a.max(b).max(c);
242
243 let aspect_ratio = if min_len < f64::EPSILON {
244 f64::INFINITY
245 } else {
246 max_len / min_len
247 };
248
249 let angle_a = angle_at(v0, v1, v2);
250 let angle_b = angle_at(v1, v0, v2);
251 let angle_c = angle_at(v2, v0, v1);
252
253 let rad_to_deg = 180.0 / std::f64::consts::PI;
254 let angles_deg = [
255 angle_a * rad_to_deg,
256 angle_b * rad_to_deg,
257 angle_c * rad_to_deg,
258 ];
259
260 let min_angle_deg = angles_deg.iter().cloned().fold(f64::INFINITY, f64::min);
261 let max_angle_deg = angles_deg.iter().cloned().fold(0.0_f64, f64::max);
262
263 let ideal = 60.0;
265 let skewness = ((max_angle_deg - ideal) / (180.0 - ideal)).clamp(0.0, 1.0);
266
267 let area = {
270 let cross = cross3(sub3(v1, v0), sub3(v2, v0));
271 len3(cross) * 0.5
272 };
273 let denom = a * b;
274 let jacobian = if denom < f64::EPSILON {
275 0.0
276 } else {
277 (2.0 * area / denom).clamp(-1.0, 1.0)
278 };
279
280 MeshQuality {
281 aspect_ratio,
282 min_angle_deg,
283 max_angle_deg,
284 skewness,
285 jacobian,
286 }
287}
288
289pub fn repair_degenerate_tets(mesh: &mut TetrahedralMesh) -> usize {
298 let threshold = 1e-12_f64;
299 let before = mesh.tets.len();
300 mesh.tets.retain(|tet| {
301 let v0 = mesh.vertices[tet[0]];
302 let v1 = mesh.vertices[tet[1]];
303 let v2 = mesh.vertices[tet[2]];
304 let v3 = mesh.vertices[tet[3]];
305 let e01 = sub3(v1, v0);
306 let e02 = sub3(v2, v0);
307 let e03 = sub3(v3, v0);
308 let vol = dot3(e01, cross3(e02, e03)).abs() / 6.0;
309 vol > threshold
310 });
311 before - mesh.tets.len()
312}
313
314pub fn smooth_laplacian(positions: &mut [[f64; 3]], tets: &[[usize; 4]], iterations: usize) {
328 let n = positions.len();
329 let mut tet_count = vec![0usize; n];
331 for tet in tets {
332 for &vi in tet.iter() {
333 tet_count[vi] += 1;
334 }
335 }
336
337 for _ in 0..iterations {
338 let mut sums = vec![[0.0_f64; 3]; n];
339 let mut counts = vec![0usize; n];
340
341 for tet in tets {
342 for k in 0..4 {
343 let vi = tet[k];
344 for j in 0..4 {
346 if j != k {
347 let vj = tet[j];
348 sums[vi] = add3(sums[vi], positions[vj]);
349 counts[vi] += 1;
350 }
351 }
352 }
353 }
354
355 for i in 0..n {
356 if counts[i] > 0 && tet_count[i] >= 4 {
358 let avg = scale3(sums[i], 1.0 / counts[i] as f64);
359 positions[i] = avg;
360 }
361 }
362 }
363}
364
365pub fn flip_edges_2d(triangles: &mut Vec<[usize; 3]>, positions: &[[f64; 3]]) -> usize {
378 let mut flips = 0usize;
379 let mut changed = true;
380
381 while changed {
382 changed = false;
383
384 let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
386 for (ti, tri) in triangles.iter().enumerate() {
387 for k in 0..3 {
388 let a = tri[k];
389 let b = tri[(k + 1) % 3];
390 let opp = tri[(k + 2) % 3]; let key = (a.min(b), a.max(b));
392 edge_map.entry(key).or_default().push((ti, opp));
393 }
394 }
395
396 for ((_ea, _eb), tris) in &edge_map {
397 if tris.len() != 2 {
398 continue; }
400 let (ti, opp_i) = tris[0];
401 let (tj, opp_j) = tris[1];
402
403 let ea = _ea;
405 let eb = _eb;
406
407 let pa = positions[*ea];
408 let pb = positions[*eb];
409 let pc = positions[opp_i];
410 let pd = positions[opp_j];
411
412 if in_circumcircle_2d(pa, pb, pc, pd) {
414 let new_t0 = [*ea, opp_i, opp_j];
416 let new_t1 = [*eb, opp_j, opp_i];
417
418 if signed_area_2d(
420 positions[new_t0[0]],
421 positions[new_t0[1]],
422 positions[new_t0[2]],
423 ) > 0.0
424 && signed_area_2d(
425 positions[new_t1[0]],
426 positions[new_t1[1]],
427 positions[new_t1[2]],
428 ) > 0.0
429 {
430 triangles[ti] = new_t0;
431 triangles[tj] = new_t1;
432 flips += 1;
433 changed = true;
434 break; }
436 }
437 }
438 }
439
440 flips
441}
442
443fn signed_area_2d(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
445 0.5 * ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]))
446}
447
448fn in_circumcircle_2d(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> bool {
452 let ax = a[0] - d[0];
453 let ay = a[1] - d[1];
454 let bx = b[0] - d[0];
455 let by = b[1] - d[1];
456 let cx = c[0] - d[0];
457 let cy = c[1] - d[1];
458
459 let det = ax * (by * (cx * cx + cy * cy) - cy * (bx * bx + by * by))
460 - ay * (bx * (cx * cx + cy * cy) - cx * (bx * bx + by * by))
461 + (ax * ax + ay * ay) * (bx * cy - by * cx);
462
463 det > 0.0
464}
465
466pub fn tet_jacobian_matrix(
475 v0: [f64; 3],
476 v1: [f64; 3],
477 v2: [f64; 3],
478 v3: [f64; 3],
479) -> [[f64; 3]; 3] {
480 let e01 = sub3(v1, v0);
481 let e02 = sub3(v2, v0);
482 let e03 = sub3(v3, v0);
483 [e01, e02, e03]
484}
485
486pub fn tet_jacobian_det(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
490 let j = tet_jacobian_matrix(v0, v1, v2, v3);
491 dot3(j[0], cross3(j[1], j[2]))
493}
494
495pub fn tet_scaled_jacobian(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
501 let j = tet_jacobian_matrix(v0, v1, v2, v3);
502 let det = dot3(j[0], cross3(j[1], j[2]));
503
504 let l0 = len3(j[0]);
505 let l1 = len3(j[1]);
506 let l2 = len3(j[2]);
507
508 let denom = l0 * l1 * l2;
509 if denom < f64::EPSILON {
510 return 0.0;
511 }
512
513 let raw = det / denom;
516 raw / (2.0_f64).sqrt()
517}
518
519pub fn tet_min_scaled_jacobian(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
524 let sj0 = tet_scaled_jacobian(v0, v1, v2, v3);
527 let sj1 = tet_scaled_jacobian(v1, v2, v0, v3);
528 let sj2 = tet_scaled_jacobian(v2, v0, v1, v3);
529 let sj3 = tet_scaled_jacobian(v3, v1, v0, v2);
530 sj0.min(sj1).min(sj2).min(sj3)
531}
532
533pub fn tet_condition_number_quality(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
543 let j = tet_jacobian_matrix(v0, v1, v2, v3);
544 let det = dot3(j[0], cross3(j[1], j[2]));
545
546 if det.abs() < f64::EPSILON {
547 return 0.0;
548 }
549
550 let mut frob_j = 0.0;
552 for row in &j {
553 frob_j += dot3(*row, *row);
554 }
555 frob_j = frob_j.sqrt();
556
557 let inv_det = 1.0 / det;
559 let j_inv = [
560 scale3(cross3(j[1], j[2]), inv_det),
561 scale3(cross3(j[2], j[0]), inv_det),
562 scale3(cross3(j[0], j[1]), inv_det),
563 ];
564
565 let mut frob_jinv = 0.0;
567 for row in &j_inv {
568 frob_jinv += dot3(*row, *row);
569 }
570 frob_jinv = frob_jinv.sqrt();
571
572 let kappa = frob_j * frob_jinv;
573 let ideal_kappa = 3.0_f64.sqrt();
576 (ideal_kappa / kappa).clamp(0.0, 1.0)
577}
578
579pub struct QualityHistogram {
585 pub bin_edges: Vec<f64>,
587 pub counts: Vec<usize>,
589 pub total: usize,
591}
592
593impl QualityHistogram {
594 pub fn from_values(values: &[f64], n_bins: usize) -> Self {
598 assert!(n_bins > 0, "n_bins must be > 0");
599 let mut counts = vec![0usize; n_bins];
600 let bin_width = 1.0 / n_bins as f64;
601 let mut bin_edges = Vec::with_capacity(n_bins + 1);
602 for i in 0..=n_bins {
603 bin_edges.push(i as f64 * bin_width);
604 }
605
606 for &v in values {
607 let bin = ((v / bin_width) as usize).min(n_bins - 1);
608 counts[bin] += 1;
609 }
610
611 Self {
612 bin_edges,
613 counts,
614 total: values.len(),
615 }
616 }
617
618 pub fn fractions(&self) -> Vec<f64> {
620 if self.total == 0 {
621 return vec![0.0; self.counts.len()];
622 }
623 self.counts
624 .iter()
625 .map(|&c| c as f64 / self.total as f64)
626 .collect()
627 }
628
629 pub fn peak_bin(&self) -> usize {
631 self.counts
632 .iter()
633 .enumerate()
634 .max_by_key(|(_, c)| **c)
635 .map(|(i, _)| i)
636 .unwrap_or(0)
637 }
638}
639
640pub fn find_worst_elements(qualities: &[f64], threshold: f64) -> Vec<usize> {
649 let mut bad: Vec<(usize, f64)> = qualities
650 .iter()
651 .enumerate()
652 .filter(|(_, q)| *q < &threshold)
653 .map(|(i, q)| (i, *q))
654 .collect();
655 bad.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
656 bad.iter().map(|&(i, _)| i).collect()
657}
658
659pub struct QualityStats {
661 pub min: f64,
663 pub max: f64,
665 pub mean: f64,
667 pub std_dev: f64,
669 pub n_poor: usize,
671}
672
673impl QualityStats {
674 pub fn compute(values: &[f64]) -> Self {
676 if values.is_empty() {
677 return Self {
678 min: 0.0,
679 max: 0.0,
680 mean: 0.0,
681 std_dev: 0.0,
682 n_poor: 0,
683 };
684 }
685 let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
686 let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
687 let mean = values.iter().sum::<f64>() / values.len() as f64;
688 let variance =
689 values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
690 let std_dev = variance.sqrt();
691 let n_poor = values.iter().filter(|&&v| v < 0.3).count();
692 Self {
693 min,
694 max,
695 mean,
696 std_dev,
697 n_poor,
698 }
699 }
700}
701
702pub fn mesh_scaled_jacobian_qualities(mesh: &TetrahedralMesh) -> Vec<f64> {
704 mesh.tets
705 .iter()
706 .map(|tet| {
707 let v0 = mesh.vertices[tet[0]];
708 let v1 = mesh.vertices[tet[1]];
709 let v2 = mesh.vertices[tet[2]];
710 let v3 = mesh.vertices[tet[3]];
711 tet_min_scaled_jacobian(v0, v1, v2, v3)
712 })
713 .collect()
714}
715
716pub fn mesh_condition_number_qualities(mesh: &TetrahedralMesh) -> Vec<f64> {
718 mesh.tets
719 .iter()
720 .map(|tet| {
721 let v0 = mesh.vertices[tet[0]];
722 let v1 = mesh.vertices[tet[1]];
723 let v2 = mesh.vertices[tet[2]];
724 let v3 = mesh.vertices[tet[3]];
725 tet_condition_number_quality(v0, v1, v2, v3)
726 })
727 .collect()
728}
729
730pub fn tet_dihedral_angle_extremes(
738 v0: [f64; 3],
739 v1: [f64; 3],
740 v2: [f64; 3],
741 v3: [f64; 3],
742) -> (f64, f64) {
743 let q = compute_tet_quality(v0, v1, v2, v3);
744 (q.min_angle_deg, q.max_angle_deg)
745}
746
747pub fn tri_angle_extremes(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> (f64, f64) {
751 let q = compute_tri_quality(v0, v1, v2);
752 (q.min_angle_deg, q.max_angle_deg)
753}
754
755pub fn line_aspect_ratio(_v0: [f64; 3], _v1: [f64; 3]) -> f64 {
761 1.0
762}
763
764pub fn quad_aspect_ratio(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
768 let d1 = dist3(v0, v2);
770 let d2 = dist3(v1, v3);
771 let min_d = d1.min(d2);
772 let max_d = d1.max(d2);
773 if min_d < f64::EPSILON {
774 f64::INFINITY
775 } else {
776 max_d / min_d
777 }
778}
779
780pub fn wedge_aspect_ratio(
784 v0: [f64; 3],
785 v1: [f64; 3],
786 v2: [f64; 3],
787 v3: [f64; 3],
788 v4: [f64; 3],
789 v5: [f64; 3],
790) -> f64 {
791 let edges = [
792 dist3(v0, v1),
793 dist3(v1, v2),
794 dist3(v2, v0), dist3(v3, v4),
796 dist3(v4, v5),
797 dist3(v5, v3), dist3(v0, v3),
799 dist3(v1, v4),
800 dist3(v2, v5), ];
802 let min_e = edges.iter().cloned().fold(f64::INFINITY, f64::min);
803 let max_e = edges.iter().cloned().fold(0.0_f64, f64::max);
804 if min_e < f64::EPSILON {
805 f64::INFINITY
806 } else {
807 max_e / min_e
808 }
809}
810
811#[allow(clippy::too_many_arguments)]
815pub fn hex_aspect_ratio(
816 v0: [f64; 3],
817 v1: [f64; 3],
818 v2: [f64; 3],
819 v3: [f64; 3],
820 v4: [f64; 3],
821 v5: [f64; 3],
822 v6: [f64; 3],
823 v7: [f64; 3],
824) -> f64 {
825 let edges = [
826 dist3(v0, v1),
827 dist3(v1, v2),
828 dist3(v2, v3),
829 dist3(v3, v0), dist3(v4, v5),
831 dist3(v5, v6),
832 dist3(v6, v7),
833 dist3(v7, v4), dist3(v0, v4),
835 dist3(v1, v5),
836 dist3(v2, v6),
837 dist3(v3, v7), ];
839 let min_e = edges.iter().cloned().fold(f64::INFINITY, f64::min);
840 let max_e = edges.iter().cloned().fold(0.0_f64, f64::max);
841 if min_e < f64::EPSILON {
842 f64::INFINITY
843 } else {
844 max_e / min_e
845 }
846}
847
848pub fn quality_guided_smoothing(
860 positions: &mut [[f64; 3]],
861 tets: &[[usize; 4]],
862 iterations: usize,
863) -> usize {
864 let n = positions.len();
865 let mut tet_count = vec![0usize; n];
866 for tet in tets {
867 for &vi in tet.iter() {
868 tet_count[vi] += 1;
869 }
870 }
871
872 let mut total_accepted = 0usize;
873
874 for _ in 0..iterations {
875 let mut sums = vec![[0.0_f64; 3]; n];
876 let mut counts = vec![0usize; n];
877
878 for tet in tets.iter() {
879 for k in 0..4 {
880 let vi = tet[k];
881 for j in 0..4 {
882 if j != k {
883 let vj = tet[j];
884 sums[vi] = add3(sums[vi], positions[vj]);
885 counts[vi] += 1;
886 }
887 }
888 }
889 }
890
891 for i in 0..n {
892 if counts[i] == 0 || tet_count[i] < 2 {
893 continue;
894 }
895 let new_pos = scale3(sums[i], 1.0 / counts[i] as f64);
896
897 let min_q_before = tets
899 .iter()
900 .filter(|t| t.contains(&i))
901 .map(|t| {
902 let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
903 tet_scaled_jacobian(vv[0], vv[1], vv[2], vv[3]).abs()
904 })
905 .fold(f64::INFINITY, f64::min);
906
907 let old_pos = positions[i];
909 positions[i] = new_pos;
910
911 let min_q_after = tets
912 .iter()
913 .filter(|t| t.contains(&i))
914 .map(|t| {
915 let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
916 tet_scaled_jacobian(vv[0], vv[1], vv[2], vv[3]).abs()
917 })
918 .fold(f64::INFINITY, f64::min);
919
920 if min_q_after >= min_q_before {
921 total_accepted += 1; } else {
923 positions[i] = old_pos; }
925 }
926 }
927
928 total_accepted
929}
930
931pub fn minimum_angle_optimization(
943 positions: &mut [[f64; 3]],
944 triangles: &[[usize; 3]],
945 iterations: usize,
946) -> usize {
947 let n = positions.len();
948
949 let mut tri_count = vec![0usize; n];
951 for tri in triangles {
952 for &vi in tri.iter() {
953 tri_count[vi] += 1;
954 }
955 }
956
957 let mut total_accepted = 0usize;
958
959 for _ in 0..iterations {
960 for i in 0..n {
961 if tri_count[i] < 2 {
962 continue; }
964
965 let mut sum = [0.0_f64; 3];
967 let mut cnt = 0usize;
968 for tri in triangles.iter().filter(|t| t.contains(&i)) {
969 for &j in tri.iter() {
970 if j != i {
971 sum = add3(sum, positions[j]);
972 cnt += 1;
973 }
974 }
975 }
976 if cnt == 0 {
977 continue;
978 }
979 let new_pos = scale3(sum, 1.0 / cnt as f64);
980
981 let min_before = triangles
983 .iter()
984 .filter(|t| t.contains(&i))
985 .map(|t| {
986 let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
987 compute_tri_quality(vv[0], vv[1], vv[2]).min_angle_deg
988 })
989 .fold(f64::INFINITY, f64::min);
990
991 let old_pos = positions[i];
992 positions[i] = new_pos;
993
994 let min_after = triangles
995 .iter()
996 .filter(|t| t.contains(&i))
997 .map(|t| {
998 let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
999 compute_tri_quality(vv[0], vv[1], vv[2]).min_angle_deg
1000 })
1001 .fold(f64::INFINITY, f64::min);
1002
1003 if min_after >= min_before {
1004 total_accepted += 1;
1005 } else {
1006 positions[i] = old_pos;
1007 }
1008 }
1009 }
1010
1011 total_accepted
1012}
1013
1014#[cfg(test)]
1019mod tests {
1020 use super::*;
1021 use std::f64::consts::PI;
1022
1023 fn unit_tet_vertices() -> ([f64; 3], [f64; 3], [f64; 3], [f64; 3]) {
1025 let v0 = [1.0, 1.0, 1.0];
1027 let v1 = [-1.0, 1.0, -1.0];
1028 let v2 = [1.0, -1.0, -1.0];
1029 let v3 = [-1.0, -1.0, 1.0];
1030 (v0, v1, v2, v3)
1031 }
1032
1033 #[test]
1034 fn test_unit_tet_aspect_ratio() {
1035 let (v0, v1, v2, v3) = unit_tet_vertices();
1036 let q = compute_tet_quality(v0, v1, v2, v3);
1037 assert!(
1039 (q.aspect_ratio - 1.0).abs() < 1e-10,
1040 "aspect_ratio={}, expected 1.0",
1041 q.aspect_ratio
1042 );
1043 }
1044
1045 #[test]
1046 fn test_unit_tet_jacobian_positive() {
1047 let (v0, v1, v2, v3) = unit_tet_vertices();
1048 let q = compute_tet_quality(v0, v1, v2, v3);
1049 assert!(
1050 q.jacobian > 0.0,
1051 "jacobian should be positive, got {}",
1052 q.jacobian
1053 );
1054 }
1055
1056 #[test]
1057 fn test_degenerate_tet_detected() {
1058 let v0 = [0.0, 0.0, 0.0];
1060 let v1 = [1.0, 0.0, 0.0];
1061 let v2 = [0.0, 1.0, 0.0];
1062 let v3 = [0.5, 0.5, 0.0]; let q = compute_tet_quality(v0, v1, v2, v3);
1064 assert!(
1065 q.is_degenerate(),
1066 "coplanar tet should be degenerate, jacobian={}",
1067 q.jacobian
1068 );
1069 }
1070
1071 #[test]
1072 fn test_repair_degenerate_tets() {
1073 let verts = vec![
1074 [0.0_f64, 0.0, 0.0],
1075 [1.0, 0.0, 0.0],
1076 [0.0, 1.0, 0.0],
1077 [0.0, 0.0, 1.0],
1078 [0.5, 0.5, 0.0], ];
1080 let tets = vec![
1081 [0usize, 1, 2, 3], [0, 1, 2, 4], ];
1084 let mut mesh = TetrahedralMesh::new(verts, tets);
1085 let removed = repair_degenerate_tets(&mut mesh);
1086 assert_eq!(removed, 1, "expected 1 degenerate tet removed");
1087 assert_eq!(mesh.tets.len(), 1);
1088 }
1089
1090 #[test]
1091 fn test_equilateral_tri_quality() {
1092 let h = (3.0_f64).sqrt() / 2.0;
1094 let v0 = [0.0, 0.0, 0.0];
1095 let v1 = [1.0, 0.0, 0.0];
1096 let v2 = [0.5, h, 0.0];
1097 let q = compute_tri_quality(v0, v1, v2);
1098 assert!(
1099 (q.aspect_ratio - 1.0).abs() < 1e-10,
1100 "aspect_ratio={}, expected 1.0",
1101 q.aspect_ratio
1102 );
1103 assert!(
1104 (q.min_angle_deg - 60.0).abs() < 1e-6,
1105 "min_angle={}, expected 60°",
1106 q.min_angle_deg
1107 );
1108 assert!(
1109 (q.skewness).abs() < 1e-6,
1110 "skewness={}, expected 0",
1111 q.skewness
1112 );
1113 }
1114
1115 #[test]
1116 fn test_right_triangle_quality() {
1117 let v0 = [0.0, 0.0, 0.0];
1119 let v1 = [3.0, 0.0, 0.0];
1120 let v2 = [0.0, 4.0, 0.0];
1121 let q = compute_tri_quality(v0, v1, v2);
1122 assert!(
1123 (q.max_angle_deg - 90.0).abs() < 1e-6,
1124 "max_angle={}, expected 90°",
1125 q.max_angle_deg
1126 );
1127 assert!(
1128 q.aspect_ratio > 1.0,
1129 "aspect_ratio should be > 1 for non-equilateral"
1130 );
1131 }
1132
1133 #[test]
1134 fn test_smooth_laplacian_runs() {
1135 let mut positions: Vec<[f64; 3]> = vec![
1136 [0.0, 0.0, 0.0],
1137 [1.0, 0.0, 0.0],
1138 [0.0, 1.0, 0.0],
1139 [0.0, 0.0, 1.0],
1140 [1.0, 1.0, 1.0],
1141 ];
1142 let tets = vec![[0usize, 1, 2, 3], [1, 2, 3, 4]];
1143 smooth_laplacian(&mut positions, &tets, 2);
1144 for p in &positions {
1146 assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
1147 }
1148 }
1149
1150 #[test]
1151 fn test_flip_edges_2d_no_flip_on_delaunay() {
1152 let positions: Vec<[f64; 3]> = vec![
1154 [0.0, 0.0, 0.0],
1155 [1.0, 0.0, 0.0],
1156 [1.0, 1.0, 0.0],
1157 [0.0, 1.0, 0.0],
1158 ];
1159 let mut tris = vec![[0usize, 1, 2], [0, 2, 3]];
1160 let flips = flip_edges_2d(&mut tris, &positions);
1161 let _ = flips;
1163 assert_eq!(tris.len(), 2, "triangle count must stay the same");
1164 }
1165
1166 #[test]
1167 fn test_tet_quality_angles_in_range() {
1168 let (v0, v1, v2, v3) = unit_tet_vertices();
1169 let q = compute_tet_quality(v0, v1, v2, v3);
1170 assert!(q.min_angle_deg > 0.0 && q.min_angle_deg < 180.0);
1171 assert!(q.max_angle_deg > 0.0 && q.max_angle_deg < 180.0);
1172 assert!(q.min_angle_deg <= q.max_angle_deg);
1173 }
1174
1175 #[test]
1176 fn test_degenerate_tri_quality() {
1177 let v0 = [0.0, 0.0, 0.0];
1179 let v1 = [1.0, 0.0, 0.0];
1180 let v2 = [2.0, 0.0, 0.0];
1181 let q = compute_tri_quality(v0, v1, v2);
1182 assert!(
1183 q.jacobian.abs() < 1e-10,
1184 "collinear triangle jacobian should be ~0, got {}",
1185 q.jacobian
1186 );
1187 }
1188
1189 #[test]
1190 fn test_pi_constant_used() {
1191 let half_pi_deg = PI / 2.0 * 180.0 / PI;
1193 assert!((half_pi_deg - 90.0).abs() < 1e-10);
1194 }
1195
1196 #[test]
1199 fn test_tet_jacobian_det_positive() {
1200 let (v0, v1, v2, v3) = unit_tet_vertices();
1201 let det = tet_jacobian_det(v0, v1, v2, v3);
1202 assert!(
1203 det.abs() > 1e-10,
1204 "regular tet should have non-zero Jacobian det"
1205 );
1206 }
1207
1208 #[test]
1209 fn test_tet_jacobian_det_degenerate() {
1210 let v0 = [0.0, 0.0, 0.0];
1211 let v1 = [1.0, 0.0, 0.0];
1212 let v2 = [0.0, 1.0, 0.0];
1213 let v3 = [0.5, 0.5, 0.0]; let det = tet_jacobian_det(v0, v1, v2, v3);
1215 assert!(
1216 det.abs() < 1e-10,
1217 "coplanar tet should have zero Jacobian det"
1218 );
1219 }
1220
1221 #[test]
1222 fn test_tet_jacobian_matrix() {
1223 let v0 = [0.0, 0.0, 0.0];
1224 let v1 = [1.0, 0.0, 0.0];
1225 let v2 = [0.0, 1.0, 0.0];
1226 let v3 = [0.0, 0.0, 1.0];
1227 let j = tet_jacobian_matrix(v0, v1, v2, v3);
1228 assert!((j[0][0] - 1.0).abs() < 1e-10);
1229 assert!((j[1][1] - 1.0).abs() < 1e-10);
1230 assert!((j[2][2] - 1.0).abs() < 1e-10);
1231 }
1232
1233 #[test]
1236 fn test_scaled_jacobian_regular_tet() {
1237 let (v0, v1, v2, v3) = unit_tet_vertices();
1238 let sj = tet_scaled_jacobian(v0, v1, v2, v3);
1239 assert!(
1240 sj > 0.0,
1241 "regular tet should have positive scaled jacobian, got {sj}"
1242 );
1243 }
1244
1245 #[test]
1246 fn test_scaled_jacobian_degenerate() {
1247 let v0 = [0.0, 0.0, 0.0];
1248 let v1 = [1.0, 0.0, 0.0];
1249 let v2 = [0.0, 1.0, 0.0];
1250 let v3 = [0.5, 0.5, 0.0];
1251 let sj = tet_scaled_jacobian(v0, v1, v2, v3);
1252 assert!(sj.abs() < 1e-10);
1253 }
1254
1255 #[test]
1256 fn test_min_scaled_jacobian_regular_tet() {
1257 let (v0, v1, v2, v3) = unit_tet_vertices();
1258 let msj = tet_min_scaled_jacobian(v0, v1, v2, v3);
1259 assert!(
1260 msj > 0.0,
1261 "min scaled jacobian should be positive for regular tet"
1262 );
1263 }
1264
1265 #[test]
1266 fn test_min_scaled_jacobian_right_angle_tet() {
1267 let v0 = [0.0, 0.0, 0.0];
1268 let v1 = [1.0, 0.0, 0.0];
1269 let v2 = [0.0, 1.0, 0.0];
1270 let v3 = [0.0, 0.0, 1.0];
1271 let msj = tet_min_scaled_jacobian(v0, v1, v2, v3);
1272 assert!(msj > 0.0);
1273 }
1274
1275 #[test]
1278 fn test_condition_number_quality_regular_tet() {
1279 let (v0, v1, v2, v3) = unit_tet_vertices();
1280 let q = tet_condition_number_quality(v0, v1, v2, v3);
1281 assert!(
1282 q > 0.3,
1283 "regular tet should have positive condition number quality, got {q}"
1284 );
1285 }
1286
1287 #[test]
1288 fn test_condition_number_quality_degenerate() {
1289 let v0 = [0.0, 0.0, 0.0];
1290 let v1 = [1.0, 0.0, 0.0];
1291 let v2 = [0.0, 1.0, 0.0];
1292 let v3 = [0.5, 0.5, 0.0];
1293 let q = tet_condition_number_quality(v0, v1, v2, v3);
1294 assert!(q.abs() < 1e-10, "degenerate tet should have zero quality");
1295 }
1296
1297 #[test]
1298 fn test_condition_number_quality_bounded() {
1299 let v0 = [0.0, 0.0, 0.0];
1300 let v1 = [1.0, 0.0, 0.0];
1301 let v2 = [0.0, 1.0, 0.0];
1302 let v3 = [0.0, 0.0, 1.0];
1303 let q = tet_condition_number_quality(v0, v1, v2, v3);
1304 assert!(
1305 (0.0..=1.0).contains(&q),
1306 "quality should be in [0,1], got {q}"
1307 );
1308 }
1309
1310 #[test]
1313 fn test_quality_histogram_basic() {
1314 let values = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
1315 let hist = QualityHistogram::from_values(&values, 5);
1316 assert_eq!(hist.counts.len(), 5);
1317 assert_eq!(hist.total, 10);
1318 let total_count: usize = hist.counts.iter().sum();
1319 assert_eq!(total_count, 10);
1320 }
1321
1322 #[test]
1323 fn test_quality_histogram_single_bin() {
1324 let values = vec![0.5, 0.6, 0.7];
1325 let hist = QualityHistogram::from_values(&values, 1);
1326 assert_eq!(hist.counts, vec![3]);
1327 }
1328
1329 #[test]
1330 fn test_quality_histogram_fractions() {
1331 let values = vec![0.1, 0.2, 0.7, 0.8];
1332 let hist = QualityHistogram::from_values(&values, 2);
1333 let fracs = hist.fractions();
1334 assert_eq!(fracs.len(), 2);
1335 assert!((fracs.iter().sum::<f64>() - 1.0).abs() < 1e-10);
1336 }
1337
1338 #[test]
1339 fn test_quality_histogram_peak_bin() {
1340 let values = vec![0.1, 0.15, 0.12, 0.8, 0.85];
1341 let hist = QualityHistogram::from_values(&values, 5);
1342 let peak = hist.peak_bin();
1343 assert!(peak < 5);
1344 }
1345
1346 #[test]
1347 fn test_quality_histogram_empty() {
1348 let hist = QualityHistogram::from_values(&[], 5);
1349 assert_eq!(hist.total, 0);
1350 assert!(hist.counts.iter().all(|&c| c == 0));
1351 }
1352
1353 #[test]
1356 fn test_find_worst_elements_all_good() {
1357 let qualities = vec![0.8, 0.9, 0.7, 0.85];
1358 let worst = find_worst_elements(&qualities, 0.3);
1359 assert!(worst.is_empty());
1360 }
1361
1362 #[test]
1363 fn test_find_worst_elements_some_bad() {
1364 let qualities = vec![0.8, 0.1, 0.9, 0.2, 0.7];
1365 let worst = find_worst_elements(&qualities, 0.3);
1366 assert_eq!(worst.len(), 2);
1367 assert_eq!(worst[0], 1); assert_eq!(worst[1], 3); }
1371
1372 #[test]
1373 fn test_find_worst_elements_empty() {
1374 let worst = find_worst_elements(&[], 0.5);
1375 assert!(worst.is_empty());
1376 }
1377
1378 #[test]
1381 fn test_quality_stats_basic() {
1382 let values = vec![0.1, 0.2, 0.5, 0.8, 0.9];
1383 let stats = QualityStats::compute(&values);
1384 assert!((stats.min - 0.1).abs() < 1e-10);
1385 assert!((stats.max - 0.9).abs() < 1e-10);
1386 assert!((stats.mean - 0.5).abs() < 1e-10);
1387 assert!(stats.std_dev > 0.0);
1388 assert_eq!(stats.n_poor, 2); }
1390
1391 #[test]
1392 fn test_quality_stats_empty() {
1393 let stats = QualityStats::compute(&[]);
1394 assert_eq!(stats.n_poor, 0);
1395 assert!((stats.mean).abs() < 1e-10);
1396 }
1397
1398 #[test]
1399 fn test_quality_stats_uniform() {
1400 let values = vec![0.5, 0.5, 0.5, 0.5];
1401 let stats = QualityStats::compute(&values);
1402 assert!((stats.std_dev).abs() < 1e-10);
1403 assert!((stats.mean - 0.5).abs() < 1e-10);
1404 }
1405
1406 #[test]
1409 fn test_mesh_scaled_jacobian_qualities() {
1410 let verts = vec![
1411 [0.0, 0.0, 0.0],
1412 [1.0, 0.0, 0.0],
1413 [0.0, 1.0, 0.0],
1414 [0.0, 0.0, 1.0],
1415 ];
1416 let tets = vec![[0, 1, 2, 3]];
1417 let mesh = TetrahedralMesh::new(verts, tets);
1418 let qualities = mesh_scaled_jacobian_qualities(&mesh);
1419 assert_eq!(qualities.len(), 1);
1420 assert!(qualities[0].is_finite());
1421 }
1422
1423 #[test]
1424 fn test_mesh_condition_number_qualities() {
1425 let verts = vec![
1426 [0.0, 0.0, 0.0],
1427 [1.0, 0.0, 0.0],
1428 [0.0, 1.0, 0.0],
1429 [0.0, 0.0, 1.0],
1430 ];
1431 let tets = vec![[0, 1, 2, 3]];
1432 let mesh = TetrahedralMesh::new(verts, tets);
1433 let qualities = mesh_condition_number_qualities(&mesh);
1434 assert_eq!(qualities.len(), 1);
1435 assert!(qualities[0] > 0.0);
1436 }
1437
1438 #[test]
1439 fn test_mesh_quality_pipeline() {
1440 let (v0, v1, v2, v3) = unit_tet_vertices();
1442 let mesh = TetrahedralMesh::new(
1443 vec![
1444 v0,
1445 v1,
1446 v2,
1447 v3,
1448 [10.0, 0.0, 0.0],
1449 [11.0, 0.0, 0.0],
1450 [10.0, 1.0, 0.0],
1451 [10.0, 0.0, 0.001],
1452 ],
1453 vec![[0, 1, 2, 3], [4, 5, 6, 7]],
1454 );
1455 let qualities = mesh_scaled_jacobian_qualities(&mesh);
1456 assert_eq!(qualities.len(), 2);
1457
1458 let stats = QualityStats::compute(&qualities);
1459 assert!(stats.min <= stats.max);
1460
1461 let hist = QualityHistogram::from_values(&qualities, 5);
1462 assert_eq!(hist.total, 2);
1463 }
1464
1465 #[test]
1468 fn test_tet_dihedral_extremes_regular() {
1469 let (v0, v1, v2, v3) = unit_tet_vertices();
1470 let (min_d, max_d) = tet_dihedral_angle_extremes(v0, v1, v2, v3);
1471 assert!(min_d > 0.0 && min_d < 180.0);
1472 assert!(max_d > 0.0 && max_d < 180.0);
1473 assert!(min_d <= max_d);
1474 }
1475
1476 #[test]
1477 fn test_tri_angle_extremes_equilateral() {
1478 let h = (3.0_f64).sqrt() / 2.0;
1479 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]);
1480 assert!(
1481 (min_a - 60.0).abs() < 1e-6,
1482 "min angle should be 60°, got {min_a}"
1483 );
1484 assert!(
1485 (max_a - 60.0).abs() < 1e-6,
1486 "max angle should be 60°, got {max_a}"
1487 );
1488 }
1489
1490 #[test]
1491 fn test_tri_angle_extremes_right_triangle() {
1492 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]);
1493 assert!((max_a - 90.0).abs() < 1e-6);
1494 assert!(min_a < 90.0);
1495 }
1496
1497 #[test]
1500 fn test_line_aspect_ratio() {
1501 let ar = line_aspect_ratio([0.0; 3], [1.0, 0.0, 0.0]);
1502 assert!((ar - 1.0).abs() < 1e-12);
1503 }
1504
1505 #[test]
1506 fn test_quad_aspect_ratio_square() {
1507 let ar = quad_aspect_ratio(
1509 [0.0, 0.0, 0.0],
1510 [1.0, 0.0, 0.0],
1511 [1.0, 1.0, 0.0],
1512 [0.0, 1.0, 0.0],
1513 );
1514 assert!(
1515 (ar - 1.0).abs() < 1e-10,
1516 "square aspect ratio should be 1, got {ar}"
1517 );
1518 }
1519
1520 #[test]
1521 fn test_quad_aspect_ratio_rectangle() {
1522 let ar = quad_aspect_ratio(
1525 [0.0, 0.0, 0.0],
1526 [4.0, 0.0, 0.0],
1527 [4.0, 1.0, 0.0],
1528 [0.0, 1.0, 0.0],
1529 );
1530 assert!(
1532 (ar - 1.0).abs() < 1e-10,
1533 "rectangle has equal diagonals, AR=1, got {ar}"
1534 );
1535 }
1536
1537 #[test]
1538 fn test_wedge_aspect_ratio_regular() {
1539 let h = (3.0_f64).sqrt() / 2.0;
1541 let ar = wedge_aspect_ratio(
1542 [0.0, 0.0, 0.0],
1543 [1.0, 0.0, 0.0],
1544 [0.5, h, 0.0],
1545 [0.0, 0.0, 1.0],
1546 [1.0, 0.0, 1.0],
1547 [0.5, h, 1.0],
1548 );
1549 assert!(ar >= 1.0, "wedge AR should be >= 1");
1550 assert!(
1552 (ar - 1.0).abs() < 1e-10,
1553 "regular prism should have AR=1, got {ar}"
1554 );
1555 }
1556
1557 #[test]
1558 fn test_hex_aspect_ratio_unit_cube() {
1559 let ar = hex_aspect_ratio(
1560 [0.0, 0.0, 0.0],
1561 [1.0, 0.0, 0.0],
1562 [1.0, 1.0, 0.0],
1563 [0.0, 1.0, 0.0],
1564 [0.0, 0.0, 1.0],
1565 [1.0, 0.0, 1.0],
1566 [1.0, 1.0, 1.0],
1567 [0.0, 1.0, 1.0],
1568 );
1569 assert!(
1570 (ar - 1.0).abs() < 1e-10,
1571 "unit cube should have AR=1, got {ar}"
1572 );
1573 }
1574
1575 #[test]
1576 fn test_hex_aspect_ratio_stretched() {
1577 let ar = hex_aspect_ratio(
1578 [0.0, 0.0, 0.0],
1579 [5.0, 0.0, 0.0],
1580 [5.0, 1.0, 0.0],
1581 [0.0, 1.0, 0.0],
1582 [0.0, 0.0, 1.0],
1583 [5.0, 0.0, 1.0],
1584 [5.0, 1.0, 1.0],
1585 [0.0, 1.0, 1.0],
1586 );
1587 assert!(
1588 (ar - 5.0).abs() < 1e-10,
1589 "stretched hex should have AR=5, got {ar}"
1590 );
1591 }
1592
1593 #[test]
1596 fn test_quality_guided_smoothing_runs() {
1597 let mut positions: Vec<[f64; 3]> = vec![
1598 [0.0, 0.0, 0.0],
1599 [1.0, 0.0, 0.0],
1600 [0.0, 1.0, 0.0],
1601 [0.0, 0.0, 1.0],
1602 [1.0, 1.0, 1.0],
1603 ];
1604 let tets = vec![[0usize, 1, 2, 3], [1, 2, 3, 4]];
1605 let accepted = quality_guided_smoothing(&mut positions, &tets, 2);
1606 let _ = accepted;
1608 for p in &positions {
1609 assert!(p.iter().all(|v| v.is_finite()));
1610 }
1611 }
1612
1613 #[test]
1614 fn test_quality_guided_smoothing_does_not_degrade() {
1615 let (v0, v1, v2, v3) = unit_tet_vertices();
1617 let mut positions = vec![v0, v1, v2, v3];
1618 let tets = vec![[0usize, 1, 2, 3]];
1619 let q_before =
1620 tet_min_scaled_jacobian(positions[0], positions[1], positions[2], positions[3]);
1621 quality_guided_smoothing(&mut positions, &tets, 3);
1622 let q_after =
1623 tet_min_scaled_jacobian(positions[0], positions[1], positions[2], positions[3]);
1624 assert!(
1625 q_after >= q_before - 1e-10,
1626 "quality should not degrade: before={q_before}, after={q_after}"
1627 );
1628 }
1629
1630 #[test]
1633 fn test_minimum_angle_optimization_runs() {
1634 let h = (3.0_f64).sqrt() / 2.0;
1635 let mut positions: Vec<[f64; 3]> = vec![
1636 [0.0, 0.0, 0.0],
1637 [1.0, 0.0, 0.0],
1638 [0.5, h, 0.0],
1639 [1.5, h, 0.0],
1640 ];
1641 let tris = vec![[0usize, 1, 2], [1, 3, 2]];
1642 let accepted = minimum_angle_optimization(&mut positions, &tris, 3);
1643 let _ = accepted;
1644 for p in &positions {
1645 assert!(p.iter().all(|v| v.is_finite()));
1646 }
1647 }
1648
1649 #[test]
1650 fn test_minimum_angle_optimization_equilateral_unchanged() {
1651 let h = (3.0_f64).sqrt() / 2.0;
1653 let v0 = [0.0, 0.0, 0.0];
1654 let v1 = [1.0, 0.0, 0.0];
1655 let v2 = [0.5, h, 0.0];
1656 let v3 = [1.5, h, 0.0];
1657 let mut positions = vec![v0, v1, v2, v3];
1658 let tris = vec![[0usize, 1, 2], [1, 3, 2]];
1659 minimum_angle_optimization(&mut positions, &tris, 5);
1660 for p in &positions {
1662 assert!(p.iter().all(|v| v.is_finite()));
1663 }
1664 }
1665}