1use ifc_lite_core::{EntityDecoder, EntityScanner, IfcType};
15use rustc_hash::{FxHashMap, FxHashSet};
16
17pub fn propagate_voids_via_aggregates(
36 void_index: &mut FxHashMap<u32, Vec<u32>>,
37 aggregate_children: &FxHashMap<u32, Vec<u32>>,
38) {
39 if void_index.is_empty() || aggregate_children.is_empty() {
40 return;
41 }
42
43 let hosts: Vec<u32> = void_index.keys().copied().collect();
45 let mut seen: FxHashSet<u32> = FxHashSet::default();
46
47 for host in hosts {
48 let openings = match void_index.get(&host) {
49 Some(list) if !list.is_empty() => list.clone(),
50 _ => continue,
51 };
52
53 walk_aggregate_descendants(host, aggregate_children, &mut seen, |part| {
55 let entry = void_index.entry(part).or_default();
56 for opening in &openings {
57 if !entry.contains(opening) {
58 entry.push(*opening);
59 }
60 }
61 true
62 });
63 }
64}
65
66fn walk_aggregate_descendants(
71 root: u32,
72 aggregate_children: &FxHashMap<u32, Vec<u32>>,
73 seen: &mut FxHashSet<u32>,
74 mut visit: impl FnMut(u32) -> bool,
75) {
76 let Some(kids) = aggregate_children.get(&root) else {
77 return;
78 };
79 seen.clear();
80 seen.insert(root);
81 let mut stack = kids.clone();
82 while let Some(id) = stack.pop() {
83 if seen.insert(id) && visit(id) {
84 if let Some(kids) = aggregate_children.get(&id) {
85 stack.extend(kids.iter().filter(|k| !seen.contains(k)));
86 }
87 }
88 }
89}
90
91pub fn build_aggregate_children_index<T>(
94 content: &T,
95 decoder: &mut EntityDecoder,
96) -> FxHashMap<u32, Vec<u32>>
97where
98 T: AsRef<[u8]> + ?Sized,
99{
100 let mut scanner = EntityScanner::new(content.as_ref());
101 let mut aggregate_children: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
102 while let Some((id, type_name, start, end)) = scanner.next_entity() {
103 if !ifc_lite_core::keyword_eq(type_name, "IFCRELAGGREGATES") {
104 continue;
105 }
106 let entity = match decoder.decode_at_with_id(id, start, end) {
107 Ok(e) => e,
108 Err(_) => continue,
109 };
110 let parent_id = match entity.get_ref(4) {
112 Some(id) => id,
113 None => continue,
114 };
115 let children: Vec<u32> = match entity.get(5).and_then(|a| a.as_list()) {
116 Some(list) => list
117 .iter()
118 .filter_map(|item| item.as_entity_ref())
119 .collect(),
120 None => continue,
121 };
122 if !children.is_empty() {
123 aggregate_children
124 .entry(parent_id)
125 .or_default()
126 .extend(children);
127 }
128 }
129 aggregate_children
130}
131
132#[must_use = "the returned part → parent map is needed to honour the merge-layers toggle"]
163pub fn propagate_voids_to_parts<T>(
164 void_index: &mut FxHashMap<u32, Vec<u32>>,
165 content: &T,
166 decoder: &mut EntityDecoder,
167) -> FxHashMap<u32, u32>
168where
169 T: AsRef<[u8]> + ?Sized,
170{
171 let content = content.as_ref();
172 let aggregate_children = build_aggregate_children_index(content, decoder);
173
174 propagate_voids_via_aggregates(void_index, &aggregate_children);
178
179 let info = |id: u32, decoder: &mut EntityDecoder| -> Option<(IfcType, bool)> {
181 let e = decoder.decode_by_id(id).ok()?;
182 Some((e.ifc_type.clone(), e.get(6).is_some_and(|a| !a.is_null())))
183 };
184
185 let mut part_to_parent: FxHashMap<u32, u32> = FxHashMap::default();
190 let mut seen: FxHashSet<u32> = FxHashSet::default();
191 for &parent_id in aggregate_children.keys() {
192 if !matches!(info(parent_id, decoder), Some((_, true))) {
193 continue;
194 }
195 walk_aggregate_descendants(parent_id, &aggregate_children, &mut seen, |id| {
196 match info(id, decoder) {
197 Some((IfcType::IfcBuildingElementPart, true)) => {
198 part_to_parent.insert(id, parent_id);
199 false
200 }
201 Some((_, has_repr)) => !has_repr,
202 None => false,
203 }
204 });
205 }
206
207 part_to_parent
208}
209
210#[must_use]
225pub fn compute_parts_to_skip<T>(
226 content: &T,
227 decoder: &mut EntityDecoder,
228) -> rustc_hash::FxHashSet<u32>
229where
230 T: AsRef<[u8]> + ?Sized,
231{
232 let content = content.as_ref();
233 let material_layer_index = crate::MaterialLayerIndex::from_content(content, decoder);
234 let mut void_index_scratch: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
235 let part_to_parent = propagate_voids_to_parts(&mut void_index_scratch, content, decoder);
236 part_to_parent
237 .into_iter()
238 .filter(|(_, parent_id)| material_layer_index.is_sliceable(*parent_id))
239 .map(|(part_id, _)| part_id)
240 .collect()
241}
242
243#[derive(Debug, Clone)]
248pub struct VoidIndex {
249 host_to_voids: FxHashMap<u32, Vec<u32>>,
251 void_to_host: FxHashMap<u32, u32>,
253 relationship_count: usize,
255}
256
257impl VoidIndex {
258 pub fn new() -> Self {
260 Self {
261 host_to_voids: FxHashMap::default(),
262 void_to_host: FxHashMap::default(),
263 relationship_count: 0,
264 }
265 }
266
267 pub fn from_content<T>(content: &T, decoder: &mut EntityDecoder) -> Self
279 where
280 T: AsRef<[u8]> + ?Sized,
281 {
282 let content = content.as_ref();
283 let mut index = Self::new();
284 let mut scanner = EntityScanner::new(content);
285
286 while let Some((_id, type_name, start, end)) = scanner.next_entity() {
287 if ifc_lite_core::keyword_eq(type_name, "IFCRELVOIDSELEMENT") {
289 if let Ok(entity) = decoder.decode_at(start, end) {
290 if let (Some(host_id), Some(void_id)) = (entity.get_ref(4), entity.get_ref(5)) {
297 index.add_relationship(host_id, void_id);
298 }
299 }
300 }
301 }
302
303 index
304 }
305
306 pub fn add_relationship(&mut self, host_id: u32, void_id: u32) {
308 self.host_to_voids.entry(host_id).or_default().push(void_id);
309 self.void_to_host.insert(void_id, host_id);
310 self.relationship_count += 1;
311 }
312
313 pub fn get_voids(&self, host_id: u32) -> &[u32] {
321 self.host_to_voids
322 .get(&host_id)
323 .map(|v| v.as_slice())
324 .unwrap_or(&[])
325 }
326
327 pub fn get_host(&self, void_id: u32) -> Option<u32> {
335 self.void_to_host.get(&void_id).copied()
336 }
337
338 pub fn has_voids(&self, host_id: u32) -> bool {
340 self.host_to_voids
341 .get(&host_id)
342 .map(|v| !v.is_empty())
343 .unwrap_or(false)
344 }
345
346 pub fn void_count(&self, host_id: u32) -> usize {
348 self.host_to_voids
349 .get(&host_id)
350 .map(|v| v.len())
351 .unwrap_or(0)
352 }
353
354 pub fn host_count(&self) -> usize {
356 self.host_to_voids.len()
357 }
358
359 pub fn total_relationships(&self) -> usize {
361 self.relationship_count
362 }
363
364 pub fn iter(&self) -> impl Iterator<Item = (u32, &[u32])> {
366 self.host_to_voids.iter().map(|(k, v)| (*k, v.as_slice()))
367 }
368
369 pub fn hosts_with_voids(&self) -> Vec<u32> {
371 self.host_to_voids.keys().copied().collect()
372 }
373
374 pub fn is_void(&self, entity_id: u32) -> bool {
376 self.void_to_host.contains_key(&entity_id)
377 }
378
379 pub fn is_host_with_voids(&self, entity_id: u32) -> bool {
381 self.host_to_voids.contains_key(&entity_id)
382 }
383}
384
385impl Default for VoidIndex {
386 fn default() -> Self {
387 Self::new()
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_void_index_basic() {
397 let mut index = VoidIndex::new();
398
399 index.add_relationship(100, 200);
401 index.add_relationship(100, 201);
402 index.add_relationship(101, 202);
403
404 assert_eq!(index.get_voids(100), &[200, 201]);
406 assert_eq!(index.get_voids(101), &[202]);
407 assert!(index.get_voids(999).is_empty());
408
409 assert_eq!(index.get_host(200), Some(100));
411 assert_eq!(index.get_host(202), Some(101));
412 assert_eq!(index.get_host(999), None);
413
414 assert_eq!(index.void_count(100), 2);
416 assert_eq!(index.void_count(101), 1);
417 assert_eq!(index.host_count(), 2);
418 assert_eq!(index.total_relationships(), 3);
419 }
420
421 #[test]
422 fn test_void_index_has_voids() {
423 let mut index = VoidIndex::new();
424 index.add_relationship(100, 200);
425
426 assert!(index.has_voids(100));
427 assert!(!index.has_voids(999));
428 }
429
430 #[test]
431 fn test_void_index_is_void() {
432 let mut index = VoidIndex::new();
433 index.add_relationship(100, 200);
434
435 assert!(index.is_void(200));
436 assert!(!index.is_void(100));
437 assert!(!index.is_void(999));
438 }
439
440 #[test]
441 fn test_void_index_hosts_with_voids() {
442 let mut index = VoidIndex::new();
443 index.add_relationship(100, 200);
444 index.add_relationship(101, 201);
445 index.add_relationship(102, 202);
446
447 let hosts = index.hosts_with_voids();
448 assert_eq!(hosts.len(), 3);
449 assert!(hosts.contains(&100));
450 assert!(hosts.contains(&101));
451 assert!(hosts.contains(&102));
452 }
453
454 use ifc_lite_core::EntityDecoder;
464
465 fn three_layer_wall_with_voids_ifc() -> String {
469 r#"ISO-10303-21;
470HEADER;
471FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
472FILE_NAME('test.ifc','2024-01-01T00:00:00',(''),(''),'','','');
473FILE_SCHEMA(('IFC4'));
474ENDSEC;
475DATA;
476#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
477#50=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#40));
478#40=IFCEXTRUDEDAREASOLID($,$,$,3.0);
479#100=IFCWALL('0001wall',$,'Parent',$,$,$,#51,$,$);
480#101=IFCBUILDINGELEMENTPART('0001p01',$,'L0',$,$,$,#51,$,$);
481#102=IFCBUILDINGELEMENTPART('0001p02',$,'L1',$,$,$,#51,$,$);
482#103=IFCBUILDINGELEMENTPART('0001p03',$,'L2',$,$,$,#51,$,$);
483#200=IFCOPENINGELEMENT('0001op',$,'Opening',$,$,$,#51,$,$);
484#210=IFCRELVOIDSELEMENT('0001rv',$,$,$,#100,#200);
485#300=IFCRELAGGREGATES('0001ra',$,$,$,#100,(#101,#102,#103));
486ENDSEC;
487END-ISO-10303-21;
488"#
489 .to_string()
490 }
491
492 fn parts_only_aggregate_ifc() -> String {
495 r#"ISO-10303-21;
496HEADER;
497FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
498FILE_NAME('test.ifc','2024-01-01T00:00:00',(''),(''),'','','');
499FILE_SCHEMA(('IFC4'));
500ENDSEC;
501DATA;
502#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
503#50=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#40));
504#40=IFCEXTRUDEDAREASOLID($,$,$,3.0);
505#100=IFCWALL('0001wall',$,'Parent',$,$,$,$,$,$);
506#101=IFCBUILDINGELEMENTPART('0001p01',$,'L0',$,$,$,#51,$,$);
507#102=IFCBUILDINGELEMENTPART('0001p02',$,'L1',$,$,$,#51,$,$);
508#103=IFCBUILDINGELEMENTPART('0001p03',$,'L2',$,$,$,#51,$,$);
509#300=IFCRELAGGREGATES('0001ra',$,$,$,#100,(#101,#102,#103));
510ENDSEC;
511END-ISO-10303-21;
512"#
513 .to_string()
514 }
515
516 #[test]
517 fn propagate_voids_returns_part_to_parent_map() {
518 let content = three_layer_wall_with_voids_ifc();
519 let mut decoder = EntityDecoder::new(&content);
520
521 let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
524 void_index.insert(100, vec![200]);
525
526 let part_to_parent = propagate_voids_to_parts(&mut void_index, &content, &mut decoder);
527
528 assert_eq!(part_to_parent.len(), 3);
530 assert_eq!(part_to_parent.get(&101).copied(), Some(100));
531 assert_eq!(part_to_parent.get(&102).copied(), Some(100));
532 assert_eq!(part_to_parent.get(&103).copied(), Some(100));
533
534 assert_eq!(void_index.get(&101).map(Vec::as_slice), Some(&[200u32][..]));
536 assert_eq!(void_index.get(&102).map(Vec::as_slice), Some(&[200u32][..]));
537 assert_eq!(void_index.get(&103).map(Vec::as_slice), Some(&[200u32][..]));
538 }
539
540 #[test]
541 fn propagate_voids_skips_parents_without_representation() {
542 let content = parts_only_aggregate_ifc();
543 let mut decoder = EntityDecoder::new(&content);
544
545 let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
547
548 let part_to_parent = propagate_voids_to_parts(&mut void_index, &content, &mut decoder);
549
550 assert!(
553 part_to_parent.is_empty(),
554 "expected empty map when parent has no representation, got {:?}",
555 part_to_parent
556 );
557 }
558
559 #[test]
560 fn propagate_voids_returns_empty_map_when_no_aggregates() {
561 let empty = r#"ISO-10303-21;
562HEADER;
563FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
564FILE_NAME('t.ifc','2024-01-01T00:00:00',(''),(''),'','','');
565FILE_SCHEMA(('IFC4'));
566ENDSEC;
567DATA;
568#1=IFCWALL('0001w',$,'L',$,$,$,$,$,$);
569ENDSEC;
570END-ISO-10303-21;
571"#
572 .to_string();
573 let mut decoder = EntityDecoder::new(&empty);
574 let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
575 let part_to_parent = propagate_voids_to_parts(&mut void_index, &empty, &mut decoder);
576 assert!(part_to_parent.is_empty());
577 assert!(void_index.is_empty());
578 }
579
580 fn agg_map(pairs: &[(u32, &[u32])]) -> FxHashMap<u32, Vec<u32>> {
583 pairs.iter().map(|(k, v)| (*k, v.to_vec())).collect()
584 }
585
586 #[test]
587 fn propagate_voids_walks_full_aggregate_tree() {
588 let mut void_index = agg_map(&[(100, &[200, 201])]);
592 let aggregate_children = agg_map(&[(100, &[110, 111]), (110, &[120])]);
593
594 propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
595
596 let expected = [200, 201];
597 for part in &[110, 111, 120] {
598 let got = void_index.get(part).expect("part should have voids");
599 assert_eq!(
600 got.iter().copied().collect::<std::collections::HashSet<_>>(),
601 expected.iter().copied().collect::<std::collections::HashSet<_>>(),
602 "part #{part} should receive both openings",
603 );
604 }
605 assert_eq!(void_index.get(&100), Some(&vec![200, 201]));
607 }
608
609 #[test]
610 fn propagate_voids_deduplicates_existing_part_voids() {
611 let mut void_index = agg_map(&[(100, &[200]), (110, &[999])]);
614 let aggregate_children = agg_map(&[(100, &[110])]);
615
616 propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
617
618 let mut part_voids = void_index.get(&110).unwrap().clone();
619 part_voids.sort();
620 assert_eq!(part_voids, vec![200, 999]);
621 }
622
623 #[test]
624 fn propagate_voids_handles_aggregate_cycles() {
625 let mut void_index = agg_map(&[(100, &[200])]);
629 let aggregate_children = agg_map(&[(100, &[110]), (110, &[120]), (120, &[110])]);
630
631 propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
632
633 assert_eq!(void_index.get(&110), Some(&vec![200]));
634 assert_eq!(void_index.get(&120), Some(&vec![200]));
635 }
636
637 #[test]
638 fn propagate_voids_no_op_when_host_has_no_parts() {
639 let mut void_index = agg_map(&[(100, &[200])]);
640 let aggregate_children = agg_map(&[(101, &[110])]); let before = void_index.clone();
642
643 propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
644
645 assert_eq!(void_index, before);
646 }
647
648 #[test]
649 fn propagate_voids_to_parts_covers_non_bep_descendants() {
650 let content = r#"ISO-10303-21;
654HEADER;
655FILE_DESCRIPTION((''),'2;1');
656FILE_NAME('t.ifc','2024-01-01T00:00:00',(''),(''),'','','');
657FILE_SCHEMA(('IFC4'));
658ENDSEC;
659DATA;
660#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
661#50=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#40));
662#40=IFCEXTRUDEDAREASOLID($,$,$,3.0);
663#100=IFCROOF('0001roof',$,'Roof',$,$,$,$,$,$);
664#101=IFCSLAB('0001slab',$,'Pitch',$,$,$,#51,$,$);
665#200=IFCOPENINGELEMENT('0001op',$,'Skylight',$,$,$,#51,$,$);
666#210=IFCRELVOIDSELEMENT('0001rv',$,$,$,#100,#200);
667#300=IFCRELAGGREGATES('0001ra',$,$,$,#100,(#101));
668ENDSEC;
669END-ISO-10303-21;
670"#;
671 let mut decoder = EntityDecoder::new(content);
672 let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
673 void_index.insert(100, vec![200]);
674
675 let part_to_parent = propagate_voids_to_parts(&mut void_index, content, &mut decoder);
676
677 assert_eq!(void_index.get(&101), Some(&vec![200]));
679 assert!(part_to_parent.is_empty());
681 }
682}