1use ifc_model::{Entity, EntityId, Model, Transaction, Value};
18
19use ifc_model::guid::Guid;
20
21use crate::{PropertyError, PropertyResult};
22
23pub mod pset_slot {
25 pub const GLOBAL_ID: usize = 0;
27 pub const NAME: usize = 2;
29 pub const DESCRIPTION: usize = 3;
31 pub const HAS_PROPERTIES: usize = 4;
33}
34
35pub mod single_value_slot {
37 pub const NAME: usize = 0;
39 pub const SPECIFICATION: usize = 1;
41 pub const NOMINAL_VALUE: usize = 2;
43 pub const UNIT: usize = 3;
45}
46
47pub mod defines_slot {
49 pub const GLOBAL_ID: usize = 0;
51 pub const RELATED_OBJECTS: usize = 4;
53 pub const RELATING_DEFINITION: usize = 5;
55}
56
57pub fn add_property_single_value(
72 tx: &mut Transaction,
73 name: &str,
74 specification: Option<&str>,
75 value: Option<Value>,
76 unit: Option<EntityId>,
77) -> PropertyResult<EntityId> {
78 if name.trim().is_empty() {
79 return Err(PropertyError::AuthoringInvalid {
80 entity: "IFCPROPERTYSINGLEVALUE",
81 attribute: "Name",
82 value: name.to_owned(),
83 });
84 }
85 let mut attributes = vec![Value::Null; single_value_slot::UNIT + 1];
86 attributes[single_value_slot::NAME] = Value::Text(name.into());
87 attributes[single_value_slot::SPECIFICATION] = optional_text(specification);
88 attributes[single_value_slot::NOMINAL_VALUE] = value.unwrap_or(Value::Null);
89 attributes[single_value_slot::UNIT] = unit.map_or(Value::Null, Value::Ref);
90 Ok(tx.create(Entity::new("IFCPROPERTYSINGLEVALUE", attributes)))
91}
92
93pub fn add_property_set(
114 tx: &mut Transaction,
115 global_id: &str,
116 name: &str,
117 description: Option<&str>,
118 properties: &[(&str, EntityId)],
119) -> PropertyResult<EntityId> {
120 check_property_set(global_id, name, properties)?;
121 let refs = properties.iter().map(|(_, id)| Value::Ref(*id)).collect();
122 let mut attributes = vec![Value::Null; pset_slot::HAS_PROPERTIES + 1];
123 attributes[pset_slot::GLOBAL_ID] = Value::Text(global_id.into());
124 attributes[pset_slot::NAME] = Value::Text(name.into());
125 attributes[pset_slot::DESCRIPTION] = optional_text(description);
126 attributes[pset_slot::HAS_PROPERTIES] = Value::List(refs);
127 Ok(tx.create(Entity::new("IFCPROPERTYSET", attributes)))
128}
129
130pub(super) fn check_property_set(
133 global_id: &str,
134 name: &str,
135 properties: &[(&str, EntityId)],
136) -> PropertyResult<()> {
137 if Guid::parse(global_id).is_none() {
138 return Err(PropertyError::AuthoringInvalid {
139 entity: "IFCPROPERTYSET",
140 attribute: "GlobalId",
141 value: global_id.to_owned(),
142 });
143 }
144 if name.trim().is_empty() {
145 return Err(PropertyError::AuthoringInvalid {
146 entity: "IFCPROPERTYSET",
147 attribute: "Name",
148 value: name.to_owned(),
149 });
150 }
151 if properties.is_empty() {
152 return Err(PropertyError::AuthoringInvalid {
153 entity: "IFCPROPERTYSET",
154 attribute: "HasProperties",
155 value: String::from("an empty set"),
156 });
157 }
158 let mut seen = std::collections::BTreeSet::new();
159 for (property_name, _) in properties {
160 if !seen.insert(*property_name) {
161 return Err(PropertyError::AuthoringInvalid {
162 entity: "IFCPROPERTYSET",
163 attribute: "HasProperties",
164 value: (*property_name).to_owned(),
165 });
166 }
167 }
168 Ok(())
169}
170
171pub(super) fn optional_text(value: Option<&str>) -> Value {
172 value.map_or(Value::Null, |text| Value::Text(text.into()))
173}
174
175pub fn attach_property_set(
201 tx: &mut Transaction,
202 model: &Model,
203 global_id: &str,
204 objects: &[EntityId],
205 property_set: EntityId,
206) -> PropertyResult<EntityId> {
207 super::root_authoring::attach_definition(tx, model, global_id, objects, property_set, None)
208}
209
210pub mod enumerated_slot {
212 pub const NAME: usize = 0;
214 pub const SPECIFICATION: usize = 1;
216 pub const VALUES: usize = 2;
218 pub const REFERENCE: usize = 3;
220}
221
222pub mod bounded_slot {
224 pub const NAME: usize = 0;
226 pub const SPECIFICATION: usize = 1;
228 pub const UPPER: usize = 2;
230 pub const LOWER: usize = 3;
232 pub const UNIT: usize = 4;
234 pub const SET_POINT: usize = 5;
236}
237
238pub mod list_slot {
240 pub const NAME: usize = 0;
242 pub const SPECIFICATION: usize = 1;
244 pub const VALUES: usize = 2;
246 pub const UNIT: usize = 3;
248}
249
250pub mod table_slot {
252 pub const NAME: usize = 0;
254 pub const SPECIFICATION: usize = 1;
256 pub const DEFINING: usize = 2;
258 pub const DEFINED: usize = 3;
260 pub const EXPRESSION: usize = 4;
262 pub const DEFINING_UNIT: usize = 5;
264 pub const DEFINED_UNIT: usize = 6;
266 pub const INTERPOLATION: usize = 7;
268}
269
270pub mod reference_slot {
272 pub const NAME: usize = 0;
274 pub const SPECIFICATION: usize = 1;
276 pub const USAGE_NAME: usize = 2;
278 pub const REFERENCE: usize = 3;
280}
281
282pub mod complex_slot {
284 pub const NAME: usize = 0;
286 pub const SPECIFICATION: usize = 1;
288 pub const USAGE_NAME: usize = 2;
290 pub const HAS_PROPERTIES: usize = 3;
292}
293
294pub(super) fn require_name(entity: &'static str, name: &str) -> PropertyResult<()> {
300 if name.trim().is_empty() {
301 return Err(PropertyError::AuthoringInvalid {
302 entity,
303 attribute: "Name",
304 value: name.to_owned(),
305 });
306 }
307 Ok(())
308}
309
310pub fn add_property_enumerated_value(
318 tx: &mut Transaction,
319 name: &str,
320 specification: Option<&str>,
321 values: Option<Vec<Value>>,
322 enumeration: Option<EntityId>,
323) -> PropertyResult<EntityId> {
324 require_name("IFCPROPERTYENUMERATEDVALUE", name)?;
325 if let Some(values) = values.as_ref() {
326 if values.is_empty() {
327 return Err(PropertyError::AuthoringInvalid {
328 entity: "IFCPROPERTYENUMERATEDVALUE",
329 attribute: "EnumerationValues",
330 value: "empty".to_owned(),
331 });
332 }
333 }
334 let mut attributes = vec![Value::Null; enumerated_slot::REFERENCE + 1];
335 attributes[enumerated_slot::NAME] = Value::Text(name.into());
336 attributes[enumerated_slot::SPECIFICATION] = optional_text(specification);
337 attributes[enumerated_slot::VALUES] = values.map_or(Value::Null, Value::List);
338 attributes[enumerated_slot::REFERENCE] = enumeration.map_or(Value::Null, Value::Ref);
339 Ok(tx.create(Entity::new("IFCPROPERTYENUMERATEDVALUE", attributes)))
340}
341
342fn measure_of(value: &Value) -> Option<&str> {
348 match value {
349 Value::Typed { type_name, .. } => Some(type_name.as_ref()),
350 _ => None,
351 }
352}
353
354pub fn add_property_bounded_value(
363 tx: &mut Transaction,
364 name: &str,
365 specification: Option<&str>,
366 upper: Option<Value>,
367 lower: Option<Value>,
368 set_point: Option<Value>,
369 unit: Option<EntityId>,
370) -> PropertyResult<EntityId> {
371 require_name("IFCPROPERTYBOUNDEDVALUE", name)?;
372 let stated: Vec<(&str, &Value)> = [
373 ("UpperBoundValue", upper.as_ref()),
374 ("LowerBoundValue", lower.as_ref()),
375 ("SetPointValue", set_point.as_ref()),
376 ]
377 .into_iter()
378 .filter_map(|(label, value)| value.map(|value| (label, value)))
379 .collect();
380 if let Some(((_, first), rest)) = stated.split_first() {
381 let want = measure_of(first);
382 for (label, value) in rest {
383 if measure_of(value) != want {
384 return Err(PropertyError::AuthoringInvalid {
385 entity: "IFCPROPERTYBOUNDEDVALUE",
386 attribute: "SameUnit",
387 value: (*label).to_owned(),
388 });
389 }
390 }
391 }
392 let mut attributes = vec![Value::Null; bounded_slot::SET_POINT + 1];
393 attributes[bounded_slot::NAME] = Value::Text(name.into());
394 attributes[bounded_slot::SPECIFICATION] = optional_text(specification);
395 attributes[bounded_slot::UPPER] = upper.unwrap_or(Value::Null);
396 attributes[bounded_slot::LOWER] = lower.unwrap_or(Value::Null);
397 attributes[bounded_slot::UNIT] = unit.map_or(Value::Null, Value::Ref);
398 attributes[bounded_slot::SET_POINT] = set_point.unwrap_or(Value::Null);
399 Ok(tx.create(Entity::new("IFCPROPERTYBOUNDEDVALUE", attributes)))
400}
401
402pub fn add_property_list_value(
409 tx: &mut Transaction,
410 name: &str,
411 specification: Option<&str>,
412 values: Option<Vec<Value>>,
413 unit: Option<EntityId>,
414) -> PropertyResult<EntityId> {
415 require_name("IFCPROPERTYLISTVALUE", name)?;
416 if let Some(values) = values.as_ref() {
417 if values.is_empty() {
418 return Err(PropertyError::AuthoringInvalid {
419 entity: "IFCPROPERTYLISTVALUE",
420 attribute: "ListValues",
421 value: "empty".to_owned(),
422 });
423 }
424 }
425 let mut attributes = vec![Value::Null; list_slot::UNIT + 1];
426 attributes[list_slot::NAME] = Value::Text(name.into());
427 attributes[list_slot::SPECIFICATION] = optional_text(specification);
428 attributes[list_slot::VALUES] = values.map_or(Value::Null, Value::List);
429 attributes[list_slot::UNIT] = unit.map_or(Value::Null, Value::Ref);
430 Ok(tx.create(Entity::new("IFCPROPERTYLISTVALUE", attributes)))
431}
432
433#[derive(Debug, Default)]
439pub struct TableValueDraft<'a> {
440 pub name: &'a str,
442 pub specification: Option<&'a str>,
444 pub defining: Option<Vec<Value>>,
446 pub defined: Option<Vec<Value>>,
448 pub defining_unit: Option<EntityId>,
450 pub defined_unit: Option<EntityId>,
452 pub expression: Option<&'a str>,
454 pub interpolation: Option<&'a str>,
456}
457
458pub fn add_property_table_value(
472 tx: &mut Transaction,
473 draft: TableValueDraft<'_>,
474) -> PropertyResult<EntityId> {
475 let TableValueDraft {
476 name,
477 specification,
478 defining,
479 defined,
480 defining_unit,
481 defined_unit,
482 expression,
483 interpolation,
484 } = draft;
485 require_name("IFCPROPERTYTABLEVALUE", name)?;
486 let sizes = (
487 defining.as_ref().map(Vec::len),
488 defined.as_ref().map(Vec::len),
489 );
490 if let (Some(left), Some(right)) = sizes {
491 if left != right {
492 return Err(PropertyError::AuthoringInvalid {
493 entity: "IFCPROPERTYTABLEVALUE",
494 attribute: "DefiningValues",
495 value: format!("{left} defining against {right} defined"),
496 });
497 }
498 }
499 for (label, column) in [
500 ("DefiningValues", defining.as_ref()),
501 ("DefinedValues", defined.as_ref()),
502 ] {
503 let Some(column) = column else { continue };
504 if column.is_empty() {
505 return Err(PropertyError::AuthoringInvalid {
506 entity: "IFCPROPERTYTABLEVALUE",
507 attribute: label,
508 value: "empty".to_owned(),
509 });
510 }
511 let want = measure_of(&column[0]);
512 if column.iter().any(|value| measure_of(value) != want) {
513 return Err(PropertyError::AuthoringInvalid {
514 entity: "IFCPROPERTYTABLEVALUE",
515 attribute: label,
516 value: "mixed measures".to_owned(),
517 });
518 }
519 }
520 let mut attributes = vec![Value::Null; table_slot::INTERPOLATION + 1];
521 attributes[table_slot::NAME] = Value::Text(name.into());
522 attributes[table_slot::SPECIFICATION] = optional_text(specification);
523 attributes[table_slot::DEFINING] = defining.map_or(Value::Null, Value::List);
524 attributes[table_slot::DEFINED] = defined.map_or(Value::Null, Value::List);
525 attributes[table_slot::EXPRESSION] = optional_text(expression);
526 attributes[table_slot::DEFINING_UNIT] = defining_unit.map_or(Value::Null, Value::Ref);
527 attributes[table_slot::DEFINED_UNIT] = defined_unit.map_or(Value::Null, Value::Ref);
528 attributes[table_slot::INTERPOLATION] =
529 interpolation.map_or(Value::Null, |token| Value::Enum(token.into()));
530 Ok(tx.create(Entity::new("IFCPROPERTYTABLEVALUE", attributes)))
531}
532
533pub fn add_property_reference_value(
542 tx: &mut Transaction,
543 name: &str,
544 specification: Option<&str>,
545 usage_name: Option<&str>,
546 reference: Option<EntityId>,
547) -> PropertyResult<EntityId> {
548 require_name("IFCPROPERTYREFERENCEVALUE", name)?;
549 let mut attributes = vec![Value::Null; reference_slot::REFERENCE + 1];
550 attributes[reference_slot::NAME] = Value::Text(name.into());
551 attributes[reference_slot::SPECIFICATION] = optional_text(specification);
552 attributes[reference_slot::USAGE_NAME] = optional_text(usage_name);
553 attributes[reference_slot::REFERENCE] = reference.map_or(Value::Null, Value::Ref);
554 Ok(tx.create(Entity::new("IFCPROPERTYREFERENCEVALUE", attributes)))
555}
556
557pub fn add_complex_property(
571 tx: &mut Transaction,
572 name: &str,
573 specification: Option<&str>,
574 usage_name: &str,
575 properties: &[(&str, EntityId)],
576) -> PropertyResult<EntityId> {
577 require_name("IFCCOMPLEXPROPERTY", name)?;
578 if usage_name.trim().is_empty() {
579 return Err(PropertyError::AuthoringInvalid {
580 entity: "IFCCOMPLEXPROPERTY",
581 attribute: "UsageName",
582 value: usage_name.to_owned(),
583 });
584 }
585 if properties.is_empty() {
586 return Err(PropertyError::AuthoringInvalid {
587 entity: "IFCCOMPLEXPROPERTY",
588 attribute: "HasProperties",
589 value: "empty".to_owned(),
590 });
591 }
592 let mut seen = std::collections::BTreeSet::new();
593 for (property_name, _) in properties {
594 if !seen.insert(*property_name) {
595 return Err(PropertyError::AuthoringInvalid {
596 entity: "IFCCOMPLEXPROPERTY",
597 attribute: "HasProperties",
598 value: (*property_name).to_owned(),
599 });
600 }
601 }
602 let refs = properties.iter().map(|(_, id)| Value::Ref(*id)).collect();
603 let mut attributes = vec![Value::Null; complex_slot::HAS_PROPERTIES + 1];
604 attributes[complex_slot::NAME] = Value::Text(name.into());
605 attributes[complex_slot::SPECIFICATION] = optional_text(specification);
606 attributes[complex_slot::USAGE_NAME] = Value::Text(usage_name.into());
607 attributes[complex_slot::HAS_PROPERTIES] = Value::List(refs);
608 let own = tx.create(Entity::new("IFCCOMPLEXPROPERTY", attributes));
612 if properties.iter().any(|(_, id)| *id == own) {
613 return Err(PropertyError::AuthoringInvalid {
614 entity: "IFCCOMPLEXPROPERTY",
615 attribute: "HasProperties",
616 value: "self reference".to_owned(),
617 });
618 }
619 Ok(own)
620}
621
622pub mod element_quantity_slot {
624 pub const GLOBAL_ID: usize = 0;
626 pub const NAME: usize = 2;
628 pub const DESCRIPTION: usize = 3;
630 pub const METHOD: usize = 4;
632 pub const QUANTITIES: usize = 5;
634}
635
636pub mod complex_quantity_slot {
638 pub const NAME: usize = 0;
640 pub const DESCRIPTION: usize = 1;
642 pub const HAS_QUANTITIES: usize = 2;
644 pub const DISCRIMINATION: usize = 3;
646 pub const QUALITY: usize = 4;
648 pub const USAGE: usize = 5;
650}
651
652pub fn add_element_quantity(
671 tx: &mut Transaction,
672 global_id: &str,
673 name: &str,
674 method_of_measurement: Option<&str>,
675 quantities: &[EntityId],
676) -> PropertyResult<EntityId> {
677 check_element_quantity(global_id, name, quantities)?;
678 let mut attributes = vec![Value::Null; element_quantity_slot::QUANTITIES + 1];
679 attributes[element_quantity_slot::GLOBAL_ID] = Value::Text(global_id.into());
680 attributes[element_quantity_slot::NAME] = Value::Text(name.into());
681 attributes[element_quantity_slot::METHOD] = optional_text(method_of_measurement);
682 attributes[element_quantity_slot::QUANTITIES] =
683 Value::List(quantities.iter().copied().map(Value::Ref).collect());
684 Ok(tx.create(Entity::new("IFCELEMENTQUANTITY", attributes)))
685}
686
687pub(super) fn check_element_quantity(
690 global_id: &str,
691 name: &str,
692 quantities: &[EntityId],
693) -> PropertyResult<()> {
694 if Guid::parse(global_id).is_none() {
695 return Err(PropertyError::AuthoringInvalid {
696 entity: "IFCELEMENTQUANTITY",
697 attribute: "GlobalId",
698 value: global_id.to_owned(),
699 });
700 }
701 require_name("IFCELEMENTQUANTITY", name)?;
702 if quantities.is_empty() {
703 return Err(PropertyError::AuthoringInvalid {
704 entity: "IFCELEMENTQUANTITY",
705 attribute: "Quantities",
706 value: "empty".to_owned(),
707 });
708 }
709 Ok(())
710}
711
712pub fn add_physical_complex_quantity(
722 tx: &mut Transaction,
723 name: &str,
724 description: Option<&str>,
725 quantities: &[EntityId],
726 discrimination: &str,
727) -> PropertyResult<EntityId> {
728 require_name("IFCPHYSICALCOMPLEXQUANTITY", name)?;
729 if discrimination.trim().is_empty() {
730 return Err(PropertyError::AuthoringInvalid {
731 entity: "IFCPHYSICALCOMPLEXQUANTITY",
732 attribute: "Discrimination",
733 value: discrimination.to_owned(),
734 });
735 }
736 if quantities.is_empty() {
737 return Err(PropertyError::AuthoringInvalid {
738 entity: "IFCPHYSICALCOMPLEXQUANTITY",
739 attribute: "HasQuantities",
740 value: "empty".to_owned(),
741 });
742 }
743 let mut attributes = vec![Value::Null; complex_quantity_slot::USAGE + 1];
744 attributes[complex_quantity_slot::NAME] = Value::Text(name.into());
745 attributes[complex_quantity_slot::DESCRIPTION] = optional_text(description);
746 attributes[complex_quantity_slot::HAS_QUANTITIES] =
747 Value::List(quantities.iter().copied().map(Value::Ref).collect());
748 attributes[complex_quantity_slot::DISCRIMINATION] = Value::Text(discrimination.into());
749 Ok(tx.create(Entity::new("IFCPHYSICALCOMPLEXQUANTITY", attributes)))
750}