1use ifc_model::guid::Guid;
19use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
20
21use super::release::stage;
22use super::{invalid, SpatialAuthoringResult};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum BoundaryLevel {
28 Base,
30 First,
32 Second,
34}
35
36impl BoundaryLevel {
37 fn type_name(self) -> &'static str {
39 match self {
40 Self::Base => "IFCRELSPACEBOUNDARY",
41 Self::First => "IFCRELSPACEBOUNDARY1STLEVEL",
42 Self::Second => "IFCRELSPACEBOUNDARY2NDLEVEL",
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy)]
52#[non_exhaustive]
53pub struct BoundaryDraft<'a> {
54 pub name: Option<&'a str>,
56 pub description: Option<&'a str>,
58 pub space: EntityId,
60 pub element: EntityId,
62 pub connection_geometry: Option<EntityId>,
64 pub physical_or_virtual: &'a str,
66 pub internal_or_external: &'a str,
68 pub parent: Option<EntityId>,
70 pub corresponding: Option<EntityId>,
72}
73
74impl<'a> BoundaryDraft<'a> {
75 #[must_use]
79 pub fn new(
80 space: EntityId,
81 element: EntityId,
82 physical_or_virtual: &'a str,
83 internal_or_external: &'a str,
84 ) -> Self {
85 Self {
86 name: None,
87 description: None,
88 space,
89 element,
90 connection_geometry: None,
91 physical_or_virtual,
92 internal_or_external,
93 parent: None,
94 corresponding: None,
95 }
96 }
97
98 #[must_use]
100 pub fn name(mut self, value: &'a str) -> Self {
101 self.name = Some(value);
102 self
103 }
104
105 #[must_use]
107 pub fn description(mut self, value: &'a str) -> Self {
108 self.description = Some(value);
109 self
110 }
111
112 #[must_use]
114 pub fn connection_geometry(mut self, value: EntityId) -> Self {
115 self.connection_geometry = Some(value);
116 self
117 }
118
119 #[must_use]
121 pub fn parent(mut self, value: EntityId) -> Self {
122 self.parent = Some(value);
123 self
124 }
125
126 #[must_use]
128 pub fn corresponding(mut self, value: EntityId) -> Self {
129 self.corresponding = Some(value);
130 self
131 }
132}
133
134const PHYS_OR_VIRT: &[&str] = &["PHYSICAL", "VIRTUAL", "NOTDEFINED"];
135const INT_OR_EXT: &[&str] = &[
136 "INTERNAL",
137 "EXTERNAL",
138 "EXTERNAL_EARTH",
139 "EXTERNAL_WATER",
140 "EXTERNAL_FIRE",
141 "NOTDEFINED",
142];
143
144fn type_name<'a>(tx: &'a Transaction, model: &'a Model, id: EntityId) -> Option<&'a str> {
146 tx.edits()
147 .iter()
148 .rev()
149 .find_map(|edit| match edit {
150 Edit::Create { id: staged, entity } if *staged == id => Some(entity.type_name.as_ref()),
151 _ => None,
152 })
153 .or_else(|| model.get(id).map(|entity| entity.type_name.as_ref()))
154}
155
156pub fn create_space_boundary(
177 tx: &mut Transaction,
178 model: &Model,
179 level: BoundaryLevel,
180 global_id: &str,
181 draft: BoundaryDraft<'_>,
182) -> SpatialAuthoringResult<EntityId> {
183 let values = boundary_values(tx, model, level, global_id, draft)?;
184 stage(tx, model, level.type_name(), values, None)
185}
186
187pub fn create_space_boundary_with_owner_history(
197 tx: &mut Transaction,
198 model: &Model,
199 level: BoundaryLevel,
200 global_id: &str,
201 draft: BoundaryDraft<'_>,
202 owner_history: EntityId,
203) -> SpatialAuthoringResult<EntityId> {
204 let values = boundary_values(tx, model, level, global_id, draft)?;
205 stage(tx, model, level.type_name(), values, Some(owner_history))
206}
207
208fn boundary_values(
210 tx: &Transaction,
211 model: &Model,
212 level: BoundaryLevel,
213 global_id: &str,
214 draft: BoundaryDraft<'_>,
215) -> SpatialAuthoringResult<Vec<(&'static str, Value)>> {
216 let entity = level.type_name();
217 if Guid::parse(global_id).is_none() {
218 return Err(invalid(entity, "GlobalId", global_id));
219 }
220 let physical = draft.physical_or_virtual.to_ascii_uppercase();
221 if !PHYS_OR_VIRT.contains(&physical.as_str()) {
222 return Err(invalid(
223 entity,
224 "PhysicalOrVirtualBoundary",
225 draft.physical_or_virtual,
226 ));
227 }
228 let internal = draft.internal_or_external.to_ascii_uppercase();
229 if !INT_OR_EXT.contains(&internal.as_str()) {
230 return Err(invalid(
231 entity,
232 "InternalOrExternalBoundary",
233 draft.internal_or_external,
234 ));
235 }
236
237 let bounded_by = type_name(tx, model, draft.element)
242 .map(str::to_ascii_uppercase)
243 .unwrap_or_default();
244 let is_virtual = bounded_by == "IFCVIRTUALELEMENT";
245 let is_opening = bounded_by == "IFCOPENINGELEMENT";
246 let agrees = match physical.as_str() {
247 "PHYSICAL" => !is_virtual,
248 "VIRTUAL" => is_virtual || is_opening,
249 _ => true,
250 };
251 if !agrees {
252 return Err(invalid(
253 entity,
254 "PhysicalOrVirtualBoundary",
255 format!("{physical} does not agree with {bounded_by}"),
256 ));
257 }
258
259 if draft.parent.is_some() && level == BoundaryLevel::Base {
262 return Err(invalid(
263 entity,
264 "ParentBoundary",
265 "not declared at this level",
266 ));
267 }
268 if draft.corresponding.is_some() && level != BoundaryLevel::Second {
269 return Err(invalid(
270 entity,
271 "CorrespondingBoundary",
272 "not declared at this level",
273 ));
274 }
275
276 let text = |value: Option<&str>| value.map_or(Value::Null, |t| Value::Text(t.into()));
277 let reference = |value: Option<EntityId>| value.map_or(Value::Null, Value::Ref);
278 Ok(vec![
279 ("GlobalId", Value::Text(global_id.into())),
280 ("Name", text(draft.name)),
281 ("Description", text(draft.description)),
282 ("RelatingSpace", Value::Ref(draft.space)),
283 ("RelatedBuildingElement", Value::Ref(draft.element)),
284 ("ConnectionGeometry", reference(draft.connection_geometry)),
285 ("PhysicalOrVirtualBoundary", Value::Enum(physical.into())),
286 ("InternalOrExternalBoundary", Value::Enum(internal.into())),
287 ("ParentBoundary", reference(draft.parent)),
288 ("CorrespondingBoundary", reference(draft.corresponding)),
289 ])
290}
291
292const CONNECTION_TYPE: &[&str] = &["ATPATH", "ATSTART", "ATEND", "NOTDEFINED"];
293const PATH: &str = "IFCRELCONNECTSPATHELEMENTS";
294
295pub fn connect_path_elements(
312 tx: &mut Transaction,
313 global_id: &str,
314 relating: EntityId,
315 related: EntityId,
316 priorities: (&[i64], &[i64]),
317 connection_types: (&str, &str),
318) -> SpatialAuthoringResult<EntityId> {
319 let (relating_token, related_token) =
320 check_path(global_id, relating, related, priorities, connection_types)?;
321 let (relating_priorities, related_priorities) = priorities;
322 let mut attributes = vec![Value::Null; 11];
323 attributes[0] = Value::Text(global_id.into());
324 attributes[5] = Value::Ref(relating);
325 attributes[6] = Value::Ref(related);
326 attributes[7] = integers(relating_priorities);
327 attributes[8] = integers(related_priorities);
328 attributes[9] = Value::Enum(related_token.into());
329 attributes[10] = Value::Enum(relating_token.into());
330 Ok(tx.create(Entity::new(PATH, attributes)))
331}
332
333#[allow(clippy::too_many_arguments)]
343pub fn connect_path_elements_with_owner_history(
344 tx: &mut Transaction,
345 model: &Model,
346 global_id: &str,
347 relating: EntityId,
348 related: EntityId,
349 priorities: (&[i64], &[i64]),
350 connection_types: (&str, &str),
351 owner_history: EntityId,
352) -> SpatialAuthoringResult<EntityId> {
353 let (relating_token, related_token) =
354 check_path(global_id, relating, related, priorities, connection_types)?;
355 let (relating_priorities, related_priorities) = priorities;
356 let values = vec![
357 ("GlobalId", Value::Text(global_id.into())),
358 ("RelatingElement", Value::Ref(relating)),
359 ("RelatedElement", Value::Ref(related)),
360 ("RelatingPriorities", integers(relating_priorities)),
361 ("RelatedPriorities", integers(related_priorities)),
362 ("RelatedConnectionType", Value::Enum(related_token.into())),
363 ("RelatingConnectionType", Value::Enum(relating_token.into())),
364 ];
365 stage(tx, model, PATH, values, Some(owner_history))
366}
367
368fn integers(values: &[i64]) -> Value {
369 Value::List(values.iter().copied().map(Value::Integer).collect())
370}
371
372fn check_path(
375 global_id: &str,
376 relating: EntityId,
377 related: EntityId,
378 priorities: (&[i64], &[i64]),
379 connection_types: (&str, &str),
380) -> SpatialAuthoringResult<(String, String)> {
381 if Guid::parse(global_id).is_none() {
382 return Err(invalid(PATH, "GlobalId", global_id));
383 }
384 if relating == related {
385 return Err(invalid(PATH, "RelatedElement", "is the relating element"));
386 }
387 let (relating_priorities, related_priorities) = priorities;
388 for (values, attribute) in [
389 (relating_priorities, "RelatingPriorities"),
390 (related_priorities, "RelatedPriorities"),
391 ] {
392 if let Some(out) = values.iter().find(|value| !(0..=100).contains(*value)) {
393 return Err(invalid(PATH, attribute, out.to_string()));
394 }
395 }
396 let (relating_type, related_type) = connection_types;
397 let relating_token = relating_type.to_ascii_uppercase();
398 let related_token = related_type.to_ascii_uppercase();
399 for (token, attribute) in [
400 (&relating_token, "RelatingConnectionType"),
401 (&related_token, "RelatedConnectionType"),
402 ] {
403 if !CONNECTION_TYPE.contains(&token.as_str()) {
404 return Err(invalid(PATH, attribute, token.clone()));
405 }
406 }
407 Ok((relating_token, related_token))
408}