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}