Skip to main content

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}