Skip to main content

ifc_systems/authoring/
mod.rs

1//! Transactional authoring of systems, ports and their connections.
2//!
3//! # Slot layouts come from the readers
4//!
5//! Each relationship here has its own idea of which end is which:
6//! IfcRelAssignsToGroup names members at slot 4 and the group at 6,
7//! IfcRelNests parent at 4 and children at 5, IfcRelConnectsPortToElement
8//! port at 4 and element at 5. The reading side already encodes all of
9//! that; authoring resolves the same constants so one cannot be
10//! corrected without the other.
11//!
12//! # Releases and `OwnerHistory` (#202)
13//!
14//! `IfcRoot.OwnerHistory` is required in IFC2X3 TC1 and `OPTIONAL` from
15//! IFC4 on:
16//!
17//! ```text
18//! IFC2X3_TC1   OwnerHistory : IfcOwnerHistory;
19//! IFC4         OwnerHistory : OPTIONAL IfcOwnerHistory;
20//! IFC4X3_ADD2  OwnerHistory : OPTIONAL IfcOwnerHistory;
21//! ```
22//!
23//! The writers without a model (`create_system`, `connect_ports`, ...)
24//! cannot see the release. They write the IFC4/IFC4X3 layout with
25//! `OwnerHistory` `$`, so their records are IFC4/IFC4X3 only. Each has a
26//! `*_with_owner_history` variant that takes the model, binds its declared
27//! release (`release.rs`), lays the record out by attribute name from that
28//! release's table, and takes a caller-supplied `IfcOwnerHistory`, which
29//! must exist (in the model or staged on the transaction) and be an
30//! `IfcOwnerHistory`; one is never invented here. This follows
31//! `ifc-material` (#77), `ifc-properties` (#191) and `ifc-classification`
32//! (#194). In IFC4 and IFC4X3 a variant writes the plain writer's record
33//! with the reference in the optional slot.
34
35use ifc_model::guid::Guid;
36use ifc_model::{Entity, EntityId, Transaction, Value};
37
38use crate::connectivity::relation::slot as connects_slot;
39use crate::port::definition::slot as port_slot;
40use crate::system::group::slot as group_slot;
41use crate::zone::spatial_group::slot as placement_slot;
42
43mod distribution;
44mod error;
45mod owned;
46mod release;
47mod system_kind;
48
49pub use distribution::{
50    create_distribution_element, create_distribution_element_with_owner_history,
51    create_spatial_zone, create_spatial_zone_with_owner_history, create_zone,
52    create_zone_with_owner_history, DistributionElementKind, ElementAttributes,
53};
54pub use error::{SystemAuthoringError, SystemAuthoringResult};
55pub use owned::{
56    assign_to_group_with_owner_history, connect_port_to_element_with_owner_history,
57    connect_ports_with_owner_history, contain_in_spatial_structure_with_owner_history,
58    create_group_with_owner_history, create_port_with_owner_history,
59    create_system_with_owner_history, nest_ports_with_owner_history,
60    reference_in_spatial_structure_with_owner_history,
61};
62pub use system_kind::{
63    create_classified_system, create_classified_system_with_owner_history, ClassifiedSystemDraft,
64    SystemKind,
65};
66
67use error::invalid;
68
69fn guid(entity: &'static str, global_id: &str) -> SystemAuthoringResult<()> {
70    if Guid::parse(global_id).is_none() {
71        return Err(invalid(entity, "GlobalId", global_id));
72    }
73    Ok(())
74}
75
76/// Refuse an empty related set, and the relating end listed in it.
77fn check_related(
78    entity: &'static str,
79    attribute: &'static str,
80    global_id: &str,
81    relating: EntityId,
82    related: &[EntityId],
83    relating_word: &str,
84) -> SystemAuthoringResult<()> {
85    guid(entity, global_id)?;
86    if related.is_empty() {
87        return Err(invalid(entity, attribute, "empty"));
88    }
89    if related.contains(&relating) {
90        return Err(invalid(
91            entity,
92            attribute,
93            format!("contains the {relating_word}"),
94        ));
95    }
96    Ok(())
97}
98
99/// Refuse a port connected to itself.
100fn check_ports(
101    global_id: &str,
102    relating: EntityId,
103    related: EntityId,
104) -> SystemAuthoringResult<()> {
105    guid("IFCRELCONNECTSPORTS", global_id)?;
106    if relating == related {
107        return Err(invalid(
108            "IFCRELCONNECTSPORTS",
109            "RelatedPort",
110            "same as RelatingPort",
111        ));
112    }
113    Ok(())
114}
115
116fn refs(ids: &[EntityId]) -> Value {
117    Value::List(ids.iter().copied().map(Value::Ref).collect())
118}
119
120/// Stage an `IfcSystem`.
121///
122/// Writes the IFC4/IFC4X3 layout with `OwnerHistory` `$`, so the record
123/// is IFC4/IFC4X3 only; use [`create_system_with_owner_history`] for a
124/// release-bound record, which IFC2X3 needs.
125///
126/// # Errors
127///
128/// Refuses a malformed GlobalId.
129pub fn create_system(
130    tx: &mut Transaction,
131    global_id: &str,
132    name: Option<&str>,
133) -> SystemAuthoringResult<EntityId> {
134    guid("IFCSYSTEM", global_id)?;
135    let mut attributes = vec![Value::Null; 5];
136    attributes[0] = Value::Text(global_id.into());
137    attributes[2] = name.map_or(Value::Null, |t| Value::Text(t.into()));
138    Ok(tx.create(Entity::new("IFCSYSTEM", attributes)))
139}
140
141/// Stage an `IfcDistributionPort`.
142///
143/// `flow_direction` is an `IfcFlowDirectionEnum` token: SOURCE, SINK
144/// or SOURCEANDSINK. It is what the flow reader follows, so a port
145/// without one is invisible to downstream/upstream queries.
146///
147/// Writes the IFC4/IFC4X3 ten-attribute layout with `OwnerHistory` `$`,
148/// so the record is IFC4/IFC4X3 only; use
149/// [`create_port_with_owner_history`] for IFC2X3, which declares eight.
150///
151/// # Errors
152///
153/// Refuses a malformed GlobalId.
154pub fn create_port(
155    tx: &mut Transaction,
156    global_id: &str,
157    name: Option<&str>,
158    flow_direction: Option<&str>,
159) -> SystemAuthoringResult<EntityId> {
160    guid("IFCDISTRIBUTIONPORT", global_id)?;
161    let mut attributes = vec![Value::Null; 10];
162    attributes[0] = Value::Text(global_id.into());
163    attributes[2] = name.map_or(Value::Null, |t| Value::Text(t.into()));
164    attributes[7] = flow_direction.map_or(Value::Null, |t| Value::Enum(t.into()));
165    Ok(tx.create(Entity::new("IFCDISTRIBUTIONPORT", attributes)))
166}
167
168/// Stage an `IfcRelAssignsToGroup`: elements joined into a system.
169///
170/// Writes `OwnerHistory` `$`, so the record is IFC4/IFC4X3 only; use
171/// [`assign_to_group_with_owner_history`] for IFC2X3.
172///
173/// # Errors
174///
175/// Refuses a malformed GlobalId, an empty member list, and the group
176/// listed among its own members.
177pub fn assign_to_group(
178    tx: &mut Transaction,
179    global_id: &str,
180    group: EntityId,
181    members: &[EntityId],
182) -> SystemAuthoringResult<EntityId> {
183    check_related(
184        "IFCRELASSIGNSTOGROUP",
185        "RelatedObjects",
186        global_id,
187        group,
188        members,
189        "group",
190    )?;
191    let width = group_slot::ASSIGNS_GROUP.max(group_slot::ASSIGNS_RELATED) + 1;
192    let mut attributes = vec![Value::Null; width];
193    attributes[0] = Value::Text(global_id.into());
194    attributes[group_slot::ASSIGNS_RELATED] = refs(members);
195    attributes[group_slot::ASSIGNS_GROUP] = Value::Ref(group);
196    Ok(tx.create(Entity::new("IFCRELASSIGNSTOGROUP", attributes)))
197}
198
199/// Stage an `IfcRelNests`: ports nested under the element owning them.
200///
201/// Writes `OwnerHistory` `$`, so the record is IFC4/IFC4X3 only; use
202/// [`nest_ports_with_owner_history`] for IFC2X3.
203///
204/// # Errors
205///
206/// Refuses a malformed GlobalId, an empty child list, and a parent
207/// nested under itself.
208pub fn nest_ports(
209    tx: &mut Transaction,
210    global_id: &str,
211    parent: EntityId,
212    children: &[EntityId],
213) -> SystemAuthoringResult<EntityId> {
214    check_related(
215        "IFCRELNESTS",
216        "RelatedObjects",
217        global_id,
218        parent,
219        children,
220        "parent",
221    )?;
222    let width = port_slot::NESTS_PARENT.max(port_slot::NESTS_CHILDREN) + 1;
223    let mut attributes = vec![Value::Null; width];
224    attributes[0] = Value::Text(global_id.into());
225    attributes[port_slot::NESTS_PARENT] = Value::Ref(parent);
226    attributes[port_slot::NESTS_CHILDREN] = refs(children);
227    Ok(tx.create(Entity::new("IFCRELNESTS", attributes)))
228}
229
230/// Stage an `IfcRelConnectsPortToElement`.
231///
232/// Writes `OwnerHistory` `$`, so the record is IFC4/IFC4X3 only; use
233/// [`connect_port_to_element_with_owner_history`] for IFC2X3.
234///
235/// # Errors
236///
237/// Refuses a malformed GlobalId.
238pub fn connect_port_to_element(
239    tx: &mut Transaction,
240    global_id: &str,
241    port: EntityId,
242    element: EntityId,
243) -> SystemAuthoringResult<EntityId> {
244    guid("IFCRELCONNECTSPORTTOELEMENT", global_id)?;
245    let width = port_slot::PORT_TO_ELEMENT_PORT.max(port_slot::PORT_TO_ELEMENT_ELEMENT) + 1;
246    let mut attributes = vec![Value::Null; width];
247    attributes[0] = Value::Text(global_id.into());
248    attributes[port_slot::PORT_TO_ELEMENT_PORT] = Value::Ref(port);
249    attributes[port_slot::PORT_TO_ELEMENT_ELEMENT] = Value::Ref(element);
250    Ok(tx.create(Entity::new("IFCRELCONNECTSPORTTOELEMENT", attributes)))
251}
252
253/// Stage an `IfcRelConnectsPorts`: the link the flow graph walks.
254///
255/// `realizing` names the element that physically realises the
256/// connection, such as the fitting between two segments.
257///
258/// Writes `OwnerHistory` `$`, so the record is IFC4/IFC4X3 only; use
259/// [`connect_ports_with_owner_history`] for IFC2X3.
260///
261/// # Errors
262///
263/// Refuses a malformed GlobalId and a port connected to itself, which
264/// is a self-loop the reachability walk would report as a component.
265pub fn connect_ports(
266    tx: &mut Transaction,
267    global_id: &str,
268    relating: EntityId,
269    related: EntityId,
270    realizing: Option<EntityId>,
271) -> SystemAuthoringResult<EntityId> {
272    check_ports(global_id, relating, related)?;
273    let width = connects_slot::REALIZING + 1;
274    let mut attributes = vec![Value::Null; width];
275    attributes[0] = Value::Text(global_id.into());
276    attributes[connects_slot::RELATING] = Value::Ref(relating);
277    attributes[connects_slot::RELATED] = Value::Ref(related);
278    attributes[connects_slot::REALIZING] = realizing.map_or(Value::Null, Value::Ref);
279    Ok(tx.create(Entity::new("IFCRELCONNECTSPORTS", attributes)))
280}
281
282/// Stage an `IfcRelContainedInSpatialStructure`.
283///
284/// Containment is exclusive: an element belongs to exactly one
285/// structure. Use [`reference_in_spatial_structure`] for the
286/// non-exclusive case, such as a duct crossing several storeys.
287///
288/// Writes `OwnerHistory` `$`, so the record is IFC4/IFC4X3 only; use
289/// [`contain_in_spatial_structure_with_owner_history`] for IFC2X3.
290///
291/// # Errors
292///
293/// Refuses a malformed GlobalId, an empty element list, and the
294/// structure listed among its own contents.
295pub fn contain_in_spatial_structure(
296    tx: &mut Transaction,
297    global_id: &str,
298    structure: EntityId,
299    elements: &[EntityId],
300) -> SystemAuthoringResult<EntityId> {
301    place(
302        tx,
303        "IFCRELCONTAINEDINSPATIALSTRUCTURE",
304        global_id,
305        structure,
306        elements,
307    )
308}
309
310/// Stage an `IfcRelReferencedInSpatialStructure`.
311///
312/// The non-exclusive counterpart of containment.
313///
314/// Writes `OwnerHistory` `$`, so the record is IFC4/IFC4X3 only; use
315/// [`reference_in_spatial_structure_with_owner_history`] for IFC2X3.
316///
317/// # Errors
318///
319/// Refuses a malformed GlobalId, an empty element list, and the
320/// structure listed among its own references.
321pub fn reference_in_spatial_structure(
322    tx: &mut Transaction,
323    global_id: &str,
324    structure: EntityId,
325    elements: &[EntityId],
326) -> SystemAuthoringResult<EntityId> {
327    place(
328        tx,
329        "IFCRELREFERENCEDINSPATIALSTRUCTURE",
330        global_id,
331        structure,
332        elements,
333    )
334}
335
336/// Both placement relationships share a layout: elements at 4,
337/// structure at 5 -- the inverse of IfcRelAggregates.
338fn place(
339    tx: &mut Transaction,
340    entity: &'static str,
341    global_id: &str,
342    structure: EntityId,
343    elements: &[EntityId],
344) -> SystemAuthoringResult<EntityId> {
345    check_related(
346        entity,
347        "RelatedElements",
348        global_id,
349        structure,
350        elements,
351        "structure",
352    )?;
353    let width = placement_slot::RELATING_STRUCTURE.max(placement_slot::RELATED_ELEMENTS) + 1;
354    let mut attributes = vec![Value::Null; width];
355    attributes[0] = Value::Text(global_id.into());
356    attributes[placement_slot::RELATED_ELEMENTS] = refs(elements);
357    attributes[placement_slot::RELATING_STRUCTURE] = Value::Ref(structure);
358    Ok(tx.create(Entity::new(entity, attributes)))
359}
360
361/// Stage an `IfcGroup`: an arbitrary named collection.
362///
363/// A group is the supertype a system specialises. Where an
364/// `IfcSystem` claims its members function together, a plain group
365/// claims only that someone gathered them, so this writer is what to
366/// reach for when no stronger statement is true.
367///
368/// Writes `OwnerHistory` `$`, so the record is IFC4/IFC4X3 only; use
369/// [`create_group_with_owner_history`] for IFC2X3.
370///
371/// # Errors
372///
373/// Refuses a malformed GlobalId.
374pub fn create_group(
375    tx: &mut Transaction,
376    global_id: &str,
377    name: Option<&str>,
378    description: Option<&str>,
379) -> SystemAuthoringResult<EntityId> {
380    guid("IFCGROUP", global_id)?;
381    let mut attributes = vec![Value::Null; 5];
382    attributes[0] = Value::Text(global_id.into());
383    attributes[2] = name.map_or(Value::Null, |t| Value::Text(t.into()));
384    attributes[3] = description.map_or(Value::Null, |t| Value::Text(t.into()));
385    Ok(tx.create(Entity::new("IFCGROUP", attributes)))
386}