1use core::fmt;
18
19use axiolid_contracts::{ExecutionOptions, GeomError};
20use axiolid_core::Tolerance;
21
22use super::faces::FaceConformanceFailure;
23use super::{check_outcome, draw, mesh_of, same_drawing, ConformanceFailure, ConformanceReport};
24use crate::fixtures::{
25 box_mesh, limits, perspective_cases, perspective_refusals, plan_frame, reversed, Fixture,
26};
27use crate::{
28 EdgeSelection, HiddenLineError, HiddenLineMesh, HiddenLineRequest, MeshHiddenLines, MeshId,
29 MeshRole, MeshVisibleFaces, View, ViewProjection, VisibleFaceRequest,
30};
31
32#[derive(Debug, Clone, PartialEq)]
34#[non_exhaustive]
35pub enum PerspectiveConformanceFailure {
36 HiddenLinesBase(ConformanceFailure),
39 VisibleFacesBase(FaceConformanceFailure),
42 UnclaimedButDrawn {
45 operation: &'static str,
47 },
48 UnclaimedWrongRefusal {
52 operation: &'static str,
54 error: HiddenLineError,
56 },
57 HiddenLines(ConformanceFailure),
60 ClaimUnverifiable {
62 operation: &'static str,
64 },
65}
66
67#[derive(Debug, Clone, PartialEq)]
69#[non_exhaustive]
70pub struct PerspectiveConformanceReport {
71 pub failures: Vec<PerspectiveConformanceFailure>,
73 pub claimed: bool,
76}
77
78impl PerspectiveConformanceReport {
79 pub fn is_success(&self) -> bool {
81 self.failures.is_empty()
82 }
83}
84
85impl fmt::Display for PerspectiveConformanceReport {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 if self.is_success() {
88 write!(
89 f,
90 "{}",
91 if self.claimed {
92 "conformant"
93 } else {
94 "not claimed"
95 }
96 )
97 } else {
98 write!(f, "{:?}", self.failures)
99 }
100 }
101}
102
103pub struct PerspectiveConformanceSuite;
106
107impl PerspectiveConformanceSuite {
108 pub fn run(provider: &dyn MeshHiddenLines) -> PerspectiveConformanceReport {
110 let claimed = provider.supports_projection(ViewProjection::Perspective);
111 let mut report = PerspectiveConformanceReport {
112 failures: Vec::new(),
113 claimed,
114 };
115 const OPERATION: &str = "hidden lines";
116 if claimed {
117 report.failures = claimed_failures(provider)
118 .into_iter()
119 .map(PerspectiveConformanceFailure::HiddenLines)
120 .collect();
121 return report;
122 }
123 let mesh = box_mesh([-1.0, -1.0, -4.0], [1.0, 1.0, -2.0]);
124 let meshes = [HiddenLineMesh::new(MeshId(1), MeshRole::Solid, &mesh)];
125 let request = HiddenLineRequest::new(
126 &meshes,
127 perspective_view(),
128 EdgeSelection::new(Some(0.5)),
129 limits(),
130 );
131 let result = provider.hidden_lines(&request, &options());
132 record_unclaimed(&mut report, OPERATION, result.map(|_| ()));
133 report
134 }
135
136 pub fn run_faces(provider: &dyn MeshVisibleFaces) -> PerspectiveConformanceReport {
138 let claimed = provider.supports_projection(ViewProjection::Perspective);
139 let mut report = PerspectiveConformanceReport {
140 failures: Vec::new(),
141 claimed,
142 };
143 const OPERATION: &str = "visible faces";
144 if claimed {
145 report
146 .failures
147 .push(PerspectiveConformanceFailure::ClaimUnverifiable {
148 operation: OPERATION,
149 });
150 return report;
151 }
152 let mesh = box_mesh([-1.0, -1.0, -4.0], [1.0, 1.0, -2.0]);
153 let meshes = [HiddenLineMesh::new(MeshId(1), MeshRole::Solid, &mesh)];
154 let request =
155 VisibleFaceRequest::new(&meshes, perspective_view(), crate::face_fixtures::limits());
156 let result = provider.visible_faces(&request, &options());
157 record_unclaimed(&mut report, OPERATION, result.map(|_| ()));
158 report
159 }
160}
161
162fn claimed_failures(provider: &dyn MeshHiddenLines) -> Vec<ConformanceFailure> {
166 let options = options();
167 let mut report = ConformanceReport::default();
168 for case in perspective_cases() {
169 let first = draw(provider, &case, &case.meshes, &options);
170 let second = draw(provider, &case, &case.meshes, &options);
171 match (first, second) {
172 (Ok(a), Ok(b)) => {
173 check_outcome(&mut report, &case, &a);
174 if a != b {
175 report
176 .failures
177 .push(ConformanceFailure::NonDeterministic { case: case.name });
178 }
179 if case.orientation_independent {
180 let flipped: Vec<Fixture> = case
181 .meshes
182 .iter()
183 .map(|f| Fixture {
184 id: f.id,
185 role: f.role,
186 mesh: reversed(&f.mesh),
187 groups: f.groups.clone(),
188 planar: f.planar,
189 })
190 .collect();
191 match draw(provider, &case, &flipped, &options) {
192 Ok(outcome) if same_drawing(&outcome, &a) => {}
193 Ok(_) => report
194 .failures
195 .push(ConformanceFailure::OrientationDependent { case: case.name }),
196 Err(error) => report.failures.push(ConformanceFailure::ReturnedError {
197 case: case.name,
198 error,
199 }),
200 }
201 }
202 }
203 (Err(error), _) | (_, Err(error)) => {
204 report.failures.push(ConformanceFailure::ReturnedError {
205 case: case.name,
206 error,
207 })
208 }
209 }
210 }
211 for refusal in perspective_refusals() {
212 let meshes: Vec<HiddenLineMesh<'_>> = refusal.meshes.iter().map(mesh_of).collect();
213 let request = HiddenLineRequest::new(&meshes, refusal.view, refusal.edges, refusal.limits);
214 match provider.hidden_lines(&request, &options) {
215 Ok(_) => report
216 .failures
217 .push(ConformanceFailure::MissingRefusal { case: refusal.name }),
218 Err(error) if !(refusal.accepts)(&error.error) => {
219 report.failures.push(ConformanceFailure::ReturnedError {
220 case: refusal.name,
221 error,
222 })
223 }
224 Err(_) => {}
225 }
226 }
227 report.failures
228}
229
230fn perspective_view() -> View {
231 View::perspective(plan_frame(), -1.0).expect("a negative finite near")
232}
233
234fn options() -> ExecutionOptions {
235 ExecutionOptions::new(Tolerance::new(1e-9, 1e-9).expect("valid tolerance"))
236}
237
238fn record_unclaimed(
239 report: &mut PerspectiveConformanceReport,
240 operation: &'static str,
241 result: Result<(), HiddenLineError>,
242) {
243 match result {
244 Ok(()) => report
245 .failures
246 .push(PerspectiveConformanceFailure::UnclaimedButDrawn { operation }),
247 Err(HiddenLineError {
248 error: GeomError::Unsupported { .. } | GeomError::UnsupportedInput { .. },
249 ..
250 }) => {}
251 Err(error) => report
252 .failures
253 .push(PerspectiveConformanceFailure::UnclaimedWrongRefusal { operation, error }),
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use axiolid_contracts::{Backend, BackendDescriptor, BackendId, ExecutionTarget, Operation};
260
261 use super::*;
262 use crate::{HiddenLineOutcome, VisibleFaceOutcome};
263
264 #[derive(Debug)]
266 struct Probe {
267 claims: bool,
268 answer: Answer,
269 }
270
271 #[derive(Debug, Clone, Copy)]
272 enum Answer {
273 Unsupported,
274 UnsupportedInput,
275 Invalid,
276 Draw,
277 }
278
279 impl Backend for Probe {
280 fn descriptor(&self) -> BackendDescriptor {
281 BackendDescriptor::new(
282 BackendId::new("perspective-probe"),
283 ExecutionTarget::PortableCpu,
284 )
285 }
286 }
287
288 impl Probe {
289 fn refusal(&self, operation: Operation) -> Option<HiddenLineError> {
290 let backend = BackendId::new("perspective-probe");
291 match self.answer {
292 Answer::Unsupported => Some(GeomError::Unsupported { backend, operation }.into()),
293 Answer::UnsupportedInput => Some(
294 GeomError::UnsupportedInput {
295 backend,
296 operation,
297 input: "perspective view",
298 }
299 .into(),
300 ),
301 Answer::Invalid => Some(GeomError::InvalidInput("no".into()).into()),
302 Answer::Draw => None,
303 }
304 }
305 }
306
307 impl MeshHiddenLines for Probe {
308 fn supports_projection(&self, projection: ViewProjection) -> bool {
309 projection == ViewProjection::Orthographic || self.claims
310 }
311
312 fn hidden_lines(
313 &self,
314 request: &HiddenLineRequest<'_>,
315 _options: &ExecutionOptions,
316 ) -> Result<HiddenLineOutcome, HiddenLineError> {
317 match self.refusal(Operation::MeshHiddenLines) {
318 Some(error) => Err(error),
319 None => Ok(HiddenLineOutcome::new(
320 request.view,
321 Vec::new(),
322 crate::HiddenLineEvidence::input_mesh(0, 0),
323 )),
324 }
325 }
326 }
327
328 impl MeshVisibleFaces for Probe {
329 fn supports_projection(&self, projection: ViewProjection) -> bool {
330 projection == ViewProjection::Orthographic || self.claims
331 }
332
333 fn visible_faces(
334 &self,
335 request: &VisibleFaceRequest<'_>,
336 _options: &ExecutionOptions,
337 ) -> Result<VisibleFaceOutcome, HiddenLineError> {
338 match self.refusal(Operation::MeshVisibleFaces) {
339 Some(error) => Err(error),
340 None => Ok(VisibleFaceOutcome::new(
341 request.view,
342 Vec::new(),
343 crate::VisibleFaceEvidence::input_mesh(0, 0),
344 )),
345 }
346 }
347 }
348
349 fn both(probe: &Probe) -> [PerspectiveConformanceReport; 2] {
350 [
351 PerspectiveConformanceSuite::run(probe),
352 PerspectiveConformanceSuite::run_faces(probe),
353 ]
354 }
355
356 #[test]
357 fn a_provider_that_refuses_by_name_passes_unclaimed() {
358 for answer in [Answer::Unsupported, Answer::UnsupportedInput] {
359 for report in both(&Probe {
360 claims: false,
361 answer,
362 }) {
363 assert!(!report.claimed);
364 assert!(report.is_success(), "{report}");
365 assert_eq!(report.to_string(), "not claimed");
366 }
367 }
368 }
369
370 #[test]
371 fn an_unclaimed_provider_that_draws_or_refuses_otherwise_fails() {
372 let [lines, faces] = both(&Probe {
373 claims: false,
374 answer: Answer::Draw,
375 });
376 assert_eq!(
377 lines.failures,
378 [PerspectiveConformanceFailure::UnclaimedButDrawn {
379 operation: "hidden lines"
380 }]
381 );
382 assert_eq!(
383 faces.failures,
384 [PerspectiveConformanceFailure::UnclaimedButDrawn {
385 operation: "visible faces"
386 }]
387 );
388 for report in both(&Probe {
389 claims: false,
390 answer: Answer::Invalid,
391 }) {
392 assert!(matches!(
393 report.failures[..],
394 [PerspectiveConformanceFailure::UnclaimedWrongRefusal { .. }]
395 ));
396 }
397 }
398
399 #[test]
400 fn a_claim_is_inferred_from_supports_projection_and_verified_by_the_fixtures() {
401 let probe = Probe {
402 claims: true,
403 answer: Answer::Draw,
404 };
405 let lines = PerspectiveConformanceSuite::run(&probe);
406 assert!(lines.claimed);
407 assert!(!lines.is_success());
410 assert!(lines
411 .failures
412 .iter()
413 .all(|f| matches!(f, PerspectiveConformanceFailure::HiddenLines(_))));
414 for case in [
415 "perspective box",
416 "perspective box cut by the near plane",
417 "perspective eye inside a box",
418 ] {
419 assert!(
420 lines.failures.iter().any(|f| matches!(
421 f,
422 PerspectiveConformanceFailure::HiddenLines(
423 ConformanceFailure::WrongSegmentCount { case: c, .. }
424 ) if *c == case
425 )),
426 "{case}: {lines}"
427 );
428 }
429 assert!(lines.failures.iter().any(|f| matches!(
430 f,
431 PerspectiveConformanceFailure::HiddenLines(ConformanceFailure::MissingRefusal {
432 case: "perspective view without a near depth"
433 })
434 )));
435 }
436
437 #[test]
438 fn a_visible_face_claim_still_fails_closed() {
439 let faces = PerspectiveConformanceSuite::run_faces(&Probe {
440 claims: true,
441 answer: Answer::Draw,
442 });
443 assert!(faces.claimed);
444 assert!(matches!(
445 faces.failures[..],
446 [PerspectiveConformanceFailure::ClaimUnverifiable {
447 operation: "visible faces"
448 }]
449 ));
450 }
451}