Skip to main content

ifc_lite_geometry/
void_index.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//! Void Index Module
6//!
7//! Builds and manages the mapping between host elements (walls, slabs, etc.)
8//! and their associated voids (openings, penetrations).
9//!
10//! In IFC, voids are related to their host elements via `IfcRelVoidsElement`:
11//! - RelatingBuildingElement: The host (wall, slab, beam, etc.)
12//! - RelatedOpeningElement: The opening (IfcOpeningElement)
13
14use ifc_lite_core::{EntityDecoder, EntityScanner, IfcType};
15use rustc_hash::FxHashMap;
16
17/// Propagate void (opening) relationships from aggregate parents to their children.
18///
19/// In IFC, multilayer walls use `IfcRelAggregates` to decompose a parent `IfcWall`
20/// into child `IfcBuildingElementPart` entities (one per material layer). The
21/// `IfcRelVoidsElement` relationships reference the parent wall, but the individual
22/// layer parts also need void subtraction to cut windows/doors through each layer.
23///
24/// This function scans for `IfcRelAggregates` where the parent has voids and copies
25/// those void relationships to every child part that has geometry.
26pub fn propagate_voids_to_parts(
27    void_index: &mut FxHashMap<u32, Vec<u32>>,
28    content: &str,
29    decoder: &mut EntityDecoder,
30) {
31    let mut scanner = EntityScanner::new(content);
32    let mut propagations: Vec<(u32, Vec<u32>)> = Vec::new();
33
34    while let Some((id, type_name, start, end)) = scanner.next_entity() {
35        if type_name != "IFCRELAGGREGATES" {
36            continue;
37        }
38        let entity = match decoder.decode_at_with_id(id, start, end) {
39            Ok(e) => e,
40            Err(_) => continue,
41        };
42
43        // IfcRelAggregates: attr 4 = RelatingObject, attr 5 = RelatedObjects
44        let parent_id = match entity.get_ref(4) {
45            Some(id) => id,
46            None => continue,
47        };
48
49        if !void_index.contains_key(&parent_id) {
50            continue;
51        }
52
53        let children_attr = match entity.get(5) {
54            Some(attr) => attr,
55            None => continue,
56        };
57        let children: Vec<u32> = match children_attr.as_list() {
58            Some(list) => list
59                .iter()
60                .filter_map(|item| item.as_entity_ref())
61                .collect(),
62            None => continue,
63        };
64
65        let mut eligible_children = Vec::new();
66        for child_id in children {
67            if let Ok(child) = decoder.decode_by_id(child_id) {
68                if child.ifc_type == IfcType::IfcBuildingElementPart {
69                    let has_repr = child.get(6).map(|a| !a.is_null()).unwrap_or(false);
70                    if has_repr {
71                        eligible_children.push(child_id);
72                    }
73                }
74            }
75        }
76
77        if !eligible_children.is_empty() {
78            propagations.push((parent_id, eligible_children));
79        }
80    }
81
82    for (parent_id, children) in propagations {
83        let parent_voids = match void_index.get(&parent_id) {
84            Some(v) => v.clone(),
85            None => continue,
86        };
87        for child_id in children {
88            void_index
89                .entry(child_id)
90                .or_default()
91                .extend(parent_voids.iter().copied());
92        }
93    }
94}
95
96/// Index mapping host elements to their voids
97///
98/// Provides efficient lookup of void entity IDs for any host element,
99/// enabling void-aware geometry processing.
100#[derive(Debug, Clone)]
101pub struct VoidIndex {
102    /// Map from host entity ID to list of void entity IDs
103    host_to_voids: FxHashMap<u32, Vec<u32>>,
104    /// Map from void entity ID to host entity ID (reverse lookup)
105    void_to_host: FxHashMap<u32, u32>,
106    /// Total number of void relationships
107    relationship_count: usize,
108}
109
110impl VoidIndex {
111    /// Create an empty void index
112    pub fn new() -> Self {
113        Self {
114            host_to_voids: FxHashMap::default(),
115            void_to_host: FxHashMap::default(),
116            relationship_count: 0,
117        }
118    }
119
120    /// Build void index from IFC content
121    ///
122    /// Scans the content for `IfcRelVoidsElement` entities and builds
123    /// the host-to-void mapping.
124    ///
125    /// # Arguments
126    /// * `content` - The raw IFC file content
127    /// * `decoder` - Entity decoder for parsing
128    ///
129    /// # Returns
130    /// A populated VoidIndex
131    pub fn from_content(content: &str, decoder: &mut EntityDecoder) -> Self {
132        let mut index = Self::new();
133        let mut scanner = EntityScanner::new(content);
134
135        while let Some((_id, type_name, start, end)) = scanner.next_entity() {
136            // Look for IfcRelVoidsElement relationships
137            if type_name == "IFCRELVOIDSELEMENT" {
138                if let Ok(entity) = decoder.decode_at(start, end) {
139                    // IfcRelVoidsElement structure:
140                    // #id = IFCRELVOIDSELEMENT(GlobalId, OwnerHistory, Name, Description,
141                    //                          RelatingBuildingElement, RelatedOpeningElement);
142                    // Indices: 0=GlobalId, 1=OwnerHistory, 2=Name, 3=Description,
143                    //          4=RelatingBuildingElement, 5=RelatedOpeningElement
144
145                    if let (Some(host_id), Some(void_id)) = (entity.get_ref(4), entity.get_ref(5)) {
146                        index.add_relationship(host_id, void_id);
147                    }
148                }
149            }
150        }
151
152        index
153    }
154
155    /// Add a void relationship
156    pub fn add_relationship(&mut self, host_id: u32, void_id: u32) {
157        self.host_to_voids.entry(host_id).or_default().push(void_id);
158        self.void_to_host.insert(void_id, host_id);
159        self.relationship_count += 1;
160    }
161
162    /// Get void IDs for a host element
163    ///
164    /// # Arguments
165    /// * `host_id` - The entity ID of the host element
166    ///
167    /// # Returns
168    /// Slice of void entity IDs, or empty slice if no voids
169    pub fn get_voids(&self, host_id: u32) -> &[u32] {
170        self.host_to_voids
171            .get(&host_id)
172            .map(|v| v.as_slice())
173            .unwrap_or(&[])
174    }
175
176    /// Get the host ID for a void element
177    ///
178    /// # Arguments
179    /// * `void_id` - The entity ID of the void/opening
180    ///
181    /// # Returns
182    /// The host entity ID, if found
183    pub fn get_host(&self, void_id: u32) -> Option<u32> {
184        self.void_to_host.get(&void_id).copied()
185    }
186
187    /// Check if an element has any voids
188    pub fn has_voids(&self, host_id: u32) -> bool {
189        self.host_to_voids
190            .get(&host_id)
191            .map(|v| !v.is_empty())
192            .unwrap_or(false)
193    }
194
195    /// Get number of voids for a host element
196    pub fn void_count(&self, host_id: u32) -> usize {
197        self.host_to_voids
198            .get(&host_id)
199            .map(|v| v.len())
200            .unwrap_or(0)
201    }
202
203    /// Get total number of host elements with voids
204    pub fn host_count(&self) -> usize {
205        self.host_to_voids.len()
206    }
207
208    /// Get total number of void relationships
209    pub fn total_relationships(&self) -> usize {
210        self.relationship_count
211    }
212
213    /// Iterate over all host elements and their voids
214    pub fn iter(&self) -> impl Iterator<Item = (u32, &[u32])> {
215        self.host_to_voids.iter().map(|(k, v)| (*k, v.as_slice()))
216    }
217
218    /// Get all host IDs that have voids
219    pub fn hosts_with_voids(&self) -> Vec<u32> {
220        self.host_to_voids.keys().copied().collect()
221    }
222
223    /// Check if an entity is a void/opening
224    pub fn is_void(&self, entity_id: u32) -> bool {
225        self.void_to_host.contains_key(&entity_id)
226    }
227
228    /// Check if an entity is a host with voids
229    pub fn is_host_with_voids(&self, entity_id: u32) -> bool {
230        self.host_to_voids.contains_key(&entity_id)
231    }
232}
233
234impl Default for VoidIndex {
235    fn default() -> Self {
236        Self::new()
237    }
238}
239
240/// Statistics about void distribution in a model
241#[derive(Debug, Clone)]
242pub struct VoidStatistics {
243    /// Total number of hosts with voids
244    pub hosts_with_voids: usize,
245    /// Total number of void relationships
246    pub total_voids: usize,
247    /// Maximum voids on a single host
248    pub max_voids_per_host: usize,
249    /// Average voids per host (that has voids)
250    pub avg_voids_per_host: f64,
251    /// Number of hosts with many voids (>10)
252    pub hosts_with_many_voids: usize,
253}
254
255impl VoidStatistics {
256    /// Compute statistics from a void index
257    pub fn from_index(index: &VoidIndex) -> Self {
258        let hosts_with_voids = index.host_count();
259        let total_voids = index.total_relationships();
260
261        let max_voids_per_host = index
262            .host_to_voids
263            .values()
264            .map(|v| v.len())
265            .max()
266            .unwrap_or(0);
267
268        let avg_voids_per_host = if hosts_with_voids > 0 {
269            total_voids as f64 / hosts_with_voids as f64
270        } else {
271            0.0
272        };
273
274        let hosts_with_many_voids = index
275            .host_to_voids
276            .values()
277            .filter(|v| v.len() > 10)
278            .count();
279
280        Self {
281            hosts_with_voids,
282            total_voids,
283            max_voids_per_host,
284            avg_voids_per_host,
285            hosts_with_many_voids,
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_void_index_basic() {
296        let mut index = VoidIndex::new();
297
298        // Add some relationships
299        index.add_relationship(100, 200);
300        index.add_relationship(100, 201);
301        index.add_relationship(101, 202);
302
303        // Test lookups
304        assert_eq!(index.get_voids(100), &[200, 201]);
305        assert_eq!(index.get_voids(101), &[202]);
306        assert!(index.get_voids(999).is_empty());
307
308        // Test reverse lookup
309        assert_eq!(index.get_host(200), Some(100));
310        assert_eq!(index.get_host(202), Some(101));
311        assert_eq!(index.get_host(999), None);
312
313        // Test counts
314        assert_eq!(index.void_count(100), 2);
315        assert_eq!(index.void_count(101), 1);
316        assert_eq!(index.host_count(), 2);
317        assert_eq!(index.total_relationships(), 3);
318    }
319
320    #[test]
321    fn test_void_index_has_voids() {
322        let mut index = VoidIndex::new();
323        index.add_relationship(100, 200);
324
325        assert!(index.has_voids(100));
326        assert!(!index.has_voids(999));
327    }
328
329    #[test]
330    fn test_void_index_is_void() {
331        let mut index = VoidIndex::new();
332        index.add_relationship(100, 200);
333
334        assert!(index.is_void(200));
335        assert!(!index.is_void(100));
336        assert!(!index.is_void(999));
337    }
338
339    #[test]
340    fn test_void_index_hosts_with_voids() {
341        let mut index = VoidIndex::new();
342        index.add_relationship(100, 200);
343        index.add_relationship(101, 201);
344        index.add_relationship(102, 202);
345
346        let hosts = index.hosts_with_voids();
347        assert_eq!(hosts.len(), 3);
348        assert!(hosts.contains(&100));
349        assert!(hosts.contains(&101));
350        assert!(hosts.contains(&102));
351    }
352
353    #[test]
354    fn test_void_statistics() {
355        let mut index = VoidIndex::new();
356
357        // Host 100 has 3 voids
358        index.add_relationship(100, 200);
359        index.add_relationship(100, 201);
360        index.add_relationship(100, 202);
361
362        // Host 101 has 1 void
363        index.add_relationship(101, 203);
364
365        let stats = VoidStatistics::from_index(&index);
366
367        assert_eq!(stats.hosts_with_voids, 2);
368        assert_eq!(stats.total_voids, 4);
369        assert_eq!(stats.max_voids_per_host, 3);
370        assert!((stats.avg_voids_per_host - 2.0).abs() < 0.01);
371        assert_eq!(stats.hosts_with_many_voids, 0);
372    }
373
374    #[test]
375    fn test_void_statistics_many_voids() {
376        let mut index = VoidIndex::new();
377
378        // Host 100 has 15 voids (> 10 threshold)
379        for i in 0..15 {
380            index.add_relationship(100, 200 + i);
381        }
382
383        let stats = VoidStatistics::from_index(&index);
384        assert_eq!(stats.hosts_with_many_voids, 1);
385    }
386}