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}