1use 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#[derive(Debug, Clone, PartialEq, Eq)]
38#[non_exhaustive]
39pub enum SpatialAuthoringError {
40 Invalid {
42 entity: &'static str,
44 attribute: &'static str,
46 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
64pub 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#[derive(Debug, Clone, Copy, Default)]
81pub struct SpatialDraft<'a> {
82 pub name: Option<&'a str>,
84 pub description: Option<&'a str>,
86 pub long_name: Option<&'a str>,
88 pub composition: Option<&'a str>,
90 pub placement: Option<EntityId>,
92}
93
94pub 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 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 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
148pub 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
173fn 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
205fn 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
236pub 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
251pub 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}