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)]
26pub enum BoundaryLevel {
27 Base,
29 First,
31 Second,
33}
34
35impl BoundaryLevel {
36 fn type_name(self) -> &'static str {
38 match self {
39 Self::Base => "IFCRELSPACEBOUNDARY",
40 Self::First => "IFCRELSPACEBOUNDARY1STLEVEL",
41 Self::Second => "IFCRELSPACEBOUNDARY2NDLEVEL",
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy)]
48pub struct BoundaryDraft<'a> {
49 pub name: Option<&'a str>,
51 pub description: Option<&'a str>,
53 pub space: EntityId,
55 pub element: EntityId,
57 pub connection_geometry: Option<EntityId>,
59 pub physical_or_virtual: &'a str,
61 pub internal_or_external: &'a str,
63 pub parent: Option<EntityId>,
65 pub corresponding: Option<EntityId>,
67}
68
69const PHYS_OR_VIRT: &[&str] = &["PHYSICAL", "VIRTUAL", "NOTDEFINED"];
70const INT_OR_EXT: &[&str] = &[
71 "INTERNAL",
72 "EXTERNAL",
73 "EXTERNAL_EARTH",
74 "EXTERNAL_WATER",
75 "EXTERNAL_FIRE",
76 "NOTDEFINED",
77];
78
79fn type_name<'a>(tx: &'a Transaction, model: &'a Model, id: EntityId) -> Option<&'a str> {
81 tx.edits()
82 .iter()
83 .rev()
84 .find_map(|edit| match edit {
85 Edit::Create { id: staged, entity } if *staged == id => Some(entity.type_name.as_ref()),
86 _ => None,
87 })
88 .or_else(|| model.get(id).map(|entity| entity.type_name.as_ref()))
89}
90
91pub fn create_space_boundary(
112 tx: &mut Transaction,
113 model: &Model,
114 level: BoundaryLevel,
115 global_id: &str,
116 draft: BoundaryDraft<'_>,
117) -> SpatialAuthoringResult<EntityId> {
118 let values = boundary_values(tx, model, level, global_id, draft)?;
119 stage(tx, model, level.type_name(), values, None)
120}
121
122pub fn create_space_boundary_with_owner_history(
132 tx: &mut Transaction,
133 model: &Model,
134 level: BoundaryLevel,
135 global_id: &str,
136 draft: BoundaryDraft<'_>,
137 owner_history: EntityId,
138) -> SpatialAuthoringResult<EntityId> {
139 let values = boundary_values(tx, model, level, global_id, draft)?;
140 stage(tx, model, level.type_name(), values, Some(owner_history))
141}
142
143fn boundary_values(
145 tx: &Transaction,
146 model: &Model,
147 level: BoundaryLevel,
148 global_id: &str,
149 draft: BoundaryDraft<'_>,
150) -> SpatialAuthoringResult<Vec<(&'static str, Value)>> {
151 let entity = level.type_name();
152 if Guid::parse(global_id).is_none() {
153 return Err(invalid(entity, "GlobalId", global_id));
154 }
155 let physical = draft.physical_or_virtual.to_ascii_uppercase();
156 if !PHYS_OR_VIRT.contains(&physical.as_str()) {
157 return Err(invalid(
158 entity,
159 "PhysicalOrVirtualBoundary",
160 draft.physical_or_virtual,
161 ));
162 }
163 let internal = draft.internal_or_external.to_ascii_uppercase();
164 if !INT_OR_EXT.contains(&internal.as_str()) {
165 return Err(invalid(
166 entity,
167 "InternalOrExternalBoundary",
168 draft.internal_or_external,
169 ));
170 }
171
172 let bounded_by = type_name(tx, model, draft.element)
177 .map(str::to_ascii_uppercase)
178 .unwrap_or_default();
179 let is_virtual = bounded_by == "IFCVIRTUALELEMENT";
180 let is_opening = bounded_by == "IFCOPENINGELEMENT";
181 let agrees = match physical.as_str() {
182 "PHYSICAL" => !is_virtual,
183 "VIRTUAL" => is_virtual || is_opening,
184 _ => true,
185 };
186 if !agrees {
187 return Err(invalid(
188 entity,
189 "PhysicalOrVirtualBoundary",
190 format!("{physical} does not agree with {bounded_by}"),
191 ));
192 }
193
194 if draft.parent.is_some() && level == BoundaryLevel::Base {
197 return Err(invalid(
198 entity,
199 "ParentBoundary",
200 "not declared at this level",
201 ));
202 }
203 if draft.corresponding.is_some() && level != BoundaryLevel::Second {
204 return Err(invalid(
205 entity,
206 "CorrespondingBoundary",
207 "not declared at this level",
208 ));
209 }
210
211 let text = |value: Option<&str>| value.map_or(Value::Null, |t| Value::Text(t.into()));
212 let reference = |value: Option<EntityId>| value.map_or(Value::Null, Value::Ref);
213 Ok(vec![
214 ("GlobalId", Value::Text(global_id.into())),
215 ("Name", text(draft.name)),
216 ("Description", text(draft.description)),
217 ("RelatingSpace", Value::Ref(draft.space)),
218 ("RelatedBuildingElement", Value::Ref(draft.element)),
219 ("ConnectionGeometry", reference(draft.connection_geometry)),
220 ("PhysicalOrVirtualBoundary", Value::Enum(physical.into())),
221 ("InternalOrExternalBoundary", Value::Enum(internal.into())),
222 ("ParentBoundary", reference(draft.parent)),
223 ("CorrespondingBoundary", reference(draft.corresponding)),
224 ])
225}
226
227const CONNECTION_TYPE: &[&str] = &["ATPATH", "ATSTART", "ATEND", "NOTDEFINED"];
228const PATH: &str = "IFCRELCONNECTSPATHELEMENTS";
229
230pub fn connect_path_elements(
247 tx: &mut Transaction,
248 global_id: &str,
249 relating: EntityId,
250 related: EntityId,
251 priorities: (&[i64], &[i64]),
252 connection_types: (&str, &str),
253) -> SpatialAuthoringResult<EntityId> {
254 let (relating_token, related_token) =
255 check_path(global_id, relating, related, priorities, connection_types)?;
256 let (relating_priorities, related_priorities) = priorities;
257 let mut attributes = vec![Value::Null; 11];
258 attributes[0] = Value::Text(global_id.into());
259 attributes[5] = Value::Ref(relating);
260 attributes[6] = Value::Ref(related);
261 attributes[7] = integers(relating_priorities);
262 attributes[8] = integers(related_priorities);
263 attributes[9] = Value::Enum(related_token.into());
264 attributes[10] = Value::Enum(relating_token.into());
265 Ok(tx.create(Entity::new(PATH, attributes)))
266}
267
268#[allow(clippy::too_many_arguments)]
278pub fn connect_path_elements_with_owner_history(
279 tx: &mut Transaction,
280 model: &Model,
281 global_id: &str,
282 relating: EntityId,
283 related: EntityId,
284 priorities: (&[i64], &[i64]),
285 connection_types: (&str, &str),
286 owner_history: EntityId,
287) -> SpatialAuthoringResult<EntityId> {
288 let (relating_token, related_token) =
289 check_path(global_id, relating, related, priorities, connection_types)?;
290 let (relating_priorities, related_priorities) = priorities;
291 let values = vec![
292 ("GlobalId", Value::Text(global_id.into())),
293 ("RelatingElement", Value::Ref(relating)),
294 ("RelatedElement", Value::Ref(related)),
295 ("RelatingPriorities", integers(relating_priorities)),
296 ("RelatedPriorities", integers(related_priorities)),
297 ("RelatedConnectionType", Value::Enum(related_token.into())),
298 ("RelatingConnectionType", Value::Enum(relating_token.into())),
299 ];
300 stage(tx, model, PATH, values, Some(owner_history))
301}
302
303fn integers(values: &[i64]) -> Value {
304 Value::List(values.iter().copied().map(Value::Integer).collect())
305}
306
307fn check_path(
310 global_id: &str,
311 relating: EntityId,
312 related: EntityId,
313 priorities: (&[i64], &[i64]),
314 connection_types: (&str, &str),
315) -> SpatialAuthoringResult<(String, String)> {
316 if Guid::parse(global_id).is_none() {
317 return Err(invalid(PATH, "GlobalId", global_id));
318 }
319 if relating == related {
320 return Err(invalid(PATH, "RelatedElement", "is the relating element"));
321 }
322 let (relating_priorities, related_priorities) = priorities;
323 for (values, attribute) in [
324 (relating_priorities, "RelatingPriorities"),
325 (related_priorities, "RelatedPriorities"),
326 ] {
327 if let Some(out) = values.iter().find(|value| !(0..=100).contains(*value)) {
328 return Err(invalid(PATH, attribute, out.to_string()));
329 }
330 }
331 let (relating_type, related_type) = connection_types;
332 let relating_token = relating_type.to_ascii_uppercase();
333 let related_token = related_type.to_ascii_uppercase();
334 for (token, attribute) in [
335 (&relating_token, "RelatingConnectionType"),
336 (&related_token, "RelatedConnectionType"),
337 ] {
338 if !CONNECTION_TYPE.contains(&token.as_str()) {
339 return Err(invalid(PATH, attribute, token.clone()));
340 }
341 }
342 Ok((relating_token, related_token))
343}