1use bimifc_model::{AttributeValue, DecodedEntity, EntityResolver, IfcType};
11use rustc_hash::FxHashMap;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct LightFixtureData {
17 pub id: u64,
19 pub global_id: Option<String>,
21 pub name: Option<String>,
23 pub description: Option<String>,
25 pub object_type: Option<String>,
27 pub position: (f64, f64, f64),
29 pub storey: Option<String>,
31 pub storey_elevation: Option<f64>,
33 pub fixture_type: Option<LightFixtureTypeData>,
35 pub light_sources: Vec<LightSourceData>,
37 pub properties: FxHashMap<String, PropertySetData>,
39}
40
41#[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#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct LightSourceData {
53 pub id: u64,
54 pub source_type: String,
55 pub color_temperature: Option<f64>,
57 pub luminous_flux: Option<f64>,
59 pub emission_source: Option<String>,
61 pub intensity: Option<f64>,
63 pub color_rgb: Option<(f64, f64, f64)>,
65 pub distribution: Option<LightDistributionData>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct LightDistributionData {
72 pub distribution_type: String,
74 pub planes: Vec<DistributionPlane>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct DistributionPlane {
81 pub main_angle: f64,
83 pub intensities: Vec<(f64, f64)>, }
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct PropertySetData {
90 pub name: String,
91 pub properties: FxHashMap<String, String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct LightingExport {
97 pub schema: String,
99 pub project_name: Option<String>,
101 pub building_name: Option<String>,
103 pub storeys: Vec<StoreyData>,
105 pub light_fixtures: Vec<LightFixtureData>,
107 pub light_fixture_types: Vec<LightFixtureTypeData>,
109 pub summary: LightingSummary,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct StoreyData {
116 pub id: u64,
117 pub name: String,
118 pub elevation: f64,
119}
120
121#[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 pub total_luminous_flux: Option<f64>,
130}
131
132pub 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 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 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 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 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 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 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 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
231fn 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
241fn 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 let position = extract_position(entity, resolver);
250
251 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 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, storey_elevation: None,
271 fixture_type,
272 light_sources,
273 properties: FxHashMap::default(),
274 }
275}
276
277fn extract_position(entity: &DecodedEntity, resolver: &dyn EntityResolver) -> (f64, f64, f64) {
279 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 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
302fn extract_cartesian_point(
304 axis_placement: &DecodedEntity,
305 resolver: &dyn EntityResolver,
306) -> (f64, f64, f64) {
307 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 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
331fn extract_light_sources(
333 _fixture: &DecodedEntity,
334 resolver: &dyn EntityResolver,
335) -> Vec<LightSourceData> {
336 let mut sources = Vec::new();
337
338 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
353fn extract_goniometric_source(
355 entity: &DecodedEntity,
356 resolver: &dyn EntityResolver,
357) -> LightSourceData {
358 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 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 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
404fn extract_distribution(
406 entity: &DecodedEntity,
407 resolver: &dyn EntityResolver,
408) -> LightDistributionData {
409 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 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
457pub 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}