1use super::TriangleMesh;
6use crate::error::SpatialResult;
7
8pub 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 let a = v0.distance(v1);
38 let b = v1.distance(v2);
39 let c = v0.distance(v2);
40
41 let s = (a + b + c) * 0.5;
43
44 let area_sq = s * (s - a) * (s - b) * (s - c);
46 if area_sq <= 0.0 {
47 return Ok(f64::INFINITY);
49 }
50 let area = area_sq.sqrt();
51
52 let circum_r = (a * b * c) / (4.0 * area);
54
55 let in_r = area / s;
57
58 if in_r < 1e-15 {
59 return Ok(f64::INFINITY);
60 }
61
62 Ok(circum_r / (2.0 * in_r))
65}
66
67pub 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 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 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#[derive(Debug, Clone)]
120pub struct QualityStats {
121 pub min_aspect_ratio: f64,
123 pub max_aspect_ratio: f64,
125 pub mean_aspect_ratio: f64,
127 pub min_angle: f64,
129 pub max_min_angle: f64,
131 pub mean_min_angle: f64,
133 pub num_faces: usize,
135}
136
137pub 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 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 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 assert!(ar > 10.0, "Expected high aspect ratio, got {}", ar);
248
249 let ang = face_min_angle(&mesh, 0).expect("min angle");
250 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}