ifc_lite_server/services/data_model/
materials.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Material association extraction.
6
7use super::material_units::MaterialUnitContext;
8use super::types::{EntityJob, MaterialAssociation};
9use ifc_lite_core::{EntityDecoder, IfcType};
10use rayon::prelude::*;
11use std::collections::HashMap;
12use std::sync::Arc;
13
14/// One resolved material layer (intermediate, before element fan-out).
15struct ResolvedLayer {
16    kind: &'static str,
17    member_count: u32,
18    set_name: Option<String>,
19    layer_index: u32,
20    material_name: String,
21    material_name_present: bool,
22    material_id: Option<u32>,
23    member_name: Option<String>,
24    material_category: Option<String>,
25    fraction: Option<f64>,
26    thickness: Option<f64>,
27    is_ventilated: Option<bool>,
28    category: Option<String>,
29}
30
31/// Resolve an `IfcMaterialLayer`'s referenced `IfcMaterial` name.
32fn material_details_of(decoder: &mut EntityDecoder, material_id: u32) -> Option<(String, Option<String>, bool)> {
33    let mat = decoder.decode_by_id(material_id).ok()?;
34    if !mat.ifc_type.is_subtype_of(IfcType::IfcMaterial) {
35        return None;
36    }
37    // An unnamed IfcMaterial still proves an association and may have a Category.
38    Some((mat.get_string(0).unwrap_or("").to_string(), mat.get_string(2).map(str::to_string), mat.get_string(0).is_some()))
39}
40
41/// Resolve a `RelatingMaterial` into a flat list of layers. Handles
42/// `IfcMaterial`, `IfcMaterialLayerSet`, `IfcMaterialLayerSetUsage` (→ set),
43/// `IfcMaterialList`, and `IfcMaterialConstituentSet`. `unit_scale` converts
44/// layer thickness to metres.
45fn resolve_material(decoder: &mut EntityDecoder, id: u32, unit_scale: f64) -> Vec<ResolvedLayer> {
46    let Ok(entity) = decoder.decode_by_id(id) else {
47        return Vec::new();
48    };
49    let ty = entity.ifc_type.as_str().to_ascii_uppercase();
50
51    match ty.as_str() {
52        "IFCMATERIAL" => {
53            vec![ResolvedLayer {
54                kind: "IfcMaterial",
55                member_count: 1,
56                set_name: None,
57                layer_index: 0,
58                material_name: entity.get_string(0).unwrap_or("").to_string(),
59                material_name_present: entity.get_string(0).is_some(),
60                material_id: Some(id),
61                member_name: None,
62                material_category: entity.get_string(2).map(str::to_string),
63                fraction: None,
64                thickness: None,
65                is_ventilated: None,
66                category: entity.get_string(2).map(str::to_string),
67            }]
68        },
69        "IFCMATERIALLAYERSETUSAGE" => {
70            // ForLayerSet is attribute 0.
71            match entity.get_ref(0) {
72                Some(set_id) => resolve_material(decoder, set_id, unit_scale),
73                None => Vec::new(),
74            }
75        }
76        "IFCMATERIALLAYERSET" => {
77            let set_name = entity.get_string(1).map(|s| s.to_string());
78            let member_count = entity.get_list(0).map_or(0, |l| l.len() as u32);
79            let layer_ids: Vec<u32> = entity
80                .get_list(0)
81                .map(|l| l.iter().filter_map(|v| v.as_entity_ref()).collect())
82                .unwrap_or_default();
83            layer_ids
84                .iter()
85                .copied()
86                .enumerate()
87                .filter_map(|(i, layer_id)| {
88                    let layer = decoder.decode_by_id(layer_id).ok()?;
89                    if !layer.ifc_type.is_subtype_of(IfcType::IfcMaterialLayer) {
90                        return None;
91                    }
92                    // IfcMaterialLayer: Material(0), LayerThickness(1),
93                    // IsVentilated(2), Name(3), Description(4), Category(5).
94                    let (material_name, material_category, material_name_present) = match layer.get_ref(0) {
95                        Some(mid) => material_details_of(decoder, mid)?,
96                        None => (String::new(), None, false),
97                    };
98                    let thickness = layer.get_float(1).map(|t| t * unit_scale);
99                    let is_ventilated = super::read_logical(&layer, 2);
100                    let category = layer.get_string(5).map(|s| s.to_string());
101                    Some(ResolvedLayer {
102                        kind: "IfcMaterialLayerSet",
103                        member_count,
104                        set_name: set_name.clone(),
105                        layer_index: i as u32,
106                        material_name,
107                        material_name_present,
108                        material_id: layer.get_ref(0),
109                        member_name: layer.get_string(3).map(str::to_string),
110                        material_category,
111                        fraction: None,
112                        thickness,
113                        is_ventilated,
114                        category,
115                    })
116                })
117                .collect()
118        }
119        "IFCMATERIALLIST" => {
120            let member_count = entity.get_list(0).map_or(0, |l| l.len() as u32);
121            let mat_ids: Vec<u32> = entity
122                .get_list(0)
123                .map(|l| l.iter().filter_map(|v| v.as_entity_ref()).collect())
124                .unwrap_or_default();
125            mat_ids
126                .iter()
127                .copied()
128                .enumerate()
129                .filter_map(|(i, mid)| {
130                    let (material_name, material_category, material_name_present) = material_details_of(decoder, mid)?;
131                    Some(ResolvedLayer {
132                        kind: "IfcMaterialList",
133                        member_count,
134                        set_name: None,
135                        layer_index: i as u32,
136                        material_name,
137                        material_name_present,
138                        material_id: Some(mid),
139                        member_name: None,
140                        material_category,
141                        fraction: None,
142                        thickness: None,
143                        is_ventilated: None,
144                        category: None,
145                    })
146                })
147                .collect()
148        }
149        "IFCMATERIALCONSTITUENTSET" => {
150            // IfcMaterialConstituentSet: Name(0), Description(1),
151            // MaterialConstituents(2). Each IfcMaterialConstituent has
152            // Name(0), Description(1), Material(2), Fraction(3), Category(4).
153            let set_name = entity.get_string(0).map(|s| s.to_string());
154            let member_count = entity.get_list(2).map_or(0, |l| l.len() as u32);
155            let constituent_ids: Vec<u32> = entity
156                .get_list(2)
157                .map(|l| l.iter().filter_map(|v| v.as_entity_ref()).collect())
158                .unwrap_or_default();
159            constituent_ids
160                .iter()
161                .copied()
162                .enumerate()
163                .filter_map(|(i, cid)| {
164                    let constituent = decoder.decode_by_id(cid).ok()?;
165                    if !constituent.ifc_type.is_subtype_of(IfcType::IfcMaterialConstituent) {
166                        return None;
167                    }
168                    let (material_name, material_category, material_name_present) = match constituent.get_ref(2) {
169                        Some(mid) => material_details_of(decoder, mid)?,
170                        None => (String::new(), None, false),
171                    };
172                    Some(ResolvedLayer {
173                        kind: "IfcMaterialConstituentSet",
174                        member_count,
175                        set_name: set_name.clone(),
176                        layer_index: i as u32,
177                        material_name,
178                        material_name_present,
179                        material_id: constituent.get_ref(2),
180                        member_name: constituent.get_string(0).map(str::to_string),
181                        material_category,
182                        fraction: constituent.get_float(3),
183                        thickness: None,
184                        is_ventilated: None,
185                        category: constituent.get_string(4).map(|s| s.to_string()),
186                    })
187                })
188                .collect()
189        }
190        "IFCMATERIALPROFILESETUSAGE" => {
191            // ForProfileSet is attribute 0.
192            match entity.get_ref(0) {
193                Some(set_id) => resolve_material(decoder, set_id, unit_scale),
194                None => Vec::new(),
195            }
196        }
197        "IFCMATERIALPROFILESET" => {
198            // IfcMaterialProfileSet: Name(0), Description(1), MaterialProfiles(2).
199            // Each IfcMaterialProfile: Name(0), Description(1), Material(2),
200            // Profile(3), Priority(4), Category(5). Profiles carry no layer
201            // thickness, so thickness stays `None`.
202            let set_name = entity.get_string(0).map(|s| s.to_string());
203            let member_count = entity.get_list(2).map_or(0, |l| l.len() as u32);
204            let profile_ids: Vec<u32> = entity
205                .get_list(2)
206                .map(|l| l.iter().filter_map(|v| v.as_entity_ref()).collect())
207                .unwrap_or_default();
208            profile_ids
209                .iter()
210                .copied()
211                .enumerate()
212                .filter_map(|(i, pid)| {
213                    let profile = decoder.decode_by_id(pid).ok()?;
214                    if !profile.ifc_type.is_subtype_of(IfcType::IfcMaterialProfile) {
215                        return None;
216                    }
217                    let (material_name, material_category, material_name_present) = match profile.get_ref(2) {
218                        Some(mid) => material_details_of(decoder, mid)?,
219                        None => (String::new(), None, false),
220                    };
221                    Some(ResolvedLayer {
222                        kind: "IfcMaterialProfileSet",
223                        member_count,
224                        set_name: set_name.clone(),
225                        layer_index: i as u32,
226                        material_name,
227                        material_name_present,
228                        material_id: profile.get_ref(2),
229                        member_name: profile.get_string(0).map(str::to_string),
230                        material_category,
231                        fraction: None,
232                        thickness: None,
233                        is_ventilated: None,
234                        category: profile.get_string(5).map(|s| s.to_string()),
235                    })
236                })
237                .collect()
238        }
239        _ => Vec::new(),
240    }
241}
242
243/// Extract material associations (`IfcRelAssociatesMaterial`).
244pub(super) fn extract_materials(
245    jobs: &[EntityJob],
246    content: &Arc<Vec<u8>>,
247    entity_index: &Arc<ifc_lite_core::EntityIndex>,
248    units: &MaterialUnitContext,
249) -> Vec<MaterialAssociation> {
250    let rel_jobs: Vec<_> = jobs
251        .iter()
252        .filter(|job| {
253            job.type_name
254                .eq_ignore_ascii_case("IFCRELASSOCIATESMATERIAL")
255        })
256        .collect();
257
258    tracing::debug!(count = rel_jobs.len(), "Extracting materials");
259
260    rel_jobs
261        .par_iter()
262        .flat_map(|job| {
263            let mut decoder =
264                EntityDecoder::with_arc_index(content.as_slice(), entity_index.clone());
265            let Ok(rel) = decoder.decode_at(job.start, job.end) else {
266                return Vec::new();
267            };
268            let related = super::related_object_ids(&rel);
269            // RelatingMaterial is attribute 5.
270            let Some(material_id) = rel.get_ref(5) else {
271                return Vec::new();
272            };
273            let mut resolved_by_scale: HashMap<u64, Vec<ResolvedLayer>> = HashMap::new();
274            related
275                .into_iter()
276                .flat_map(|element_id| {
277                    let scale = units.scale_for(element_id);
278                    let layers = resolved_by_scale
279                        .entry(scale.to_bits())
280                        .or_insert_with(|| resolve_material(&mut decoder, material_id, scale));
281                    if units.has_mixed_type_scales(element_id)
282                        && layers.iter().any(|layer| layer.thickness.is_some())
283                    {
284                        return Vec::new();
285                    }
286                    layers
287                        .iter()
288                        .map(move |layer| MaterialAssociation {
289                            element_id,
290                            association_id: job.id,
291                            definition_id: material_id,
292                            member_count: layer.member_count,
293                            kind: layer.kind.to_string(),
294                            set_name: layer.set_name.clone(),
295                            layer_index: layer.layer_index,
296                            material_name: layer.material_name.clone(),
297                            material_name_present: layer.material_name_present,
298                            material_id: layer.material_id,
299                            member_name: layer.member_name.clone(),
300                            material_category: layer.material_category.clone(),
301                            fraction: layer.fraction,
302                            thickness: layer.thickness,
303                            is_ventilated: layer.is_ventilated,
304                            category: layer.category.clone(),
305                        })
306                        .collect::<Vec<_>>()
307                })
308                .collect()
309        })
310        .collect()
311}