Skip to main content

ifc_material/usage/
resolution.rs

1//! Deterministic material-select and occurrence/type resolution.
2//!
3//! Every hop reads against the model's release: `IfcRelDefinesByType` slots
4//! and the legal `IfcTypeObject` subtypes come from its bundled table, and a
5//! select member the release does not declare is refused.
6
7use ifc_model::{Entity, EntityId};
8
9use crate::release::{is_type_object, Release};
10use crate::view::{required_ref, required_refs};
11use crate::{
12    Material, MaterialAssignment, MaterialConstituent, MaterialConstituentSet, MaterialError,
13    MaterialLayer, MaterialLayerSet, MaterialLayerSetUsage, MaterialLayerWithOffsets, MaterialList,
14    MaterialProfile, MaterialProfileSet, MaterialProfileSetUsage, MaterialProfileSetUsageTapering,
15    MaterialProfileWithOffsets, MaterialResult, MaterialView,
16};
17
18/// A resolved branch of the abstract `IfcMaterialDefinition` select.
19///
20/// IFC2X3 has no `IfcMaterialDefinition` supertype; its `IfcMaterialSelect`
21/// members `IfcMaterial`, `IfcMaterialLayer` and `IfcMaterialLayerSet`
22/// resolve to the same branches here. The constituent and profile branches
23/// only occur for IFC4 and IFC4X3 models.
24#[derive(Debug, Clone, Copy)]
25pub enum MaterialDefinition<'m> {
26    /// Resolves to an `IfcMaterial`.
27    Material(Material<'m>),
28    /// Resolves to an `IfcMaterialConstituent`.
29    Constituent(MaterialConstituent<'m>),
30    /// Resolves to an `IfcMaterialConstituentSet`.
31    ConstituentSet(MaterialConstituentSet<'m>),
32    /// Resolves to an `IfcMaterialLayer`.
33    Layer(MaterialLayer<'m>),
34    /// Resolves to an `IfcMaterialLayerWithOffsets`.
35    LayerWithOffsets(MaterialLayerWithOffsets<'m>),
36    /// Resolves to an `IfcMaterialLayerSet`.
37    LayerSet(MaterialLayerSet<'m>),
38    /// Resolves to an `IfcMaterialProfile`.
39    Profile(MaterialProfile<'m>),
40    /// Resolves to an `IfcMaterialProfileWithOffsets`.
41    ProfileWithOffsets(MaterialProfileWithOffsets<'m>),
42    /// Resolves to an `IfcMaterialProfileSet`.
43    ProfileSet(MaterialProfileSet<'m>),
44}
45
46/// A resolved branch of the abstract `IfcMaterialUsageDefinition` select.
47///
48/// IFC2X3 has no `IfcMaterialUsageDefinition` supertype; its
49/// `IfcMaterialLayerSetUsage` resolves to [`Self::LayerSet`].
50#[derive(Debug, Clone, Copy)]
51pub enum MaterialUsageDefinition<'m> {
52    /// Resolves to an `IfcMaterialLayerSetUsage`.
53    LayerSet(MaterialLayerSetUsage<'m>),
54    /// Resolves to an `IfcMaterialProfileSetUsage`.
55    ProfileSet(MaterialProfileSetUsage<'m>),
56    /// Resolves to an `IfcMaterialProfileSetUsageTapering`.
57    ProfileSetTapering(MaterialProfileSetUsageTapering<'m>),
58}
59
60/// A resolved branch of the abstract `IfcMaterialSelect`.
61#[derive(Debug, Clone, Copy)]
62pub enum ResolvedMaterialSelect<'m> {
63    /// Resolves to an `IfcMaterialDefinition` subtype.
64    Definition(MaterialDefinition<'m>),
65    /// Resolves to an `IfcMaterialList`.
66    List(MaterialList<'m>),
67    /// Resolves to an `IfcMaterialUsageDefinition` subtype.
68    Usage(MaterialUsageDefinition<'m>),
69}
70
71/// Whether a resolved material assignment came from the object directly or
72/// was inherited from its `IfcTypeObject`.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum AssignmentSource {
75    /// The `IfcRelAssociatesMaterial` directly relates the queried object.
76    Occurrence,
77    /// The `IfcRelAssociatesMaterial` relates the object's `IfcTypeObject`,
78    /// identified by this entity id, and was inherited from it.
79    Type(EntityId),
80}
81
82/// A fully resolved material for one object, with provenance.
83#[derive(Debug, Clone, Copy)]
84#[non_exhaustive]
85pub struct ResolvedAssignment<'m> {
86    /// The `IfcRelAssociatesMaterial` that produced this resolution.
87    pub assignment: MaterialAssignment<'m>,
88    /// The resolved `IfcMaterialSelect` branch.
89    pub material: ResolvedMaterialSelect<'m>,
90    /// Whether the assignment applied directly or via the object's type.
91    pub source: AssignmentSource,
92}
93
94/// Every concrete `IfcMaterialSelect` member of any bundled release that
95/// [`classify`] dispatches. `release_layout` tests pin, per release, that
96/// the release's own select closure is exactly the instantiable subset.
97pub(crate) const SELECT_MEMBERS: &[&str] = &[
98    "IFCMATERIAL",
99    "IFCMATERIALCONSTITUENT",
100    "IFCMATERIALCONSTITUENTSET",
101    "IFCMATERIALLAYER",
102    "IFCMATERIALLAYERWITHOFFSETS",
103    "IFCMATERIALLAYERSET",
104    "IFCMATERIALPROFILE",
105    "IFCMATERIALPROFILEWITHOFFSETS",
106    "IFCMATERIALPROFILESET",
107    "IFCMATERIALLIST",
108    "IFCMATERIALLAYERSETUSAGE",
109    "IFCMATERIALPROFILESETUSAGE",
110    "IFCMATERIALPROFILESETUSAGETAPERING",
111];
112
113/// Dispatch a record to its select branch by type name alone, bound to
114/// `release`. `None` for a type that is no select member of any release.
115fn classify<'m>(
116    id: EntityId,
117    entity: &'m Entity,
118    release: Release<'m>,
119) -> Option<(&'static str, ResolvedMaterialSelect<'m>)> {
120    use MaterialDefinition as D;
121    use MaterialUsageDefinition as U;
122    use ResolvedMaterialSelect as S;
123    let name = SELECT_MEMBERS
124        .iter()
125        .copied()
126        .find(|name| entity.is_type(name))?;
127    let resolved = match name {
128        "IFCMATERIAL" => S::Definition(D::Material(Material::from_known(id, entity, release))),
129        "IFCMATERIALCONSTITUENT" => S::Definition(D::Constituent(MaterialConstituent::from_known(
130            id, entity, release,
131        ))),
132        "IFCMATERIALCONSTITUENTSET" => S::Definition(D::ConstituentSet(
133            MaterialConstituentSet::from_known(id, entity, release),
134        )),
135        "IFCMATERIALLAYER" => {
136            S::Definition(D::Layer(MaterialLayer::from_known(id, entity, release)))
137        }
138        "IFCMATERIALLAYERWITHOFFSETS" => S::Definition(D::LayerWithOffsets(
139            MaterialLayerWithOffsets::from_known(id, entity, release),
140        )),
141        "IFCMATERIALLAYERSET" => S::Definition(D::LayerSet(MaterialLayerSet::from_known(
142            id, entity, release,
143        ))),
144        "IFCMATERIALPROFILE" => {
145            S::Definition(D::Profile(MaterialProfile::from_known(id, entity, release)))
146        }
147        "IFCMATERIALPROFILEWITHOFFSETS" => S::Definition(D::ProfileWithOffsets(
148            MaterialProfileWithOffsets::from_known(id, entity, release),
149        )),
150        "IFCMATERIALPROFILESET" => S::Definition(D::ProfileSet(MaterialProfileSet::from_known(
151            id, entity, release,
152        ))),
153        "IFCMATERIALLIST" => S::List(MaterialList::from_known(id, entity, release)),
154        "IFCMATERIALLAYERSETUSAGE" => S::Usage(U::LayerSet(MaterialLayerSetUsage::from_known(
155            id, entity, release,
156        ))),
157        "IFCMATERIALPROFILESETUSAGE" => S::Usage(U::ProfileSet(
158            MaterialProfileSetUsage::from_known(id, entity, release),
159        )),
160        _ => S::Usage(U::ProfileSetTapering(
161            MaterialProfileSetUsageTapering::from_known(id, entity, release),
162        )),
163    };
164    Some((name, resolved))
165}
166
167impl<'m> MaterialView<'m> {
168    /// Every concrete subtype of the abstract `IfcMaterialDefinition`.
169    ///
170    /// Yields by type name; a record the model's release does not declare
171    /// (an IFC2X3 constituent set, say) is still yielded, and its accessors
172    /// fail with [`MaterialError::EntityNotInSchema`].
173    pub fn material_definitions(self) -> impl Iterator<Item = MaterialDefinition<'m>> + 'm {
174        let release = self.release();
175        self.model()
176            .iter()
177            .filter_map(move |(id, entity)| match classify(id, entity, release)?.1 {
178                ResolvedMaterialSelect::Definition(definition) => Some(definition),
179                _ => None,
180            })
181    }
182
183    /// Every concrete subtype of the abstract `IfcMaterialUsageDefinition`.
184    ///
185    /// Yields by type name, as [`Self::material_definitions`] does.
186    pub fn material_usage_definitions(
187        self,
188    ) -> impl Iterator<Item = MaterialUsageDefinition<'m>> + 'm {
189        let release = self.release();
190        self.model()
191            .iter()
192            .filter_map(move |(id, entity)| match classify(id, entity, release)?.1 {
193                ResolvedMaterialSelect::Usage(usage) => Some(usage),
194                _ => None,
195            })
196    }
197
198    /// Resolves `id` as an `IfcMaterialSelect` branch of the model's release
199    /// by dispatching on its concrete IFC entity type.
200    ///
201    /// # Errors
202    ///
203    /// [`MaterialError::WrongEntityType`] if `id` names an entity that is no
204    /// `MaterialResource` select member;
205    /// [`MaterialError::EntityNotInSchema`] if it is a member of another
206    /// release only, such as an `IfcMaterialConstituentSet` or
207    /// `IfcMaterialProfileSet` in an IFC2X3 model; and the release-binding
208    /// errors when the header binds no release.
209    pub fn resolve_material_select(
210        self,
211        id: EntityId,
212    ) -> MaterialResult<ResolvedMaterialSelect<'m>> {
213        let entity = self.entity(id, id)?;
214        let wrong = || MaterialError::WrongEntityType {
215            expected: "IfcMaterialSelect",
216            actual: entity.type_name.to_string(),
217        };
218        let (name, resolved) = classify(id, entity, self.release()).ok_or_else(wrong)?;
219        let (_, schema) = self.release().require_entity(name, Some(id))?;
220        if !schema.accepts_type("IfcMaterialSelect", name) {
221            return Err(wrong());
222        }
223        Ok(resolved)
224    }
225
226    /// Resolves the single material that applies to `object`, preferring a
227    /// direct `IfcRelAssociatesMaterial` on the object itself and falling
228    /// back to the material assigned to its `IfcTypeObject` via
229    /// `IfcRelDefinesByType`. Returns `Ok(None)` if neither exists. Fails
230    /// with [`crate::MaterialError::AmbiguousAssignment`] or
231    /// [`crate::MaterialError::AmbiguousType`] if more than one candidate is
232    /// found at either level, and with
233    /// `crate::MaterialError::UnknownEntity` if `object` is not in the
234    /// model.
235    ///
236    /// Every record is read against the model's release, so an IFC2X3 model
237    /// resolves with IFC2X3 slots and type objects.
238    ///
239    /// The type fallback is exactly one `IfcRelDefinesByType` hop, and two
240    /// such relations are ambiguous even when they name the same type: the
241    /// file states the typing twice, and this lookup does not guess which
242    /// statement was meant. (`ifc-resource` deliberately accepts a repeated
243    /// relation naming the same type.)
244    pub fn assigned_material(
245        self,
246        object: EntityId,
247    ) -> MaterialResult<Option<ResolvedAssignment<'m>>> {
248        self.model()
249            .get(object)
250            .ok_or(MaterialError::UnknownEntity { id: object })?;
251
252        let direct = self.assignments_for(object)?;
253        if direct.len() > 1 {
254            return Err(MaterialError::AmbiguousAssignment {
255                object,
256                count: direct.len(),
257            });
258        }
259        if let Some(assignment) = direct.first().copied() {
260            return self
261                .resolve_assignment(assignment, AssignmentSource::Occurrence)
262                .map(Some);
263        }
264
265        let Some((relation_id, type_id)) = self.type_relation(object)? else {
266            return Ok(None);
267        };
268        let type_entity = self.entity(relation_id, type_id)?;
269        if !is_type_object(self.release(), &type_entity.type_name)? {
270            return Err(MaterialError::ReferenceType {
271                source_id: relation_id,
272                target: type_id,
273                expected: "IFCTYPEOBJECT subtype",
274                actual: type_entity.type_name.to_string(),
275            });
276        }
277
278        let assigned = self.assignments_for(type_id)?;
279        if assigned.len() > 1 {
280            return Err(MaterialError::AmbiguousAssignment {
281                object: type_id,
282                count: assigned.len(),
283            });
284        }
285        assigned
286            .first()
287            .copied()
288            .map(|assignment| self.resolve_assignment(assignment, AssignmentSource::Type(type_id)))
289            .transpose()
290    }
291
292    /// The single `IfcRelDefinesByType` typing `object`, as
293    /// `(relation, type)`.
294    fn type_relation(self, object: EntityId) -> MaterialResult<Option<(EntityId, EntityId)>> {
295        const ENTITY: &str = "IFCRELDEFINESBYTYPE";
296        let release = self.release();
297        let mut type_relations = Vec::new();
298        for (relation_id, relation) in self.model().of_type(ENTITY) {
299            let related_slot = release.slot(ENTITY, relation_id, "RelatedObjects")?;
300            let related = required_refs(
301                ENTITY,
302                relation_id,
303                relation,
304                related_slot,
305                "RelatedObjects",
306                1,
307            )?;
308            if related.contains(&object) {
309                let type_slot = release.slot(ENTITY, relation_id, "RelatingType")?;
310                let type_id =
311                    required_ref(ENTITY, relation_id, relation, type_slot, "RelatingType")?;
312                type_relations.push((relation_id, type_id));
313            }
314        }
315        if type_relations.len() > 1 {
316            return Err(MaterialError::AmbiguousType {
317                object,
318                count: type_relations.len(),
319            });
320        }
321        Ok(type_relations.first().copied())
322    }
323
324    fn resolve_assignment(
325        self,
326        assignment: MaterialAssignment<'m>,
327        source: AssignmentSource,
328    ) -> MaterialResult<ResolvedAssignment<'m>> {
329        let material_id = assignment.relating_material_id()?;
330        Ok(ResolvedAssignment {
331            assignment,
332            material: self.resolve_material_select(material_id)?,
333            source,
334        })
335    }
336}