1use super::composition::compose_nodes;
10use super::types::{attr, ComposedNode, IfcClass, IfcxFile};
11use crate::Result;
12use bimifc_model::{
13 AttributeValue, DecodedEntity, EntityId, EntityResolver, IfcModel, IfcType, ModelMetadata,
14 Property, PropertyReader, PropertySet, Quantity, SpatialNode, SpatialNodeType, SpatialQuery,
15 StoreyInfo,
16};
17use rustc_hash::FxHashMap;
18use std::sync::Arc;
19
20pub struct IfcxModel {
22 nodes: FxHashMap<String, ComposedNode>,
24 path_to_id: FxHashMap<String, EntityId>,
26 id_to_path: FxHashMap<EntityId, String>,
28 type_index: FxHashMap<IfcType, Vec<EntityId>>,
30 entities: FxHashMap<EntityId, Arc<DecodedEntity>>,
32 spatial_tree: Option<SpatialNode>,
34 metadata: ModelMetadata,
36 unit_scale: f64,
38}
39
40impl IfcxModel {
41 pub fn parse(content: &str) -> Result<Self> {
43 let file: IfcxFile = serde_json::from_str(content)
45 .map_err(|e| bimifc_model::ParseError::InvalidFormat(e.to_string()))?;
46
47 let nodes = compose_nodes(&file.data);
49
50 let mut path_to_id = FxHashMap::default();
52 let mut id_to_path = FxHashMap::default();
53 let mut next_id = 1u32;
54
55 for path in nodes.keys() {
56 let id = EntityId(next_id);
57 path_to_id.insert(path.clone(), id);
58 id_to_path.insert(id, path.clone());
59 next_id += 1;
60 }
61
62 let mut type_index: FxHashMap<IfcType, Vec<EntityId>> = FxHashMap::default();
64 let mut entities = FxHashMap::default();
65
66 for (path, node) in &nodes {
67 let id = path_to_id[path];
68 let ifc_type = extract_ifc_type(node);
69
70 let entity = Arc::new(DecodedEntity {
72 id,
73 ifc_type: ifc_type.clone(),
74 attributes: build_attributes(node, &path_to_id),
75 });
76
77 entities.insert(id, entity);
78 type_index.entry(ifc_type).or_default().push(id);
79 }
80
81 let spatial_tree = build_spatial_tree(&nodes, &path_to_id);
83
84 let metadata = ModelMetadata {
86 schema_version: format!("IFC5 ({})", file.header.ifcx_version),
87 originating_system: Some(file.header.author.clone()),
88 file_name: Some(file.header.id.clone()),
89 timestamp: Some(file.header.timestamp.clone()),
90 ..Default::default()
91 };
92
93 Ok(Self {
94 nodes,
95 path_to_id,
96 id_to_path,
97 type_index,
98 entities,
99 spatial_tree,
100 metadata,
101 unit_scale: 1.0, })
103 }
104
105 pub fn node(&self, id: EntityId) -> Option<&ComposedNode> {
107 let path = self.id_to_path.get(&id)?;
108 self.nodes.get(path)
109 }
110
111 pub fn path(&self, id: EntityId) -> Option<&str> {
113 self.id_to_path.get(&id).map(|s| s.as_str())
114 }
115
116 pub fn id_for_path(&self, path: &str) -> Option<EntityId> {
118 self.path_to_id.get(path).copied()
119 }
120}
121
122fn extract_ifc_type(node: &ComposedNode) -> IfcType {
124 if let Some(class_val) = node.attributes.get(attr::CLASS) {
125 if let Some(class) = IfcClass::from_value(class_val) {
126 return IfcType::parse(&class.code);
127 }
128 }
129 IfcType::Unknown(String::new())
130}
131
132fn build_attributes(
134 node: &ComposedNode,
135 path_to_id: &FxHashMap<String, EntityId>,
136) -> Vec<AttributeValue> {
137 let mut attrs = vec![AttributeValue::Null; 10];
146
147 attrs[0] = AttributeValue::String(node.path.clone());
149
150 attrs[1] = AttributeValue::Null;
152
153 if let Some(name) = node
155 .attributes
156 .get("bsi::ifc::prop::Name")
157 .or_else(|| node.attributes.get("bsi::ifc::prop::TypeName"))
158 {
159 if let Some(s) = name.as_str() {
160 attrs[2] = AttributeValue::String(s.to_string());
161 }
162 }
163
164 if let Some(desc) = node.attributes.get("bsi::ifc::prop::Description") {
166 if let Some(s) = desc.as_str() {
167 attrs[3] = AttributeValue::String(s.to_string());
168 }
169 }
170
171 let child_refs: Vec<AttributeValue> = node
173 .children
174 .iter()
175 .filter_map(|child_path| {
176 path_to_id
177 .get(child_path)
178 .map(|id| AttributeValue::EntityRef(*id))
179 })
180 .collect();
181
182 if !child_refs.is_empty() {
183 attrs[5] = AttributeValue::List(child_refs);
184 }
185
186 attrs
187}
188
189fn build_spatial_tree(
191 nodes: &FxHashMap<String, ComposedNode>,
192 path_to_id: &FxHashMap<String, EntityId>,
193) -> Option<SpatialNode> {
194 let mut root_path: Option<&str> = None;
196
197 for (path, node) in nodes {
198 let ifc_type = extract_ifc_type(node);
199 if matches!(ifc_type, IfcType::IfcProject) {
200 root_path = Some(path);
201 break;
202 }
203 }
204
205 if root_path.is_none() {
207 for (path, node) in nodes {
208 if node.parent.is_none() && !node.children.is_empty() {
209 root_path = Some(path);
210 break;
211 }
212 }
213 }
214
215 let root_path = root_path?;
216
217 fn build_node(
219 path: &str,
220 nodes: &FxHashMap<String, ComposedNode>,
221 path_to_id: &FxHashMap<String, EntityId>,
222 ) -> Option<SpatialNode> {
223 let node = nodes.get(path)?;
224 let id = *path_to_id.get(path)?;
225 let ifc_type = extract_ifc_type(node);
226
227 let name = node
229 .attributes
230 .get("bsi::ifc::prop::Name")
231 .or_else(|| node.attributes.get("bsi::ifc::prop::TypeName"))
232 .and_then(|v| v.as_str())
233 .map(String::from)
234 .unwrap_or_else(|| path.to_string());
235
236 let node_type = SpatialNodeType::from_ifc_type(&ifc_type);
237 let entity_type = ifc_type.name().to_string();
238
239 let has_geometry = node.attributes.contains_key(attr::MESH);
241
242 let children: Vec<SpatialNode> = node
244 .children
245 .iter()
246 .filter_map(|child_path| build_node(child_path, nodes, path_to_id))
247 .collect();
248
249 let mut spatial_node = SpatialNode::new(id, node_type, name, entity_type);
250 spatial_node.children = children;
251 spatial_node.has_geometry = has_geometry;
252
253 Some(spatial_node)
254 }
255
256 build_node(root_path, nodes, path_to_id)
257}
258
259impl IfcModel for IfcxModel {
261 fn resolver(&self) -> &dyn EntityResolver {
262 self
263 }
264
265 fn properties(&self) -> &dyn PropertyReader {
266 self
267 }
268
269 fn spatial(&self) -> &dyn SpatialQuery {
270 self
271 }
272
273 fn unit_scale(&self) -> f64 {
274 self.unit_scale
275 }
276
277 fn metadata(&self) -> &ModelMetadata {
278 &self.metadata
279 }
280}
281
282impl EntityResolver for IfcxModel {
284 fn get(&self, id: EntityId) -> Option<Arc<DecodedEntity>> {
285 self.entities.get(&id).cloned()
286 }
287
288 fn entities_by_type(&self, ifc_type: &IfcType) -> Vec<Arc<DecodedEntity>> {
289 self.type_index
290 .get(ifc_type)
291 .map(|ids| {
292 ids.iter()
293 .filter_map(|id| self.entities.get(id).cloned())
294 .collect()
295 })
296 .unwrap_or_default()
297 }
298
299 fn find_by_type_name(&self, type_name: &str) -> Vec<Arc<DecodedEntity>> {
300 let target = IfcType::parse(type_name);
301 self.entities_by_type(&target)
302 }
303
304 fn count_by_type(&self, ifc_type: &IfcType) -> usize {
305 self.type_index.get(ifc_type).map(|v| v.len()).unwrap_or(0)
306 }
307
308 fn all_ids(&self) -> Vec<EntityId> {
309 self.entities.keys().copied().collect()
310 }
311
312 fn raw_bytes(&self, _id: EntityId) -> Option<&[u8]> {
313 None
315 }
316}
317
318impl PropertyReader for IfcxModel {
320 fn property_sets(&self, id: EntityId) -> Vec<PropertySet> {
321 let Some(node) = self.node(id) else {
322 return Vec::new();
323 };
324
325 let mut psets: FxHashMap<String, Vec<Property>> = FxHashMap::default();
327
328 for (key, value) in &node.attributes {
329 if key.starts_with("usd::") || key == attr::CLASS || key == attr::MATERIAL {
331 continue;
332 }
333
334 let (namespace, prop_name) = if let Some(pos) = key.rfind("::") {
336 (key[..pos].to_string(), key[pos + 2..].to_string())
337 } else {
338 ("Properties".to_string(), key.clone())
339 };
340
341 let prop_value = json_to_string(value);
343
344 psets
345 .entry(namespace)
346 .or_default()
347 .push(Property::new(prop_name, prop_value));
348 }
349
350 psets
351 .into_iter()
352 .map(|(name, properties)| PropertySet { name, properties })
353 .collect()
354 }
355
356 fn quantities(&self, _id: EntityId) -> Vec<Quantity> {
357 Vec::new()
360 }
361
362 fn global_id(&self, id: EntityId) -> Option<String> {
363 self.path(id).map(String::from)
365 }
366
367 fn name(&self, id: EntityId) -> Option<String> {
368 let node = self.node(id)?;
369 node.attributes
370 .get("bsi::ifc::prop::Name")
371 .or_else(|| node.attributes.get("bsi::ifc::prop::TypeName"))
372 .and_then(|v| v.as_str())
373 .map(String::from)
374 }
375
376 fn description(&self, id: EntityId) -> Option<String> {
377 let node = self.node(id)?;
378 node.attributes
379 .get("bsi::ifc::prop::Description")
380 .and_then(|v| v.as_str())
381 .map(String::from)
382 }
383}
384
385fn json_to_string(value: &serde_json::Value) -> String {
387 match value {
388 serde_json::Value::Bool(b) => b.to_string(),
389 serde_json::Value::Number(n) => n.to_string(),
390 serde_json::Value::String(s) => s.clone(),
391 serde_json::Value::Array(arr) => {
392 let items: Vec<String> = arr.iter().map(json_to_string).collect();
393 format!("[{}]", items.join(", "))
394 }
395 serde_json::Value::Object(obj) => {
396 if let Some(code) = obj.get("code").and_then(|v| v.as_str()) {
398 code.to_string()
399 } else {
400 value.to_string()
401 }
402 }
403 serde_json::Value::Null => "null".to_string(),
404 }
405}
406
407impl SpatialQuery for IfcxModel {
409 fn spatial_tree(&self) -> Option<&SpatialNode> {
410 self.spatial_tree.as_ref()
411 }
412
413 fn storeys(&self) -> Vec<StoreyInfo> {
414 let Some(tree) = &self.spatial_tree else {
415 return Vec::new();
416 };
417
418 let mut storeys = Vec::new();
419
420 fn find_storeys(node: &SpatialNode, storeys: &mut Vec<StoreyInfo>) {
421 if node.node_type == SpatialNodeType::Storey {
422 storeys.push(StoreyInfo {
423 id: node.id,
424 name: node.name.clone(),
425 elevation: node.elevation.unwrap_or(0.0),
426 element_count: node.element_count(),
427 });
428 }
429 for child in &node.children {
430 find_storeys(child, storeys);
431 }
432 }
433
434 find_storeys(tree, &mut storeys);
435 storeys.sort_by(|a, b| a.elevation.partial_cmp(&b.elevation).unwrap());
436 storeys
437 }
438
439 fn elements_in_storey(&self, storey_id: EntityId) -> Vec<EntityId> {
440 let Some(tree) = &self.spatial_tree else {
441 return Vec::new();
442 };
443
444 if let Some(storey_node) = tree.find(storey_id) {
445 storey_node.element_ids()
446 } else {
447 Vec::new()
448 }
449 }
450
451 fn containing_storey(&self, element_id: EntityId) -> Option<EntityId> {
452 let path = self.id_to_path.get(&element_id)?;
454 let mut current_path = self.nodes.get(path)?.parent.clone();
455
456 while let Some(p) = current_path {
457 let node = self.nodes.get(&p)?;
458 let ifc_type = extract_ifc_type(node);
459 if matches!(ifc_type, IfcType::IfcBuildingStorey) {
460 return self.path_to_id.get(&p).copied();
461 }
462 current_path = node.parent.clone();
463 }
464
465 None
466 }
467
468 fn search(&self, query: &str) -> Vec<EntityId> {
469 let query_lower = query.to_lowercase();
470 let mut results = Vec::new();
471
472 for (path, node) in &self.nodes {
473 if let Some(name) = node
475 .attributes
476 .get("bsi::ifc::prop::Name")
477 .or_else(|| node.attributes.get("bsi::ifc::prop::TypeName"))
478 .and_then(|v| v.as_str())
479 {
480 if name.to_lowercase().contains(&query_lower) {
481 if let Some(id) = self.path_to_id.get(path) {
482 results.push(*id);
483 continue;
484 }
485 }
486 }
487
488 let ifc_type = extract_ifc_type(node);
490 if ifc_type.name().to_lowercase().contains(&query_lower) {
491 if let Some(id) = self.path_to_id.get(path) {
492 results.push(*id);
493 }
494 }
495 }
496
497 results
498 }
499
500 fn elements_by_type(&self, ifc_type: &IfcType) -> Vec<EntityId> {
501 self.type_index.get(ifc_type).cloned().unwrap_or_default()
502 }
503}