1use crate::circulation::{CirculationMap, CirculationRequest};
7use crate::door_leaves::{SweptDoor, tidy_swept};
8use crate::services::reviewable_exact_evidence;
9use crate::{MetricPoint, MobilityProfile, ThresholdVerdict};
10use axioval_ir::{Evidence, ObjectId};
11use std::sync::Arc;
12use thiserror::Error;
13
14#[derive(Clone, Debug, Error, PartialEq, Eq)]
15pub enum FreeSpaceError {
16 #[error("free-area interval is invalid")]
17 InvalidAreaInterval,
18 #[error("metric direction is zero or non-finite")]
19 InvalidMetricDirection,
20 #[error("metric frame axes are not mutually perpendicular")]
21 InvalidMetricFrame,
22 #[error("clearance shape dimensions must be positive and finite")]
23 InvalidClearanceShape,
24 #[error("placement offset interval is non-finite or reversed")]
25 InvalidOffsetInterval,
26 #[error("placement support gap must be finite and non-negative")]
27 InvalidSupportGap,
28 #[error("clearance evidence is incomplete")]
29 IncompleteClearanceEvidence,
30 #[error("obstruction evidence has no blocking objects")]
31 EmptyObstructionEvidence,
32 #[error("obstruction evidence names an object outside the request candidate set")]
33 UnexpectedObstacleEvidence,
34 #[error("obstruction provenance is not exact and reviewable")]
35 InexactObstructionEvidence,
36 #[error("free-area evidence is not exact and reviewable")]
37 InexactAreaEvidence,
38 #[error("placement evidence is not exact and reviewable")]
39 InexactPlacementEvidence,
40 #[error("support evidence is not exact and reviewable")]
41 InexactSupportEvidence,
42 #[error("supported placement has no complete support evidence")]
43 MissingSupportEvidence,
44 #[error("support evidence does not match the requested support or found frame")]
45 SupportEvidenceMismatch,
46 #[error("placement frame is not grounded in the requested scope")]
47 PlacementScopeMismatch,
48 #[error("placement witness falls outside its requested search domain")]
49 PlacementDomainMismatch,
50 #[error("placement witness does not follow the requested orientation")]
51 PlacementOrientationMismatch,
52 #[error("frame-offset placement of a box needs a fixed orientation along the anchor axes")]
53 OrientationDomainConflict,
54 #[error("placement elevation band must be finite, start at or above the floor and be ordered")]
55 InvalidElevationBand,
56 #[error("a merged scope is the search scope itself or one of its obstacles")]
57 MergedScopeConflict,
58 #[error("a swept door is given twice with different sectors")]
60 ConflictingSweptDoors,
61 #[error("free-space backend returned evidence for another request")]
62 ResponseRequestMismatch,
63 #[error("free-space geometry is unavailable for `{0}`")]
64 MissingGeometry(Box<ObjectId>),
65 #[error("free-space query unavailable: {0}")]
66 Unavailable(String),
67}
68
69#[derive(Clone, Copy, Debug, PartialEq)]
70pub struct AreaInterval {
71 lower_square_metres: f64,
72 upper_square_metres: f64,
73}
74impl AreaInterval {
75 pub fn try_new(lower: f64, upper: f64) -> Result<Self, FreeSpaceError> {
76 if !valid_non_negative(lower) || !valid_non_negative(upper) || lower > upper {
77 return Err(FreeSpaceError::InvalidAreaInterval);
78 }
79 Ok(Self {
80 lower_square_metres: lower,
81 upper_square_metres: upper,
82 })
83 }
84 pub fn exact(square_metres: f64) -> Result<Self, FreeSpaceError> {
85 Self::try_new(square_metres, square_metres)
86 }
87 pub fn lower_square_metres(&self) -> f64 {
88 self.lower_square_metres
89 }
90 pub fn upper_square_metres(&self) -> f64 {
91 self.upper_square_metres
92 }
93 pub fn compare_minimum(&self, minimum: f64) -> Result<ThresholdVerdict, FreeSpaceError> {
94 if !valid_non_negative(minimum) {
95 return Err(FreeSpaceError::InvalidAreaInterval);
96 }
97 if self.lower_square_metres >= minimum {
98 Ok(ThresholdVerdict::Satisfied)
99 } else if self.upper_square_metres < minimum {
100 Ok(ThresholdVerdict::Violated)
101 } else {
102 Ok(ThresholdVerdict::Indeterminate)
103 }
104 }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq)]
108pub struct MetricDirection([f64; 3]);
109impl MetricDirection {
110 pub fn try_new(vector: [f64; 3]) -> Result<Self, FreeSpaceError> {
111 if !vector.iter().all(|v| v.is_finite()) {
112 return Err(FreeSpaceError::InvalidMetricDirection);
113 }
114 let norm = vector.iter().map(|v| v * v).sum::<f64>().sqrt();
115 if norm <= f64::EPSILON {
116 return Err(FreeSpaceError::InvalidMetricDirection);
117 }
118 Ok(Self([vector[0] / norm, vector[1] / norm, vector[2] / norm]))
119 }
120 pub fn components(&self) -> [f64; 3] {
121 self.0
122 }
123 fn dot(self, other: Self) -> f64 {
124 self.0[0] * other.0[0] + self.0[1] * other.0[1] + self.0[2] * other.0[2]
125 }
126}
127
128#[derive(Clone, Debug, PartialEq)]
129pub struct MetricFrame {
130 origin: MetricPoint,
131 right: MetricDirection,
132 forward: MetricDirection,
133 up: MetricDirection,
134}
135impl MetricFrame {
136 pub fn try_new(
137 origin: MetricPoint,
138 right: MetricDirection,
139 forward: MetricDirection,
140 up: MetricDirection,
141 ) -> Result<Self, FreeSpaceError> {
142 const ORTHOGONAL_TOLERANCE: f64 = 1.0e-9;
143 let [rx, ry, rz] = right.components();
144 let [fx, fy, fz] = forward.components();
145 let [ux, uy, uz] = up.components();
146 let handedness =
147 (ry * fz - rz * fy) * ux + (rz * fx - rx * fz) * uy + (rx * fy - ry * fx) * uz;
148 if right.dot(forward).abs() > ORTHOGONAL_TOLERANCE
149 || right.dot(up).abs() > ORTHOGONAL_TOLERANCE
150 || forward.dot(up).abs() > ORTHOGONAL_TOLERANCE
151 || handedness < 1.0 - ORTHOGONAL_TOLERANCE
152 {
153 return Err(FreeSpaceError::InvalidMetricFrame);
154 }
155 Ok(Self {
156 origin,
157 right,
158 forward,
159 up,
160 })
161 }
162 pub fn origin(&self) -> &MetricPoint {
163 &self.origin
164 }
165 pub fn right(&self) -> MetricDirection {
166 self.right
167 }
168 pub fn forward(&self) -> MetricDirection {
169 self.forward
170 }
171 pub fn up(&self) -> MetricDirection {
172 self.up
173 }
174}
175
176#[derive(Clone, Copy, Debug, PartialEq)]
177pub struct BoxClearance {
178 width: f64,
179 depth: f64,
180 height: f64,
181}
182impl BoxClearance {
183 pub fn try_new(width: f64, depth: f64, height: f64) -> Result<Self, FreeSpaceError> {
184 if !valid_positive(width) || !valid_positive(depth) || !valid_positive(height) {
185 return Err(FreeSpaceError::InvalidClearanceShape);
186 }
187 Ok(Self {
188 width,
189 depth,
190 height,
191 })
192 }
193 pub fn width_metres(&self) -> f64 {
194 self.width
195 }
196 pub fn depth_metres(&self) -> f64 {
197 self.depth
198 }
199 pub fn height_metres(&self) -> f64 {
200 self.height
201 }
202}
203
204#[derive(Clone, Copy, Debug, PartialEq)]
205pub struct CylinderClearance {
206 radius: f64,
207 height: f64,
208}
209impl CylinderClearance {
210 pub fn try_new(radius: f64, height: f64) -> Result<Self, FreeSpaceError> {
211 if !valid_positive(radius) || !valid_positive(height) {
212 return Err(FreeSpaceError::InvalidClearanceShape);
213 }
214 Ok(Self { radius, height })
215 }
216 pub fn radius_metres(&self) -> f64 {
217 self.radius
218 }
219 pub fn height_metres(&self) -> f64 {
220 self.height
221 }
222}
223
224#[derive(Clone, Copy, Debug, PartialEq)]
225pub enum ClearanceShape {
226 Box(BoxClearance),
227 Cylinder(CylinderClearance),
228}
229
230#[derive(Clone, Debug, PartialEq)]
231pub struct ClearanceRequest {
232 frame: MetricFrame,
233 shape: ClearanceShape,
234 obstacles: Vec<ObjectId>,
235}
236impl ClearanceRequest {
237 pub fn new(frame: MetricFrame, shape: ClearanceShape, mut obstacles: Vec<ObjectId>) -> Self {
238 obstacles.sort();
239 obstacles.dedup();
240 Self {
241 frame,
242 shape,
243 obstacles,
244 }
245 }
246 pub fn frame(&self) -> &MetricFrame {
247 &self.frame
248 }
249 pub fn shape(&self) -> ClearanceShape {
250 self.shape
251 }
252 pub fn obstacles(&self) -> &[ObjectId] {
253 &self.obstacles
254 }
255}
256
257#[derive(Clone, Debug, PartialEq)]
258pub struct FreeAreaRequest {
259 scope: ObjectId,
260 mobility: MobilityProfile,
261 obstacles: Vec<ObjectId>,
262}
263impl FreeAreaRequest {
264 pub fn new(scope: ObjectId, mobility: MobilityProfile, mut obstacles: Vec<ObjectId>) -> Self {
265 obstacles.sort();
266 obstacles.dedup();
267 Self {
268 scope,
269 mobility,
270 obstacles,
271 }
272 }
273 pub fn scope(&self) -> &ObjectId {
274 &self.scope
275 }
276 pub fn mobility(&self) -> MobilityProfile {
277 self.mobility
278 }
279 pub fn obstacles(&self) -> &[ObjectId] {
280 &self.obstacles
281 }
282}
283
284#[derive(Clone, Copy, Debug, PartialEq)]
286pub struct SignedDistanceInterval {
287 lower_metres: f64,
288 upper_metres: f64,
289}
290impl SignedDistanceInterval {
291 pub fn try_new(lower_metres: f64, upper_metres: f64) -> Result<Self, FreeSpaceError> {
292 if !lower_metres.is_finite() || !upper_metres.is_finite() || lower_metres > upper_metres {
293 return Err(FreeSpaceError::InvalidOffsetInterval);
294 }
295 Ok(Self {
296 lower_metres,
297 upper_metres,
298 })
299 }
300 pub fn exact(metres: f64) -> Result<Self, FreeSpaceError> {
301 Self::try_new(metres, metres)
302 }
303 pub fn lower_metres(&self) -> f64 {
304 self.lower_metres
305 }
306 pub fn upper_metres(&self) -> f64 {
307 self.upper_metres
308 }
309 fn contains(self, value: f64) -> bool {
310 value >= self.lower_metres && value <= self.upper_metres
311 }
312}
313
314#[derive(Clone, Debug, PartialEq)]
316pub struct SupportedPlacement {
317 support: ObjectId,
318 maximum_gap_metres: f64,
319}
320impl SupportedPlacement {
321 pub fn try_new(support: ObjectId, maximum_gap_metres: f64) -> Result<Self, FreeSpaceError> {
322 if !valid_non_negative(maximum_gap_metres) {
323 return Err(FreeSpaceError::InvalidSupportGap);
324 }
325 Ok(Self {
326 support,
327 maximum_gap_metres,
328 })
329 }
330 pub fn support(&self) -> &ObjectId {
331 &self.support
332 }
333 pub fn maximum_gap_metres(&self) -> f64 {
334 self.maximum_gap_metres
335 }
336}
337
338#[derive(Clone, Debug, PartialEq)]
340pub struct FrameOffsetPlacement {
341 anchor: MetricFrame,
342 right: SignedDistanceInterval,
343 forward: SignedDistanceInterval,
344 up: SignedDistanceInterval,
345}
346impl FrameOffsetPlacement {
347 pub fn new(
348 anchor: MetricFrame,
349 right: SignedDistanceInterval,
350 forward: SignedDistanceInterval,
351 up: SignedDistanceInterval,
352 ) -> Self {
353 Self {
354 anchor,
355 right,
356 forward,
357 up,
358 }
359 }
360 pub fn anchor(&self) -> &MetricFrame {
361 &self.anchor
362 }
363 pub fn right(&self) -> SignedDistanceInterval {
364 self.right
365 }
366 pub fn forward(&self) -> SignedDistanceInterval {
367 self.forward
368 }
369 pub fn up(&self) -> SignedDistanceInterval {
370 self.up
371 }
372 pub fn contains_frame(&self, frame: &MetricFrame) -> bool {
376 let aligned = self.anchor.right() == frame.right()
377 && self.anchor.forward() == frame.forward()
378 && self.anchor.up() == frame.up();
379 if !aligned {
380 return false;
381 }
382 let anchor = self.anchor.origin().coordinates_metres();
383 let found = frame.origin().coordinates_metres();
384 let delta = [
385 found[0] - anchor[0],
386 found[1] - anchor[1],
387 found[2] - anchor[2],
388 ];
389 let project = |axis: MetricDirection| {
390 axis.components()
391 .into_iter()
392 .zip(delta)
393 .map(|(a, b)| a * b)
394 .sum()
395 };
396 self.right.contains(project(self.anchor.right()))
397 && self.forward.contains(project(self.anchor.forward()))
398 && self.up.contains(project(self.anchor.up()))
399 }
400}
401
402#[derive(Clone, Debug, PartialEq)]
404pub enum PlacementDomain {
405 Unconstrained,
406 Supported(SupportedPlacement),
407 FrameOffsets(FrameOffsetPlacement),
408 SupportedFrameOffsets {
409 support: SupportedPlacement,
410 offsets: FrameOffsetPlacement,
411 },
412}
413
414fn requested_support(domain: &PlacementDomain) -> Option<&SupportedPlacement> {
415 match domain {
416 PlacementDomain::Supported(support)
417 | PlacementDomain::SupportedFrameOffsets { support, .. } => Some(support),
418 _ => None,
419 }
420}
421
422#[derive(Clone, Debug, PartialEq)]
428pub enum PlacementOrientation {
429 Fixed(MetricFrame),
432 Any,
434}
435
436#[derive(Clone, Copy, Debug, PartialEq)]
442pub struct ElevationBand {
443 from_metres: f64,
444 to_metres: f64,
445}
446impl ElevationBand {
447 pub fn try_new(from_metres: f64, to_metres: f64) -> Result<Self, FreeSpaceError> {
448 if !from_metres.is_finite()
449 || !to_metres.is_finite()
450 || from_metres < 0.0
451 || from_metres >= to_metres
452 {
453 return Err(FreeSpaceError::InvalidElevationBand);
454 }
455 Ok(Self {
456 from_metres,
457 to_metres,
458 })
459 }
460 pub fn from_metres(&self) -> f64 {
462 self.from_metres
463 }
464 pub fn to_metres(&self) -> f64 {
466 self.to_metres
467 }
468}
469
470#[derive(Clone, Debug, PartialEq)]
475pub enum PlacementShape {
476 Box {
477 shape: BoxClearance,
478 orientation: PlacementOrientation,
479 },
480 Cylinder(CylinderClearance),
481}
482impl PlacementShape {
483 pub fn clearance(&self) -> ClearanceShape {
484 match self {
485 Self::Box { shape, .. } => ClearanceShape::Box(*shape),
486 Self::Cylinder(shape) => ClearanceShape::Cylinder(*shape),
487 }
488 }
489 pub fn orientation(&self) -> Option<&PlacementOrientation> {
490 match self {
491 Self::Box { orientation, .. } => Some(orientation),
492 Self::Cylinder(_) => None,
493 }
494 }
495}
496
497fn same_axes(a: &MetricFrame, b: &MetricFrame) -> bool {
498 a.right() == b.right() && a.forward() == b.forward() && a.up() == b.up()
499}
500
501#[derive(Clone, Debug, PartialEq)]
511pub struct EntranceReach {
512 entrances: Vec<ObjectId>,
513 width_metres: f64,
514 tolerance_metres: f64,
515}
516
517impl EntranceReach {
518 pub fn try_new(
527 mut entrances: Vec<ObjectId>,
528 width_metres: f64,
529 tolerance_metres: f64,
530 ) -> Result<Self, FreeSpaceError> {
531 if !(width_metres.is_finite()
532 && width_metres > 0.0
533 && tolerance_metres.is_finite()
534 && tolerance_metres >= 0.0)
535 {
536 return Err(FreeSpaceError::InvalidClearanceShape);
537 }
538 entrances.sort();
539 entrances.dedup();
540 Ok(Self {
541 entrances,
542 width_metres,
543 tolerance_metres,
544 })
545 }
546
547 #[must_use]
549 pub fn entrances(&self) -> &[ObjectId] {
550 &self.entrances
551 }
552
553 #[must_use]
555 pub fn width_metres(&self) -> f64 {
556 self.width_metres
557 }
558
559 #[must_use]
562 pub fn tolerance_metres(&self) -> f64 {
563 self.tolerance_metres
564 }
565}
566
567#[derive(Clone, Debug, PartialEq)]
569pub struct PlacementRequest {
570 scope: ObjectId,
571 shape: PlacementShape,
572 obstacles: Vec<ObjectId>,
573 domain: PlacementDomain,
574 band: Option<ElevationBand>,
575 merged: Vec<ObjectId>,
576 swept: Vec<SweptDoor>,
577 reach: Option<EntranceReach>,
578}
579impl PlacementRequest {
580 pub fn new(scope: ObjectId, shape: PlacementShape, mut obstacles: Vec<ObjectId>) -> Self {
581 obstacles.sort();
582 obstacles.dedup();
583 Self {
584 scope,
585 shape,
586 obstacles,
587 domain: PlacementDomain::Unconstrained,
588 band: None,
589 merged: Vec::new(),
590 swept: Vec::new(),
591 reach: None,
592 }
593 }
594 pub fn new_in_domain(
595 scope: ObjectId,
596 shape: PlacementShape,
597 mut obstacles: Vec<ObjectId>,
598 domain: PlacementDomain,
599 ) -> Result<Self, FreeSpaceError> {
600 let offsets = match &domain {
601 PlacementDomain::FrameOffsets(offsets)
602 | PlacementDomain::SupportedFrameOffsets { offsets, .. } => Some(offsets),
603 _ => None,
604 };
605 if let (Some(offsets), Some(orientation)) = (offsets, shape.orientation()) {
611 match orientation {
612 PlacementOrientation::Fixed(frame) if same_axes(frame, offsets.anchor()) => {}
613 _ => return Err(FreeSpaceError::OrientationDomainConflict),
614 }
615 }
616 obstacles.sort();
617 obstacles.dedup();
618 Ok(Self {
619 scope,
620 shape,
621 obstacles,
622 domain,
623 band: None,
624 merged: Vec::new(),
625 swept: Vec::new(),
626 reach: None,
627 })
628 }
629 #[must_use]
631 pub fn with_band(mut self, band: ElevationBand) -> Self {
632 self.band = Some(band);
633 self
634 }
635 pub fn with_merged_scopes(mut self, mut merged: Vec<ObjectId>) -> Result<Self, FreeSpaceError> {
639 merged.sort();
640 merged.dedup();
641 if merged
642 .iter()
643 .any(|id| id == &self.scope || self.obstacles.binary_search(id).is_ok())
644 {
645 return Err(FreeSpaceError::MergedScopeConflict);
646 }
647 self.merged = merged;
648 Ok(self)
649 }
650 pub fn with_swept_doors(mut self, swept: Vec<SweptDoor>) -> Result<Self, FreeSpaceError> {
659 let mut swept = tidy_swept(swept).ok_or(FreeSpaceError::ConflictingSweptDoors)?;
660 if let Some(reach) = &self.reach {
661 swept.retain(|door| reach.entrances.binary_search(door.door()).is_err());
662 }
663 self.swept = swept;
664 Ok(self)
665 }
666 #[must_use]
670 pub fn with_entrance_reach(mut self, reach: EntranceReach) -> Self {
671 self.obstacles
672 .retain(|object| reach.entrances.binary_search(object).is_err());
673 self.swept
674 .retain(|door| reach.entrances.binary_search(door.door()).is_err());
675 self.reach = Some(reach);
676 self
677 }
678 pub fn entrance_reach(&self) -> Option<&EntranceReach> {
680 self.reach.as_ref()
681 }
682 pub fn swept_doors(&self) -> &[SweptDoor] {
684 &self.swept
685 }
686 pub fn scope(&self) -> &ObjectId {
687 &self.scope
688 }
689 pub fn shape(&self) -> &PlacementShape {
690 &self.shape
691 }
692 pub fn obstacles(&self) -> &[ObjectId] {
693 &self.obstacles
694 }
695 pub fn domain(&self) -> &PlacementDomain {
696 &self.domain
697 }
698 pub fn band(&self) -> Option<ElevationBand> {
700 self.band
701 }
702 pub fn effective_band(&self) -> ElevationBand {
705 self.band.unwrap_or(ElevationBand {
706 from_metres: 0.0,
707 to_metres: match &self.shape {
708 PlacementShape::Box { shape, .. } => shape.height_metres(),
709 PlacementShape::Cylinder(shape) => shape.height_metres(),
710 },
711 })
712 }
713 pub fn merged_scopes(&self) -> &[ObjectId] {
715 &self.merged
716 }
717}
718
719#[derive(Clone, Debug, PartialEq)]
721pub struct CompleteSupportEvidence {
722 support: ObjectId,
723 frame: MetricFrame,
724 maximum_gap_metres: f64,
725 evidence: Evidence,
726}
727
728impl CompleteSupportEvidence {
729 pub fn try_new(
730 support: ObjectId,
731 frame: MetricFrame,
732 maximum_gap_metres: f64,
733 evidence: Evidence,
734 ) -> Result<Self, FreeSpaceError> {
735 if !valid_non_negative(maximum_gap_metres) {
736 return Err(FreeSpaceError::InvalidSupportGap);
737 }
738 if !reviewable_exact_evidence(&evidence) {
739 return Err(FreeSpaceError::InexactSupportEvidence);
740 }
741 Ok(Self {
742 support,
743 frame,
744 maximum_gap_metres,
745 evidence,
746 })
747 }
748 pub fn support(&self) -> &ObjectId {
749 &self.support
750 }
751 pub fn frame(&self) -> &MetricFrame {
752 &self.frame
753 }
754 pub fn maximum_gap_metres(&self) -> f64 {
755 self.maximum_gap_metres
756 }
757 pub fn evidence(&self) -> &Evidence {
758 &self.evidence
759 }
760}
761
762#[derive(Clone, Debug, PartialEq)]
763pub struct CompleteClearanceEvidence {
764 request: ClearanceRequest,
765 evidence: Evidence,
766}
767impl CompleteClearanceEvidence {
768 pub fn try_new(request: ClearanceRequest, evidence: Evidence) -> Result<Self, FreeSpaceError> {
769 if !reviewable_exact_evidence(&evidence) {
770 return Err(FreeSpaceError::IncompleteClearanceEvidence);
771 }
772 Ok(Self { request, evidence })
773 }
774 pub fn request(&self) -> &ClearanceRequest {
775 &self.request
776 }
777 pub fn evidence(&self) -> &Evidence {
778 &self.evidence
779 }
780}
781
782#[derive(Clone, Debug, PartialEq)]
783pub struct ObstructionEvidence {
784 request: ClearanceRequest,
785 blockers: Vec<ObjectId>,
786 evidence: Evidence,
787}
788impl ObstructionEvidence {
789 pub fn try_new(
790 request: ClearanceRequest,
791 mut blockers: Vec<ObjectId>,
792 evidence: Evidence,
793 ) -> Result<Self, FreeSpaceError> {
794 if blockers.is_empty() {
795 return Err(FreeSpaceError::EmptyObstructionEvidence);
796 }
797 if !reviewable_exact_evidence(&evidence) {
798 return Err(FreeSpaceError::InexactObstructionEvidence);
799 }
800 blockers.sort();
801 blockers.dedup();
802 if blockers
803 .iter()
804 .any(|blocker| request.obstacles().binary_search(blocker).is_err())
805 {
806 return Err(FreeSpaceError::UnexpectedObstacleEvidence);
807 }
808 Ok(Self {
809 request,
810 blockers,
811 evidence,
812 })
813 }
814 pub fn request(&self) -> &ClearanceRequest {
815 &self.request
816 }
817 pub fn blockers(&self) -> &[ObjectId] {
818 &self.blockers
819 }
820 pub fn evidence(&self) -> &Evidence {
821 &self.evidence
822 }
823}
824
825#[derive(Clone, Debug, PartialEq)]
826pub enum ClearanceOutcome {
827 Clear(CompleteClearanceEvidence),
828 Obstructed(ObstructionEvidence),
829}
830
831#[derive(Clone, Debug, PartialEq)]
839pub struct ContainmentRequest {
840 frame: MetricFrame,
841 shape: ClearanceShape,
842 scopes: Vec<ObjectId>,
843}
844impl ContainmentRequest {
845 pub fn new(frame: MetricFrame, shape: ClearanceShape, mut scopes: Vec<ObjectId>) -> Self {
846 scopes.sort();
847 scopes.dedup();
848 Self {
849 frame,
850 shape,
851 scopes,
852 }
853 }
854 pub fn frame(&self) -> &MetricFrame {
855 &self.frame
856 }
857 pub fn shape(&self) -> ClearanceShape {
858 self.shape
859 }
860 pub fn scopes(&self) -> &[ObjectId] {
861 &self.scopes
862 }
863}
864
865#[derive(Clone, Debug, PartialEq)]
867pub struct ContainmentEvidence {
868 request: ContainmentRequest,
869 evidence: Evidence,
870}
871impl ContainmentEvidence {
872 pub fn try_new(
873 request: ContainmentRequest,
874 evidence: Evidence,
875 ) -> Result<Self, FreeSpaceError> {
876 if !reviewable_exact_evidence(&evidence) {
877 return Err(FreeSpaceError::IncompleteClearanceEvidence);
878 }
879 Ok(Self { request, evidence })
880 }
881 pub fn request(&self) -> &ContainmentRequest {
882 &self.request
883 }
884 pub fn evidence(&self) -> &Evidence {
885 &self.evidence
886 }
887}
888
889#[derive(Clone, Debug, PartialEq)]
896pub enum ContainmentOutcome {
897 Inside(ContainmentEvidence),
898 Outside(ContainmentEvidence),
899}
900
901#[derive(Clone, Debug, PartialEq)]
911pub struct SupportCoverageRequest {
912 frame: MetricFrame,
913 shape: ClearanceShape,
914 supports: Vec<ObjectId>,
915 from: f64,
916 to: f64,
917}
918impl SupportCoverageRequest {
919 pub fn try_new(
921 frame: MetricFrame,
922 shape: ClearanceShape,
923 mut supports: Vec<ObjectId>,
924 from_metres: f64,
925 to_metres: f64,
926 ) -> Result<Self, FreeSpaceError> {
927 if !(from_metres.is_finite() && to_metres.is_finite() && from_metres <= to_metres) {
928 return Err(FreeSpaceError::InvalidElevationBand);
929 }
930 supports.sort();
931 supports.dedup();
932 Ok(Self {
933 frame,
934 shape,
935 supports,
936 from: from_metres,
937 to: to_metres,
938 })
939 }
940 pub fn frame(&self) -> &MetricFrame {
941 &self.frame
942 }
943 pub fn shape(&self) -> ClearanceShape {
944 self.shape
945 }
946 pub fn supports(&self) -> &[ObjectId] {
947 &self.supports
948 }
949 pub fn from_metres(&self) -> f64 {
951 self.from
952 }
953 pub fn to_metres(&self) -> f64 {
955 self.to
956 }
957}
958
959#[derive(Clone, Debug, PartialEq)]
961pub struct SupportCoverageEvidence {
962 request: SupportCoverageRequest,
963 evidence: Evidence,
964}
965impl SupportCoverageEvidence {
966 pub fn try_new(
967 request: SupportCoverageRequest,
968 evidence: Evidence,
969 ) -> Result<Self, FreeSpaceError> {
970 if !reviewable_exact_evidence(&evidence) {
971 return Err(FreeSpaceError::InexactSupportEvidence);
972 }
973 Ok(Self { request, evidence })
974 }
975 pub fn request(&self) -> &SupportCoverageRequest {
976 &self.request
977 }
978 pub fn evidence(&self) -> &Evidence {
979 &self.evidence
980 }
981}
982
983#[derive(Clone, Debug, PartialEq)]
990pub enum SupportCoverageOutcome {
991 Supported(SupportCoverageEvidence),
992 Unsupported(SupportCoverageEvidence),
993}
994
995#[derive(Clone, Debug, PartialEq)]
997pub struct ClearancePlacementEvidence {
998 request: PlacementRequest,
999 frame: MetricFrame,
1000 support_evidence: Option<Box<CompleteSupportEvidence>>,
1001 evidence: Evidence,
1002}
1003fn validate_placement_witness(
1004 request: &PlacementRequest,
1005 frame: &MetricFrame,
1006 evidence: &Evidence,
1007) -> Result<(), FreeSpaceError> {
1008 if frame.origin().subject() != request.scope() {
1009 return Err(FreeSpaceError::PlacementScopeMismatch);
1010 }
1011 let offsets = match request.domain() {
1012 PlacementDomain::FrameOffsets(offsets)
1013 | PlacementDomain::SupportedFrameOffsets { offsets, .. } => Some(offsets),
1014 _ => None,
1015 };
1016 if offsets.is_some_and(|offsets| !offsets.contains_frame(frame)) {
1017 return Err(FreeSpaceError::PlacementDomainMismatch);
1018 }
1019 if let Some(PlacementOrientation::Fixed(fixed)) = request.shape().orientation() {
1020 if !same_axes(fixed, frame) {
1021 return Err(FreeSpaceError::PlacementOrientationMismatch);
1022 }
1023 }
1024 if !reviewable_exact_evidence(evidence) {
1025 return Err(FreeSpaceError::InexactPlacementEvidence);
1026 }
1027 Ok(())
1028}
1029
1030impl ClearancePlacementEvidence {
1031 pub fn try_new(
1032 request: PlacementRequest,
1033 frame: MetricFrame,
1034 evidence: Evidence,
1035 ) -> Result<Self, FreeSpaceError> {
1036 if requested_support(request.domain()).is_some() {
1037 return Err(FreeSpaceError::MissingSupportEvidence);
1038 }
1039 validate_placement_witness(&request, &frame, &evidence)?;
1040 Ok(Self {
1041 request,
1042 frame,
1043 support_evidence: None,
1044 evidence,
1045 })
1046 }
1047 pub fn try_new_supported(
1048 request: PlacementRequest,
1049 frame: MetricFrame,
1050 support_evidence: CompleteSupportEvidence,
1051 evidence: Evidence,
1052 ) -> Result<Self, FreeSpaceError> {
1053 validate_placement_witness(&request, &frame, &evidence)?;
1054 let required =
1055 requested_support(request.domain()).ok_or(FreeSpaceError::SupportEvidenceMismatch)?;
1056 if support_evidence.support() != required.support()
1057 || support_evidence.frame() != &frame
1058 || support_evidence.maximum_gap_metres() > required.maximum_gap_metres()
1059 {
1060 return Err(FreeSpaceError::SupportEvidenceMismatch);
1061 }
1062 Ok(Self {
1063 request,
1064 frame,
1065 support_evidence: Some(Box::new(support_evidence)),
1066 evidence,
1067 })
1068 }
1069 pub fn request(&self) -> &PlacementRequest {
1070 &self.request
1071 }
1072 pub fn frame(&self) -> &MetricFrame {
1073 &self.frame
1074 }
1075 pub fn support_evidence(&self) -> Option<&CompleteSupportEvidence> {
1076 self.support_evidence.as_deref()
1077 }
1078 pub fn evidence(&self) -> &Evidence {
1079 &self.evidence
1080 }
1081}
1082
1083#[derive(Clone, Debug, PartialEq)]
1085pub struct CompletePlacementEvidence {
1086 request: PlacementRequest,
1087 evidence: Evidence,
1088}
1089impl CompletePlacementEvidence {
1090 pub fn try_new(request: PlacementRequest, evidence: Evidence) -> Result<Self, FreeSpaceError> {
1091 if !reviewable_exact_evidence(&evidence) {
1092 return Err(FreeSpaceError::IncompleteClearanceEvidence);
1093 }
1094 Ok(Self { request, evidence })
1095 }
1096 pub fn request(&self) -> &PlacementRequest {
1097 &self.request
1098 }
1099 pub fn evidence(&self) -> &Evidence {
1100 &self.evidence
1101 }
1102}
1103
1104#[derive(Clone, Debug, PartialEq)]
1105pub enum PlacementOutcome {
1106 Found(ClearancePlacementEvidence),
1107 NoPlacement(CompletePlacementEvidence),
1108}
1109
1110#[derive(Clone, Debug, PartialEq)]
1111pub struct FreeAreaEvidence {
1112 request: FreeAreaRequest,
1113 available_area: AreaInterval,
1114 evidence: Evidence,
1115}
1116impl FreeAreaEvidence {
1117 pub fn try_new(
1118 request: FreeAreaRequest,
1119 available_area: AreaInterval,
1120 evidence: Evidence,
1121 ) -> Result<Self, FreeSpaceError> {
1122 if !reviewable_exact_evidence(&evidence) {
1123 return Err(FreeSpaceError::InexactAreaEvidence);
1124 }
1125 Ok(Self {
1126 request,
1127 available_area,
1128 evidence,
1129 })
1130 }
1131 pub fn request(&self) -> &FreeAreaRequest {
1132 &self.request
1133 }
1134 pub fn available_area(&self) -> &AreaInterval {
1135 &self.available_area
1136 }
1137 pub fn evidence(&self) -> &Evidence {
1138 &self.evidence
1139 }
1140}
1141
1142pub trait FreeSpaceService: Send + Sync + 'static {
1143 fn assess_clearance(
1144 &self,
1145 request: &ClearanceRequest,
1146 ) -> Result<ClearanceOutcome, FreeSpaceError>;
1147 fn find_placement(
1148 &self,
1149 request: &PlacementRequest,
1150 ) -> Result<PlacementOutcome, FreeSpaceError>;
1151 fn measure_free_area(
1152 &self,
1153 request: &FreeAreaRequest,
1154 ) -> Result<FreeAreaEvidence, FreeSpaceError>;
1155 fn assess_containment(
1158 &self,
1159 request: &ContainmentRequest,
1160 ) -> Result<ContainmentOutcome, FreeSpaceError> {
1161 let _ = request;
1162 Err(FreeSpaceError::Unavailable(
1163 "this free-space service does not compare clearance footprints with scopes".into(),
1164 ))
1165 }
1166 fn assess_support_coverage(
1170 &self,
1171 request: &SupportCoverageRequest,
1172 ) -> Result<SupportCoverageOutcome, FreeSpaceError> {
1173 let _ = request;
1174 Err(FreeSpaceError::Unavailable(
1175 "this free-space service does not compare clearance footprints with supports".into(),
1176 ))
1177 }
1178 fn map_circulation(
1182 &self,
1183 request: &CirculationRequest,
1184 ) -> Result<CirculationMap, FreeSpaceError> {
1185 let _ = request;
1186 Err(FreeSpaceError::Unavailable(
1187 "this free-space service does not map circulation".into(),
1188 ))
1189 }
1190}
1191
1192#[derive(Clone)]
1193pub struct FreeSpaceServiceHandle(Arc<dyn FreeSpaceService>);
1194impl FreeSpaceServiceHandle {
1195 pub fn new(service: Arc<dyn FreeSpaceService>) -> Self {
1196 Self(service)
1197 }
1198 pub fn assess_clearance(
1199 &self,
1200 request: &ClearanceRequest,
1201 ) -> Result<ClearanceOutcome, FreeSpaceError> {
1202 let outcome = self.0.assess_clearance(request)?;
1203 let actual = match &outcome {
1204 ClearanceOutcome::Clear(value) => value.request(),
1205 ClearanceOutcome::Obstructed(value) => value.request(),
1206 };
1207 if actual != request {
1208 return Err(FreeSpaceError::ResponseRequestMismatch);
1209 }
1210 Ok(outcome)
1211 }
1212 pub fn find_placement(
1213 &self,
1214 request: &PlacementRequest,
1215 ) -> Result<PlacementOutcome, FreeSpaceError> {
1216 let outcome = self.0.find_placement(request)?;
1217 let actual = match &outcome {
1218 PlacementOutcome::Found(value) => value.request(),
1219 PlacementOutcome::NoPlacement(value) => value.request(),
1220 };
1221 if actual != request {
1222 return Err(FreeSpaceError::ResponseRequestMismatch);
1223 }
1224 Ok(outcome)
1225 }
1226 pub fn measure_free_area(
1227 &self,
1228 request: &FreeAreaRequest,
1229 ) -> Result<FreeAreaEvidence, FreeSpaceError> {
1230 let evidence = self.0.measure_free_area(request)?;
1231 if evidence.request() != request {
1232 return Err(FreeSpaceError::ResponseRequestMismatch);
1233 }
1234 Ok(evidence)
1235 }
1236 pub fn assess_containment(
1237 &self,
1238 request: &ContainmentRequest,
1239 ) -> Result<ContainmentOutcome, FreeSpaceError> {
1240 let outcome = self.0.assess_containment(request)?;
1241 let actual = match &outcome {
1242 ContainmentOutcome::Inside(value) | ContainmentOutcome::Outside(value) => {
1243 value.request()
1244 }
1245 };
1246 if actual != request {
1247 return Err(FreeSpaceError::ResponseRequestMismatch);
1248 }
1249 Ok(outcome)
1250 }
1251}
1252
1253impl FreeSpaceServiceHandle {
1254 pub fn assess_support_coverage(
1261 &self,
1262 request: &SupportCoverageRequest,
1263 ) -> Result<SupportCoverageOutcome, FreeSpaceError> {
1264 let outcome = self.0.assess_support_coverage(request)?;
1265 let actual = match &outcome {
1266 SupportCoverageOutcome::Supported(value)
1267 | SupportCoverageOutcome::Unsupported(value) => value.request(),
1268 };
1269 if actual != request {
1270 return Err(FreeSpaceError::ResponseRequestMismatch);
1271 }
1272 Ok(outcome)
1273 }
1274
1275 pub fn map_circulation(
1282 &self,
1283 request: &CirculationRequest,
1284 ) -> Result<CirculationMap, FreeSpaceError> {
1285 let map = self.0.map_circulation(request)?;
1286 if map.request() != request {
1287 return Err(FreeSpaceError::ResponseRequestMismatch);
1288 }
1289 Ok(map)
1290 }
1291}
1292
1293fn valid_non_negative(value: f64) -> bool {
1294 value.is_finite() && value >= 0.0
1295}
1296fn valid_positive(value: f64) -> bool {
1297 value.is_finite() && value > 0.0
1298}