1use crate::error::{GeometryError, GeometryResult};
11use crate::select::subtype::is_a;
12use ifc_model::{Entity, EntityId, Model, Value};
13
14fn not_a_member(entity: EntityId, actual: &str, expected: &'static str) -> GeometryError {
16 GeometryError::WrongEntityType {
17 entity,
18 actual: actual.to_string(),
19 expected,
20 }
21}
22
23fn resolve_and_classify<T>(
28 model: &Model,
29 referrer: EntityId,
30 target: EntityId,
31 expected: &'static str,
32 f: impl Fn(&str) -> Option<T>,
33) -> GeometryResult<T> {
34 let entity: &Entity = model.get(target).ok_or(GeometryError::MissingEntity {
35 referrer,
36 missing: target,
37 })?;
38 f(&entity.type_name).ok_or_else(|| not_a_member(target, &entity.type_name, expected))
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Axis2Placement {
48 TwoD(EntityId),
50 ThreeD(EntityId),
52}
53
54impl Axis2Placement {
55 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
57 resolve_and_classify(model, referrer, target, "IfcAxis2Placement", |t| {
58 if is_a(t, "IFCAXIS2PLACEMENT2D") {
59 Some(Self::TwoD(target))
60 } else if is_a(t, "IFCAXIS2PLACEMENT3D") {
61 Some(Self::ThreeD(target))
62 } else {
63 None
64 }
65 })
66 }
67
68 pub fn id(&self) -> EntityId {
70 match self {
71 Self::TwoD(id) | Self::ThreeD(id) => *id,
72 }
73 }
74
75 pub fn dimension(&self) -> usize {
77 match self {
78 Self::TwoD(_) => 2,
79 Self::ThreeD(_) => 3,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum BooleanOperand {
95 Solid(EntityId),
97 HalfSpace(EntityId),
99 BooleanResult(EntityId),
101 CsgPrimitive(EntityId),
103 TessellatedFaceSet(EntityId),
105}
106
107impl BooleanOperand {
108 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
110 resolve_and_classify(model, referrer, target, "IfcBooleanOperand", |t| {
111 if is_a(t, "IFCHALFSPACESOLID") {
115 Some(Self::HalfSpace(target))
116 } else if is_a(t, "IFCBOOLEANRESULT") {
117 Some(Self::BooleanResult(target))
118 } else if is_a(t, "IFCCSGPRIMITIVE3D") {
119 Some(Self::CsgPrimitive(target))
120 } else if is_a(t, "IFCTESSELLATEDFACESET") {
121 Some(Self::TessellatedFaceSet(target))
122 } else if is_a(t, "IFCSOLIDMODEL") {
123 Some(Self::Solid(target))
124 } else {
125 None
126 }
127 })
128 }
129
130 pub fn id(&self) -> EntityId {
132 match self {
133 Self::Solid(id)
134 | Self::HalfSpace(id)
135 | Self::BooleanResult(id)
136 | Self::CsgPrimitive(id)
137 | Self::TessellatedFaceSet(id) => *id,
138 }
139 }
140
141 pub fn is_unbounded(&self) -> bool {
143 matches!(self, Self::HalfSpace(_))
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum CsgSelect {
150 BooleanResult(EntityId),
152 Primitive(EntityId),
154}
155
156impl CsgSelect {
157 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
159 resolve_and_classify(model, referrer, target, "IfcCsgSelect", |t| {
160 if is_a(t, "IFCBOOLEANRESULT") {
161 Some(Self::BooleanResult(target))
162 } else if is_a(t, "IFCCSGPRIMITIVE3D") {
163 Some(Self::Primitive(target))
164 } else {
165 None
166 }
167 })
168 }
169
170 pub fn id(&self) -> EntityId {
172 match self {
173 Self::BooleanResult(id) | Self::Primitive(id) => *id,
174 }
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum SolidOrShell {
181 ClosedShell(EntityId),
183 Solid(EntityId),
185}
186
187impl SolidOrShell {
188 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
190 resolve_and_classify(model, referrer, target, "IfcSolidOrShell", |t| {
191 if is_a(t, "IFCCLOSEDSHELL") {
192 Some(Self::ClosedShell(target))
193 } else if is_a(t, "IFCSOLIDMODEL") {
194 Some(Self::Solid(target))
195 } else {
196 None
197 }
198 })
199 }
200
201 pub fn id(&self) -> EntityId {
203 match self {
204 Self::ClosedShell(id) | Self::Solid(id) => *id,
205 }
206 }
207}
208
209#[derive(Debug, Clone, PartialEq)]
218pub struct TrimmingSelect {
219 pub point: Option<EntityId>,
221 pub parameter: Option<f64>,
223}
224
225impl TrimmingSelect {
226 pub fn from_value(value: &Value) -> Self {
231 let mut out = Self {
232 point: None,
233 parameter: None,
234 };
235 let items: Vec<&Value> = match value {
236 Value::List(items) => items.iter().collect(),
237 single => vec![single],
238 };
239 for item in items {
240 match item {
241 Value::Ref(id) => out.point = Some(*id),
242 other => {
243 if let Some(n) = other.unwrap_typed().as_f64() {
244 out.parameter = Some(n);
245 }
246 }
247 }
248 }
249 out
250 }
251
252 pub fn is_empty(&self) -> bool {
254 self.point.is_none() && self.parameter.is_none()
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum VectorOrDirection {
264 Direction(EntityId),
266 Vector(EntityId),
268}
269
270impl VectorOrDirection {
271 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
273 resolve_and_classify(model, referrer, target, "IfcVectorOrDirection", |t| {
274 if is_a(t, "IFCVECTOR") {
275 Some(Self::Vector(target))
276 } else if is_a(t, "IFCDIRECTION") {
277 Some(Self::Direction(target))
278 } else {
279 None
280 }
281 })
282 }
283
284 pub fn id(&self) -> EntityId {
286 match self {
287 Self::Direction(id) | Self::Vector(id) => *id,
288 }
289 }
290
291 pub fn has_magnitude(&self) -> bool {
293 matches!(self, Self::Vector(_))
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 fn model_with(id: u64, type_name: &str) -> Model {
302 let mut model = Model::new();
303 model.insert(EntityId(id), Entity::new(type_name, vec![Value::Null; 4]));
304 model
305 }
306
307 #[test]
309 fn a_concrete_solid_resolves_as_the_abstract_solid_model_branch() {
310 let model = model_with(5, "IFCEXTRUDEDAREASOLID");
311 let operand = BooleanOperand::resolve(&model, EntityId(1), EntityId(5)).unwrap();
312 assert_eq!(operand, BooleanOperand::Solid(EntityId(5)));
313 assert!(!operand.is_unbounded());
314 }
315
316 #[test]
318 fn half_space_operands_are_flagged_unbounded() {
319 for t in ["IFCHALFSPACESOLID", "IFCPOLYGONALBOUNDEDHALFSPACE"] {
320 let model = model_with(5, t);
321 let operand = BooleanOperand::resolve(&model, EntityId(1), EntityId(5)).unwrap();
322 assert!(operand.is_unbounded(), "{t} must be unbounded");
323 }
324 }
325
326 #[test]
327 fn nested_boolean_results_are_their_own_branch() {
328 let model = model_with(5, "IFCBOOLEANCLIPPINGRESULT");
329 assert_eq!(
330 BooleanOperand::resolve(&model, EntityId(1), EntityId(5)).unwrap(),
331 BooleanOperand::BooleanResult(EntityId(5))
332 );
333 }
334
335 #[test]
336 fn an_entity_outside_the_select_is_rejected_with_its_actual_type() {
337 let model = model_with(5, "IFCWALL");
338 let err = BooleanOperand::resolve(&model, EntityId(1), EntityId(5)).unwrap_err();
339 assert!(err.to_string().contains("IFCWALL"), "got {err}");
340 }
341
342 #[test]
343 fn a_dangling_reference_is_reported_as_missing_not_as_wrong_type() {
344 let model = Model::new();
345 assert!(matches!(
346 BooleanOperand::resolve(&model, EntityId(1), EntityId(99)).unwrap_err(),
347 GeometryError::MissingEntity { .. }
348 ));
349 }
350
351 #[test]
352 fn placement_dimension_comes_from_the_target_type() {
353 let two = model_with(5, "IFCAXIS2PLACEMENT2D");
354 assert_eq!(
355 Axis2Placement::resolve(&two, EntityId(1), EntityId(5))
356 .unwrap()
357 .dimension(),
358 2
359 );
360 let three = model_with(5, "IFCAXIS2PLACEMENT3D");
361 assert_eq!(
362 Axis2Placement::resolve(&three, EntityId(1), EntityId(5))
363 .unwrap()
364 .dimension(),
365 3
366 );
367 }
368
369 #[test]
371 fn a_trim_can_carry_a_point_and_a_parameter_together() {
372 let trim = TrimmingSelect::from_value(&Value::List(vec![
373 Value::Ref(EntityId(7)),
374 Value::Typed {
375 type_name: "IFCPARAMETERVALUE".into(),
376 value: Box::new(Value::Real(0.75)),
377 },
378 ]));
379 assert_eq!(trim.point, Some(EntityId(7)));
380 assert_eq!(trim.parameter, Some(0.75));
381 assert!(!trim.is_empty());
382 }
383
384 #[test]
385 fn a_trim_may_be_a_bare_parameter() {
386 let trim = TrimmingSelect::from_value(&Value::Typed {
387 type_name: "IFCPARAMETERVALUE".into(),
388 value: Box::new(Value::Real(0.0)),
389 });
390 assert_eq!(trim.parameter, Some(0.0));
391 assert_eq!(trim.point, None);
392 }
393
394 #[test]
396 fn vectors_are_distinguished_from_directions() {
397 let vector = model_with(5, "IFCVECTOR");
398 assert!(
399 VectorOrDirection::resolve(&vector, EntityId(1), EntityId(5))
400 .unwrap()
401 .has_magnitude()
402 );
403
404 let direction = model_with(5, "IFCDIRECTION");
405 assert!(
406 !VectorOrDirection::resolve(&direction, EntityId(1), EntityId(5))
407 .unwrap()
408 .has_magnitude()
409 );
410 }
411
412 #[test]
413 fn solid_or_shell_separates_boundaries_from_solids() {
414 let shell = model_with(5, "IFCCLOSEDSHELL");
415 assert_eq!(
416 SolidOrShell::resolve(&shell, EntityId(1), EntityId(5)).unwrap(),
417 SolidOrShell::ClosedShell(EntityId(5))
418 );
419 let solid = model_with(5, "IFCFACETEDBREP");
420 assert_eq!(
421 SolidOrShell::resolve(&solid, EntityId(1), EntityId(5)).unwrap(),
422 SolidOrShell::Solid(EntityId(5))
423 );
424 }
425 #[test]
430 fn csg_branches_separate_nested_booleans_from_leaf_primitives() {
431 let mut model = Model::new();
432 model.insert(EntityId(1), Entity::new("IFCBOOLEANRESULT", vec![]));
433 model.insert(EntityId(2), Entity::new("IFCBLOCK", vec![]));
434 model.insert(EntityId(3), Entity::new("IFCPOLYLINE", vec![]));
435 assert_eq!(
436 CsgSelect::resolve(&model, EntityId(9), EntityId(1)).unwrap(),
437 CsgSelect::BooleanResult(EntityId(1))
438 );
439 assert_eq!(
440 CsgSelect::resolve(&model, EntityId(9), EntityId(2)).unwrap(),
441 CsgSelect::Primitive(EntityId(2)),
442 "IfcBlock is an IfcCsgPrimitive3D leaf"
443 );
444 let error = CsgSelect::resolve(&model, EntityId(9), EntityId(3))
445 .expect_err("a polyline is not a CSG node");
446 assert_eq!(error.entity(), Some(EntityId(3)));
447 }
448}