1use super::step_parser::{StepParser, StepValue};
12use anyhow::{anyhow, Result};
13
14#[derive(Debug, Clone, Default, PartialEq)]
16pub struct ImportedLuminaire {
17 pub name: String,
19 pub description: Option<String>,
21 pub manufacturer: Option<String>,
23 pub model_reference: Option<String>,
25 pub variants: Vec<ImportedVariant>,
27 pub descriptive_attributes: ProductDescriptiveAttributes,
29 pub embedded_files: Vec<EmbeddedFile>,
31}
32
33#[derive(Debug, Clone, Default, PartialEq)]
35pub struct ProductDescriptiveAttributes {
36 pub electrical_safety_class: Option<String>,
38 pub ip_code: Option<String>,
40 pub median_useful_life: Option<String>,
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub struct EmbeddedFile {
47 pub file_type: String,
49 pub filename: String,
51 pub content_type: String,
53 pub content: Vec<u8>,
55}
56
57#[derive(Debug, Clone, Default, PartialEq)]
59pub struct ImportedVariant {
60 pub name: String,
62 pub description: Option<String>,
64 pub gldf_variant_id: Option<String>,
66 pub fixture_type: Option<String>,
68 pub light_sources: Vec<ImportedLightSource>,
70 pub photometry: Option<ImportedPhotometry>,
72 pub geometry: Option<ImportedGeometry>,
74 pub properties: ImportedProperties,
76}
77
78#[derive(Debug, Clone, Default, PartialEq)]
80pub struct ImportedLightSource {
81 pub name: String,
83 pub color_temperature: Option<f64>,
85 pub luminous_flux: Option<f64>,
87 pub emission_source: Option<String>,
89 pub distribution: Option<ImportedDistribution>,
91 pub photometry_filename: Option<String>,
93 pub ldt_metadata: Option<LdtMetadata>,
95}
96
97#[derive(Debug, Clone, Default, PartialEq)]
99pub struct LdtMetadata {
100 pub symmetry: i32,
102 pub num_c_planes: i32,
104 pub num_g_angles: i32,
106 pub dc: f64,
108 pub dg: f64,
110 pub total_flux: Option<f64>,
112 pub dr: Option<[f64; 10]>,
114 pub luminaire_name: Option<String>,
116 pub manufacturer: Option<String>,
118 pub raw_content: Option<String>,
120}
121
122#[derive(Debug, Clone, Default, PartialEq)]
124pub struct ImportedDistribution {
125 pub distribution_type: String,
127 pub data: Vec<DistributionPlane>,
129}
130
131#[derive(Debug, Clone, Default, PartialEq)]
133pub struct DistributionPlane {
134 pub main_angle: f64,
136 pub intensities: Vec<(f64, f64)>, }
139
140#[derive(Debug, Clone, Default, PartialEq)]
142pub struct ImportedPhotometry {
143 pub name: String,
145 pub luminous_flux: f64,
147 pub num_c_planes: usize,
149 pub num_gamma_angles: usize,
151 pub c_angles: Vec<f64>,
153 pub gamma_angles: Vec<f64>,
155 pub intensities: Vec<Vec<f64>>,
157}
158
159#[derive(Debug, Clone, Default, PartialEq)]
161pub struct ImportedGeometry {
162 pub vertices: Vec<(f64, f64, f64)>,
164 pub triangles: Vec<(u32, u32, u32)>,
166}
167
168#[derive(Debug, Clone, Default, PartialEq)]
170pub struct ImportedProperties {
171 pub number_of_sources: Option<i32>,
173 pub total_wattage: Option<f64>,
175 pub mounting_type: Option<String>,
177 pub color_temperature: Option<f64>,
179 pub luminous_flux: Option<f64>,
181 pub power: Option<f64>,
183 pub efficacy: Option<f64>,
185 pub cri: Option<i32>,
187 pub weight: Option<f64>,
189 pub ip_code: Option<String>,
191 pub ik_code: Option<String>,
193 pub rated_voltage: Option<f64>,
195 pub rated_voltage_max: Option<f64>,
197 pub rated_current: Option<f64>,
199 pub power_factor: Option<f64>,
201 pub nominal_frequency_min: Option<f64>,
203 pub nominal_frequency_max: Option<f64>,
205 pub number_of_poles: Option<i32>,
207 pub has_protective_earth: Option<bool>,
209 pub insulation_standard_class: Option<String>,
211 pub heat_dissipation: Option<f64>,
213 pub nominal_power_consumption: Option<f64>,
215 pub number_of_power_supply_ports: Option<i32>,
217 pub electrical_safety_class: Option<String>,
219 pub median_useful_life: Option<String>,
221 pub gldf_mounting_type: Option<String>,
223 pub recessed_depth: Option<f64>,
225 pub maintenance_factor: Option<f64>,
227}
228
229pub struct IfcImporter {
231 parser: StepParser,
232}
233
234impl IfcImporter {
235 #[allow(clippy::should_implement_trait)]
237 pub fn from_str(content: &str) -> Result<Self> {
238 let parser = StepParser::parse(content)?;
239 Ok(Self { parser })
240 }
241
242 pub fn import(&self) -> Result<ImportedLuminaire> {
244 let mut luminaire = ImportedLuminaire::default();
245
246 let fixture_types = self.parser.find_by_type("IFCLIGHTFIXTURETYPE");
248
249 if fixture_types.is_empty() {
250 return Err(anyhow!("No IFCLIGHTFIXTURETYPE found in IFC file"));
251 }
252
253 if let Some(first_type) = fixture_types.first() {
255 let params = first_type.get_params();
256
257 if let Some(name) = params.get(2).and_then(|v| v.as_string()) {
259 if let Some(pos) = name.find(" - ") {
261 luminaire.name = name[..pos].to_string();
262 } else {
263 luminaire.name = name.to_string();
264 }
265 }
266
267 if let Some(desc) = params.get(3).and_then(|v| v.as_string()) {
269 luminaire.description = Some(desc.to_string());
270 }
271
272 if let Some(pset) =
274 self.find_property_set(first_type.id, "Pset_ManufacturerTypeInformation")
275 {
276 if let Some(mfr) = self.get_property_value(&pset, "Manufacturer") {
277 luminaire.manufacturer = mfr.as_string().map(|s| s.to_string());
278 }
279 if let Some(model) = self.get_property_value(&pset, "ModelReference") {
280 luminaire.model_reference = model.as_string().map(|s| s.to_string());
281 }
282 }
283 }
284
285 for fixture_type in &fixture_types {
287 let variant = self.extract_variant(fixture_type)?;
288 luminaire.variants.push(variant);
289 }
290
291 if let Some(first_variant) = luminaire.variants.first() {
294 luminaire.descriptive_attributes.electrical_safety_class =
295 first_variant.properties.electrical_safety_class.clone();
296 luminaire.descriptive_attributes.ip_code = first_variant.properties.ip_code.clone();
297 luminaire.descriptive_attributes.median_useful_life =
298 first_variant.properties.median_useful_life.clone();
299 }
300
301 luminaire.embedded_files = self.extract_embedded_files();
303
304 Ok(luminaire)
305 }
306
307 fn extract_embedded_files(&self) -> Vec<EmbeddedFile> {
309 use base64::{engine::general_purpose::STANDARD, Engine};
310
311 let mut files = Vec::new();
312
313 for entity in self.parser.find_by_type("IFCPROPERTYSET") {
315 let params = entity.get_params();
316 if let Some(name) = params.get(2).and_then(|v| v.as_string()) {
317 if name == "Pset_GLDF_EmbeddedFile" {
318 let mut file_type = None;
320 let mut filename = None;
321 let mut content_type = None;
322 let mut content_b64 = None;
323
324 if let Some(StepValue::List(props)) = params.get(4) {
325 for prop_ref in props {
326 if let StepValue::Ref(prop_id) = prop_ref {
327 if let Some(prop_entity) = self.parser.get(*prop_id) {
328 let prop_params = prop_entity.get_params();
329 if let Some(prop_name) =
330 prop_params.first().and_then(|v: &StepValue| v.as_string())
331 {
332 let value = prop_params
333 .get(2)
334 .and_then(|v: &StepValue| v.as_string());
335 match prop_name {
336 "GLDF_FileType" => file_type = value.map(String::from),
337 "GLDF_Filename" => filename = value.map(String::from),
338 "GLDF_ContentType" => {
339 content_type = value.map(String::from)
340 }
341 "GLDF_FileContent" => {
342 content_b64 = value.map(String::from)
343 }
344 _ => {}
345 }
346 }
347 }
348 }
349 }
350 }
351
352 if let (Some(ft), Some(fn_), Some(ct), Some(b64)) =
354 (file_type, filename, content_type, content_b64)
355 {
356 if let Ok(content) = STANDARD.decode(&b64) {
357 files.push(EmbeddedFile {
358 file_type: ft,
359 filename: fn_,
360 content_type: ct,
361 content,
362 });
363 }
364 }
365 }
366 }
367 }
368
369 files
370 }
371
372 fn extract_variant(
374 &self,
375 fixture_type: &super::step_parser::StepEntity,
376 ) -> Result<ImportedVariant> {
377 let mut variant = ImportedVariant::default();
378 let params = fixture_type.get_params();
379
380 if let Some(name) = params.get(2).and_then(|v| v.as_string()) {
382 variant.name = name.to_string();
383 }
384
385 if let Some(desc) = params.get(3).and_then(|v| v.as_string()) {
387 variant.description = Some(desc.to_string());
388 }
389
390 if let Some(ptype) = params.get(9).and_then(|v| v.as_enum()) {
392 variant.fixture_type = Some(ptype.to_string());
393 }
394
395 if let Some(pset) = self.find_property_set(fixture_type.id, "Pset_LightFixtureTypeCommon") {
397 if let Some(v) = self.get_property_value(&pset, "NumberOfSources") {
398 variant.properties.number_of_sources = v.as_integer().map(|i| i as i32);
399 }
400 if let Some(v) = self.get_property_value(&pset, "TotalWattage") {
401 variant.properties.total_wattage = v.as_real();
402 }
403 if let Some(v) = self.get_property_value(&pset, "LightFixtureMountingType") {
404 variant.properties.mounting_type = v.as_string().map(|s| s.to_string());
405 }
406 }
407
408 if let Some(pset) = self.find_property_set(fixture_type.id, "Pset_LuminaireVariant") {
410 if let Some(v) = self.get_property_value(&pset, "GLDF_VariantId") {
411 variant.gldf_variant_id = v.as_string().map(|s| s.to_string());
412 }
413 if let Some(v) = self.get_property_value(&pset, "ColorTemperature") {
414 variant.properties.color_temperature = v.as_real();
415 }
416 if let Some(v) = self.get_property_value(&pset, "LuminousFlux") {
417 variant.properties.luminous_flux = v.as_real();
418 }
419 if let Some(v) = self.get_property_value(&pset, "Power") {
420 variant.properties.power = v.as_real();
421 }
422 if let Some(v) = self.get_property_value(&pset, "Efficacy") {
423 variant.properties.efficacy = v.as_real();
424 }
425 if let Some(v) = self.get_property_value(&pset, "ColorRenderingIndex") {
426 variant.properties.cri = v.as_integer().map(|i| i as i32);
427 }
428 if let Some(v) = self.get_property_value(&pset, "Weight") {
429 variant.properties.weight = v.as_real();
430 }
431 }
432
433 if let Some(pset) = self.find_property_set(fixture_type.id, "Pset_ElectricalDeviceCommon") {
435 if let Some(v) = self.get_property_value(&pset, "IP_Code") {
437 variant.properties.ip_code = v.as_string().map(|s| s.to_string());
438 }
439 if variant.properties.ip_code.is_none() {
441 if let Some(v) = self.get_property_value(&pset, "IPCode") {
442 variant.properties.ip_code = v.as_string().map(|s| s.to_string());
443 }
444 }
445 if let Some(v) = self.get_property_value(&pset, "IK_Code") {
447 variant.properties.ik_code = v.as_string().map(|s| s.to_string());
448 }
449 if let Some(v) = self.get_property_value(&pset, "RatedVoltage") {
451 variant.properties.rated_voltage = v.as_real();
452 }
454 if let Some(v) = self.get_property_value(&pset, "RatedCurrent") {
456 variant.properties.rated_current = v.as_real();
457 }
458 if let Some(v) = self.get_property_value(&pset, "PowerFactor") {
460 variant.properties.power_factor = v.as_real();
461 }
462 if let Some(v) = self.get_property_value(&pset, "NominalFrequencyRange") {
464 if let Some(freq) = v.as_real() {
466 variant.properties.nominal_frequency_min = Some(freq);
467 variant.properties.nominal_frequency_max = Some(freq);
468 }
469 }
470 if let Some(v) = self.get_property_value(&pset, "NumberOfPoles") {
472 variant.properties.number_of_poles = v.as_integer().map(|i| i as i32);
473 }
474 if let Some(v) = self.get_property_value(&pset, "HasProtectiveEarth") {
476 variant.properties.has_protective_earth = v.as_boolean();
477 }
478 if let Some(v) = self.get_property_value(&pset, "InsulationStandardClass") {
480 variant.properties.insulation_standard_class = v
481 .as_enum()
482 .map(|s| s.to_string())
483 .or_else(|| v.as_string().map(|s| s.to_string()));
484 }
485 if let Some(v) = self.get_property_value(&pset, "HeatDissipation") {
487 variant.properties.heat_dissipation = v.as_real();
488 }
489 if let Some(v) = self.get_property_value(&pset, "Power") {
491 if variant.properties.power.is_none() {
493 variant.properties.power = v.as_real();
494 }
495 }
496 if let Some(v) = self.get_property_value(&pset, "NominalPowerConsumption") {
498 variant.properties.nominal_power_consumption = v.as_real();
499 }
500 if let Some(v) = self.get_property_value(&pset, "NumberOfPowerSupplyPorts") {
502 variant.properties.number_of_power_supply_ports = v.as_integer().map(|i| i as i32);
503 }
504 }
505
506 if let Some(pset) = self.find_property_set(fixture_type.id, "Pset_LightFixtureTypeCommon") {
508 if let Some(v) = self.get_property_value(&pset, "MaintenanceFactor") {
509 variant.properties.maintenance_factor = v.as_real();
510 }
511 if variant.properties.number_of_sources.is_none() {
513 if let Some(v) = self.get_property_value(&pset, "NumberOfSources") {
514 variant.properties.number_of_sources = v.as_integer().map(|i| i as i32);
515 }
516 }
517 if variant.properties.total_wattage.is_none() {
518 if let Some(v) = self.get_property_value(&pset, "TotalWattage") {
519 variant.properties.total_wattage = v.as_real();
520 }
521 }
522 }
523
524 for pset in self.find_all_property_sets(fixture_type.id, "Pset_GLDF_DescriptiveAttributes")
526 {
527 if let Some(v) = self.get_property_value(&pset, "ElectricalSafetyClass") {
528 variant.properties.electrical_safety_class = v.as_string().map(|s| s.to_string());
529 }
530 if let Some(v) = self.get_property_value(&pset, "MedianUsefulLife") {
531 variant.properties.median_useful_life = v.as_string().map(|s| s.to_string());
532 }
533 if let Some(v) = self.get_property_value(&pset, "GLDF_MountingType") {
534 variant.properties.gldf_mounting_type = v.as_string().map(|s| s.to_string());
535 }
536 if let Some(v) = self.get_property_value(&pset, "GLDF_RecessedDepth") {
537 variant.properties.recessed_depth = v.as_real();
538 }
539 }
540
541 let photometry_filenames = self.extract_photometry_filenames(fixture_type.id);
543
544 let ldt_metadata_map = self.extract_ldt_metadata(fixture_type.id);
546
547 let ldt_raw_content_map = self.extract_ldt_raw_content(fixture_type.id);
549
550 let light_sources = self.find_light_sources_for_fixture(fixture_type.id);
552 for (idx, ls_id) in light_sources.iter().enumerate() {
553 if let Some(mut ls) = self.extract_light_source(*ls_id) {
554 if let Some(filename) = photometry_filenames.get(idx) {
556 ls.photometry_filename = Some(filename.clone());
557 }
558 if let Some((filename, content)) = ldt_raw_content_map.get(&(idx + 1)) {
561 let mut meta = ldt_metadata_map
562 .get(&(idx + 1))
563 .cloned()
564 .unwrap_or_default();
565 meta.raw_content = Some(content.clone());
566 ls.photometry_filename = Some(filename.clone());
568 ls.ldt_metadata = Some(meta);
569 } else if let Some(meta) = ldt_metadata_map.get(&(idx + 1)) {
570 ls.ldt_metadata = Some(meta.clone());
571 }
572 variant.light_sources.push(ls);
573 }
574 }
575
576 if let Some(occurrence_id) = self.find_fixture_occurrence(fixture_type.id) {
578 if let Some(geom) = self.extract_geometry(occurrence_id) {
579 variant.geometry = Some(geom);
580 }
581 }
582
583 Ok(variant)
584 }
585
586 fn extract_photometry_filenames(&self, element_id: u64) -> Vec<String> {
588 let mut filenames = Vec::new();
589
590 if let Some(pset_id) = self.find_property_set(element_id, "Pset_GLDF_PhotometryFiles") {
591 for i in 1..=100 {
593 let prop_name = format!("PhotometryFile_{}", i);
594 if let Some(value) = self.get_property_value(&pset_id, &prop_name) {
595 if let Some(filename) = value.as_string() {
596 filenames.push(filename.to_string());
597 }
598 } else {
599 break; }
601 }
602 }
603
604 filenames
605 }
606
607 fn extract_ldt_metadata(
609 &self,
610 element_id: u64,
611 ) -> std::collections::HashMap<usize, LdtMetadata> {
612 use std::collections::HashMap;
613 let mut metadata_map: HashMap<usize, LdtMetadata> = HashMap::new();
614
615 for pset_id in self.find_all_property_sets(element_id, "Pset_GLDF_LDTMetadata") {
617 for i in 1..=100 {
620 let idx_name = format!("LDT_{}_Index", i);
621 if self.get_property_value(&pset_id, &idx_name).is_none() {
623 continue; }
625
626 let mut meta = LdtMetadata::default();
627
628 if let Some(v) = self.get_property_value(&pset_id, &format!("LDT_{}_Symmetry", i)) {
630 meta.symmetry = v.as_integer().unwrap_or(0) as i32;
631 }
632
633 if let Some(v) = self.get_property_value(&pset_id, &format!("LDT_{}_NumCPlanes", i))
635 {
636 meta.num_c_planes = v.as_integer().unwrap_or(1) as i32;
637 }
638
639 if let Some(v) = self.get_property_value(&pset_id, &format!("LDT_{}_NumGAngles", i))
641 {
642 meta.num_g_angles = v.as_integer().unwrap_or(19) as i32;
643 }
644
645 if let Some(v) = self.get_property_value(&pset_id, &format!("LDT_{}_Dc", i)) {
647 meta.dc = v.as_real().unwrap_or(0.0);
648 }
649
650 if let Some(v) = self.get_property_value(&pset_id, &format!("LDT_{}_Dg", i)) {
652 meta.dg = v.as_real().unwrap_or(0.0);
653 }
654
655 if let Some(v) = self.get_property_value(&pset_id, &format!("LDT_{}_TotalFlux", i))
657 {
658 meta.total_flux = Some(v.as_real().unwrap_or(1000.0));
659 }
660
661 if let Some(v) = self.get_property_value(&pset_id, &format!("LDT_{}_DR", i)) {
663 if let Some(dr_str) = v.as_string() {
664 let dr_vals: Vec<f64> = dr_str
665 .split(',')
666 .filter_map(|s| s.trim().parse().ok())
667 .collect();
668 if dr_vals.len() == 10 {
669 let mut dr_arr = [1.0; 10];
670 for (j, val) in dr_vals.iter().enumerate() {
671 dr_arr[j] = *val;
672 }
673 meta.dr = Some(dr_arr);
674 }
675 }
676 }
677
678 if let Some(v) =
680 self.get_property_value(&pset_id, &format!("LDT_{}_LuminaireName", i))
681 {
682 meta.luminaire_name = v.as_string().map(|s| s.to_string());
683 }
684
685 metadata_map.insert(i, meta);
686 }
687 }
688
689 metadata_map
690 }
691
692 fn extract_ldt_raw_content(
694 &self,
695 element_id: u64,
696 ) -> std::collections::HashMap<usize, (String, String)> {
697 use std::collections::HashMap;
698 let mut content_map: HashMap<usize, (String, String)> = HashMap::new();
699
700 for pset_id in self.find_all_property_sets(element_id, "Pset_GLDF_LDTRawContent") {
702 for i in 1..=100 {
704 let filename_prop = format!("LDT_{}_Filename", i);
705 let content_prop = format!("LDT_{}_Content", i);
706
707 let filename = self
708 .get_property_value(&pset_id, &filename_prop)
709 .and_then(|v| v.as_string().map(|s| s.to_string()));
710 let content_b64 = self
711 .get_property_value(&pset_id, &content_prop)
712 .and_then(|v| v.as_string().map(|s| s.to_string()));
713
714 if let (Some(f), Some(c)) = (filename, content_b64) {
715 use base64::{engine::general_purpose::STANDARD, Engine};
717 if let Ok(decoded) = STANDARD.decode(&c) {
718 if let Ok(content) = String::from_utf8(decoded) {
719 content_map.insert(i, (f, content));
720 }
721 }
722 }
723 }
724 }
725
726 content_map
727 }
728
729 fn find_all_property_sets(&self, element_id: u64, pset_name: &str) -> Vec<u64> {
731 let mut psets = Vec::new();
732
733 for entity in self.parser.find_by_type("IFCRELDEFINESBYPROPERTIES") {
735 let params = entity.get_params();
736
737 let related_to_element = params
739 .get(4)
740 .and_then(|v| v.as_list())
741 .is_some_and(|list| list.iter().any(|item| item.as_ref() == Some(element_id)));
742
743 if !related_to_element {
744 continue;
745 }
746
747 if let Some(pset_ref) = params.get(5).and_then(|v| v.as_ref()) {
749 if let Some(pset) = self.parser.get(pset_ref) {
750 if pset.entity_type == "IFCPROPERTYSET" {
751 let pset_params = pset.get_params();
752 if let Some(name) = pset_params.get(2).and_then(|v| v.as_string()) {
754 if name == pset_name {
755 psets.push(pset_ref);
756 }
757 }
758 }
759 }
760 }
761 }
762
763 psets
764 }
765
766 fn find_property_set(&self, element_id: u64, pset_name: &str) -> Option<u64> {
768 for entity in self.parser.find_by_type("IFCRELDEFINESBYPROPERTIES") {
770 let params = entity.get_params();
771
772 if let Some(related) = params.get(4).and_then(|v| v.as_list()) {
774 let has_element = related.iter().any(|v| v.as_ref() == Some(element_id));
775
776 if has_element {
777 if let Some(pset_ref) = params.get(5).and_then(|v| v.as_ref()) {
779 if let Some(pset) = self.parser.get(pset_ref) {
780 if pset.entity_type == "IFCPROPERTYSET" {
781 let pset_params = pset.get_params();
782 if let Some(name) = pset_params.get(2).and_then(|v| v.as_string()) {
784 if name == pset_name {
785 return Some(pset_ref);
786 }
787 }
788 }
789 }
790 }
791 }
792 }
793 }
794 None
795 }
796
797 fn get_property_value(&self, pset_id: &u64, property_name: &str) -> Option<StepValue> {
799 let pset = self.parser.get(*pset_id)?;
800 let params = pset.get_params();
801
802 let properties = params.get(4)?.as_list()?;
804
805 for prop_ref in properties {
806 if let Some(prop_id) = prop_ref.as_ref() {
807 if let Some(prop) = self.parser.get(prop_id) {
808 if prop.entity_type == "IFCPROPERTYSINGLEVALUE" {
809 let prop_params = prop.get_params();
810 if let Some(name) = prop_params.first().and_then(|v| v.as_string()) {
812 if name == property_name {
813 return prop_params.get(2).cloned();
815 }
816 }
817 }
818 }
819 }
820 }
821 None
822 }
823
824 fn find_light_sources_for_fixture(&self, fixture_type_id: u64) -> Vec<u64> {
826 let mut sources = Vec::new();
827
828 let occurrences = self.find_occurrences_of_type(fixture_type_id);
830
831 for occ_id in occurrences {
832 for entity in self.parser.find_by_type("IFCRELASSIGNSTOGROUP") {
834 let params = entity.get_params();
835
836 if params.get(6).and_then(|v| v.as_ref()) == Some(occ_id) {
838 if let Some(related) = params.get(5).and_then(|v| v.as_list()) {
840 for item in related {
841 if let Some(id) = item.as_ref() {
842 if let Some(e) = self.parser.get(id) {
843 if e.entity_type == "IFCLIGHTSOURCEGONIOMETRIC"
844 || e.entity_type == "IFCLIGHTSOURCESPOT"
845 || e.entity_type == "IFCLIGHTSOURCEPOSITIONAL"
846 {
847 sources.push(id);
848 }
849 }
850 }
851 }
852 }
853 }
854 }
855 }
856
857 if sources.is_empty() {
860 for entity in self.parser.find_by_type("IFCLIGHTSOURCEGONIOMETRIC") {
861 sources.push(entity.id);
862 }
863 }
864
865 sources
866 }
867
868 fn find_occurrences_of_type(&self, type_id: u64) -> Vec<u64> {
870 let mut occurrences = Vec::new();
871
872 for entity in self.parser.find_by_type("IFCRELDEFINESBYTYPE") {
874 let params = entity.get_params();
875
876 if params.get(5).and_then(|v| v.as_ref()) == Some(type_id) {
878 if let Some(related) = params.get(4).and_then(|v| v.as_list()) {
880 for item in related {
881 if let Some(id) = item.as_ref() {
882 occurrences.push(id);
883 }
884 }
885 }
886 }
887 }
888
889 occurrences
890 }
891
892 fn find_fixture_occurrence(&self, type_id: u64) -> Option<u64> {
894 self.find_occurrences_of_type(type_id).first().copied()
895 }
896
897 fn extract_light_source(&self, light_source_id: u64) -> Option<ImportedLightSource> {
899 let entity = self.parser.get(light_source_id)?;
900 let params = entity.get_params();
901
902 let mut source = ImportedLightSource::default();
903
904 if let Some(name) = params.first().and_then(|v| v.as_string()) {
911 source.name = name.to_string();
912 }
913
914 if let Some(cct) = params.get(6).and_then(|v| v.as_real()) {
916 source.color_temperature = Some(cct);
917 }
918
919 if let Some(flux) = params.get(7).and_then(|v| v.as_real()) {
921 source.luminous_flux = Some(flux);
922 }
923
924 if let Some(emission) = params.get(8).and_then(|v| v.as_enum()) {
926 source.emission_source = Some(emission.to_string());
927 }
928
929 if let Some(dist_ref) = params.get(9).and_then(|v| v.as_ref()) {
931 if let Some(ext_ref) = self.parser.get(dist_ref) {
933 if ext_ref.entity_type == "IFCEXTERNALREFERENCE" {
934 let ext_params = ext_ref.get_params();
935 if let Some(location) = ext_params.first().and_then(|v| v.as_string()) {
937 let filename = location.rsplit('/').next().unwrap_or(location);
939 source.photometry_filename = Some(filename.to_string());
940 }
941 }
942 }
943 source.distribution = self.extract_distribution(dist_ref);
945 }
946
947 Some(source)
948 }
949
950 fn extract_distribution(&self, dist_id: u64) -> Option<ImportedDistribution> {
952 let entity = self.parser.get(dist_id)?;
953
954 if entity.entity_type == "IFCLIGHTINTENSITYDISTRIBUTION" {
956 let params = entity.get_params();
957 let mut dist = ImportedDistribution::default();
958
959 if let Some(curve_type) = params.first().and_then(|v| v.as_enum()) {
961 dist.distribution_type = curve_type.to_string();
962 }
963
964 if let Some(data_list) = params.get(1).and_then(|v| v.as_list()) {
966 for data_ref in data_list {
967 if let Some(data_id) = data_ref.as_ref() {
968 if let Some(plane) = self.extract_distribution_plane(data_id) {
969 dist.data.push(plane);
970 }
971 }
972 }
973 }
974
975 return Some(dist);
976 }
977
978 None
979 }
980
981 fn extract_distribution_plane(&self, data_id: u64) -> Option<DistributionPlane> {
983 let entity = self.parser.get(data_id)?;
984 if entity.entity_type != "IFCLIGHTDISTRIBUTIONDATA" {
985 return None;
986 }
987
988 let params = entity.get_params();
989 let mut plane = DistributionPlane::default();
990
991 if let Some(main) = params.first().and_then(|v| v.as_real()) {
993 plane.main_angle = main;
994 }
995
996 if let Some(secondary_list) = params.get(1).and_then(|v| v.as_list()) {
999 let intensity_list = params.get(2).and_then(|v| v.as_list());
1000
1001 for (i, angle_val) in secondary_list.iter().enumerate() {
1002 if let Some(angle) = angle_val.as_real() {
1003 let intensity = intensity_list
1004 .and_then(|l| l.get(i))
1005 .and_then(|v| v.as_real())
1006 .unwrap_or(0.0);
1007 plane.intensities.push((angle, intensity));
1008 }
1009 }
1010 }
1011
1012 Some(plane)
1013 }
1014
1015 fn extract_geometry(&self, occurrence_id: u64) -> Option<ImportedGeometry> {
1017 let entity = self.parser.get(occurrence_id)?;
1018 let params = entity.get_params();
1019
1020 let rep_ref = params.get(6).and_then(|v| v.as_ref())?;
1022 let rep = self.parser.get(rep_ref)?;
1023
1024 if rep.entity_type != "IFCPRODUCTDEFINITIONSHAPE" {
1025 return None;
1026 }
1027
1028 let rep_params = rep.get_params();
1029 let reps = rep_params.get(2)?.as_list()?;
1031
1032 for rep_item in reps {
1033 if let Some(shape_rep_id) = rep_item.as_ref() {
1034 if let Some(geom) = self.extract_shape_representation(shape_rep_id) {
1035 return Some(geom);
1036 }
1037 }
1038 }
1039
1040 None
1041 }
1042
1043 fn extract_shape_representation(&self, rep_id: u64) -> Option<ImportedGeometry> {
1045 let rep = self.parser.get(rep_id)?;
1046 if rep.entity_type != "IFCSHAPEREPRESENTATION" {
1047 return None;
1048 }
1049
1050 let params = rep.get_params();
1051 let rep_type = params.get(2).and_then(|v| v.as_string())?;
1053
1054 let items = params.get(3)?.as_list()?;
1056
1057 match rep_type {
1059 "Tessellation" => {
1060 for item in items {
1062 if let Some(item_id) = item.as_ref() {
1063 if let Some(geom) = self.extract_triangulated_faceset(item_id) {
1064 return Some(geom);
1065 }
1066 }
1067 }
1068 }
1069 "MappedRepresentation" => {
1070 for item in items {
1072 if let Some(item_id) = item.as_ref() {
1073 if let Some(geom) = self.extract_mapped_item(item_id) {
1074 return Some(geom);
1075 }
1076 }
1077 }
1078 }
1079 "SweptSolid" => {
1080 for item in items {
1082 if let Some(item_id) = item.as_ref() {
1083 if let Some(geom) = self.extract_extruded_solid(item_id) {
1084 return Some(geom);
1085 }
1086 }
1087 }
1088 }
1089 _ => {}
1090 }
1091
1092 None
1093 }
1094
1095 fn extract_mapped_item(&self, item_id: u64) -> Option<ImportedGeometry> {
1097 let entity = self.parser.get(item_id)?;
1098 if entity.entity_type != "IFCMAPPEDITEM" {
1099 return None;
1100 }
1101
1102 let params = entity.get_params();
1103 let map_ref = params.first().and_then(|v| v.as_ref())?;
1105 let rep_map = self.parser.get(map_ref)?;
1106
1107 if rep_map.entity_type != "IFCREPRESENTATIONMAP" {
1108 return None;
1109 }
1110
1111 let map_params = rep_map.get_params();
1112 let shape_rep_ref = map_params.get(1).and_then(|v| v.as_ref())?;
1114
1115 self.extract_shape_representation(shape_rep_ref)
1117 }
1118
1119 fn extract_triangulated_faceset(&self, faceset_id: u64) -> Option<ImportedGeometry> {
1121 let entity = self.parser.get(faceset_id)?;
1122 if entity.entity_type != "IFCTRIANGULATEDFACESET" {
1123 return None;
1124 }
1125
1126 let params = entity.get_params();
1127 let mut geom = ImportedGeometry::default();
1128
1129 if let Some(coords_ref) = params.first().and_then(|v| v.as_ref()) {
1131 if let Some(coords) = self.parser.get(coords_ref) {
1132 if coords.entity_type == "IFCCARTESIANPOINTLIST3D" {
1133 let coords_params = coords.get_params();
1134 if let Some(coord_list) = coords_params.first().and_then(|v| v.as_list()) {
1136 for point in coord_list {
1137 if let Some(point_coords) = point.as_list() {
1138 let x = point_coords
1139 .first()
1140 .and_then(|v| v.as_real())
1141 .unwrap_or(0.0);
1142 let y =
1143 point_coords.get(1).and_then(|v| v.as_real()).unwrap_or(0.0);
1144 let z =
1145 point_coords.get(2).and_then(|v| v.as_real()).unwrap_or(0.0);
1146 geom.vertices.push((x, y, z));
1147 }
1148 }
1149 }
1150 }
1151 }
1152 }
1153
1154 if let Some(index_list) = params.get(3).and_then(|v| v.as_list()) {
1156 for tri in index_list {
1157 if let Some(indices) = tri.as_list() {
1158 let i0 = indices.first().and_then(|v| v.as_integer()).unwrap_or(1) as u32 - 1;
1159 let i1 = indices.get(1).and_then(|v| v.as_integer()).unwrap_or(1) as u32 - 1;
1160 let i2 = indices.get(2).and_then(|v| v.as_integer()).unwrap_or(1) as u32 - 1;
1161 geom.triangles.push((i0, i1, i2));
1162 }
1163 }
1164 }
1165
1166 if geom.vertices.is_empty() || geom.triangles.is_empty() {
1167 return None;
1168 }
1169
1170 Some(geom)
1171 }
1172
1173 fn extract_extruded_solid(&self, solid_id: u64) -> Option<ImportedGeometry> {
1175 let entity = self.parser.get(solid_id)?;
1176 if entity.entity_type != "IFCEXTRUDEDAREASOLID" {
1177 return None;
1178 }
1179
1180 let params = entity.get_params();
1181
1182 let profile_ref = params.first().and_then(|v| v.as_ref())?;
1184 let profile = self.parser.get(profile_ref)?;
1185
1186 let depth = params.get(3).and_then(|v| v.as_real()).unwrap_or(0.1);
1188
1189 if profile.entity_type == "IFCRECTANGLEPROFILEDEF" {
1191 let profile_params = profile.get_params();
1192 let width = profile_params
1193 .get(3)
1194 .and_then(|v| v.as_real())
1195 .unwrap_or(0.3);
1196 let height = profile_params
1197 .get(4)
1198 .and_then(|v| v.as_real())
1199 .unwrap_or(0.3);
1200
1201 return Some(self.create_box_mesh(width, height, depth));
1202 }
1203
1204 None
1205 }
1206
1207 fn create_box_mesh(&self, width: f64, height: f64, depth: f64) -> ImportedGeometry {
1209 let hw = width / 2.0;
1210 let hh = height / 2.0;
1211
1212 let vertices = vec![
1213 (-hw, -hh, 0.0),
1215 (hw, -hh, 0.0),
1216 (hw, hh, 0.0),
1217 (-hw, hh, 0.0),
1218 (-hw, -hh, depth),
1220 (hw, -hh, depth),
1221 (hw, hh, depth),
1222 (-hw, hh, depth),
1223 ];
1224
1225 let triangles = vec![
1226 (0, 2, 1),
1228 (0, 3, 2),
1229 (4, 5, 6),
1231 (4, 6, 7),
1232 (0, 1, 5),
1234 (0, 5, 4),
1235 (2, 3, 7),
1237 (2, 7, 6),
1238 (0, 4, 7),
1240 (0, 7, 3),
1241 (1, 2, 6),
1243 (1, 6, 5),
1244 ];
1245
1246 ImportedGeometry {
1247 vertices,
1248 triangles,
1249 }
1250 }
1251}
1252
1253#[cfg(test)]
1254mod tests {
1255 use super::*;
1256
1257 #[test]
1258 fn test_import_basic() {
1259 let content = r#"
1260ISO-10303-21;
1261HEADER;
1262FILE_DESCRIPTION(('GLDF to IFC Export'),'2;1');
1263FILE_NAME('test.ifc','2024-01-01',(''),(''),'gldf-rs','gldf-rs','');
1264FILE_SCHEMA(('IFC4'));
1265ENDSEC;
1266
1267DATA;
1268#1=IFCPERSON($,$,'',$,$,$,$,$);
1269#2=IFCORGANIZATION($,'Test Corp','GLDF Export',$,$);
1270#3=IFCPERSONANDORGANIZATION(#1,#2,$);
1271#4=IFCAPPLICATION(#2,'1.0','Test','Test');
1272#5=IFCOWNERHISTORY(#3,#4,.READWRITE.,.ADDED.,$,$,$,0);
1273#6=IFCLIGHTFIXTURETYPE('guid1',#5,'LED Panel - 4000K','Test fixture',$,$,$,$,$,.POINTSOURCE.);
1274#7=IFCPROPERTYSINGLEVALUE('Manufacturer',$,IFCLABEL('Test Corp'),$);
1275#8=IFCPROPERTYSINGLEVALUE('ModelReference',$,IFCLABEL('LED-001'),$);
1276#9=IFCPROPERTYSET('guid2',#5,'Pset_ManufacturerTypeInformation',$,(#7,#8));
1277#10=IFCRELDEFINESBYPROPERTIES('guid3',#5,$,$,(#6),#9);
1278ENDSEC;
1279
1280END-ISO-10303-21;
1281"#;
1282
1283 let importer = IfcImporter::from_str(content).unwrap();
1284 let luminaire = importer.import().unwrap();
1285
1286 assert_eq!(luminaire.name, "LED Panel");
1287 assert_eq!(luminaire.manufacturer, Some("Test Corp".to_string()));
1288 assert_eq!(luminaire.model_reference, Some("LED-001".to_string()));
1289 assert_eq!(luminaire.variants.len(), 1);
1290 assert_eq!(luminaire.variants[0].name, "LED Panel - 4000K");
1291 }
1292}