1use core::cmp::Ordering;
14use core::fmt;
15
16use axiolid_contracts::{DisplacementBound, ExecutionOptions};
17use axiolid_core::{Point2, Tolerance};
18
19use crate::face_fixtures::{
20 area_cases, cases, face_refusals, snap_cases, EvidenceExpect, FaceFixture, AREA_TOLERANCE,
21};
22use crate::fixtures::reversed;
23use crate::{
24 FaceSource, HiddenLineError, HiddenLineMesh, MeshId, MeshRole, MeshVisibleFaces, View,
25 VisibleFaceLimits, VisibleFaceOutcome, VisibleFaceRequest, VisibleRegion,
26};
27
28#[derive(Debug, Clone, PartialEq)]
29pub enum FaceConformanceFailure {
30 ReturnedError {
33 case: &'static str,
34 error: HiddenLineError,
35 },
36 MissingRefusal { case: &'static str },
38 WrongRefusalMesh {
40 case: &'static str,
41 expected: Option<MeshId>,
42 actual: Option<MeshId>,
43 },
44 WrongRegions {
46 case: &'static str,
47 expected: Vec<VisibleRegion>,
48 actual: Vec<VisibleRegion>,
49 },
50 WrongArea {
52 case: &'static str,
53 mesh: MeshId,
54 source: FaceSource,
55 expected: f64,
56 actual: f64,
57 },
58 WrongRegionCount {
60 case: &'static str,
61 expected: usize,
62 actual: usize,
63 },
64 IncorrectEvidence { case: &'static str },
66 NonDeterministic { case: &'static str },
68 OrientationDependent { case: &'static str },
70 InvalidShape { case: &'static str, detail: String },
72}
73
74#[derive(Debug, Clone, Default, PartialEq)]
75pub struct FaceConformanceReport {
76 pub failures: Vec<FaceConformanceFailure>,
77}
78
79impl FaceConformanceReport {
80 pub fn is_success(&self) -> bool {
81 self.failures.is_empty()
82 }
83}
84
85impl fmt::Display for FaceConformanceReport {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 if self.is_success() {
88 write!(f, "conformant")
89 } else {
90 write!(f, "{:?}", self.failures)
91 }
92 }
93}
94
95pub struct FaceConformanceSuite;
96
97struct Scene<'a> {
99 name: &'static str,
100 meshes: &'a [FaceFixture],
101 view: View,
102}
103
104impl FaceConformanceSuite {
105 pub fn run(provider: &dyn MeshVisibleFaces) -> FaceConformanceReport {
106 let mut report = FaceConformanceReport::default();
107 let options = ExecutionOptions::new(Tolerance::new(1e-9, 1e-9).expect("valid tolerance"));
108
109 for case in cases() {
110 let scene = Scene {
111 name: case.name,
112 meshes: &case.meshes,
113 view: case.view,
114 };
115 let first = answer(
116 provider,
117 &case.meshes,
118 case.view,
119 case.region,
120 case.limits,
121 &options,
122 );
123 let second = answer(
124 provider,
125 &case.meshes,
126 case.view,
127 case.region,
128 case.limits,
129 &options,
130 );
131 match (first, second) {
132 (Ok(a), Ok(b)) => {
133 check_shape(&mut report, &scene, &a);
134 if a.regions != case.regions {
135 report.failures.push(FaceConformanceFailure::WrongRegions {
136 case: case.name,
137 expected: case.regions.clone(),
138 actual: a.regions.clone(),
139 });
140 }
141 check_expected_evidence(&mut report, case.name, &case.evidence, &a);
142 if a != b {
143 report
144 .failures
145 .push(FaceConformanceFailure::NonDeterministic { case: case.name });
146 }
147 check_orientation(
148 provider,
149 &mut report,
150 &scene,
151 case.region,
152 case.limits,
153 &a,
154 &options,
155 );
156 }
157 (Err(error), _) | (_, Err(error)) => {
158 report.failures.push(FaceConformanceFailure::ReturnedError {
159 case: case.name,
160 error,
161 })
162 }
163 }
164 }
165
166 for case in area_cases() {
167 let scene = Scene {
168 name: case.name,
169 meshes: &case.meshes,
170 view: case.view,
171 };
172 let first = answer(
173 provider,
174 &case.meshes,
175 case.view,
176 None,
177 case.limits,
178 &options,
179 );
180 let second = answer(
181 provider,
182 &case.meshes,
183 case.view,
184 None,
185 case.limits,
186 &options,
187 );
188 match (first, second) {
189 (Ok(a), Ok(b)) => {
190 check_shape(&mut report, &scene, &a);
191 check_areas(&mut report, &case.areas, case.name, &a);
192 if a != b {
193 report
194 .failures
195 .push(FaceConformanceFailure::NonDeterministic { case: case.name });
196 }
197 check_orientation(
198 provider,
199 &mut report,
200 &scene,
201 None,
202 case.limits,
203 &a,
204 &options,
205 );
206 }
207 (Err(error), _) | (_, Err(error)) => {
208 report.failures.push(FaceConformanceFailure::ReturnedError {
209 case: case.name,
210 error,
211 })
212 }
213 }
214 }
215
216 for case in snap_cases() {
217 let scene = Scene {
218 name: case.name,
219 meshes: &case.meshes,
220 view: case.view,
221 };
222 let first = answer(
223 provider,
224 &case.meshes,
225 case.view,
226 None,
227 case.limits,
228 &options,
229 );
230 let second = answer(
231 provider,
232 &case.meshes,
233 case.view,
234 None,
235 case.limits,
236 &options,
237 );
238 match (first, second) {
239 (Ok(a), Ok(b)) => {
240 check_shape(&mut report, &scene, &a);
241 let evidence = &a.evidence;
244 if evidence.rerouted_edges
245 + evidence.collapsed_regions
246 + evidence.collapsed_holes
247 == 0
248 || evidence.snap_displacement == DisplacementBound::ZERO
249 {
250 report
251 .failures
252 .push(FaceConformanceFailure::IncorrectEvidence { case: case.name });
253 }
254 if a != b {
255 report
256 .failures
257 .push(FaceConformanceFailure::NonDeterministic { case: case.name });
258 }
259 check_orientation(
260 provider,
261 &mut report,
262 &scene,
263 None,
264 case.limits,
265 &a,
266 &options,
267 );
268 }
269 (Err(error), _) | (_, Err(error)) => {
270 report.failures.push(FaceConformanceFailure::ReturnedError {
271 case: case.name,
272 error,
273 })
274 }
275 }
276 }
277
278 for refusal in face_refusals() {
279 match answer(
280 provider,
281 &refusal.meshes,
282 refusal.view,
283 None,
284 refusal.limits,
285 &options,
286 ) {
287 Ok(_) => report
288 .failures
289 .push(FaceConformanceFailure::MissingRefusal { case: refusal.name }),
290 Err(error) if !(refusal.accepts)(&error.error) => {
291 report.failures.push(FaceConformanceFailure::ReturnedError {
292 case: refusal.name,
293 error,
294 })
295 }
296 Err(error) if error.mesh != refusal.mesh => {
297 report
298 .failures
299 .push(FaceConformanceFailure::WrongRefusalMesh {
300 case: refusal.name,
301 expected: refusal.mesh,
302 actual: error.mesh,
303 })
304 }
305 Err(_) => {}
306 }
307 }
308
309 report
310 }
311}
312
313fn answer(
314 provider: &dyn MeshVisibleFaces,
315 fixtures: &[FaceFixture],
316 view: View,
317 region: Option<axiolid_core::Aabb2>,
318 limits: VisibleFaceLimits,
319 options: &ExecutionOptions,
320) -> Result<VisibleFaceOutcome, HiddenLineError> {
321 let meshes: Vec<HiddenLineMesh<'_>> = fixtures
322 .iter()
323 .map(|f| {
324 let mesh = HiddenLineMesh::new(f.id, f.role, &f.mesh);
325 if f.planar {
326 mesh.with_planar_groups()
327 } else if f.groups.is_empty() {
328 mesh
329 } else {
330 mesh.with_groups(&f.groups)
331 }
332 })
333 .collect();
334 let mut request = VisibleFaceRequest::new(&meshes, view, limits);
335 if let Some(region) = region {
336 request = request.with_region(region);
337 }
338 provider.visible_faces(&request, options)
339}
340
341fn same_result(a: &VisibleFaceOutcome, b: &VisibleFaceOutcome) -> bool {
344 let mut evidence = b.evidence;
345 evidence.exact_escalations = a.evidence.exact_escalations;
346 a.view == b.view && a.regions == b.regions && a.evidence == evidence
347}
348
349fn check_orientation(
350 provider: &dyn MeshVisibleFaces,
351 report: &mut FaceConformanceReport,
352 scene: &Scene<'_>,
353 region: Option<axiolid_core::Aabb2>,
354 limits: VisibleFaceLimits,
355 expected: &VisibleFaceOutcome,
356 options: &ExecutionOptions,
357) {
358 let flipped: Vec<FaceFixture> = scene
359 .meshes
360 .iter()
361 .map(|f| FaceFixture {
362 id: f.id,
363 role: f.role,
364 mesh: reversed(&f.mesh),
365 groups: f.groups.clone(),
366 planar: f.planar,
367 })
368 .collect();
369 match answer(provider, &flipped, scene.view, region, limits, options) {
370 Ok(outcome) if same_result(&outcome, expected) => {}
371 Ok(_) => report
372 .failures
373 .push(FaceConformanceFailure::OrientationDependent { case: scene.name }),
374 Err(error) => report.failures.push(FaceConformanceFailure::ReturnedError {
375 case: scene.name,
376 error,
377 }),
378 }
379}
380
381fn signed_area(ring: &[Point2]) -> f64 {
382 let n = ring.len();
383 (0..n)
384 .map(|i| {
385 let (a, b) = (ring[i], ring[(i + 1) % n]);
386 a.x * b.y - b.x * a.y
387 })
388 .sum::<f64>()
389 / 2.0
390}
391
392fn grow(expansion: &mut Vec<f64>, b: f64) {
395 let mut carry = b;
396 for component in expansion.iter_mut() {
397 let sum = carry + *component;
398 let virtual_b = sum - carry;
399 let virtual_a = sum - virtual_b;
400 *component = (carry - virtual_a) + (*component - virtual_b);
401 carry = sum;
402 }
403 expansion.push(carry);
404}
405
406fn area_sign(ring: &[Point2]) -> Ordering {
409 let mut sum: Vec<f64> = Vec::new();
410 for (i, a) in ring.iter().enumerate() {
411 let b = ring[(i + 1) % ring.len()];
412 for (x, y, sign) in [(a.x, b.y, 1.0), (b.x, a.y, -1.0)] {
413 let product = x * y;
414 grow(&mut sum, sign * product);
415 grow(&mut sum, sign * x.mul_add(y, -product));
416 }
417 }
418 sum.iter()
419 .rev()
420 .find(|&&component| component != 0.0)
421 .map_or(Ordering::Equal, |component| component.total_cmp(&0.0))
422}
423
424fn point_order(a: &Point2, b: &Point2) -> Ordering {
425 a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y))
426}
427
428fn sequence_order(a: &[Point2], b: &[Point2]) -> Ordering {
429 a.iter()
430 .zip(b)
431 .map(|(p, q)| point_order(p, q))
432 .find(|o| o.is_ne())
433 .unwrap_or_else(|| a.len().cmp(&b.len()))
434}
435
436fn cut_by_near(fixture: &FaceFixture, view: &View) -> bool {
439 let Some(near) = view.near else {
440 return false;
441 };
442 let depth = |p: &axiolid_core::Point3| (*p - view.frame.origin).dot(view.frame.z);
443 let beyond = fixture
444 .mesh
445 .positions
446 .iter()
447 .filter(|p| depth(p) > near)
448 .count();
449 fixture.role == MeshRole::Solid && beyond > 0 && beyond < fixture.mesh.positions.len()
450}
451
452fn check_ring(detail: &mut Option<String>, what: &str, ring: &[Point2], outer: bool) {
453 let mut fail = |text: String| {
454 detail.get_or_insert(text);
455 };
456 if ring.len() < 3 {
457 return fail(format!("{what} has fewer than three points"));
458 }
459 if ring.iter().any(|p| !p.x.is_finite() || !p.y.is_finite()) {
460 return fail(format!("{what} has a non-finite point"));
461 }
462 if ring.iter().any(|p| point_order(p, &ring[0]).is_lt()) {
463 fail(format!("{what} does not start at its smallest point"));
464 }
465 if ring.first() == ring.last() {
466 fail(format!("{what} repeats its first point"));
467 }
468 let area = area_sign(ring);
469 if outer && area != Ordering::Greater {
470 fail(format!("{what} is not counter-clockwise"));
471 }
472 if !outer && area != Ordering::Less {
473 fail(format!("{what} is not clockwise"));
474 }
475}
476
477fn check_shape(
479 report: &mut FaceConformanceReport,
480 scene: &Scene<'_>,
481 outcome: &VisibleFaceOutcome,
482) {
483 let mut invalid = |detail: String| {
484 report.failures.push(FaceConformanceFailure::InvalidShape {
485 case: scene.name,
486 detail,
487 });
488 };
489 let position = |id: MeshId| scene.meshes.iter().position(|m| m.id == id);
490 let mut previous: Option<(usize, FaceSource, &[Point2])> = None;
491 let mut caps = 0;
492 for region in &outcome.regions {
493 let Some(index) = position(region.mesh) else {
494 invalid(format!(
495 "region of mesh {} that is not in the request",
496 region.mesh.0
497 ));
498 continue;
499 };
500 let fixture = &scene.meshes[index];
501 let mut detail = None;
502 check_ring(&mut detail, "outer ring", ®ion.outer, true);
503 for hole in ®ion.holes {
504 check_ring(&mut detail, "hole", hole, false);
505 }
506 if region
507 .holes
508 .windows(2)
509 .any(|w| sequence_order(&w[0], &w[1]).is_ge())
510 {
511 detail.get_or_insert("holes are not strictly sorted".into());
512 }
513 if region.triangles.windows(2).any(|w| w[0] >= w[1]) {
514 detail.get_or_insert("triangles are not strictly ascending".into());
515 }
516 let count = fixture.mesh.triangle_count();
517 if region.triangles.iter().any(|&t| t as usize >= count) {
518 detail.get_or_insert("a triangle index is out of range".into());
519 }
520 match region.source {
521 FaceSource::Group(group) => {
522 if region
523 .triangles
524 .iter()
525 .any(|&t| (t as usize) < count && fixture.group(t as usize) != group)
526 {
527 detail.get_or_insert("a triangle is not in the region's group".into());
528 }
529 }
530 FaceSource::Cap => {
531 caps += 1;
532 if !cut_by_near(fixture, &scene.view) {
533 detail.get_or_insert("cap of a mesh the near plane does not cut".into());
534 }
535 if !region.triangles.is_empty() {
536 detail.get_or_insert("cap lists triangles".into());
537 }
538 }
539 }
540 if let Some((prev_mesh, prev_source, prev_outer)) = previous {
541 let order = prev_mesh
542 .cmp(&index)
543 .then(prev_source.cmp(®ion.source))
544 .then_with(|| sequence_order(prev_outer, ®ion.outer));
545 if order.is_ge() {
546 detail.get_or_insert("regions are not strictly sorted".into());
547 }
548 }
549 previous = Some((index, region.source, ®ion.outer));
550 if let Some(detail) = detail {
551 invalid(format!("mesh {}: {detail}", region.mesh.0));
552 }
553 }
554
555 let evidence = &outcome.evidence;
556 let triangles: usize = scene.meshes.iter().map(|m| m.mesh.triangle_count()).sum();
557 if !evidence.is_derived_from_input_mesh()
558 || evidence.source_meshes != scene.meshes.len()
559 || evidence.source_triangles != triangles
560 || evidence.regions != outcome.regions.len()
561 || evidence.cap_regions != caps
562 {
563 report
564 .failures
565 .push(FaceConformanceFailure::IncorrectEvidence { case: scene.name });
566 }
567}
568
569fn check_expected_evidence(
570 report: &mut FaceConformanceReport,
571 case: &'static str,
572 expect: &EvidenceExpect,
573 outcome: &VisibleFaceOutcome,
574) {
575 let evidence = &outcome.evidence;
576 let wrong = evidence.snap_displacement != DisplacementBound::ZERO
578 || evidence.rerouted_edges != 0
579 || evidence.collapsed_regions != 0
580 || evidence.collapsed_holes != 0
581 || (expect.intersections && evidence.intersection_segments == 0)
582 || (expect.ties && evidence.tied_sides == 0)
583 || expect
584 .edge_on
585 .is_some_and(|n| evidence.edge_on_triangles != n);
586 if wrong {
587 report
588 .failures
589 .push(FaceConformanceFailure::IncorrectEvidence { case });
590 }
591}
592
593fn check_areas(
594 report: &mut FaceConformanceReport,
595 expected: &[(MeshId, FaceSource, f64)],
596 case: &'static str,
597 outcome: &VisibleFaceOutcome,
598) {
599 let known = |r: &VisibleRegion| {
600 expected
601 .iter()
602 .any(|&(m, s, _)| m == r.mesh && s == r.source)
603 };
604 let unexpected = outcome.regions.iter().filter(|r| !known(r)).count();
605 if unexpected > 0 {
606 report
607 .failures
608 .push(FaceConformanceFailure::WrongRegionCount {
609 case,
610 expected: expected.len(),
611 actual: unexpected + expected.len(),
612 });
613 }
614 for &(mesh, source, area) in expected {
615 let actual: f64 = outcome
616 .regions
617 .iter()
618 .filter(|r| r.mesh == mesh && r.source == source)
619 .map(|r| signed_area(&r.outer) + r.holes.iter().map(|h| signed_area(h)).sum::<f64>())
620 .sum();
621 let close = (actual - area).abs() <= AREA_TOLERANCE * area;
623 if !close {
624 report.failures.push(FaceConformanceFailure::WrongArea {
625 case,
626 mesh,
627 source,
628 expected: area,
629 actual,
630 });
631 }
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use axiolid_contracts::{Backend, BackendDescriptor, BackendId, ExecutionTarget, GeomError};
638
639 use super::*;
640 use crate::VisibleFaceEvidence;
641
642 const REFUSAL_CASES: [&str; 8] = [
643 "three triangles on one edge",
644 "inconsistent winding",
645 "sheet declared solid",
646 "group boundaries with a short group slice",
647 "group count mismatch",
648 "region limit",
649 "region vertex limit",
650 "candidate limit",
651 ];
652
653 #[derive(Debug)]
657 struct Refuser {
658 blame_offender: bool,
659 }
660
661 impl Backend for Refuser {
662 fn descriptor(&self) -> BackendDescriptor {
663 BackendDescriptor::new(
664 BackendId::new("face-conformance-refuser"),
665 ExecutionTarget::PortableCpu,
666 )
667 }
668 }
669
670 impl MeshVisibleFaces for Refuser {
671 fn visible_faces(
672 &self,
673 request: &VisibleFaceRequest<'_>,
674 _options: &ExecutionOptions,
675 ) -> Result<VisibleFaceOutcome, HiddenLineError> {
676 if request.meshes.len() == 2 && request.meshes[1].id == MeshId(2) {
677 let mut error = GeomError::NotManifold("refused".into());
678 let groups = request.meshes[1].groups;
679 if matches!(groups, crate::FaceGroups::PerTriangle(g)
680 if g.len() != request.meshes[1].mesh.triangle_count())
681 {
682 error = GeomError::InvalidInput("refused".into());
683 }
684 if request.meshes[1].mesh.triangle_count() == 12
685 && matches!(groups, crate::FaceGroups::Single)
686 {
687 return self.budget(request);
689 }
690 return Err(if self.blame_offender {
691 HiddenLineError::mesh(request.meshes[1].id, error)
692 } else {
693 HiddenLineError::request(error)
694 });
695 }
696 Err(GeomError::Cancelled.into())
697 }
698 }
699
700 impl Refuser {
701 fn budget(
702 &self,
703 request: &VisibleFaceRequest<'_>,
704 ) -> Result<VisibleFaceOutcome, HiddenLineError> {
705 let resource = if request.limits.max_regions == 1 {
706 "visible-face regions"
707 } else if request.limits.max_region_vertices == 5 {
708 "visible-face region vertices"
709 } else if request.limits.max_candidate_pairs == 1 {
710 "visible-face candidate pairs"
711 } else {
712 return Err(GeomError::Cancelled.into());
713 };
714 Err(GeomError::BudgetExceeded { resource }.into())
715 }
716 }
717
718 fn refusal_failures(report: &FaceConformanceReport) -> Vec<&FaceConformanceFailure> {
719 report
720 .failures
721 .iter()
722 .filter(|failure| match failure {
723 FaceConformanceFailure::ReturnedError { case, .. }
724 | FaceConformanceFailure::MissingRefusal { case }
725 | FaceConformanceFailure::WrongRefusalMesh { case, .. } => {
726 REFUSAL_CASES.contains(case)
727 }
728 _ => false,
729 })
730 .collect()
731 }
732
733 #[test]
734 fn a_provider_that_names_the_offending_mesh_passes_the_refusal_cases() {
735 let report = FaceConformanceSuite::run(&Refuser {
736 blame_offender: true,
737 });
738 assert!(refusal_failures(&report).is_empty(), "{report}");
739 assert!(!report.is_success());
741 }
742
743 #[test]
744 fn a_refusal_that_loses_the_mesh_id_fails_exactly_the_mesh_cases() {
745 let report = FaceConformanceSuite::run(&Refuser {
746 blame_offender: false,
747 });
748 let wrong = refusal_failures(&report);
749 assert_eq!(wrong.len(), 5, "{wrong:?}");
750 for failure in wrong {
751 assert!(matches!(
752 failure,
753 FaceConformanceFailure::WrongRefusalMesh { actual: None, .. }
754 ));
755 }
756 }
757
758 #[derive(Debug)]
760 struct Empty;
761
762 impl Backend for Empty {
763 fn descriptor(&self) -> BackendDescriptor {
764 BackendDescriptor::new(
765 BackendId::new("face-conformance-empty"),
766 ExecutionTarget::PortableCpu,
767 )
768 }
769 }
770
771 impl MeshVisibleFaces for Empty {
772 fn visible_faces(
773 &self,
774 request: &VisibleFaceRequest<'_>,
775 _options: &ExecutionOptions,
776 ) -> Result<VisibleFaceOutcome, HiddenLineError> {
777 let triangles = request.meshes.iter().map(|m| m.mesh.triangle_count()).sum();
778 Ok(VisibleFaceOutcome::new(
779 request.view,
780 Vec::new(),
781 VisibleFaceEvidence::input_mesh(request.meshes.len(), triangles),
782 ))
783 }
784 }
785
786 #[test]
787 fn an_empty_answer_and_missing_refusals_are_reported() {
788 let report = FaceConformanceSuite::run(&Empty);
789 let cases = cases().len();
790 let missing = report
791 .failures
792 .iter()
793 .filter(|f| matches!(f, FaceConformanceFailure::MissingRefusal { .. }))
794 .count();
795 let regions = report
796 .failures
797 .iter()
798 .filter(|f| matches!(f, FaceConformanceFailure::WrongRegions { .. }))
799 .count();
800 let areas = report
801 .failures
802 .iter()
803 .filter(|f| matches!(f, FaceConformanceFailure::WrongArea { .. }))
804 .count();
805 assert_eq!(missing, REFUSAL_CASES.len(), "{report}");
806 assert_eq!(regions, cases, "{report}");
807 assert_eq!(areas, 6, "{report}");
808 }
809
810 #[derive(Debug)]
813 struct Fixed(Vec<VisibleRegion>);
814
815 impl Backend for Fixed {
816 fn descriptor(&self) -> BackendDescriptor {
817 BackendDescriptor::new(
818 BackendId::new("face-conformance-fixed"),
819 ExecutionTarget::PortableCpu,
820 )
821 }
822 }
823
824 impl MeshVisibleFaces for Fixed {
825 fn visible_faces(
826 &self,
827 request: &VisibleFaceRequest<'_>,
828 _options: &ExecutionOptions,
829 ) -> Result<VisibleFaceOutcome, HiddenLineError> {
830 Ok(VisibleFaceOutcome::new(
831 request.view,
832 self.0.clone(),
833 VisibleFaceEvidence::input_mesh(0, 0),
834 ))
835 }
836 }
837
838 #[test]
839 fn evidence_that_disagrees_with_the_request_is_reported() {
840 let first = cases().remove(0);
841 let report = FaceConformanceSuite::run(&Fixed(first.regions.clone()));
842 let hit = report.failures.iter().any(|f| {
843 matches!(f, FaceConformanceFailure::IncorrectEvidence { case } if *case == first.name)
844 });
845 assert!(hit, "{report}");
846 assert!(!report.failures.iter().any(|f| {
848 matches!(f, FaceConformanceFailure::WrongRegions { case, .. } if *case == first.name)
849 }));
850 }
851
852 #[test]
853 fn the_expected_regions_pass_the_shape_checks() {
854 for case in cases() {
855 let triangles: usize = case.meshes.iter().map(|m| m.mesh.triangle_count()).sum();
856 let caps = case
857 .regions
858 .iter()
859 .filter(|r| r.source == FaceSource::Cap)
860 .count();
861 let mut evidence = crate::VisibleFaceEvidence::input_mesh(case.meshes.len(), triangles);
862 evidence.regions = case.regions.len();
863 evidence.cap_regions = caps;
864 let outcome = VisibleFaceOutcome::new(case.view, case.regions.clone(), evidence);
865 let scene = Scene {
866 name: case.name,
867 meshes: &case.meshes,
868 view: case.view,
869 };
870 let mut report = FaceConformanceReport::default();
871 check_shape(&mut report, &scene, &outcome);
872 assert!(report.is_success(), "{}: {report}", case.name);
873 }
874 }
875
876 #[test]
877 fn the_area_sign_is_exact_for_slivers() {
878 let at = |x: f64, y: f64| Point2::new(x, y);
879 let apex = |y: f64| [at(1.0, 1.0), at(2.0, 2.0), at(1.5, y)];
881 assert_eq!(area_sign(&apex(1.5f64.next_up())), Ordering::Greater);
882 assert_eq!(area_sign(&apex(1.5)), Ordering::Equal);
883 assert_eq!(area_sign(&apex(1.5f64.next_down())), Ordering::Less);
884 let mut reversed = apex(1.5f64.next_down());
885 reversed.reverse();
886 assert_eq!(area_sign(&reversed), Ordering::Greater);
887 }
888
889 #[test]
890 fn shape_checks_catch_each_kind_of_bad_region() {
891 let case = cases().remove(0);
892 let scene = Scene {
893 name: case.name,
894 meshes: &case.meshes,
895 view: case.view,
896 };
897 let triangles = 24;
898 let mut evidence = crate::VisibleFaceEvidence::input_mesh(2, triangles);
899 evidence.regions = 2;
900 let run = |regions: Vec<VisibleRegion>| {
901 let mut e = evidence;
902 e.regions = regions.len();
903 let mut report = FaceConformanceReport::default();
904 check_shape(
905 &mut report,
906 &scene,
907 &VisibleFaceOutcome::new(case.view, regions, e),
908 );
909 report
910 };
911 let good = case.regions.clone();
912 assert!(run(good.clone()).is_success());
913
914 let mutate = |f: &dyn Fn(&mut Vec<VisibleRegion>)| {
915 let mut regions = good.clone();
916 f(&mut regions);
917 run(regions)
918 };
919 let shaped = |r: &FaceConformanceReport| {
920 r.failures
921 .iter()
922 .any(|f| matches!(f, FaceConformanceFailure::InvalidShape { .. }))
923 };
924 assert!(shaped(&mutate(&|r| r[0].outer.reverse())));
926 assert!(shaped(&mutate(&|r| r[0].outer.rotate_left(1))));
928 assert!(shaped(&mutate(&|r| r.reverse())));
930 assert!(shaped(&mutate(&|r| r[0].triangles = vec![3, 2])));
932 assert!(shaped(&mutate(&|r| r[0].triangles = vec![2, 99])));
933 assert!(shaped(&mutate(&|r| r[0].source = FaceSource::Cap)));
935 assert!(shaped(&mutate(&|r| {
937 r[0].holes = vec![vec![
938 Point2::new(0.5, 0.5),
939 Point2::new(1.0, 0.5),
940 Point2::new(1.0, 1.0),
941 ]]
942 })));
943 assert!(shaped(&mutate(&|r| r[0].mesh = MeshId(99))));
945 }
946}