Skip to main content

ifc_spatial/authoring/
mod.rs

1//! Transactional authoring of the spatial structure.
2//!
3//! # Owner history and the declared release (#202)
4//!
5//! `IfcRoot.OwnerHistory` is required in IFC2X3 and optional from IFC4 on.
6//! The writers that take no model cannot see the release: they write the
7//! IFC4/IFC4X3 layout with `OwnerHistory` `$` and are IFC4/IFC4X3 only.
8//! Every one has a `*_with_owner_history` variant that binds the model's
9//! declared release, lays the record out by attribute name from its table,
10//! and takes a caller-supplied `IfcOwnerHistory`; none is ever invented.
11//! [`create_space_boundary`], which takes the model, binds the release too
12//! and refuses IFC2X3 without an owner history.
13//!
14//! # Slot layouts are not repeated here
15//!
16//! IfcRelAggregates and IfcRelContainedInSpatialStructure disagree
17//! about which slot holds the parent. The reader already encodes
18//! that in relation::slots; authoring resolves the same constants
19//! so a correction cannot update one side and leave the other.
20
21use ifc_model::guid::Guid;
22use ifc_model::{Entity, EntityId, Transaction, Value};
23mod connections;
24mod error;
25mod external;
26mod owned;
27mod owned_relationships;
28pub(crate) mod release;
29
30pub(crate) use error::invalid;
31pub use error::{SpatialAuthoringError, SpatialAuthoringResult};
32
33pub use external::{
34    create_external_spatial_element, create_project_library, ExternalSpatialDraft,
35    ProjectLibraryDraft,
36};
37
38use crate::relation::slots::{RelSlots, AGGREGATES, CONTAINED_IN};
39
40mod boundary;
41mod relationships;
42
43pub use boundary::{
44    connect_path_elements, connect_path_elements_with_owner_history, create_space_boundary,
45    create_space_boundary_with_owner_history, BoundaryDraft, BoundaryLevel,
46};
47pub use owned::{
48    aggregate_with_owner_history, contain_with_owner_history,
49    create_external_spatial_element_with_owner_history, create_project_library_with_owner_history,
50    create_project_with_owner_history, create_spatial_element_with_owner_history,
51};
52pub use owned_relationships::{
53    adhere_to_element_with_owner_history, assign_to_actor_with_owner_history,
54    assign_to_group_by_factor_with_owner_history, assign_to_process_with_owner_history,
55    assign_to_product_with_owner_history, assign_to_resource_with_owner_history,
56    associate_profile_def_with_owner_history, connect_elements_with_owner_history,
57    connect_with_realizing_elements_with_owner_history, control_flow_element_with_owner_history,
58    cover_elements_with_owner_history, cover_spaces_with_owner_history, declare_with_owner_history,
59    define_by_object_with_owner_history, fill_element_with_owner_history,
60    interfere_elements_with_owner_history, position_products_with_owner_history,
61    project_element_with_owner_history, serve_buildings_with_owner_history,
62    void_element_with_owner_history,
63};
64
65use crate::tree::SpatialKind;
66pub use connections::{connect_elements, connect_with_realizing_elements, interfere_elements};
67pub use relationships::{
68    adhere_to_element, assign_to_actor, assign_to_group_by_factor, assign_to_process,
69    assign_to_product, assign_to_resource, associate_profile_def, control_flow_element,
70    cover_elements, cover_spaces, declare, define_by_object, fill_element, position_products,
71    project_element, serve_buildings, void_element,
72};
73
74/// Authored fields shared by the spatial containers.
75#[derive(Debug, Clone, Copy, Default)]
76pub struct SpatialDraft<'a> {
77    /// `IfcRoot.Name`.
78    pub name: Option<&'a str>,
79    /// `IfcRoot.Description`.
80    pub description: Option<&'a str>,
81    /// `IfcSpatialStructureElement.LongName`.
82    pub long_name: Option<&'a str>,
83    /// `CompositionType`, an `IfcElementCompositionEnum` token.
84    pub composition: Option<&'a str>,
85    /// `ObjectPlacement`, when the container is placed.
86    pub placement: Option<EntityId>,
87}
88
89/// Stage a spatial container.
90///
91/// `IfcSite`, `IfcBuilding`, `IfcBuildingStorey` and `IfcSpace` share
92/// the `IfcSpatialStructureElement` prefix, so one staging path
93/// serves all four and cannot drift between them.
94///
95/// IFC4 and IFC4X3 only: it writes their layout and leaves
96/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
97/// [`create_spatial_element_with_owner_history`], which binds the model's declared
98/// release.
99///
100/// # Errors
101///
102/// Refuses a malformed GlobalId. IfcRoot.GlobalId is required and
103/// is how every relationship names this container.
104pub fn create_spatial_element(
105    tx: &mut Transaction,
106    kind: SpatialKind,
107    global_id: &str,
108    draft: SpatialDraft<'_>,
109) -> SpatialAuthoringResult<EntityId> {
110    let (type_name, width) = container(kind, global_id)?;
111    let mut attributes = vec![Value::Null; width];
112    attributes[0] = Value::Text(global_id.into());
113    attributes[2] = optional_text(draft.name);
114    attributes[3] = optional_text(draft.description);
115    attributes[5] = draft.placement.map_or(Value::Null, Value::Ref);
116    attributes[7] = optional_text(draft.long_name);
117    attributes[8] = draft
118        .composition
119        .map_or(Value::Null, |t| Value::Enum(t.into()));
120    Ok(tx.create(Entity::new(type_name, attributes)))
121}
122
123/// The entity and IFC4 attribute count `kind` stages, after checking the
124/// GlobalId.
125fn container(kind: SpatialKind, global_id: &str) -> SpatialAuthoringResult<(&'static str, usize)> {
126    if Guid::parse(global_id).is_none() {
127        return Err(invalid("IFCSPATIALSTRUCTUREELEMENT", "GlobalId", global_id));
128    }
129    // Each kind declares its own attribute count: writing Site width
130    // onto a Storey would leave trailing slots the schema does not
131    // define for it.
132    Ok(match kind {
133        SpatialKind::Site => ("IFCSITE", 14),
134        SpatialKind::Building => ("IFCBUILDING", 12),
135        SpatialKind::Storey => ("IFCBUILDINGSTOREY", 10),
136        SpatialKind::Space => ("IFCSPACE", 11),
137        // Project is an IfcContext with a different layout; the other
138        // variants the reader uses to classify are not containers this
139        // function can author.
140        other => {
141            return Err(invalid(
142                "IFCSPATIALSTRUCTUREELEMENT",
143                "kind",
144                format!("{other:?}"),
145            ))
146        }
147    })
148}
149
150pub(crate) fn optional_text(value: Option<&str>) -> Value {
151    value.map_or(Value::Null, |t| Value::Text(t.into()))
152}
153
154/// Stage an `IfcProject`.
155///
156/// Kept separate from the containers: IfcProject is an IfcContext,
157/// not an IfcSpatialStructureElement, so slots 5 and up mean
158/// different things and sharing the path would misplace them.
159///
160/// IFC4 and IFC4X3 only: it writes their layout and leaves
161/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
162/// [`create_project_with_owner_history`], which binds the model's declared
163/// release.
164///
165/// # Errors
166///
167/// Refuses a malformed GlobalId.
168pub fn create_project(
169    tx: &mut Transaction,
170    global_id: &str,
171    name: Option<&str>,
172    units: Option<EntityId>,
173) -> SpatialAuthoringResult<EntityId> {
174    if Guid::parse(global_id).is_none() {
175        return Err(invalid("IFCPROJECT", "GlobalId", global_id));
176    }
177    let mut attributes = vec![Value::Null; 9];
178    attributes[0] = Value::Text(global_id.into());
179    attributes[2] = optional_text(name);
180    attributes[8] = units.map_or(Value::Null, Value::Ref);
181    Ok(tx.create(Entity::new("IFCPROJECT", attributes)))
182}
183
184/// Stage a relationship using the slot layout the reader resolves.
185///
186/// `parent` always goes to the slot the reader treats as the
187/// containing end, whichever index that is for this relationship.
188fn relate(
189    tx: &mut Transaction,
190    rel: RelSlots,
191    global_id: &str,
192    parent: EntityId,
193    children: &[EntityId],
194) -> SpatialAuthoringResult<EntityId> {
195    check_relate(rel, global_id, parent, children)?;
196    let width = rel.relating.max(rel.related) + 1;
197    let mut attributes = vec![Value::Null; width];
198    attributes[0] = Value::Text(global_id.into());
199    attributes[rel.relating] = Value::Ref(parent);
200    attributes[rel.related] = Value::List(children.iter().copied().map(Value::Ref).collect());
201    Ok(tx.create(Entity::new(rel.type_name, attributes)))
202}
203
204/// Stage a relationship whose related end is a single reference.
205///
206/// `relate` writes a `SET` to the related slot. Three of the feature
207/// relationships take exactly one element there, and a one-element list
208/// is not the same value: a reader resolving `RelatedOpeningElement`
209/// expects a reference, not a list holding one.
210fn relate_one(
211    tx: &mut Transaction,
212    rel: RelSlots,
213    global_id: &str,
214    relating: EntityId,
215    related: EntityId,
216) -> SpatialAuthoringResult<EntityId> {
217    check_relate_one(rel, global_id, relating, related)?;
218    let width = rel.relating.max(rel.related) + 1;
219    let mut attributes = vec![Value::Null; width];
220    attributes[0] = Value::Text(global_id.into());
221    attributes[rel.relating] = Value::Ref(relating);
222    attributes[rel.related] = Value::Ref(related);
223    Ok(tx.create(Entity::new(rel.type_name, attributes)))
224}
225
226/// The checks of [`relate`]: a GlobalId, a non-empty child set, and no
227/// parent among its own children.
228fn check_relate(
229    rel: RelSlots,
230    global_id: &str,
231    parent: EntityId,
232    children: &[EntityId],
233) -> SpatialAuthoringResult<()> {
234    if Guid::parse(global_id).is_none() {
235        return Err(invalid(rel.type_name, "GlobalId", global_id));
236    }
237    if children.is_empty() {
238        return Err(invalid(rel.type_name, "RelatedObjects", "empty"));
239    }
240    if children.contains(&parent) {
241        return Err(invalid(
242            rel.type_name,
243            "RelatedObjects",
244            "contains the parent",
245        ));
246    }
247    Ok(())
248}
249
250/// The checks of [`relate_one`]: a GlobalId and two distinct ends.
251fn check_relate_one(
252    rel: RelSlots,
253    global_id: &str,
254    relating: EntityId,
255    related: EntityId,
256) -> SpatialAuthoringResult<()> {
257    if Guid::parse(global_id).is_none() {
258        return Err(invalid(rel.type_name, "GlobalId", global_id));
259    }
260    if relating == related {
261        return Err(invalid(
262            rel.type_name,
263            "RelatedElement",
264            "is the relating element",
265        ));
266    }
267    Ok(())
268}
269
270/// Stage an `IfcRelAggregates`: a container decomposed into parts.
271///
272/// IFC4 and IFC4X3 only: it writes their layout and leaves
273/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
274/// [`aggregate_with_owner_history`], which binds the model's declared
275/// release.
276///
277/// # Errors
278///
279/// Refuses a malformed GlobalId, an empty child list, and a parent
280/// listed among its own children.
281pub fn aggregate(
282    tx: &mut Transaction,
283    global_id: &str,
284    parent: EntityId,
285    children: &[EntityId],
286) -> SpatialAuthoringResult<EntityId> {
287    relate(tx, AGGREGATES, global_id, parent, children)
288}
289
290/// Stage an `IfcRelContainedInSpatialStructure`.
291///
292/// Note the slot inversion against IfcRelAggregates: here the
293/// structure is slot 5 and the elements slot 4. Passing `structure`
294/// as the parent keeps callers from having to know that.
295///
296/// IFC4 and IFC4X3 only: it writes their layout and leaves
297/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
298/// [`contain_with_owner_history`], which binds the model's declared
299/// release.
300///
301/// # Errors
302///
303/// Refuses a malformed GlobalId, an empty element list, and a
304/// structure listed among its own contents.
305pub fn contain(
306    tx: &mut Transaction,
307    global_id: &str,
308    structure: EntityId,
309    elements: &[EntityId],
310) -> SpatialAuthoringResult<EntityId> {
311    relate(tx, CONTAINED_IN, global_id, structure, elements)
312}