1use core::fmt;
4
5use axiolid_contracts::ExecutionOptions;
6use axiolid_core::{Frame3, Point3, Tolerance, Vec3};
7use axiolid_mesh::TriMesh;
8
9pub use crate::region_conformance::{
10 RegionConformanceFailure, RegionConformanceReport, RegionConformanceSuite,
11};
12use crate::{MeshPlaneSection, SectionLimits};
13
14#[derive(Debug, Clone, PartialEq)]
15pub enum ConformanceFailure {
16 ReturnedError(String),
17 EmptyCentralSection,
18 IncorrectEvidence,
19 NonDeterministic,
20 WrongContourCount {
22 case: &'static str,
23 expected: usize,
24 actual: usize,
25 },
26 WrongSectionArea {
28 case: &'static str,
29 expected: f64,
30 actual: f64,
31 },
32 TangentPlaneHasArea {
35 case: &'static str,
36 area: f64,
37 },
38}
39
40#[derive(Debug, Clone, Default, PartialEq)]
41pub struct ConformanceReport {
42 pub failures: Vec<ConformanceFailure>,
43}
44
45impl ConformanceReport {
46 pub fn is_success(&self) -> bool {
47 self.failures.is_empty()
48 }
49}
50
51impl fmt::Display for ConformanceReport {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 if self.is_success() {
54 write!(f, "conformant")
55 } else {
56 write!(f, "{:?}", self.failures)
57 }
58 }
59}
60
61pub struct ConformanceSuite;
62
63impl ConformanceSuite {
64 pub fn run(provider: &dyn MeshPlaneSection) -> ConformanceReport {
65 let mut report = ConformanceReport::default();
66 let mesh = unit_cube();
67 let frame = Frame3 {
68 origin: Point3::new(0.0, 0.0, 0.5),
69 x: Vec3::X,
70 y: Vec3::Y,
71 z: Vec3::Z,
72 };
73 let limits = SectionLimits::new(8, 12, 32, 4);
74 let options = ExecutionOptions::new(Tolerance::new(1e-9, 1e-9).expect("valid tolerance"));
75 let first = provider.section(&mesh, frame, limits, &options);
76 let second = provider.section(&mesh, frame, limits, &options);
77 match (first, second) {
78 (Ok(a), Ok(b)) => {
79 if a.contours.is_empty() {
80 report
81 .failures
82 .push(ConformanceFailure::EmptyCentralSection);
83 }
84 if a.evidence.source_triangles != mesh.triangles().len()
85 || !a.evidence.is_derived_from_input_mesh()
86 {
87 report.failures.push(ConformanceFailure::IncorrectEvidence);
88 }
89 if a != b {
90 report.failures.push(ConformanceFailure::NonDeterministic);
91 }
92 }
93 (Err(error), _) | (_, Err(error)) => report
94 .failures
95 .push(ConformanceFailure::ReturnedError(error.to_string())),
96 }
97
98 let sphere = icosphere(1.0, 3);
108 for spec in &[
109 SphereCase {
111 case: "sphere central",
112 height: 0.0,
113 contours: 1,
114 tolerance: 0.01,
115 },
116 SphereCase {
119 case: "sphere h=0.5",
120 height: 0.5,
121 contours: 1,
122 tolerance: 0.02,
123 },
124 SphereCase {
126 case: "sphere above pole",
127 height: 1.5,
128 contours: 0,
129 tolerance: 0.0,
130 },
131 SphereCase {
135 case: "sphere tangent pole",
136 height: 1.0,
137 contours: 0,
138 tolerance: 0.0,
139 },
140 ] {
141 check_sphere_section(provider, &mut report, &sphere, 1.0, spec);
142 }
143
144 report
145 }
146}
147
148pub(crate) fn unit_cube() -> TriMesh {
149 TriMesh::new(
150 vec![
151 Point3::new(0.0, 0.0, 0.0),
152 Point3::new(1.0, 0.0, 0.0),
153 Point3::new(1.0, 1.0, 0.0),
154 Point3::new(0.0, 1.0, 0.0),
155 Point3::new(0.0, 0.0, 1.0),
156 Point3::new(1.0, 0.0, 1.0),
157 Point3::new(1.0, 1.0, 1.0),
158 Point3::new(0.0, 1.0, 1.0),
159 ],
160 vec![
161 0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 1, 2, 6, 1, 6, 5, 2, 3, 7, 2, 7,
162 6, 3, 0, 4, 3, 4, 7,
163 ],
164 )
165}
166
167fn contour_area(contour: &crate::SectionContour) -> f64 {
169 let p = &contour.points;
170 if p.len() < 3 {
171 return 0.0;
172 }
173 let mut twice = 0.0;
174 for i in 0..p.len() {
175 let a = p[i];
176 let b = p[(i + 1) % p.len()];
177 twice += a.x * b.y - b.x * a.y;
178 }
179 (twice / 2.0).abs()
180}
181
182fn total_area(contours: &[crate::SectionContour]) -> f64 {
184 contours.iter().map(contour_area).sum()
185}
186
187fn icosphere(radius: f64, subdivisions: u32) -> TriMesh {
195 let t = (1.0 + 5.0_f64.sqrt()) / 2.0;
196 let mut v: Vec<[f64; 3]> = vec![
197 [-1.0, t, 0.0],
198 [1.0, t, 0.0],
199 [-1.0, -t, 0.0],
200 [1.0, -t, 0.0],
201 [0.0, -1.0, t],
202 [0.0, 1.0, t],
203 [0.0, -1.0, -t],
204 [0.0, 1.0, -t],
205 [t, 0.0, -1.0],
206 [t, 0.0, 1.0],
207 [-t, 0.0, -1.0],
208 [-t, 0.0, 1.0],
209 ];
210 let mut f: Vec<[u32; 3]> = vec![
211 [0, 11, 5],
212 [0, 5, 1],
213 [0, 1, 7],
214 [0, 7, 10],
215 [0, 10, 11],
216 [1, 5, 9],
217 [5, 11, 4],
218 [11, 10, 2],
219 [10, 7, 6],
220 [7, 1, 8],
221 [3, 9, 4],
222 [3, 4, 2],
223 [3, 2, 6],
224 [3, 6, 8],
225 [3, 8, 9],
226 [4, 9, 5],
227 [2, 4, 11],
228 [6, 2, 10],
229 [8, 6, 7],
230 [9, 8, 1],
231 ];
232 for _ in 0..subdivisions {
233 let mut mid: std::collections::HashMap<(u32, u32), u32> = std::collections::HashMap::new();
234 let mut next: Vec<[u32; 3]> = Vec::with_capacity(f.len() * 4);
235 for tri in &f {
236 let mut m = [0u32; 3];
237 for e in 0..3 {
238 let (a, b) = (tri[e], tri[(e + 1) % 3]);
239 let key = (a.min(b), a.max(b));
240 m[e] = *mid.entry(key).or_insert_with(|| {
241 let (pa, pb) = (v[a as usize], v[b as usize]);
242 v.push([
243 (pa[0] + pb[0]) * 0.5,
244 (pa[1] + pb[1]) * 0.5,
245 (pa[2] + pb[2]) * 0.5,
246 ]);
247 (v.len() - 1) as u32
248 });
249 }
250 next.push([tri[0], m[0], m[2]]);
251 next.push([tri[1], m[1], m[0]]);
252 next.push([tri[2], m[2], m[1]]);
253 next.push([m[0], m[1], m[2]]);
254 }
255 f = next;
256 }
257 let positions: Vec<Point3> = v
258 .iter()
259 .map(|p| {
260 let l = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
261 let k = radius / l;
262 Point3::new(p[0] * k, p[1] * k, p[2] * k)
263 })
264 .collect();
265 let indices: Vec<u32> = f.iter().flat_map(|t| [t[0], t[1], t[2]]).collect();
266 TriMesh::new(positions, indices)
267}
268
269struct SphereCase {
271 case: &'static str,
273 height: f64,
275 contours: usize,
277 tolerance: f64,
279}
280
281fn check_sphere_section(
289 provider: &dyn MeshPlaneSection,
290 report: &mut ConformanceReport,
291 sphere: &TriMesh,
292 radius: f64,
293 spec: &SphereCase,
294) {
295 let SphereCase {
296 case,
297 height,
298 contours: expect_contours,
299 tolerance: rel_tolerance,
300 } = *spec;
301 let frame = Frame3 {
302 origin: Point3::new(0.0, 0.0, height),
303 x: Vec3::X,
304 y: Vec3::Y,
305 z: Vec3::Z,
306 };
307 let limits = SectionLimits::new(4096, 8192, 16384, 64);
308 let options = ExecutionOptions::new(Tolerance::new(1e-9, 1e-9).expect("valid tolerance"));
309 match provider.section(sphere, frame, limits, &options) {
310 Err(error) => report
311 .failures
312 .push(ConformanceFailure::ReturnedError(error.to_string())),
313 Ok(outcome) => {
314 let closed = outcome.contours.len();
315 if closed != expect_contours {
316 report.failures.push(ConformanceFailure::WrongContourCount {
317 case,
318 expected: expect_contours,
319 actual: closed,
320 });
321 }
322 let area = total_area(&outcome.contours);
323 if expect_contours == 0 {
324 if area > 1e-9 {
327 report
328 .failures
329 .push(ConformanceFailure::TangentPlaneHasArea { case, area });
330 }
331 return;
332 }
333 let expected = core::f64::consts::PI * (radius * radius - height * height);
334 if expected > 0.0 && (area - expected).abs() / expected > rel_tolerance {
335 report.failures.push(ConformanceFailure::WrongSectionArea {
336 case,
337 expected,
338 actual: area,
339 });
340 }
341 }
342 }
343}