1use ifc_lite_core::{EntityDecoder, EntityScanner, IfcType};
15use rustc_hash::{FxHashMap, FxHashSet};
16
17pub fn propagate_voids_via_aggregates(
34 void_index: &mut FxHashMap<u32, Vec<u32>>,
35 aggregate_children: &FxHashMap<u32, Vec<u32>>,
36) {
37 if void_index.is_empty() || aggregate_children.is_empty() {
38 return;
39 }
40
41 let hosts: Vec<u32> = void_index.keys().copied().collect();
43
44 for host in hosts {
45 let openings = match void_index.get(&host) {
46 Some(list) if !list.is_empty() => list.clone(),
47 _ => continue,
48 };
49
50 let mut stack: Vec<u32> = match aggregate_children.get(&host) {
52 Some(kids) => kids.clone(),
53 None => continue,
54 };
55 let mut seen: FxHashSet<u32> = FxHashSet::default();
56 seen.insert(host);
57
58 while let Some(part) = stack.pop() {
59 if !seen.insert(part) {
60 continue;
61 }
62
63 let entry = void_index.entry(part).or_default();
65 for opening in &openings {
66 if !entry.contains(opening) {
67 entry.push(*opening);
68 }
69 }
70
71 if let Some(grand_kids) = aggregate_children.get(&part) {
72 for kid in grand_kids {
73 if !seen.contains(kid) {
74 stack.push(*kid);
75 }
76 }
77 }
78 }
79 }
80}
81
82pub fn build_aggregate_children_index<T>(
85 content: &T,
86 decoder: &mut EntityDecoder,
87) -> FxHashMap<u32, Vec<u32>>
88where
89 T: AsRef<[u8]> + ?Sized,
90{
91 let mut scanner = EntityScanner::new(content.as_ref());
92 let mut aggregate_children: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
93 while let Some((id, type_name, start, end)) = scanner.next_entity() {
94 if type_name != "IFCRELAGGREGATES" {
95 continue;
96 }
97 let entity = match decoder.decode_at_with_id(id, start, end) {
98 Ok(e) => e,
99 Err(_) => continue,
100 };
101 let parent_id = match entity.get_ref(4) {
103 Some(id) => id,
104 None => continue,
105 };
106 let children: Vec<u32> = match entity.get(5).and_then(|a| a.as_list()) {
107 Some(list) => list
108 .iter()
109 .filter_map(|item| item.as_entity_ref())
110 .collect(),
111 None => continue,
112 };
113 if !children.is_empty() {
114 aggregate_children
115 .entry(parent_id)
116 .or_default()
117 .extend(children);
118 }
119 }
120 aggregate_children
121}
122
123#[must_use = "the returned part → parent map is needed to honour the merge-layers toggle"]
148pub fn propagate_voids_to_parts<T>(
149 void_index: &mut FxHashMap<u32, Vec<u32>>,
150 content: &T,
151 decoder: &mut EntityDecoder,
152) -> FxHashMap<u32, u32>
153where
154 T: AsRef<[u8]> + ?Sized,
155{
156 let content = content.as_ref();
157 let aggregate_children = build_aggregate_children_index(content, decoder);
158
159 propagate_voids_via_aggregates(void_index, &aggregate_children);
163
164 let mut part_to_parent: FxHashMap<u32, u32> = FxHashMap::default();
168 for (&parent_id, children) in &aggregate_children {
169 let parent_has_repr = decoder
170 .decode_by_id(parent_id)
171 .map(|p| p.get(6).map(|a| !a.is_null()).unwrap_or(false))
172 .unwrap_or(false);
173 if !parent_has_repr {
174 continue;
175 }
176 for &child_id in children {
177 if let Ok(child) = decoder.decode_by_id(child_id) {
178 if child.ifc_type == IfcType::IfcBuildingElementPart {
179 let has_repr = child.get(6).map(|a| !a.is_null()).unwrap_or(false);
180 if has_repr {
181 part_to_parent.insert(child_id, parent_id);
182 }
183 }
184 }
185 }
186 }
187
188 part_to_parent
189}
190
191#[must_use]
204pub fn compute_parts_to_skip<T>(
205 content: &T,
206 decoder: &mut EntityDecoder,
207) -> rustc_hash::FxHashSet<u32>
208where
209 T: AsRef<[u8]> + ?Sized,
210{
211 let content = content.as_ref();
212 let material_layer_index = crate::MaterialLayerIndex::from_content(content, decoder);
213 let mut void_index_scratch: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
214 let part_to_parent = propagate_voids_to_parts(&mut void_index_scratch, content, decoder);
215 part_to_parent
216 .into_iter()
217 .filter(|(_, parent_id)| material_layer_index.is_sliceable(*parent_id))
218 .map(|(part_id, _)| part_id)
219 .collect()
220}
221
222#[derive(Debug, Clone)]
227pub struct VoidIndex {
228 host_to_voids: FxHashMap<u32, Vec<u32>>,
230 void_to_host: FxHashMap<u32, u32>,
232 relationship_count: usize,
234}
235
236impl VoidIndex {
237 pub fn new() -> Self {
239 Self {
240 host_to_voids: FxHashMap::default(),
241 void_to_host: FxHashMap::default(),
242 relationship_count: 0,
243 }
244 }
245
246 pub fn from_content<T>(content: &T, decoder: &mut EntityDecoder) -> Self
258 where
259 T: AsRef<[u8]> + ?Sized,
260 {
261 let content = content.as_ref();
262 let mut index = Self::new();
263 let mut scanner = EntityScanner::new(content);
264
265 while let Some((_id, type_name, start, end)) = scanner.next_entity() {
266 if type_name == "IFCRELVOIDSELEMENT" {
268 if let Ok(entity) = decoder.decode_at(start, end) {
269 if let (Some(host_id), Some(void_id)) = (entity.get_ref(4), entity.get_ref(5)) {
276 index.add_relationship(host_id, void_id);
277 }
278 }
279 }
280 }
281
282 index
283 }
284
285 pub fn add_relationship(&mut self, host_id: u32, void_id: u32) {
287 self.host_to_voids.entry(host_id).or_default().push(void_id);
288 self.void_to_host.insert(void_id, host_id);
289 self.relationship_count += 1;
290 }
291
292 pub fn get_voids(&self, host_id: u32) -> &[u32] {
300 self.host_to_voids
301 .get(&host_id)
302 .map(|v| v.as_slice())
303 .unwrap_or(&[])
304 }
305
306 pub fn get_host(&self, void_id: u32) -> Option<u32> {
314 self.void_to_host.get(&void_id).copied()
315 }
316
317 pub fn has_voids(&self, host_id: u32) -> bool {
319 self.host_to_voids
320 .get(&host_id)
321 .map(|v| !v.is_empty())
322 .unwrap_or(false)
323 }
324
325 pub fn void_count(&self, host_id: u32) -> usize {
327 self.host_to_voids
328 .get(&host_id)
329 .map(|v| v.len())
330 .unwrap_or(0)
331 }
332
333 pub fn host_count(&self) -> usize {
335 self.host_to_voids.len()
336 }
337
338 pub fn total_relationships(&self) -> usize {
340 self.relationship_count
341 }
342
343 pub fn iter(&self) -> impl Iterator<Item = (u32, &[u32])> {
345 self.host_to_voids.iter().map(|(k, v)| (*k, v.as_slice()))
346 }
347
348 pub fn hosts_with_voids(&self) -> Vec<u32> {
350 self.host_to_voids.keys().copied().collect()
351 }
352
353 pub fn is_void(&self, entity_id: u32) -> bool {
355 self.void_to_host.contains_key(&entity_id)
356 }
357
358 pub fn is_host_with_voids(&self, entity_id: u32) -> bool {
360 self.host_to_voids.contains_key(&entity_id)
361 }
362}
363
364impl Default for VoidIndex {
365 fn default() -> Self {
366 Self::new()
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn test_void_index_basic() {
376 let mut index = VoidIndex::new();
377
378 index.add_relationship(100, 200);
380 index.add_relationship(100, 201);
381 index.add_relationship(101, 202);
382
383 assert_eq!(index.get_voids(100), &[200, 201]);
385 assert_eq!(index.get_voids(101), &[202]);
386 assert!(index.get_voids(999).is_empty());
387
388 assert_eq!(index.get_host(200), Some(100));
390 assert_eq!(index.get_host(202), Some(101));
391 assert_eq!(index.get_host(999), None);
392
393 assert_eq!(index.void_count(100), 2);
395 assert_eq!(index.void_count(101), 1);
396 assert_eq!(index.host_count(), 2);
397 assert_eq!(index.total_relationships(), 3);
398 }
399
400 #[test]
401 fn test_void_index_has_voids() {
402 let mut index = VoidIndex::new();
403 index.add_relationship(100, 200);
404
405 assert!(index.has_voids(100));
406 assert!(!index.has_voids(999));
407 }
408
409 #[test]
410 fn test_void_index_is_void() {
411 let mut index = VoidIndex::new();
412 index.add_relationship(100, 200);
413
414 assert!(index.is_void(200));
415 assert!(!index.is_void(100));
416 assert!(!index.is_void(999));
417 }
418
419 #[test]
420 fn test_void_index_hosts_with_voids() {
421 let mut index = VoidIndex::new();
422 index.add_relationship(100, 200);
423 index.add_relationship(101, 201);
424 index.add_relationship(102, 202);
425
426 let hosts = index.hosts_with_voids();
427 assert_eq!(hosts.len(), 3);
428 assert!(hosts.contains(&100));
429 assert!(hosts.contains(&101));
430 assert!(hosts.contains(&102));
431 }
432
433 use ifc_lite_core::EntityDecoder;
443
444 fn three_layer_wall_with_voids_ifc() -> String {
448 r#"ISO-10303-21;
449HEADER;
450FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
451FILE_NAME('test.ifc','2024-01-01T00:00:00',(''),(''),'','','');
452FILE_SCHEMA(('IFC4'));
453ENDSEC;
454DATA;
455#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
456#50=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#40));
457#40=IFCEXTRUDEDAREASOLID($,$,$,3.0);
458#100=IFCWALL('0001wall',$,'Parent',$,$,$,#51,$,$);
459#101=IFCBUILDINGELEMENTPART('0001p01',$,'L0',$,$,$,#51,$,$);
460#102=IFCBUILDINGELEMENTPART('0001p02',$,'L1',$,$,$,#51,$,$);
461#103=IFCBUILDINGELEMENTPART('0001p03',$,'L2',$,$,$,#51,$,$);
462#200=IFCOPENINGELEMENT('0001op',$,'Opening',$,$,$,#51,$,$);
463#210=IFCRELVOIDSELEMENT('0001rv',$,$,$,#100,#200);
464#300=IFCRELAGGREGATES('0001ra',$,$,$,#100,(#101,#102,#103));
465ENDSEC;
466END-ISO-10303-21;
467"#
468 .to_string()
469 }
470
471 fn parts_only_aggregate_ifc() -> String {
474 r#"ISO-10303-21;
475HEADER;
476FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
477FILE_NAME('test.ifc','2024-01-01T00:00:00',(''),(''),'','','');
478FILE_SCHEMA(('IFC4'));
479ENDSEC;
480DATA;
481#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
482#50=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#40));
483#40=IFCEXTRUDEDAREASOLID($,$,$,3.0);
484#100=IFCWALL('0001wall',$,'Parent',$,$,$,$,$,$);
485#101=IFCBUILDINGELEMENTPART('0001p01',$,'L0',$,$,$,#51,$,$);
486#102=IFCBUILDINGELEMENTPART('0001p02',$,'L1',$,$,$,#51,$,$);
487#103=IFCBUILDINGELEMENTPART('0001p03',$,'L2',$,$,$,#51,$,$);
488#300=IFCRELAGGREGATES('0001ra',$,$,$,#100,(#101,#102,#103));
489ENDSEC;
490END-ISO-10303-21;
491"#
492 .to_string()
493 }
494
495 #[test]
496 fn propagate_voids_returns_part_to_parent_map() {
497 let content = three_layer_wall_with_voids_ifc();
498 let mut decoder = EntityDecoder::new(&content);
499
500 let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
503 void_index.insert(100, vec![200]);
504
505 let part_to_parent = propagate_voids_to_parts(&mut void_index, &content, &mut decoder);
506
507 assert_eq!(part_to_parent.len(), 3);
509 assert_eq!(part_to_parent.get(&101).copied(), Some(100));
510 assert_eq!(part_to_parent.get(&102).copied(), Some(100));
511 assert_eq!(part_to_parent.get(&103).copied(), Some(100));
512
513 assert_eq!(void_index.get(&101).map(Vec::as_slice), Some(&[200u32][..]));
515 assert_eq!(void_index.get(&102).map(Vec::as_slice), Some(&[200u32][..]));
516 assert_eq!(void_index.get(&103).map(Vec::as_slice), Some(&[200u32][..]));
517 }
518
519 #[test]
520 fn propagate_voids_skips_parents_without_representation() {
521 let content = parts_only_aggregate_ifc();
522 let mut decoder = EntityDecoder::new(&content);
523
524 let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
526
527 let part_to_parent = propagate_voids_to_parts(&mut void_index, &content, &mut decoder);
528
529 assert!(
532 part_to_parent.is_empty(),
533 "expected empty map when parent has no representation, got {:?}",
534 part_to_parent
535 );
536 }
537
538 #[test]
539 fn propagate_voids_returns_empty_map_when_no_aggregates() {
540 let empty = r#"ISO-10303-21;
541HEADER;
542FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
543FILE_NAME('t.ifc','2024-01-01T00:00:00',(''),(''),'','','');
544FILE_SCHEMA(('IFC4'));
545ENDSEC;
546DATA;
547#1=IFCWALL('0001w',$,'L',$,$,$,$,$,$);
548ENDSEC;
549END-ISO-10303-21;
550"#
551 .to_string();
552 let mut decoder = EntityDecoder::new(&empty);
553 let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
554 let part_to_parent = propagate_voids_to_parts(&mut void_index, &empty, &mut decoder);
555 assert!(part_to_parent.is_empty());
556 assert!(void_index.is_empty());
557 }
558
559 fn agg_map(pairs: &[(u32, &[u32])]) -> FxHashMap<u32, Vec<u32>> {
562 pairs.iter().map(|(k, v)| (*k, v.to_vec())).collect()
563 }
564
565 #[test]
566 fn propagate_voids_walks_full_aggregate_tree() {
567 let mut void_index = agg_map(&[(100, &[200, 201])]);
571 let aggregate_children = agg_map(&[(100, &[110, 111]), (110, &[120])]);
572
573 propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
574
575 let expected = [200, 201];
576 for part in &[110, 111, 120] {
577 let got = void_index.get(part).expect("part should have voids");
578 assert_eq!(
579 got.iter().copied().collect::<std::collections::HashSet<_>>(),
580 expected.iter().copied().collect::<std::collections::HashSet<_>>(),
581 "part #{part} should receive both openings",
582 );
583 }
584 assert_eq!(void_index.get(&100), Some(&vec![200, 201]));
586 }
587
588 #[test]
589 fn propagate_voids_deduplicates_existing_part_voids() {
590 let mut void_index = agg_map(&[(100, &[200]), (110, &[999])]);
593 let aggregate_children = agg_map(&[(100, &[110])]);
594
595 propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
596
597 let mut part_voids = void_index.get(&110).unwrap().clone();
598 part_voids.sort();
599 assert_eq!(part_voids, vec![200, 999]);
600 }
601
602 #[test]
603 fn propagate_voids_handles_aggregate_cycles() {
604 let mut void_index = agg_map(&[(100, &[200])]);
608 let aggregate_children = agg_map(&[(100, &[110]), (110, &[120]), (120, &[110])]);
609
610 propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
611
612 assert_eq!(void_index.get(&110), Some(&vec![200]));
613 assert_eq!(void_index.get(&120), Some(&vec![200]));
614 }
615
616 #[test]
617 fn propagate_voids_no_op_when_host_has_no_parts() {
618 let mut void_index = agg_map(&[(100, &[200])]);
619 let aggregate_children = agg_map(&[(101, &[110])]); let before = void_index.clone();
621
622 propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
623
624 assert_eq!(void_index, before);
625 }
626
627 #[test]
628 fn propagate_voids_to_parts_covers_non_bep_descendants() {
629 let content = r#"ISO-10303-21;
633HEADER;
634FILE_DESCRIPTION((''),'2;1');
635FILE_NAME('t.ifc','2024-01-01T00:00:00',(''),(''),'','','');
636FILE_SCHEMA(('IFC4'));
637ENDSEC;
638DATA;
639#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
640#50=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#40));
641#40=IFCEXTRUDEDAREASOLID($,$,$,3.0);
642#100=IFCROOF('0001roof',$,'Roof',$,$,$,$,$,$);
643#101=IFCSLAB('0001slab',$,'Pitch',$,$,$,#51,$,$);
644#200=IFCOPENINGELEMENT('0001op',$,'Skylight',$,$,$,#51,$,$);
645#210=IFCRELVOIDSELEMENT('0001rv',$,$,$,#100,#200);
646#300=IFCRELAGGREGATES('0001ra',$,$,$,#100,(#101));
647ENDSEC;
648END-ISO-10303-21;
649"#;
650 let mut decoder = EntityDecoder::new(content);
651 let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
652 void_index.insert(100, vec![200]);
653
654 let part_to_parent = propagate_voids_to_parts(&mut void_index, content, &mut decoder);
655
656 assert_eq!(void_index.get(&101), Some(&vec![200]));
658 assert!(part_to_parent.is_empty());
660 }
661}