Skip to main content

axioval_engine/
object_frame.rs

1//! Object frames: where an object is placed and, when its source says so,
2//! which way it faces.
3//!
4//! Clearance, placement and orientation checks are measured in an object's
5//! own frame: the area in front of a component, beside a fixture, along a
6//! bay. This seam supplies that frame as a [`MetricFrame`] in canonical
7//! metres, grounded on the requested object, together with the front the
8//! source states for it.
9//!
10//! The frame's axes are the source's placement axes, nothing more. Its
11//! `forward` axis is the placement's second axis and says nothing about which
12//! side of the object is its front. A front is reported only where the source
13//! states one ([`ObjectFront::Stated`]); otherwise it is
14//! [`ObjectFront::NotStated`], never guessed from the axes, the shape or the
15//! object's type.
16//!
17//! The same service supplies a door's leaves and a window's panels
18//! ([`ObjectFrameService::leaves`], see [`crate::DoorLeaves`]): they are
19//! placed in the door's or window's frame, so the source that places it
20//! states them. The method refuses by default.
21
22use std::sync::Arc;
23
24use axioval_ir::{Evidence, ObjectId, SourceId};
25use thiserror::Error;
26
27use crate::services::reviewable_exact_evidence;
28use crate::{
29    DoorLeaves, DoorLeavesError, MetricDirection, MetricFrame, SnapshotBoundService, SourceSnapshot,
30};
31
32/// Failure to supply an object's frame.
33#[derive(Clone, Debug, Error, PartialEq, Eq)]
34pub enum ObjectFrameError {
35    /// The service does not cover the object's source.
36    #[error("object-frame service does not cover source `{0}`")]
37    UncoveredSource(SourceId),
38    /// The object is not part of the source.
39    #[error("object `{0}` is not in the source")]
40    UnknownObject(ObjectId),
41    /// The source states no placement for the object, so it has no frame.
42    /// Never read as the source's origin.
43    #[error("the source states no placement for `{0}`")]
44    NotPlaced(ObjectId),
45    /// The object is placed by a construct the service cannot resolve
46    /// exactly.
47    #[error("object placement unsupported: {0}")]
48    Unsupported(String),
49    /// The placement, or the unit it is stated in, is malformed or cannot be
50    /// read exactly.
51    #[error("object placement cannot be read exactly: {0}")]
52    Unreadable(String),
53    /// The frame is not grounded on the object, or its axes are not a
54    /// right-handed orthonormal triple.
55    #[error("object frame is invalid")]
56    InvalidFrame,
57    /// The frame's evidence is not exact, not reviewable, or from another
58    /// source.
59    #[error("object-frame evidence is not exact and reviewable")]
60    InexactEvidence,
61    /// The service answered for another object.
62    #[error("object-frame service answered for another object")]
63    ResponseRequestMismatch,
64}
65
66/// Which way an object faces, as far as its source states it.
67#[derive(Clone, Copy, Debug, PartialEq)]
68pub enum ObjectFront {
69    /// The source states the object's front: the direction, in canonical
70    /// coordinates, from the object towards the side it is used from.
71    Stated(MetricDirection),
72    /// The source states no front. A rule that needs one cannot be decided
73    /// from this frame; the placement axes are not a substitute.
74    NotStated,
75}
76
77/// One object's placement frame and stated front, with provenance.
78#[derive(Clone, Debug, PartialEq)]
79pub struct ObjectFrame {
80    object: ObjectId,
81    frame: MetricFrame,
82    front: ObjectFront,
83    evidence: Evidence,
84}
85
86impl ObjectFrame {
87    /// The frame of `object`.
88    ///
89    /// The frame's origin must be grounded on `object`, and the evidence must
90    /// be exact, reviewable and from the object's source: a placement is
91    /// stated, never estimated.
92    pub fn try_new(
93        object: ObjectId,
94        frame: MetricFrame,
95        front: ObjectFront,
96        evidence: Evidence,
97    ) -> Result<Self, ObjectFrameError> {
98        if frame.origin().subject() != &object {
99            return Err(ObjectFrameError::InvalidFrame);
100        }
101        if !reviewable_exact_evidence(&evidence) || evidence.source != object.source {
102            return Err(ObjectFrameError::InexactEvidence);
103        }
104        Ok(Self {
105            object,
106            frame,
107            front,
108            evidence,
109        })
110    }
111
112    /// The object this frame belongs to.
113    #[must_use]
114    pub fn object(&self) -> &ObjectId {
115        &self.object
116    }
117
118    /// The placement frame: origin in canonical metres and right-handed
119    /// orthonormal right, forward and up axes.
120    #[must_use]
121    pub fn frame(&self) -> &MetricFrame {
122        &self.frame
123    }
124
125    /// The front the source states, or [`ObjectFront::NotStated`].
126    #[must_use]
127    pub fn front(&self) -> ObjectFront {
128        self.front
129    }
130
131    /// Reviewable provenance of the placement.
132    #[must_use]
133    pub fn evidence(&self) -> &Evidence {
134        &self.evidence
135    }
136}
137
138/// Trusted adapter seam supplying objects' placement frames.
139pub trait ObjectFrameService: Send + Sync + 'static {
140    /// Exact source snapshots this service answers for.
141    fn source_snapshots(&self) -> &[SourceSnapshot];
142    /// The placement frame of `object`, or why it has none.
143    fn object_frame(&self, object: &ObjectId) -> Result<ObjectFrame, ObjectFrameError>;
144    /// The leaves of `door`, a door or a window: each leaf's (or window
145    /// panel's) closed position, width, motion, hinge side and swing
146    /// sector, and a window panel's height and tilt sector.
147    ///
148    /// The default refuses with [`DoorLeavesError::Unsupported`], so a
149    /// service that reads no door operation fails closed.
150    fn leaves(&self, door: &ObjectId) -> Result<DoorLeaves, DoorLeavesError> {
151        let _ = door;
152        Err(DoorLeavesError::Unsupported)
153    }
154}
155
156/// Registry handle for an [`ObjectFrameService`].
157#[derive(Clone)]
158pub struct ObjectFrameServiceHandle(Arc<dyn ObjectFrameService>);
159
160impl ObjectFrameServiceHandle {
161    /// Wraps a trusted object-frame service.
162    #[must_use]
163    pub fn new(service: Arc<dyn ObjectFrameService>) -> Self {
164        Self(service)
165    }
166
167    /// The frame of `object`. Objects of an uncovered source are refused,
168    /// and so is a frame of another object.
169    pub fn object_frame(&self, object: &ObjectId) -> Result<ObjectFrame, ObjectFrameError> {
170        if !self
171            .0
172            .source_snapshots()
173            .iter()
174            .any(|snapshot| *snapshot.source() == object.source)
175        {
176            return Err(ObjectFrameError::UncoveredSource(object.source.clone()));
177        }
178        let frame = self.0.object_frame(object)?;
179        if frame.object() != object {
180            return Err(ObjectFrameError::ResponseRequestMismatch);
181        }
182        Ok(frame)
183    }
184
185    /// The leaves of `door`, a door or a window. Objects of an uncovered
186    /// source are refused, and so are leaves of another object.
187    pub fn leaves(&self, door: &ObjectId) -> Result<DoorLeaves, DoorLeavesError> {
188        if !self
189            .0
190            .source_snapshots()
191            .iter()
192            .any(|snapshot| *snapshot.source() == door.source)
193        {
194            return Err(DoorLeavesError::UncoveredSource(door.source.clone()));
195        }
196        let leaves = self.0.leaves(door)?;
197        if leaves.door() != door {
198            return Err(DoorLeavesError::ResponseRequestMismatch);
199        }
200        Ok(leaves)
201    }
202}
203
204impl SnapshotBoundService for ObjectFrameServiceHandle {
205    fn source_snapshots(&self) -> &[SourceSnapshot] {
206        self.0.source_snapshots()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::MetricPoint;
214
215    fn source() -> SourceId {
216        SourceId::new("cad", "m").unwrap()
217    }
218
219    fn id(local: &str) -> ObjectId {
220        ObjectId::new(source(), local).unwrap()
221    }
222
223    fn direction(vector: [f64; 3]) -> MetricDirection {
224        MetricDirection::try_new(vector).unwrap()
225    }
226
227    fn frame(subject: &str) -> MetricFrame {
228        MetricFrame::try_new(
229            MetricPoint::try_new(id(subject), [1.0, 2.0, 0.0]).unwrap(),
230            direction([1.0, 0.0, 0.0]),
231            direction([0.0, 1.0, 0.0]),
232            direction([0.0, 0.0, 1.0]),
233        )
234        .unwrap()
235    }
236
237    fn exact() -> Evidence {
238        Evidence::exact(source(), "placement:a")
239    }
240
241    #[test]
242    fn a_frame_is_grounded_on_its_object() {
243        assert!(ObjectFrame::try_new(id("a"), frame("a"), ObjectFront::NotStated, exact()).is_ok());
244        assert_eq!(
245            ObjectFrame::try_new(id("a"), frame("b"), ObjectFront::NotStated, exact()),
246            Err(ObjectFrameError::InvalidFrame)
247        );
248    }
249
250    #[test]
251    fn a_frame_needs_exact_reviewable_evidence_from_its_source() {
252        let mut approximate = exact();
253        approximate.exact = false;
254        let unlocated = Evidence::exact(source(), " ");
255        let foreign = Evidence::exact(SourceId::new("cad", "other").unwrap(), "placement:a");
256        for evidence in [approximate, unlocated, foreign] {
257            assert_eq!(
258                ObjectFrame::try_new(id("a"), frame("a"), ObjectFront::NotStated, evidence),
259                Err(ObjectFrameError::InexactEvidence)
260            );
261        }
262    }
263
264    #[test]
265    fn a_left_handed_frame_cannot_be_formed() {
266        let mirrored = MetricFrame::try_new(
267            MetricPoint::try_new(id("a"), [0.0; 3]).unwrap(),
268            direction([-1.0, 0.0, 0.0]),
269            direction([0.0, 1.0, 0.0]),
270            direction([0.0, 0.0, 1.0]),
271        );
272        assert!(mirrored.is_err());
273    }
274
275    struct Fixed(Vec<SourceSnapshot>, ObjectFrame);
276    impl ObjectFrameService for Fixed {
277        fn source_snapshots(&self) -> &[SourceSnapshot] {
278            &self.0
279        }
280        fn object_frame(&self, _: &ObjectId) -> Result<ObjectFrame, ObjectFrameError> {
281            Ok(self.1.clone())
282        }
283    }
284
285    fn handle() -> ObjectFrameServiceHandle {
286        let snapshot = SourceSnapshot::try_new(source(), "r1", "sha256:1").unwrap();
287        let answer = ObjectFrame::try_new(
288            id("b"),
289            frame("b"),
290            ObjectFront::Stated(direction([0.0, -1.0, 0.0])),
291            exact(),
292        )
293        .unwrap();
294        ObjectFrameServiceHandle::new(Arc::new(Fixed(vec![snapshot], answer)))
295    }
296
297    #[test]
298    fn the_handle_binds_answers_to_the_request() {
299        let handle = handle();
300        let frame = handle.object_frame(&id("b")).unwrap();
301        assert_eq!(
302            frame.front(),
303            ObjectFront::Stated(direction([0.0, -1.0, 0.0]))
304        );
305        assert_eq!(
306            handle.object_frame(&id("a")),
307            Err(ObjectFrameError::ResponseRequestMismatch)
308        );
309        let foreign = ObjectId::new(SourceId::new("cad", "other").unwrap(), "b").unwrap();
310        assert!(matches!(
311            handle.object_frame(&foreign),
312            Err(ObjectFrameError::UncoveredSource(_))
313        ));
314    }
315
316    #[test]
317    fn door_leaves_are_refused_by_default() {
318        let handle = handle();
319        assert_eq!(handle.leaves(&id("b")), Err(DoorLeavesError::Unsupported));
320        let foreign = ObjectId::new(SourceId::new("cad", "other").unwrap(), "b").unwrap();
321        assert!(matches!(
322            handle.leaves(&foreign),
323            Err(DoorLeavesError::UncoveredSource(_))
324        ));
325    }
326}