1use crate::{MetricPoint, MobilityProfile, ThresholdVerdict};
7use axioval_ir::{Evidence, ObjectId};
8use std::sync::Arc;
9use thiserror::Error;
10
11#[derive(Clone, Debug, Error, PartialEq, Eq)]
12pub enum FreeSpaceError {
13 #[error("free-area interval is invalid")]
14 InvalidAreaInterval,
15 #[error("metric direction is zero or non-finite")]
16 InvalidMetricDirection,
17 #[error("metric frame axes are not mutually perpendicular")]
18 InvalidMetricFrame,
19 #[error("clearance shape dimensions must be positive and finite")]
20 InvalidClearanceShape,
21 #[error("placement offset interval is non-finite or reversed")]
22 InvalidOffsetInterval,
23 #[error("placement support gap must be finite and non-negative")]
24 InvalidSupportGap,
25 #[error("clearance evidence is incomplete")]
26 IncompleteClearanceEvidence,
27 #[error("obstruction evidence has no blocking objects")]
28 EmptyObstructionEvidence,
29 #[error("obstruction evidence names an object outside the request candidate set")]
30 UnexpectedObstacleEvidence,
31 #[error("obstruction provenance is not exact and reviewable")]
32 InexactObstructionEvidence,
33 #[error("free-area evidence is not exact and reviewable")]
34 InexactAreaEvidence,
35 #[error("placement evidence is not exact and reviewable")]
36 InexactPlacementEvidence,
37 #[error("support evidence is not exact and reviewable")]
38 InexactSupportEvidence,
39 #[error("supported placement has no complete support evidence")]
40 MissingSupportEvidence,
41 #[error("support evidence does not match the requested support or found frame")]
42 SupportEvidenceMismatch,
43 #[error("placement frame is not grounded in the requested scope")]
44 PlacementScopeMismatch,
45 #[error("placement witness falls outside its requested search domain")]
46 PlacementDomainMismatch,
47 #[error("free-space backend returned evidence for another request")]
48 ResponseRequestMismatch,
49 #[error("free-space geometry is unavailable for `{0}`")]
50 MissingGeometry(Box<ObjectId>),
51 #[error("free-space query unavailable: {0}")]
52 Unavailable(String),
53}
54
55#[derive(Clone, Copy, Debug, PartialEq)]
56pub struct AreaInterval {
57 lower_square_metres: f64,
58 upper_square_metres: f64,
59}
60impl AreaInterval {
61 pub fn try_new(lower: f64, upper: f64) -> Result<Self, FreeSpaceError> {
62 if !valid_non_negative(lower) || !valid_non_negative(upper) || lower > upper {
63 return Err(FreeSpaceError::InvalidAreaInterval);
64 }
65 Ok(Self {
66 lower_square_metres: lower,
67 upper_square_metres: upper,
68 })
69 }
70 pub fn exact(square_metres: f64) -> Result<Self, FreeSpaceError> {
71 Self::try_new(square_metres, square_metres)
72 }
73 pub fn lower_square_metres(&self) -> f64 {
74 self.lower_square_metres
75 }
76 pub fn upper_square_metres(&self) -> f64 {
77 self.upper_square_metres
78 }
79 pub fn compare_minimum(&self, minimum: f64) -> Result<ThresholdVerdict, FreeSpaceError> {
80 if !valid_non_negative(minimum) {
81 return Err(FreeSpaceError::InvalidAreaInterval);
82 }
83 if self.lower_square_metres >= minimum {
84 Ok(ThresholdVerdict::Satisfied)
85 } else if self.upper_square_metres < minimum {
86 Ok(ThresholdVerdict::Violated)
87 } else {
88 Ok(ThresholdVerdict::Indeterminate)
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq)]
94pub struct MetricDirection([f64; 3]);
95impl MetricDirection {
96 pub fn try_new(vector: [f64; 3]) -> Result<Self, FreeSpaceError> {
97 if !vector.iter().all(|v| v.is_finite()) {
98 return Err(FreeSpaceError::InvalidMetricDirection);
99 }
100 let norm = vector.iter().map(|v| v * v).sum::<f64>().sqrt();
101 if norm <= f64::EPSILON {
102 return Err(FreeSpaceError::InvalidMetricDirection);
103 }
104 Ok(Self([vector[0] / norm, vector[1] / norm, vector[2] / norm]))
105 }
106 pub fn components(&self) -> [f64; 3] {
107 self.0
108 }
109 fn dot(self, other: Self) -> f64 {
110 self.0[0] * other.0[0] + self.0[1] * other.0[1] + self.0[2] * other.0[2]
111 }
112}
113
114#[derive(Clone, Debug, PartialEq)]
115pub struct MetricFrame {
116 origin: MetricPoint,
117 right: MetricDirection,
118 forward: MetricDirection,
119 up: MetricDirection,
120}
121impl MetricFrame {
122 pub fn try_new(
123 origin: MetricPoint,
124 right: MetricDirection,
125 forward: MetricDirection,
126 up: MetricDirection,
127 ) -> Result<Self, FreeSpaceError> {
128 const ORTHOGONAL_TOLERANCE: f64 = 1.0e-9;
129 let [rx, ry, rz] = right.components();
130 let [fx, fy, fz] = forward.components();
131 let [ux, uy, uz] = up.components();
132 let handedness =
133 (ry * fz - rz * fy) * ux + (rz * fx - rx * fz) * uy + (rx * fy - ry * fx) * uz;
134 if right.dot(forward).abs() > ORTHOGONAL_TOLERANCE
135 || right.dot(up).abs() > ORTHOGONAL_TOLERANCE
136 || forward.dot(up).abs() > ORTHOGONAL_TOLERANCE
137 || handedness < 1.0 - ORTHOGONAL_TOLERANCE
138 {
139 return Err(FreeSpaceError::InvalidMetricFrame);
140 }
141 Ok(Self {
142 origin,
143 right,
144 forward,
145 up,
146 })
147 }
148 pub fn origin(&self) -> &MetricPoint {
149 &self.origin
150 }
151 pub fn right(&self) -> MetricDirection {
152 self.right
153 }
154 pub fn forward(&self) -> MetricDirection {
155 self.forward
156 }
157 pub fn up(&self) -> MetricDirection {
158 self.up
159 }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq)]
163pub struct BoxClearance {
164 width: f64,
165 depth: f64,
166 height: f64,
167}
168impl BoxClearance {
169 pub fn try_new(width: f64, depth: f64, height: f64) -> Result<Self, FreeSpaceError> {
170 if !valid_positive(width) || !valid_positive(depth) || !valid_positive(height) {
171 return Err(FreeSpaceError::InvalidClearanceShape);
172 }
173 Ok(Self {
174 width,
175 depth,
176 height,
177 })
178 }
179 pub fn width_metres(&self) -> f64 {
180 self.width
181 }
182 pub fn depth_metres(&self) -> f64 {
183 self.depth
184 }
185 pub fn height_metres(&self) -> f64 {
186 self.height
187 }
188}
189
190#[derive(Clone, Copy, Debug, PartialEq)]
191pub struct CylinderClearance {
192 radius: f64,
193 height: f64,
194}
195impl CylinderClearance {
196 pub fn try_new(radius: f64, height: f64) -> Result<Self, FreeSpaceError> {
197 if !valid_positive(radius) || !valid_positive(height) {
198 return Err(FreeSpaceError::InvalidClearanceShape);
199 }
200 Ok(Self { radius, height })
201 }
202 pub fn radius_metres(&self) -> f64 {
203 self.radius
204 }
205 pub fn height_metres(&self) -> f64 {
206 self.height
207 }
208}
209
210#[derive(Clone, Copy, Debug, PartialEq)]
211pub enum ClearanceShape {
212 Box(BoxClearance),
213 Cylinder(CylinderClearance),
214}
215
216#[derive(Clone, Debug, PartialEq)]
217pub struct ClearanceRequest {
218 frame: MetricFrame,
219 shape: ClearanceShape,
220 obstacles: Vec<ObjectId>,
221}
222impl ClearanceRequest {
223 pub fn new(frame: MetricFrame, shape: ClearanceShape, mut obstacles: Vec<ObjectId>) -> Self {
224 obstacles.sort();
225 obstacles.dedup();
226 Self {
227 frame,
228 shape,
229 obstacles,
230 }
231 }
232 pub fn frame(&self) -> &MetricFrame {
233 &self.frame
234 }
235 pub fn shape(&self) -> ClearanceShape {
236 self.shape
237 }
238 pub fn obstacles(&self) -> &[ObjectId] {
239 &self.obstacles
240 }
241}
242
243#[derive(Clone, Debug, PartialEq)]
244pub struct FreeAreaRequest {
245 scope: ObjectId,
246 mobility: MobilityProfile,
247 obstacles: Vec<ObjectId>,
248}
249impl FreeAreaRequest {
250 pub fn new(scope: ObjectId, mobility: MobilityProfile, mut obstacles: Vec<ObjectId>) -> Self {
251 obstacles.sort();
252 obstacles.dedup();
253 Self {
254 scope,
255 mobility,
256 obstacles,
257 }
258 }
259 pub fn scope(&self) -> &ObjectId {
260 &self.scope
261 }
262 pub fn mobility(&self) -> MobilityProfile {
263 self.mobility
264 }
265 pub fn obstacles(&self) -> &[ObjectId] {
266 &self.obstacles
267 }
268}
269
270#[derive(Clone, Copy, Debug, PartialEq)]
272pub struct SignedDistanceInterval {
273 lower_metres: f64,
274 upper_metres: f64,
275}
276impl SignedDistanceInterval {
277 pub fn try_new(lower_metres: f64, upper_metres: f64) -> Result<Self, FreeSpaceError> {
278 if !lower_metres.is_finite() || !upper_metres.is_finite() || lower_metres > upper_metres {
279 return Err(FreeSpaceError::InvalidOffsetInterval);
280 }
281 Ok(Self {
282 lower_metres,
283 upper_metres,
284 })
285 }
286 pub fn exact(metres: f64) -> Result<Self, FreeSpaceError> {
287 Self::try_new(metres, metres)
288 }
289 pub fn lower_metres(&self) -> f64 {
290 self.lower_metres
291 }
292 pub fn upper_metres(&self) -> f64 {
293 self.upper_metres
294 }
295 fn contains(self, value: f64) -> bool {
296 value >= self.lower_metres && value <= self.upper_metres
297 }
298}
299
300#[derive(Clone, Debug, PartialEq)]
302pub struct SupportedPlacement {
303 support: ObjectId,
304 maximum_gap_metres: f64,
305}
306impl SupportedPlacement {
307 pub fn try_new(support: ObjectId, maximum_gap_metres: f64) -> Result<Self, FreeSpaceError> {
308 if !valid_non_negative(maximum_gap_metres) {
309 return Err(FreeSpaceError::InvalidSupportGap);
310 }
311 Ok(Self {
312 support,
313 maximum_gap_metres,
314 })
315 }
316 pub fn support(&self) -> &ObjectId {
317 &self.support
318 }
319 pub fn maximum_gap_metres(&self) -> f64 {
320 self.maximum_gap_metres
321 }
322}
323
324#[derive(Clone, Debug, PartialEq)]
326pub struct FrameOffsetPlacement {
327 anchor: MetricFrame,
328 right: SignedDistanceInterval,
329 forward: SignedDistanceInterval,
330 up: SignedDistanceInterval,
331}
332impl FrameOffsetPlacement {
333 pub fn new(
334 anchor: MetricFrame,
335 right: SignedDistanceInterval,
336 forward: SignedDistanceInterval,
337 up: SignedDistanceInterval,
338 ) -> Self {
339 Self {
340 anchor,
341 right,
342 forward,
343 up,
344 }
345 }
346 pub fn anchor(&self) -> &MetricFrame {
347 &self.anchor
348 }
349 pub fn right(&self) -> SignedDistanceInterval {
350 self.right
351 }
352 pub fn forward(&self) -> SignedDistanceInterval {
353 self.forward
354 }
355 pub fn up(&self) -> SignedDistanceInterval {
356 self.up
357 }
358 fn contains_frame(&self, frame: &MetricFrame) -> bool {
359 let aligned = self.anchor.right() == frame.right()
360 && self.anchor.forward() == frame.forward()
361 && self.anchor.up() == frame.up();
362 if !aligned {
363 return false;
364 }
365 let anchor = self.anchor.origin().coordinates_metres();
366 let found = frame.origin().coordinates_metres();
367 let delta = [
368 found[0] - anchor[0],
369 found[1] - anchor[1],
370 found[2] - anchor[2],
371 ];
372 let project = |axis: MetricDirection| {
373 axis.components()
374 .into_iter()
375 .zip(delta)
376 .map(|(a, b)| a * b)
377 .sum()
378 };
379 self.right.contains(project(self.anchor.right()))
380 && self.forward.contains(project(self.anchor.forward()))
381 && self.up.contains(project(self.anchor.up()))
382 }
383}
384
385#[derive(Clone, Debug, PartialEq)]
387pub enum PlacementDomain {
388 Unconstrained,
389 Supported(SupportedPlacement),
390 FrameOffsets(FrameOffsetPlacement),
391 SupportedFrameOffsets {
392 support: SupportedPlacement,
393 offsets: FrameOffsetPlacement,
394 },
395}
396
397fn requested_support(domain: &PlacementDomain) -> Option<&SupportedPlacement> {
398 match domain {
399 PlacementDomain::Supported(support)
400 | PlacementDomain::SupportedFrameOffsets { support, .. } => Some(support),
401 _ => None,
402 }
403}
404
405#[derive(Clone, Debug, PartialEq)]
407pub struct PlacementRequest {
408 scope: ObjectId,
409 shape: ClearanceShape,
410 obstacles: Vec<ObjectId>,
411 domain: PlacementDomain,
412}
413impl PlacementRequest {
414 pub fn new(scope: ObjectId, shape: ClearanceShape, mut obstacles: Vec<ObjectId>) -> Self {
415 obstacles.sort();
416 obstacles.dedup();
417 Self {
418 scope,
419 shape,
420 obstacles,
421 domain: PlacementDomain::Unconstrained,
422 }
423 }
424 pub fn new_in_domain(
425 scope: ObjectId,
426 shape: ClearanceShape,
427 mut obstacles: Vec<ObjectId>,
428 domain: PlacementDomain,
429 ) -> Result<Self, FreeSpaceError> {
430 let offsets = match &domain {
431 PlacementDomain::FrameOffsets(offsets)
432 | PlacementDomain::SupportedFrameOffsets { offsets, .. } => Some(offsets),
433 _ => None,
434 };
435 if offsets.is_some_and(|offsets| offsets.anchor().origin().subject() != &scope) {
436 return Err(FreeSpaceError::PlacementScopeMismatch);
437 }
438 obstacles.sort();
439 obstacles.dedup();
440 Ok(Self {
441 scope,
442 shape,
443 obstacles,
444 domain,
445 })
446 }
447 pub fn scope(&self) -> &ObjectId {
448 &self.scope
449 }
450 pub fn shape(&self) -> ClearanceShape {
451 self.shape
452 }
453 pub fn obstacles(&self) -> &[ObjectId] {
454 &self.obstacles
455 }
456 pub fn domain(&self) -> &PlacementDomain {
457 &self.domain
458 }
459}
460
461#[derive(Clone, Debug, PartialEq)]
463pub struct CompleteSupportEvidence {
464 support: ObjectId,
465 frame: MetricFrame,
466 maximum_gap_metres: f64,
467 evidence: Evidence,
468}
469
470impl CompleteSupportEvidence {
471 pub fn try_new(
472 support: ObjectId,
473 frame: MetricFrame,
474 maximum_gap_metres: f64,
475 evidence: Evidence,
476 ) -> Result<Self, FreeSpaceError> {
477 if !valid_non_negative(maximum_gap_metres) {
478 return Err(FreeSpaceError::InvalidSupportGap);
479 }
480 if !reviewable_exact_evidence(&evidence) {
481 return Err(FreeSpaceError::InexactSupportEvidence);
482 }
483 Ok(Self {
484 support,
485 frame,
486 maximum_gap_metres,
487 evidence,
488 })
489 }
490 pub fn support(&self) -> &ObjectId {
491 &self.support
492 }
493 pub fn frame(&self) -> &MetricFrame {
494 &self.frame
495 }
496 pub fn maximum_gap_metres(&self) -> f64 {
497 self.maximum_gap_metres
498 }
499 pub fn evidence(&self) -> &Evidence {
500 &self.evidence
501 }
502}
503
504#[derive(Clone, Debug, PartialEq)]
505pub struct CompleteClearanceEvidence {
506 request: ClearanceRequest,
507 evidence: Evidence,
508}
509impl CompleteClearanceEvidence {
510 pub fn try_new(request: ClearanceRequest, evidence: Evidence) -> Result<Self, FreeSpaceError> {
511 if !reviewable_exact_evidence(&evidence) {
512 return Err(FreeSpaceError::IncompleteClearanceEvidence);
513 }
514 Ok(Self { request, evidence })
515 }
516 pub fn request(&self) -> &ClearanceRequest {
517 &self.request
518 }
519 pub fn evidence(&self) -> &Evidence {
520 &self.evidence
521 }
522}
523
524#[derive(Clone, Debug, PartialEq)]
525pub struct ObstructionEvidence {
526 request: ClearanceRequest,
527 blockers: Vec<ObjectId>,
528 evidence: Evidence,
529}
530impl ObstructionEvidence {
531 pub fn try_new(
532 request: ClearanceRequest,
533 mut blockers: Vec<ObjectId>,
534 evidence: Evidence,
535 ) -> Result<Self, FreeSpaceError> {
536 if blockers.is_empty() {
537 return Err(FreeSpaceError::EmptyObstructionEvidence);
538 }
539 if !reviewable_exact_evidence(&evidence) {
540 return Err(FreeSpaceError::InexactObstructionEvidence);
541 }
542 blockers.sort();
543 blockers.dedup();
544 if blockers
545 .iter()
546 .any(|blocker| request.obstacles().binary_search(blocker).is_err())
547 {
548 return Err(FreeSpaceError::UnexpectedObstacleEvidence);
549 }
550 Ok(Self {
551 request,
552 blockers,
553 evidence,
554 })
555 }
556 pub fn request(&self) -> &ClearanceRequest {
557 &self.request
558 }
559 pub fn blockers(&self) -> &[ObjectId] {
560 &self.blockers
561 }
562 pub fn evidence(&self) -> &Evidence {
563 &self.evidence
564 }
565}
566
567#[derive(Clone, Debug, PartialEq)]
568pub enum ClearanceOutcome {
569 Clear(CompleteClearanceEvidence),
570 Obstructed(ObstructionEvidence),
571}
572
573#[derive(Clone, Debug, PartialEq)]
575pub struct ClearancePlacementEvidence {
576 request: PlacementRequest,
577 frame: MetricFrame,
578 support_evidence: Option<Box<CompleteSupportEvidence>>,
579 evidence: Evidence,
580}
581fn validate_placement_witness(
582 request: &PlacementRequest,
583 frame: &MetricFrame,
584 evidence: &Evidence,
585) -> Result<(), FreeSpaceError> {
586 if frame.origin().subject() != request.scope() {
587 return Err(FreeSpaceError::PlacementScopeMismatch);
588 }
589 let offsets = match request.domain() {
590 PlacementDomain::FrameOffsets(offsets)
591 | PlacementDomain::SupportedFrameOffsets { offsets, .. } => Some(offsets),
592 _ => None,
593 };
594 if offsets.is_some_and(|offsets| !offsets.contains_frame(frame)) {
595 return Err(FreeSpaceError::PlacementDomainMismatch);
596 }
597 if !reviewable_exact_evidence(evidence) {
598 return Err(FreeSpaceError::InexactPlacementEvidence);
599 }
600 Ok(())
601}
602
603impl ClearancePlacementEvidence {
604 pub fn try_new(
605 request: PlacementRequest,
606 frame: MetricFrame,
607 evidence: Evidence,
608 ) -> Result<Self, FreeSpaceError> {
609 if requested_support(request.domain()).is_some() {
610 return Err(FreeSpaceError::MissingSupportEvidence);
611 }
612 validate_placement_witness(&request, &frame, &evidence)?;
613 Ok(Self {
614 request,
615 frame,
616 support_evidence: None,
617 evidence,
618 })
619 }
620 pub fn try_new_supported(
621 request: PlacementRequest,
622 frame: MetricFrame,
623 support_evidence: CompleteSupportEvidence,
624 evidence: Evidence,
625 ) -> Result<Self, FreeSpaceError> {
626 validate_placement_witness(&request, &frame, &evidence)?;
627 let required =
628 requested_support(request.domain()).ok_or(FreeSpaceError::SupportEvidenceMismatch)?;
629 if support_evidence.support() != required.support()
630 || support_evidence.frame() != &frame
631 || support_evidence.maximum_gap_metres() > required.maximum_gap_metres()
632 {
633 return Err(FreeSpaceError::SupportEvidenceMismatch);
634 }
635 Ok(Self {
636 request,
637 frame,
638 support_evidence: Some(Box::new(support_evidence)),
639 evidence,
640 })
641 }
642 pub fn request(&self) -> &PlacementRequest {
643 &self.request
644 }
645 pub fn frame(&self) -> &MetricFrame {
646 &self.frame
647 }
648 pub fn support_evidence(&self) -> Option<&CompleteSupportEvidence> {
649 self.support_evidence.as_deref()
650 }
651 pub fn evidence(&self) -> &Evidence {
652 &self.evidence
653 }
654}
655
656#[derive(Clone, Debug, PartialEq)]
658pub struct CompletePlacementEvidence {
659 request: PlacementRequest,
660 evidence: Evidence,
661}
662impl CompletePlacementEvidence {
663 pub fn try_new(request: PlacementRequest, evidence: Evidence) -> Result<Self, FreeSpaceError> {
664 if !reviewable_exact_evidence(&evidence) {
665 return Err(FreeSpaceError::IncompleteClearanceEvidence);
666 }
667 Ok(Self { request, evidence })
668 }
669 pub fn request(&self) -> &PlacementRequest {
670 &self.request
671 }
672 pub fn evidence(&self) -> &Evidence {
673 &self.evidence
674 }
675}
676
677#[derive(Clone, Debug, PartialEq)]
678pub enum PlacementOutcome {
679 Found(ClearancePlacementEvidence),
680 NoPlacement(CompletePlacementEvidence),
681}
682
683#[derive(Clone, Debug, PartialEq)]
684pub struct FreeAreaEvidence {
685 request: FreeAreaRequest,
686 available_area: AreaInterval,
687 evidence: Evidence,
688}
689impl FreeAreaEvidence {
690 pub fn try_new(
691 request: FreeAreaRequest,
692 available_area: AreaInterval,
693 evidence: Evidence,
694 ) -> Result<Self, FreeSpaceError> {
695 if !reviewable_exact_evidence(&evidence) {
696 return Err(FreeSpaceError::InexactAreaEvidence);
697 }
698 Ok(Self {
699 request,
700 available_area,
701 evidence,
702 })
703 }
704 pub fn request(&self) -> &FreeAreaRequest {
705 &self.request
706 }
707 pub fn available_area(&self) -> &AreaInterval {
708 &self.available_area
709 }
710 pub fn evidence(&self) -> &Evidence {
711 &self.evidence
712 }
713}
714
715pub trait FreeSpaceService: Send + Sync + 'static {
716 fn assess_clearance(
717 &self,
718 request: &ClearanceRequest,
719 ) -> Result<ClearanceOutcome, FreeSpaceError>;
720 fn find_placement(
721 &self,
722 request: &PlacementRequest,
723 ) -> Result<PlacementOutcome, FreeSpaceError>;
724 fn measure_free_area(
725 &self,
726 request: &FreeAreaRequest,
727 ) -> Result<FreeAreaEvidence, FreeSpaceError>;
728}
729
730#[derive(Clone)]
731pub struct FreeSpaceServiceHandle(Arc<dyn FreeSpaceService>);
732impl FreeSpaceServiceHandle {
733 pub fn new(service: Arc<dyn FreeSpaceService>) -> Self {
734 Self(service)
735 }
736 pub fn assess_clearance(
737 &self,
738 request: &ClearanceRequest,
739 ) -> Result<ClearanceOutcome, FreeSpaceError> {
740 let outcome = self.0.assess_clearance(request)?;
741 let actual = match &outcome {
742 ClearanceOutcome::Clear(value) => value.request(),
743 ClearanceOutcome::Obstructed(value) => value.request(),
744 };
745 if actual != request {
746 return Err(FreeSpaceError::ResponseRequestMismatch);
747 }
748 Ok(outcome)
749 }
750 pub fn find_placement(
751 &self,
752 request: &PlacementRequest,
753 ) -> Result<PlacementOutcome, FreeSpaceError> {
754 let outcome = self.0.find_placement(request)?;
755 let actual = match &outcome {
756 PlacementOutcome::Found(value) => value.request(),
757 PlacementOutcome::NoPlacement(value) => value.request(),
758 };
759 if actual != request {
760 return Err(FreeSpaceError::ResponseRequestMismatch);
761 }
762 Ok(outcome)
763 }
764 pub fn measure_free_area(
765 &self,
766 request: &FreeAreaRequest,
767 ) -> Result<FreeAreaEvidence, FreeSpaceError> {
768 let evidence = self.0.measure_free_area(request)?;
769 if evidence.request() != request {
770 return Err(FreeSpaceError::ResponseRequestMismatch);
771 }
772 Ok(evidence)
773 }
774}
775
776fn valid_non_negative(value: f64) -> bool {
777 value.is_finite() && value >= 0.0
778}
779fn valid_positive(value: f64) -> bool {
780 value.is_finite() && value > 0.0
781}
782fn reviewable_exact_evidence(evidence: &Evidence) -> bool {
783 evidence.exact && !evidence.locator.trim().is_empty()
784}