Skip to main content

ifc_spatial/authoring/
external.rs

1//! Staging the external spatial element and the project library.
2//!
3//! # Two records that are not spatial containers
4//!
5//! `IfcExternalSpatialElement` is a product: it carries a placement
6//! and a representation, and describes space *outside* the building
7//! envelope -- the air a facade radiates into, the earth a
8//! foundation sits in. `IfcProjectLibrary` is an `IfcContext`,
9//! sharing `IfcProject`'s tail rather than a container's.
10//!
11//! Neither is an `IfcSpatialStructureElement`, so neither can use
12//! the container path: slot 5 onward means something different in
13//! each of the three shapes and sharing one path would file a
14//! placement as a long name.
15
16use ifc_model::guid::Guid;
17use ifc_model::{Entity, EntityId, Transaction, Value};
18
19use super::{invalid, optional_text, SpatialAuthoringResult};
20
21/// `IfcExternalSpatialElementTypeEnum`.
22///
23/// Closed: a token outside it names a kind of exterior space the
24/// schema does not define.
25const EXTERNAL_KIND: &[&str] = &[
26    "EXTERNAL",
27    "EXTERNAL_EARTH",
28    "EXTERNAL_FIRE",
29    "EXTERNAL_WATER",
30    "USERDEFINED",
31    "NOTDEFINED",
32];
33
34/// Attributes of an `IfcExternalSpatialElement`.
35#[derive(Debug, Clone, Copy, Default)]
36pub struct ExternalSpatialDraft<'a> {
37    /// `Name`.
38    pub name: Option<&'a str>,
39    /// `Description`.
40    pub description: Option<&'a str>,
41    /// `ObjectType`. Required when `predefined_type` is `USERDEFINED`.
42    pub object_type: Option<&'a str>,
43    /// `ObjectPlacement`.
44    pub placement: Option<EntityId>,
45    /// `Representation`.
46    pub representation: Option<EntityId>,
47    /// `LongName`.
48    pub long_name: Option<&'a str>,
49    /// `PredefinedType`, an `IfcExternalSpatialElementTypeEnum` token.
50    pub predefined_type: Option<&'a str>,
51}
52
53/// Stage an `IfcExternalSpatialElement`.
54///
55/// IFC4 and IFC4X3 only: it writes their layout and leaves
56/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
57/// [`create_external_spatial_element_with_owner_history`](super::create_external_spatial_element_with_owner_history), which binds the model's declared
58/// release.
59///
60/// # Errors
61///
62/// Refuses a malformed GlobalId, a token outside
63/// `IfcExternalSpatialElementTypeEnum`, and `USERDEFINED` without
64/// `ObjectType`.
65pub fn create_external_spatial_element(
66    tx: &mut Transaction,
67    global_id: &str,
68    draft: ExternalSpatialDraft<'_>,
69) -> SpatialAuthoringResult<EntityId> {
70    check_external(global_id, &draft)?;
71    let mut attributes = vec![Value::Null; 9];
72    attributes[0] = Value::Text(global_id.into());
73    attributes[2] = optional_text(draft.name);
74    attributes[3] = optional_text(draft.description);
75    attributes[4] = optional_text(draft.object_type);
76    attributes[5] = draft.placement.map_or(Value::Null, Value::Ref);
77    attributes[6] = draft.representation.map_or(Value::Null, Value::Ref);
78    attributes[7] = optional_text(draft.long_name);
79    attributes[8] = draft
80        .predefined_type
81        .map_or(Value::Null, |t| Value::Enum(t.into()));
82    Ok(tx.create(Entity::new(EXTERNAL, attributes)))
83}
84
85pub(super) const EXTERNAL: &str = "IFCEXTERNALSPATIALELEMENT";
86
87/// The checks of [`create_external_spatial_element`].
88pub(super) fn check_external(
89    global_id: &str,
90    draft: &ExternalSpatialDraft<'_>,
91) -> SpatialAuthoringResult<()> {
92    if Guid::parse(global_id).is_none() {
93        return Err(invalid(EXTERNAL, "GlobalId", global_id));
94    }
95    if let Some(token) = draft.predefined_type {
96        if !EXTERNAL_KIND.contains(&token) {
97            return Err(invalid(EXTERNAL, "PredefinedType", token));
98        }
99        if token == "USERDEFINED"
100            && draft
101                .object_type
102                .is_none_or(|value| value.trim().is_empty())
103        {
104            return Err(invalid(EXTERNAL, "ObjectType", "required by USERDEFINED"));
105        }
106    }
107    Ok(())
108}
109
110/// Attributes of an `IfcProjectLibrary`.
111#[derive(Debug, Clone, Copy, Default)]
112pub struct ProjectLibraryDraft<'a> {
113    /// `Name`.
114    pub name: Option<&'a str>,
115    /// `Description`.
116    pub description: Option<&'a str>,
117    /// `ObjectType`.
118    pub object_type: Option<&'a str>,
119    /// `LongName`.
120    pub long_name: Option<&'a str>,
121    /// `Phase`.
122    pub phase: Option<&'a str>,
123    /// `UnitsInContext`, an `IfcUnitAssignment`.
124    pub units: Option<EntityId>,
125}
126
127/// Stage an `IfcProjectLibrary`.
128///
129/// A library holds shared definitions -- types, properties,
130/// materials -- for reuse across projects. It is an `IfcContext`
131/// like `IfcProject`, not a container, so `RepresentationContexts`
132/// and `UnitsInContext` sit where a container keeps its placement.
133///
134/// `RepresentationContexts` is `SET [1:?]`: absent is legal,
135/// present and empty is not.
136///
137/// IFC4 and IFC4X3 only: it writes their layout and leaves
138/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
139/// [`create_project_library_with_owner_history`](super::create_project_library_with_owner_history), which binds the model's declared
140/// release.
141///
142/// # Errors
143///
144/// Refuses a malformed GlobalId and an empty representation-context set.
145pub fn create_project_library(
146    tx: &mut Transaction,
147    global_id: &str,
148    draft: ProjectLibraryDraft<'_>,
149    representation_contexts: &[EntityId],
150) -> SpatialAuthoringResult<EntityId> {
151    const ENTITY: &str = "IFCPROJECTLIBRARY";
152    if Guid::parse(global_id).is_none() {
153        return Err(invalid(ENTITY, "GlobalId", global_id));
154    }
155    let mut attributes = vec![Value::Null; 9];
156    attributes[0] = Value::Text(global_id.into());
157    attributes[2] = optional_text(draft.name);
158    attributes[3] = optional_text(draft.description);
159    attributes[4] = optional_text(draft.object_type);
160    attributes[5] = optional_text(draft.long_name);
161    attributes[6] = optional_text(draft.phase);
162    if !representation_contexts.is_empty() {
163        attributes[7] = Value::List(
164            representation_contexts
165                .iter()
166                .copied()
167                .map(Value::Ref)
168                .collect(),
169        );
170    }
171    attributes[8] = draft.units.map_or(Value::Null, Value::Ref);
172    Ok(tx.create(Entity::new(ENTITY, attributes)))
173}