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};
15use rustc_hash::FxHashMap;
16
17/// Index mapping host elements to their voids
18///
19/// Provides efficient lookup of void entity IDs for any host element,
20/// enabling void-aware geometry processing.
21#[derive(Debug, Clone)]
22pub struct VoidIndex {
23    /// Map from host entity ID to list of void entity IDs
24    host_to_voids: FxHashMap<u32, Vec<u32>>,
25    /// Map from void entity ID to host entity ID (reverse lookup)
26    void_to_host: FxHashMap<u32, u32>,
27    /// Total number of void relationships
28    relationship_count: usize,
29}
30
31impl VoidIndex {
32    /// Create an empty void index
33    pub fn new() -> Self {
34        Self {
35            host_to_voids: FxHashMap::default(),
36            void_to_host: FxHashMap::default(),
37            relationship_count: 0,
38        }
39    }
40
41    /// Build void index from IFC content
42    ///
43    /// Scans the content for `IfcRelVoidsElement` entities and builds
44    /// the host-to-void mapping.
45    ///
46    /// # Arguments
47    /// * `content` - The raw IFC file content
48    /// * `decoder` - Entity decoder for parsing
49    ///
50    /// # Returns
51    /// A populated VoidIndex
52    pub fn from_content(content: &str, decoder: &mut EntityDecoder) -> Self {
53        let mut index = Self::new();
54        let mut scanner = EntityScanner::new(content);
55
56        while let Some((_id, type_name, start, end)) = scanner.next_entity() {
57            // Look for IfcRelVoidsElement relationships
58            if type_name == "IFCRELVOIDSELEMENT" {
59                if let Ok(entity) = decoder.decode_at(start, end) {
60                    // IfcRelVoidsElement structure:
61                    // #id = IFCRELVOIDSELEMENT(GlobalId, OwnerHistory, Name, Description,
62                    //                          RelatingBuildingElement, RelatedOpeningElement);
63                    // Indices: 0=GlobalId, 1=OwnerHistory, 2=Name, 3=Description,
64                    //          4=RelatingBuildingElement, 5=RelatedOpeningElement
65
66                    if let (Some(host_id), Some(void_id)) = (entity.get_ref(4), entity.get_ref(5)) {
67                        index.add_relationship(host_id, void_id);
68                    }
69                }
70            }
71        }
72
73        index
74    }
75
76    /// Add a void relationship
77    pub fn add_relationship(&mut self, host_id: u32, void_id: u32) {
78        self.host_to_voids.entry(host_id).or_default().push(void_id);
79        self.void_to_host.insert(void_id, host_id);
80        self.relationship_count += 1;
81    }
82
83    /// Get void IDs for a host element
84    ///
85    /// # Arguments
86    /// * `host_id` - The entity ID of the host element
87    ///
88    /// # Returns
89    /// Slice of void entity IDs, or empty slice if no voids
90    pub fn get_voids(&self, host_id: u32) -> &[u32] {
91        self.host_to_voids
92            .get(&host_id)
93            .map(|v| v.as_slice())
94            .unwrap_or(&[])
95    }
96
97    /// Get the host ID for a void element
98    ///
99    /// # Arguments
100    /// * `void_id` - The entity ID of the void/opening
101    ///
102    /// # Returns
103    /// The host entity ID, if found
104    pub fn get_host(&self, void_id: u32) -> Option<u32> {
105        self.void_to_host.get(&void_id).copied()
106    }
107
108    /// Check if an element has any voids
109    pub fn has_voids(&self, host_id: u32) -> bool {
110        self.host_to_voids
111            .get(&host_id)
112            .map(|v| !v.is_empty())
113            .unwrap_or(false)
114    }
115
116    /// Get number of voids for a host element
117    pub fn void_count(&self, host_id: u32) -> usize {
118        self.host_to_voids
119            .get(&host_id)
120            .map(|v| v.len())
121            .unwrap_or(0)
122    }
123
124    /// Get total number of host elements with voids
125    pub fn host_count(&self) -> usize {
126        self.host_to_voids.len()
127    }
128
129    /// Get total number of void relationships
130    pub fn total_relationships(&self) -> usize {
131        self.relationship_count
132    }
133
134    /// Iterate over all host elements and their voids
135    pub fn iter(&self) -> impl Iterator<Item = (u32, &[u32])> {
136        self.host_to_voids.iter().map(|(k, v)| (*k, v.as_slice()))
137    }
138
139    /// Get all host IDs that have voids
140    pub fn hosts_with_voids(&self) -> Vec<u32> {
141        self.host_to_voids.keys().copied().collect()
142    }
143
144    /// Check if an entity is a void/opening
145    pub fn is_void(&self, entity_id: u32) -> bool {
146        self.void_to_host.contains_key(&entity_id)
147    }
148
149    /// Check if an entity is a host with voids
150    pub fn is_host_with_voids(&self, entity_id: u32) -> bool {
151        self.host_to_voids.contains_key(&entity_id)
152    }
153}
154
155impl Default for VoidIndex {
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161/// Statistics about void distribution in a model
162#[derive(Debug, Clone)]
163pub struct VoidStatistics {
164    /// Total number of hosts with voids
165    pub hosts_with_voids: usize,
166    /// Total number of void relationships
167    pub total_voids: usize,
168    /// Maximum voids on a single host
169    pub max_voids_per_host: usize,
170    /// Average voids per host (that has voids)
171    pub avg_voids_per_host: f64,
172    /// Number of hosts with many voids (>10)
173    pub hosts_with_many_voids: usize,
174}
175
176impl VoidStatistics {
177    /// Compute statistics from a void index
178    pub fn from_index(index: &VoidIndex) -> Self {
179        let hosts_with_voids = index.host_count();
180        let total_voids = index.total_relationships();
181
182        let max_voids_per_host = index
183            .host_to_voids
184            .values()
185            .map(|v| v.len())
186            .max()
187            .unwrap_or(0);
188
189        let avg_voids_per_host = if hosts_with_voids > 0 {
190            total_voids as f64 / hosts_with_voids as f64
191        } else {
192            0.0
193        };
194
195        let hosts_with_many_voids = index.host_to_voids.values().filter(|v| v.len() > 10).count();
196
197        Self {
198            hosts_with_voids,
199            total_voids,
200            max_voids_per_host,
201            avg_voids_per_host,
202            hosts_with_many_voids,
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_void_index_basic() {
213        let mut index = VoidIndex::new();
214
215        // Add some relationships
216        index.add_relationship(100, 200);
217        index.add_relationship(100, 201);
218        index.add_relationship(101, 202);
219
220        // Test lookups
221        assert_eq!(index.get_voids(100), &[200, 201]);
222        assert_eq!(index.get_voids(101), &[202]);
223        assert!(index.get_voids(999).is_empty());
224
225        // Test reverse lookup
226        assert_eq!(index.get_host(200), Some(100));
227        assert_eq!(index.get_host(202), Some(101));
228        assert_eq!(index.get_host(999), None);
229
230        // Test counts
231        assert_eq!(index.void_count(100), 2);
232        assert_eq!(index.void_count(101), 1);
233        assert_eq!(index.host_count(), 2);
234        assert_eq!(index.total_relationships(), 3);
235    }
236
237    #[test]
238    fn test_void_index_has_voids() {
239        let mut index = VoidIndex::new();
240        index.add_relationship(100, 200);
241
242        assert!(index.has_voids(100));
243        assert!(!index.has_voids(999));
244    }
245
246    #[test]
247    fn test_void_index_is_void() {
248        let mut index = VoidIndex::new();
249        index.add_relationship(100, 200);
250
251        assert!(index.is_void(200));
252        assert!(!index.is_void(100));
253        assert!(!index.is_void(999));
254    }
255
256    #[test]
257    fn test_void_index_hosts_with_voids() {
258        let mut index = VoidIndex::new();
259        index.add_relationship(100, 200);
260        index.add_relationship(101, 201);
261        index.add_relationship(102, 202);
262
263        let hosts = index.hosts_with_voids();
264        assert_eq!(hosts.len(), 3);
265        assert!(hosts.contains(&100));
266        assert!(hosts.contains(&101));
267        assert!(hosts.contains(&102));
268    }
269
270    #[test]
271    fn test_void_statistics() {
272        let mut index = VoidIndex::new();
273
274        // Host 100 has 3 voids
275        index.add_relationship(100, 200);
276        index.add_relationship(100, 201);
277        index.add_relationship(100, 202);
278
279        // Host 101 has 1 void
280        index.add_relationship(101, 203);
281
282        let stats = VoidStatistics::from_index(&index);
283
284        assert_eq!(stats.hosts_with_voids, 2);
285        assert_eq!(stats.total_voids, 4);
286        assert_eq!(stats.max_voids_per_host, 3);
287        assert!((stats.avg_voids_per_host - 2.0).abs() < 0.01);
288        assert_eq!(stats.hosts_with_many_voids, 0);
289    }
290
291    #[test]
292    fn test_void_statistics_many_voids() {
293        let mut index = VoidIndex::new();
294
295        // Host 100 has 15 voids (> 10 threshold)
296        for i in 0..15 {
297            index.add_relationship(100, 200 + i);
298        }
299
300        let stats = VoidStatistics::from_index(&index);
301        assert_eq!(stats.hosts_with_many_voids, 1);
302    }
303}