1use super::types::{
6 DistributionSystemEnum, ElectricalDeviceProperties, EntityRef, FlowDirectionEnum,
7 InsulationStandardClass, LightEmissionSourceEnum, LightFixtureCommonProperties,
8 LightFixtureTypeEnum, ManufacturerTypeInfo, OptionalRef, ServiceLifeInfo, WarrantyInfo,
9};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Default)]
17pub struct MeshData {
18 pub vertices: Vec<(f64, f64, f64)>,
20 pub triangles: Vec<(u32, u32, u32)>,
23}
24
25#[cfg(not(target_arch = "wasm32"))]
26use chrono::{DateTime, Utc};
27
28pub struct StepWriter {
32 schema: String,
33 next_id: u64,
34 entities: Vec<String>,
35 entity_map: HashMap<String, EntityRef>,
37 light_source_ids: Vec<EntityRef>,
39 guid_counter: u64,
41}
42
43impl StepWriter {
44 pub fn new(schema: &str) -> Self {
46 Self {
47 schema: schema.to_string(),
48 next_id: 1,
49 entities: Vec::new(),
50 entity_map: HashMap::new(),
51 light_source_ids: Vec::new(),
52 guid_counter: 1,
53 }
54 }
55
56 fn next_id(&mut self) -> EntityRef {
58 let id = EntityRef::new(self.next_id);
59 self.next_id += 1;
60 id
61 }
62
63 fn add_entity(&mut self, entity: String) -> EntityRef {
65 let id = self.next_id();
66 self.entities.push(format!("{}={}", id, entity));
67 id
68 }
69
70 fn generate_guid(&mut self) -> String {
75 const CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
77
78 #[cfg(not(target_arch = "wasm32"))]
80 let seed = {
81 use std::time::{SystemTime, UNIX_EPOCH};
82 let time_part = SystemTime::now()
83 .duration_since(UNIX_EPOCH)
84 .map(|d| d.as_nanos())
85 .unwrap_or(0);
86 time_part.wrapping_add(self.guid_counter as u128 * 0x517cc1b727220a95)
88 };
89
90 #[cfg(target_arch = "wasm32")]
91 let seed = {
92 let base = 0x0123456789ABCDEFu128;
94 base.wrapping_mul(self.guid_counter as u128)
95 .wrapping_add(0xFEDCBA9876543210u128)
96 };
97
98 self.guid_counter += 1;
99
100 let mut result = String::with_capacity(22);
103 let mut n = seed;
104
105 for _ in 0..22 {
106 result.push(CHARS[(n % 64) as usize] as char);
107 n /= 64;
108 }
109
110 result.chars().rev().collect()
112 }
113
114 fn escape_string(s: &str) -> String {
116 s.replace('\\', "\\\\").replace('\'', "''")
117 }
118
119 fn add_axis2_placement_3d(
121 &mut self,
122 origin: EntityRef,
123 axis: Option<EntityRef>,
124 ref_dir: Option<EntityRef>,
125 ) -> EntityRef {
126 let axis_str = axis
127 .map(|a| a.to_string())
128 .unwrap_or_else(|| "$".to_string());
129 let ref_str = ref_dir
130 .map(|r| r.to_string())
131 .unwrap_or_else(|| "$".to_string());
132 let entity_str = format!("IFCAXIS2PLACEMENT3D({},{},{})", origin, axis_str, ref_str);
133 self.add_entity(entity_str)
134 }
135
136 fn add_local_placement(
138 &mut self,
139 relative_to: Option<EntityRef>,
140 placement: EntityRef,
141 ) -> EntityRef {
142 let relative_str = relative_to
143 .map(|r| r.to_string())
144 .unwrap_or_else(|| "$".to_string());
145 let entity_str = format!("IFCLOCALPLACEMENT({},{})", relative_str, placement);
146 self.add_entity(entity_str)
147 }
148
149 pub fn add_owner_history(&mut self, organization_name: &str) -> EntityRef {
155 let person = self.add_entity("IFCPERSON($,$,'',$,$,$,$,$)".to_string());
157
158 let org_str = format!(
160 "IFCORGANIZATION($,'{}','GLDF Export',$,$)",
161 Self::escape_string(organization_name)
162 );
163 let org = self.add_entity(org_str);
164
165 let po_str = format!("IFCPERSONANDORGANIZATION({},{},$)", person, org);
167 let person_org = self.add_entity(po_str);
168
169 let app_str = format!("IFCAPPLICATION({},'0.3.3','gldf-rs','gldf-rs')", org);
171 let app = self.add_entity(app_str);
172
173 let oh_str = format!(
175 "IFCOWNERHISTORY({},{},{},{},$,$,$,{})",
176 person_org,
177 app,
178 ".READWRITE.",
179 ".ADDED.",
180 Self::current_timestamp()
181 );
182 self.add_entity(oh_str)
183 }
184
185 pub fn add_project(&mut self, name: &str, owner_history: EntityRef) -> EntityRef {
187 let si_length = self.add_entity("IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.)".to_string());
189 let si_area = self.add_entity("IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.)".to_string());
190 let si_volume = self.add_entity("IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.)".to_string());
191 let si_angle = self.add_entity("IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.)".to_string());
192 let si_lumen = self.add_entity("IFCSIUNIT(*,.LUMINOUSFLUXUNIT.,$,.LUMEN.)".to_string());
193 let si_power = self.add_entity("IFCSIUNIT(*,.POWERUNIT.,$,.WATT.)".to_string());
194
195 let ua_str = format!(
196 "IFCUNITASSIGNMENT(({},{},{},{},{},{}))",
197 si_length, si_area, si_volume, si_angle, si_lumen, si_power
198 );
199 let unit_assignment = self.add_entity(ua_str);
200
201 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
203 let axis = self.add_entity("IFCDIRECTION((0.,0.,1.))".to_string());
204 let ref_dir = self.add_entity("IFCDIRECTION((1.,0.,0.))".to_string());
205 let placement = self.add_axis2_placement_3d(origin, Some(axis), Some(ref_dir));
206
207 let ctx_str = format!(
208 "IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,{},{})",
209 placement,
210 OptionalRef::None
211 );
212 let context_3d = self.add_entity(ctx_str);
213
214 let guid = self.generate_guid();
216 let escaped_name = Self::escape_string(name);
217 let proj_str = format!(
218 "IFCPROJECT('{}',{},'{}','GLDF to IFC Export',$,$,$,({}),{})",
219 guid, owner_history, escaped_name, context_3d, unit_assignment
220 );
221 let project = self.add_entity(proj_str);
222
223 self.entity_map.insert("project".to_string(), project);
224 self.entity_map.insert("context_3d".to_string(), context_3d);
225 project
226 }
227
228 pub fn add_site(
230 &mut self,
231 name: &str,
232 owner_history: EntityRef,
233 project: EntityRef,
234 ) -> EntityRef {
235 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
237 let axis2_placement = self.add_axis2_placement_3d(origin, None, None);
238 let placement = self.add_local_placement(None, axis2_placement);
239
240 let guid = self.generate_guid();
241 let escaped_name = Self::escape_string(name);
242 let site_str = format!(
243 "IFCSITE('{}',{},'{}','',$,{},$,$,.ELEMENT.,$,$,$,$,$)",
244 guid, owner_history, escaped_name, placement
245 );
246 let site = self.add_entity(site_str);
247
248 let guid2 = self.generate_guid();
250 let rel_str = format!(
251 "IFCRELAGGREGATES('{}',{},$,$,{},({}))",
252 guid2, owner_history, project, site
253 );
254 self.add_entity(rel_str);
255
256 site
257 }
258
259 pub fn add_building(
261 &mut self,
262 name: &str,
263 owner_history: EntityRef,
264 site: EntityRef,
265 ) -> EntityRef {
266 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
268 let axis2_placement = self.add_axis2_placement_3d(origin, None, None);
269 let placement = self.add_local_placement(None, axis2_placement);
270
271 let guid = self.generate_guid();
272 let escaped_name = Self::escape_string(name);
273 let bldg_str = format!(
274 "IFCBUILDING('{}',{},'{}','',$,{},$,$,.ELEMENT.,$,$,$)",
275 guid, owner_history, escaped_name, placement
276 );
277 let building = self.add_entity(bldg_str);
278
279 let guid2 = self.generate_guid();
281 let rel_str = format!(
282 "IFCRELAGGREGATES('{}',{},$,$,{},({}))",
283 guid2, owner_history, site, building
284 );
285 self.add_entity(rel_str);
286
287 building
288 }
289
290 pub fn add_storey(
292 &mut self,
293 name: &str,
294 owner_history: EntityRef,
295 building: EntityRef,
296 ) -> EntityRef {
297 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
299 let axis2_placement = self.add_axis2_placement_3d(origin, None, None);
300 let placement = self.add_local_placement(None, axis2_placement);
301
302 let guid = self.generate_guid();
303 let escaped_name = Self::escape_string(name);
304 let storey_str = format!(
305 "IFCBUILDINGSTOREY('{}',{},'{}','',$,{},$,$,.ELEMENT.,0.)",
306 guid, owner_history, escaped_name, placement
307 );
308 let storey = self.add_entity(storey_str);
309
310 let guid2 = self.generate_guid();
312 let rel_str = format!(
313 "IFCRELAGGREGATES('{}',{},$,$,{},({}))",
314 guid2, owner_history, building, storey
315 );
316 self.add_entity(rel_str);
317
318 storey
319 }
320
321 pub fn add_light_fixture_type(
337 &mut self,
338 name: &str,
339 manufacturer: &str,
340 predefined_type: LightFixtureTypeEnum,
341 owner_history: EntityRef,
342 ) -> EntityRef {
343 self.add_light_fixture_type_with_geometry(
344 name,
345 manufacturer,
346 predefined_type,
347 owner_history,
348 None,
349 )
350 }
351
352 pub fn add_light_fixture_type_with_geometry(
354 &mut self,
355 name: &str,
356 manufacturer: &str,
357 predefined_type: LightFixtureTypeEnum,
358 owner_history: EntityRef,
359 representation_map: Option<EntityRef>,
360 ) -> EntityRef {
361 let guid = self.generate_guid();
362 let escaped_name = Self::escape_string(name);
363
364 let rep_maps = representation_map
368 .map(|r| format!("({})", r))
369 .unwrap_or_else(|| "$".to_string());
370
371 let type_str = format!(
372 "IFCLIGHTFIXTURETYPE('{}',{},'{}','Luminaire from GLDF',$,$,{},$,$,{})",
373 guid,
374 owner_history,
375 escaped_name,
376 rep_maps,
377 predefined_type.to_step()
378 );
379 let fixture_type = self.add_entity(type_str);
380
381 self.add_manufacturer_pset(fixture_type, owner_history, manufacturer, name);
383
384 fixture_type
385 }
386
387 fn create_fixture_geometry(&mut self, width: f64, depth: f64, height: f64) -> EntityRef {
389 let context_3d = self.entity_map.get("context_3d").copied();
390
391 let profile_str = format!(
393 "IFCRECTANGLEPROFILEDEF(.AREA.,$,$,{:.3},{:.3})",
394 width, depth
395 );
396 let profile = self.add_entity(profile_str);
397
398 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
400 let axis = self.add_entity("IFCDIRECTION((0.,0.,1.))".to_string());
401 let ref_dir = self.add_entity("IFCDIRECTION((1.,0.,0.))".to_string());
402 let extrusion_placement = self.add_axis2_placement_3d(origin, Some(axis), Some(ref_dir));
403
404 let extrude_dir = self.add_entity("IFCDIRECTION((0.,0.,1.))".to_string());
406
407 let solid_str = format!(
409 "IFCEXTRUDEDAREASOLID({},{},{},{:.3})",
410 profile, extrusion_placement, extrude_dir, height
411 );
412 let solid = self.add_entity(solid_str);
413
414 let context_str = context_3d
416 .map(|c| c.to_string())
417 .unwrap_or_else(|| "$".to_string());
418 let shape_str = format!(
419 "IFCSHAPEREPRESENTATION({},'Body','SweptSolid',({}))",
420 context_str, solid
421 );
422 let shape_rep = self.add_entity(shape_str);
423
424 let prod_str = format!("IFCPRODUCTDEFINITIONSHAPE($,$,({}))", shape_rep);
426 self.add_entity(prod_str)
427 }
428
429 pub fn create_representation_map(&mut self, mesh: Option<&MeshData>) -> EntityRef {
440 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
442 let axis2_placement = self.add_axis2_placement_3d(origin, None, None);
443
444 let shape_rep = if let Some(m) = mesh {
446 if !m.vertices.is_empty() && !m.triangles.is_empty() {
447 self.create_tessellated_shape_representation(m)
448 } else {
449 self.create_box_shape_representation(0.3, 0.3, 0.1)
450 }
451 } else {
452 self.create_box_shape_representation(0.3, 0.3, 0.1)
453 };
454
455 let map_str = format!("IFCREPRESENTATIONMAP({},{})", axis2_placement, shape_rep);
457 self.add_entity(map_str)
458 }
459
460 fn create_tessellated_shape_representation(&mut self, mesh: &MeshData) -> EntityRef {
462 let context_3d = self.entity_map.get("context_3d").copied();
463
464 let coords_str: String = mesh
466 .vertices
467 .iter()
468 .map(|(x, y, z)| format!("({:.6},{:.6},{:.6})", x, y, z))
469 .collect::<Vec<_>>()
470 .join(",");
471
472 let point_list_str = format!("IFCCARTESIANPOINTLIST3D(({}))", coords_str);
473 let point_list = self.add_entity(point_list_str);
474
475 let triangles_str: String = mesh
477 .triangles
478 .iter()
479 .map(|(a, b, c)| format!("({},{},{})", a, b, c))
480 .collect::<Vec<_>>()
481 .join(",");
482
483 let faceset_str = format!(
484 "IFCTRIANGULATEDFACESET({},$,.T.,({}),())",
485 point_list, triangles_str
486 );
487 let faceset = self.add_entity(faceset_str);
488
489 let context_str = context_3d
490 .map(|c| c.to_string())
491 .unwrap_or_else(|| "$".to_string());
492 let shape_str = format!(
493 "IFCSHAPEREPRESENTATION({},'Body','Tessellation',({}))",
494 context_str, faceset
495 );
496 self.add_entity(shape_str)
497 }
498
499 fn create_box_shape_representation(
501 &mut self,
502 width: f64,
503 height: f64,
504 depth: f64,
505 ) -> EntityRef {
506 let context_3d = self.entity_map.get("context_3d").copied();
507
508 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
509 let axis2 = self.add_axis2_placement_3d(origin, None, None);
510
511 let profile_str = format!(
512 "IFCRECTANGLEPROFILEDEF(.AREA.,'Luminaire Profile',{},{},{})",
513 axis2, width, height
514 );
515 let profile = self.add_entity(profile_str);
516
517 let dir_str = "IFCDIRECTION((0.,0.,1.))";
518 let dir = self.add_entity(dir_str.to_string());
519
520 let solid_str = format!(
521 "IFCEXTRUDEDAREASOLID({},{},{},{})",
522 profile, axis2, dir, depth
523 );
524 let solid = self.add_entity(solid_str);
525
526 let context_str = context_3d
527 .map(|c| c.to_string())
528 .unwrap_or_else(|| "$".to_string());
529 let shape_str = format!(
530 "IFCSHAPEREPRESENTATION({},'Body','SweptSolid',({}))",
531 context_str, solid
532 );
533 self.add_entity(shape_str)
534 }
535
536 pub fn create_tessellated_geometry(&mut self, mesh: &MeshData) -> EntityRef {
549 let context_3d = self.entity_map.get("context_3d").copied();
550
551 let coords_str: String = mesh
553 .vertices
554 .iter()
555 .map(|(x, y, z)| format!("({:.6},{:.6},{:.6})", x, y, z))
556 .collect::<Vec<_>>()
557 .join(",");
558
559 let point_list_str = format!("IFCCARTESIANPOINTLIST3D(({}))", coords_str);
560 let point_list = self.add_entity(point_list_str);
561
562 let triangles_str: String = mesh
565 .triangles
566 .iter()
567 .map(|(a, b, c)| format!("({},{},{})", a, b, c))
568 .collect::<Vec<_>>()
569 .join(",");
570
571 let faceset_str = format!(
579 "IFCTRIANGULATEDFACESET({},$,.T.,({}),())",
580 point_list, triangles_str
581 );
582 let faceset = self.add_entity(faceset_str);
583
584 let context_str = context_3d
586 .map(|c| c.to_string())
587 .unwrap_or_else(|| "$".to_string());
588 let shape_str = format!(
589 "IFCSHAPEREPRESENTATION({},'Body','Tessellation',({}))",
590 context_str, faceset
591 );
592 let shape_rep = self.add_entity(shape_str);
593
594 let prod_str = format!("IFCPRODUCTDEFINITIONSHAPE($,$,({}))", shape_rep);
596 self.add_entity(prod_str)
597 }
598
599 pub fn add_light_fixture_with_map(
610 &mut self,
611 name: &str,
612 owner_history: EntityRef,
613 storey: EntityRef,
614 fixture_type: Option<EntityRef>,
615 representation_map: Option<EntityRef>,
616 mesh_data: Option<&MeshData>,
617 ) -> EntityRef {
618 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
620 let axis2_placement = self.add_axis2_placement_3d(origin, None, None);
621 let placement = self.add_local_placement(None, axis2_placement);
622
623 let geometry = if let Some(rep_map) = representation_map {
625 self.create_mapped_item_geometry(rep_map)
626 } else if let Some(mesh) = mesh_data {
627 if !mesh.vertices.is_empty() && !mesh.triangles.is_empty() {
628 self.create_tessellated_geometry(mesh)
629 } else {
630 self.create_fixture_geometry(0.3, 0.3, 0.1)
631 }
632 } else {
633 self.create_fixture_geometry(0.3, 0.3, 0.1)
634 };
635
636 let guid = self.generate_guid();
637 let escaped_name = Self::escape_string(name);
638 let fixture_str = format!(
639 "IFCLIGHTFIXTURE('{}',{},'{}','',$,{},{},$,.NOTDEFINED.)",
640 guid, owner_history, escaped_name, placement, geometry
641 );
642 let fixture = self.add_entity(fixture_str);
643
644 let guid2 = self.generate_guid();
646 let rel_str = format!(
647 "IFCRELCONTAINEDINSPATIALSTRUCTURE('{}',{},$,$,({}),{})",
648 guid2, owner_history, fixture, storey
649 );
650 self.add_entity(rel_str);
651
652 if let Some(ft) = fixture_type {
654 let guid3 = self.generate_guid();
655 let rel_type_str = format!(
656 "IFCRELDEFINESBYTYPE('{}',{},$,$,({}),{})",
657 guid3, owner_history, fixture, ft
658 );
659 self.add_entity(rel_type_str);
660 }
661
662 if !self.light_source_ids.is_empty() {
664 let guid4 = self.generate_guid();
665 let sources_list = self
666 .light_source_ids
667 .iter()
668 .map(|id| id.to_string())
669 .collect::<Vec<_>>()
670 .join(",");
671 let rel_group_str = format!(
672 "IFCRELASSIGNSTOGROUP('{}',{},'LightSources','Light sources for fixture',$,({}),{})",
673 guid4, owner_history, sources_list, fixture
674 );
675 self.add_entity(rel_group_str);
676 }
677
678 fixture
679 }
680
681 fn create_mapped_item_geometry(&mut self, rep_map: EntityRef) -> EntityRef {
683 let context_3d = self.entity_map.get("context_3d").copied();
684
685 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
689 let transform = self.add_entity(format!(
690 "IFCCARTESIANTRANSFORMATIONOPERATOR3D($,$,{},1.,$)",
691 origin
692 ));
693
694 let mapped_item = self.add_entity(format!("IFCMAPPEDITEM({},{})", rep_map, transform));
696
697 let context_str = context_3d
699 .map(|c| c.to_string())
700 .unwrap_or_else(|| "$".to_string());
701 let shape_str = format!(
702 "IFCSHAPEREPRESENTATION({},'Body','MappedRepresentation',({}))",
703 context_str, mapped_item
704 );
705 let shape_rep = self.add_entity(shape_str);
706
707 let prod_str = format!("IFCPRODUCTDEFINITIONSHAPE($,$,({}))", shape_rep);
709 self.add_entity(prod_str)
710 }
711
712 pub fn add_light_fixture(
722 &mut self,
723 name: &str,
724 owner_history: EntityRef,
725 storey: EntityRef,
726 fixture_type: Option<EntityRef>,
727 mesh_data: Option<&MeshData>,
728 ) -> EntityRef {
729 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
731 let axis2_placement = self.add_axis2_placement_3d(origin, None, None);
732 let placement = self.add_local_placement(None, axis2_placement);
733
734 let geometry = if let Some(mesh) = mesh_data {
736 if !mesh.vertices.is_empty() && !mesh.triangles.is_empty() {
737 self.create_tessellated_geometry(mesh)
738 } else {
739 self.create_fixture_geometry(0.3, 0.3, 0.1)
741 }
742 } else {
743 self.create_fixture_geometry(0.3, 0.3, 0.1)
745 };
746
747 let guid = self.generate_guid();
748 let escaped_name = Self::escape_string(name);
749 let fixture_str = format!(
750 "IFCLIGHTFIXTURE('{}',{},'{}','',$,{},{},$,.NOTDEFINED.)",
751 guid, owner_history, escaped_name, placement, geometry
752 );
753 let fixture = self.add_entity(fixture_str);
754
755 let guid2 = self.generate_guid();
757 let rel_str = format!(
758 "IFCRELCONTAINEDINSPATIALSTRUCTURE('{}',{},$,$,({}),{})",
759 guid2, owner_history, fixture, storey
760 );
761 self.add_entity(rel_str);
762
763 if let Some(ft) = fixture_type {
765 let guid3 = self.generate_guid();
766 let type_str = format!(
767 "IFCRELDEFINESBYTYPE('{}',{},$,$,({}),{})",
768 guid3, owner_history, fixture, ft
769 );
770 self.add_entity(type_str);
771 }
772
773 if !self.light_source_ids.is_empty() {
775 let source_refs: Vec<String> = self
776 .light_source_ids
777 .iter()
778 .map(|s| s.to_string())
779 .collect();
780 let guid4 = self.generate_guid();
781 let group_str = format!(
782 "IFCRELASSIGNSTOGROUP('{}',{},'LightSources','Light sources for fixture',$,({}),{})",
783 guid4, owner_history, source_refs.join(","), fixture
784 );
785 self.add_entity(group_str);
786 }
787
788 fixture
789 }
790
791 fn add_manufacturer_pset(
793 &mut self,
794 element: EntityRef,
795 owner_history: EntityRef,
796 manufacturer: &str,
797 model_reference: &str,
798 ) {
799 let mfr_str = format!(
800 "IFCPROPERTYSINGLEVALUE('Manufacturer',$,IFCLABEL('{}'),$)",
801 Self::escape_string(manufacturer)
802 );
803 let mfr_value = self.add_entity(mfr_str);
804
805 let model_str = format!(
806 "IFCPROPERTYSINGLEVALUE('ModelReference',$,IFCLABEL('{}'),$)",
807 Self::escape_string(model_reference)
808 );
809 let model_value = self.add_entity(model_str);
810
811 let guid = self.generate_guid();
812 let pset_str = format!(
813 "IFCPROPERTYSET('{}',{},'Pset_ManufacturerTypeInformation',$,({},{}))",
814 guid, owner_history, mfr_value, model_value
815 );
816 let pset = self.add_entity(pset_str);
817
818 let guid2 = self.generate_guid();
819 let rel_str = format!(
820 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
821 guid2, owner_history, element, pset
822 );
823 self.add_entity(rel_str);
824 }
825
826 pub fn add_external_reference(
832 &mut self,
833 location: &str,
834 identification: Option<&str>,
835 name: &str,
836 ) -> EntityRef {
837 let ident = identification
838 .map(|s| format!("'{}'", Self::escape_string(s)))
839 .unwrap_or_else(|| "$".to_string());
840
841 let ref_str = format!(
842 "IFCEXTERNALREFERENCE('{}',{},'{}')",
843 Self::escape_string(location),
844 ident,
845 Self::escape_string(name)
846 );
847 self.add_entity(ref_str)
848 }
849
850 pub fn add_light_source_goniometric(
852 &mut self,
853 name: &str,
854 colour_appearance: Option<(f64, f64, f64)>,
855 colour_temperature: f64,
856 luminous_flux: f64,
857 emission_source: LightEmissionSourceEnum,
858 photometry_file: Option<&str>,
859 ) -> EntityRef {
860 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
862 let axis = self.add_entity("IFCDIRECTION((0.,0.,-1.))".to_string()); let ref_dir = self.add_entity("IFCDIRECTION((1.,0.,0.))".to_string());
864 let position = self.add_axis2_placement_3d(origin, Some(axis), Some(ref_dir));
865
866 let colour_ref = if let Some((r, g, b)) = colour_appearance {
868 let colour_str = format!("IFCCOLOURRGB($,{:.4},{:.4},{:.4})", r, g, b);
869 let colour = self.add_entity(colour_str);
870 OptionalRef::Some(colour)
871 } else {
872 OptionalRef::None
873 };
874
875 let distribution_ref = if let Some(file_path) = photometry_file {
877 let escaped_name = Self::escape_string(name);
878 let ext_str = format!(
879 "IFCEXTERNALREFERENCE('{}',$,'{}')",
880 Self::escape_string(file_path),
881 escaped_name
882 );
883 let ext_ref = self.add_entity(ext_str);
884 OptionalRef::Some(ext_ref)
885 } else {
886 OptionalRef::None
887 };
888
889 let escaped_name = Self::escape_string(name);
891 let ls_str = format!(
892 "IFCLIGHTSOURCEGONIOMETRIC('{}',{},0.,1.,{},{},{:.1},{:.1},{},{})",
893 escaped_name,
894 colour_ref,
895 position,
896 colour_ref,
897 colour_temperature,
898 luminous_flux,
899 emission_source.to_step(),
900 distribution_ref
901 );
902 let light_source = self.add_entity(ls_str);
903
904 self.light_source_ids.push(light_source);
906
907 light_source
908 }
909
910 pub fn add_light_source_goniometric_with_distribution(
915 &mut self,
916 name: &str,
917 colour_appearance: Option<(f64, f64, f64)>,
918 colour_temperature: f64,
919 luminous_flux: f64,
920 emission_source: LightEmissionSourceEnum,
921 distribution_data: &[(f64, f64, f64)],
922 ) -> EntityRef {
923 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
925 let axis = self.add_entity("IFCDIRECTION((0.,0.,-1.))".to_string());
926 let ref_dir = self.add_entity("IFCDIRECTION((1.,0.,0.))".to_string());
927 let position = self.add_axis2_placement_3d(origin, Some(axis), Some(ref_dir));
928
929 let colour_ref = if let Some((r, g, b)) = colour_appearance {
931 let colour_str = format!("IFCCOLOURRGB($,{:.4},{:.4},{:.4})", r, g, b);
932 let colour = self.add_entity(colour_str);
933 OptionalRef::Some(colour)
934 } else {
935 OptionalRef::None
936 };
937
938 let distribution_ref = if !distribution_data.is_empty() {
940 let dist = self.add_light_intensity_distribution("TYPE_C", distribution_data);
941 OptionalRef::Some(dist)
942 } else {
943 OptionalRef::None
944 };
945
946 let escaped_name = Self::escape_string(name);
948 let ls_str = format!(
949 "IFCLIGHTSOURCEGONIOMETRIC('{}',{},0.,1.,{},{},{:.1},{:.1},{},{})",
950 escaped_name,
951 colour_ref,
952 position,
953 colour_ref,
954 colour_temperature,
955 luminous_flux,
956 emission_source.to_step(),
957 distribution_ref
958 );
959 let light_source = self.add_entity(ls_str);
960
961 self.light_source_ids.push(light_source);
963
964 light_source
965 }
966
967 pub fn add_light_source_goniometric_with_distribution_ref(
971 &mut self,
972 name: &str,
973 colour_appearance: Option<(f64, f64, f64)>,
974 colour_temperature: f64,
975 luminous_flux: f64,
976 emission_source: LightEmissionSourceEnum,
977 distribution_ref: EntityRef,
978 ) -> EntityRef {
979 let origin = self.add_entity("IFCCARTESIANPOINT((0.,0.,0.))".to_string());
981 let axis = self.add_entity("IFCDIRECTION((0.,0.,-1.))".to_string());
982 let ref_dir = self.add_entity("IFCDIRECTION((1.,0.,0.))".to_string());
983 let position = self.add_axis2_placement_3d(origin, Some(axis), Some(ref_dir));
984
985 let colour_ref = if let Some((r, g, b)) = colour_appearance {
987 let colour_str = format!("IFCCOLOURRGB($,{:.4},{:.4},{:.4})", r, g, b);
988 let colour = self.add_entity(colour_str);
989 OptionalRef::Some(colour)
990 } else {
991 OptionalRef::None
992 };
993
994 let escaped_name = Self::escape_string(name);
996 let ls_str = format!(
997 "IFCLIGHTSOURCEGONIOMETRIC('{}',{},0.,1.,{},{},{:.1},{:.1},{},{})",
998 escaped_name,
999 colour_ref,
1000 position,
1001 colour_ref,
1002 colour_temperature,
1003 luminous_flux,
1004 emission_source.to_step(),
1005 distribution_ref
1006 );
1007 let light_source = self.add_entity(ls_str);
1008
1009 self.light_source_ids.push(light_source);
1011
1012 light_source
1013 }
1014
1015 pub fn add_light_intensity_distribution(
1020 &mut self,
1021 distribution_type: &str,
1022 distribution_data: &[(f64, f64, f64)],
1023 ) -> EntityRef {
1024 let mut grouped: std::collections::BTreeMap<i64, Vec<(f64, f64)>> =
1027 std::collections::BTreeMap::new();
1028 for &(main, secondary, intensity) in distribution_data {
1029 let key = (main * 100.0) as i64;
1031 grouped.entry(key).or_default().push((secondary, intensity));
1032 }
1033
1034 let mut data_entries = Vec::new();
1036 for (main_key, values) in grouped {
1037 let main_angle = main_key as f64 / 100.0;
1038
1039 let mut sorted_values = values;
1041 sorted_values
1042 .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1043
1044 let secondary_list: Vec<String> = sorted_values
1046 .iter()
1047 .map(|(s, _)| format!("{:.2}", s))
1048 .collect();
1049 let intensity_list: Vec<String> = sorted_values
1050 .iter()
1051 .map(|(_, i)| format!("{:.4}", i))
1052 .collect();
1053
1054 let entry_str = format!(
1055 "IFCLIGHTDISTRIBUTIONDATA({:.2},({}),({}))",
1056 main_angle,
1057 secondary_list.join(","),
1058 intensity_list.join(",")
1059 );
1060 let entry = self.add_entity(entry_str);
1061 data_entries.push(entry.to_string());
1062 }
1063
1064 let data_list = if data_entries.is_empty() {
1065 "$".to_string()
1066 } else {
1067 format!("({})", data_entries.join(","))
1068 };
1069
1070 let dist_str = format!(
1071 "IFCLIGHTINTENSITYDISTRIBUTION(.{}.,{})",
1072 distribution_type, data_list
1073 );
1074 self.add_entity(dist_str)
1075 }
1076
1077 pub fn add_light_fixture_common_pset(
1083 &mut self,
1084 element: EntityRef,
1085 owner_history: EntityRef,
1086 number_of_sources: Option<i32>,
1087 total_wattage: Option<f64>,
1088 light_fixture_mounting_type: Option<&str>,
1089 light_fixture_placement_type: Option<&str>,
1090 ) -> EntityRef {
1091 let mut properties = Vec::new();
1092
1093 if let Some(n) = number_of_sources {
1094 let prop_str = format!(
1095 "IFCPROPERTYSINGLEVALUE('NumberOfSources',$,IFCINTEGER({}),$)",
1096 n
1097 );
1098 let prop = self.add_entity(prop_str);
1099 properties.push(prop);
1100 }
1101
1102 if let Some(w) = total_wattage {
1103 let prop_str = format!(
1104 "IFCPROPERTYSINGLEVALUE('TotalWattage',$,IFCPOWERMEASURE({:.2}),$)",
1105 w
1106 );
1107 let prop = self.add_entity(prop_str);
1108 properties.push(prop);
1109 }
1110
1111 if let Some(mt) = light_fixture_mounting_type {
1112 let prop_str = format!(
1113 "IFCPROPERTYSINGLEVALUE('LightFixtureMountingType',$,IFCLABEL('{}'),$)",
1114 Self::escape_string(mt)
1115 );
1116 let prop = self.add_entity(prop_str);
1117 properties.push(prop);
1118 }
1119
1120 if let Some(pt) = light_fixture_placement_type {
1121 let prop_str = format!(
1122 "IFCPROPERTYSINGLEVALUE('LightFixturePlacementType',$,IFCLABEL('{}'),$)",
1123 Self::escape_string(pt)
1124 );
1125 let prop = self.add_entity(prop_str);
1126 properties.push(prop);
1127 }
1128
1129 if properties.is_empty() {
1130 return EntityRef::new(0);
1131 }
1132
1133 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
1134
1135 let guid = self.generate_guid();
1136 let pset_str = format!(
1137 "IFCPROPERTYSET('{}',{},'Pset_LightFixtureTypeCommon',$,({}))",
1138 guid,
1139 owner_history,
1140 prop_list.join(",")
1141 );
1142 let pset = self.add_entity(pset_str);
1143
1144 let guid2 = self.generate_guid();
1145 let rel_str = format!(
1146 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
1147 guid2, owner_history, element, pset
1148 );
1149 self.add_entity(rel_str);
1150
1151 pset
1152 }
1153
1154 pub fn add_electrical_pset(
1156 &mut self,
1157 element: EntityRef,
1158 owner_history: EntityRef,
1159 rated_voltage: Option<f64>,
1160 rated_current: Option<f64>,
1161 power_factor: Option<f64>,
1162 ip_code: Option<&str>,
1163 ) -> EntityRef {
1164 let mut properties = Vec::new();
1165
1166 if let Some(v) = rated_voltage {
1167 let prop_str = format!(
1168 "IFCPROPERTYSINGLEVALUE('RatedVoltage',$,IFCELECTRICVOLTAGEMEASURE({:.1}),$)",
1169 v
1170 );
1171 let prop = self.add_entity(prop_str);
1172 properties.push(prop);
1173 }
1174
1175 if let Some(i) = rated_current {
1176 let prop_str = format!(
1177 "IFCPROPERTYSINGLEVALUE('RatedCurrent',$,IFCELECTRICCURRENTMEASURE({:.3}),$)",
1178 i
1179 );
1180 let prop = self.add_entity(prop_str);
1181 properties.push(prop);
1182 }
1183
1184 if let Some(pf) = power_factor {
1185 let prop_str = format!(
1186 "IFCPROPERTYSINGLEVALUE('PowerFactor',$,IFCNORMALISEDRATIOMEASURE({:.3}),$)",
1187 pf
1188 );
1189 let prop = self.add_entity(prop_str);
1190 properties.push(prop);
1191 }
1192
1193 if let Some(ip) = ip_code {
1194 let normalized_ip = ip.trim_start_matches("IP").trim_start_matches("ip");
1196 let prop_str = format!(
1197 "IFCPROPERTYSINGLEVALUE('IPCode',$,IFCLABEL('IP{}'),$)",
1198 Self::escape_string(normalized_ip)
1199 );
1200 let prop = self.add_entity(prop_str);
1201 properties.push(prop);
1202 }
1203
1204 if properties.is_empty() {
1205 return EntityRef::new(0);
1206 }
1207
1208 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
1209
1210 let guid = self.generate_guid();
1211 let pset_str = format!(
1212 "IFCPROPERTYSET('{}',{},'Pset_ElectricalDeviceCommon',$,({}))",
1213 guid,
1214 owner_history,
1215 prop_list.join(",")
1216 );
1217 let pset = self.add_entity(pset_str);
1218
1219 let guid2 = self.generate_guid();
1220 let rel_str = format!(
1221 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
1222 guid2, owner_history, element, pset
1223 );
1224 self.add_entity(rel_str);
1225
1226 pset
1227 }
1228
1229 pub fn add_electrical_device_pset(
1234 &mut self,
1235 element: EntityRef,
1236 owner_history: EntityRef,
1237 props: &ElectricalDeviceProperties,
1238 ) -> EntityRef {
1239 let mut properties = Vec::new();
1240
1241 if let Some(current) = props.rated_current {
1243 let prop = self.add_entity(format!(
1244 "IFCPROPERTYSINGLEVALUE('RatedCurrent',$,IFCELECTRICCURRENTMEASURE({:.3}),$)",
1245 current
1246 ));
1247 properties.push(prop);
1248 }
1249
1250 if let Some(voltage) = props.rated_voltage {
1252 if let Some(voltage_max) = props.rated_voltage_max {
1253 let prop = self.add_entity(format!(
1255 "IFCPROPERTYBOUNDEDVALUE('RatedVoltage',$,IFCELECTRICVOLTAGEMEASURE({:.1}),IFCELECTRICVOLTAGEMEASURE({:.1}),$,$)",
1256 voltage, voltage_max
1257 ));
1258 properties.push(prop);
1259 } else {
1260 let prop = self.add_entity(format!(
1261 "IFCPROPERTYSINGLEVALUE('RatedVoltage',$,IFCELECTRICVOLTAGEMEASURE({:.1}),$)",
1262 voltage
1263 ));
1264 properties.push(prop);
1265 }
1266 }
1267
1268 if let (Some(freq_min), Some(freq_max)) =
1270 (props.nominal_frequency_min, props.nominal_frequency_max)
1271 {
1272 let prop = self.add_entity(format!(
1273 "IFCPROPERTYBOUNDEDVALUE('NominalFrequencyRange',$,IFCFREQUENCYMEASURE({:.1}),IFCFREQUENCYMEASURE({:.1}),$,$)",
1274 freq_min, freq_max
1275 ));
1276 properties.push(prop);
1277 }
1278
1279 if let Some(pf) = props.power_factor {
1281 let prop = self.add_entity(format!(
1282 "IFCPROPERTYSINGLEVALUE('PowerFactor',$,IFCNORMALISEDRATIOMEASURE({:.3}),$)",
1283 pf.clamp(0.0, 1.0)
1284 ));
1285 properties.push(prop);
1286 }
1287
1288 if let Some(ref cf) = props.conductor_function {
1290 let prop = self.add_entity(format!(
1291 "IFCPROPERTYENUMERATEDVALUE('ConductorFunction',$,(IFCLABEL('{}')),{})",
1292 cf.to_step().trim_matches('.'),
1293 "$" ));
1295 properties.push(prop);
1296 }
1297
1298 if let Some(poles) = props.number_of_poles {
1300 let prop = self.add_entity(format!(
1301 "IFCPROPERTYSINGLEVALUE('NumberOfPoles',$,IFCCOUNTMEASURE({}),$)",
1302 poles
1303 ));
1304 properties.push(prop);
1305 }
1306
1307 if let Some(has_pe) = props.has_protective_earth {
1309 let prop = self.add_entity(format!(
1310 "IFCPROPERTYSINGLEVALUE('HasProtectiveEarth',$,IFCBOOLEAN({}),$)",
1311 if has_pe { ".T." } else { ".F." }
1312 ));
1313 properties.push(prop);
1314 }
1315
1316 if let Some(ref isc) = props.insulation_standard_class {
1318 if *isc != InsulationStandardClass::NotDefined {
1319 let prop = self.add_entity(format!(
1320 "IFCPROPERTYENUMERATEDVALUE('InsulationStandardClass',$,(IFCLABEL('{}')),{})",
1321 isc.to_step().trim_matches('.'),
1322 "$"
1323 ));
1324 properties.push(prop);
1325 }
1326 }
1327
1328 if let Some(ref ip) = props.ip_code {
1330 let normalized_ip = if ip.to_uppercase().starts_with("IP") {
1332 ip.clone()
1333 } else {
1334 format!("IP{}", ip)
1335 };
1336 let prop = self.add_entity(format!(
1337 "IFCPROPERTYSINGLEVALUE('IP_Code',$,IFCLABEL('{}'),$)",
1338 Self::escape_string(&normalized_ip)
1339 ));
1340 properties.push(prop);
1341 }
1342
1343 if let Some(ref ik) = props.ik_code {
1345 let normalized_ik = if ik.to_uppercase().starts_with("IK") {
1347 ik.clone()
1348 } else {
1349 format!("IK{}", ik)
1350 };
1351 let prop = self.add_entity(format!(
1352 "IFCPROPERTYSINGLEVALUE('IK_Code',$,IFCLABEL('{}'),$)",
1353 Self::escape_string(&normalized_ik)
1354 ));
1355 properties.push(prop);
1356 }
1357
1358 if let Some(ref es) = props.earthing_style {
1360 let prop = self.add_entity(format!(
1361 "IFCPROPERTYSINGLEVALUE('EarthingStyle',$,IFCLABEL('{}'),$)",
1362 Self::escape_string(es)
1363 ));
1364 properties.push(prop);
1365 }
1366
1367 if let Some(heat) = props.heat_dissipation {
1369 let prop = self.add_entity(format!(
1370 "IFCPROPERTYSINGLEVALUE('HeatDissipation',$,IFCPOWERMEASURE({:.2}),$)",
1371 heat
1372 ));
1373 properties.push(prop);
1374 }
1375
1376 if let Some(power) = props.power {
1378 let prop = self.add_entity(format!(
1379 "IFCPROPERTYSINGLEVALUE('Power',$,IFCPOWERMEASURE({:.2}),$)",
1380 power
1381 ));
1382 properties.push(prop);
1383 }
1384
1385 if let Some(npc) = props.nominal_power_consumption {
1387 let prop = self.add_entity(format!(
1388 "IFCPROPERTYSINGLEVALUE('NominalPowerConsumption',$,IFCPOWERMEASURE({:.2}),$)",
1389 npc
1390 ));
1391 properties.push(prop);
1392 }
1393
1394 if let Some(ports) = props.number_of_power_supply_ports {
1396 let prop = self.add_entity(format!(
1397 "IFCPROPERTYSINGLEVALUE('NumberOfPowerSupplyPorts',$,IFCINTEGER({}),$)",
1398 ports
1399 ));
1400 properties.push(prop);
1401 }
1402
1403 if properties.is_empty() {
1404 return EntityRef::new(0);
1405 }
1406
1407 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
1408
1409 let guid = self.generate_guid();
1410 let pset = self.add_entity(format!(
1411 "IFCPROPERTYSET('{}',{},'Pset_ElectricalDeviceCommon',$,({}))",
1412 guid,
1413 owner_history,
1414 prop_list.join(",")
1415 ));
1416
1417 let guid2 = self.generate_guid();
1418 self.add_entity(format!(
1419 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
1420 guid2, owner_history, element, pset
1421 ));
1422
1423 pset
1424 }
1425
1426 pub fn add_light_fixture_common_pset_full(
1430 &mut self,
1431 element: EntityRef,
1432 owner_history: EntityRef,
1433 props: &LightFixtureCommonProperties,
1434 ) -> EntityRef {
1435 let mut properties = Vec::new();
1436
1437 if let Some(ref reference) = props.reference {
1439 let prop = self.add_entity(format!(
1440 "IFCPROPERTYSINGLEVALUE('Reference',$,IFCIDENTIFIER('{}'),$)",
1441 Self::escape_string(reference)
1442 ));
1443 properties.push(prop);
1444 }
1445
1446 if let Some(ref status) = props.status {
1448 let prop = self.add_entity(format!(
1449 "IFCPROPERTYENUMERATEDVALUE('Status',$,(IFCLABEL('{}')),$)",
1450 Self::escape_string(status)
1451 ));
1452 properties.push(prop);
1453 }
1454
1455 if let Some(num) = props.number_of_sources {
1457 let prop = self.add_entity(format!(
1458 "IFCPROPERTYSINGLEVALUE('NumberOfSources',$,IFCINTEGER({}),$)",
1459 num
1460 ));
1461 properties.push(prop);
1462 }
1463
1464 if let Some(wattage) = props.total_wattage {
1466 let prop = self.add_entity(format!(
1467 "IFCPROPERTYSINGLEVALUE('TotalWattage',$,IFCPOWERMEASURE({:.2}),$)",
1468 wattage
1469 ));
1470 properties.push(prop);
1471 }
1472
1473 if let Some(ref mounting) = props.mounting_type {
1475 let prop = self.add_entity(format!(
1476 "IFCPROPERTYENUMERATEDVALUE('LightFixtureMountingType',$,(IFCLABEL('{}')),$)",
1477 Self::escape_string(mounting)
1478 ));
1479 properties.push(prop);
1480 }
1481
1482 if let Some(ref placing) = props.placing_type {
1484 let prop = self.add_entity(format!(
1485 "IFCPROPERTYENUMERATEDVALUE('LightFixturePlacingType',$,(IFCLABEL('{}')),$)",
1486 Self::escape_string(placing)
1487 ));
1488 properties.push(prop);
1489 }
1490
1491 if let Some(mf) = props.maintenance_factor {
1493 let prop = self.add_entity(format!(
1494 "IFCPROPERTYSINGLEVALUE('MaintenanceFactor',$,IFCREAL({:.3}),$)",
1495 mf
1496 ));
1497 properties.push(prop);
1498 }
1499
1500 if let Some(load) = props.max_plenum_sensible_load {
1502 let prop = self.add_entity(format!(
1503 "IFCPROPERTYSINGLEVALUE('MaximumPlenumSensibleLoad',$,IFCPOWERMEASURE({:.2}),$)",
1504 load
1505 ));
1506 properties.push(prop);
1507 }
1508
1509 if let Some(load) = props.max_space_sensible_load {
1511 let prop = self.add_entity(format!(
1512 "IFCPROPERTYSINGLEVALUE('MaximumSpaceSensibleLoad',$,IFCPOWERMEASURE({:.2}),$)",
1513 load
1514 ));
1515 properties.push(prop);
1516 }
1517
1518 if let Some(ratio) = props.sensible_load_to_radiant {
1520 let prop = self.add_entity(format!(
1521 "IFCPROPERTYSINGLEVALUE('SensibleLoadToRadiant',$,IFCPOSITIVERATIOMEASURE({:.3}),$)",
1522 ratio
1523 ));
1524 properties.push(prop);
1525 }
1526
1527 if properties.is_empty() {
1528 return EntityRef::new(0);
1529 }
1530
1531 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
1532
1533 let guid = self.generate_guid();
1534 let pset = self.add_entity(format!(
1535 "IFCPROPERTYSET('{}',{},'Pset_LightFixtureTypeCommon',$,({}))",
1536 guid,
1537 owner_history,
1538 prop_list.join(",")
1539 ));
1540
1541 let guid2 = self.generate_guid();
1542 self.add_entity(format!(
1543 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
1544 guid2, owner_history, element, pset
1545 ));
1546
1547 pset
1548 }
1549
1550 pub fn add_manufacturer_info_pset(
1552 &mut self,
1553 element: EntityRef,
1554 owner_history: EntityRef,
1555 info: &ManufacturerTypeInfo,
1556 ) -> EntityRef {
1557 let mut properties = Vec::new();
1558
1559 if let Some(ref gtin) = info.global_trade_item_number {
1560 let prop = self.add_entity(format!(
1561 "IFCPROPERTYSINGLEVALUE('GlobalTradeItemNumber',$,IFCIDENTIFIER('{}'),$)",
1562 Self::escape_string(gtin)
1563 ));
1564 properties.push(prop);
1565 }
1566
1567 if let Some(ref article) = info.article_number {
1568 let prop = self.add_entity(format!(
1569 "IFCPROPERTYSINGLEVALUE('ArticleNumber',$,IFCIDENTIFIER('{}'),$)",
1570 Self::escape_string(article)
1571 ));
1572 properties.push(prop);
1573 }
1574
1575 if let Some(ref model_ref) = info.model_reference {
1576 let prop = self.add_entity(format!(
1577 "IFCPROPERTYSINGLEVALUE('ModelReference',$,IFCLABEL('{}'),$)",
1578 Self::escape_string(model_ref)
1579 ));
1580 properties.push(prop);
1581 }
1582
1583 if let Some(ref model_label) = info.model_label {
1584 let prop = self.add_entity(format!(
1585 "IFCPROPERTYSINGLEVALUE('ModelLabel',$,IFCLABEL('{}'),$)",
1586 Self::escape_string(model_label)
1587 ));
1588 properties.push(prop);
1589 }
1590
1591 if let Some(ref mfr) = info.manufacturer {
1592 let prop = self.add_entity(format!(
1593 "IFCPROPERTYSINGLEVALUE('Manufacturer',$,IFCLABEL('{}'),$)",
1594 Self::escape_string(mfr)
1595 ));
1596 properties.push(prop);
1597 }
1598
1599 if let Some(year) = info.production_year {
1600 let prop = self.add_entity(format!(
1601 "IFCPROPERTYSINGLEVALUE('ProductionYear',$,IFCINTEGER({}),$)",
1602 year
1603 ));
1604 properties.push(prop);
1605 }
1606
1607 if let Some(ref place) = info.assembly_place {
1608 let prop = self.add_entity(format!(
1609 "IFCPROPERTYSINGLEVALUE('AssemblyPlace',$,IFCLABEL('{}'),$)",
1610 Self::escape_string(place)
1611 ));
1612 properties.push(prop);
1613 }
1614
1615 if properties.is_empty() {
1616 return EntityRef::new(0);
1617 }
1618
1619 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
1620
1621 let guid = self.generate_guid();
1622 let pset = self.add_entity(format!(
1623 "IFCPROPERTYSET('{}',{},'Pset_ManufacturerTypeInformation',$,({}))",
1624 guid,
1625 owner_history,
1626 prop_list.join(",")
1627 ));
1628
1629 let guid2 = self.generate_guid();
1630 self.add_entity(format!(
1631 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
1632 guid2, owner_history, element, pset
1633 ));
1634
1635 pset
1636 }
1637
1638 pub fn add_warranty_pset(
1640 &mut self,
1641 element: EntityRef,
1642 owner_history: EntityRef,
1643 warranty: &WarrantyInfo,
1644 ) -> EntityRef {
1645 let mut properties = Vec::new();
1646
1647 if let Some(ref id) = warranty.warranty_identifier {
1648 let prop = self.add_entity(format!(
1649 "IFCPROPERTYSINGLEVALUE('WarrantyIdentifier',$,IFCIDENTIFIER('{}'),$)",
1650 Self::escape_string(id)
1651 ));
1652 properties.push(prop);
1653 }
1654
1655 if let Some(ref start) = warranty.warranty_start_date {
1656 let prop = self.add_entity(format!(
1657 "IFCPROPERTYSINGLEVALUE('WarrantyStartDate',$,IFCDATE('{}'),$)",
1658 Self::escape_string(start)
1659 ));
1660 properties.push(prop);
1661 }
1662
1663 if let Some(ref end) = warranty.warranty_end_date {
1664 let prop = self.add_entity(format!(
1665 "IFCPROPERTYSINGLEVALUE('WarrantyEndDate',$,IFCDATE('{}'),$)",
1666 Self::escape_string(end)
1667 ));
1668 properties.push(prop);
1669 }
1670
1671 if let Some(period) = warranty.warranty_period {
1672 let prop = self.add_entity(format!(
1673 "IFCPROPERTYSINGLEVALUE('WarrantyPeriod',$,IFCREAL({:.1}),$)",
1674 period
1675 ));
1676 properties.push(prop);
1677 }
1678
1679 if let Some(ref contact) = warranty.point_of_contact {
1680 let prop = self.add_entity(format!(
1681 "IFCPROPERTYSINGLEVALUE('PointOfContact',$,IFCLABEL('{}'),$)",
1682 Self::escape_string(contact)
1683 ));
1684 properties.push(prop);
1685 }
1686
1687 if let Some(ref terms) = warranty.terms_and_conditions {
1688 let prop = self.add_entity(format!(
1689 "IFCPROPERTYSINGLEVALUE('TermsAndConditions',$,IFCTEXT('{}'),$)",
1690 Self::escape_string(terms)
1691 ));
1692 properties.push(prop);
1693 }
1694
1695 if let Some(ref exclusions) = warranty.exclusions {
1696 let prop = self.add_entity(format!(
1697 "IFCPROPERTYSINGLEVALUE('Exclusions',$,IFCTEXT('{}'),$)",
1698 Self::escape_string(exclusions)
1699 ));
1700 properties.push(prop);
1701 }
1702
1703 if properties.is_empty() {
1704 return EntityRef::new(0);
1705 }
1706
1707 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
1708
1709 let guid = self.generate_guid();
1710 let pset = self.add_entity(format!(
1711 "IFCPROPERTYSET('{}',{},'Pset_Warranty',$,({}))",
1712 guid,
1713 owner_history,
1714 prop_list.join(",")
1715 ));
1716
1717 let guid2 = self.generate_guid();
1718 self.add_entity(format!(
1719 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
1720 guid2, owner_history, element, pset
1721 ));
1722
1723 pset
1724 }
1725
1726 pub fn add_service_life_pset(
1728 &mut self,
1729 element: EntityRef,
1730 owner_history: EntityRef,
1731 service_life: &ServiceLifeInfo,
1732 ) -> EntityRef {
1733 let mut properties = Vec::new();
1734
1735 if let Some(duration) = service_life.service_life_duration {
1736 let prop = self.add_entity(format!(
1737 "IFCPROPERTYSINGLEVALUE('ServiceLifeDuration',$,IFCREAL({:.1}),$)",
1738 duration
1739 ));
1740 properties.push(prop);
1741 }
1742
1743 if let Some(ref sl_type) = service_life.service_life_type {
1744 let prop = self.add_entity(format!(
1745 "IFCPROPERTYENUMERATEDVALUE('ServiceLifeType',$,(IFCLABEL('{}')),$)",
1746 Self::escape_string(sl_type)
1747 ));
1748 properties.push(prop);
1749 }
1750
1751 if let Some(mtbf) = service_life.mean_time_between_failure {
1752 let prop = self.add_entity(format!(
1753 "IFCPROPERTYSINGLEVALUE('MeanTimeBetweenFailure',$,IFCREAL({:.0}),$)",
1754 mtbf
1755 ));
1756 properties.push(prop);
1757 }
1758
1759 if properties.is_empty() {
1760 return EntityRef::new(0);
1761 }
1762
1763 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
1764
1765 let guid = self.generate_guid();
1766 let pset = self.add_entity(format!(
1767 "IFCPROPERTYSET('{}',{},'Pset_ServiceLife',$,({}))",
1768 guid,
1769 owner_history,
1770 prop_list.join(",")
1771 ));
1772
1773 let guid2 = self.generate_guid();
1774 self.add_entity(format!(
1775 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
1776 guid2, owner_history, element, pset
1777 ));
1778
1779 pset
1780 }
1781
1782 pub fn add_distribution_port(
1787 &mut self,
1788 name: &str,
1789 owner_history: EntityRef,
1790 parent_element: EntityRef,
1791 flow_direction: FlowDirectionEnum,
1792 system_type: DistributionSystemEnum,
1793 ) -> EntityRef {
1794 let guid = self.generate_guid();
1795
1796 let port = self.add_entity(format!(
1798 "IFCDISTRIBUTIONPORT('{}',{},'{}','Electrical connection port',$,$,{},{})",
1799 guid,
1800 owner_history,
1801 Self::escape_string(name),
1802 flow_direction.to_step(),
1803 system_type.to_step()
1804 ));
1805
1806 let guid2 = self.generate_guid();
1808 self.add_entity(format!(
1809 "IFCRELNESTS('{}',{},$,$,{},({}))",
1810 guid2, owner_history, parent_element, port
1811 ));
1812
1813 port
1814 }
1815
1816 pub fn add_lighting_system(
1820 &mut self,
1821 name: &str,
1822 owner_history: EntityRef,
1823 building: EntityRef,
1824 ) -> EntityRef {
1825 let guid = self.generate_guid();
1826
1827 let system = self.add_entity(format!(
1828 "IFCDISTRIBUTIONSYSTEM('{}',{},'{}','Lighting distribution system',$,.LIGHTING.)",
1829 guid,
1830 owner_history,
1831 Self::escape_string(name)
1832 ));
1833
1834 let guid2 = self.generate_guid();
1836 self.add_entity(format!(
1837 "IFCRELSERVICESBUILDINGS('{}',{},$,$,{},({}))",
1838 guid2, owner_history, system, building
1839 ));
1840
1841 system
1842 }
1843
1844 pub fn assign_to_system(
1846 &mut self,
1847 owner_history: EntityRef,
1848 system: EntityRef,
1849 elements: &[EntityRef],
1850 ) {
1851 if elements.is_empty() {
1852 return;
1853 }
1854
1855 let guid = self.generate_guid();
1856 let element_list: Vec<String> = elements.iter().map(|e| e.to_string()).collect();
1857
1858 self.add_entity(format!(
1859 "IFCRELASSIGNSTOGROUP('{}',{},$,$,({}),$.PRODUCT.,{})",
1860 guid,
1861 owner_history,
1862 element_list.join(","),
1863 system
1864 ));
1865 }
1866
1867 #[allow(clippy::too_many_arguments)]
1869 pub fn add_variant_pset(
1870 &mut self,
1871 element: EntityRef,
1872 owner_history: EntityRef,
1873 variant_id: &str,
1874 cct: Option<i32>,
1875 luminous_flux: Option<f64>,
1876 power: Option<f64>,
1877 cri: Option<i32>,
1878 weight: Option<f64>,
1879 ) -> EntityRef {
1880 let mut properties = Vec::new();
1881
1882 let variant_prop_str = format!(
1884 "IFCPROPERTYSINGLEVALUE('GLDF_VariantId',$,IFCLABEL('{}'),$)",
1885 Self::escape_string(variant_id)
1886 );
1887 let variant_prop = self.add_entity(variant_prop_str);
1888 properties.push(variant_prop);
1889
1890 if let Some(temp) = cct {
1892 let prop_str = format!(
1893 "IFCPROPERTYSINGLEVALUE('ColorTemperature',$,IFCTHERMODYNAMICTEMPERATUREMEASURE({:.0}),$)",
1894 temp as f64
1895 );
1896 let prop = self.add_entity(prop_str);
1897 properties.push(prop);
1898 }
1899
1900 if let Some(flux) = luminous_flux {
1902 let prop_str = format!(
1903 "IFCPROPERTYSINGLEVALUE('LuminousFlux',$,IFCLUMINOUSFLUXMEASURE({:.1}),$)",
1904 flux
1905 );
1906 let prop = self.add_entity(prop_str);
1907 properties.push(prop);
1908 }
1909
1910 if let Some(p) = power {
1912 let prop_str = format!(
1913 "IFCPROPERTYSINGLEVALUE('Power',$,IFCPOWERMEASURE({:.2}),$)",
1914 p
1915 );
1916 let prop = self.add_entity(prop_str);
1917 properties.push(prop);
1918 }
1919
1920 if let (Some(flux), Some(p)) = (luminous_flux, power) {
1922 if p > 0.0 {
1923 let efficacy = flux / p;
1924 let prop_str = format!(
1925 "IFCPROPERTYSINGLEVALUE('Efficacy',$,IFCREAL({:.1}),$)",
1926 efficacy
1927 );
1928 let prop = self.add_entity(prop_str);
1929 properties.push(prop);
1930 }
1931 }
1932
1933 if let Some(index) = cri {
1935 let prop_str = format!(
1936 "IFCPROPERTYSINGLEVALUE('ColorRenderingIndex',$,IFCINTEGER({}),$)",
1937 index
1938 );
1939 let prop = self.add_entity(prop_str);
1940 properties.push(prop);
1941 }
1942
1943 if let Some(w) = weight {
1945 let prop_str = format!(
1946 "IFCPROPERTYSINGLEVALUE('Weight',$,IFCMASSMEASURE({:.3}),$)",
1947 w
1948 );
1949 let prop = self.add_entity(prop_str);
1950 properties.push(prop);
1951 }
1952
1953 if properties.is_empty() {
1954 return EntityRef::new(0);
1955 }
1956
1957 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
1958
1959 let guid = self.generate_guid();
1960 let pset_str = format!(
1961 "IFCPROPERTYSET('{}',{},'Pset_LuminaireVariant',$,({}))",
1962 guid,
1963 owner_history,
1964 prop_list.join(",")
1965 );
1966 let pset = self.add_entity(pset_str);
1967
1968 let guid2 = self.generate_guid();
1969 let rel_str = format!(
1970 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
1971 guid2, owner_history, element, pset
1972 );
1973 self.add_entity(rel_str);
1974
1975 pset
1976 }
1977
1978 pub fn clear_light_sources(&mut self) {
1980 self.light_source_ids.clear();
1981 }
1982
1983 pub fn add_photometry_filenames_pset(
1987 &mut self,
1988 element: EntityRef,
1989 owner_history: EntityRef,
1990 filenames: &[String],
1991 ) -> EntityRef {
1992 let mut properties = Vec::new();
1993
1994 for (i, filename) in filenames.iter().enumerate() {
1996 let prop_str = format!(
1997 "IFCPROPERTYSINGLEVALUE('PhotometryFile_{}',$,IFCLABEL('{}'),$)",
1998 i + 1,
1999 Self::escape_string(filename)
2000 );
2001 let prop = self.add_entity(prop_str);
2002 properties.push(prop);
2003 }
2004
2005 if properties.is_empty() {
2006 return EntityRef::new(0);
2007 }
2008
2009 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
2010
2011 let guid = self.generate_guid();
2012 let pset_str = format!(
2013 "IFCPROPERTYSET('{}',{},'Pset_GLDF_PhotometryFiles',$,({}))",
2014 guid,
2015 owner_history,
2016 prop_list.join(",")
2017 );
2018 let pset = self.add_entity(pset_str);
2019
2020 let guid2 = self.generate_guid();
2021 let rel_str = format!(
2022 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
2023 guid2, owner_history, element, pset
2024 );
2025 self.add_entity(rel_str);
2026
2027 pset
2028 }
2029
2030 pub fn add_gldf_descriptive_pset(
2038 &mut self,
2039 element: EntityRef,
2040 owner_history: EntityRef,
2041 safety_class: Option<&str>,
2042 median_useful_life: Option<&str>,
2043 mounting_type: Option<&str>,
2044 recessed_depth: Option<f64>,
2045 ) -> EntityRef {
2046 let mut properties = Vec::new();
2047
2048 if let Some(sc) = safety_class {
2049 let prop = self.add_entity(format!(
2050 "IFCPROPERTYSINGLEVALUE('ElectricalSafetyClass',$,IFCLABEL('{}'),$)",
2051 Self::escape_string(sc)
2052 ));
2053 properties.push(prop);
2054 }
2055
2056 if let Some(mul) = median_useful_life {
2057 let prop = self.add_entity(format!(
2058 "IFCPROPERTYSINGLEVALUE('MedianUsefulLife',$,IFCLABEL('{}'),$)",
2059 Self::escape_string(mul)
2060 ));
2061 properties.push(prop);
2062 }
2063
2064 if let Some(mt) = mounting_type {
2065 let prop = self.add_entity(format!(
2066 "IFCPROPERTYSINGLEVALUE('GLDF_MountingType',$,IFCLABEL('{}'),$)",
2067 Self::escape_string(mt)
2068 ));
2069 properties.push(prop);
2070 }
2071
2072 if let Some(rd) = recessed_depth {
2073 let prop = self.add_entity(format!(
2074 "IFCPROPERTYSINGLEVALUE('GLDF_RecessedDepth',$,IFCLENGTHMEASURE({:.1}),$)",
2075 rd
2076 ));
2077 properties.push(prop);
2078 }
2079
2080 if properties.is_empty() {
2081 return EntityRef::new(0);
2082 }
2083
2084 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
2085
2086 let guid = self.generate_guid();
2087 let pset_str = format!(
2088 "IFCPROPERTYSET('{}',{},'Pset_GLDF_DescriptiveAttributes',$,({}))",
2089 guid,
2090 owner_history,
2091 prop_list.join(",")
2092 );
2093 let pset = self.add_entity(pset_str);
2094
2095 let guid2 = self.generate_guid();
2096 let rel_str = format!(
2097 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
2098 guid2, owner_history, element, pset
2099 );
2100 self.add_entity(rel_str);
2101
2102 pset
2103 }
2104
2105 pub fn add_ldt_raw_content_pset(
2109 &mut self,
2110 element: EntityRef,
2111 owner_history: EntityRef,
2112 index: usize,
2113 filename: &str,
2114 raw_content: &str,
2115 ) -> EntityRef {
2116 use base64::{engine::general_purpose::STANDARD, Engine};
2117
2118 let encoded = STANDARD.encode(raw_content.as_bytes());
2119
2120 let filename_prop = self.add_entity(format!(
2121 "IFCPROPERTYSINGLEVALUE('LDT_{}_Filename',$,IFCLABEL('{}'),$)",
2122 index,
2123 Self::escape_string(filename)
2124 ));
2125
2126 let content_prop = self.add_entity(format!(
2127 "IFCPROPERTYSINGLEVALUE('LDT_{}_Content',$,IFCLABEL('{}'),$)",
2128 index,
2129 Self::escape_string(&encoded)
2130 ));
2131
2132 let guid = self.generate_guid();
2133 let pset_str = format!(
2134 "IFCPROPERTYSET('{}',{},'Pset_GLDF_LDTRawContent',$,({},{}))",
2135 guid, owner_history, filename_prop, content_prop
2136 );
2137 let pset = self.add_entity(pset_str);
2138
2139 let guid2 = self.generate_guid();
2140 let rel_str = format!(
2141 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
2142 guid2, owner_history, element, pset
2143 );
2144 self.add_entity(rel_str);
2145
2146 pset
2147 }
2148
2149 #[allow(clippy::too_many_arguments)]
2154 pub fn add_ldt_metadata_pset(
2155 &mut self,
2156 element: EntityRef,
2157 owner_history: EntityRef,
2158 index: usize,
2159 symmetry: i32,
2160 num_c_planes: i32,
2161 num_g_angles: i32,
2162 dc: f64,
2163 dg: f64,
2164 total_flux: f64,
2165 dr: &[f64; 10],
2166 luminaire_name: &str,
2167 ) -> EntityRef {
2168 let mut properties = Vec::new();
2169
2170 let idx_prop = self.add_entity(format!(
2172 "IFCPROPERTYSINGLEVALUE('LDT_{}_Index',$,IFCINTEGER({}),$)",
2173 index, index
2174 ));
2175 properties.push(idx_prop);
2176
2177 let sym_prop = self.add_entity(format!(
2179 "IFCPROPERTYSINGLEVALUE('LDT_{}_Symmetry',$,IFCINTEGER({}),$)",
2180 index, symmetry
2181 ));
2182 properties.push(sym_prop);
2183
2184 let mc_prop = self.add_entity(format!(
2186 "IFCPROPERTYSINGLEVALUE('LDT_{}_NumCPlanes',$,IFCINTEGER({}),$)",
2187 index, num_c_planes
2188 ));
2189 properties.push(mc_prop);
2190
2191 let ng_prop = self.add_entity(format!(
2193 "IFCPROPERTYSINGLEVALUE('LDT_{}_NumGAngles',$,IFCINTEGER({}),$)",
2194 index, num_g_angles
2195 ));
2196 properties.push(ng_prop);
2197
2198 let dc_prop = self.add_entity(format!(
2200 "IFCPROPERTYSINGLEVALUE('LDT_{}_Dc',$,IFCREAL({:.2}),$)",
2201 index, dc
2202 ));
2203 properties.push(dc_prop);
2204
2205 let dg_prop = self.add_entity(format!(
2207 "IFCPROPERTYSINGLEVALUE('LDT_{}_Dg',$,IFCREAL({:.2}),$)",
2208 index, dg
2209 ));
2210 properties.push(dg_prop);
2211
2212 let flux_prop = self.add_entity(format!(
2214 "IFCPROPERTYSINGLEVALUE('LDT_{}_TotalFlux',$,IFCLUMINOUSFLUXMEASURE({:.1}),$)",
2215 index, total_flux
2216 ));
2217 properties.push(flux_prop);
2218
2219 let dr_str = dr
2221 .iter()
2222 .map(|v| format!("{:.5}", v))
2223 .collect::<Vec<_>>()
2224 .join(",");
2225 let dr_prop = self.add_entity(format!(
2226 "IFCPROPERTYSINGLEVALUE('LDT_{}_DR',$,IFCLABEL('{}'),$)",
2227 index,
2228 Self::escape_string(&dr_str)
2229 ));
2230 properties.push(dr_prop);
2231
2232 let name_prop = self.add_entity(format!(
2234 "IFCPROPERTYSINGLEVALUE('LDT_{}_LuminaireName',$,IFCLABEL('{}'),$)",
2235 index,
2236 Self::escape_string(luminaire_name)
2237 ));
2238 properties.push(name_prop);
2239
2240 let prop_list: Vec<String> = properties.iter().map(|p| p.to_string()).collect();
2241
2242 let guid = self.generate_guid();
2243 let pset_str = format!(
2244 "IFCPROPERTYSET('{}',{},'Pset_GLDF_LDTMetadata',$,({}))",
2245 guid,
2246 owner_history,
2247 prop_list.join(",")
2248 );
2249 let pset = self.add_entity(pset_str);
2250
2251 let guid2 = self.generate_guid();
2252 let rel_str = format!(
2253 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
2254 guid2, owner_history, element, pset
2255 );
2256 self.add_entity(rel_str);
2257
2258 pset
2259 }
2260
2261 pub fn add_gldf_file_pset(
2265 &mut self,
2266 project: EntityRef,
2267 owner_history: EntityRef,
2268 file_type: &str, filename: &str,
2270 content_type: &str, content: &[u8],
2272 ) -> EntityRef {
2273 use base64::{engine::general_purpose::STANDARD, Engine};
2274
2275 let encoded = STANDARD.encode(content);
2276
2277 let type_prop = self.add_entity(format!(
2278 "IFCPROPERTYSINGLEVALUE('GLDF_FileType',$,IFCLABEL('{}'),$)",
2279 Self::escape_string(file_type)
2280 ));
2281
2282 let filename_prop = self.add_entity(format!(
2283 "IFCPROPERTYSINGLEVALUE('GLDF_Filename',$,IFCLABEL('{}'),$)",
2284 Self::escape_string(filename)
2285 ));
2286
2287 let content_type_prop = self.add_entity(format!(
2288 "IFCPROPERTYSINGLEVALUE('GLDF_ContentType',$,IFCLABEL('{}'),$)",
2289 Self::escape_string(content_type)
2290 ));
2291
2292 let content_prop = self.add_entity(format!(
2293 "IFCPROPERTYSINGLEVALUE('GLDF_FileContent',$,IFCLABEL('{}'),$)",
2294 Self::escape_string(&encoded)
2295 ));
2296
2297 let guid = self.generate_guid();
2298 let pset_str = format!(
2299 "IFCPROPERTYSET('{}',{},'Pset_GLDF_EmbeddedFile',$,({},{},{},{}))",
2300 guid, owner_history, type_prop, filename_prop, content_type_prop, content_prop
2301 );
2302 let pset = self.add_entity(pset_str);
2303
2304 let guid2 = self.generate_guid();
2305 let rel_str = format!(
2306 "IFCRELDEFINESBYPROPERTIES('{}',{},$,$,({}),{})",
2307 guid2, owner_history, project, pset
2308 );
2309 self.add_entity(rel_str);
2310
2311 pset
2312 }
2313
2314 fn current_timestamp() -> i64 {
2320 #[cfg(not(target_arch = "wasm32"))]
2321 {
2322 use std::time::{SystemTime, UNIX_EPOCH};
2323 SystemTime::now()
2324 .duration_since(UNIX_EPOCH)
2325 .map(|d| d.as_secs() as i64)
2326 .unwrap_or(0)
2327 }
2328
2329 #[cfg(target_arch = "wasm32")]
2330 {
2331 1704067200_i64
2332 }
2333 }
2334
2335 pub fn to_step_string(&self) -> String {
2337 #[cfg(not(target_arch = "wasm32"))]
2338 let timestamp = {
2339 let now: DateTime<Utc> = Utc::now();
2340 now.format("%Y-%m-%dT%H:%M:%S").to_string()
2341 };
2342
2343 #[cfg(target_arch = "wasm32")]
2344 let timestamp = "2024-01-01T00:00:00".to_string();
2345
2346 let mut output = String::new();
2347
2348 output.push_str("ISO-10303-21;\n");
2350 output.push_str("HEADER;\n");
2351 output.push_str("FILE_DESCRIPTION(('GLDF to IFC Export'),'2;1');\n");
2352 output.push_str(&format!(
2353 "FILE_NAME('export.ifc','{}',(''),(''),'gldf-rs','gldf-rs','');\n",
2354 timestamp
2355 ));
2356 output.push_str(&format!("FILE_SCHEMA(('{}'));\n", self.schema));
2357 output.push_str("ENDSEC;\n");
2358 output.push('\n');
2359
2360 output.push_str("DATA;\n");
2362 for entity in &self.entities {
2363 output.push_str(entity);
2364 output.push_str(";\n");
2365 }
2366 output.push_str("ENDSEC;\n");
2367 output.push('\n');
2368
2369 output.push_str("END-ISO-10303-21;\n");
2371
2372 output
2373 }
2374}
2375
2376#[cfg(test)]
2377mod tests {
2378 use super::*;
2379
2380 #[test]
2381 fn test_minimal_ifc() {
2382 let mut writer = StepWriter::new("IFC4");
2383 let oh = writer.add_owner_history("Test Corp");
2384 let project = writer.add_project("Test Project", oh);
2385 let site = writer.add_site("Test Site", oh, project);
2386 let building = writer.add_building("Test Building", oh, site);
2387 let storey = writer.add_storey("Ground Floor", oh, building);
2388
2389 let fixture_type = writer.add_light_fixture_type(
2390 "LED Downlight",
2391 "Test Corp",
2392 LightFixtureTypeEnum::PointSource,
2393 oh,
2394 );
2395
2396 let _fixture =
2397 writer.add_light_fixture("LED Downlight 001", oh, storey, Some(fixture_type), None);
2398
2399 let output = writer.to_step_string();
2400
2401 assert!(output.contains("ISO-10303-21"));
2403 assert!(output.contains("IFC4"));
2404 assert!(output.contains("IFCPROJECT"));
2405 assert!(output.contains("IFCSITE"));
2406 assert!(output.contains("IFCBUILDING"));
2407 assert!(output.contains("IFCBUILDINGSTOREY"));
2408 assert!(output.contains("IFCLIGHTFIXTURETYPE"));
2409 assert!(output.contains("IFCLIGHTFIXTURE"));
2410 assert!(output.contains("Pset_ManufacturerTypeInformation"));
2411
2412 assert!(!output.contains("IFCLOCALPLACEMENT($,IFCAXIS2PLACEMENT3D"));
2414
2415 println!("{}", output);
2421 }
2422
2423 #[test]
2424 fn test_light_source_goniometric() {
2425 let mut writer = StepWriter::new("IFC4");
2426 let oh = writer.add_owner_history("Test Corp");
2427 let project = writer.add_project("Test Project", oh);
2428 let site = writer.add_site("Test Site", oh, project);
2429 let building = writer.add_building("Test Building", oh, site);
2430 let storey = writer.add_storey("Ground Floor", oh, building);
2431
2432 let fixture_type = writer.add_light_fixture_type(
2433 "LED Panel",
2434 "Test Corp",
2435 LightFixtureTypeEnum::DirectionSource,
2436 oh,
2437 );
2438
2439 let _light_source = writer.add_light_source_goniometric(
2441 "LED Panel Light Source",
2442 Some((1.0, 0.95, 0.9)),
2443 3000.0,
2444 4500.0,
2445 LightEmissionSourceEnum::Led,
2446 Some("photometry/led_panel.ldt"),
2447 );
2448
2449 writer.add_light_fixture_common_pset(
2451 fixture_type,
2452 oh,
2453 Some(1),
2454 Some(36.0),
2455 Some("SURFACE"),
2456 Some("CEILING"),
2457 );
2458
2459 writer.add_electrical_pset(
2461 fixture_type,
2462 oh,
2463 Some(230.0),
2464 Some(0.16),
2465 Some(0.95),
2466 Some("IP20"),
2467 );
2468
2469 let _fixture =
2470 writer.add_light_fixture("LED Panel 001", oh, storey, Some(fixture_type), None);
2471
2472 let output = writer.to_step_string();
2473
2474 assert!(output.contains("IFCLIGHTSOURCEGONIOMETRIC"));
2476 assert!(output.contains("led_panel.ldt"));
2477 assert!(output.contains("3000.0"));
2478 assert!(output.contains("4500.0"));
2479 assert!(output.contains(".LED."));
2480
2481 assert!(output.contains("Pset_LightFixtureTypeCommon"));
2483 assert!(output.contains("NumberOfSources"));
2484 assert!(output.contains("TotalWattage"));
2485
2486 assert!(output.contains("Pset_ElectricalDeviceCommon"));
2487 assert!(output.contains("RatedVoltage"));
2488 assert!(output.contains("PowerFactor"));
2489 assert!(output.contains("IPCode"));
2490
2491 assert!(output.contains("IFCLABEL('IP20')"));
2493 assert!(!output.contains("IPIP20"));
2494
2495 assert!(output.contains("IFCRELASSIGNSTOGROUP"));
2497
2498 println!("{}", output);
2499 }
2500
2501 #[test]
2502 fn test_embedded_light_distribution() {
2503 let mut writer = StepWriter::new("IFC4");
2504
2505 let distribution_data: Vec<(f64, f64, f64)> = vec![
2506 (0.0, 0.0, 1.0),
2507 (0.0, 30.0, 0.866),
2508 (0.0, 60.0, 0.5),
2509 (0.0, 90.0, 0.0),
2510 ];
2511
2512 let _dist = writer.add_light_intensity_distribution("TYPE_C", &distribution_data);
2513
2514 let output = writer.to_step_string();
2515 assert!(output.contains("IFCLIGHTINTENSITYDISTRIBUTION"));
2516 assert!(output.contains("IFCLIGHTDISTRIBUTIONDATA"));
2517 assert!(output.contains(".TYPE_C."));
2518 }
2519
2520 #[test]
2521 fn test_guid_format() {
2522 let mut writer = StepWriter::new("IFC4");
2523
2524 for _ in 0..10 {
2525 let guid = writer.generate_guid();
2526 assert_eq!(
2527 guid.len(),
2528 22,
2529 "GUID should be exactly 22 characters: {}",
2530 guid
2531 );
2532
2533 for c in guid.chars() {
2534 assert!(
2535 c.is_ascii_alphanumeric() || c == '_' || c == '$',
2536 "Invalid character in GUID: {} (full: {})",
2537 c,
2538 guid
2539 );
2540 }
2541 }
2542 }
2543
2544 #[test]
2545 fn test_ip_code_normalization() {
2546 let mut writer = StepWriter::new("IFC4");
2547 let oh = writer.add_owner_history("Test");
2548 let project = writer.add_project("Test", oh);
2549 let site = writer.add_site("Site", oh, project);
2550 let building = writer.add_building("Building", oh, site);
2551 let _storey = writer.add_storey("Storey", oh, building);
2552
2553 let fixture_type =
2554 writer.add_light_fixture_type("Test", "Test", LightFixtureTypeEnum::NotDefined, oh);
2555
2556 writer.add_electrical_pset(fixture_type, oh, None, None, None, Some("IP20"));
2558 let output = writer.to_step_string();
2559 assert!(output.contains("IFCLABEL('IP20')"));
2560 assert!(!output.contains("IPIP"));
2561
2562 let mut writer2 = StepWriter::new("IFC4");
2564 let oh2 = writer2.add_owner_history("Test");
2565 let project2 = writer2.add_project("Test", oh2);
2566 let site2 = writer2.add_site("Site", oh2, project2);
2567 let building2 = writer2.add_building("Building", oh2, site2);
2568 let _storey2 = writer2.add_storey("Storey", oh2, building2);
2569 let fixture_type2 =
2570 writer2.add_light_fixture_type("Test", "Test", LightFixtureTypeEnum::NotDefined, oh2);
2571 writer2.add_electrical_pset(fixture_type2, oh2, None, None, None, Some("20"));
2572 let output2 = writer2.to_step_string();
2573 assert!(output2.contains("IFCLABEL('IP20')"));
2574 }
2575}