1use 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#[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#[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#[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#[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}