Skip to main content

ifc_alignment/
view.rs

1//! Pins the authoritative IFC4X3 profile and exposes bounded traversal.
2//!
3//! `IfcAlignment*` entities were introduced in IFC4X3; IFC2X3 and IFC4 ADD2
4//! TC1 do not declare them at all. Unlike `ifc-resource`/`ifc-structural`,
5//! there is therefore no cross-version dispatch table here -- exactly one
6//! schema profile is authoritative, and any other declared schema is a typed
7//! refusal rather than an approximation.
8
9use std::collections::HashSet;
10
11use ifc_model::{EntityId, Model};
12use ifc_schema::{ifc4x3, Schema, SchemaVersion};
13
14use crate::error::{AlignmentError, AlignmentResult};
15
16/// A model whose declared schema has been pinned to IFC4X3 ADD2.
17///
18/// Holding the schema alongside the model lets traversal use `is_a` for
19/// subtype-aware queries instead of exact type-name matching.
20#[derive(Debug, Clone, Copy)]
21pub struct AlignmentView<'m> {
22    pub(crate) model: &'m Model,
23    pub(crate) schema: &'static Schema,
24}
25
26impl<'m> AlignmentView<'m> {
27    /// Pin the model's declared schema and confirm it is IFC4X3.
28    pub fn for_model(model: &'m Model) -> AlignmentResult<Self> {
29        let token = match model.header().schema.as_slice() {
30            [] => return Err(AlignmentError::MissingSchema),
31            [token] => token,
32            tokens => {
33                return Err(AlignmentError::AmbiguousSchema {
34                    tokens: tokens.to_vec(),
35                });
36            }
37        };
38        let version = SchemaVersion::from_header_token(token).ok_or_else(|| {
39            AlignmentError::UnsupportedSchema {
40                token: token.clone(),
41            }
42        })?;
43        if version != SchemaVersion::Ifc4x3 {
44            return Err(AlignmentError::UnsupportedSchema {
45                token: token.clone(),
46            });
47        }
48        Ok(Self {
49            model,
50            schema: ifc4x3(),
51        })
52    }
53
54    /// Entity ids whose declared type is `ancestor` or a subtype of it.
55    pub(crate) fn ids_of_ancestor(&self, ancestor: &str) -> Vec<EntityId> {
56        self.model
57            .iter()
58            .filter_map(|(id, entity)| self.schema.is_a(&entity.type_name, ancestor).then_some(id))
59            .collect()
60    }
61
62    /// Directly nested children of `parent` (`IfcRelNests.RelatedObjects` in
63    /// `LIST` order), filtered to entities whose type satisfies `expected`.
64    ///
65    /// Nesting order is load-bearing for alignment: segment order along a
66    /// horizontal/vertical/cant layout is defined by nesting order, not by
67    /// any numeric field on the segment itself.
68    pub(crate) fn nested_children(
69        &self,
70        parent: EntityId,
71        expected: &str,
72    ) -> AlignmentResult<Vec<EntityId>> {
73        let mut result = Vec::new();
74        for relation in self.ids_of_ancestor("IfcRelNests") {
75            let entity = self
76                .model
77                .get(relation)
78                .ok_or(AlignmentError::MissingEntity { entity: relation })?;
79            let relating = entity
80                .attributes
81                .get(4)
82                .and_then(|value| value.as_ref_id())
83                .ok_or(AlignmentError::InvalidAttribute {
84                    entity: relation,
85                    index: 4,
86                    name: "RelatingObject",
87                })?;
88            if relating != parent {
89                continue;
90            }
91            let related = entity
92                .attributes
93                .get(5)
94                .and_then(|value| value.as_list())
95                .ok_or(AlignmentError::InvalidAttribute {
96                    entity: relation,
97                    index: 5,
98                    name: "RelatedObjects",
99                })?;
100            for value in related {
101                let child = value.as_ref_id().ok_or(AlignmentError::InvalidAttribute {
102                    entity: relation,
103                    index: 5,
104                    name: "RelatedObjects",
105                })?;
106                let child_entity =
107                    self.model
108                        .get(child)
109                        .ok_or(AlignmentError::DanglingReference {
110                            entity: relation,
111                            attribute: "RelatedObjects",
112                            target: child,
113                        })?;
114                if self.schema.is_a(&child_entity.type_name, expected) {
115                    result.push(child);
116                }
117            }
118        }
119        Ok(result)
120    }
121
122    /// The `IfcAlignmentSegment` instances nested under `parent`, together
123    /// with the `IfcAlignmentParameterSegment` each one's `DesignParameters`
124    /// resolves to, in authored order.
125    ///
126    /// `IfcAlignmentSegment` is the geometry-bearing node in the nesting
127    /// tree; `DesignParameters` is a direct forward reference to the typed
128    /// parameter entity (`IfcAlignmentHorizontalSegment`, etc), not itself
129    /// nested. Both indirections are resolved here so callers get the
130    /// concrete parameter entity id directly.
131    pub(crate) fn segment_chain(
132        &self,
133        parent: EntityId,
134        design_parameters_type: &str,
135    ) -> AlignmentResult<Vec<EntityId>> {
136        let segments = self.nested_children(parent, "IfcAlignmentSegment")?;
137        let mut seen = HashSet::with_capacity(segments.len());
138        let mut result = Vec::with_capacity(segments.len());
139        for segment in segments {
140            if !seen.insert(segment) {
141                return Err(AlignmentError::SemanticViolation {
142                    entity: Some(segment),
143                    rule: "IfcRelNests must not list the same segment twice",
144                });
145            }
146            let entity = self
147                .model
148                .get(segment)
149                .ok_or(AlignmentError::MissingEntity { entity: segment })?;
150            // `IfcAlignmentSegment` subtype of `IfcLinearElement` subtype of
151            // `IfcProduct`; `DesignParameters` is the sole attribute this
152            // crate's declaration contributes, past the inherited slots.
153            let design_parameters = entity
154                .attributes
155                .last()
156                .and_then(|value| value.as_ref_id())
157                .ok_or(AlignmentError::InvalidAttribute {
158                    entity: segment,
159                    index: entity.attributes.len().saturating_sub(1),
160                    name: "DesignParameters",
161                })?;
162            let parameters_entity =
163                self.model
164                    .get(design_parameters)
165                    .ok_or(AlignmentError::DanglingReference {
166                        entity: segment,
167                        attribute: "DesignParameters",
168                        target: design_parameters,
169                    })?;
170            if !self
171                .schema
172                .is_a(&parameters_entity.type_name, design_parameters_type)
173            {
174                return Err(AlignmentError::WrongType {
175                    entity: design_parameters,
176                    expected: "matches the requested design-parameters family",
177                    actual: parameters_entity.type_name.to_string(),
178                });
179            }
180            result.push(design_parameters);
181        }
182        Ok(result)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use ifc_model::Header;
190
191    fn model_with_schema(tokens: &[&str]) -> Model {
192        let mut model = Model::default();
193        *model.header_mut() = Header {
194            schema: tokens.iter().map(|s| s.to_string()).collect(),
195            ..Header::default()
196        };
197        model
198    }
199
200    #[test]
201    fn refuses_a_missing_schema_declaration() {
202        let model = model_with_schema(&[]);
203        assert!(matches!(
204            AlignmentView::for_model(&model),
205            Err(AlignmentError::MissingSchema)
206        ));
207    }
208
209    #[test]
210    fn refuses_an_ambiguous_schema_declaration() {
211        let model = model_with_schema(&["IFC4X3_ADD2", "IFC4"]);
212        assert!(matches!(
213            AlignmentView::for_model(&model),
214            Err(AlignmentError::AmbiguousSchema { .. })
215        ));
216    }
217
218    #[test]
219    fn refuses_ifc2x3_because_alignment_entities_do_not_exist_there() {
220        let model = model_with_schema(&["IFC2X3"]);
221        assert!(matches!(
222            AlignmentView::for_model(&model),
223            Err(AlignmentError::UnsupportedSchema { token }) if token == "IFC2X3"
224        ));
225    }
226
227    #[test]
228    fn refuses_ifc4_because_alignment_entities_do_not_exist_there() {
229        let model = model_with_schema(&["IFC4"]);
230        assert!(matches!(
231            AlignmentView::for_model(&model),
232            Err(AlignmentError::UnsupportedSchema { token }) if token == "IFC4"
233        ));
234    }
235
236    #[test]
237    fn accepts_ifc4x3_add2() {
238        let model = model_with_schema(&["IFC4X3_ADD2"]);
239        assert!(AlignmentView::for_model(&model).is_ok());
240    }
241
242    #[test]
243    fn accepts_the_bare_ifc4x3_token() {
244        let model = model_with_schema(&["IFC4X3"]);
245        assert!(AlignmentView::for_model(&model).is_ok());
246    }
247
248    #[test]
249    fn refuses_an_unrecognized_token() {
250        let model = model_with_schema(&["IFC5"]);
251        assert!(matches!(
252            AlignmentView::for_model(&model),
253            Err(AlignmentError::UnsupportedSchema { token }) if token == "IFC5"
254        ));
255    }
256}