1use ifc_model::guid::Guid;
15use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
16
17use super::{invalid, SpatialAuthoringResult};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum BoundaryLevel {
22 Base,
24 First,
26 Second,
28}
29
30impl BoundaryLevel {
31 fn type_name(self) -> &'static str {
33 match self {
34 Self::Base => "IFCRELSPACEBOUNDARY",
35 Self::First => "IFCRELSPACEBOUNDARY1STLEVEL",
36 Self::Second => "IFCRELSPACEBOUNDARY2NDLEVEL",
37 }
38 }
39
40 fn arity(self) -> usize {
42 match self {
43 Self::Base => 9,
44 Self::First => 10,
45 Self::Second => 11,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy)]
52pub struct BoundaryDraft<'a> {
53 pub name: Option<&'a str>,
55 pub description: Option<&'a str>,
57 pub space: EntityId,
59 pub element: EntityId,
61 pub connection_geometry: Option<EntityId>,
63 pub physical_or_virtual: &'a str,
65 pub internal_or_external: &'a str,
67 pub parent: Option<EntityId>,
69 pub corresponding: Option<EntityId>,
71}
72
73const PHYS_OR_VIRT: &[&str] = &["PHYSICAL", "VIRTUAL", "NOTDEFINED"];
74const INT_OR_EXT: &[&str] = &[
75 "INTERNAL",
76 "EXTERNAL",
77 "EXTERNAL_EARTH",
78 "EXTERNAL_WATER",
79 "EXTERNAL_FIRE",
80 "NOTDEFINED",
81];
82
83fn type_name<'a>(tx: &'a Transaction, model: &'a Model, id: EntityId) -> Option<&'a str> {
85 tx.edits()
86 .iter()
87 .rev()
88 .find_map(|edit| match edit {
89 Edit::Create { id: staged, entity } if *staged == id => Some(entity.type_name.as_ref()),
90 _ => None,
91 })
92 .or_else(|| model.get(id).map(|entity| entity.type_name.as_ref()))
93}
94
95pub fn create_space_boundary(
104 tx: &mut Transaction,
105 model: &Model,
106 level: BoundaryLevel,
107 global_id: &str,
108 draft: BoundaryDraft<'_>,
109) -> SpatialAuthoringResult<EntityId> {
110 let entity = level.type_name();
111 if Guid::parse(global_id).is_none() {
112 return Err(invalid(entity, "GlobalId", global_id));
113 }
114 let physical = draft.physical_or_virtual.to_ascii_uppercase();
115 if !PHYS_OR_VIRT.contains(&physical.as_str()) {
116 return Err(invalid(
117 entity,
118 "PhysicalOrVirtualBoundary",
119 draft.physical_or_virtual,
120 ));
121 }
122 let internal = draft.internal_or_external.to_ascii_uppercase();
123 if !INT_OR_EXT.contains(&internal.as_str()) {
124 return Err(invalid(
125 entity,
126 "InternalOrExternalBoundary",
127 draft.internal_or_external,
128 ));
129 }
130
131 let bounded_by = type_name(tx, model, draft.element)
136 .map(str::to_ascii_uppercase)
137 .unwrap_or_default();
138 let is_virtual = bounded_by == "IFCVIRTUALELEMENT";
139 let is_opening = bounded_by == "IFCOPENINGELEMENT";
140 let agrees = match physical.as_str() {
141 "PHYSICAL" => !is_virtual,
142 "VIRTUAL" => is_virtual || is_opening,
143 _ => true,
144 };
145 if !agrees {
146 return Err(invalid(
147 entity,
148 "PhysicalOrVirtualBoundary",
149 format!("{physical} does not agree with {bounded_by}"),
150 ));
151 }
152
153 if draft.parent.is_some() && level == BoundaryLevel::Base {
156 return Err(invalid(
157 entity,
158 "ParentBoundary",
159 "not declared at this level",
160 ));
161 }
162 if draft.corresponding.is_some() && level != BoundaryLevel::Second {
163 return Err(invalid(
164 entity,
165 "CorrespondingBoundary",
166 "not declared at this level",
167 ));
168 }
169
170 let mut attributes = vec![Value::Null; level.arity()];
171 attributes[0] = Value::Text(global_id.into());
172 attributes[2] = draft.name.map_or(Value::Null, |t| Value::Text(t.into()));
173 attributes[3] = draft
174 .description
175 .map_or(Value::Null, |t| Value::Text(t.into()));
176 attributes[4] = Value::Ref(draft.space);
177 attributes[5] = Value::Ref(draft.element);
178 attributes[6] = draft.connection_geometry.map_or(Value::Null, Value::Ref);
179 attributes[7] = Value::Enum(physical.into());
180 attributes[8] = Value::Enum(internal.into());
181 if let Some(parent) = draft.parent {
182 attributes[9] = Value::Ref(parent);
183 }
184 if let Some(corresponding) = draft.corresponding {
185 attributes[10] = Value::Ref(corresponding);
186 }
187 Ok(tx.create(Entity::new(entity, attributes)))
188}
189
190const CONNECTION_TYPE: &[&str] = &["ATPATH", "ATSTART", "ATEND", "NOTDEFINED"];
191
192pub fn connect_path_elements(
204 tx: &mut Transaction,
205 global_id: &str,
206 relating: EntityId,
207 related: EntityId,
208 priorities: (&[i64], &[i64]),
209 connection_types: (&str, &str),
210) -> SpatialAuthoringResult<EntityId> {
211 const ENTITY: &str = "IFCRELCONNECTSPATHELEMENTS";
212 if Guid::parse(global_id).is_none() {
213 return Err(invalid(ENTITY, "GlobalId", global_id));
214 }
215 if relating == related {
216 return Err(invalid(ENTITY, "RelatedElement", "is the relating element"));
217 }
218 let (relating_priorities, related_priorities) = priorities;
219 for (values, attribute) in [
220 (relating_priorities, "RelatingPriorities"),
221 (related_priorities, "RelatedPriorities"),
222 ] {
223 if let Some(out) = values.iter().find(|value| !(0..=100).contains(*value)) {
224 return Err(invalid(ENTITY, attribute, out.to_string()));
225 }
226 }
227 let (relating_type, related_type) = connection_types;
228 let relating_token = relating_type.to_ascii_uppercase();
229 let related_token = related_type.to_ascii_uppercase();
230 for (token, attribute) in [
231 (&relating_token, "RelatingConnectionType"),
232 (&related_token, "RelatedConnectionType"),
233 ] {
234 if !CONNECTION_TYPE.contains(&token.as_str()) {
235 return Err(invalid(ENTITY, attribute, token.clone()));
236 }
237 }
238
239 let integers =
240 |values: &[i64]| Value::List(values.iter().copied().map(Value::Integer).collect());
241 let mut attributes = vec![Value::Null; 11];
242 attributes[0] = Value::Text(global_id.into());
243 attributes[5] = Value::Ref(relating);
244 attributes[6] = Value::Ref(related);
245 attributes[7] = integers(relating_priorities);
246 attributes[8] = integers(related_priorities);
247 attributes[9] = Value::Enum(related_token.into());
248 attributes[10] = Value::Enum(relating_token.into());
249 Ok(tx.create(Entity::new(ENTITY, attributes)))
250}