bimifc_model/spatial.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//! Spatial structure and hierarchy traversal
6
7use crate::{EntityId, IfcType};
8use serde::{Deserialize, Serialize};
9
10/// Type of spatial structure node
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum SpatialNodeType {
13 /// IfcProject - root of the hierarchy
14 Project,
15 /// IfcSite - geographic site
16 Site,
17 /// IfcBuilding - a building structure
18 Building,
19 /// IfcBuildingStorey - a floor/level
20 Storey,
21 /// IfcSpace - a room or area
22 Space,
23 /// Building element (wall, door, etc.)
24 Element,
25 /// IFC4x3 Facility (road, bridge, etc.)
26 Facility,
27 /// IFC4x3 Facility part
28 FacilityPart,
29}
30
31impl SpatialNodeType {
32 /// Get display name for UI
33 pub fn display_name(&self) -> &'static str {
34 match self {
35 SpatialNodeType::Project => "Project",
36 SpatialNodeType::Site => "Site",
37 SpatialNodeType::Building => "Building",
38 SpatialNodeType::Storey => "Storey",
39 SpatialNodeType::Space => "Space",
40 SpatialNodeType::Element => "Element",
41 SpatialNodeType::Facility => "Facility",
42 SpatialNodeType::FacilityPart => "Facility Part",
43 }
44 }
45
46 /// Get icon for UI
47 pub fn icon(&self) -> &'static str {
48 match self {
49 SpatialNodeType::Project => "📋",
50 SpatialNodeType::Site => "🌍",
51 SpatialNodeType::Building => "🏢",
52 SpatialNodeType::Storey => "📐",
53 SpatialNodeType::Space => "🚪",
54 SpatialNodeType::Element => "🧱",
55 SpatialNodeType::Facility => "🛣️",
56 SpatialNodeType::FacilityPart => "🔧",
57 }
58 }
59
60 /// Determine node type from IFC type
61 pub fn from_ifc_type(ifc_type: &IfcType) -> Self {
62 match ifc_type {
63 IfcType::IfcProject => SpatialNodeType::Project,
64 IfcType::IfcSite => SpatialNodeType::Site,
65 IfcType::IfcBuilding => SpatialNodeType::Building,
66 IfcType::IfcBuildingStorey => SpatialNodeType::Storey,
67 IfcType::IfcSpace => SpatialNodeType::Space,
68 IfcType::IfcFacility | IfcType::IfcRoad | IfcType::IfcBridge | IfcType::IfcRailway => {
69 SpatialNodeType::Facility
70 }
71 IfcType::IfcFacilityPart
72 | IfcType::IfcRoadPart
73 | IfcType::IfcBridgePart
74 | IfcType::IfcRailwayPart => SpatialNodeType::FacilityPart,
75 _ => SpatialNodeType::Element,
76 }
77 }
78}
79
80/// Node in the spatial hierarchy tree
81///
82/// Represents an entry in the IFC spatial structure hierarchy.
83/// The tree typically follows: Project → Site → Building → Storey → Elements
84#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
85pub struct SpatialNode {
86 /// Entity ID
87 pub id: EntityId,
88 /// Type of spatial node
89 pub node_type: SpatialNodeType,
90 /// Display name
91 pub name: String,
92 /// IFC entity type name (e.g., "IfcWall")
93 pub entity_type: String,
94 /// Elevation (for storeys)
95 pub elevation: Option<f32>,
96 /// Child nodes
97 pub children: Vec<SpatialNode>,
98 /// Whether this entity has geometry
99 pub has_geometry: bool,
100}
101
102impl SpatialNode {
103 /// Create a new spatial node
104 pub fn new(
105 id: EntityId,
106 node_type: SpatialNodeType,
107 name: impl Into<String>,
108 entity_type: impl Into<String>,
109 ) -> Self {
110 Self {
111 id,
112 node_type,
113 name: name.into(),
114 entity_type: entity_type.into(),
115 elevation: None,
116 children: Vec::new(),
117 has_geometry: false,
118 }
119 }
120
121 /// Set elevation
122 pub fn with_elevation(mut self, elevation: f32) -> Self {
123 self.elevation = Some(elevation);
124 self
125 }
126
127 /// Set has_geometry flag
128 pub fn with_geometry(mut self, has_geometry: bool) -> Self {
129 self.has_geometry = has_geometry;
130 self
131 }
132
133 /// Add a child node
134 pub fn add_child(&mut self, child: SpatialNode) {
135 self.children.push(child);
136 }
137
138 /// Get total element count (recursive)
139 pub fn element_count(&self) -> usize {
140 let own = if self.node_type == SpatialNodeType::Element {
141 1
142 } else {
143 0
144 };
145 own + self
146 .children
147 .iter()
148 .map(|c| c.element_count())
149 .sum::<usize>()
150 }
151
152 /// Find a node by ID (recursive)
153 pub fn find(&self, id: EntityId) -> Option<&SpatialNode> {
154 if self.id == id {
155 return Some(self);
156 }
157 for child in &self.children {
158 if let Some(found) = child.find(id) {
159 return Some(found);
160 }
161 }
162 None
163 }
164
165 /// Find a node by ID (mutable, recursive)
166 pub fn find_mut(&mut self, id: EntityId) -> Option<&mut SpatialNode> {
167 if self.id == id {
168 return Some(self);
169 }
170 for child in &mut self.children {
171 if let Some(found) = child.find_mut(id) {
172 return Some(found);
173 }
174 }
175 None
176 }
177
178 /// Iterate all nodes (depth-first)
179 pub fn iter(&self) -> SpatialNodeIter<'_> {
180 SpatialNodeIter { stack: vec![self] }
181 }
182
183 /// Get all element IDs in this subtree
184 pub fn element_ids(&self) -> Vec<EntityId> {
185 self.iter()
186 .filter(|n| n.node_type == SpatialNodeType::Element)
187 .map(|n| n.id)
188 .collect()
189 }
190}
191
192/// Iterator over spatial nodes (depth-first)
193pub struct SpatialNodeIter<'a> {
194 stack: Vec<&'a SpatialNode>,
195}
196
197impl<'a> Iterator for SpatialNodeIter<'a> {
198 type Item = &'a SpatialNode;
199
200 fn next(&mut self) -> Option<Self::Item> {
201 let node = self.stack.pop()?;
202 // Add children in reverse order so first child is processed first
203 for child in node.children.iter().rev() {
204 self.stack.push(child);
205 }
206 Some(node)
207 }
208}
209
210/// Building storey information
211#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
212pub struct StoreyInfo {
213 /// Entity ID
214 pub id: EntityId,
215 /// Storey name
216 pub name: String,
217 /// Elevation in meters
218 pub elevation: f32,
219 /// Number of elements in this storey
220 pub element_count: usize,
221}
222
223impl StoreyInfo {
224 /// Create new storey info
225 pub fn new(
226 id: EntityId,
227 name: impl Into<String>,
228 elevation: f32,
229 element_count: usize,
230 ) -> Self {
231 Self {
232 id,
233 name: name.into(),
234 elevation,
235 element_count,
236 }
237 }
238}
239
240/// Spatial query interface
241///
242/// Provides access to the spatial structure hierarchy and search capabilities.
243///
244/// # Example
245///
246/// ```ignore
247/// use bimifc_model::{SpatialQuery, EntityId};
248///
249/// fn explore_building(spatial: &dyn SpatialQuery) {
250/// // Get spatial tree
251/// if let Some(tree) = spatial.spatial_tree() {
252/// println!("Project: {}", tree.name);
253/// for child in &tree.children {
254/// println!(" {}: {}", child.node_type.display_name(), child.name);
255/// }
256/// }
257///
258/// // List storeys
259/// for storey in spatial.storeys() {
260/// println!("Storey {} at elevation {}m ({} elements)",
261/// storey.name, storey.elevation, storey.element_count);
262/// }
263///
264/// // Search for walls
265/// let wall_ids = spatial.search("wall");
266/// println!("Found {} walls", wall_ids.len());
267/// }
268/// ```
269pub trait SpatialQuery: Send + Sync {
270 /// Get the spatial hierarchy tree
271 ///
272 /// Returns the root of the spatial structure tree (typically IfcProject).
273 /// The tree contains all spatial structure elements and their contained elements.
274 ///
275 /// # Returns
276 /// The root spatial node, or `None` if no spatial structure exists
277 fn spatial_tree(&self) -> Option<&SpatialNode>;
278
279 /// Get all building storeys
280 ///
281 /// Returns information about all storeys in the model, sorted by elevation.
282 ///
283 /// # Returns
284 /// A vector of storey information
285 fn storeys(&self) -> Vec<StoreyInfo>;
286
287 /// Get elements contained in a storey
288 ///
289 /// # Arguments
290 /// * `storey_id` - The storey entity ID
291 ///
292 /// # Returns
293 /// A vector of element IDs contained in the storey
294 fn elements_in_storey(&self, storey_id: EntityId) -> Vec<EntityId>;
295
296 /// Get the containing storey for an element
297 ///
298 /// # Arguments
299 /// * `element_id` - The element entity ID
300 ///
301 /// # Returns
302 /// The storey ID if the element is contained in a storey
303 fn containing_storey(&self, element_id: EntityId) -> Option<EntityId>;
304
305 /// Search entities by name or type
306 ///
307 /// Performs a case-insensitive search across entity names and types.
308 ///
309 /// # Arguments
310 /// * `query` - The search query string
311 ///
312 /// # Returns
313 /// A vector of matching entity IDs
314 fn search(&self, query: &str) -> Vec<EntityId>;
315
316 /// Get elements of a specific type
317 ///
318 /// # Arguments
319 /// * `ifc_type` - The IFC type to filter by
320 ///
321 /// # Returns
322 /// A vector of entity IDs of the specified type
323 fn elements_by_type(&self, ifc_type: &IfcType) -> Vec<EntityId>;
324
325 /// Get all building elements (walls, slabs, etc.)
326 ///
327 /// # Returns
328 /// A vector of all building element IDs
329 fn all_elements(&self) -> Vec<EntityId> {
330 if let Some(tree) = self.spatial_tree() {
331 tree.element_ids()
332 } else {
333 Vec::new()
334 }
335 }
336
337 /// Get element count
338 fn element_count(&self) -> usize {
339 self.all_elements().len()
340 }
341}