Skip to main content

oxiphysics_io/mesh_quality/
functions_2.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::triangle_angles_deg;
6#[allow(unused_imports)]
7use super::functions::*;
8use super::types::{
9    AutoImproveSuggestion, PerTypeMeshStats, PoorShapeReason, PoorlyShapedElement,
10    QualityThresholds, QualityVisualizationData, TriangleMesh,
11};
12
13/// Compute per-element-type quality statistics.
14#[allow(dead_code)]
15pub fn per_type_stats(mesh: &TriangleMesh) -> PerTypeMeshStats {
16    let mut stats = PerTypeMeshStats::default();
17    let mut eq_ar_sum = 0.0_f32;
18    let mut ob_ar_sum = 0.0_f32;
19    let mut rt_ar_sum = 0.0_f32;
20    for tri in &mesh.triangles {
21        let v0 = mesh.vertices[tri[0] as usize];
22        let v1 = mesh.vertices[tri[1] as usize];
23        let v2 = mesh.vertices[tri[2] as usize];
24        let area = triangle_area(v0, v1, v2);
25        if area < 1e-10 {
26            stats.n_degenerate += 1;
27            continue;
28        }
29        let angles = triangle_angles_deg(v0, v1, v2);
30        let min_a = angles[0].min(angles[1]).min(angles[2]);
31        let max_a = angles[0].max(angles[1]).max(angles[2]);
32        let ar = triangle_aspect_ratio(v0, v1, v2);
33        if min_a >= 50.0 && max_a <= 70.0 {
34            stats.n_equilateral += 1;
35            eq_ar_sum += ar;
36        } else if (max_a - 90.0).abs() < 5.0 {
37            stats.n_right += 1;
38            rt_ar_sum += ar;
39        } else if max_a > 95.0 {
40            stats.n_obtuse += 1;
41            ob_ar_sum += ar;
42        } else {
43            stats.n_acute += 1;
44        }
45    }
46    if stats.n_equilateral > 0 {
47        stats.equilateral_mean_ar = eq_ar_sum / stats.n_equilateral as f32;
48    }
49    if stats.n_obtuse > 0 {
50        stats.obtuse_mean_ar = ob_ar_sum / stats.n_obtuse as f32;
51    }
52    if stats.n_right > 0 {
53        stats.right_mean_ar = rt_ar_sum / stats.n_right as f32;
54    }
55    stats
56}
57/// Generate per-triangle quality values for visualization (e.g. coloring in ParaView).
58#[allow(dead_code)]
59pub fn quality_visualization_data(mesh: &TriangleMesh) -> QualityVisualizationData {
60    let n = mesh.triangles.len();
61    let mut ars = Vec::with_capacity(n);
62    let mut skews = Vec::with_capacity(n);
63    let mut min_as = Vec::with_capacity(n);
64    let mut max_as = Vec::with_capacity(n);
65    let mut areas = Vec::with_capacity(n);
66    let mut jacs = Vec::with_capacity(n);
67    for tri in &mesh.triangles {
68        let v0 = mesh.vertices[tri[0] as usize];
69        let v1 = mesh.vertices[tri[1] as usize];
70        let v2 = mesh.vertices[tri[2] as usize];
71        let a = triangle_area(v0, v1, v2);
72        areas.push(a);
73        jacs.push(2.0 * a);
74        ars.push(triangle_aspect_ratio(v0, v1, v2));
75        skews.push(triangle_skewness(v0, v1, v2));
76        min_as.push(triangle_min_angle_deg(v0, v1, v2));
77        max_as.push(triangle_max_angle_deg(v0, v1, v2));
78    }
79    QualityVisualizationData {
80        aspect_ratios: ars,
81        skewness: skews,
82        min_angles: min_as,
83        max_angles: max_as,
84        areas,
85        jacobians: jacs,
86    }
87}
88/// Detect poorly-shaped elements using per-element thresholds.
89#[allow(dead_code)]
90pub fn detect_poorly_shaped(
91    mesh: &TriangleMesh,
92    max_aspect_ratio: f32,
93    min_angle_deg: f32,
94    max_angle_deg: f32,
95    max_skewness: f32,
96) -> Vec<PoorlyShapedElement> {
97    let mut poor = Vec::new();
98    for (idx, tri) in mesh.triangles.iter().enumerate() {
99        let v0 = mesh.vertices[tri[0] as usize];
100        let v1 = mesh.vertices[tri[1] as usize];
101        let v2 = mesh.vertices[tri[2] as usize];
102        let mut reasons = Vec::new();
103        let area = triangle_area(v0, v1, v2);
104        if area < 1e-10 {
105            reasons.push(PoorShapeReason::Degenerate);
106        }
107        let ar = triangle_aspect_ratio(v0, v1, v2);
108        if ar.is_finite() && ar > max_aspect_ratio {
109            reasons.push(PoorShapeReason::HighAspectRatio(ar));
110        }
111        let min_a = triangle_min_angle_deg(v0, v1, v2);
112        if min_a < min_angle_deg {
113            reasons.push(PoorShapeReason::SmallAngle(min_a));
114        }
115        let max_a = triangle_max_angle_deg(v0, v1, v2);
116        if max_a > max_angle_deg {
117            reasons.push(PoorShapeReason::LargeAngle(max_a));
118        }
119        let skew = triangle_skewness(v0, v1, v2);
120        if skew > max_skewness {
121            reasons.push(PoorShapeReason::HighSkewness(skew));
122        }
123        if !reasons.is_empty() {
124            poor.push(PoorlyShapedElement {
125                index: idx,
126                reasons,
127            });
128        }
129    }
130    poor
131}
132/// Generate automated quality improvement suggestions with priorities.
133#[allow(dead_code)]
134pub fn auto_improve_suggestions(mesh: &TriangleMesh) -> Vec<AutoImproveSuggestion> {
135    let mut suggestions = Vec::new();
136    let report = compute_quality_report(mesh);
137    let thresholds = QualityThresholds::default();
138    let check = check_quality(mesh, &thresholds);
139    if report.n_degenerate > 0 {
140        suggestions.push(AutoImproveSuggestion::new(
141            format!(
142                "Remove {} degenerate triangles (area ≈ 0)",
143                report.n_degenerate
144            ),
145            1,
146            report.n_degenerate as usize,
147        ));
148    }
149    if check.n_bad_aspect_ratio > 0 {
150        suggestions.push(AutoImproveSuggestion::new(
151            format!(
152                "Refine {} high-aspect-ratio triangles (AR > {:.1})",
153                check.n_bad_aspect_ratio, thresholds.max_aspect_ratio
154            ),
155            2,
156            check.n_bad_aspect_ratio as usize,
157        ));
158    }
159    if check.n_small_angle > 0 {
160        suggestions.push(AutoImproveSuggestion::new(
161            format!(
162                "Fix {} triangles with small angles (< {:.0}°)",
163                check.n_small_angle, thresholds.min_angle
164            ),
165            2,
166            check.n_small_angle as usize,
167        ));
168    }
169    if check.n_large_angle > 0 {
170        suggestions.push(AutoImproveSuggestion::new(
171            format!(
172                "Split {} triangles with large angles (> {:.0}°)",
173                check.n_large_angle, thresholds.max_angle
174            ),
175            3,
176            check.n_large_angle as usize,
177        ));
178    }
179    if check.n_bad_skewness > 0 {
180        suggestions.push(AutoImproveSuggestion::new(
181            format!(
182                "Smooth {} high-skewness triangles (skewness > {:.2})",
183                check.n_bad_skewness, thresholds.max_skewness
184            ),
185            4,
186            check.n_bad_skewness as usize,
187        ));
188    }
189    if suggestions.is_empty() {
190        suggestions.push(AutoImproveSuggestion::new(
191            "Mesh passes all quality checks — no improvements needed.".to_string(),
192            5,
193            0,
194        ));
195    }
196    suggestions.sort_by_key(|s| s.priority);
197    suggestions
198}
199#[cfg(test)]
200mod tests_mesh_quality_extended {
201    use super::*;
202
203    use crate::mesh_quality::types::*;
204    fn equilateral() -> TriangleMesh {
205        let v0 = [0.0_f32, 0.0, 0.0];
206        let v1 = [2.0, 0.0, 0.0];
207        let v2 = [1.0, 3.0_f32.sqrt(), 0.0];
208        TriangleMesh::from_raw(vec![v0, v1, v2], vec![[0, 1, 2]])
209    }
210    fn degenerate_mesh() -> TriangleMesh {
211        let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
212        TriangleMesh::from_raw(verts, vec![[0, 1, 2]])
213    }
214    fn mixed_mesh() -> TriangleMesh {
215        let verts = vec![
216            [0.0, 0.0, 0.0],
217            [2.0, 0.0, 0.0],
218            [1.0, 3.0_f32.sqrt(), 0.0],
219            [0.0, 0.0, 0.0],
220            [1.0, 0.0, 0.0],
221            [0.0, 1.0, 0.0],
222            [0.0, 0.0, 0.0],
223            [1.0, 0.0, 0.0],
224            [2.0, 0.0, 0.0],
225        ];
226        let tris = vec![[0, 1, 2], [3, 4, 5], [6, 7, 8]];
227        TriangleMesh::from_raw(verts, tris)
228    }
229    #[test]
230    fn test_full_quality_report_empty() {
231        let m = TriangleMesh::new();
232        let r = compute_full_quality_report(&m);
233        assert_eq!(r.n_poorly_shaped, 0);
234        assert_eq!(r.total_surface_area, 0.0);
235    }
236    #[test]
237    fn test_full_quality_report_equilateral() {
238        let m = equilateral();
239        let r = compute_full_quality_report(&m);
240        assert!(
241            (r.global_min_angle_deg - 60.0).abs() < 1.0,
242            "min_angle={}",
243            r.global_min_angle_deg
244        );
245        assert!(
246            (r.global_max_angle_deg - 60.0).abs() < 1.0,
247            "max_angle={}",
248            r.global_max_angle_deg
249        );
250        assert_eq!(r.n_poorly_shaped, 0);
251    }
252    #[test]
253    fn test_full_quality_report_degenerate_poorly_shaped() {
254        let m = degenerate_mesh();
255        let r = compute_full_quality_report(&m);
256        assert!(r.n_poorly_shaped > 0);
257    }
258    #[test]
259    fn test_full_quality_report_jacobian_positive() {
260        let m = equilateral();
261        let r = compute_full_quality_report(&m);
262        assert!(r.min_jacobian > 0.0);
263        assert!(r.max_jacobian >= r.min_jacobian);
264    }
265    #[test]
266    fn test_full_quality_report_element_type_counts() {
267        let m = equilateral();
268        let r = compute_full_quality_report(&m);
269        let eq_count: u32 = r
270            .element_type_counts
271            .iter()
272            .find(|(t, _)| t == "equilateral")
273            .map(|(_, c)| *c)
274            .unwrap_or(0);
275        assert_eq!(eq_count, 1);
276    }
277    #[test]
278    fn test_per_type_stats_equilateral() {
279        let m = equilateral();
280        let s = per_type_stats(&m);
281        assert_eq!(s.n_equilateral, 1);
282        assert_eq!(s.n_degenerate, 0);
283        assert!(s.equilateral_mean_ar > 0.0);
284    }
285    #[test]
286    fn test_per_type_stats_degenerate() {
287        let m = degenerate_mesh();
288        let s = per_type_stats(&m);
289        assert_eq!(s.n_degenerate, 1);
290    }
291    #[test]
292    fn test_per_type_stats_mixed() {
293        let m = mixed_mesh();
294        let s = per_type_stats(&m);
295        assert_eq!(
296            s.n_equilateral + s.n_right + s.n_obtuse + s.n_acute + s.n_degenerate,
297            3
298        );
299    }
300    #[test]
301    fn test_quality_visualization_data_lengths() {
302        let m = equilateral();
303        let d = quality_visualization_data(&m);
304        assert_eq!(d.aspect_ratios.len(), 1);
305        assert_eq!(d.skewness.len(), 1);
306        assert_eq!(d.min_angles.len(), 1);
307        assert_eq!(d.areas.len(), 1);
308        assert_eq!(d.jacobians.len(), 1);
309    }
310    #[test]
311    fn test_quality_visualization_jacobian_twice_area() {
312        let m = equilateral();
313        let d = quality_visualization_data(&m);
314        for (j, a) in d.jacobians.iter().zip(d.areas.iter()) {
315            assert!((j - 2.0 * a).abs() < 1e-6, "j={j}, 2a={}", 2.0 * a);
316        }
317    }
318    #[test]
319    fn test_quality_visualization_empty() {
320        let m = TriangleMesh::new();
321        let d = quality_visualization_data(&m);
322        assert!(d.aspect_ratios.is_empty());
323    }
324    #[test]
325    fn test_detect_poorly_shaped_equilateral_none() {
326        let m = equilateral();
327        let poor = detect_poorly_shaped(&m, 5.0, 15.0, 150.0, 0.8);
328        assert!(poor.is_empty(), "Equilateral should not be poorly shaped");
329    }
330    #[test]
331    fn test_detect_poorly_shaped_degenerate() {
332        let m = degenerate_mesh();
333        let poor = detect_poorly_shaped(&m, 5.0, 15.0, 150.0, 0.8);
334        assert!(!poor.is_empty());
335        assert!(
336            poor[0]
337                .reasons
338                .iter()
339                .any(|r| matches!(r, PoorShapeReason::Degenerate))
340        );
341    }
342    #[test]
343    fn test_detect_poorly_shaped_strict_aspect_ratio() {
344        let verts = vec![[0.0_f32, 0.0, 0.0], [100.0, 0.0, 0.0], [50.0, 0.01, 0.0]];
345        let m = TriangleMesh::from_raw(verts, vec![[0, 1, 2]]);
346        let poor = detect_poorly_shaped(&m, 2.0, 1.0, 179.0, 0.99);
347        assert!(!poor.is_empty());
348    }
349    #[test]
350    fn test_detect_poorly_shaped_index_correct() {
351        let m = mixed_mesh();
352        let poor = detect_poorly_shaped(&m, 5.0, 15.0, 150.0, 0.8);
353        for p in &poor {
354            assert!(p.index < m.triangle_count());
355        }
356    }
357    #[test]
358    fn test_auto_improve_suggestions_good_mesh() {
359        let m = equilateral();
360        let s = auto_improve_suggestions(&m);
361        assert!(!s.is_empty());
362        assert!(s[0].description.contains("no improvements") || s[0].priority == 5);
363    }
364    #[test]
365    fn test_auto_improve_suggestions_degenerate_priority_1() {
366        let m = degenerate_mesh();
367        let s = auto_improve_suggestions(&m);
368        assert!(
369            s.iter().any(|sg| sg.priority == 1),
370            "Degenerate should be priority 1"
371        );
372    }
373    #[test]
374    fn test_auto_improve_suggestions_sorted_by_priority() {
375        let m = mixed_mesh();
376        let s = auto_improve_suggestions(&m);
377        let priorities: Vec<u8> = s.iter().map(|sg| sg.priority).collect();
378        let sorted: Vec<u8> = {
379            let mut p = priorities.clone();
380            p.sort_unstable();
381            p
382        };
383        assert_eq!(
384            priorities, sorted,
385            "Suggestions should be sorted by priority"
386        );
387    }
388    #[test]
389    fn test_auto_improve_suggestions_empty_mesh() {
390        let m = TriangleMesh::new();
391        let s = auto_improve_suggestions(&m);
392        assert!(!s.is_empty());
393    }
394    #[test]
395    fn test_poor_shape_reason_equality() {
396        assert_eq!(PoorShapeReason::Degenerate, PoorShapeReason::Degenerate);
397        assert_ne!(
398            PoorShapeReason::Degenerate,
399            PoorShapeReason::SmallAngle(10.0)
400        );
401    }
402    #[test]
403    fn test_full_quality_report_surface_area() {
404        let verts = vec![[0.0_f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
405        let m = TriangleMesh::from_raw(verts, vec![[0, 1, 2]]);
406        let r = compute_full_quality_report(&m);
407        assert!(
408            (r.total_surface_area - 0.5).abs() < 1e-5,
409            "area={}",
410            r.total_surface_area
411        );
412    }
413}
414#[cfg(test)]
415mod tests_mesh_quality_new {
416    use super::*;
417    use crate::mesh_quality::types::MeshQuality;
418
419    #[test]
420    fn test_dihedral_flat_triangles_is_180() {
421        let ea = [0.0_f32, 0.0, 0.0];
422        let eb = [1.0, 0.0, 0.0];
423        let p0 = [0.5, 1.0, 0.0];
424        let p1 = [0.5, -1.0, 0.0];
425        let angle = MeshQuality::compute_dihedral_angle(ea, eb, p0, p1);
426        assert!(
427            (angle - 180.0).abs() < 1e-4,
428            "Expected ~180°, got {}",
429            angle
430        );
431    }
432    #[test]
433    fn test_dihedral_perpendicular_is_90() {
434        let ea = [0.0_f32, 0.0, 0.0];
435        let eb = [1.0, 0.0, 0.0];
436        let p0 = [0.5, 1.0, 0.0];
437        let p1 = [0.5, 0.0, 1.0];
438        let angle = MeshQuality::compute_dihedral_angle(ea, eb, p0, p1);
439        assert!((angle - 90.0).abs() < 1e-4, "Expected 90°, got {}", angle);
440    }
441    #[test]
442    fn test_dihedral_degenerate_edge_is_zero() {
443        let p = [0.0_f32, 0.0, 0.0];
444        let q = [1.0, 0.0, 0.0];
445        let r = [0.0, 1.0, 0.0];
446        let angle = MeshQuality::compute_dihedral_angle(p, p, q, r);
447        assert!(angle.abs() < 1e-6 || angle.is_finite(), "Should not panic");
448    }
449    #[test]
450    fn test_edge_length_ratio_equilateral_is_one() {
451        let sqrt3 = 3.0_f32.sqrt();
452        let v0 = [0.0_f32, 0.0, 0.0];
453        let v1 = [2.0, 0.0, 0.0];
454        let v2 = [1.0, sqrt3, 0.0];
455        let ratio = MeshQuality::compute_edge_length_ratio(v0, v1, v2);
456        assert!(
457            (ratio - 1.0).abs() < 1e-5,
458            "Equilateral ratio should be 1, got {}",
459            ratio
460        );
461    }
462    #[test]
463    fn test_edge_length_ratio_elongated_gt_one() {
464        let v0 = [0.0_f32, 0.0, 0.0];
465        let v1 = [10.0, 0.0, 0.0];
466        let v2 = [5.0, 0.1, 0.0];
467        let ratio = MeshQuality::compute_edge_length_ratio(v0, v1, v2);
468        assert!(ratio > 1.0, "Elongated triangle ratio should be > 1");
469    }
470    #[test]
471    fn test_edge_length_ratio_degenerate_returns_infinity() {
472        let p = [0.0_f32, 0.0, 0.0];
473        let ratio = MeshQuality::compute_edge_length_ratio(p, p, p);
474        assert!(
475            ratio.is_infinite(),
476            "Degenerate triangle should return infinity"
477        );
478    }
479    #[test]
480    fn test_regularity_empty_mesh_is_one() {
481        let m = TriangleMesh::new();
482        let r = MeshQuality::compute_mesh_regularity(&m);
483        assert!((r - 1.0).abs() < 1e-6, "Empty mesh regularity should be 1");
484    }
485    #[test]
486    fn test_regularity_equilateral_near_one() {
487        let sqrt3 = 3.0_f32.sqrt();
488        let verts = vec![[0.0_f32, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, sqrt3, 0.0]];
489        let m = TriangleMesh::from_raw(verts, vec![[0, 1, 2]]);
490        let r = MeshQuality::compute_mesh_regularity(&m);
491        assert!(
492            (r - 1.0).abs() < 1e-4,
493            "Equilateral mesh regularity should be ~1, got {}",
494            r
495        );
496    }
497    #[test]
498    fn test_regularity_mixed_mesh_in_range() {
499        let verts = vec![
500            [0.0_f32, 0.0, 0.0],
501            [2.0, 0.0, 0.0],
502            [1.0, 3.0_f32.sqrt(), 0.0],
503            [0.0, 0.0, 0.0],
504            [100.0, 0.0, 0.0],
505            [50.0, 0.01, 0.0],
506        ];
507        let tris = vec![[0u32, 1, 2], [3, 4, 5]];
508        let m = TriangleMesh::from_raw(verts, tris);
509        let r = MeshQuality::compute_mesh_regularity(&m);
510        assert!(
511            r > 0.0 && r < 1.0,
512            "Mixed mesh regularity should be in (0,1), got {}",
513            r
514        );
515    }
516}