Skip to main content

bimifc_parser/
lighting.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//! Lighting data extraction from IFC files
6//!
7//! This module extracts light fixtures, light sources, and photometric data
8//! from IFC files for use with lighting analysis and GLDF viewers.
9
10use bimifc_model::{AttributeValue, DecodedEntity, EntityResolver, IfcType};
11use rustc_hash::FxHashMap;
12use serde::{Deserialize, Serialize};
13
14/// Extracted light fixture data
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct LightFixtureData {
17    /// Entity ID
18    pub id: u64,
19    /// GlobalId (GUID)
20    pub global_id: Option<String>,
21    /// Name
22    pub name: Option<String>,
23    /// Description
24    pub description: Option<String>,
25    /// Object type (predefined or user-defined)
26    pub object_type: Option<String>,
27    /// Position (X, Y, Z) in meters
28    pub position: (f64, f64, f64),
29    /// Associated storey name
30    pub storey: Option<String>,
31    /// Storey elevation
32    pub storey_elevation: Option<f64>,
33    /// Light fixture type reference
34    pub fixture_type: Option<LightFixtureTypeData>,
35    /// Light sources
36    pub light_sources: Vec<LightSourceData>,
37    /// Properties from property sets
38    pub properties: FxHashMap<String, PropertySetData>,
39}
40
41/// Light fixture type definition
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct LightFixtureTypeData {
44    pub id: u64,
45    pub name: Option<String>,
46    pub description: Option<String>,
47    pub predefined_type: Option<String>,
48}
49
50/// Light source data (goniometric, positional, etc.)
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct LightSourceData {
53    pub id: u64,
54    pub source_type: String,
55    /// Color temperature in Kelvin
56    pub color_temperature: Option<f64>,
57    /// Luminous flux in lumens
58    pub luminous_flux: Option<f64>,
59    /// Light emission source (LED, FLUORESCENT, etc.)
60    pub emission_source: Option<String>,
61    /// Intensity (cd)
62    pub intensity: Option<f64>,
63    /// Color RGB
64    pub color_rgb: Option<(f64, f64, f64)>,
65    /// Light distribution data
66    pub distribution: Option<LightDistributionData>,
67}
68
69/// Light distribution/photometry data
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct LightDistributionData {
72    /// Distribution type (TYPE_A, TYPE_B, TYPE_C)
73    pub distribution_type: String,
74    /// Distribution planes (C-planes for Type C)
75    pub planes: Vec<DistributionPlane>,
76}
77
78/// A single distribution plane
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct DistributionPlane {
81    /// Main plane angle (C-angle for Type C)
82    pub main_angle: f64,
83    /// Intensity values at secondary angles (gamma angles)
84    pub intensities: Vec<(f64, f64)>, // (angle, intensity in cd)
85}
86
87/// Property set with values
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct PropertySetData {
90    pub name: String,
91    pub properties: FxHashMap<String, String>,
92}
93
94/// Complete lighting data from an IFC file
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct LightingExport {
97    /// Schema version
98    pub schema: String,
99    /// Project name
100    pub project_name: Option<String>,
101    /// Building name
102    pub building_name: Option<String>,
103    /// Building storeys
104    pub storeys: Vec<StoreyData>,
105    /// Light fixtures
106    pub light_fixtures: Vec<LightFixtureData>,
107    /// Light fixture types
108    pub light_fixture_types: Vec<LightFixtureTypeData>,
109    /// Summary statistics
110    pub summary: LightingSummary,
111}
112
113/// Building storey data
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct StoreyData {
116    pub id: u64,
117    pub name: String,
118    pub elevation: f64,
119}
120
121/// Summary of lighting data
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct LightingSummary {
124    pub total_fixtures: usize,
125    pub total_light_sources: usize,
126    pub fixtures_per_storey: FxHashMap<String, usize>,
127    pub fixture_types_used: Vec<String>,
128    /// Total luminous flux if available
129    pub total_luminous_flux: Option<f64>,
130}
131
132/// Extract lighting data from an IFC model
133pub fn extract_lighting_data(resolver: &dyn EntityResolver) -> LightingExport {
134    let mut export = LightingExport {
135        schema: String::new(),
136        project_name: None,
137        building_name: None,
138        storeys: Vec::new(),
139        light_fixtures: Vec::new(),
140        light_fixture_types: Vec::new(),
141        summary: LightingSummary {
142            total_fixtures: 0,
143            total_light_sources: 0,
144            fixtures_per_storey: FxHashMap::default(),
145            fixture_types_used: Vec::new(),
146            total_luminous_flux: None,
147        },
148    };
149
150    // Extract project info
151    let projects = resolver.entities_by_type(&IfcType::IfcProject);
152    if let Some(project) = projects.first() {
153        export.project_name = project.get_string(2).map(|s| s.to_string());
154    }
155
156    // Extract building info
157    let buildings = resolver.entities_by_type(&IfcType::IfcBuilding);
158    if let Some(building) = buildings.first() {
159        export.building_name = building.get_string(2).map(|s| s.to_string());
160    }
161
162    // Extract storeys
163    let storeys = resolver.entities_by_type(&IfcType::IfcBuildingStorey);
164    for storey in storeys {
165        let name = storey
166            .get_string(2)
167            .map(|s| s.to_string())
168            .unwrap_or_default();
169        let elevation = storey.get_float(9).unwrap_or(0.0);
170        export.storeys.push(StoreyData {
171            id: storey.id.0 as u64,
172            name,
173            elevation,
174        });
175    }
176
177    // Extract light fixture types
178    let fixture_types = resolver.entities_by_type(&IfcType::IfcLightFixtureType);
179    for fixture_type in fixture_types {
180        let type_data = extract_fixture_type(&fixture_type);
181        export.light_fixture_types.push(type_data);
182    }
183
184    // Extract light fixtures
185    let fixtures = resolver.entities_by_type(&IfcType::IfcLightFixture);
186    let mut total_flux: f64 = 0.0;
187    let mut has_flux = false;
188
189    for fixture in fixtures {
190        let fixture_data = extract_fixture(&fixture, resolver);
191
192        // Update summary
193        if let Some(ref storey) = fixture_data.storey {
194            *export
195                .summary
196                .fixtures_per_storey
197                .entry(storey.clone())
198                .or_insert(0) += 1;
199        }
200
201        for source in &fixture_data.light_sources {
202            if let Some(flux) = source.luminous_flux {
203                total_flux += flux;
204                has_flux = true;
205            }
206        }
207
208        export.summary.total_light_sources += fixture_data.light_sources.len();
209        export.light_fixtures.push(fixture_data);
210    }
211
212    export.summary.total_fixtures = export.light_fixtures.len();
213    if has_flux {
214        export.summary.total_luminous_flux = Some(total_flux);
215    }
216
217    // Collect unique fixture types used
218    for fixture in &export.light_fixtures {
219        if let Some(ref ft) = fixture.fixture_type {
220            if let Some(ref name) = ft.name {
221                if !export.summary.fixture_types_used.contains(name) {
222                    export.summary.fixture_types_used.push(name.clone());
223                }
224            }
225        }
226    }
227
228    export
229}
230
231/// Extract data from a light fixture type entity
232fn extract_fixture_type(entity: &DecodedEntity) -> LightFixtureTypeData {
233    LightFixtureTypeData {
234        id: entity.id.0 as u64,
235        name: entity.get_string(2).map(|s| s.to_string()),
236        description: entity.get_string(3).map(|s| s.to_string()),
237        predefined_type: entity.get_enum(9).map(|s| s.to_string()),
238    }
239}
240
241/// Extract data from a light fixture entity
242fn extract_fixture(entity: &DecodedEntity, resolver: &dyn EntityResolver) -> LightFixtureData {
243    let global_id = entity.get_string(0).map(|s| s.to_string());
244    let name = entity.get_string(2).map(|s| s.to_string());
245    let description = entity.get_string(3).map(|s| s.to_string());
246    let object_type = entity.get_string(4).map(|s| s.to_string());
247
248    // Get position from placement
249    let position = extract_position(entity, resolver);
250
251    // Get fixture type
252    let fixture_type = entity.get_ref(5).and_then(|type_ref| {
253        resolver
254            .get(type_ref)
255            .map(|type_entity| extract_fixture_type(&type_entity))
256    });
257
258    // Extract light sources associated with this fixture
259    // In IFC, light sources are typically referenced through the representation
260    let light_sources = extract_light_sources(entity, resolver);
261
262    LightFixtureData {
263        id: entity.id.0 as u64,
264        global_id,
265        name,
266        description,
267        object_type,
268        position,
269        storey: None, // Could be populated from spatial containment
270        storey_elevation: None,
271        fixture_type,
272        light_sources,
273        properties: FxHashMap::default(),
274    }
275}
276
277/// Extract position from entity placement
278fn extract_position(entity: &DecodedEntity, resolver: &dyn EntityResolver) -> (f64, f64, f64) {
279    // ObjectPlacement is typically at index 5 for IfcProduct
280    let placement_ref = match entity.get_ref(5) {
281        Some(id) => id,
282        None => return (0.0, 0.0, 0.0),
283    };
284
285    let placement = match resolver.get(placement_ref) {
286        Some(p) => p,
287        None => return (0.0, 0.0, 0.0),
288    };
289
290    // IfcLocalPlacement has RelativePlacement at index 1
291    if placement.ifc_type == IfcType::IfcLocalPlacement {
292        if let Some(rel_placement_ref) = placement.get_ref(1) {
293            if let Some(axis_placement) = resolver.get(rel_placement_ref) {
294                return extract_cartesian_point(&axis_placement, resolver);
295            }
296        }
297    }
298
299    (0.0, 0.0, 0.0)
300}
301
302/// Extract coordinates from axis placement
303fn extract_cartesian_point(
304    axis_placement: &DecodedEntity,
305    resolver: &dyn EntityResolver,
306) -> (f64, f64, f64) {
307    // IfcAxis2Placement3D has Location at index 0
308    let point_ref = match axis_placement.get_ref(0) {
309        Some(id) => id,
310        None => return (0.0, 0.0, 0.0),
311    };
312
313    let point = match resolver.get(point_ref) {
314        Some(p) => p,
315        None => return (0.0, 0.0, 0.0),
316    };
317
318    if point.ifc_type == IfcType::IfcCartesianPoint {
319        // Coordinates are in a list at index 0
320        if let Some(coords) = point.get_list(0) {
321            let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
322            let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
323            let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
324            return (x, y, z);
325        }
326    }
327
328    (0.0, 0.0, 0.0)
329}
330
331/// Extract light sources from a fixture
332fn extract_light_sources(
333    _fixture: &DecodedEntity,
334    resolver: &dyn EntityResolver,
335) -> Vec<LightSourceData> {
336    let mut sources = Vec::new();
337
338    // Light sources can be found in various ways:
339    // 1. Through IFCRELASSOCIATESMATERIAL relationships
340    // 2. Through representation items
341    // 3. Direct references
342
343    // For now, search all goniometric light sources and match by containment
344    let goniometric_sources = resolver.entities_by_type(&IfcType::IfcLightSourceGoniometric);
345
346    for source in goniometric_sources {
347        sources.push(extract_goniometric_source(&source, resolver));
348    }
349
350    sources
351}
352
353/// Extract data from a goniometric light source
354fn extract_goniometric_source(
355    entity: &DecodedEntity,
356    resolver: &dyn EntityResolver,
357) -> LightSourceData {
358    // IfcLightSourceGoniometric attributes:
359    // 0: Name
360    // 1: LightColour (IfcColourRgb)
361    // 2: AmbientIntensity
362    // 3: Intensity
363    // 4: Position (IfcAxis2Placement3D)
364    // 5: ColourAppearance (IfcColourRgb)
365    // 6: ColourTemperature
366    // 7: LuminousFlux
367    // 8: LightEmissionSource
368    // 9: LightDistributionDataSource
369
370    let color_temperature = entity.get_float(6);
371    let luminous_flux = entity.get_float(7);
372    let emission_source = entity.get_enum(8).map(|s| s.to_string());
373    let intensity = entity.get_float(3);
374
375    // Extract color RGB
376    let color_rgb = entity.get_ref(1).and_then(|color_ref| {
377        resolver.get(color_ref).and_then(|color| {
378            let r = color.get_float(1)?;
379            let g = color.get_float(2)?;
380            let b = color.get_float(3)?;
381            Some((r, g, b))
382        })
383    });
384
385    // Extract distribution data
386    let distribution = entity.get_ref(9).and_then(|dist_ref| {
387        resolver
388            .get(dist_ref)
389            .map(|dist| extract_distribution(&dist, resolver))
390    });
391
392    LightSourceData {
393        id: entity.id.0 as u64,
394        source_type: "GONIOMETRIC".to_string(),
395        color_temperature,
396        luminous_flux,
397        emission_source,
398        intensity,
399        color_rgb,
400        distribution,
401    }
402}
403
404/// Extract light intensity distribution data
405fn extract_distribution(
406    entity: &DecodedEntity,
407    resolver: &dyn EntityResolver,
408) -> LightDistributionData {
409    // IfcLightIntensityDistribution attributes:
410    // 0: LightDistributionCurve (enum: TYPE_A, TYPE_B, TYPE_C)
411    // 1: DistributionData (list of IfcLightDistributionData)
412
413    let distribution_type = entity
414        .get_enum(0)
415        .map(|s| s.to_string())
416        .unwrap_or_else(|| "TYPE_C".to_string());
417
418    let mut planes = Vec::new();
419
420    if let Some(data_list) = entity.get_list(1) {
421        for data_item in data_list {
422            if let AttributeValue::EntityRef(data_ref) = data_item {
423                if let Some(data_entity) = resolver.get(*data_ref) {
424                    // IfcLightDistributionData:
425                    // 0: MainPlaneAngle
426                    // 1: SecondaryPlaneAngle (list)
427                    // 2: LuminousIntensity (list)
428
429                    let main_angle = data_entity.get_float(0).unwrap_or(0.0);
430                    let mut intensities = Vec::new();
431
432                    if let (Some(angles), Some(values)) =
433                        (data_entity.get_list(1), data_entity.get_list(2))
434                    {
435                        for (angle, value) in angles.iter().zip(values.iter()) {
436                            let a = angle.as_float().unwrap_or(0.0);
437                            let v = value.as_float().unwrap_or(0.0);
438                            intensities.push((a, v));
439                        }
440                    }
441
442                    planes.push(DistributionPlane {
443                        main_angle,
444                        intensities,
445                    });
446                }
447            }
448        }
449    }
450
451    LightDistributionData {
452        distribution_type,
453        planes,
454    }
455}
456
457/// Export lighting data to JSON format compatible with gldf-ifc-viewer
458pub fn export_to_json(export: &LightingExport) -> String {
459    serde_json::to_string_pretty(export).unwrap_or_else(|_| "{}".to_string())
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_light_fixture_data_serialization() {
468        let fixture = LightFixtureData {
469            id: 123,
470            global_id: Some("abc-def".to_string()),
471            name: Some("Test Fixture".to_string()),
472            description: None,
473            object_type: None,
474            position: (1.0, 2.0, 3.0),
475            storey: Some("Ground Floor".to_string()),
476            storey_elevation: Some(0.0),
477            fixture_type: None,
478            light_sources: vec![],
479            properties: FxHashMap::default(),
480        };
481
482        let json = serde_json::to_string(&fixture).unwrap();
483        assert!(json.contains("Test Fixture"));
484        assert!(json.contains("123"));
485    }
486}