Skip to main content

axioval_engine/
plan_span.rs

1//! Plan spans: how far an object's footprint reaches across itself, how far
2//! apart two footprints lie between their centres or their farthest points,
3//! and where a footprint's centre lies.
4//!
5//! ADR 0004: this seam measures. Whether a room's exits are far enough apart
6//! for its size is a rule's judgement over these measurements. The closest
7//! distance between two footprints is the proximity service's `horizontal`
8//! projection; this seam does not repeat it.
9//!
10//! It also owns the rectangle of least area enclosing a footprint, which
11//! gives a footprint its own axes: a parking bay's length along the bay, a
12//! wall's direction. The rectangle says how well its orientation is known;
13//! a square has no long axis, and a tessellated footprint no proven one.
14//!
15//! It measures the recesses of a footprint (the pockets between it and its
16//! convex hull) and the section several footprints share, such as the
17//! clear shaft of a light well. A section's width and length are the sides
18//! of the same least-area rectangle, and only when its orientation is
19//! unique: another rectangle of least area may have other sides.
20//!
21//! A length is an interval. A mesh that is the object's exact shape measures
22//! exactly; one that approximates curved faces measures within a bound the
23//! adapter derives from its declared chord deviation, and a rule must decide
24//! from the whole interval, never from its midpoint.
25
26use 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/// Failure to measure a plan span.
35#[derive(Clone, Debug, Error, PartialEq, Eq)]
36pub enum PlanSpanError {
37    /// The service holds no geometry for this object.
38    #[error("no geometry for `{0}`")]
39    UnknownObject(ObjectId),
40    /// The geometry could not be measured, for example an object without a
41    /// footprint or an overlay that cannot be computed.
42    #[error("plan span unavailable: {0}")]
43    Unavailable(String),
44    /// A measurement is non-finite, negative, or its bounds are reversed.
45    #[error("plan span measurement is invalid")]
46    InvalidMeasurement,
47    /// An interval was reported as exact, or a point as inexact.
48    #[error("plan span evidence does not match its exactness")]
49    InexactEvidence,
50}
51
52/// Between which points of two footprints a span is measured.
53#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub enum PlanSpan {
55    /// Between the footprints' centroids: the centres of their plan areas.
56    Centres,
57    /// Between the two points, one in each footprint, farthest apart.
58    Farthest,
59}
60
61impl PlanSpan {
62    /// The span's stable name, as evidence locators cite it.
63    #[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/// A measured plan length in metres, with its evidence.
73#[derive(Clone, Debug, PartialEq)]
74pub struct PlanLength {
75    lower: f64,
76    upper: f64,
77    evidence: Evidence,
78}
79
80impl PlanLength {
81    /// A length known to lie in `[lower, upper]`.
82    ///
83    /// The evidence is exact exactly when the bounds coincide: an interval
84    /// cannot be exact evidence, and a point cannot be approximate.
85    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    /// Shortest the length can be, in metres.
102    #[must_use]
103    pub fn lower_metres(&self) -> f64 {
104        self.lower
105    }
106
107    /// Longest the length can be, in metres.
108    #[must_use]
109    pub fn upper_metres(&self) -> f64 {
110        self.upper
111    }
112
113    /// Whether the length is known exactly.
114    #[must_use]
115    pub fn is_exact(&self) -> bool {
116        self.evidence.exact
117    }
118
119    /// Reviewable provenance of the measurement.
120    #[must_use]
121    pub fn evidence(&self) -> &Evidence {
122        &self.evidence
123    }
124}
125
126/// Where a footprint's centre lies with respect to the footprint itself.
127#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
128pub enum CentrePlacement {
129    /// Every point the centre may be lies inside the footprint, off its
130    /// boundary.
131    Inside,
132    /// Every point the centre may be lies outside the footprint, as for an
133    /// L- or U-shaped room.
134    Outside,
135    /// The centre lies on the boundary, or close enough to it that the
136    /// measurement's uncertainty could put it on either side.
137    Undecided,
138}
139
140/// The centroid of an object's footprint: a plan point, how far the true
141/// centroid can lie from it, and whether it lies inside the footprint.
142#[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    /// The centre of `object`'s footprint, known to lie within `radius`
153    /// metres of `point`.
154    ///
155    /// The evidence is exact exactly when the radius is zero.
156    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    /// The object whose footprint this is the centre of.
181    #[must_use]
182    pub fn object(&self) -> &ObjectId {
183        &self.object
184    }
185
186    /// The measured centre, in canonical metres.
187    #[must_use]
188    pub fn point(&self) -> [f64; 2] {
189        self.point
190    }
191
192    /// How far, in metres, the true centre can lie from [`Self::point`].
193    #[must_use]
194    pub fn radius_metres(&self) -> f64 {
195        self.radius
196    }
197
198    /// Whether the centre lies inside the footprint.
199    #[must_use]
200    pub fn placement(&self) -> CentrePlacement {
201        self.placement
202    }
203
204    /// Whether the centre is known exactly.
205    #[must_use]
206    pub fn is_exact(&self) -> bool {
207        self.evidence.exact
208    }
209
210    /// Reviewable provenance of the measurement.
211    #[must_use]
212    pub fn evidence(&self) -> &Evidence {
213        &self.evidence
214    }
215}
216
217/// How well the orientation of a [`PlanRectangle`] is known.
218#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
219pub enum RectangleOrientation {
220    /// Exactly one orientation encloses the footprint with the least area,
221    /// up to quarter turns, and the rectangle has it: its axes are the
222    /// footprint's own.
223    Unique,
224    /// Several orientations share the least area. The rectangle is one of
225    /// them; another may have other sides, so neither its axes nor its
226    /// sides are the footprint's own.
227    Tied,
228    /// The mesh approximates the object's shape, so which orientation
229    /// encloses the true footprint with the least area is not known. The
230    /// half extents still bound the true footprint along these axes.
231    Unproven,
232}
233
234impl RectangleOrientation {
235    /// The orientation's stable name, as evidence locators cite it.
236    #[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/// The rectangle of least area enclosing an object's footprint, oriented
247/// in plan: a centre, two unit axes and the half extents along them.
248///
249/// Every value is bounded: the true centre lies within
250/// [`Self::centre_radius_metres`] of [`Self::centre`], each true axis within
251/// [`Self::axis_error_radians`] of the stated one, and each true half
252/// extent inside its interval. The evidence is exact exactly when every
253/// bound is a point and the orientation is [`RectangleOrientation::Unique`].
254#[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
266/// How far a stated axis may stray from unit length and from a quarter
267/// turn of the other: the rounding of a normalised vector, with room.
268const AXIS_ROUNDING: f64 = 1e-9;
269
270impl PlanRectangle {
271    /// The rectangle enclosing `object`'s footprint.
272    ///
273    /// `axes` are unit vectors, the second the first turned a quarter
274    /// counter-clockwise, the first pointing into `[0, 90)` degrees, so one
275    /// rectangle has one spelling. `half_extents` are `(lower, upper)`
276    /// intervals along the axes in order.
277    #[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    /// The object whose footprint the rectangle encloses.
330    #[must_use]
331    pub fn object(&self) -> &ObjectId {
332        &self.object
333    }
334
335    /// The measured centre, in canonical metres.
336    #[must_use]
337    pub fn centre(&self) -> [f64; 2] {
338        self.centre
339    }
340
341    /// How far, in metres, the true centre can lie from [`Self::centre`].
342    #[must_use]
343    pub fn centre_radius_metres(&self) -> f64 {
344        self.centre_radius
345    }
346
347    /// The two unit axes, the second the first turned a quarter
348    /// counter-clockwise.
349    #[must_use]
350    pub fn axes(&self) -> [[f64; 2]; 2] {
351        self.axes
352    }
353
354    /// How far, in radians, each true axis can be turned from the stated
355    /// one.
356    #[must_use]
357    pub fn axis_error_radians(&self) -> f64 {
358        self.axis_error
359    }
360
361    /// The half extents along the two axes, each `(lower, upper)` metres.
362    #[must_use]
363    pub fn half_extents_metres(&self) -> [(f64, f64); 2] {
364        self.half_extents
365    }
366
367    /// How well the orientation is known.
368    #[must_use]
369    pub fn orientation(&self) -> RectangleOrientation {
370        self.orientation
371    }
372
373    /// Whether the rectangle is known exactly.
374    #[must_use]
375    pub fn is_exact(&self) -> bool {
376        self.evidence.exact
377    }
378
379    /// Reviewable provenance of the measurement.
380    #[must_use]
381    pub fn evidence(&self) -> &Evidence {
382        &self.evidence
383    }
384
385    /// Why the axes are not the footprint's own, or `None` when they are.
386    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    /// The footprint's width and length along its own axes: the shorter and
402    /// the longer side, each `(lower, upper)` metres.
403    ///
404    /// Refused unless the orientation is [`RectangleOrientation::Unique`]:
405    /// another rectangle of least area may have other sides. Near-equal
406    /// sides are fine; which of them is the longer does not matter here.
407    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    /// The index of the longer axis, when the orientation is unique and one
419    /// side is surely longer than the other. A square, or a rectangle whose
420    /// sides the measurement cannot order, has no long axis.
421    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    /// The acute angle between this footprint's long axis and `other`'s, in
439    /// degrees within `[0, 90]`, as `(lower, upper)` sure to hold the angle
440    /// between the true axes.
441    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        // The rounding of the products and the arctangent, well inside a
448        // micro-degree, plus how far either axis may be turned.
449        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/// One recess of a footprint: a pocket between the footprint and its convex
455/// hull, closed by one hull edge, its mouth.
456///
457/// The width is the mouth's length; the depth the farthest the pocket
458/// reaches from the mouth's line. Both are [`PlanLength`]s, so a rule judges
459/// them as intervals.
460#[derive(Clone, Debug, PartialEq)]
461pub struct PlanRecess {
462    mouth: [[f64; 2]; 2],
463    width: PlanLength,
464    depth: PlanLength,
465}
466
467impl PlanRecess {
468    /// A recess whose mouth runs from `mouth[0]` to `mouth[1]`.
469    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    /// The mouth's ends, in canonical metres, as a reviewer locates the
485    /// recess.
486    #[must_use]
487    pub fn mouth(&self) -> [[f64; 2]; 2] {
488        self.mouth
489    }
490
491    /// The width of the mouth.
492    #[must_use]
493    pub fn width(&self) -> &PlanLength {
494        &self.width
495    }
496
497    /// How deep the recess reaches behind its mouth.
498    #[must_use]
499    pub fn depth(&self) -> &PlanLength {
500        &self.depth
501    }
502}
503
504/// Every recess of one object's footprint.
505#[derive(Clone, Debug, PartialEq)]
506pub struct PlanRecesses {
507    object: ObjectId,
508    recesses: Vec<PlanRecess>,
509    evidence: Evidence,
510}
511
512impl PlanRecesses {
513    /// The recesses of `object`, in the order the adapter walks its
514    /// boundary; none for a convex footprint.
515    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    /// The object whose footprint was measured.
531    #[must_use]
532    pub fn object(&self) -> &ObjectId {
533        &self.object
534    }
535
536    /// The recesses found.
537    #[must_use]
538    pub fn recesses(&self) -> &[PlanRecess] {
539        &self.recesses
540    }
541
542    /// Reviewable provenance of the measurement.
543    #[must_use]
544    pub fn evidence(&self) -> &Evidence {
545        &self.evidence
546    }
547}
548
549/// The plan section several objects share: the intersection of their
550/// footprints, such as the clear shaft of spaces stacked into a light well.
551#[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    /// The section of `objects`, with an area in `[lower, upper]` square
561    /// metres and, unless it is empty, the short and long sides of its
562    /// least-area enclosing rectangle, the one [`PlanRectangle`] describes
563    /// for a footprint. A service states sides only for a unique
564    /// orientation ([`RectangleOrientation::Unique`]) and refuses a section
565    /// whose least-area rectangles may have other sides.
566    ///
567    /// The evidence is exact exactly when the area is a point. An empty
568    /// section (upper bound zero) has no rectangle, and any other has one.
569    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    /// The objects whose footprints were intersected.
601    #[must_use]
602    pub fn objects(&self) -> &[ObjectId] {
603        &self.objects
604    }
605
606    /// Smallest the section's area can be, in square metres.
607    #[must_use]
608    pub fn area_lower(&self) -> f64 {
609        self.area.0
610    }
611
612    /// Largest the section's area can be, in square metres.
613    #[must_use]
614    pub fn area_upper(&self) -> f64 {
615        self.area.1
616    }
617
618    /// The short side of the section's minimum-area enclosing rectangle,
619    /// its width; `None` for an empty section.
620    #[must_use]
621    pub fn width(&self) -> Option<&PlanLength> {
622        self.sides.as_ref().map(|(short, _)| short)
623    }
624
625    /// The long side of the section's minimum-area enclosing rectangle;
626    /// `None` for an empty section.
627    #[must_use]
628    pub fn length(&self) -> Option<&PlanLength> {
629        self.sides.as_ref().map(|(_, long)| long)
630    }
631
632    /// Reviewable provenance of the measurement.
633    #[must_use]
634    pub fn evidence(&self) -> &Evidence {
635        &self.evidence
636    }
637}
638
639/// Measures plan spans of model objects.
640pub trait PlanSpanService: Send + Sync + 'static {
641    /// The longest distance between two points of `object`'s footprint: its
642    /// longest plan diagonal. A footprint with no point has none, and is
643    /// refused, never zero.
644    fn measure_diameter(&self, object: &ObjectId) -> Result<PlanLength, PlanSpanError>;
645    /// The plan distance between the footprints of `first` and `second`,
646    /// measured `between` their centres or their farthest points.
647    fn measure_span(
648        &self,
649        first: &ObjectId,
650        second: &ObjectId,
651        between: PlanSpan,
652    ) -> Result<PlanLength, PlanSpanError>;
653    /// The centroid of `object`'s footprint, the same centre
654    /// [`PlanSpan::Centres`] measures between, and whether it lies inside
655    /// the footprint. A service that does not locate centres refuses by
656    /// default, never answering with another point.
657    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    /// The rectangle of least area enclosing `object`'s footprint. A
663    /// service that does not orient footprints refuses by default, never
664    /// answering with the footprint's axis-aligned box.
665    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    /// The recesses of `object`'s footprint: the pockets between its outer
671    /// boundary and its convex hull. A service that does not find recesses
672    /// refuses by default, never answering that there are none.
673    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    /// The plan section `objects` share: the intersection of their
679    /// footprints, its area and its minimum-area rectangle. A service that
680    /// does not intersect footprints refuses by default.
681    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    /// The ends of the paths through the request's space footprint, the
688    /// wall each runs into, and the request's subjects measured against
689    /// every decided wall (see [`CorridorEnds`]). A service that does not
690    /// find corridor ends refuses by default, never answering with none.
691    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    /// The request's candidates beside each side of its object's
701    /// least-area rectangle, and how far from its centre lines (see
702    /// [`SideDistances`]). A service that does not measure them refuses by
703    /// default, never answering with none.
704    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/// Registry handle for a [`PlanSpanService`].
716#[derive(Clone)]
717pub struct PlanSpanServiceHandle(Arc<dyn PlanSpanService>);
718
719impl PlanSpanServiceHandle {
720    /// Wraps a trusted plan-span service.
721    #[must_use]
722    pub fn new(service: Arc<dyn PlanSpanService>) -> Self {
723        Self(service)
724    }
725
726    /// The longest plan diagonal of `object`'s footprint.
727    pub fn measure_diameter(&self, object: &ObjectId) -> Result<PlanLength, PlanSpanError> {
728        self.0.measure_diameter(object)
729    }
730
731    /// The span between two footprints; one object twice is refused rather
732    /// than measured against itself.
733    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    /// The centre of `object`'s footprint; a centre naming another object
748    /// is refused.
749    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    /// The least-area rectangle enclosing `object`'s footprint; a rectangle
761    /// naming another object is refused.
762    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    /// The recesses of `object`'s footprint; recesses naming another object
774    /// are refused.
775    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    /// The section `objects` share. No object is refused, and so is a
787    /// section naming other objects than those asked for.
788    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    /// The corridor ends of the request's space; an answer about another
810    /// space, or a wall not measured against exactly the requested subjects
811    /// in order, is refused.
812    pub fn measure_corridor_ends(
813        &self,
814        request: &CorridorEndRequest,
815    ) -> Result<CorridorEnds, PlanSpanError> {
816        corridor_end::measure(&self.0, request)
817    }
818
819    /// The candidates beside each side of the request's object; an answer
820    /// to another request is refused.
821    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    /// Answers every centre about object `a`, and nothing else.
845    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    /// Measures nothing, and locates no centre.
871    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    /// A rectangle of `a` with half extents `half` along the axes, turned
888    /// `turn` radians from the x-axis.
889    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        // Sides whose intervals overlap cannot be ordered.
924        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        // The turned one's long axis is its second: 120 degrees, so 60.
940        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        // A first axis outside [0, 90) degrees has another spelling.
953        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}