Skip to main content

axioval_engine/
corridor_end.rs

1//! Corridor ends: where the paths through a space's footprint end, which
2//! wall each end runs into, and how the requested subjects sit against it.
3//!
4//! ADR 0004: this seam measures. Whether a window in the end wall of a
5//! corridor is allowed is a rule's judgement over these measurements.
6//!
7//! A footprint's paths are its skeleton, an approximation of its medial axis:
8//! the ends, their positions and the direction each path runs in are not
9//! certified, so [`CorridorEnds`] always carries approximate evidence. What is
10//! certified is stated per part: every end lies inside the footprint, its
11//! clearance (distance to the nearest wall) is an interval sure to hold the
12//! true value, and a decided end wall is a segment of the footprint's own
13//! boundary. A service decides an end's wall only when the approximation
14//! cannot change which wall it is; otherwise the wall is
15//! [`EndWall::Undecided`] and a rule must not guess it.
16//!
17//! How a subject sits against a decided wall is measured on the footprints
18//! themselves, as [`PlanLength`] intervals exact exactly when a point: the
19//! plan gap between the subject's footprint and the wall segment, and the
20//! length of the segment the subject's footprint faces (the overlap of its
21//! projection onto the wall's line with the segment).
22
23use std::sync::Arc;
24
25use axioval_ir::{Evidence, ObjectId};
26
27use crate::plan_span::{PlanLength, PlanSpanError, PlanSpanService};
28
29/// The space whose corridor ends are asked for, and the subjects (such as
30/// its windows) to measure against each end wall.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct CorridorEndRequest {
33    space: ObjectId,
34    subjects: Vec<ObjectId>,
35}
36
37impl CorridorEndRequest {
38    /// A request for `space`'s corridor ends, measuring `subjects` against
39    /// each end wall. Subjects are sorted and deduplicated; the space
40    /// itself is refused as a subject.
41    pub fn try_new(
42        space: ObjectId,
43        subjects: impl IntoIterator<Item = ObjectId>,
44    ) -> Result<Self, PlanSpanError> {
45        let mut subjects: Vec<ObjectId> = subjects.into_iter().collect();
46        subjects.sort();
47        subjects.dedup();
48        if subjects.contains(&space) {
49            return Err(PlanSpanError::Unavailable(format!(
50                "{space} cannot be measured against its own corridor ends"
51            )));
52        }
53        Ok(Self { space, subjects })
54    }
55
56    /// The space.
57    #[must_use]
58    pub fn space(&self) -> &ObjectId {
59        &self.space
60    }
61
62    /// The subjects, sorted without repeats.
63    #[must_use]
64    pub fn subjects(&self) -> &[ObjectId] {
65        &self.subjects
66    }
67}
68
69/// How one subject sits against an end wall.
70#[derive(Clone, Debug, PartialEq)]
71pub struct WallContact {
72    subject: ObjectId,
73    gap: PlanLength,
74    facing: PlanLength,
75}
76
77impl WallContact {
78    /// `subject`'s footprint lies `gap` from the wall segment in plan and
79    /// faces `facing` of its length.
80    #[must_use]
81    pub fn new(subject: ObjectId, gap: PlanLength, facing: PlanLength) -> Self {
82        Self {
83            subject,
84            gap,
85            facing,
86        }
87    }
88
89    /// The subject.
90    #[must_use]
91    pub fn subject(&self) -> &ObjectId {
92        &self.subject
93    }
94
95    /// The plan distance between the subject's footprint and the wall
96    /// segment: zero where they meet.
97    #[must_use]
98    pub fn gap(&self) -> &PlanLength {
99        &self.gap
100    }
101
102    /// The length of the wall segment the subject's footprint faces: the
103    /// overlap of its projection onto the wall's line with the segment.
104    /// Zero for a subject beside the wall's end, such as a window in a side
105    /// wall next to the corner.
106    #[must_use]
107    pub fn facing(&self) -> &PlanLength {
108        &self.facing
109    }
110}
111
112/// The wall a corridor end runs into.
113#[derive(Clone, Debug, PartialEq)]
114pub enum EndWall {
115    /// A straight run of the footprint's boundary, from `start` to `end` in
116    /// canonical metres, and every requested subject measured against it in
117    /// the request's order.
118    Decided {
119        /// The segment's first point.
120        start: [f64; 2],
121        /// The segment's last point.
122        end: [f64; 2],
123        /// The requested subjects against this wall.
124        contacts: Vec<WallContact>,
125    },
126    /// The approximation could change which wall the end runs into, or the
127    /// path does not stop at one; the reason says which.
128    Undecided(String),
129}
130
131/// One end of a path through the footprint.
132#[derive(Clone, Debug, PartialEq)]
133pub struct CorridorEnd {
134    point: [f64; 2],
135    clearance: (f64, f64),
136    wall: EndWall,
137}
138
139impl CorridorEnd {
140    /// An end at `point` (inside the footprint), whose distance to the
141    /// nearest wall lies in `clearance`, running into `wall`.
142    pub fn try_new(
143        point: [f64; 2],
144        clearance: (f64, f64),
145        wall: EndWall,
146    ) -> Result<Self, PlanSpanError> {
147        let (lower, upper) = clearance;
148        if !point.iter().all(|value| value.is_finite())
149            || !lower.is_finite()
150            || !upper.is_finite()
151            || lower < 0.0
152            || lower > upper
153        {
154            return Err(PlanSpanError::InvalidMeasurement);
155        }
156        if let EndWall::Decided { start, end, .. } = &wall {
157            #[allow(clippy::float_cmp)]
158            let degenerate = start == end;
159            if degenerate || !start.iter().chain(end).all(|value| value.is_finite()) {
160                return Err(PlanSpanError::InvalidMeasurement);
161            }
162        }
163        Ok(Self {
164            point,
165            clearance,
166            wall,
167        })
168    }
169
170    /// Where the path ends, in canonical metres: inside the footprint, but
171    /// an approximation of where the true axis ends.
172    #[must_use]
173    pub fn point(&self) -> [f64; 2] {
174        self.point
175    }
176
177    /// Bounds `(lower, upper)` on the distance from [`Self::point`] to the
178    /// nearest wall, sure to hold the true value.
179    #[must_use]
180    pub fn clearance_metres(&self) -> (f64, f64) {
181        self.clearance
182    }
183
184    /// The wall the path runs into.
185    #[must_use]
186    pub fn wall(&self) -> &EndWall {
187        &self.wall
188    }
189}
190
191/// The corridor ends of a space's footprint.
192#[derive(Clone, Debug, PartialEq)]
193pub struct CorridorEnds {
194    space: ObjectId,
195    ends: Vec<CorridorEnd>,
196    evidence: Evidence,
197}
198
199impl CorridorEnds {
200    /// The ends of `space`'s footprint. The evidence is always approximate:
201    /// the skeleton the ends come from is not certified.
202    pub fn try_new(
203        space: ObjectId,
204        ends: Vec<CorridorEnd>,
205        evidence: Evidence,
206    ) -> Result<Self, PlanSpanError> {
207        if evidence.exact || evidence.locator.trim().is_empty() {
208            return Err(PlanSpanError::InexactEvidence);
209        }
210        Ok(Self {
211            space,
212            ends,
213            evidence,
214        })
215    }
216
217    /// The space whose ends these are.
218    #[must_use]
219    pub fn space(&self) -> &ObjectId {
220        &self.space
221    }
222
223    /// The ends, in the service's order.
224    #[must_use]
225    pub fn ends(&self) -> &[CorridorEnd] {
226        &self.ends
227    }
228
229    /// Reviewable provenance of the skeleton.
230    #[must_use]
231    pub fn evidence(&self) -> &Evidence {
232        &self.evidence
233    }
234}
235
236/// Checks that `ends` answers `request`: the same space, and every decided
237/// wall measured against exactly the requested subjects, in order.
238pub(crate) fn bound(
239    request: &CorridorEndRequest,
240    ends: CorridorEnds,
241) -> Result<CorridorEnds, PlanSpanError> {
242    if ends.space() != request.space() {
243        return Err(PlanSpanError::Unavailable(format!(
244            "corridor ends of {} were returned for {}",
245            ends.space(),
246            request.space()
247        )));
248    }
249    for end in ends.ends() {
250        if let EndWall::Decided { contacts, .. } = end.wall() {
251            let named = contacts.iter().map(WallContact::subject);
252            if !named.eq(request.subjects()) {
253                return Err(PlanSpanError::Unavailable(format!(
254                    "an end wall of {} was not measured against exactly the requested subjects",
255                    request.space()
256                )));
257            }
258        }
259    }
260    Ok(ends)
261}
262
263/// Asks `service` for `request`'s corridor ends and binds the answer to it.
264pub(crate) fn measure(
265    service: &Arc<dyn PlanSpanService>,
266    request: &CorridorEndRequest,
267) -> Result<CorridorEnds, PlanSpanError> {
268    bound(request, service.measure_corridor_ends(request)?)
269}
270
271#[cfg(test)]
272mod tests {
273    use axioval_ir::{Evidence, ObjectId, SourceId};
274
275    use super::{CorridorEnd, CorridorEndRequest, CorridorEnds, EndWall, WallContact, bound};
276    use crate::plan_span::{PlanLength, PlanSpanError};
277
278    fn source() -> SourceId {
279        SourceId::new("cad", "m").unwrap()
280    }
281
282    fn id(local: &str) -> ObjectId {
283        ObjectId::new(source(), local).unwrap()
284    }
285
286    fn approximate() -> Evidence {
287        Evidence {
288            source: source(),
289            locator: "corridor-ends:hall".into(),
290            exact: false,
291        }
292    }
293
294    fn zero() -> PlanLength {
295        PlanLength::try_new(0.0, 0.0, Evidence::exact(source(), "gap")).unwrap()
296    }
297
298    fn end(contacts: Vec<WallContact>) -> CorridorEnd {
299        CorridorEnd::try_new(
300            [0.5, 0.5],
301            (0.5, 0.5),
302            EndWall::Decided {
303                start: [0.0, 1.0],
304                end: [0.0, 0.0],
305                contacts,
306            },
307        )
308        .unwrap()
309    }
310
311    #[test]
312    fn a_request_sorts_its_subjects_and_refuses_the_space() {
313        let request =
314            CorridorEndRequest::try_new(id("hall"), [id("w2"), id("w1"), id("w2")]).unwrap();
315        assert_eq!(request.subjects(), [id("w1"), id("w2")]);
316        assert!(CorridorEndRequest::try_new(id("hall"), [id("hall")]).is_err());
317    }
318
319    #[test]
320    fn corridor_ends_are_never_exact() {
321        let exact = Evidence::exact(source(), "corridor-ends:hall");
322        assert_eq!(
323            CorridorEnds::try_new(id("hall"), Vec::new(), exact),
324            Err(PlanSpanError::InexactEvidence)
325        );
326        assert!(CorridorEnds::try_new(id("hall"), Vec::new(), approximate()).is_ok());
327    }
328
329    #[test]
330    fn an_end_refuses_bad_clearances_and_degenerate_walls() {
331        let wall = || EndWall::Undecided("unused".into());
332        assert!(CorridorEnd::try_new([0.0, 0.0], (0.5, 0.4), wall()).is_err());
333        assert!(CorridorEnd::try_new([0.0, 0.0], (-0.1, 0.4), wall()).is_err());
334        assert!(CorridorEnd::try_new([f64::NAN, 0.0], (0.1, 0.4), wall()).is_err());
335        assert!(
336            CorridorEnd::try_new(
337                [0.0, 0.0],
338                (0.1, 0.1),
339                EndWall::Decided {
340                    start: [1.0, 1.0],
341                    end: [1.0, 1.0],
342                    contacts: Vec::new()
343                }
344            )
345            .is_err()
346        );
347    }
348
349    #[test]
350    fn an_answer_is_bound_to_its_space_and_subjects() {
351        let request = CorridorEndRequest::try_new(id("hall"), [id("w1")]).unwrap();
352        let answer = |space: &str, contacts| {
353            CorridorEnds::try_new(id(space), vec![end(contacts)], approximate()).unwrap()
354        };
355        let contact = |subject: &str| WallContact::new(id(subject), zero(), zero());
356        assert!(bound(&request, answer("hall", vec![contact("w1")])).is_ok());
357        assert!(bound(&request, answer("room", vec![contact("w1")])).is_err());
358        assert!(bound(&request, answer("hall", vec![contact("w2")])).is_err());
359        assert!(bound(&request, answer("hall", Vec::new())).is_err());
360        assert!(bound(&request, answer("hall", vec![contact("w1"), contact("w1")])).is_err());
361    }
362}