Skip to main content

scirs2_spatial/mesh/
quality.rs

1//! Mesh quality metrics
2//!
3//! Provides triangle quality assessment including aspect ratio and minimum angle.
4
5use super::TriangleMesh;
6use crate::error::SpatialResult;
7
8/// Compute the aspect ratio of a triangle face
9///
10/// The aspect ratio is the ratio of the circumscribed circle radius to the
11/// inscribed circle radius, normalized so that an equilateral triangle has
12/// aspect ratio 1.0. Higher values indicate poorer quality.
13///
14/// # Arguments
15///
16/// * `mesh` - The triangle mesh
17/// * `face_idx` - Index of the face
18///
19/// # Returns
20///
21/// * The aspect ratio (>= 1.0, where 1.0 is equilateral)
22pub fn face_aspect_ratio(mesh: &TriangleMesh, face_idx: usize) -> SpatialResult<f64> {
23    if face_idx >= mesh.faces.len() {
24        return Err(crate::error::SpatialError::ValueError(format!(
25            "Face index {} out of range (num faces = {})",
26            face_idx,
27            mesh.faces.len()
28        )));
29    }
30
31    let face = &mesh.faces[face_idx];
32    let v0 = &mesh.vertices[face.v0];
33    let v1 = &mesh.vertices[face.v1];
34    let v2 = &mesh.vertices[face.v2];
35
36    // Edge lengths
37    let a = v0.distance(v1);
38    let b = v1.distance(v2);
39    let c = v0.distance(v2);
40
41    // Semi-perimeter
42    let s = (a + b + c) * 0.5;
43
44    // Area via Heron's formula
45    let area_sq = s * (s - a) * (s - b) * (s - c);
46    if area_sq <= 0.0 {
47        // Degenerate triangle
48        return Ok(f64::INFINITY);
49    }
50    let area = area_sq.sqrt();
51
52    // Circumradius R = abc / (4 * area)
53    let circum_r = (a * b * c) / (4.0 * area);
54
55    // Inradius r = area / s
56    let in_r = area / s;
57
58    if in_r < 1e-15 {
59        return Ok(f64::INFINITY);
60    }
61
62    // Aspect ratio normalized so equilateral = 1.0
63    // For equilateral: R/r = 2, so we divide by 2
64    Ok(circum_r / (2.0 * in_r))
65}
66
67/// Compute the minimum angle (in radians) of a triangle face
68///
69/// # Arguments
70///
71/// * `mesh` - The triangle mesh
72/// * `face_idx` - Index of the face
73///
74/// # Returns
75///
76/// * The minimum angle in radians
77pub fn face_min_angle(mesh: &TriangleMesh, face_idx: usize) -> SpatialResult<f64> {
78    if face_idx >= mesh.faces.len() {
79        return Err(crate::error::SpatialError::ValueError(format!(
80            "Face index {} out of range (num faces = {})",
81            face_idx,
82            mesh.faces.len()
83        )));
84    }
85
86    let face = &mesh.faces[face_idx];
87    let v0 = &mesh.vertices[face.v0];
88    let v1 = &mesh.vertices[face.v1];
89    let v2 = &mesh.vertices[face.v2];
90
91    // Edge lengths squared
92    let a_sq = v0.distance_sq(v1);
93    let b_sq = v1.distance_sq(v2);
94    let c_sq = v0.distance_sq(v2);
95
96    let a = a_sq.sqrt();
97    let b = b_sq.sqrt();
98    let c = c_sq.sqrt();
99
100    if a < 1e-15 || b < 1e-15 || c < 1e-15 {
101        return Ok(0.0);
102    }
103
104    // Angles via law of cosines
105    let angle_at_0 = ((a_sq + c_sq - b_sq) / (2.0 * a * c))
106        .clamp(-1.0, 1.0)
107        .acos();
108    let angle_at_1 = ((a_sq + b_sq - c_sq) / (2.0 * a * b))
109        .clamp(-1.0, 1.0)
110        .acos();
111    let angle_at_2 = ((b_sq + c_sq - a_sq) / (2.0 * b * c))
112        .clamp(-1.0, 1.0)
113        .acos();
114
115    Ok(angle_at_0.min(angle_at_1).min(angle_at_2))
116}
117
118/// Summary statistics for mesh quality
119#[derive(Debug, Clone)]
120pub struct QualityStats {
121    /// Minimum aspect ratio across all faces
122    pub min_aspect_ratio: f64,
123    /// Maximum aspect ratio across all faces
124    pub max_aspect_ratio: f64,
125    /// Mean aspect ratio
126    pub mean_aspect_ratio: f64,
127    /// Minimum angle (radians) across all faces
128    pub min_angle: f64,
129    /// Maximum minimum-angle across all faces
130    pub max_min_angle: f64,
131    /// Mean minimum-angle
132    pub mean_min_angle: f64,
133    /// Number of faces analyzed
134    pub num_faces: usize,
135}
136
137/// Compute quality statistics over the entire mesh
138///
139/// # Arguments
140///
141/// * `mesh` - The triangle mesh
142///
143/// # Returns
144///
145/// * Quality statistics summary
146pub fn mesh_quality_stats(mesh: &TriangleMesh) -> SpatialResult<QualityStats> {
147    let nf = mesh.num_faces();
148    if nf == 0 {
149        return Ok(QualityStats {
150            min_aspect_ratio: 0.0,
151            max_aspect_ratio: 0.0,
152            mean_aspect_ratio: 0.0,
153            min_angle: 0.0,
154            max_min_angle: 0.0,
155            mean_min_angle: 0.0,
156            num_faces: 0,
157        });
158    }
159
160    let mut min_ar = f64::INFINITY;
161    let mut max_ar = f64::NEG_INFINITY;
162    let mut sum_ar = 0.0;
163    let mut min_ang = f64::INFINITY;
164    let mut max_min_ang = f64::NEG_INFINITY;
165    let mut sum_ang = 0.0;
166
167    for i in 0..nf {
168        let ar = face_aspect_ratio(mesh, i)?;
169        let ang = face_min_angle(mesh, i)?;
170
171        if ar.is_finite() {
172            min_ar = min_ar.min(ar);
173            max_ar = max_ar.max(ar);
174            sum_ar += ar;
175        }
176
177        min_ang = min_ang.min(ang);
178        max_min_ang = max_min_ang.max(ang);
179        sum_ang += ang;
180    }
181
182    Ok(QualityStats {
183        min_aspect_ratio: if min_ar.is_finite() { min_ar } else { 0.0 },
184        max_aspect_ratio: if max_ar.is_finite() { max_ar } else { 0.0 },
185        mean_aspect_ratio: sum_ar / nf as f64,
186        min_angle: min_ang,
187        max_min_angle: max_min_ang,
188        mean_min_angle: sum_ang / nf as f64,
189        num_faces: nf,
190    })
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use crate::mesh::{Face, Vertex};
197
198    fn equilateral_mesh() -> TriangleMesh {
199        let h = (3.0_f64).sqrt() / 2.0;
200        let vertices = vec![
201            Vertex::new(0.0, 0.0, 0.0),
202            Vertex::new(1.0, 0.0, 0.0),
203            Vertex::new(0.5, h, 0.0),
204        ];
205        let faces = vec![Face::new(0, 1, 2)];
206        TriangleMesh::new(vertices, faces).expect("valid")
207    }
208
209    #[test]
210    fn test_equilateral_aspect_ratio() {
211        let mesh = equilateral_mesh();
212        let ar = face_aspect_ratio(&mesh, 0).expect("aspect ratio failed");
213        // Equilateral triangle should have aspect ratio 1.0
214        assert!(
215            (ar - 1.0).abs() < 1e-10,
216            "Expected aspect ratio ~1.0, got {}",
217            ar
218        );
219    }
220
221    #[test]
222    fn test_equilateral_min_angle() {
223        let mesh = equilateral_mesh();
224        let ang = face_min_angle(&mesh, 0).expect("min angle failed");
225        // Equilateral triangle: all angles are pi/3
226        let expected = std::f64::consts::PI / 3.0;
227        assert!(
228            (ang - expected).abs() < 1e-10,
229            "Expected angle ~{}, got {}",
230            expected,
231            ang
232        );
233    }
234
235    #[test]
236    fn test_skinny_triangle() {
237        let vertices = vec![
238            Vertex::new(0.0, 0.0, 0.0),
239            Vertex::new(10.0, 0.0, 0.0),
240            Vertex::new(5.0, 0.01, 0.0),
241        ];
242        let faces = vec![Face::new(0, 1, 2)];
243        let mesh = TriangleMesh::new(vertices, faces).expect("valid");
244
245        let ar = face_aspect_ratio(&mesh, 0).expect("aspect ratio");
246        // Skinny triangle has high aspect ratio
247        assert!(ar > 10.0, "Expected high aspect ratio, got {}", ar);
248
249        let ang = face_min_angle(&mesh, 0).expect("min angle");
250        // Skinny triangle has very small minimum angle
251        assert!(ang < 0.01, "Expected very small angle, got {}", ang);
252    }
253
254    #[test]
255    fn test_quality_stats() {
256        let h = (3.0_f64).sqrt() / 2.0;
257        let vertices = vec![
258            Vertex::new(0.0, 0.0, 0.0),
259            Vertex::new(1.0, 0.0, 0.0),
260            Vertex::new(0.5, h, 0.0),
261            Vertex::new(0.5, 0.5, 1.0),
262        ];
263        let faces = vec![
264            Face::new(0, 1, 2),
265            Face::new(0, 1, 3),
266            Face::new(1, 2, 3),
267            Face::new(0, 2, 3),
268        ];
269        let mesh = TriangleMesh::new(vertices, faces).expect("valid");
270
271        let stats = mesh_quality_stats(&mesh).expect("stats failed");
272        assert_eq!(stats.num_faces, 4);
273        assert!(stats.min_aspect_ratio >= 1.0 - 1e-10);
274        assert!(stats.max_aspect_ratio >= stats.min_aspect_ratio);
275        assert!(stats.min_angle > 0.0);
276        assert!(stats.min_angle <= stats.max_min_angle);
277    }
278}