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