Skip to main content

ifc_material/usage/
resolution.rs

1//! Deterministic material-select and occurrence/type resolution.
2
3use ifc_model::EntityId;
4
5use crate::{
6    Material, MaterialAssignment, MaterialConstituent, MaterialConstituentSet, MaterialError,
7    MaterialLayer, MaterialLayerSet, MaterialLayerSetUsage, MaterialLayerWithOffsets, MaterialList,
8    MaterialProfile, MaterialProfileSet, MaterialProfileSetUsage, MaterialProfileSetUsageTapering,
9    MaterialProfileWithOffsets, MaterialResult, MaterialView,
10};
11
12/// A resolved branch of the abstract `IfcMaterialDefinition` select.
13#[derive(Debug, Clone, Copy)]
14pub enum MaterialDefinition<'m> {
15    /// Resolves to an `IfcMaterial`.
16    Material(Material<'m>),
17    /// Resolves to an `IfcMaterialConstituent`.
18    Constituent(MaterialConstituent<'m>),
19    /// Resolves to an `IfcMaterialConstituentSet`.
20    ConstituentSet(MaterialConstituentSet<'m>),
21    /// Resolves to an `IfcMaterialLayer`.
22    Layer(MaterialLayer<'m>),
23    /// Resolves to an `IfcMaterialLayerWithOffsets`.
24    LayerWithOffsets(MaterialLayerWithOffsets<'m>),
25    /// Resolves to an `IfcMaterialLayerSet`.
26    LayerSet(MaterialLayerSet<'m>),
27    /// Resolves to an `IfcMaterialProfile`.
28    Profile(MaterialProfile<'m>),
29    /// Resolves to an `IfcMaterialProfileWithOffsets`.
30    ProfileWithOffsets(MaterialProfileWithOffsets<'m>),
31    /// Resolves to an `IfcMaterialProfileSet`.
32    ProfileSet(MaterialProfileSet<'m>),
33}
34
35/// A resolved branch of the abstract `IfcMaterialUsageDefinition` select.
36#[derive(Debug, Clone, Copy)]
37pub enum MaterialUsageDefinition<'m> {
38    /// Resolves to an `IfcMaterialLayerSetUsage`.
39    LayerSet(MaterialLayerSetUsage<'m>),
40    /// Resolves to an `IfcMaterialProfileSetUsage`.
41    ProfileSet(MaterialProfileSetUsage<'m>),
42    /// Resolves to an `IfcMaterialProfileSetUsageTapering`.
43    ProfileSetTapering(MaterialProfileSetUsageTapering<'m>),
44}
45
46/// A resolved branch of the abstract `IfcMaterialSelect`.
47#[derive(Debug, Clone, Copy)]
48pub enum ResolvedMaterialSelect<'m> {
49    /// Resolves to an `IfcMaterialDefinition` subtype.
50    Definition(MaterialDefinition<'m>),
51    /// Resolves to an `IfcMaterialList`.
52    List(MaterialList<'m>),
53    /// Resolves to an `IfcMaterialUsageDefinition` subtype.
54    Usage(MaterialUsageDefinition<'m>),
55}
56
57/// Whether a resolved material assignment came from the object directly or
58/// was inherited from its `IfcTypeObject`.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum AssignmentSource {
61    /// The `IfcRelAssociatesMaterial` directly relates the queried object.
62    Occurrence,
63    /// The `IfcRelAssociatesMaterial` relates the object's `IfcTypeObject`,
64    /// identified by this entity id, and was inherited from it.
65    Type(EntityId),
66}
67
68/// A fully resolved material for one object, with provenance.
69#[derive(Debug, Clone, Copy)]
70pub struct ResolvedAssignment<'m> {
71    /// The `IfcRelAssociatesMaterial` that produced this resolution.
72    pub assignment: MaterialAssignment<'m>,
73    /// The resolved `IfcMaterialSelect` branch.
74    pub material: ResolvedMaterialSelect<'m>,
75    /// Whether the assignment applied directly or via the object's type.
76    pub source: AssignmentSource,
77}
78
79impl<'m> MaterialView<'m> {
80    /// Every concrete subtype of the abstract `IfcMaterialDefinition`.
81    pub fn material_definitions(self) -> impl Iterator<Item = MaterialDefinition<'m>> + 'm {
82        self.model().iter().filter_map(move |(id, _)| {
83            match self.resolve_material_select(id).ok()? {
84                ResolvedMaterialSelect::Definition(definition) => Some(definition),
85                _ => None,
86            }
87        })
88    }
89
90    /// Every concrete subtype of the abstract `IfcMaterialUsageDefinition`.
91    pub fn material_usage_definitions(
92        self,
93    ) -> impl Iterator<Item = MaterialUsageDefinition<'m>> + 'm {
94        self.model().iter().filter_map(move |(id, _)| {
95            match self.resolve_material_select(id).ok()? {
96                ResolvedMaterialSelect::Usage(usage) => Some(usage),
97                _ => None,
98            }
99        })
100    }
101
102    /// Resolves `id` as an `IfcMaterialSelect` branch by dispatching on its
103    /// concrete IFC entity type. Fails with
104    /// [`crate::MaterialError::WrongEntityType`] if `id` names an entity
105    /// that is not a `MaterialResource` select member.
106    pub fn resolve_material_select(
107        self,
108        id: EntityId,
109    ) -> MaterialResult<ResolvedMaterialSelect<'m>> {
110        let entity = self.entity(id, id)?;
111        let entity_type = entity.type_name.to_ascii_uppercase();
112        let definition = match entity_type.as_str() {
113            "IFCMATERIAL" => MaterialDefinition::Material(Material::from_known(id, entity)),
114            "IFCMATERIALCONSTITUENT" => {
115                MaterialDefinition::Constituent(MaterialConstituent::from_known(id, entity))
116            }
117            "IFCMATERIALCONSTITUENTSET" => {
118                MaterialDefinition::ConstituentSet(MaterialConstituentSet::from_known(id, entity))
119            }
120            "IFCMATERIALLAYER" => MaterialDefinition::Layer(MaterialLayer::from_known(id, entity)),
121            "IFCMATERIALLAYERWITHOFFSETS" => MaterialDefinition::LayerWithOffsets(
122                MaterialLayerWithOffsets::from_known(id, entity),
123            ),
124            "IFCMATERIALLAYERSET" => {
125                MaterialDefinition::LayerSet(MaterialLayerSet::from_known(id, entity))
126            }
127            "IFCMATERIALPROFILE" => {
128                MaterialDefinition::Profile(MaterialProfile::from_known(id, entity))
129            }
130            "IFCMATERIALPROFILEWITHOFFSETS" => MaterialDefinition::ProfileWithOffsets(
131                MaterialProfileWithOffsets::from_known(id, entity),
132            ),
133            "IFCMATERIALPROFILESET" => {
134                MaterialDefinition::ProfileSet(MaterialProfileSet::from_known(id, entity))
135            }
136            "IFCMATERIALLIST" => {
137                return Ok(ResolvedMaterialSelect::List(MaterialList::from_known(
138                    id, entity,
139                )));
140            }
141            "IFCMATERIALLAYERSETUSAGE" => {
142                return Ok(ResolvedMaterialSelect::Usage(
143                    MaterialUsageDefinition::LayerSet(MaterialLayerSetUsage::from_known(
144                        id, entity,
145                    )),
146                ));
147            }
148            "IFCMATERIALPROFILESETUSAGE" => {
149                return Ok(ResolvedMaterialSelect::Usage(
150                    MaterialUsageDefinition::ProfileSet(MaterialProfileSetUsage::from_known(
151                        id, entity,
152                    )),
153                ));
154            }
155            "IFCMATERIALPROFILESETUSAGETAPERING" => {
156                return Ok(ResolvedMaterialSelect::Usage(
157                    MaterialUsageDefinition::ProfileSetTapering(
158                        MaterialProfileSetUsageTapering::from_known(id, entity),
159                    ),
160                ));
161            }
162            _ => {
163                return Err(MaterialError::WrongEntityType {
164                    expected: "IfcMaterialSelect",
165                    actual: entity.type_name.to_string(),
166                });
167            }
168        };
169        Ok(ResolvedMaterialSelect::Definition(definition))
170    }
171
172    /// Resolves the single material that applies to `object`, preferring a
173    /// direct `IfcRelAssociatesMaterial` on the object itself and falling
174    /// back to the material assigned to its `IfcTypeObject` via
175    /// `IfcRelDefinesByType`. Returns `Ok(None)` if neither exists. Fails
176    /// with [`crate::MaterialError::AmbiguousAssignment`] or
177    /// [`crate::MaterialError::AmbiguousType`] if more than one candidate is
178    /// found at either level, and with
179    /// `crate::MaterialError::UnknownEntity` if `object` is not in the
180    /// model.
181    pub fn assigned_material(
182        self,
183        object: EntityId,
184    ) -> MaterialResult<Option<ResolvedAssignment<'m>>> {
185        self.model()
186            .get(object)
187            .ok_or(MaterialError::UnknownEntity { id: object })?;
188
189        let direct = self.assignments_for(object)?;
190        if direct.len() > 1 {
191            return Err(MaterialError::AmbiguousAssignment {
192                object,
193                count: direct.len(),
194            });
195        }
196        if let Some(assignment) = direct.first().copied() {
197            return self
198                .resolve_assignment(assignment, AssignmentSource::Occurrence)
199                .map(Some);
200        }
201
202        let mut type_relations = Vec::new();
203        for (relation_id, relation) in self.model().of_type("IFCRELDEFINESBYTYPE") {
204            let related = crate::view::required_refs(
205                "IFCRELDEFINESBYTYPE",
206                relation_id,
207                relation,
208                4,
209                "RelatedObjects",
210                1,
211            )?;
212            if related.contains(&object) {
213                let type_id = crate::view::required_ref(
214                    "IFCRELDEFINESBYTYPE",
215                    relation_id,
216                    relation,
217                    5,
218                    "RelatingType",
219                )?;
220                type_relations.push((relation_id, type_id));
221            }
222        }
223        if type_relations.len() > 1 {
224            return Err(MaterialError::AmbiguousType {
225                object,
226                count: type_relations.len(),
227            });
228        }
229        let Some((relation_id, type_id)) = type_relations.first().copied() else {
230            return Ok(None);
231        };
232        let type_entity = self.entity(relation_id, type_id)?;
233        if !super::ifc4_type_objects::is_concrete_type_object(&type_entity.type_name) {
234            return Err(MaterialError::ReferenceType {
235                source_id: relation_id,
236                target: type_id,
237                expected: "IFCTYPEOBJECT subtype",
238                actual: type_entity.type_name.to_string(),
239            });
240        }
241
242        let assigned = self.assignments_for(type_id)?;
243        if assigned.len() > 1 {
244            return Err(MaterialError::AmbiguousAssignment {
245                object: type_id,
246                count: assigned.len(),
247            });
248        }
249        assigned
250            .first()
251            .copied()
252            .map(|assignment| self.resolve_assignment(assignment, AssignmentSource::Type(type_id)))
253            .transpose()
254    }
255
256    fn resolve_assignment(
257        self,
258        assignment: MaterialAssignment<'m>,
259        source: AssignmentSource,
260    ) -> MaterialResult<ResolvedAssignment<'m>> {
261        let material_id = assignment.relating_material_id()?;
262        Ok(ResolvedAssignment {
263            assignment,
264            material: self.resolve_material_select(material_id)?,
265            source,
266        })
267    }
268}