use ifc_lite_core::{EntityDecoder, EntityScanner, IfcType};
use rustc_hash::{FxHashMap, FxHashSet};
pub fn propagate_voids_via_aggregates(
void_index: &mut FxHashMap<u32, Vec<u32>>,
aggregate_children: &FxHashMap<u32, Vec<u32>>,
) {
if void_index.is_empty() || aggregate_children.is_empty() {
return;
}
let hosts: Vec<u32> = void_index.keys().copied().collect();
for host in hosts {
let openings = match void_index.get(&host) {
Some(list) if !list.is_empty() => list.clone(),
_ => continue,
};
let mut stack: Vec<u32> = match aggregate_children.get(&host) {
Some(kids) => kids.clone(),
None => continue,
};
let mut seen: FxHashSet<u32> = FxHashSet::default();
seen.insert(host);
while let Some(part) = stack.pop() {
if !seen.insert(part) {
continue;
}
let entry = void_index.entry(part).or_default();
for opening in &openings {
if !entry.contains(opening) {
entry.push(*opening);
}
}
if let Some(grand_kids) = aggregate_children.get(&part) {
for kid in grand_kids {
if !seen.contains(kid) {
stack.push(*kid);
}
}
}
}
}
}
pub fn build_aggregate_children_index(
content: &str,
decoder: &mut EntityDecoder,
) -> FxHashMap<u32, Vec<u32>> {
let mut scanner = EntityScanner::new(content);
let mut aggregate_children: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if type_name != "IFCRELAGGREGATES" {
continue;
}
let entity = match decoder.decode_at_with_id(id, start, end) {
Ok(e) => e,
Err(_) => continue,
};
let parent_id = match entity.get_ref(4) {
Some(id) => id,
None => continue,
};
let children: Vec<u32> = match entity.get(5).and_then(|a| a.as_list()) {
Some(list) => list
.iter()
.filter_map(|item| item.as_entity_ref())
.collect(),
None => continue,
};
if !children.is_empty() {
aggregate_children
.entry(parent_id)
.or_default()
.extend(children);
}
}
aggregate_children
}
#[must_use = "the returned part → parent map is needed to honour the merge-layers toggle"]
pub fn propagate_voids_to_parts(
void_index: &mut FxHashMap<u32, Vec<u32>>,
content: &str,
decoder: &mut EntityDecoder,
) -> FxHashMap<u32, u32> {
let aggregate_children = build_aggregate_children_index(content, decoder);
propagate_voids_via_aggregates(void_index, &aggregate_children);
let mut part_to_parent: FxHashMap<u32, u32> = FxHashMap::default();
for (&parent_id, children) in &aggregate_children {
let parent_has_repr = decoder
.decode_by_id(parent_id)
.map(|p| p.get(6).map(|a| !a.is_null()).unwrap_or(false))
.unwrap_or(false);
if !parent_has_repr {
continue;
}
for &child_id in children {
if let Ok(child) = decoder.decode_by_id(child_id) {
if child.ifc_type == IfcType::IfcBuildingElementPart {
let has_repr = child.get(6).map(|a| !a.is_null()).unwrap_or(false);
if has_repr {
part_to_parent.insert(child_id, parent_id);
}
}
}
}
}
part_to_parent
}
#[must_use]
pub fn compute_parts_to_skip(
content: &str,
decoder: &mut EntityDecoder,
) -> rustc_hash::FxHashSet<u32> {
let material_layer_index = crate::MaterialLayerIndex::from_content(content, decoder);
let mut void_index_scratch: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
let part_to_parent = propagate_voids_to_parts(&mut void_index_scratch, content, decoder);
part_to_parent
.into_iter()
.filter(|(_, parent_id)| material_layer_index.is_sliceable(*parent_id))
.map(|(part_id, _)| part_id)
.collect()
}
#[derive(Debug, Clone)]
pub struct VoidIndex {
host_to_voids: FxHashMap<u32, Vec<u32>>,
void_to_host: FxHashMap<u32, u32>,
relationship_count: usize,
}
impl VoidIndex {
pub fn new() -> Self {
Self {
host_to_voids: FxHashMap::default(),
void_to_host: FxHashMap::default(),
relationship_count: 0,
}
}
pub fn from_content(content: &str, decoder: &mut EntityDecoder) -> Self {
let mut index = Self::new();
let mut scanner = EntityScanner::new(content);
while let Some((_id, type_name, start, end)) = scanner.next_entity() {
if type_name == "IFCRELVOIDSELEMENT" {
if let Ok(entity) = decoder.decode_at(start, end) {
if let (Some(host_id), Some(void_id)) = (entity.get_ref(4), entity.get_ref(5)) {
index.add_relationship(host_id, void_id);
}
}
}
}
index
}
pub fn add_relationship(&mut self, host_id: u32, void_id: u32) {
self.host_to_voids.entry(host_id).or_default().push(void_id);
self.void_to_host.insert(void_id, host_id);
self.relationship_count += 1;
}
pub fn get_voids(&self, host_id: u32) -> &[u32] {
self.host_to_voids
.get(&host_id)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn get_host(&self, void_id: u32) -> Option<u32> {
self.void_to_host.get(&void_id).copied()
}
pub fn has_voids(&self, host_id: u32) -> bool {
self.host_to_voids
.get(&host_id)
.map(|v| !v.is_empty())
.unwrap_or(false)
}
pub fn void_count(&self, host_id: u32) -> usize {
self.host_to_voids
.get(&host_id)
.map(|v| v.len())
.unwrap_or(0)
}
pub fn host_count(&self) -> usize {
self.host_to_voids.len()
}
pub fn total_relationships(&self) -> usize {
self.relationship_count
}
pub fn iter(&self) -> impl Iterator<Item = (u32, &[u32])> {
self.host_to_voids.iter().map(|(k, v)| (*k, v.as_slice()))
}
pub fn hosts_with_voids(&self) -> Vec<u32> {
self.host_to_voids.keys().copied().collect()
}
pub fn is_void(&self, entity_id: u32) -> bool {
self.void_to_host.contains_key(&entity_id)
}
pub fn is_host_with_voids(&self, entity_id: u32) -> bool {
self.host_to_voids.contains_key(&entity_id)
}
}
impl Default for VoidIndex {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct VoidStatistics {
pub hosts_with_voids: usize,
pub total_voids: usize,
pub max_voids_per_host: usize,
pub avg_voids_per_host: f64,
pub hosts_with_many_voids: usize,
}
impl VoidStatistics {
pub fn from_index(index: &VoidIndex) -> Self {
let hosts_with_voids = index.host_count();
let total_voids = index.total_relationships();
let max_voids_per_host = index
.host_to_voids
.values()
.map(|v| v.len())
.max()
.unwrap_or(0);
let avg_voids_per_host = if hosts_with_voids > 0 {
total_voids as f64 / hosts_with_voids as f64
} else {
0.0
};
let hosts_with_many_voids = index
.host_to_voids
.values()
.filter(|v| v.len() > 10)
.count();
Self {
hosts_with_voids,
total_voids,
max_voids_per_host,
avg_voids_per_host,
hosts_with_many_voids,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_void_index_basic() {
let mut index = VoidIndex::new();
index.add_relationship(100, 200);
index.add_relationship(100, 201);
index.add_relationship(101, 202);
assert_eq!(index.get_voids(100), &[200, 201]);
assert_eq!(index.get_voids(101), &[202]);
assert!(index.get_voids(999).is_empty());
assert_eq!(index.get_host(200), Some(100));
assert_eq!(index.get_host(202), Some(101));
assert_eq!(index.get_host(999), None);
assert_eq!(index.void_count(100), 2);
assert_eq!(index.void_count(101), 1);
assert_eq!(index.host_count(), 2);
assert_eq!(index.total_relationships(), 3);
}
#[test]
fn test_void_index_has_voids() {
let mut index = VoidIndex::new();
index.add_relationship(100, 200);
assert!(index.has_voids(100));
assert!(!index.has_voids(999));
}
#[test]
fn test_void_index_is_void() {
let mut index = VoidIndex::new();
index.add_relationship(100, 200);
assert!(index.is_void(200));
assert!(!index.is_void(100));
assert!(!index.is_void(999));
}
#[test]
fn test_void_index_hosts_with_voids() {
let mut index = VoidIndex::new();
index.add_relationship(100, 200);
index.add_relationship(101, 201);
index.add_relationship(102, 202);
let hosts = index.hosts_with_voids();
assert_eq!(hosts.len(), 3);
assert!(hosts.contains(&100));
assert!(hosts.contains(&101));
assert!(hosts.contains(&102));
}
#[test]
fn test_void_statistics() {
let mut index = VoidIndex::new();
index.add_relationship(100, 200);
index.add_relationship(100, 201);
index.add_relationship(100, 202);
index.add_relationship(101, 203);
let stats = VoidStatistics::from_index(&index);
assert_eq!(stats.hosts_with_voids, 2);
assert_eq!(stats.total_voids, 4);
assert_eq!(stats.max_voids_per_host, 3);
assert!((stats.avg_voids_per_host - 2.0).abs() < 0.01);
assert_eq!(stats.hosts_with_many_voids, 0);
}
#[test]
fn test_void_statistics_many_voids() {
let mut index = VoidIndex::new();
for i in 0..15 {
index.add_relationship(100, 200 + i);
}
let stats = VoidStatistics::from_index(&index);
assert_eq!(stats.hosts_with_many_voids, 1);
}
use ifc_lite_core::EntityDecoder;
fn three_layer_wall_with_voids_ifc() -> String {
r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('test.ifc','2024-01-01T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
#50=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#40));
#40=IFCEXTRUDEDAREASOLID($,$,$,3.0);
#100=IFCWALL('0001wall',$,'Parent',$,$,$,#51,$,$);
#101=IFCBUILDINGELEMENTPART('0001p01',$,'L0',$,$,$,#51,$,$);
#102=IFCBUILDINGELEMENTPART('0001p02',$,'L1',$,$,$,#51,$,$);
#103=IFCBUILDINGELEMENTPART('0001p03',$,'L2',$,$,$,#51,$,$);
#200=IFCOPENINGELEMENT('0001op',$,'Opening',$,$,$,#51,$,$);
#210=IFCRELVOIDSELEMENT('0001rv',$,$,$,#100,#200);
#300=IFCRELAGGREGATES('0001ra',$,$,$,#100,(#101,#102,#103));
ENDSEC;
END-ISO-10303-21;
"#
.to_string()
}
fn parts_only_aggregate_ifc() -> String {
r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('test.ifc','2024-01-01T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
#50=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#40));
#40=IFCEXTRUDEDAREASOLID($,$,$,3.0);
#100=IFCWALL('0001wall',$,'Parent',$,$,$,$,$,$);
#101=IFCBUILDINGELEMENTPART('0001p01',$,'L0',$,$,$,#51,$,$);
#102=IFCBUILDINGELEMENTPART('0001p02',$,'L1',$,$,$,#51,$,$);
#103=IFCBUILDINGELEMENTPART('0001p03',$,'L2',$,$,$,#51,$,$);
#300=IFCRELAGGREGATES('0001ra',$,$,$,#100,(#101,#102,#103));
ENDSEC;
END-ISO-10303-21;
"#
.to_string()
}
#[test]
fn propagate_voids_returns_part_to_parent_map() {
let content = three_layer_wall_with_voids_ifc();
let mut decoder = EntityDecoder::new(&content);
let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
void_index.insert(100, vec![200]);
let part_to_parent = propagate_voids_to_parts(&mut void_index, &content, &mut decoder);
assert_eq!(part_to_parent.len(), 3);
assert_eq!(part_to_parent.get(&101).copied(), Some(100));
assert_eq!(part_to_parent.get(&102).copied(), Some(100));
assert_eq!(part_to_parent.get(&103).copied(), Some(100));
assert_eq!(void_index.get(&101).map(Vec::as_slice), Some(&[200u32][..]));
assert_eq!(void_index.get(&102).map(Vec::as_slice), Some(&[200u32][..]));
assert_eq!(void_index.get(&103).map(Vec::as_slice), Some(&[200u32][..]));
}
#[test]
fn propagate_voids_skips_parents_without_representation() {
let content = parts_only_aggregate_ifc();
let mut decoder = EntityDecoder::new(&content);
let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
let part_to_parent = propagate_voids_to_parts(&mut void_index, &content, &mut decoder);
assert!(
part_to_parent.is_empty(),
"expected empty map when parent has no representation, got {:?}",
part_to_parent
);
}
#[test]
fn propagate_voids_returns_empty_map_when_no_aggregates() {
let empty = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('t.ifc','2024-01-01T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCWALL('0001w',$,'L',$,$,$,$,$,$);
ENDSEC;
END-ISO-10303-21;
"#
.to_string();
let mut decoder = EntityDecoder::new(&empty);
let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
let part_to_parent = propagate_voids_to_parts(&mut void_index, &empty, &mut decoder);
assert!(part_to_parent.is_empty());
assert!(void_index.is_empty());
}
fn agg_map(pairs: &[(u32, &[u32])]) -> FxHashMap<u32, Vec<u32>> {
pairs.iter().map(|(k, v)| (*k, v.to_vec())).collect()
}
#[test]
fn propagate_voids_walks_full_aggregate_tree() {
let mut void_index = agg_map(&[(100, &[200, 201])]);
let aggregate_children = agg_map(&[(100, &[110, 111]), (110, &[120])]);
propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
let expected = [200, 201];
for part in &[110, 111, 120] {
let got = void_index.get(part).expect("part should have voids");
assert_eq!(
got.iter().copied().collect::<std::collections::HashSet<_>>(),
expected.iter().copied().collect::<std::collections::HashSet<_>>(),
"part #{part} should receive both openings",
);
}
assert_eq!(void_index.get(&100), Some(&vec![200, 201]));
}
#[test]
fn propagate_voids_deduplicates_existing_part_voids() {
let mut void_index = agg_map(&[(100, &[200]), (110, &[999])]);
let aggregate_children = agg_map(&[(100, &[110])]);
propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
let mut part_voids = void_index.get(&110).unwrap().clone();
part_voids.sort();
assert_eq!(part_voids, vec![200, 999]);
}
#[test]
fn propagate_voids_handles_aggregate_cycles() {
let mut void_index = agg_map(&[(100, &[200])]);
let aggregate_children = agg_map(&[(100, &[110]), (110, &[120]), (120, &[110])]);
propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
assert_eq!(void_index.get(&110), Some(&vec![200]));
assert_eq!(void_index.get(&120), Some(&vec![200]));
}
#[test]
fn propagate_voids_no_op_when_host_has_no_parts() {
let mut void_index = agg_map(&[(100, &[200])]);
let aggregate_children = agg_map(&[(101, &[110])]); let before = void_index.clone();
propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
assert_eq!(void_index, before);
}
#[test]
fn propagate_voids_to_parts_covers_non_bep_descendants() {
let content = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2024-01-01T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
#50=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#40));
#40=IFCEXTRUDEDAREASOLID($,$,$,3.0);
#100=IFCROOF('0001roof',$,'Roof',$,$,$,$,$,$);
#101=IFCSLAB('0001slab',$,'Pitch',$,$,$,#51,$,$);
#200=IFCOPENINGELEMENT('0001op',$,'Skylight',$,$,$,#51,$,$);
#210=IFCRELVOIDSELEMENT('0001rv',$,$,$,#100,#200);
#300=IFCRELAGGREGATES('0001ra',$,$,$,#100,(#101));
ENDSEC;
END-ISO-10303-21;
"#;
let mut decoder = EntityDecoder::new(content);
let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
void_index.insert(100, vec![200]);
let part_to_parent = propagate_voids_to_parts(&mut void_index, content, &mut decoder);
assert_eq!(void_index.get(&101), Some(&vec![200]));
assert!(part_to_parent.is_empty());
}
}