Skip to main content

ifc_alignment/view/
hierarchy.rs

1//! The public `IfcAlignment` hierarchy.
2//!
3//! IFC4X3 ADD2 relates an alignment to its parts through three
4//! relationships, and this module is the one place that walks them:
5//!
6//! - `IfcRelNests` (ordered) from the alignment to its horizontal, vertical
7//!   and cant layouts ("Alignment Layouts") and to its `IfcReferent`s
8//!   ("Object Nesting": the first nested referent is the start station);
9//! - `IfcRelAggregates` from a parent alignment to child alignments that
10//!   reuse its horizontal layout ("Alignment Layout - Reusing Horizontal
11//!   Layout");
12//! - `IfcRelPositions` from the alignment, as an `IfcPositioningElement`,
13//!   to the products it positions.
14//!
15//! The view only reports what the file states. Choosing one layout out of
16//! several is a separate, explicit step ([`AlignmentHierarchy::sole_vertical`]
17//! and siblings), because silently picking one would describe a road other
18//! than the one the caller meant.
19
20use std::collections::HashSet;
21
22use ifc_model::{EntityId, Model};
23
24use super::{AlignmentView, RelationSlots};
25use crate::error::{AlignmentError, AlignmentResult};
26
27/// What one `IfcAlignment` is made of, as stated by its relationships.
28///
29/// Every list keeps the order the file states: nesting order for layouts
30/// and referents, relationship order for children and positioned products.
31/// A list may be empty; how many layouts of a kind an alignment may have is
32/// a question the `sole_*` methods answer with a typed refusal.
33#[derive(Debug, Clone, PartialEq, Eq)]
34#[non_exhaustive]
35pub struct AlignmentHierarchy {
36    /// The `IfcAlignment` described.
37    pub alignment: EntityId,
38    /// The `IfcAlignment` that aggregates this one, when it is a child.
39    /// `None` for an alignment aggregated directly under the project (or
40    /// not aggregated at all).
41    pub parent: Option<EntityId>,
42    /// Nested `IfcAlignmentHorizontal` layouts.
43    pub horizontal: Vec<EntityId>,
44    /// Nested `IfcAlignmentVertical` layouts.
45    pub vertical: Vec<EntityId>,
46    /// Nested `IfcAlignmentCant` layouts.
47    pub cant: Vec<EntityId>,
48    /// Nested `IfcReferent`s, in nesting order.
49    pub referents: Vec<EntityId>,
50    /// Child `IfcAlignment`s aggregated under this one.
51    pub children: Vec<EntityId>,
52    /// Products this alignment positions through `IfcRelPositions`.
53    pub positioned: Vec<EntityId>,
54}
55
56impl AlignmentHierarchy {
57    /// The alignment's own horizontal layout: `None` when it nests none
58    /// (a child reusing its parent's, see
59    /// [`AlignmentView::governing_horizontal`]).
60    ///
61    /// # Errors
62    ///
63    /// [`AlignmentError::SemanticViolation`] when it nests several.
64    pub fn sole_horizontal(&self) -> AlignmentResult<Option<EntityId>> {
65        sole(
66            self.alignment,
67            &self.horizontal,
68            "an alignment nesting several horizontal layouts is ambiguous",
69        )
70    }
71
72    /// The alignment's vertical layout, `None` when it nests none.
73    ///
74    /// # Errors
75    ///
76    /// [`AlignmentError::SemanticViolation`] when it nests several.
77    pub fn sole_vertical(&self) -> AlignmentResult<Option<EntityId>> {
78        sole(
79            self.alignment,
80            &self.vertical,
81            "an alignment nesting several vertical layouts is ambiguous",
82        )
83    }
84
85    /// The alignment's cant layout, `None` when it nests none.
86    ///
87    /// # Errors
88    ///
89    /// [`AlignmentError::SemanticViolation`] when it nests several.
90    pub fn sole_cant(&self) -> AlignmentResult<Option<EntityId>> {
91        sole(
92            self.alignment,
93            &self.cant,
94            "an alignment nesting several cant layouts is ambiguous",
95        )
96    }
97}
98
99fn sole(
100    alignment: EntityId,
101    layouts: &[EntityId],
102    rule: &'static str,
103) -> AlignmentResult<Option<EntityId>> {
104    match layouts {
105        [] => Ok(None),
106        [only] => Ok(Some(*only)),
107        _ => Err(AlignmentError::SemanticViolation {
108            entity: Some(alignment),
109            rule,
110        }),
111    }
112}
113
114/// How many aggregation levels [`AlignmentView::governing_horizontal`] climbs
115/// before refusing. Real files nest one level; the bound only exists so a
116/// malformed file cannot make the walk unbounded.
117const MAX_PARENT_DEPTH: usize = 64;
118
119impl<'m> AlignmentView<'m> {
120    /// The model this view reads.
121    #[must_use]
122    pub fn model(&self) -> &'m Model {
123        self.model
124    }
125
126    /// Every `IfcAlignment` in the model, in file order.
127    #[must_use]
128    pub fn alignments(&self) -> Vec<EntityId> {
129        self.ids_of_ancestor("IfcAlignment")
130    }
131
132    /// The hierarchy of one `IfcAlignment`.
133    ///
134    /// # Errors
135    ///
136    /// Refuses an id that is missing or not an `IfcAlignment`
137    /// ([`AlignmentError::WrongType`]); a malformed or dangling relationship;
138    /// the same object nested twice; and an alignment aggregated by more
139    /// than one relationship (`Decomposes` is `SET [0:1]`).
140    pub fn hierarchy(&self, alignment: EntityId) -> AlignmentResult<AlignmentHierarchy> {
141        self.require(alignment, "IfcAlignment")?;
142        let nested = self.related_by("IfcRelNests", RelationSlots::DECOMPOSES, alignment)?;
143        let mut seen = HashSet::with_capacity(nested.len());
144        if let Some(twice) = nested.iter().find(|id| !seen.insert(**id)) {
145            return Err(AlignmentError::SemanticViolation {
146                entity: Some(*twice),
147                rule: "IfcRelNests must not list the same object twice",
148            });
149        }
150        let aggregated =
151            self.related_by("IfcRelAggregates", RelationSlots::DECOMPOSES, alignment)?;
152        let positioned = self.related_by("IfcRelPositions", RelationSlots::POSITIONS, alignment)?;
153        Ok(AlignmentHierarchy {
154            alignment,
155            parent: self.parent_alignment(alignment)?,
156            horizontal: self.filter_is_a(nested.clone(), "IfcAlignmentHorizontal"),
157            vertical: self.filter_is_a(nested.clone(), "IfcAlignmentVertical"),
158            cant: self.filter_is_a(nested.clone(), "IfcAlignmentCant"),
159            referents: self.filter_is_a(nested, "IfcReferent"),
160            children: self.filter_is_a(aggregated, "IfcAlignment"),
161            positioned,
162        })
163    }
164
165    /// The `IfcAlignment` aggregating `alignment`, if any.
166    ///
167    /// # Errors
168    ///
169    /// As [`Self::hierarchy`].
170    pub fn parent_alignment(&self, alignment: EntityId) -> AlignmentResult<Option<EntityId>> {
171        self.require(alignment, "IfcAlignment")?;
172        let parents = self.relating_of("IfcRelAggregates", RelationSlots::DECOMPOSES, alignment)?;
173        match parents.as_slice() {
174            [] => Ok(None),
175            [only] => Ok(self
176                .model
177                .get(*only)
178                .filter(|entity| self.schema.is_a(&entity.type_name, "IfcAlignment"))
179                .map(|_| *only)),
180            _ => Err(AlignmentError::SemanticViolation {
181                entity: Some(alignment),
182                rule:
183                    "an object is aggregated by at most one IfcRelAggregates (Decomposes SET [0:1])",
184            }),
185        }
186    }
187
188    /// The horizontal layout that governs `alignment`: its own, or, for a
189    /// child alignment that nests none, the nearest ancestor's ("Alignment
190    /// Layout - Reusing Horizontal Layout"). `None` when no alignment on the
191    /// way up nests one.
192    ///
193    /// # Errors
194    ///
195    /// As [`Self::hierarchy`], plus several horizontal layouts on the
196    /// governing alignment and an aggregation cycle.
197    pub fn governing_horizontal(&self, alignment: EntityId) -> AlignmentResult<Option<EntityId>> {
198        let mut current = alignment;
199        let mut visited = HashSet::new();
200        for _ in 0..MAX_PARENT_DEPTH {
201            if !visited.insert(current) {
202                return Err(AlignmentError::SemanticViolation {
203                    entity: Some(current),
204                    rule: "IfcAlignment aggregation must not form a cycle",
205                });
206            }
207            let hierarchy = self.hierarchy(current)?;
208            if let Some(horizontal) = hierarchy.sole_horizontal()? {
209                return Ok(Some(horizontal));
210            }
211            match hierarchy.parent {
212                Some(parent) => current = parent,
213                None => return Ok(None),
214            }
215        }
216        Err(AlignmentError::BudgetExceeded {
217            max_depth: MAX_PARENT_DEPTH,
218            max_nodes: MAX_PARENT_DEPTH,
219        })
220    }
221
222    /// The parameter segments of one layout (`IfcAlignmentHorizontal`,
223    /// `IfcAlignmentVertical` or `IfcAlignmentCant`), in nesting order.
224    ///
225    /// Each id is the `IfcAlignmentSegment.DesignParameters` target, ready
226    /// for `read_horizontal_segment`, `read_vertical_segment` or
227    /// `read_cant_segment`.
228    ///
229    /// # Errors
230    ///
231    /// Refuses an id that is not one of the three layouts, a segment listed
232    /// twice, and a `DesignParameters` of the wrong family.
233    pub fn layout_segments(&self, layout: EntityId) -> AlignmentResult<Vec<EntityId>> {
234        let entity = self
235            .model
236            .get(layout)
237            .ok_or(AlignmentError::MissingEntity { entity: layout })?;
238        let family = [
239            ("IfcAlignmentHorizontal", "IfcAlignmentHorizontalSegment"),
240            ("IfcAlignmentVertical", "IfcAlignmentVerticalSegment"),
241            ("IfcAlignmentCant", "IfcAlignmentCantSegment"),
242        ]
243        .into_iter()
244        .find(|(kind, _)| self.schema.is_a(&entity.type_name, kind))
245        .map(|(_, parameters)| parameters)
246        .ok_or_else(|| AlignmentError::WrongType {
247            entity: layout,
248            expected: "IfcAlignmentHorizontal, IfcAlignmentVertical or IfcAlignmentCant",
249            actual: entity.type_name.to_string(),
250        })?;
251        self.segment_chain(layout, family)
252    }
253
254    /// Refuse `id` unless it exists and is an `expected` (or subtype).
255    pub(crate) fn require(&self, id: EntityId, expected: &'static str) -> AlignmentResult<()> {
256        let entity = self
257            .model
258            .get(id)
259            .ok_or(AlignmentError::MissingEntity { entity: id })?;
260        if self.schema.is_a(&entity.type_name, expected) {
261            Ok(())
262        } else {
263            Err(AlignmentError::WrongType {
264                entity: id,
265                expected,
266                actual: entity.type_name.to_string(),
267            })
268        }
269    }
270}