1use std::sync::Arc;
43
44use axioval_ir::{Evidence, ObjectId};
45
46use crate::{ConvexPlanRegion, LengthInterval, MetricDirection, SignedDistanceInterval};
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
50pub enum ProximityError {
51 #[error("proximity measurement is unavailable for the requested object")]
53 Unavailable,
54 #[error("the requested object occupies no material")]
58 NoBody,
59 #[error("proximity measurement is not finite, non-negative and coherent")]
61 InvalidMeasurement,
62 #[error("proximity evidence exactness must match geometry fidelity and be reviewable")]
64 EvidenceFidelityMismatch,
65 #[error("proximity of an object to itself is undefined")]
67 SameObject,
68 #[error("the requested proximity projection is not supported")]
70 UnsupportedProjection,
71}
72
73#[derive(Clone, Copy, Debug, PartialEq)]
75pub struct Bounds3 {
76 min: [f64; 3],
77 max: [f64; 3],
78}
79
80impl Bounds3 {
81 pub fn try_new(min: [f64; 3], max: [f64; 3]) -> Result<Self, ProximityError> {
83 let coherent = (0..3)
84 .all(|axis| min[axis].is_finite() && max[axis].is_finite() && min[axis] <= max[axis]);
85 if !coherent {
86 return Err(ProximityError::InvalidMeasurement);
87 }
88 Ok(Self { min, max })
89 }
90 pub fn min(&self) -> [f64; 3] {
91 self.min
92 }
93 pub fn max(&self) -> [f64; 3] {
94 self.max
95 }
96 #[must_use]
98 pub fn expanded(&self, margin: f64) -> Self {
99 Self {
100 min: self.min.map(|value| value - margin),
101 max: self.max.map(|value| value + margin),
102 }
103 }
104 pub fn gap(&self, other: &Self) -> f64 {
109 (0..3)
110 .map(|axis| {
111 let gap = (other.min[axis] - self.max[axis])
112 .max(self.min[axis] - other.max[axis])
113 .max(0.0);
114 gap * gap
115 })
116 .sum::<f64>()
117 .sqrt()
118 }
119}
120
121#[derive(Clone, Copy, Debug, PartialEq)]
123pub enum GeometryFidelity {
124 Exact,
126 Tessellated { chord_deviation_metres: f64 },
129}
130
131impl GeometryFidelity {
132 pub fn tessellated(chord_deviation_metres: f64) -> Result<Self, ProximityError> {
134 if !chord_deviation_metres.is_finite() || chord_deviation_metres < 0.0 {
135 return Err(ProximityError::InvalidMeasurement);
136 }
137 Ok(Self::Tessellated {
138 chord_deviation_metres,
139 })
140 }
141 pub fn is_exact(&self) -> bool {
143 matches!(self, Self::Exact)
144 }
145 pub fn deviation_metres(&self) -> f64 {
147 match self {
148 Self::Exact => 0.0,
149 Self::Tessellated {
150 chord_deviation_metres,
151 } => *chord_deviation_metres,
152 }
153 }
154 #[must_use]
159 pub fn combined(self, other: Self) -> Self {
160 if self.is_exact() && other.is_exact() {
161 Self::Exact
162 } else {
163 Self::Tessellated {
164 chord_deviation_metres: self.deviation_metres() + other.deviation_metres(),
165 }
166 }
167 }
168}
169
170#[derive(Clone, Debug, PartialEq)]
172pub struct ObjectBounds {
173 object: ObjectId,
174 bounds: Bounds3,
175 fidelity: GeometryFidelity,
176}
177
178impl ObjectBounds {
179 pub fn try_new(
180 object: ObjectId,
181 bounds: Bounds3,
182 fidelity: GeometryFidelity,
183 ) -> Result<Self, ProximityError> {
184 let deviation = fidelity.deviation_metres();
186 if !deviation.is_finite() || deviation < 0.0 {
187 return Err(ProximityError::InvalidMeasurement);
188 }
189 Ok(Self {
190 object,
191 bounds,
192 fidelity,
193 })
194 }
195 pub fn object(&self) -> &ObjectId {
196 &self.object
197 }
198 pub fn bounds(&self) -> Bounds3 {
200 self.bounds
201 }
202 pub fn fidelity(&self) -> GeometryFidelity {
203 self.fidelity
204 }
205 pub fn enclosing(&self) -> Bounds3 {
209 self.bounds.expanded(self.fidelity.deviation_metres())
210 }
211}
212
213#[derive(Clone, Copy, Debug)]
218pub enum ProximityProjection {
219 Minimum3d,
221 Horizontal,
223 Vertical {
234 footprint_offset_metres: f64,
235 direction: VerticalDirection,
236 surfaces: VerticalSurfaces,
237 },
238 PlanOverlap,
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
255pub enum VerticalDirection {
256 Either,
258 Above,
261 Below,
264}
265
266#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
281pub enum VerticalSurfaces {
282 #[default]
284 Extents,
285 Between {
287 subject: SubjectSurface,
288 counterpart: CounterpartSurface,
289 },
290}
291
292#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
294pub enum SubjectSurface {
295 Top,
296 Bottom,
297}
298
299#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
301pub enum CounterpartSurface {
302 Top,
303 Bottom,
304 Nearest,
306}
307
308impl SubjectSurface {
309 pub fn name(self) -> &'static str {
311 match self {
312 Self::Top => "top",
313 Self::Bottom => "bottom",
314 }
315 }
316}
317
318impl CounterpartSurface {
319 pub fn name(self) -> &'static str {
321 match self {
322 Self::Top => "top",
323 Self::Bottom => "bottom",
324 Self::Nearest => "nearest",
325 }
326 }
327}
328
329impl VerticalDirection {
330 pub fn name(self) -> &'static str {
332 match self {
333 Self::Either => "either",
334 Self::Above => "above",
335 Self::Below => "below",
336 }
337 }
338}
339
340impl ProximityProjection {
341 fn is_valid(&self) -> bool {
343 match self {
344 Self::Vertical {
345 footprint_offset_metres,
346 surfaces,
347 ..
348 } => {
349 let nearest = matches!(
350 surfaces,
351 VerticalSurfaces::Between {
352 counterpart: CounterpartSurface::Nearest,
353 ..
354 }
355 );
356 footprint_offset_metres.is_finite()
357 && *footprint_offset_metres >= 0.0
358 && !(nearest && *footprint_offset_metres > 0.0)
359 }
360 _ => true,
361 }
362 }
363 pub fn may_be_unrelated(&self) -> bool {
365 matches!(self, Self::Vertical { .. } | Self::PlanOverlap)
366 }
367 pub fn name(&self) -> &'static str {
369 match self {
370 Self::Minimum3d => "minimum_3d",
371 Self::Horizontal => "horizontal",
372 Self::Vertical { .. } => "vertical",
373 Self::PlanOverlap => "plan_overlap",
374 }
375 }
376 fn key(&self) -> (u8, f64, VerticalDirection, VerticalSurfaces) {
377 let none = (VerticalDirection::Either, VerticalSurfaces::Extents);
378 match self {
379 Self::Minimum3d => (0, 0.0, none.0, none.1),
380 Self::Horizontal => (1, 0.0, none.0, none.1),
381 Self::Vertical {
382 footprint_offset_metres,
383 direction,
384 surfaces,
385 } => (2, *footprint_offset_metres, *direction, *surfaces),
386 Self::PlanOverlap => (3, 0.0, none.0, none.1),
387 }
388 }
389}
390
391impl PartialEq for ProximityProjection {
392 fn eq(&self, other: &Self) -> bool {
393 self.cmp(other) == std::cmp::Ordering::Equal
394 }
395}
396impl Eq for ProximityProjection {}
397impl PartialOrd for ProximityProjection {
398 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
399 Some(self.cmp(other))
400 }
401}
402impl Ord for ProximityProjection {
403 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
404 let (a, a_offset, a_direction, a_surfaces) = self.key();
405 let (b, b_offset, b_direction, b_surfaces) = other.key();
406 a.cmp(&b)
407 .then_with(|| a_offset.total_cmp(&b_offset))
408 .then_with(|| a_direction.cmp(&b_direction))
409 .then_with(|| a_surfaces.cmp(&b_surfaces))
410 }
411}
412
413#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
415pub struct ProximityRequest {
416 subject: ObjectId,
417 counterpart: ObjectId,
418 projection: ProximityProjection,
419}
420
421impl ProximityRequest {
422 pub fn try_new(subject: ObjectId, counterpart: ObjectId) -> Result<Self, ProximityError> {
424 Self::projected(subject, counterpart, ProximityProjection::Minimum3d)
425 }
426 pub fn projected(
429 subject: ObjectId,
430 counterpart: ObjectId,
431 projection: ProximityProjection,
432 ) -> Result<Self, ProximityError> {
433 if subject == counterpart {
434 return Err(ProximityError::SameObject);
435 }
436 if !projection.is_valid() {
437 return Err(ProximityError::InvalidMeasurement);
438 }
439 Ok(Self {
440 subject,
441 counterpart,
442 projection,
443 })
444 }
445 pub fn subject(&self) -> &ObjectId {
446 &self.subject
447 }
448 pub fn counterpart(&self) -> &ObjectId {
449 &self.counterpart
450 }
451 pub fn projection(&self) -> ProximityProjection {
452 self.projection
453 }
454}
455
456#[derive(Clone, Copy, Debug, PartialEq)]
464pub struct OverlapExtents {
465 axes: [LengthInterval; 3],
466}
467
468impl OverlapExtents {
469 #[must_use]
471 pub fn new(x: LengthInterval, y: LengthInterval, z: LengthInterval) -> Self {
472 Self { axes: [x, y, z] }
473 }
474 pub fn x(&self) -> LengthInterval {
475 self.axes[0]
476 }
477 pub fn y(&self) -> LengthInterval {
478 self.axes[1]
479 }
480 pub fn z(&self) -> LengthInterval {
481 self.axes[2]
482 }
483 pub fn horizontal(&self) -> LengthInterval {
486 let (x, y) = (self.axes[0], self.axes[1]);
487 LengthInterval::try_new(
488 x.lower_metres().min(y.lower_metres()),
489 x.upper_metres().min(y.upper_metres()),
490 )
491 .unwrap_or_else(|_| unreachable!("the lesser of two intervals is an interval"))
492 }
493 pub fn vertical(&self) -> LengthInterval {
495 self.axes[2]
496 }
497 fn is_empty(&self) -> bool {
498 self.axes.iter().all(|axis| axis.lower_metres() == 0.0)
499 }
500}
501
502#[derive(Clone, Debug, PartialEq)]
505pub struct OverlapAlongRequest {
506 subject: ObjectId,
507 counterpart: ObjectId,
508 directions: Vec<MetricDirection>,
509}
510
511impl OverlapAlongRequest {
512 pub const MAX_DIRECTIONS: usize = 6;
514
515 pub fn try_new(
518 subject: ObjectId,
519 counterpart: ObjectId,
520 directions: Vec<MetricDirection>,
521 ) -> Result<Self, ProximityError> {
522 if subject == counterpart {
523 return Err(ProximityError::SameObject);
524 }
525 if directions.is_empty() || directions.len() > Self::MAX_DIRECTIONS {
526 return Err(ProximityError::InvalidMeasurement);
527 }
528 Ok(Self {
529 subject,
530 counterpart,
531 directions,
532 })
533 }
534 pub fn subject(&self) -> &ObjectId {
535 &self.subject
536 }
537 pub fn counterpart(&self) -> &ObjectId {
538 &self.counterpart
539 }
540 pub fn directions(&self) -> &[MetricDirection] {
542 &self.directions
543 }
544}
545
546#[derive(Clone, Debug, PartialEq)]
555pub struct OverlapAlongEvidence {
556 request: OverlapAlongRequest,
557 extents: Vec<LengthInterval>,
558 fidelity: GeometryFidelity,
559 evidence: Evidence,
560}
561
562impl OverlapAlongEvidence {
563 pub fn try_new(
566 request: OverlapAlongRequest,
567 extents: Vec<LengthInterval>,
568 fidelity: GeometryFidelity,
569 evidence: Evidence,
570 ) -> Result<Self, ProximityError> {
571 if extents.len() != request.directions.len() {
572 return Err(ProximityError::InvalidMeasurement);
573 }
574 if evidence.exact != fidelity.is_exact() || evidence.locator.trim().is_empty() {
575 return Err(ProximityError::EvidenceFidelityMismatch);
576 }
577 Ok(Self {
578 request,
579 extents,
580 fidelity,
581 evidence,
582 })
583 }
584 pub fn request(&self) -> &OverlapAlongRequest {
585 &self.request
586 }
587 pub fn extents(&self) -> &[LengthInterval] {
589 &self.extents
590 }
591 pub fn fidelity(&self) -> GeometryFidelity {
592 self.fidelity
593 }
594 pub fn evidence(&self) -> &Evidence {
595 &self.evidence
596 }
597}
598
599#[derive(Clone, Copy, Debug, PartialEq, Eq)]
601pub enum BodyContainment {
602 SubjectInsideCounterpart,
603 CounterpartInsideSubject,
604}
605
606#[derive(Clone, Copy, Debug, PartialEq)]
609pub struct VolumeInterval {
610 lower_cubic_metres: f64,
611 upper_cubic_metres: f64,
612}
613
614impl VolumeInterval {
615 pub fn try_new(
617 lower_cubic_metres: f64,
618 upper_cubic_metres: f64,
619 ) -> Result<Self, ProximityError> {
620 let valid = |value: f64| value.is_finite() && value >= 0.0;
621 if !valid(lower_cubic_metres)
622 || !valid(upper_cubic_metres)
623 || lower_cubic_metres > upper_cubic_metres
624 {
625 return Err(ProximityError::InvalidMeasurement);
626 }
627 Ok(Self {
628 lower_cubic_metres,
629 upper_cubic_metres,
630 })
631 }
632 pub fn exact(cubic_metres: f64) -> Result<Self, ProximityError> {
634 Self::try_new(cubic_metres, cubic_metres)
635 }
636 pub fn lower_cubic_metres(&self) -> f64 {
637 self.lower_cubic_metres
638 }
639 pub fn upper_cubic_metres(&self) -> f64 {
640 self.upper_cubic_metres
641 }
642 #[allow(clippy::float_cmp)]
644 pub fn is_exact(&self) -> bool {
645 self.lower_cubic_metres == self.upper_cubic_metres
646 }
647 pub fn share_of(self, whole: Self) -> (f64, f64) {
651 let lower = if whole.upper_cubic_metres > 0.0 {
652 (self.lower_cubic_metres / whole.upper_cubic_metres).next_down()
653 } else {
654 0.0
655 };
656 let upper = if whole.lower_cubic_metres > 0.0 {
657 (self.upper_cubic_metres / whole.lower_cubic_metres).next_up()
658 } else {
659 1.0
660 };
661 let upper = upper.clamp(0.0, 1.0);
662 (lower.clamp(0.0, upper), upper)
663 }
664}
665
666#[derive(Clone, Copy, Debug, PartialEq)]
671pub struct IntersectionVolume {
672 shared: VolumeInterval,
673 subject: VolumeInterval,
674 counterpart: VolumeInterval,
675}
676
677impl IntersectionVolume {
678 pub fn try_new(
681 shared: VolumeInterval,
682 subject: VolumeInterval,
683 counterpart: VolumeInterval,
684 ) -> Result<Self, ProximityError> {
685 let most = subject
686 .upper_cubic_metres
687 .min(counterpart.upper_cubic_metres);
688 if shared.lower_cubic_metres > most {
689 return Err(ProximityError::InvalidMeasurement);
690 }
691 Ok(Self {
692 shared,
693 subject,
694 counterpart,
695 })
696 }
697 pub fn shared(&self) -> VolumeInterval {
699 self.shared
700 }
701 pub fn subject(&self) -> VolumeInterval {
703 self.subject
704 }
705 pub fn counterpart(&self) -> VolumeInterval {
707 self.counterpart
708 }
709 pub fn smaller(&self) -> VolumeInterval {
711 VolumeInterval {
712 lower_cubic_metres: self
713 .subject
714 .lower_cubic_metres
715 .min(self.counterpart.lower_cubic_metres),
716 upper_cubic_metres: self
717 .subject
718 .upper_cubic_metres
719 .min(self.counterpart.upper_cubic_metres),
720 }
721 }
722 pub fn ratio_of_smaller(&self) -> (f64, f64) {
726 self.shared.share_of(self.smaller())
727 }
728}
729
730#[derive(Clone, Debug, PartialEq)]
736pub struct BodyVolume {
737 object: ObjectId,
738 volume: VolumeInterval,
739 fidelity: GeometryFidelity,
740 evidence: Evidence,
741}
742
743impl BodyVolume {
744 pub fn try_new(
747 object: ObjectId,
748 volume: VolumeInterval,
749 fidelity: GeometryFidelity,
750 evidence: Evidence,
751 ) -> Result<Self, ProximityError> {
752 if evidence.exact && !fidelity.is_exact() || evidence.locator.trim().is_empty() {
753 return Err(ProximityError::EvidenceFidelityMismatch);
754 }
755 Ok(Self {
756 object,
757 volume,
758 fidelity,
759 evidence,
760 })
761 }
762 pub fn object(&self) -> &ObjectId {
764 &self.object
765 }
766 pub fn volume(&self) -> VolumeInterval {
768 self.volume
769 }
770 pub fn fidelity(&self) -> GeometryFidelity {
771 self.fidelity
772 }
773 pub fn evidence(&self) -> &Evidence {
774 &self.evidence
775 }
776}
777
778#[derive(Clone, Debug, PartialEq)]
780pub struct ProximityEvidence {
781 request: ProximityRequest,
782 separation_metres: f64,
783 penetration_metres: Option<f64>,
784 plan_overlap_square_metres: f64,
785 containment: Option<BodyContainment>,
786 overlap_extents: Option<OverlapExtents>,
787 hausdorff: Option<LengthInterval>,
788 volume: Option<IntersectionVolume>,
789 fidelity: GeometryFidelity,
790 evidence: Evidence,
791}
792
793impl ProximityEvidence {
794 pub fn try_new(
802 request: ProximityRequest,
803 separation_metres: f64,
804 penetration_metres: Option<f64>,
805 plan_overlap_square_metres: f64,
806 containment: Option<BodyContainment>,
807 fidelity: GeometryFidelity,
808 evidence: Evidence,
809 ) -> Result<Self, ProximityError> {
810 let finite_non_negative = |v: f64| v.is_finite() && v >= 0.0;
811 if !finite_non_negative(separation_metres)
812 || !finite_non_negative(plan_overlap_square_metres)
813 || penetration_metres.is_some_and(|depth| !finite_non_negative(depth))
814 || !finite_non_negative(fidelity.deviation_metres())
815 {
816 return Err(ProximityError::InvalidMeasurement);
817 }
818 let separated = separation_metres > 0.0;
822 if (separated && containment.is_none() && penetration_metres.is_some_and(|d| d > 0.0))
823 || (containment.is_some() && (!separated || penetration_metres.is_none()))
824 {
825 return Err(ProximityError::InvalidMeasurement);
826 }
827 if evidence.exact != fidelity.is_exact() || evidence.locator.trim().is_empty() {
828 return Err(ProximityError::EvidenceFidelityMismatch);
829 }
830 if request.projection() != ProximityProjection::Minimum3d {
832 return Err(ProximityError::UnsupportedProjection);
833 }
834 Ok(Self {
835 request,
836 separation_metres,
837 penetration_metres,
838 plan_overlap_square_metres,
839 containment,
840 overlap_extents: None,
841 hausdorff: None,
842 volume: None,
843 fidelity,
844 evidence,
845 })
846 }
847
848 pub fn with_overlap_extents(mut self, extents: OverlapExtents) -> Result<Self, ProximityError> {
854 let disjoint = self.separation_metres > 0.0 && self.containment.is_none();
855 if self.penetration_metres.is_none() || (disjoint && !extents.is_empty()) {
856 return Err(ProximityError::InvalidMeasurement);
857 }
858 self.overlap_extents = Some(extents);
859 Ok(self)
860 }
861
862 pub fn with_hausdorff(mut self, interval: LengthInterval) -> Result<Self, ProximityError> {
868 if interval.upper_metres() < self.separation_interval_metres().0 {
869 return Err(ProximityError::InvalidMeasurement);
870 }
871 self.hausdorff = Some(interval);
872 Ok(self)
873 }
874
875 pub fn with_intersection_volume(
883 mut self,
884 volume: IntersectionVolume,
885 ) -> Result<Self, ProximityError> {
886 let disjoint = self.separation_metres > 0.0 && self.containment.is_none();
887 let shared = volume.shared();
888 let whole = |inner: VolumeInterval| {
889 shared.upper_cubic_metres >= inner.lower_cubic_metres
890 && shared.lower_cubic_metres <= inner.upper_cubic_metres
891 };
892 let contained = match self.containment {
893 Some(BodyContainment::SubjectInsideCounterpart) => whole(volume.subject()),
894 Some(BodyContainment::CounterpartInsideSubject) => whole(volume.counterpart()),
895 None => true,
896 };
897 if self.penetration_metres.is_none()
898 || (disjoint && shared.lower_cubic_metres > 0.0)
899 || !contained
900 {
901 return Err(ProximityError::InvalidMeasurement);
902 }
903 self.volume = Some(volume);
904 Ok(self)
905 }
906
907 pub fn request(&self) -> &ProximityRequest {
908 &self.request
909 }
910 pub fn separation_metres(&self) -> f64 {
912 self.separation_metres
913 }
914 pub fn separation_interval_metres(&self) -> (f64, f64) {
918 let deviation = self.fidelity.deviation_metres();
919 (
920 (self.separation_metres - deviation).max(0.0),
921 self.separation_metres + deviation,
922 )
923 }
924 pub fn penetration_metres(&self) -> Option<f64> {
926 self.penetration_metres
927 }
928 pub fn plan_overlap_square_metres(&self) -> f64 {
929 self.plan_overlap_square_metres
930 }
931 pub fn containment(&self) -> Option<BodyContainment> {
932 self.containment
933 }
934 pub fn overlap_extents(&self) -> Option<OverlapExtents> {
937 self.overlap_extents
938 }
939 pub fn hausdorff_interval_metres(&self) -> Option<LengthInterval> {
943 self.hausdorff
944 }
945 pub fn intersection_volume(&self) -> Option<IntersectionVolume> {
948 self.volume
949 }
950 pub fn fidelity(&self) -> GeometryFidelity {
951 self.fidelity
952 }
953 pub fn evidence(&self) -> &Evidence {
954 &self.evidence
955 }
956}
957
958#[derive(Clone, Debug, PartialEq)]
967pub struct ProjectedDistanceEvidence {
968 request: ProximityRequest,
969 lower_metres: f64,
970 upper_metres: f64,
971 fidelity: GeometryFidelity,
972 evidence: Evidence,
973}
974
975impl ProjectedDistanceEvidence {
976 pub fn try_new(
979 request: ProximityRequest,
980 lower_metres: f64,
981 upper_metres: f64,
982 fidelity: GeometryFidelity,
983 evidence: Evidence,
984 ) -> Result<Self, ProximityError> {
985 let deviation = fidelity.deviation_metres();
986 let unbounded_allowed = request.projection().may_be_unrelated();
987 let bound_ok = |value: f64| {
988 value >= 0.0 && (value.is_finite() || (unbounded_allowed && value == f64::INFINITY))
989 };
990 if !bound_ok(lower_metres)
991 || !bound_ok(upper_metres)
992 || lower_metres > upper_metres
993 || !deviation.is_finite()
994 || deviation < 0.0
995 || (fidelity.is_exact() && lower_metres < upper_metres)
996 {
997 return Err(ProximityError::InvalidMeasurement);
998 }
999 if evidence.exact != fidelity.is_exact() || evidence.locator.trim().is_empty() {
1000 return Err(ProximityError::EvidenceFidelityMismatch);
1001 }
1002 Ok(Self {
1003 request,
1004 lower_metres,
1005 upper_metres,
1006 fidelity,
1007 evidence,
1008 })
1009 }
1010
1011 pub fn from_proximity(measured: &ProximityEvidence) -> Result<Self, ProximityError> {
1013 let (lower, upper) = measured.separation_interval_metres();
1014 Self::try_new(
1015 measured.request().clone(),
1016 lower,
1017 upper,
1018 measured.fidelity(),
1019 measured.evidence().clone(),
1020 )
1021 }
1022
1023 pub fn request(&self) -> &ProximityRequest {
1024 &self.request
1025 }
1026 pub fn interval_metres(&self) -> (f64, f64) {
1028 (self.lower_metres, self.upper_metres)
1029 }
1030 pub fn fidelity(&self) -> GeometryFidelity {
1031 self.fidelity
1032 }
1033 pub fn evidence(&self) -> &Evidence {
1034 &self.evidence
1035 }
1036}
1037
1038#[derive(Clone, Debug, PartialEq)]
1042pub struct RegionDistanceRequest {
1043 region: ConvexPlanRegion,
1044 counterpart: ObjectId,
1045}
1046
1047impl RegionDistanceRequest {
1048 pub fn new(region: ConvexPlanRegion, counterpart: ObjectId) -> Self {
1049 Self {
1050 region,
1051 counterpart,
1052 }
1053 }
1054 pub fn region(&self) -> &ConvexPlanRegion {
1055 &self.region
1056 }
1057 pub fn counterpart(&self) -> &ObjectId {
1058 &self.counterpart
1059 }
1060}
1061
1062#[derive(Clone, Debug, PartialEq)]
1066pub struct RegionDistanceEvidence {
1067 request: RegionDistanceRequest,
1068 lower_metres: f64,
1069 upper_metres: f64,
1070 fidelity: GeometryFidelity,
1071 evidence: Evidence,
1072}
1073
1074impl RegionDistanceEvidence {
1075 pub fn try_new(
1079 request: RegionDistanceRequest,
1080 lower_metres: f64,
1081 upper_metres: f64,
1082 fidelity: GeometryFidelity,
1083 evidence: Evidence,
1084 ) -> Result<Self, ProximityError> {
1085 let bound_ok = |value: f64| value.is_finite() && value >= 0.0;
1086 if !bound_ok(lower_metres)
1087 || !bound_ok(upper_metres)
1088 || lower_metres > upper_metres
1089 || (fidelity.is_exact() && lower_metres < upper_metres)
1090 {
1091 return Err(ProximityError::InvalidMeasurement);
1092 }
1093 if evidence.exact != fidelity.is_exact()
1094 || evidence.locator.trim().is_empty()
1095 || evidence.source != request.counterpart().source
1096 {
1097 return Err(ProximityError::EvidenceFidelityMismatch);
1098 }
1099 Ok(Self {
1100 request,
1101 lower_metres,
1102 upper_metres,
1103 fidelity,
1104 evidence,
1105 })
1106 }
1107 pub fn request(&self) -> &RegionDistanceRequest {
1108 &self.request
1109 }
1110 pub fn interval_metres(&self) -> (f64, f64) {
1112 (self.lower_metres, self.upper_metres)
1113 }
1114 pub fn fidelity(&self) -> GeometryFidelity {
1115 self.fidelity
1116 }
1117 pub fn evidence(&self) -> &Evidence {
1118 &self.evidence
1119 }
1120}
1121
1122#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1130pub enum FaceClass {
1131 Top,
1132 Side,
1133 Bottom,
1134 Any,
1135}
1136
1137impl FaceClass {
1138 pub fn name(self) -> &'static str {
1140 match self {
1141 Self::Top => "top",
1142 Self::Side => "side",
1143 Self::Bottom => "bottom",
1144 Self::Any => "any",
1145 }
1146 }
1147 pub fn parse(name: &str) -> Option<Self> {
1149 [Self::Top, Self::Side, Self::Bottom, Self::Any]
1150 .into_iter()
1151 .find(|class| class.name() == name)
1152 }
1153}
1154
1155#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
1157pub enum FaceDistanceError {
1158 #[error("face distances are not measured by this service")]
1160 Unsupported,
1161 #[error("face distance is unavailable for the requested object")]
1163 Unavailable,
1164 #[error("the requested object occupies no material")]
1166 NoBody,
1167 #[error("the host body is not a closed solid")]
1170 NotClosed,
1171 #[error("the host body has no face of the requested class")]
1173 NoFaces,
1174 #[error("the host body is tessellated, so its face classes are unknown")]
1177 InexactHost,
1178 #[error("a face of the host lies on the boundary between two face classes")]
1181 AmbiguousFace,
1182 #[error("face distance is not finite and coherent")]
1184 InvalidMeasurement,
1185 #[error("face distance evidence exactness must match geometry fidelity and be reviewable")]
1187 EvidenceFidelityMismatch,
1188 #[error("the face distance of an object to itself is undefined")]
1190 SameObject,
1191}
1192
1193#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1196pub struct FaceDistanceRequest {
1197 body: ObjectId,
1198 host: ObjectId,
1199 faces: FaceClass,
1200}
1201
1202impl FaceDistanceRequest {
1203 pub fn try_new(
1205 body: ObjectId,
1206 host: ObjectId,
1207 faces: FaceClass,
1208 ) -> Result<Self, FaceDistanceError> {
1209 if body == host {
1210 return Err(FaceDistanceError::SameObject);
1211 }
1212 Ok(Self { body, host, faces })
1213 }
1214 pub fn body(&self) -> &ObjectId {
1216 &self.body
1217 }
1218 pub fn host(&self) -> &ObjectId {
1220 &self.host
1221 }
1222 pub fn faces(&self) -> FaceClass {
1223 self.faces
1224 }
1225}
1226
1227#[derive(Clone, Debug, PartialEq)]
1243pub struct FaceDistanceEvidence {
1244 request: FaceDistanceRequest,
1245 signed: SignedDistanceInterval,
1246 fidelity: GeometryFidelity,
1247 evidence: Evidence,
1248}
1249
1250impl FaceDistanceEvidence {
1251 pub fn try_new(
1254 request: FaceDistanceRequest,
1255 signed: SignedDistanceInterval,
1256 fidelity: GeometryFidelity,
1257 evidence: Evidence,
1258 ) -> Result<Self, FaceDistanceError> {
1259 let deviation = fidelity.deviation_metres();
1260 if !deviation.is_finite() || deviation < 0.0 {
1261 return Err(FaceDistanceError::InvalidMeasurement);
1262 }
1263 if evidence.exact != fidelity.is_exact() || evidence.locator.trim().is_empty() {
1264 return Err(FaceDistanceError::EvidenceFidelityMismatch);
1265 }
1266 Ok(Self {
1267 request,
1268 signed,
1269 fidelity,
1270 evidence,
1271 })
1272 }
1273 pub fn request(&self) -> &FaceDistanceRequest {
1274 &self.request
1275 }
1276 pub fn signed(&self) -> SignedDistanceInterval {
1279 self.signed
1280 }
1281 pub fn fidelity(&self) -> GeometryFidelity {
1282 self.fidelity
1283 }
1284 pub fn evidence(&self) -> &Evidence {
1285 &self.evidence
1286 }
1287}
1288
1289pub trait ProximityService: Send + Sync + 'static {
1293 fn bounds(&self, object: &ObjectId) -> Result<ObjectBounds, ProximityError>;
1295 fn measure_proximity(
1300 &self,
1301 request: &ProximityRequest,
1302 ) -> Result<ProximityEvidence, ProximityError>;
1303 fn measure_distance(
1309 &self,
1310 request: &ProximityRequest,
1311 ) -> Result<ProjectedDistanceEvidence, ProximityError> {
1312 match request.projection() {
1313 ProximityProjection::Minimum3d => {
1314 ProjectedDistanceEvidence::from_proximity(&self.measure_proximity(request)?)
1315 }
1316 _ => Err(ProximityError::UnsupportedProjection),
1317 }
1318 }
1319 fn measure_face_distance(
1324 &self,
1325 request: &FaceDistanceRequest,
1326 ) -> Result<FaceDistanceEvidence, FaceDistanceError> {
1327 let _ = request;
1328 Err(FaceDistanceError::Unsupported)
1329 }
1330 fn measure_region_distance(
1335 &self,
1336 request: &RegionDistanceRequest,
1337 ) -> Result<RegionDistanceEvidence, ProximityError> {
1338 let _ = request;
1339 Err(ProximityError::UnsupportedProjection)
1340 }
1341 fn measure_overlap_along(
1346 &self,
1347 request: &OverlapAlongRequest,
1348 ) -> Result<OverlapAlongEvidence, ProximityError> {
1349 let _ = request;
1350 Err(ProximityError::UnsupportedProjection)
1351 }
1352
1353 fn measure_body_volume(&self, object: &ObjectId) -> Result<BodyVolume, ProximityError> {
1358 let _ = object;
1359 Err(ProximityError::Unavailable)
1360 }
1361}
1362
1363#[derive(Clone)]
1365pub struct ProximityServiceHandle(Arc<dyn ProximityService>);
1366
1367impl ProximityServiceHandle {
1368 pub fn new(service: Arc<dyn ProximityService>) -> Self {
1369 Self(service)
1370 }
1371 pub fn bounds(&self, object: &ObjectId) -> Result<ObjectBounds, ProximityError> {
1372 self.0.bounds(object)
1373 }
1374 pub fn measure_proximity(
1375 &self,
1376 request: &ProximityRequest,
1377 ) -> Result<ProximityEvidence, ProximityError> {
1378 self.0.measure_proximity(request)
1379 }
1380 pub fn measure_distance(
1383 &self,
1384 request: &ProximityRequest,
1385 ) -> Result<ProjectedDistanceEvidence, ProximityError> {
1386 let measured = self.0.measure_distance(request)?;
1387 if measured.request() != request {
1388 return Err(ProximityError::InvalidMeasurement);
1389 }
1390 Ok(measured)
1391 }
1392 pub fn measure_face_distance(
1395 &self,
1396 request: &FaceDistanceRequest,
1397 ) -> Result<FaceDistanceEvidence, FaceDistanceError> {
1398 let measured = self.0.measure_face_distance(request)?;
1399 if measured.request() != request {
1400 return Err(FaceDistanceError::InvalidMeasurement);
1401 }
1402 Ok(measured)
1403 }
1404 pub fn measure_overlap_along(
1407 &self,
1408 request: &OverlapAlongRequest,
1409 ) -> Result<OverlapAlongEvidence, ProximityError> {
1410 let measured = self.0.measure_overlap_along(request)?;
1411 if measured.request() != request {
1412 return Err(ProximityError::InvalidMeasurement);
1413 }
1414 Ok(measured)
1415 }
1416 pub fn measure_body_volume(&self, object: &ObjectId) -> Result<BodyVolume, ProximityError> {
1419 let measured = self.0.measure_body_volume(object)?;
1420 if measured.object() != object {
1421 return Err(ProximityError::InvalidMeasurement);
1422 }
1423 Ok(measured)
1424 }
1425 pub fn measure_region_distance(
1428 &self,
1429 request: &RegionDistanceRequest,
1430 ) -> Result<RegionDistanceEvidence, ProximityError> {
1431 let measured = self.0.measure_region_distance(request)?;
1432 if measured.request() != request {
1433 return Err(ProximityError::InvalidMeasurement);
1434 }
1435 Ok(measured)
1436 }
1437}
1438
1439#[cfg(test)]
1440mod tests {
1441 use super::*;
1442 use axioval_ir::SourceId;
1443
1444 fn id(local: &str) -> ObjectId {
1445 ObjectId::new(SourceId::new("cad", "m").unwrap(), local).unwrap()
1446 }
1447 fn request() -> ProximityRequest {
1448 ProximityRequest::try_new(id("pipe"), id("wall")).unwrap()
1449 }
1450
1451 struct OneVolume(BodyVolume);
1453
1454 #[test]
1455 fn a_body_volume_keeps_its_exactness_honest_and_names_its_object() {
1456 let tessellated = GeometryFidelity::Tessellated {
1457 chord_deviation_metres: 0.01,
1458 };
1459 let volume = VolumeInterval::try_new(1.0, 1.1).unwrap();
1460 assert_eq!(
1461 BodyVolume::try_new(id("wall"), volume, tessellated, exact()),
1462 Err(ProximityError::EvidenceFidelityMismatch)
1463 );
1464 let measured = BodyVolume::try_new(id("wall"), volume, tessellated, approximate()).unwrap();
1465 let handle = ProximityServiceHandle::new(Arc::new(OneVolume(measured.clone())));
1466 assert_eq!(handle.measure_body_volume(&id("wall")), Ok(measured));
1467 assert_eq!(
1468 handle.measure_body_volume(&id("pipe")),
1469 Err(ProximityError::InvalidMeasurement)
1470 );
1471 }
1472
1473 impl ProximityService for OneVolume {
1474 fn bounds(&self, _: &ObjectId) -> Result<ObjectBounds, ProximityError> {
1475 Err(ProximityError::Unavailable)
1476 }
1477 fn measure_proximity(
1478 &self,
1479 _: &ProximityRequest,
1480 ) -> Result<ProximityEvidence, ProximityError> {
1481 Err(ProximityError::Unavailable)
1482 }
1483 fn measure_body_volume(&self, _: &ObjectId) -> Result<BodyVolume, ProximityError> {
1484 Ok(self.0.clone())
1485 }
1486 }
1487 fn exact() -> Evidence {
1488 Evidence::exact(SourceId::new("cad", "m").unwrap(), "proximity:pipe:wall")
1489 }
1490 fn approximate() -> Evidence {
1491 Evidence {
1492 exact: false,
1493 ..exact()
1494 }
1495 }
1496
1497 #[test]
1498 fn an_object_is_not_measured_against_itself() {
1499 assert_eq!(
1500 ProximityRequest::try_new(id("pipe"), id("pipe")),
1501 Err(ProximityError::SameObject)
1502 );
1503 }
1504
1505 #[test]
1508 fn evidence_exactness_must_match_fidelity() {
1509 let tessellated = GeometryFidelity::tessellated(0.002).unwrap();
1510 assert_eq!(
1511 ProximityEvidence::try_new(request(), 0.1, Some(0.0), 0.0, None, tessellated, exact()),
1512 Err(ProximityError::EvidenceFidelityMismatch)
1513 );
1514 assert_eq!(
1515 ProximityEvidence::try_new(
1516 request(),
1517 0.1,
1518 Some(0.0),
1519 0.0,
1520 None,
1521 GeometryFidelity::Exact,
1522 approximate()
1523 ),
1524 Err(ProximityError::EvidenceFidelityMismatch)
1525 );
1526 assert!(
1527 ProximityEvidence::try_new(
1528 request(),
1529 0.1,
1530 Some(0.0),
1531 0.0,
1532 None,
1533 tessellated,
1534 approximate()
1535 )
1536 .is_ok()
1537 );
1538 }
1539
1540 #[test]
1541 fn separated_bodies_cannot_penetrate_unless_one_contains_the_other() {
1542 let exact_fidelity = GeometryFidelity::Exact;
1543 assert_eq!(
1544 ProximityEvidence::try_new(
1545 request(),
1546 0.1,
1547 Some(0.05),
1548 0.0,
1549 None,
1550 exact_fidelity,
1551 exact()
1552 ),
1553 Err(ProximityError::InvalidMeasurement)
1554 );
1555 assert!(
1556 ProximityEvidence::try_new(
1557 request(),
1558 0.1,
1559 Some(0.05),
1560 0.0,
1561 Some(BodyContainment::SubjectInsideCounterpart),
1562 exact_fidelity,
1563 exact()
1564 )
1565 .is_ok()
1566 );
1567 assert_eq!(
1569 ProximityEvidence::try_new(
1570 request(),
1571 0.0,
1572 Some(0.05),
1573 0.0,
1574 Some(BodyContainment::SubjectInsideCounterpart),
1575 exact_fidelity,
1576 exact()
1577 ),
1578 Err(ProximityError::InvalidMeasurement)
1579 );
1580 }
1581
1582 #[test]
1583 fn tessellated_separation_widens_by_the_combined_deviation() {
1584 let fidelity = GeometryFidelity::tessellated(0.002)
1585 .unwrap()
1586 .combined(GeometryFidelity::tessellated(0.001).unwrap());
1587 let measured = ProximityEvidence::try_new(
1588 request(),
1589 0.01,
1590 Some(0.0),
1591 0.0,
1592 None,
1593 fidelity,
1594 approximate(),
1595 )
1596 .unwrap();
1597 let (lower, upper) = measured.separation_interval_metres();
1598 assert!((lower - 0.007).abs() < 1e-12 && (upper - 0.013).abs() < 1e-12);
1599 }
1600
1601 #[test]
1602 fn a_vertical_offset_must_be_finite_and_non_negative() {
1603 for offset in [-0.1, f64::NAN, f64::INFINITY] {
1604 assert_eq!(
1605 ProximityRequest::projected(
1606 id("pipe"),
1607 id("wall"),
1608 ProximityProjection::Vertical {
1609 footprint_offset_metres: offset,
1610 direction: VerticalDirection::Either,
1611 surfaces: crate::VerticalSurfaces::Extents,
1612 }
1613 ),
1614 Err(ProximityError::InvalidMeasurement)
1615 );
1616 }
1617 }
1618
1619 #[test]
1620 fn surface_pairs_are_distinct_projections_and_nearest_takes_no_offset() {
1621 use crate::{CounterpartSurface, SubjectSurface, VerticalSurfaces};
1622 let between = |counterpart, offset| ProximityProjection::Vertical {
1623 footprint_offset_metres: offset,
1624 direction: VerticalDirection::Above,
1625 surfaces: VerticalSurfaces::Between {
1626 subject: SubjectSurface::Top,
1627 counterpart,
1628 },
1629 };
1630 let extents = ProximityProjection::Vertical {
1631 footprint_offset_metres: 0.0,
1632 direction: VerticalDirection::Above,
1633 surfaces: VerticalSurfaces::Extents,
1634 };
1635 assert_ne!(between(CounterpartSurface::Nearest, 0.0), extents);
1636 assert_ne!(
1637 between(CounterpartSurface::Nearest, 0.0),
1638 between(CounterpartSurface::Bottom, 0.0)
1639 );
1640 assert!(
1641 ProximityRequest::projected(
1642 id("pipe"),
1643 id("wall"),
1644 between(CounterpartSurface::Bottom, 0.5)
1645 )
1646 .is_ok()
1647 );
1648 assert_eq!(
1649 ProximityRequest::projected(
1650 id("pipe"),
1651 id("wall"),
1652 between(CounterpartSurface::Nearest, 0.5)
1653 ),
1654 Err(ProximityError::InvalidMeasurement)
1655 );
1656 assert_eq!(SubjectSurface::Bottom.name(), "bottom");
1657 assert_eq!(CounterpartSurface::Nearest.name(), "nearest");
1658 }
1659
1660 #[test]
1662 fn a_vertical_direction_distinguishes_requests() {
1663 let vertical = |direction| ProximityProjection::Vertical {
1664 footprint_offset_metres: 0.0,
1665 direction,
1666 surfaces: crate::VerticalSurfaces::Extents,
1667 };
1668 let above = vertical(VerticalDirection::Above);
1669 assert_ne!(above, vertical(VerticalDirection::Below));
1670 assert_ne!(above, vertical(VerticalDirection::Either));
1671 assert!(vertical(VerticalDirection::Either) < above);
1672 }
1673
1674 #[test]
1676 fn full_proximity_evidence_is_only_measured_in_space() {
1677 let horizontal =
1678 ProximityRequest::projected(id("pipe"), id("wall"), ProximityProjection::Horizontal)
1679 .unwrap();
1680 assert_eq!(
1681 ProximityEvidence::try_new(
1682 horizontal,
1683 0.1,
1684 Some(0.0),
1685 0.0,
1686 None,
1687 GeometryFidelity::Exact,
1688 exact()
1689 ),
1690 Err(ProximityError::UnsupportedProjection)
1691 );
1692 }
1693
1694 #[test]
1695 fn projected_distance_intervals_are_coherent() {
1696 let plan =
1697 ProximityRequest::projected(id("pipe"), id("wall"), ProximityProjection::PlanOverlap)
1698 .unwrap();
1699 let tessellated = GeometryFidelity::tessellated(0.002).unwrap();
1700 assert!(
1702 ProjectedDistanceEvidence::try_new(
1703 plan.clone(),
1704 f64::INFINITY,
1705 f64::INFINITY,
1706 GeometryFidelity::Exact,
1707 exact()
1708 )
1709 .is_ok()
1710 );
1711 assert_eq!(
1712 ProjectedDistanceEvidence::try_new(
1713 plan.clone(),
1714 0.0,
1715 f64::INFINITY,
1716 GeometryFidelity::Exact,
1717 exact()
1718 ),
1719 Err(ProximityError::InvalidMeasurement)
1720 );
1721 assert!(
1723 ProjectedDistanceEvidence::try_new(
1724 plan.clone(),
1725 0.0,
1726 f64::INFINITY,
1727 tessellated,
1728 approximate()
1729 )
1730 .is_ok()
1731 );
1732 assert_eq!(
1733 ProjectedDistanceEvidence::try_new(plan, 0.0, 0.0, tessellated, exact()),
1734 Err(ProximityError::EvidenceFidelityMismatch)
1735 );
1736 assert_eq!(
1738 ProjectedDistanceEvidence::try_new(
1739 request(),
1740 0.0,
1741 f64::INFINITY,
1742 tessellated,
1743 approximate()
1744 ),
1745 Err(ProximityError::InvalidMeasurement)
1746 );
1747 assert_eq!(
1748 ProjectedDistanceEvidence::try_new(request(), 0.3, 0.2, tessellated, approximate()),
1749 Err(ProximityError::InvalidMeasurement)
1750 );
1751 }
1752
1753 #[test]
1755 fn horizontal_extent_is_the_narrower_plan_axis() {
1756 let extents = OverlapExtents::new(
1757 LengthInterval::try_new(0.1, 0.3).unwrap(),
1758 LengthInterval::try_new(0.2, 0.25).unwrap(),
1759 LengthInterval::exact(1.0).unwrap(),
1760 );
1761 assert_eq!(
1762 extents.horizontal(),
1763 LengthInterval::try_new(0.1, 0.25).unwrap()
1764 );
1765 assert_eq!(extents.vertical(), LengthInterval::exact(1.0).unwrap());
1766 }
1767
1768 #[test]
1769 fn overlap_extents_need_a_shared_volume() {
1770 let extents = OverlapExtents::new(
1771 LengthInterval::exact(0.1).unwrap(),
1772 LengthInterval::exact(0.1).unwrap(),
1773 LengthInterval::exact(0.1).unwrap(),
1774 );
1775 let measured = |separation: f64, penetration: Option<f64>| {
1776 ProximityEvidence::try_new(
1777 request(),
1778 separation,
1779 penetration,
1780 0.0,
1781 None,
1782 GeometryFidelity::Exact,
1783 exact(),
1784 )
1785 .unwrap()
1786 };
1787 assert!(
1788 measured(0.0, Some(0.1))
1789 .with_overlap_extents(extents)
1790 .is_ok()
1791 );
1792 assert_eq!(
1794 measured(0.0, None).with_overlap_extents(extents),
1795 Err(ProximityError::InvalidMeasurement)
1796 );
1797 assert_eq!(
1799 measured(0.2, Some(0.0)).with_overlap_extents(extents),
1800 Err(ProximityError::InvalidMeasurement)
1801 );
1802 }
1803
1804 #[test]
1806 fn hausdorff_distance_is_never_below_the_separation() {
1807 let measured = ProximityEvidence::try_new(
1808 request(),
1809 0.2,
1810 Some(0.0),
1811 0.0,
1812 None,
1813 GeometryFidelity::Exact,
1814 exact(),
1815 )
1816 .unwrap();
1817 assert_eq!(
1818 measured
1819 .clone()
1820 .with_hausdorff(LengthInterval::try_new(0.0, 0.1).unwrap()),
1821 Err(ProximityError::InvalidMeasurement)
1822 );
1823 assert!(
1824 measured
1825 .with_hausdorff(LengthInterval::try_new(0.2, 0.5).unwrap())
1826 .is_ok()
1827 );
1828 }
1829
1830 fn volume(lower: f64, upper: f64) -> VolumeInterval {
1831 VolumeInterval::try_new(lower, upper).unwrap()
1832 }
1833
1834 #[test]
1835 fn volume_intervals_are_finite_non_negative_and_ordered() {
1836 for (lower, upper) in [
1837 (-0.1, 0.1),
1838 (0.2, 0.1),
1839 (0.0, f64::INFINITY),
1840 (f64::NAN, 1.0),
1841 ] {
1842 assert_eq!(
1843 VolumeInterval::try_new(lower, upper),
1844 Err(ProximityError::InvalidMeasurement)
1845 );
1846 }
1847 assert!(VolumeInterval::exact(0.0).unwrap().is_exact());
1848 }
1849
1850 #[test]
1852 fn a_shared_volume_never_exceeds_either_body() {
1853 assert_eq!(
1854 IntersectionVolume::try_new(volume(0.5, 0.6), volume(1.0, 1.0), volume(0.4, 0.4)),
1855 Err(ProximityError::InvalidMeasurement)
1856 );
1857 let measured =
1858 IntersectionVolume::try_new(volume(0.3, 0.4), volume(1.0, 1.0), volume(0.4, 0.5))
1859 .unwrap();
1860 assert_eq!(measured.smaller(), volume(0.4, 0.5));
1861 let (lower, upper) = measured.ratio_of_smaller();
1862 assert!(lower <= 0.6 && lower > 0.599, "{lower}");
1863 assert!((upper - 1.0).abs() < f64::EPSILON, "{upper}");
1864 }
1865
1866 #[test]
1868 fn a_volume_share_is_rounded_outward_and_clamped() {
1869 let third = volume(1.0 / 3.0, 1.0 / 3.0);
1870 let (lower, upper) = third.share_of(volume(1.0, 1.0));
1871 assert!(lower < 1.0 / 3.0 && upper > 1.0 / 3.0);
1872 assert_eq!(volume(0.0, 0.0).share_of(volume(0.0, 0.0)), (0.0, 1.0));
1873 assert!((volume(1.0, 1.0).share_of(volume(1.0, 1.0)).1 - 1.0).abs() < f64::EPSILON);
1874 }
1875
1876 #[test]
1877 fn an_intersection_volume_needs_a_shared_inside() {
1878 let measured = |separation: f64, penetration: Option<f64>, containment| {
1879 ProximityEvidence::try_new(
1880 request(),
1881 separation,
1882 penetration,
1883 0.0,
1884 containment,
1885 GeometryFidelity::Exact,
1886 exact(),
1887 )
1888 .unwrap()
1889 };
1890 let shared = |lower: f64, upper: f64| {
1891 IntersectionVolume::try_new(volume(lower, upper), volume(1.0, 1.0), volume(8.0, 8.0))
1892 .unwrap()
1893 };
1894 assert!(
1895 measured(0.0, Some(0.1), None)
1896 .with_intersection_volume(shared(0.2, 0.2))
1897 .is_ok()
1898 );
1899 assert_eq!(
1901 measured(0.0, None, None).with_intersection_volume(shared(0.0, 0.0)),
1902 Err(ProximityError::InvalidMeasurement)
1903 );
1904 assert_eq!(
1907 measured(0.2, Some(0.0), None).with_intersection_volume(shared(0.1, 0.2)),
1908 Err(ProximityError::InvalidMeasurement)
1909 );
1910 assert!(
1911 measured(0.2, Some(0.0), None)
1912 .with_intersection_volume(shared(0.0, 0.0))
1913 .is_ok()
1914 );
1915 let inside = || {
1917 measured(
1918 0.2,
1919 Some(0.1),
1920 Some(BodyContainment::SubjectInsideCounterpart),
1921 )
1922 };
1923 assert!(inside().with_intersection_volume(shared(1.0, 1.0)).is_ok());
1924 assert_eq!(
1925 inside().with_intersection_volume(shared(0.5, 0.6)),
1926 Err(ProximityError::InvalidMeasurement)
1927 );
1928 }
1929
1930 #[test]
1931 fn a_face_distance_is_between_two_objects() {
1932 assert_eq!(
1933 FaceDistanceRequest::try_new(id("pipe"), id("pipe"), FaceClass::Top),
1934 Err(FaceDistanceError::SameObject)
1935 );
1936 for class in [
1937 FaceClass::Top,
1938 FaceClass::Side,
1939 FaceClass::Bottom,
1940 FaceClass::Any,
1941 ] {
1942 assert_eq!(FaceClass::parse(class.name()), Some(class));
1943 }
1944 assert_eq!(FaceClass::parse("front"), None);
1945 let request =
1946 FaceDistanceRequest::try_new(id("pipe"), id("wall"), FaceClass::Side).unwrap();
1947 let signed = SignedDistanceInterval::try_new(-0.1, 0.2).unwrap();
1948 assert_eq!(
1949 FaceDistanceEvidence::try_new(
1950 request.clone(),
1951 signed,
1952 GeometryFidelity::tessellated(0.001).unwrap(),
1953 exact()
1954 ),
1955 Err(FaceDistanceError::EvidenceFidelityMismatch)
1956 );
1957 assert!(
1958 FaceDistanceEvidence::try_new(request, signed, GeometryFidelity::Exact, exact())
1959 .is_ok()
1960 );
1961 }
1962
1963 #[test]
1965 fn face_distances_are_refused_by_default() {
1966 struct Silent;
1967 impl ProximityService for Silent {
1968 fn bounds(&self, _: &ObjectId) -> Result<ObjectBounds, ProximityError> {
1969 Err(ProximityError::Unavailable)
1970 }
1971 fn measure_proximity(
1972 &self,
1973 _: &ProximityRequest,
1974 ) -> Result<ProximityEvidence, ProximityError> {
1975 Err(ProximityError::Unavailable)
1976 }
1977 }
1978 let request = FaceDistanceRequest::try_new(id("pipe"), id("wall"), FaceClass::Any).unwrap();
1979 assert_eq!(
1980 ProximityServiceHandle::new(Arc::new(Silent)).measure_face_distance(&request),
1981 Err(FaceDistanceError::Unsupported)
1982 );
1983 }
1984
1985 #[test]
1986 fn box_gap_is_euclidean_and_zero_when_boxes_meet() {
1987 let a = Bounds3::try_new([0.0; 3], [1.0; 3]).unwrap();
1988 let b = Bounds3::try_new([4.0, 5.0, 0.0], [5.0, 6.0, 1.0]).unwrap();
1989 assert!((a.gap(&b) - 5.0).abs() < 1e-12);
1990 let touching = Bounds3::try_new([1.0, 0.0, 0.0], [2.0, 1.0, 1.0]).unwrap();
1991 assert!(a.gap(&touching).abs() < f64::EPSILON);
1992 assert_eq!(
1993 Bounds3::try_new([1.0, 0.0, 0.0], [0.0, 1.0, 1.0]),
1994 Err(ProximityError::InvalidMeasurement)
1995 );
1996 }
1997
1998 #[test]
1999 fn region_distances_bind_their_request_and_default_to_refusal() {
2000 struct Nothing;
2001 impl ProximityService for Nothing {
2002 fn bounds(&self, _: &ObjectId) -> Result<ObjectBounds, ProximityError> {
2003 Err(ProximityError::Unavailable)
2004 }
2005 fn measure_proximity(
2006 &self,
2007 _: &ProximityRequest,
2008 ) -> Result<ProximityEvidence, ProximityError> {
2009 Err(ProximityError::Unavailable)
2010 }
2011 }
2012 let region = ConvexPlanRegion::try_new(vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]).unwrap();
2013 let request = RegionDistanceRequest::new(region, id("wall"));
2014 let evidence = |exact: bool| Evidence {
2015 source: SourceId::new("cad", "m").unwrap(),
2016 locator: "region".into(),
2017 exact,
2018 };
2019 assert!(
2020 RegionDistanceEvidence::try_new(
2021 request.clone(),
2022 0.5,
2023 0.5,
2024 GeometryFidelity::Exact,
2025 evidence(true)
2026 )
2027 .is_ok()
2028 );
2029 assert_eq!(
2031 RegionDistanceEvidence::try_new(
2032 request.clone(),
2033 0.4,
2034 0.5,
2035 GeometryFidelity::Exact,
2036 evidence(true)
2037 ),
2038 Err(ProximityError::InvalidMeasurement)
2039 );
2040 assert_eq!(
2041 RegionDistanceEvidence::try_new(
2042 request.clone(),
2043 0.4,
2044 0.5,
2045 GeometryFidelity::tessellated(0.05).unwrap(),
2046 evidence(true)
2047 ),
2048 Err(ProximityError::EvidenceFidelityMismatch)
2049 );
2050 assert_eq!(
2051 ProximityServiceHandle::new(Arc::new(Nothing)).measure_region_distance(&request),
2052 Err(ProximityError::UnsupportedProjection)
2053 );
2054 }
2055
2056 #[test]
2059 fn extents_along_directions_bind_their_request_and_default_to_refusal() {
2060 struct Nothing;
2061 impl ProximityService for Nothing {
2062 fn bounds(&self, _: &ObjectId) -> Result<ObjectBounds, ProximityError> {
2063 Err(ProximityError::Unavailable)
2064 }
2065 fn measure_proximity(
2066 &self,
2067 _: &ProximityRequest,
2068 ) -> Result<ProximityEvidence, ProximityError> {
2069 Err(ProximityError::Unavailable)
2070 }
2071 }
2072 struct Other(OverlapAlongEvidence);
2074 impl ProximityService for Other {
2075 fn bounds(&self, _: &ObjectId) -> Result<ObjectBounds, ProximityError> {
2076 Err(ProximityError::Unavailable)
2077 }
2078 fn measure_proximity(
2079 &self,
2080 _: &ProximityRequest,
2081 ) -> Result<ProximityEvidence, ProximityError> {
2082 Err(ProximityError::Unavailable)
2083 }
2084 fn measure_overlap_along(
2085 &self,
2086 _: &OverlapAlongRequest,
2087 ) -> Result<OverlapAlongEvidence, ProximityError> {
2088 Ok(self.0.clone())
2089 }
2090 }
2091 let axis = |vector| MetricDirection::try_new(vector).unwrap();
2092 let along = |directions: Vec<MetricDirection>| {
2093 OverlapAlongRequest::try_new(id("pipe"), id("wall"), directions)
2094 };
2095 assert_eq!(along(Vec::new()), Err(ProximityError::InvalidMeasurement));
2096 assert_eq!(
2097 along(vec![axis([1.0, 0.0, 0.0]); 7]),
2098 Err(ProximityError::InvalidMeasurement)
2099 );
2100 assert_eq!(
2101 OverlapAlongRequest::try_new(id("pipe"), id("pipe"), vec![axis([0.0, 0.0, 1.0])]),
2102 Err(ProximityError::SameObject)
2103 );
2104 let request = along(vec![axis([1.0, 1.0, 0.0]), axis([0.0, 0.0, 1.0])]).unwrap();
2105 let extent = LengthInterval::try_new(0.01, 0.02).unwrap();
2106 assert_eq!(
2107 OverlapAlongEvidence::try_new(
2108 request.clone(),
2109 vec![extent],
2110 GeometryFidelity::Exact,
2111 exact()
2112 ),
2113 Err(ProximityError::InvalidMeasurement)
2114 );
2115 assert_eq!(
2116 OverlapAlongEvidence::try_new(
2117 request.clone(),
2118 vec![extent, extent],
2119 GeometryFidelity::tessellated(0.001).unwrap(),
2120 exact()
2121 ),
2122 Err(ProximityError::EvidenceFidelityMismatch)
2123 );
2124 let measured = OverlapAlongEvidence::try_new(
2125 request.clone(),
2126 vec![extent, extent],
2127 GeometryFidelity::Exact,
2128 exact(),
2129 )
2130 .unwrap();
2131 assert_eq!(measured.extents(), &[extent, extent]);
2132
2133 assert_eq!(
2134 ProximityServiceHandle::new(Arc::new(Nothing)).measure_overlap_along(&request),
2135 Err(ProximityError::UnsupportedProjection)
2136 );
2137
2138 let other = along(vec![axis([0.0, 1.0, 0.0]), axis([0.0, 0.0, 1.0])]).unwrap();
2139 assert_eq!(
2140 ProximityServiceHandle::new(Arc::new(Other(measured))).measure_overlap_along(&other),
2141 Err(ProximityError::InvalidMeasurement)
2142 );
2143 }
2144}