Skip to main content

ifc_spatial/authoring/
mod.rs

1//! Transactional authoring of the spatial structure.
2//!
3//! # Slot layouts are not repeated here
4//!
5//! IfcRelAggregates and IfcRelContainedInSpatialStructure disagree
6//! about which slot holds the parent. The reader already encodes
7//! that in relation::slots; authoring resolves the same constants
8//! so a correction cannot update one side and leave the other.
9
10use ifc_model::guid::Guid;
11use ifc_model::{Entity, EntityId, Transaction, Value};
12
13mod external;
14
15pub use external::{
16    create_external_spatial_element, create_project_library, ExternalSpatialDraft,
17    ProjectLibraryDraft,
18};
19
20use crate::relation::slots::{RelSlots, AGGREGATES, CONTAINED_IN};
21
22mod boundary;
23mod relationships;
24
25pub use boundary::{connect_path_elements, create_space_boundary, BoundaryDraft, BoundaryLevel};
26
27use crate::tree::SpatialKind;
28pub use relationships::{
29    adhere_to_element, assign_to_actor, assign_to_group_by_factor, assign_to_process,
30    assign_to_product, assign_to_resource, associate_profile_def, connect_elements,
31    connect_with_realizing_elements, control_flow_element, cover_elements, cover_spaces, declare,
32    define_by_object, fill_element, interfere_elements, position_products, project_element,
33    serve_buildings, void_element,
34};
35
36/// Why a spatial record was refused.
37#[derive(Debug, Clone, PartialEq, Eq)]
38#[non_exhaustive]
39pub enum SpatialAuthoringError {
40    /// A value the schema constrains was not acceptable.
41    Invalid {
42        /// The entity being authored.
43        entity: &'static str,
44        /// The attribute at fault.
45        attribute: &'static str,
46        /// What was supplied.
47        value: String,
48    },
49}
50
51impl std::fmt::Display for SpatialAuthoringError {
52    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
53        let Self::Invalid {
54            entity,
55            attribute,
56            value,
57        } = self;
58        write!(f, "{entity}.{attribute}: {value}")
59    }
60}
61
62impl std::error::Error for SpatialAuthoringError {}
63
64/// Result of staging a spatial record.
65pub type SpatialAuthoringResult<T> = Result<T, SpatialAuthoringError>;
66
67pub(crate) fn invalid(
68    entity: &'static str,
69    attribute: &'static str,
70    value: impl Into<String>,
71) -> SpatialAuthoringError {
72    SpatialAuthoringError::Invalid {
73        entity,
74        attribute,
75        value: value.into(),
76    }
77}
78
79/// Authored fields shared by the spatial containers.
80#[derive(Debug, Clone, Copy, Default)]
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}
93
94/// Stage a spatial container.
95///
96/// `IfcSite`, `IfcBuilding`, `IfcBuildingStorey` and `IfcSpace` share
97/// the `IfcSpatialStructureElement` prefix, so one staging path
98/// serves all four and cannot drift between them.
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    if Guid::parse(global_id).is_none() {
111        return Err(invalid("IFCSPATIALSTRUCTUREELEMENT", "GlobalId", global_id));
112    }
113    // Each kind declares its own attribute count: writing Site width
114    // onto a Storey would leave trailing slots the schema does not
115    // define for it.
116    let (type_name, width) = match kind {
117        SpatialKind::Site => ("IFCSITE", 14),
118        SpatialKind::Building => ("IFCBUILDING", 12),
119        SpatialKind::Storey => ("IFCBUILDINGSTOREY", 10),
120        SpatialKind::Space => ("IFCSPACE", 11),
121        // Project is an IfcContext with a different layout; the other
122        // variants the reader uses to classify are not containers this
123        // function can author.
124        other => {
125            return Err(invalid(
126                "IFCSPATIALSTRUCTUREELEMENT",
127                "kind",
128                format!("{other:?}"),
129            ))
130        }
131    };
132    let mut attributes = vec![Value::Null; width];
133    attributes[0] = Value::Text(global_id.into());
134    attributes[2] = optional_text(draft.name);
135    attributes[3] = optional_text(draft.description);
136    attributes[5] = draft.placement.map_or(Value::Null, Value::Ref);
137    attributes[7] = optional_text(draft.long_name);
138    attributes[8] = draft
139        .composition
140        .map_or(Value::Null, |t| Value::Enum(t.into()));
141    Ok(tx.create(Entity::new(type_name, attributes)))
142}
143
144pub(crate) fn optional_text(value: Option<&str>) -> Value {
145    value.map_or(Value::Null, |t| Value::Text(t.into()))
146}
147
148/// Stage an `IfcProject`.
149///
150/// Kept separate from the containers: IfcProject is an IfcContext,
151/// not an IfcSpatialStructureElement, so slots 5 and up mean
152/// different things and sharing the path would misplace them.
153///
154/// # Errors
155///
156/// Refuses a malformed GlobalId.
157pub fn create_project(
158    tx: &mut Transaction,
159    global_id: &str,
160    name: Option<&str>,
161    units: Option<EntityId>,
162) -> SpatialAuthoringResult<EntityId> {
163    if Guid::parse(global_id).is_none() {
164        return Err(invalid("IFCPROJECT", "GlobalId", global_id));
165    }
166    let mut attributes = vec![Value::Null; 9];
167    attributes[0] = Value::Text(global_id.into());
168    attributes[2] = optional_text(name);
169    attributes[8] = units.map_or(Value::Null, Value::Ref);
170    Ok(tx.create(Entity::new("IFCPROJECT", attributes)))
171}
172
173/// Stage a relationship using the slot layout the reader resolves.
174///
175/// `parent` always goes to the slot the reader treats as the
176/// containing end, whichever index that is for this relationship.
177fn relate(
178    tx: &mut Transaction,
179    rel: RelSlots,
180    global_id: &str,
181    parent: EntityId,
182    children: &[EntityId],
183) -> SpatialAuthoringResult<EntityId> {
184    if Guid::parse(global_id).is_none() {
185        return Err(invalid(rel.type_name, "GlobalId", global_id));
186    }
187    if children.is_empty() {
188        return Err(invalid(rel.type_name, "RelatedObjects", "empty"));
189    }
190    if children.contains(&parent) {
191        return Err(invalid(
192            rel.type_name,
193            "RelatedObjects",
194            "contains the parent",
195        ));
196    }
197    let width = rel.relating.max(rel.related) + 1;
198    let mut attributes = vec![Value::Null; width];
199    attributes[0] = Value::Text(global_id.into());
200    attributes[rel.relating] = Value::Ref(parent);
201    attributes[rel.related] = Value::List(children.iter().copied().map(Value::Ref).collect());
202    Ok(tx.create(Entity::new(rel.type_name, attributes)))
203}
204
205/// Stage a relationship whose related end is a single reference.
206///
207/// `relate` writes a `SET` to the related slot. Three of the feature
208/// relationships take exactly one element there, and a one-element list
209/// is not the same value: a reader resolving `RelatedOpeningElement`
210/// expects a reference, not a list holding one.
211fn relate_one(
212    tx: &mut Transaction,
213    rel: RelSlots,
214    global_id: &str,
215    relating: EntityId,
216    related: EntityId,
217) -> SpatialAuthoringResult<EntityId> {
218    if Guid::parse(global_id).is_none() {
219        return Err(invalid(rel.type_name, "GlobalId", global_id));
220    }
221    if relating == related {
222        return Err(invalid(
223            rel.type_name,
224            "RelatedElement",
225            "is the relating element",
226        ));
227    }
228    let width = rel.relating.max(rel.related) + 1;
229    let mut attributes = vec![Value::Null; width];
230    attributes[0] = Value::Text(global_id.into());
231    attributes[rel.relating] = Value::Ref(relating);
232    attributes[rel.related] = Value::Ref(related);
233    Ok(tx.create(Entity::new(rel.type_name, attributes)))
234}
235
236/// Stage an `IfcRelAggregates`: a container decomposed into parts.
237///
238/// # Errors
239///
240/// Refuses a malformed GlobalId, an empty child list, and a parent
241/// listed among its own children.
242pub fn aggregate(
243    tx: &mut Transaction,
244    global_id: &str,
245    parent: EntityId,
246    children: &[EntityId],
247) -> SpatialAuthoringResult<EntityId> {
248    relate(tx, AGGREGATES, global_id, parent, children)
249}
250
251/// Stage an `IfcRelContainedInSpatialStructure`.
252///
253/// Note the slot inversion against IfcRelAggregates: here the
254/// structure is slot 5 and the elements slot 4. Passing `structure`
255/// as the parent keeps callers from having to know that.
256///
257/// # Errors
258///
259/// Refuses a malformed GlobalId, an empty element list, and a
260/// structure listed among its own contents.
261pub fn contain(
262    tx: &mut Transaction,
263    global_id: &str,
264    structure: EntityId,
265    elements: &[EntityId],
266) -> SpatialAuthoringResult<EntityId> {
267    relate(tx, CONTAINED_IN, global_id, structure, elements)
268}