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///
76/// `#[non_exhaustive]`: build it with [`SpatialDraft::new`] and the
77/// setters, so a field a later release needs can be added without breaking
78/// callers.
79#[derive(Debug, Clone, Copy, Default)]
80#[non_exhaustive]
81pub struct SpatialDraft<'a> {
82    /// `IfcRoot.Name`.
83    pub name: Option<&'a str>,
84    /// `IfcRoot.Description`.
85    pub description: Option<&'a str>,
86    /// `IfcSpatialStructureElement.LongName`.
87    pub long_name: Option<&'a str>,
88    /// `CompositionType`, an `IfcElementCompositionEnum` token.
89    pub composition: Option<&'a str>,
90    /// `ObjectPlacement`, when the container is placed.
91    pub placement: Option<EntityId>,
92    /// IFC2X3 `IfcSpace.InteriorOrExteriorSpace`, an
93    /// `IfcInternalOrExternalEnum` token that release requires on a space
94    /// (#214). IFC4 and IFC4X3 do not declare it, so a value there, or on a
95    /// container other than a space, is refused rather than dropped.
96    pub interior_or_exterior: Option<&'a str>,
97}
98
99impl<'a> SpatialDraft<'a> {
100    /// An empty draft: every attribute unset.
101    #[must_use]
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Set `IfcRoot.Name`.
107    #[must_use]
108    pub fn name(mut self, value: &'a str) -> Self {
109        self.name = Some(value);
110        self
111    }
112
113    /// Set `IfcRoot.Description`.
114    #[must_use]
115    pub fn description(mut self, value: &'a str) -> Self {
116        self.description = Some(value);
117        self
118    }
119
120    /// Set `LongName`.
121    #[must_use]
122    pub fn long_name(mut self, value: &'a str) -> Self {
123        self.long_name = Some(value);
124        self
125    }
126
127    /// Set `CompositionType`, an `IfcElementCompositionEnum` token.
128    #[must_use]
129    pub fn composition(mut self, value: &'a str) -> Self {
130        self.composition = Some(value);
131        self
132    }
133
134    /// Set `ObjectPlacement`.
135    #[must_use]
136    pub fn placement(mut self, value: EntityId) -> Self {
137        self.placement = Some(value);
138        self
139    }
140
141    /// Set the IFC2X3 `IfcSpace.InteriorOrExteriorSpace` token.
142    #[must_use]
143    pub fn interior_or_exterior(mut self, value: &'a str) -> Self {
144        self.interior_or_exterior = Some(value);
145        self
146    }
147}
148
149/// The IFC2X3 `IfcSpace` attribute [`SpatialDraft::interior_or_exterior`]
150/// fills.
151pub(crate) const INTERIOR_OR_EXTERIOR: &str = "InteriorOrExteriorSpace";
152
153/// Stage a spatial container.
154///
155/// `IfcSite`, `IfcBuilding`, `IfcBuildingStorey` and `IfcSpace` share
156/// the `IfcSpatialStructureElement` prefix, so one staging path
157/// serves all four and cannot drift between them.
158///
159/// IFC4 and IFC4X3 only: it writes their layout and leaves
160/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
161/// [`create_spatial_element_with_owner_history`], which binds the model's declared
162/// release.
163///
164/// # Errors
165///
166/// Refuses a malformed GlobalId. IfcRoot.GlobalId is required and
167/// is how every relationship names this container. Refuses
168/// `interior_or_exterior` with
169/// [`AuthoringNotInSchema`](SpatialAuthoringError::AuthoringNotInSchema):
170/// only IFC2X3 declares it, and this writer cannot write IFC2X3.
171pub fn create_spatial_element(
172    tx: &mut Transaction,
173    kind: SpatialKind,
174    global_id: &str,
175    draft: SpatialDraft<'_>,
176) -> SpatialAuthoringResult<EntityId> {
177    let (type_name, width) = container(kind, global_id)?;
178    // IFC4 and IFC4X3 declare no `InteriorOrExteriorSpace`; dropping the
179    // value would lose what the caller stated.
180    if draft.interior_or_exterior.is_some() {
181        return Err(SpatialAuthoringError::AuthoringNotInSchema {
182            entity: type_name,
183            attribute: INTERIOR_OR_EXTERIOR,
184            schema: ifc_schema::SchemaVersion::Ifc4,
185        });
186    }
187    let mut attributes = vec![Value::Null; width];
188    attributes[0] = Value::Text(global_id.into());
189    attributes[2] = optional_text(draft.name);
190    attributes[3] = optional_text(draft.description);
191    attributes[5] = draft.placement.map_or(Value::Null, Value::Ref);
192    attributes[7] = optional_text(draft.long_name);
193    attributes[8] = draft
194        .composition
195        .map_or(Value::Null, |t| Value::Enum(t.into()));
196    Ok(tx.create(Entity::new(type_name, attributes)))
197}
198
199/// The entity and IFC4 attribute count `kind` stages, after checking the
200/// GlobalId.
201fn container(kind: SpatialKind, global_id: &str) -> SpatialAuthoringResult<(&'static str, usize)> {
202    if Guid::parse(global_id).is_none() {
203        return Err(invalid("IFCSPATIALSTRUCTUREELEMENT", "GlobalId", global_id));
204    }
205    // Each kind declares its own attribute count: writing Site width
206    // onto a Storey would leave trailing slots the schema does not
207    // define for it.
208    Ok(match kind {
209        SpatialKind::Site => ("IFCSITE", 14),
210        SpatialKind::Building => ("IFCBUILDING", 12),
211        SpatialKind::Storey => ("IFCBUILDINGSTOREY", 10),
212        SpatialKind::Space => ("IFCSPACE", 11),
213        // Project is an IfcContext with a different layout; the other
214        // variants the reader uses to classify are not containers this
215        // function can author.
216        other => {
217            return Err(invalid(
218                "IFCSPATIALSTRUCTUREELEMENT",
219                "kind",
220                format!("{other:?}"),
221            ))
222        }
223    })
224}
225
226pub(crate) fn optional_text(value: Option<&str>) -> Value {
227    value.map_or(Value::Null, |t| Value::Text(t.into()))
228}
229
230/// Stage an `IfcProject`.
231///
232/// Kept separate from the containers: IfcProject is an IfcContext,
233/// not an IfcSpatialStructureElement, so slots 5 and up mean
234/// different things and sharing the path would misplace them.
235///
236/// IFC4 and IFC4X3 only: it writes their layout and leaves
237/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
238/// [`create_project_with_owner_history`], which binds the model's declared
239/// release.
240///
241/// # Errors
242///
243/// Refuses a malformed GlobalId.
244pub fn create_project(
245    tx: &mut Transaction,
246    global_id: &str,
247    name: Option<&str>,
248    units: Option<EntityId>,
249) -> SpatialAuthoringResult<EntityId> {
250    if Guid::parse(global_id).is_none() {
251        return Err(invalid("IFCPROJECT", "GlobalId", global_id));
252    }
253    let mut attributes = vec![Value::Null; 9];
254    attributes[0] = Value::Text(global_id.into());
255    attributes[2] = optional_text(name);
256    attributes[8] = units.map_or(Value::Null, Value::Ref);
257    Ok(tx.create(Entity::new("IFCPROJECT", attributes)))
258}
259
260/// Stage a relationship using the slot layout the reader resolves.
261///
262/// `parent` always goes to the slot the reader treats as the
263/// containing end, whichever index that is for this relationship.
264fn relate(
265    tx: &mut Transaction,
266    rel: RelSlots,
267    global_id: &str,
268    parent: EntityId,
269    children: &[EntityId],
270) -> SpatialAuthoringResult<EntityId> {
271    check_relate(rel, global_id, parent, children)?;
272    let width = rel.relating.max(rel.related) + 1;
273    let mut attributes = vec![Value::Null; width];
274    attributes[0] = Value::Text(global_id.into());
275    attributes[rel.relating] = Value::Ref(parent);
276    attributes[rel.related] = Value::List(children.iter().copied().map(Value::Ref).collect());
277    Ok(tx.create(Entity::new(rel.type_name, attributes)))
278}
279
280/// Stage a relationship whose related end is a single reference.
281///
282/// `relate` writes a `SET` to the related slot. Three of the feature
283/// relationships take exactly one element there, and a one-element list
284/// is not the same value: a reader resolving `RelatedOpeningElement`
285/// expects a reference, not a list holding one.
286fn relate_one(
287    tx: &mut Transaction,
288    rel: RelSlots,
289    global_id: &str,
290    relating: EntityId,
291    related: EntityId,
292) -> SpatialAuthoringResult<EntityId> {
293    check_relate_one(rel, global_id, relating, related)?;
294    let width = rel.relating.max(rel.related) + 1;
295    let mut attributes = vec![Value::Null; width];
296    attributes[0] = Value::Text(global_id.into());
297    attributes[rel.relating] = Value::Ref(relating);
298    attributes[rel.related] = Value::Ref(related);
299    Ok(tx.create(Entity::new(rel.type_name, attributes)))
300}
301
302/// The checks of [`relate`]: a GlobalId, a non-empty child set, and no
303/// parent among its own children.
304fn check_relate(
305    rel: RelSlots,
306    global_id: &str,
307    parent: EntityId,
308    children: &[EntityId],
309) -> SpatialAuthoringResult<()> {
310    if Guid::parse(global_id).is_none() {
311        return Err(invalid(rel.type_name, "GlobalId", global_id));
312    }
313    if children.is_empty() {
314        return Err(invalid(rel.type_name, "RelatedObjects", "empty"));
315    }
316    if children.contains(&parent) {
317        return Err(invalid(
318            rel.type_name,
319            "RelatedObjects",
320            "contains the parent",
321        ));
322    }
323    Ok(())
324}
325
326/// The checks of [`relate_one`]: a GlobalId and two distinct ends.
327fn check_relate_one(
328    rel: RelSlots,
329    global_id: &str,
330    relating: EntityId,
331    related: EntityId,
332) -> SpatialAuthoringResult<()> {
333    if Guid::parse(global_id).is_none() {
334        return Err(invalid(rel.type_name, "GlobalId", global_id));
335    }
336    if relating == related {
337        return Err(invalid(
338            rel.type_name,
339            "RelatedElement",
340            "is the relating element",
341        ));
342    }
343    Ok(())
344}
345
346/// Stage an `IfcRelAggregates`: a container decomposed into parts.
347///
348/// IFC4 and IFC4X3 only: it writes their layout and leaves
349/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
350/// [`aggregate_with_owner_history`], which binds the model's declared
351/// release.
352///
353/// # Errors
354///
355/// Refuses a malformed GlobalId, an empty child list, and a parent
356/// listed among its own children.
357pub fn aggregate(
358    tx: &mut Transaction,
359    global_id: &str,
360    parent: EntityId,
361    children: &[EntityId],
362) -> SpatialAuthoringResult<EntityId> {
363    relate(tx, AGGREGATES, global_id, parent, children)
364}
365
366/// Stage an `IfcRelContainedInSpatialStructure`.
367///
368/// Note the slot inversion against IfcRelAggregates: here the
369/// structure is slot 5 and the elements slot 4. Passing `structure`
370/// as the parent keeps callers from having to know that.
371///
372/// IFC4 and IFC4X3 only: it writes their layout and leaves
373/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
374/// [`contain_with_owner_history`], which binds the model's declared
375/// release.
376///
377/// # Errors
378///
379/// Refuses a malformed GlobalId, an empty element list, and a
380/// structure listed among its own contents.
381pub fn contain(
382    tx: &mut Transaction,
383    global_id: &str,
384    structure: EntityId,
385    elements: &[EntityId],
386) -> SpatialAuthoringResult<EntityId> {
387    relate(tx, CONTAINED_IN, global_id, structure, elements)
388}