1use std::sync::Arc;
27
28use axioval_ir::{Evidence, ObjectId};
29use thiserror::Error;
30
31use crate::corridor_end::{self, CorridorEndRequest, CorridorEnds};
32use crate::side_distance::{self, SideDistanceRequest, SideDistances};
33
34#[derive(Clone, Debug, Error, PartialEq, Eq)]
36pub enum PlanSpanError {
37 #[error("no geometry for `{0}`")]
39 UnknownObject(ObjectId),
40 #[error("plan span unavailable: {0}")]
43 Unavailable(String),
44 #[error("plan span measurement is invalid")]
46 InvalidMeasurement,
47 #[error("plan span evidence does not match its exactness")]
49 InexactEvidence,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub enum PlanSpan {
55 Centres,
57 Farthest,
59}
60
61impl PlanSpan {
62 #[must_use]
64 pub fn name(self) -> &'static str {
65 match self {
66 Self::Centres => "centres",
67 Self::Farthest => "farthest",
68 }
69 }
70}
71
72#[derive(Clone, Debug, PartialEq)]
74pub struct PlanLength {
75 lower: f64,
76 upper: f64,
77 evidence: Evidence,
78}
79
80impl PlanLength {
81 pub fn try_new(lower: f64, upper: f64, evidence: Evidence) -> Result<Self, PlanSpanError> {
86 if !lower.is_finite() || !upper.is_finite() || lower < 0.0 || lower > upper {
87 return Err(PlanSpanError::InvalidMeasurement);
88 }
89 #[allow(clippy::float_cmp)]
90 let exact = lower == upper;
91 if evidence.exact != exact || evidence.locator.trim().is_empty() {
92 return Err(PlanSpanError::InexactEvidence);
93 }
94 Ok(Self {
95 lower,
96 upper,
97 evidence,
98 })
99 }
100
101 #[must_use]
103 pub fn lower_metres(&self) -> f64 {
104 self.lower
105 }
106
107 #[must_use]
109 pub fn upper_metres(&self) -> f64 {
110 self.upper
111 }
112
113 #[must_use]
115 pub fn is_exact(&self) -> bool {
116 self.evidence.exact
117 }
118
119 #[must_use]
121 pub fn evidence(&self) -> &Evidence {
122 &self.evidence
123 }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
128pub enum CentrePlacement {
129 Inside,
132 Outside,
135 Undecided,
138}
139
140#[derive(Clone, Debug, PartialEq)]
143pub struct PlanCentre {
144 object: ObjectId,
145 point: [f64; 2],
146 radius: f64,
147 placement: CentrePlacement,
148 evidence: Evidence,
149}
150
151impl PlanCentre {
152 pub fn try_new(
157 object: ObjectId,
158 point: [f64; 2],
159 radius: f64,
160 placement: CentrePlacement,
161 evidence: Evidence,
162 ) -> Result<Self, PlanSpanError> {
163 if !point.iter().all(|value| value.is_finite()) || !radius.is_finite() || radius < 0.0 {
164 return Err(PlanSpanError::InvalidMeasurement);
165 }
166 #[allow(clippy::float_cmp)]
167 let exact = radius == 0.0;
168 if evidence.exact != exact || evidence.locator.trim().is_empty() {
169 return Err(PlanSpanError::InexactEvidence);
170 }
171 Ok(Self {
172 object,
173 point,
174 radius,
175 placement,
176 evidence,
177 })
178 }
179
180 #[must_use]
182 pub fn object(&self) -> &ObjectId {
183 &self.object
184 }
185
186 #[must_use]
188 pub fn point(&self) -> [f64; 2] {
189 self.point
190 }
191
192 #[must_use]
194 pub fn radius_metres(&self) -> f64 {
195 self.radius
196 }
197
198 #[must_use]
200 pub fn placement(&self) -> CentrePlacement {
201 self.placement
202 }
203
204 #[must_use]
206 pub fn is_exact(&self) -> bool {
207 self.evidence.exact
208 }
209
210 #[must_use]
212 pub fn evidence(&self) -> &Evidence {
213 &self.evidence
214 }
215}
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
219pub enum RectangleOrientation {
220 Unique,
224 Tied,
228 Unproven,
232}
233
234impl RectangleOrientation {
235 #[must_use]
237 pub fn name(self) -> &'static str {
238 match self {
239 Self::Unique => "unique",
240 Self::Tied => "tied",
241 Self::Unproven => "unproven",
242 }
243 }
244}
245
246#[derive(Clone, Debug, PartialEq)]
255pub struct PlanRectangle {
256 object: ObjectId,
257 centre: [f64; 2],
258 centre_radius: f64,
259 axes: [[f64; 2]; 2],
260 axis_error: f64,
261 half_extents: [(f64, f64); 2],
262 orientation: RectangleOrientation,
263 evidence: Evidence,
264}
265
266const AXIS_ROUNDING: f64 = 1e-9;
269
270impl PlanRectangle {
271 #[allow(clippy::too_many_arguments)]
278 pub fn try_new(
279 object: ObjectId,
280 centre: [f64; 2],
281 centre_radius: f64,
282 axes: [[f64; 2]; 2],
283 axis_error: f64,
284 half_extents: [(f64, f64); 2],
285 orientation: RectangleOrientation,
286 evidence: Evidence,
287 ) -> Result<Self, PlanSpanError> {
288 let finite = centre
289 .iter()
290 .chain(axes.iter().flatten())
291 .all(|value| value.is_finite());
292 let bounded = |value: f64| value.is_finite() && value >= 0.0;
293 let [[ux, uy], [vx, vy]] = axes;
294 let unit = (ux.mul_add(ux, uy * uy) - 1.0).abs() <= AXIS_ROUNDING;
295 let quarter = (vx + uy).abs() <= AXIS_ROUNDING && (vy - ux).abs() <= AXIS_ROUNDING;
296 if !finite
297 || !bounded(centre_radius)
298 || !bounded(axis_error)
299 || axis_error > std::f64::consts::FRAC_PI_2
300 || !unit
301 || !quarter
302 || !(ux > 0.0 && uy >= 0.0)
303 || half_extents
304 .iter()
305 .any(|&(lower, upper)| !bounded(lower) || !upper.is_finite() || lower > upper)
306 {
307 return Err(PlanSpanError::InvalidMeasurement);
308 }
309 #[allow(clippy::float_cmp)]
310 let exact = centre_radius == 0.0
311 && axis_error == 0.0
312 && half_extents.iter().all(|(lower, upper)| lower == upper)
313 && orientation == RectangleOrientation::Unique;
314 if evidence.exact != exact || evidence.locator.trim().is_empty() {
315 return Err(PlanSpanError::InexactEvidence);
316 }
317 Ok(Self {
318 object,
319 centre,
320 centre_radius,
321 axes,
322 axis_error,
323 half_extents,
324 orientation,
325 evidence,
326 })
327 }
328
329 #[must_use]
331 pub fn object(&self) -> &ObjectId {
332 &self.object
333 }
334
335 #[must_use]
337 pub fn centre(&self) -> [f64; 2] {
338 self.centre
339 }
340
341 #[must_use]
343 pub fn centre_radius_metres(&self) -> f64 {
344 self.centre_radius
345 }
346
347 #[must_use]
350 pub fn axes(&self) -> [[f64; 2]; 2] {
351 self.axes
352 }
353
354 #[must_use]
357 pub fn axis_error_radians(&self) -> f64 {
358 self.axis_error
359 }
360
361 #[must_use]
363 pub fn half_extents_metres(&self) -> [(f64, f64); 2] {
364 self.half_extents
365 }
366
367 #[must_use]
369 pub fn orientation(&self) -> RectangleOrientation {
370 self.orientation
371 }
372
373 #[must_use]
375 pub fn is_exact(&self) -> bool {
376 self.evidence.exact
377 }
378
379 #[must_use]
381 pub fn evidence(&self) -> &Evidence {
382 &self.evidence
383 }
384
385 fn unoriented(&self) -> Option<String> {
387 match self.orientation {
388 RectangleOrientation::Unique => None,
389 RectangleOrientation::Tied => Some(format!(
390 "several orientations enclose {} with the least area",
391 self.object
392 )),
393 RectangleOrientation::Unproven => Some(format!(
394 "{} is tessellated, so the orientation enclosing its true footprint with the \
395 least area is not known",
396 self.object
397 )),
398 }
399 }
400
401 pub fn width_and_length(&self) -> Result<[(f64, f64); 2], String> {
408 if let Some(reason) = self.unoriented() {
409 return Err(reason);
410 }
411 let [(a0, a1), (b0, b1)] = self.half_extents;
412 Ok([
413 (2.0 * a0.min(b0), 2.0 * a1.min(b1)),
414 (2.0 * a0.max(b0), 2.0 * a1.max(b1)),
415 ])
416 }
417
418 pub fn long_axis(&self) -> Result<usize, String> {
422 if let Some(reason) = self.unoriented() {
423 return Err(reason);
424 }
425 let [(a0, a1), (b0, b1)] = self.half_extents;
426 if a0 > b1 {
427 Ok(0)
428 } else if b0 > a1 {
429 Ok(1)
430 } else {
431 Err(format!(
432 "the sides of {} are too close to equal to tell its long axis",
433 self.object
434 ))
435 }
436 }
437
438 pub fn long_axis_angle(&self, other: &Self) -> Result<(f64, f64), String> {
442 let own = self.axes[self.long_axis()?];
443 let theirs = other.axes[other.long_axis()?];
444 let dot = own[0].mul_add(theirs[0], own[1] * theirs[1]).abs();
445 let cross = own[0].mul_add(theirs[1], -(own[1] * theirs[0])).abs();
446 let angle = cross.atan2(dot).to_degrees();
447 let slack = (self.axis_error + other.axis_error).to_degrees() + 1e-9;
450 Ok(((angle - slack).max(0.0), (angle + slack).min(90.0)))
451 }
452}
453
454#[derive(Clone, Debug, PartialEq)]
461pub struct PlanRecess {
462 mouth: [[f64; 2]; 2],
463 width: PlanLength,
464 depth: PlanLength,
465}
466
467impl PlanRecess {
468 pub fn try_new(
470 mouth: [[f64; 2]; 2],
471 width: PlanLength,
472 depth: PlanLength,
473 ) -> Result<Self, PlanSpanError> {
474 if !mouth.iter().flatten().all(|value| value.is_finite()) {
475 return Err(PlanSpanError::InvalidMeasurement);
476 }
477 Ok(Self {
478 mouth,
479 width,
480 depth,
481 })
482 }
483
484 #[must_use]
487 pub fn mouth(&self) -> [[f64; 2]; 2] {
488 self.mouth
489 }
490
491 #[must_use]
493 pub fn width(&self) -> &PlanLength {
494 &self.width
495 }
496
497 #[must_use]
499 pub fn depth(&self) -> &PlanLength {
500 &self.depth
501 }
502}
503
504#[derive(Clone, Debug, PartialEq)]
506pub struct PlanRecesses {
507 object: ObjectId,
508 recesses: Vec<PlanRecess>,
509 evidence: Evidence,
510}
511
512impl PlanRecesses {
513 pub fn try_new(
516 object: ObjectId,
517 recesses: Vec<PlanRecess>,
518 evidence: Evidence,
519 ) -> Result<Self, PlanSpanError> {
520 if evidence.locator.trim().is_empty() {
521 return Err(PlanSpanError::InexactEvidence);
522 }
523 Ok(Self {
524 object,
525 recesses,
526 evidence,
527 })
528 }
529
530 #[must_use]
532 pub fn object(&self) -> &ObjectId {
533 &self.object
534 }
535
536 #[must_use]
538 pub fn recesses(&self) -> &[PlanRecess] {
539 &self.recesses
540 }
541
542 #[must_use]
544 pub fn evidence(&self) -> &Evidence {
545 &self.evidence
546 }
547}
548
549#[derive(Clone, Debug, PartialEq)]
552pub struct PlanSection {
553 objects: Vec<ObjectId>,
554 area: (f64, f64),
555 sides: Option<(PlanLength, PlanLength)>,
556 evidence: Evidence,
557}
558
559impl PlanSection {
560 pub fn try_new(
570 objects: Vec<ObjectId>,
571 area: (f64, f64),
572 sides: Option<(PlanLength, PlanLength)>,
573 evidence: Evidence,
574 ) -> Result<Self, PlanSpanError> {
575 let (lower, upper) = area;
576 if !lower.is_finite() || !upper.is_finite() || lower < 0.0 || lower > upper {
577 return Err(PlanSpanError::InvalidMeasurement);
578 }
579 #[allow(clippy::float_cmp)]
580 let exact = lower == upper;
581 if evidence.exact != exact || evidence.locator.trim().is_empty() {
582 return Err(PlanSpanError::InexactEvidence);
583 }
584 if (upper == 0.0) != sides.is_none() {
585 return Err(PlanSpanError::InvalidMeasurement);
586 }
587 if let Some((short, long)) = &sides
588 && short.lower_metres() > long.upper_metres()
589 {
590 return Err(PlanSpanError::InvalidMeasurement);
591 }
592 Ok(Self {
593 objects,
594 area,
595 sides,
596 evidence,
597 })
598 }
599
600 #[must_use]
602 pub fn objects(&self) -> &[ObjectId] {
603 &self.objects
604 }
605
606 #[must_use]
608 pub fn area_lower(&self) -> f64 {
609 self.area.0
610 }
611
612 #[must_use]
614 pub fn area_upper(&self) -> f64 {
615 self.area.1
616 }
617
618 #[must_use]
621 pub fn width(&self) -> Option<&PlanLength> {
622 self.sides.as_ref().map(|(short, _)| short)
623 }
624
625 #[must_use]
628 pub fn length(&self) -> Option<&PlanLength> {
629 self.sides.as_ref().map(|(_, long)| long)
630 }
631
632 #[must_use]
634 pub fn evidence(&self) -> &Evidence {
635 &self.evidence
636 }
637}
638
639pub trait PlanSpanService: Send + Sync + 'static {
641 fn measure_diameter(&self, object: &ObjectId) -> Result<PlanLength, PlanSpanError>;
645 fn measure_span(
648 &self,
649 first: &ObjectId,
650 second: &ObjectId,
651 between: PlanSpan,
652 ) -> Result<PlanLength, PlanSpanError>;
653 fn measure_centre(&self, object: &ObjectId) -> Result<PlanCentre, PlanSpanError> {
658 Err(PlanSpanError::Unavailable(format!(
659 "this plan-span service does not locate the centre of {object}"
660 )))
661 }
662 fn measure_rectangle(&self, object: &ObjectId) -> Result<PlanRectangle, PlanSpanError> {
666 Err(PlanSpanError::Unavailable(format!(
667 "this plan-span service does not orient the footprint of {object}"
668 )))
669 }
670 fn measure_recesses(&self, object: &ObjectId) -> Result<PlanRecesses, PlanSpanError> {
674 Err(PlanSpanError::Unavailable(format!(
675 "this plan-span service does not measure the recesses of {object}"
676 )))
677 }
678 fn measure_section(&self, objects: &[ObjectId]) -> Result<PlanSection, PlanSpanError> {
682 Err(PlanSpanError::Unavailable(format!(
683 "this plan-span service does not measure the section of {} objects",
684 objects.len()
685 )))
686 }
687 fn measure_corridor_ends(
692 &self,
693 request: &CorridorEndRequest,
694 ) -> Result<CorridorEnds, PlanSpanError> {
695 Err(PlanSpanError::Unavailable(format!(
696 "this plan-span service does not find the corridor ends of {}",
697 request.space()
698 )))
699 }
700 fn measure_side_distances(
705 &self,
706 request: &SideDistanceRequest,
707 ) -> Result<SideDistances, PlanSpanError> {
708 Err(PlanSpanError::Unavailable(format!(
709 "this plan-span service does not measure what lies beside {}",
710 request.object()
711 )))
712 }
713}
714
715#[derive(Clone)]
717pub struct PlanSpanServiceHandle(Arc<dyn PlanSpanService>);
718
719impl PlanSpanServiceHandle {
720 #[must_use]
722 pub fn new(service: Arc<dyn PlanSpanService>) -> Self {
723 Self(service)
724 }
725
726 pub fn measure_diameter(&self, object: &ObjectId) -> Result<PlanLength, PlanSpanError> {
728 self.0.measure_diameter(object)
729 }
730
731 pub fn measure_span(
734 &self,
735 first: &ObjectId,
736 second: &ObjectId,
737 between: PlanSpan,
738 ) -> Result<PlanLength, PlanSpanError> {
739 if first == second {
740 return Err(PlanSpanError::Unavailable(format!(
741 "a span needs two objects, not {first} twice"
742 )));
743 }
744 self.0.measure_span(first, second, between)
745 }
746
747 pub fn measure_centre(&self, object: &ObjectId) -> Result<PlanCentre, PlanSpanError> {
750 let centre = self.0.measure_centre(object)?;
751 if centre.object() != object {
752 return Err(PlanSpanError::Unavailable(format!(
753 "a centre of {} was returned for {object}",
754 centre.object()
755 )));
756 }
757 Ok(centre)
758 }
759
760 pub fn measure_rectangle(&self, object: &ObjectId) -> Result<PlanRectangle, PlanSpanError> {
763 let rectangle = self.0.measure_rectangle(object)?;
764 if rectangle.object() != object {
765 return Err(PlanSpanError::Unavailable(format!(
766 "a rectangle of {} was returned for {object}",
767 rectangle.object()
768 )));
769 }
770 Ok(rectangle)
771 }
772
773 pub fn measure_recesses(&self, object: &ObjectId) -> Result<PlanRecesses, PlanSpanError> {
776 let recesses = self.0.measure_recesses(object)?;
777 if recesses.object() != object {
778 return Err(PlanSpanError::Unavailable(format!(
779 "the recesses of {} were returned for {object}",
780 recesses.object()
781 )));
782 }
783 Ok(recesses)
784 }
785
786 pub fn measure_section(&self, objects: &[ObjectId]) -> Result<PlanSection, PlanSpanError> {
789 let mut asked = objects.to_vec();
790 asked.sort();
791 asked.dedup();
792 if asked.is_empty() {
793 return Err(PlanSpanError::Unavailable(
794 "a section needs at least one object".into(),
795 ));
796 }
797 let section = self.0.measure_section(&asked)?;
798 let mut answered = section.objects().to_vec();
799 answered.sort();
800 answered.dedup();
801 if answered != asked {
802 return Err(PlanSpanError::Unavailable(
803 "a section of other objects was returned".into(),
804 ));
805 }
806 Ok(section)
807 }
808
809 pub fn measure_corridor_ends(
813 &self,
814 request: &CorridorEndRequest,
815 ) -> Result<CorridorEnds, PlanSpanError> {
816 corridor_end::measure(&self.0, request)
817 }
818
819 pub fn measure_side_distances(
822 &self,
823 request: &SideDistanceRequest,
824 ) -> Result<SideDistances, PlanSpanError> {
825 side_distance::measure(&self.0, request)
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use std::sync::Arc;
832
833 use super::{
834 CentrePlacement, PlanCentre, PlanLength, PlanRecess, PlanRecesses, PlanRectangle,
835 PlanSection, PlanSpan, PlanSpanError, PlanSpanService, PlanSpanServiceHandle,
836 RectangleOrientation,
837 };
838 use axioval_ir::{Evidence, ObjectId, SourceId};
839
840 fn id(local: &str) -> ObjectId {
841 ObjectId::new(SourceId::new("cad", "m").unwrap(), local).unwrap()
842 }
843
844 struct AlwaysA;
846
847 impl PlanSpanService for AlwaysA {
848 fn measure_diameter(&self, _: &ObjectId) -> Result<PlanLength, PlanSpanError> {
849 Err(PlanSpanError::Unavailable("unused".into()))
850 }
851 fn measure_span(
852 &self,
853 _: &ObjectId,
854 _: &ObjectId,
855 _: PlanSpan,
856 ) -> Result<PlanLength, PlanSpanError> {
857 Err(PlanSpanError::Unavailable("unused".into()))
858 }
859 fn measure_centre(&self, _: &ObjectId) -> Result<PlanCentre, PlanSpanError> {
860 PlanCentre::try_new(
861 id("a"),
862 [1.0, 2.0],
863 0.0,
864 CentrePlacement::Inside,
865 Evidence::exact(SourceId::new("cad", "m").unwrap(), "plan-centre:a"),
866 )
867 }
868 }
869
870 struct Silent;
872
873 impl PlanSpanService for Silent {
874 fn measure_diameter(&self, _: &ObjectId) -> Result<PlanLength, PlanSpanError> {
875 Err(PlanSpanError::Unavailable("unused".into()))
876 }
877 fn measure_span(
878 &self,
879 _: &ObjectId,
880 _: &ObjectId,
881 _: PlanSpan,
882 ) -> Result<PlanLength, PlanSpanError> {
883 Err(PlanSpanError::Unavailable("unused".into()))
884 }
885 }
886
887 fn rectangle(
890 half: [(f64, f64); 2],
891 turn: f64,
892 orientation: RectangleOrientation,
893 ) -> Result<PlanRectangle, PlanSpanError> {
894 #[allow(clippy::float_cmp)]
895 let exact = half.iter().all(|(low, high)| low == high)
896 && orientation == RectangleOrientation::Unique;
897 let (sin, cos) = turn.sin_cos();
898 PlanRectangle::try_new(
899 id("a"),
900 [0.0, 0.0],
901 0.0,
902 [[cos, sin], [-sin, cos]],
903 0.0,
904 half,
905 orientation,
906 Evidence {
907 source: SourceId::new("cad", "m").unwrap(),
908 locator: "plan-rectangle:a".into(),
909 exact,
910 },
911 )
912 }
913
914 #[test]
915 fn a_rectangle_has_a_long_axis_only_when_one_side_is_surely_longer() {
916 let unique = RectangleOrientation::Unique;
917 let bay = rectangle([(1.25, 1.25), (2.5, 2.5)], 0.0, unique).unwrap();
918 assert_eq!(bay.long_axis(), Ok(1));
919 assert_eq!(bay.width_and_length(), Ok([(2.5, 2.5), (5.0, 5.0)]));
920 let square = rectangle([(1.5, 1.5), (1.5, 1.5)], 0.0, unique).unwrap();
921 assert!(square.long_axis().is_err());
922 assert_eq!(square.width_and_length(), Ok([(3.0, 3.0), (3.0, 3.0)]));
923 let close = rectangle([(1.0, 1.2), (1.1, 1.3)], 0.0, unique).unwrap();
925 assert!(close.long_axis().is_err());
926 assert_eq!(close.width_and_length(), Ok([(2.0, 2.4), (2.2, 2.6)]));
927 for orientation in [RectangleOrientation::Tied, RectangleOrientation::Unproven] {
928 let other = rectangle([(1.25, 1.25), (2.5, 2.5)], 0.0, orientation).unwrap();
929 assert!(other.long_axis().is_err());
930 assert!(other.width_and_length().is_err());
931 }
932 }
933
934 #[test]
935 fn long_axes_meet_at_an_acute_angle_interval() {
936 let unique = RectangleOrientation::Unique;
937 let along = rectangle([(2.5, 2.5), (1.0, 1.0)], 0.0, unique).unwrap();
938 let turned = rectangle([(1.0, 1.0), (2.5, 2.5)], 30.0_f64.to_radians(), unique).unwrap();
939 let (low, high) = along.long_axis_angle(&turned).unwrap();
941 assert!(
942 low <= 60.0 && 60.0 <= high && high - low < 1e-6,
943 "{low} {high}"
944 );
945 let (low, _) = along.long_axis_angle(&along).unwrap();
946 assert!(low.abs() < 1e-12);
947 }
948
949 #[test]
950 fn a_rectangle_must_be_valid_and_honest_about_its_exactness() {
951 let unique = RectangleOrientation::Unique;
952 assert_eq!(
954 rectangle([(1.0, 1.0), (2.0, 2.0)], 100.0_f64.to_radians(), unique),
955 Err(PlanSpanError::InvalidMeasurement)
956 );
957 assert_eq!(
958 rectangle([(2.0, 1.0), (2.0, 2.0)], 0.0, unique),
959 Err(PlanSpanError::InvalidMeasurement)
960 );
961 let mut evidence = Evidence::exact(SourceId::new("cad", "m").unwrap(), "r");
962 evidence.exact = true;
963 assert_eq!(
964 PlanRectangle::try_new(
965 id("a"),
966 [0.0, 0.0],
967 0.0,
968 [[1.0, 0.0], [0.0, 1.0]],
969 0.0,
970 [(1.0, 1.0), (2.0, 2.0)],
971 RectangleOrientation::Unproven,
972 evidence,
973 ),
974 Err(PlanSpanError::InexactEvidence)
975 );
976 let handle = PlanSpanServiceHandle::new(Arc::new(Silent));
977 assert!(matches!(
978 handle.measure_rectangle(&id("a")),
979 Err(PlanSpanError::Unavailable(_))
980 ));
981 }
982
983 #[test]
984 fn recesses_and_sections_are_bound_to_their_request_and_refused_by_default() {
985 struct Wrong;
986 impl PlanSpanService for Wrong {
987 fn measure_diameter(&self, _: &ObjectId) -> Result<PlanLength, PlanSpanError> {
988 Err(PlanSpanError::Unavailable("unused".into()))
989 }
990 fn measure_span(
991 &self,
992 _: &ObjectId,
993 _: &ObjectId,
994 _: PlanSpan,
995 ) -> Result<PlanLength, PlanSpanError> {
996 Err(PlanSpanError::Unavailable("unused".into()))
997 }
998 fn measure_recesses(&self, _: &ObjectId) -> Result<PlanRecesses, PlanSpanError> {
999 PlanRecesses::try_new(id("a"), Vec::new(), exact())
1000 }
1001 fn measure_section(&self, _: &[ObjectId]) -> Result<PlanSection, PlanSpanError> {
1002 PlanSection::try_new(vec![id("a")], (0.0, 0.0), None, exact())
1003 }
1004 }
1005 let handle = PlanSpanServiceHandle::new(Arc::new(Wrong));
1006 assert!(handle.measure_recesses(&id("a")).is_ok());
1007 assert!(handle.measure_recesses(&id("b")).is_err());
1008 assert!(handle.measure_section(&[id("a"), id("a")]).is_ok());
1009 assert!(handle.measure_section(&[id("a"), id("b")]).is_err());
1010 assert!(handle.measure_section(&[]).is_err());
1011 let silent = PlanSpanServiceHandle::new(Arc::new(Silent));
1012 assert!(silent.measure_recesses(&id("a")).is_err());
1013 assert!(silent.measure_section(&[id("a")]).is_err());
1014 }
1015
1016 #[test]
1017 fn a_section_has_a_rectangle_exactly_when_it_is_not_empty() {
1018 let side = |value| PlanLength::try_new(value, value, exact()).unwrap();
1019 let section = |area: (f64, f64), sides| {
1020 PlanSection::try_new(vec![id("a")], area, sides, {
1021 let mut evidence = exact();
1022 #[allow(clippy::float_cmp)]
1023 {
1024 evidence.exact = area.0 == area.1;
1025 }
1026 evidence
1027 })
1028 };
1029 assert!(section((0.0, 0.0), None).is_ok());
1030 assert!(section((4.0, 4.0), Some((side(2.0), side(2.0)))).is_ok());
1031 assert!(section((1.0, 2.0), Some((side(1.0), side(2.0)))).is_ok());
1032 assert_eq!(
1033 section((4.0, 4.0), None),
1034 Err(PlanSpanError::InvalidMeasurement)
1035 );
1036 assert_eq!(
1037 section((0.0, 0.0), Some((side(1.0), side(1.0)))),
1038 Err(PlanSpanError::InvalidMeasurement)
1039 );
1040 assert_eq!(
1041 section((4.0, 4.0), Some((side(3.0), side(2.0)))),
1042 Err(PlanSpanError::InvalidMeasurement)
1043 );
1044 assert_eq!(
1045 PlanSection::try_new(vec![id("a")], (1.0, 2.0), None, exact()),
1046 Err(PlanSpanError::InexactEvidence)
1047 );
1048 assert_eq!(
1049 PlanRecess::try_new([[f64::NAN, 0.0], [1.0, 0.0]], side(1.0), side(1.0)),
1050 Err(PlanSpanError::InvalidMeasurement)
1051 );
1052 }
1053
1054 #[test]
1055 fn a_centre_is_bound_to_its_object_and_refused_by_default() {
1056 let handle = PlanSpanServiceHandle::new(Arc::new(AlwaysA));
1057 let centre = handle.measure_centre(&id("a")).unwrap();
1058 assert_eq!(centre.object(), &id("a"));
1059 assert_eq!(centre.placement(), CentrePlacement::Inside);
1060 assert!(matches!(
1061 handle.measure_centre(&id("b")),
1062 Err(PlanSpanError::Unavailable(_))
1063 ));
1064 let silent = PlanSpanServiceHandle::new(Arc::new(Silent));
1065 assert!(matches!(
1066 silent.measure_centre(&id("a")),
1067 Err(PlanSpanError::Unavailable(_))
1068 ));
1069 }
1070
1071 #[test]
1072 fn a_centre_is_exact_exactly_when_its_radius_is_zero() {
1073 let evidence = |exact| Evidence {
1074 source: SourceId::new("cad", "m").unwrap(),
1075 locator: "plan-centre:a".into(),
1076 exact,
1077 };
1078 let centre = |radius, exact| {
1079 PlanCentre::try_new(
1080 id("a"),
1081 [0.0, 0.0],
1082 radius,
1083 CentrePlacement::Undecided,
1084 evidence(exact),
1085 )
1086 };
1087 assert!(centre(0.0, true).is_ok());
1088 assert!(centre(0.1, false).is_ok());
1089 assert_eq!(centre(0.1, true), Err(PlanSpanError::InexactEvidence));
1090 assert_eq!(centre(0.0, false), Err(PlanSpanError::InexactEvidence));
1091 assert_eq!(centre(-0.1, false), Err(PlanSpanError::InvalidMeasurement));
1092 assert_eq!(
1093 PlanCentre::try_new(
1094 id("a"),
1095 [f64::NAN, 0.0],
1096 0.0,
1097 CentrePlacement::Inside,
1098 evidence(true)
1099 ),
1100 Err(PlanSpanError::InvalidMeasurement)
1101 );
1102 }
1103
1104 fn exact() -> Evidence {
1105 Evidence::exact(SourceId::new("cad", "m").unwrap(), "plan-diameter:a")
1106 }
1107
1108 #[test]
1109 fn exactness_and_bounds_must_agree() {
1110 assert!(PlanLength::try_new(2.0, 2.0, exact()).is_ok());
1111 assert_eq!(
1112 PlanLength::try_new(1.0, 2.0, exact()),
1113 Err(PlanSpanError::InexactEvidence)
1114 );
1115 let mut approximate = exact();
1116 approximate.exact = false;
1117 assert!(PlanLength::try_new(1.0, 2.0, approximate.clone()).is_ok());
1118 assert_eq!(
1119 PlanLength::try_new(2.0, 2.0, approximate),
1120 Err(PlanSpanError::InexactEvidence)
1121 );
1122 }
1123
1124 #[test]
1125 fn reversed_negative_or_non_finite_bounds_are_refused() {
1126 for (lower, upper) in [
1127 (2.0, 1.0),
1128 (-1.0, 1.0),
1129 (0.0, f64::NAN),
1130 (0.0, f64::INFINITY),
1131 ] {
1132 assert_eq!(
1133 PlanLength::try_new(lower, upper, exact()),
1134 Err(PlanSpanError::InvalidMeasurement),
1135 "{lower} {upper}"
1136 );
1137 }
1138 }
1139}