Skip to main content

ifc_properties/pset/
authoring.rs

1//! Transactional authoring of property sets and their attachment.
2//!
3//! Reading lives in the sibling modules; this is the write side.
4//! Slot order is resolved from the bundled IFC4X3 schema, not
5//! from memory: a pset writes HasProperties at slot 4 because
6//! four IfcRoot fields precede it.
7//!
8//! The `IfcRoot` writers that take the model, and the
9//! `*_with_owner_history` variants, are bound to the declared release in
10//! `root_authoring.rs` (#191).
11//!
12//! # Why the schema WHERE rules are enforced here
13//!
14//! IfcPropertySet states ExistsName and UniquePropertyNames.
15//! A nameless pset cannot be looked up, and duplicate property
16//! names make a lookup ambiguous: both parse, both corrupt.
17use ifc_model::{Entity, EntityId, Model, Transaction, Value};
18
19use ifc_model::guid::Guid;
20
21use crate::{PropertyError, PropertyResult};
22
23/// `IfcPropertySet` slots.
24pub mod pset_slot {
25    /// `GlobalId` (from `IfcRoot`).
26    pub const GLOBAL_ID: usize = 0;
27    /// `Name` (from `IfcRoot`). Required by the ExistsName rule.
28    pub const NAME: usize = 2;
29    /// `Description` (from `IfcRoot`).
30    pub const DESCRIPTION: usize = 3;
31    /// `HasProperties`. Required.
32    pub const HAS_PROPERTIES: usize = 4;
33}
34
35/// `IfcPropertySingleValue` slots.
36pub mod single_value_slot {
37    /// `Name` (from `IfcProperty`). Required.
38    pub const NAME: usize = 0;
39    /// `Specification` (from `IfcProperty`).
40    pub const SPECIFICATION: usize = 1;
41    /// `NominalValue`.
42    pub const NOMINAL_VALUE: usize = 2;
43    /// `Unit`.
44    pub const UNIT: usize = 3;
45}
46
47/// `IfcRelDefinesByProperties` slots.
48pub mod defines_slot {
49    /// `GlobalId` (from `IfcRoot`).
50    pub const GLOBAL_ID: usize = 0;
51    /// `RelatedObjects`. Required.
52    pub const RELATED_OBJECTS: usize = 4;
53    /// `RelatingPropertyDefinition`. Required.
54    pub const RELATING_DEFINITION: usize = 5;
55}
56
57/// Stage an `IfcPropertySingleValue`.
58///
59/// `value` is an `IfcValue`: a measure-wrapped scalar such as
60/// `Value::Typed { type_name: IFCLENGTHMEASURE, .. }`. A bare
61/// literal is legal but dimensionally meaningless, so the caller
62/// chooses; this crate will not invent a measure.
63///
64/// `specification` is `IfcProperty.Specification`: prose describing
65/// what the property means, kept distinct from its value.
66///
67/// # Errors
68///
69/// Refuses a blank name: IfcProperty.Name is required, and a
70/// whitespace-only name satisfies EXISTS while naming nothing.
71pub 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
93/// Stage an `IfcPropertySet`.
94///
95/// # Release
96///
97/// `OwnerHistory` is left `$`, which IFC4 and IFC4X3 allow and IFC2X3
98/// forbids. This function takes no model, so it cannot see the release and
99/// cannot refuse an IFC2X3 one; for IFC2X3 use
100/// [`add_property_set_with_owner_history`](crate::add_property_set_with_owner_history).
101/// [`attach_property_set_with_owner_history`](crate::attach_property_set_with_owner_history)
102/// refuses to attach, in IFC2X3, a set written here.
103///
104/// # Errors
105///
106/// Refuses a blank name (ExistsName), an empty property list
107/// (HasProperties is required), and duplicate property names
108/// (UniquePropertyNames).
109///
110/// Properties are passed as `(name, id)` pairs rather than bare ids:
111/// the uniqueness rule is stated over names, and a staged entity
112/// cannot be read back out of the transaction to recover them.
113pub 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
130/// The `IfcPropertySet` rules [`add_property_set`] enforces, shared with its
131/// release-bound variant.
132pub(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
175/// Stage an `IfcRelDefinesByProperties` attaching a set to objects.
176///
177/// # Release
178///
179/// Bound to the model's declared release (`quantity/release.rs`): the
180/// record is laid out by attribute name from its table, and the
181/// NoRelatedTypeObject check uses its inheritance. `OwnerHistory` is left
182/// `$`, which IFC4 and IFC4X3 allow; IFC2X3 requires it, so an IFC2X3 model
183/// is refused with [`PropertyError::AuthoringRequired`]. Use
184/// [`attach_property_set_with_owner_history`](crate::attach_property_set_with_owner_history)
185/// there.
186///
187/// # Errors
188///
189/// Refuses an empty object list, and any object whose type is an
190/// `IfcTypeObject` subtype. The schema states NoRelatedTypeObject:
191/// a type carries properties through IfcRelDefinesByType instead, and
192/// attaching here would be read by nothing that walks type properties.
193/// Refuses a model that binds no single known release with
194/// [`PropertyError::MultipleSchemas`] or
195/// [`PropertyError::UnsupportedSchema`], and an IFC2X3 model with
196/// [`PropertyError::AuthoringRequired`]. Nothing is staged on an error.
197///
198/// Needs the model because the rule is stated over the related
199/// objects types, which only the committed model knows.
200pub 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
210/// `IfcPropertyEnumeratedValue` slots.
211pub mod enumerated_slot {
212    /// `Name`. Required.
213    pub const NAME: usize = 0;
214    /// `Specification`.
215    pub const SPECIFICATION: usize = 1;
216    /// `EnumerationValues`.
217    pub const VALUES: usize = 2;
218    /// `EnumerationReference`.
219    pub const REFERENCE: usize = 3;
220}
221
222/// `IfcPropertyBoundedValue` slots.
223pub mod bounded_slot {
224    /// `Name`. Required.
225    pub const NAME: usize = 0;
226    /// `Specification`.
227    pub const SPECIFICATION: usize = 1;
228    /// `UpperBoundValue`.
229    pub const UPPER: usize = 2;
230    /// `LowerBoundValue`.
231    pub const LOWER: usize = 3;
232    /// `Unit`.
233    pub const UNIT: usize = 4;
234    /// `SetPointValue`.
235    pub const SET_POINT: usize = 5;
236}
237
238/// `IfcPropertyListValue` slots.
239pub mod list_slot {
240    /// `Name`. Required.
241    pub const NAME: usize = 0;
242    /// `Specification`.
243    pub const SPECIFICATION: usize = 1;
244    /// `ListValues`.
245    pub const VALUES: usize = 2;
246    /// `Unit`.
247    pub const UNIT: usize = 3;
248}
249
250/// `IfcPropertyTableValue` slots.
251pub mod table_slot {
252    /// `Name`. Required.
253    pub const NAME: usize = 0;
254    /// `Specification`.
255    pub const SPECIFICATION: usize = 1;
256    /// `DefiningValues`, the independent variable.
257    pub const DEFINING: usize = 2;
258    /// `DefinedValues`, the dependent variable.
259    pub const DEFINED: usize = 3;
260    /// `Expression`.
261    pub const EXPRESSION: usize = 4;
262    /// `DefiningUnit`.
263    pub const DEFINING_UNIT: usize = 5;
264    /// `DefinedUnit`.
265    pub const DEFINED_UNIT: usize = 6;
266    /// `CurveInterpolation`.
267    pub const INTERPOLATION: usize = 7;
268}
269
270/// `IfcPropertyReferenceValue` slots.
271pub mod reference_slot {
272    /// `Name`. Required.
273    pub const NAME: usize = 0;
274    /// `Specification`.
275    pub const SPECIFICATION: usize = 1;
276    /// `UsageName`.
277    pub const USAGE_NAME: usize = 2;
278    /// `PropertyReference`.
279    pub const REFERENCE: usize = 3;
280}
281
282/// `IfcComplexProperty` slots.
283pub mod complex_slot {
284    /// `Name`. Required.
285    pub const NAME: usize = 0;
286    /// `Specification`.
287    pub const SPECIFICATION: usize = 1;
288    /// `UsageName`. Required.
289    pub const USAGE_NAME: usize = 2;
290    /// `HasProperties`. Required.
291    pub const HAS_PROPERTIES: usize = 3;
292}
293
294/// Refuse a blank property name.
295///
296/// `IfcProperty.Name` is required and is the key every lookup uses. A
297/// whitespace-only name satisfies the schema's EXISTS check and still
298/// names nothing.
299pub(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
310/// Stage an `IfcPropertyEnumeratedValue`.
311///
312/// # Errors
313///
314/// Refuses a blank name, and an empty value list: the schema types
315/// `EnumerationValues` as `LIST [1:?]`, so an empty aggregate is
316/// malformed where omission is legal.
317pub 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
342/// The declared measure of a value, when it wears one.
343///
344/// `Value::Typed { type_name, .. }` is how a measure-wrapped scalar
345/// reaches this crate. A bare literal has no measure and compares
346/// equal to any other bare literal.
347fn measure_of(value: &Value) -> Option<&str> {
348    match value {
349        Value::Typed { type_name, .. } => Some(type_name.as_ref()),
350        _ => None,
351    }
352}
353
354/// Stage an `IfcPropertyBoundedValue`.
355///
356/// # Errors
357///
358/// Refuses a blank name, and bounds whose measures disagree. The
359/// schema states SameUnitUpperLower, SameUnitLowerSet and
360/// SameUnitUpperSet: mixing a length lower bound with a mass upper
361/// bound parses and then compares two different quantities.
362pub 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
402/// Stage an `IfcPropertyListValue`.
403///
404/// # Errors
405///
406/// Refuses a blank name and an empty list, which the schema types as
407/// `LIST [1:?]`.
408pub 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/// Authored fields for `IfcPropertyTableValue`.
434///
435/// A struct rather than a parameter list: the entity carries two
436/// columns, two units and two free-text fields, and positional
437/// arguments of the same types are easy to transpose silently.
438#[derive(Debug, Default)]
439pub struct TableValueDraft<'a> {
440    /// `Name`. Required, and the key every lookup uses.
441    pub name: &'a str,
442    /// `Specification`.
443    pub specification: Option<&'a str>,
444    /// `DefiningValues`, the independent variable.
445    pub defining: Option<Vec<Value>>,
446    /// `DefinedValues`, the dependent variable.
447    pub defined: Option<Vec<Value>>,
448    /// `DefiningUnit`.
449    pub defining_unit: Option<EntityId>,
450    /// `DefinedUnit`.
451    pub defined_unit: Option<EntityId>,
452    /// `Expression`, the closed form when one exists.
453    pub expression: Option<&'a str>,
454    /// `CurveInterpolation`, an `IfcCurveInterpolationEnum` token.
455    pub interpolation: Option<&'a str>,
456}
457
458/// Stage an `IfcPropertyTableValue`.
459///
460/// A lookup table: `defining` is the independent variable, `defined`
461/// the dependent one, and row `i` pairs `defining[i]` with
462/// `defined[i]`.
463///
464/// # Errors
465///
466/// Refuses a blank name. Enforces the schema's three table rules:
467/// WR21, the two columns must be the same length, since a row whose
468/// input has no output is not a row; WR22 and WR23, each column must
469/// be homogeneous, since a column mixing lengths and masses cannot be
470/// interpolated.
471pub 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
533/// Stage an `IfcPropertyReferenceValue`.
534///
535/// Points at another entity rather than carrying a scalar: a table,
536/// a document reference, a time series.
537///
538/// # Errors
539///
540/// Refuses a blank name.
541pub 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
557/// Stage an `IfcComplexProperty`.
558///
559/// Groups properties under one name, so a U-value can carry its own
560/// measurement conditions as nested properties.
561///
562/// # Errors
563///
564/// Refuses a blank name or usage name, an empty `HasProperties` (the
565/// schema types it `SET [1:?]`), duplicate nested names, and a
566/// property listed as its own child. The schema states WR21 as
567/// `SELF :=: temp`, forbidding direct self-containment: a complex
568/// property inside itself is a cycle that a recursive reader cannot
569/// terminate on.
570pub 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    // WR21 forbids SELF among HasProperties. The id is only known after
609    // staging, so the check runs here; the refusal abandons the staged
610    // record, which commit never sees because the caller drops the error.
611    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
622/// `IfcElementQuantity` slots.
623pub mod element_quantity_slot {
624    /// `GlobalId` (from `IfcRoot`). Required.
625    pub const GLOBAL_ID: usize = 0;
626    /// `Name` (from `IfcRoot`).
627    pub const NAME: usize = 2;
628    /// `Description` (from `IfcRoot`).
629    pub const DESCRIPTION: usize = 3;
630    /// `MethodOfMeasurement`.
631    pub const METHOD: usize = 4;
632    /// `Quantities`. Required.
633    pub const QUANTITIES: usize = 5;
634}
635
636/// `IfcPhysicalComplexQuantity` slots.
637pub mod complex_quantity_slot {
638    /// `Name`. Required.
639    pub const NAME: usize = 0;
640    /// `Description`.
641    pub const DESCRIPTION: usize = 1;
642    /// `HasQuantities`. Required.
643    pub const HAS_QUANTITIES: usize = 2;
644    /// `Discrimination`. Required.
645    pub const DISCRIMINATION: usize = 3;
646    /// `Quality`.
647    pub const QUALITY: usize = 4;
648    /// `Usage`.
649    pub const USAGE: usize = 5;
650}
651
652/// Stage an `IfcElementQuantity`.
653///
654/// The quantity-takeoff counterpart of a property set: a named group
655/// of measured quantities attached to an object through
656/// [`attach_property_set`], which accepts any
657/// `IfcPropertySetDefinition`.
658///
659/// # Release
660///
661/// `OwnerHistory` is left `$`, which IFC4 and IFC4X3 allow and IFC2X3
662/// forbids. This function takes no model, so it cannot refuse an IFC2X3
663/// one; there use
664/// [`add_element_quantity_with_owner_history`](crate::add_element_quantity_with_owner_history).
665///
666/// # Errors
667///
668/// Refuses a malformed GUID and an empty quantity list, which the
669/// schema types as `SET [1:?]`.
670pub 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
687/// The `IfcElementQuantity` rules [`add_element_quantity`] enforces, shared
688/// with its release-bound variant.
689pub(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
712/// Stage an `IfcPhysicalComplexQuantity`.
713///
714/// Groups quantities that share a discrimination, so a wall's gross
715/// and net areas can sit under one heading.
716///
717/// # Errors
718///
719/// Refuses a blank name or discrimination, and an empty
720/// `HasQuantities`, typed `SET [1:?]` by the schema.
721pub 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}