Skip to main content

bimifc_model/
properties.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//! Property and quantity access for IFC entities
6
7use crate::EntityId;
8use serde::{Deserialize, Serialize};
9
10/// A single property value with optional unit
11#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12pub struct Property {
13    /// Property name
14    pub name: String,
15    /// Property value as formatted string
16    pub value: String,
17    /// Unit of measurement (if applicable)
18    pub unit: Option<String>,
19}
20
21impl Property {
22    /// Create a new property
23    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
24        Self {
25            name: name.into(),
26            value: value.into(),
27            unit: None,
28        }
29    }
30
31    /// Create a property with unit
32    pub fn with_unit(
33        name: impl Into<String>,
34        value: impl Into<String>,
35        unit: impl Into<String>,
36    ) -> Self {
37        Self {
38            name: name.into(),
39            value: value.into(),
40            unit: Some(unit.into()),
41        }
42    }
43}
44
45/// A property set containing multiple properties
46#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
47pub struct PropertySet {
48    /// Property set name (e.g., "Pset_WallCommon")
49    pub name: String,
50    /// Properties in this set
51    pub properties: Vec<Property>,
52}
53
54impl PropertySet {
55    /// Create a new property set
56    pub fn new(name: impl Into<String>) -> Self {
57        Self {
58            name: name.into(),
59            properties: Vec::new(),
60        }
61    }
62
63    /// Add a property to this set
64    pub fn add(&mut self, property: Property) {
65        self.properties.push(property);
66    }
67
68    /// Get a property by name
69    pub fn get(&self, name: &str) -> Option<&Property> {
70        self.properties.iter().find(|p| p.name == name)
71    }
72}
73
74/// Quantity types supported in IFC
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
76pub enum QuantityType {
77    /// Linear measurement (IfcQuantityLength)
78    Length,
79    /// Area measurement (IfcQuantityArea)
80    Area,
81    /// Volume measurement (IfcQuantityVolume)
82    Volume,
83    /// Count (IfcQuantityCount)
84    Count,
85    /// Weight/mass measurement (IfcQuantityWeight)
86    Weight,
87    /// Time measurement (IfcQuantityTime)
88    Time,
89}
90
91impl QuantityType {
92    /// Get default unit for this quantity type
93    pub fn default_unit(&self) -> &'static str {
94        match self {
95            QuantityType::Length => "m",
96            QuantityType::Area => "m²",
97            QuantityType::Volume => "m³",
98            QuantityType::Count => "",
99            QuantityType::Weight => "kg",
100            QuantityType::Time => "s",
101        }
102    }
103}
104
105/// A quantity value with type and unit
106#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
107pub struct Quantity {
108    /// Quantity name
109    pub name: String,
110    /// Numeric value
111    pub value: f64,
112    /// Unit of measurement
113    pub unit: String,
114    /// Type of quantity
115    pub quantity_type: QuantityType,
116}
117
118impl Quantity {
119    /// Create a new quantity
120    pub fn new(name: impl Into<String>, value: f64, quantity_type: QuantityType) -> Self {
121        Self {
122            name: name.into(),
123            value,
124            unit: quantity_type.default_unit().to_string(),
125            quantity_type,
126        }
127    }
128
129    /// Create a quantity with custom unit
130    pub fn with_unit(
131        name: impl Into<String>,
132        value: f64,
133        unit: impl Into<String>,
134        quantity_type: QuantityType,
135    ) -> Self {
136        Self {
137            name: name.into(),
138            value,
139            unit: unit.into(),
140            quantity_type,
141        }
142    }
143
144    /// Format the value with unit
145    pub fn formatted(&self) -> String {
146        if self.unit.is_empty() {
147            format!("{}", self.value)
148        } else {
149            format!("{} {}", self.value, self.unit)
150        }
151    }
152}
153
154/// A single C-plane of light distribution data
155#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
156pub struct LightDistributionPlane {
157    /// C-plane angle in degrees (0, 5, 10, ..., 355)
158    pub c_angle: f64,
159    /// Gamma angles in degrees (typically 0-180)
160    pub gamma_angles: Vec<f64>,
161    /// Luminous intensity values in candela at each gamma angle
162    pub intensities: Vec<f64>,
163}
164
165/// IFC-native goniometric light source data (from IfcLightSourceGoniometric)
166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
167pub struct GoniometricData {
168    /// Light source name
169    pub name: String,
170    /// Colour temperature in Kelvin
171    pub colour_temperature: f64,
172    /// Total luminous flux in lumens
173    pub luminous_flux: f64,
174    /// Light emitter type (e.g., "LIGHTEMITTINGDIODE", "FLUORESCENT")
175    pub emitter_type: String,
176    /// Distribution type (e.g., "TYPE_C", "TYPE_B", "TYPE_A")
177    pub distribution_type: String,
178    /// Light distribution data per C-plane
179    pub planes: Vec<LightDistributionPlane>,
180}
181
182/// Property and quantity reader trait
183///
184/// Provides access to property sets and quantities associated with IFC entities.
185/// Property sets come from IfcPropertySet entities linked via IfcRelDefinesByProperties.
186/// Quantities come from IfcElementQuantity entities.
187///
188/// # Example
189///
190/// ```ignore
191/// use bimifc_model::{PropertyReader, EntityId};
192///
193/// fn show_wall_properties(props: &dyn PropertyReader, wall_id: EntityId) {
194///     // Get all property sets
195///     for pset in props.property_sets(wall_id) {
196///         println!("Property Set: {}", pset.name);
197///         for prop in &pset.properties {
198///             println!("  {}: {}", prop.name, prop.value);
199///         }
200///     }
201///
202///     // Get quantities
203///     for qty in props.quantities(wall_id) {
204///         println!("{}: {} {}", qty.name, qty.value, qty.unit);
205///     }
206///
207///     // Get specific property
208///     if let Some(fire_rating) = props.get_property(wall_id, "FireRating") {
209///         println!("Fire Rating: {}", fire_rating.value);
210///     }
211/// }
212/// ```
213pub trait PropertyReader: Send + Sync {
214    /// Get all property sets associated with an entity
215    ///
216    /// # Arguments
217    /// * `id` - The entity ID to get property sets for
218    ///
219    /// # Returns
220    /// A vector of property sets (empty if none found)
221    fn property_sets(&self, id: EntityId) -> Vec<PropertySet>;
222
223    /// Get all quantities associated with an entity
224    ///
225    /// # Arguments
226    /// * `id` - The entity ID to get quantities for
227    ///
228    /// # Returns
229    /// A vector of quantities (empty if none found)
230    fn quantities(&self, id: EntityId) -> Vec<Quantity>;
231
232    /// Get a specific property by name
233    ///
234    /// Searches all property sets for the entity and returns the first
235    /// property with the matching name.
236    ///
237    /// # Arguments
238    /// * `id` - The entity ID to search
239    /// * `name` - The property name to find
240    ///
241    /// # Returns
242    /// The property if found
243    fn get_property(&self, id: EntityId, name: &str) -> Option<Property> {
244        self.property_sets(id)
245            .into_iter()
246            .flat_map(|pset| pset.properties)
247            .find(|p| p.name == name)
248    }
249
250    /// Get a specific quantity by name
251    ///
252    /// # Arguments
253    /// * `id` - The entity ID to search
254    /// * `name` - The quantity name to find
255    ///
256    /// # Returns
257    /// The quantity if found
258    fn get_quantity(&self, id: EntityId, name: &str) -> Option<Quantity> {
259        self.quantities(id).into_iter().find(|q| q.name == name)
260    }
261
262    /// Get entity's GlobalId (GUID)
263    ///
264    /// The GlobalId is a unique identifier assigned to IFC entities,
265    /// typically a 22-character base64-encoded GUID.
266    ///
267    /// # Arguments
268    /// * `id` - The entity ID
269    ///
270    /// # Returns
271    /// The GlobalId string if available
272    fn global_id(&self, id: EntityId) -> Option<String>;
273
274    /// Get entity's Name attribute
275    ///
276    /// Most IFC entities have a Name attribute (typically at index 2).
277    ///
278    /// # Arguments
279    /// * `id` - The entity ID
280    ///
281    /// # Returns
282    /// The name string if available
283    fn name(&self, id: EntityId) -> Option<String>;
284
285    /// Get entity's Description attribute
286    ///
287    /// # Arguments
288    /// * `id` - The entity ID
289    ///
290    /// # Returns
291    /// The description string if available
292    fn description(&self, id: EntityId) -> Option<String>;
293
294    /// Get entity's ObjectType attribute
295    ///
296    /// ObjectType is often used as a more specific type indicator
297    /// beyond the IFC class name.
298    ///
299    /// # Arguments
300    /// * `id` - The entity ID
301    ///
302    /// # Returns
303    /// The object type string if available
304    fn object_type(&self, _id: EntityId) -> Option<String> {
305        None
306    }
307
308    /// Get entity's Tag attribute
309    ///
310    /// Tag is often used for element identification/marking.
311    ///
312    /// # Arguments
313    /// * `id` - The entity ID
314    ///
315    /// # Returns
316    /// The tag string if available
317    fn tag(&self, _id: EntityId) -> Option<String> {
318        None
319    }
320
321    /// Get goniometric light source data associated with an entity
322    ///
323    /// Finds IfcLightSourceGoniometric entities connected to this entity
324    /// through IfcRelDefinesByType → representation maps, or through
325    /// the entity's own representations.
326    ///
327    /// # Arguments
328    /// * `id` - The entity ID (typically an IfcLightFixture instance)
329    ///
330    /// # Returns
331    /// A vector of goniometric data (empty if none found)
332    fn goniometric_sources(&self, _id: EntityId) -> Vec<GoniometricData> {
333        Vec::new()
334    }
335}