1use crate::error::{GeometryError, GeometryResult};
8use crate::select::subtype::is_a;
9use ifc_model::{Entity, EntityId, Model, Value};
10
11fn classify<T>(
13 model: &Model,
14 referrer: EntityId,
15 target: EntityId,
16 expected: &'static str,
17 f: impl Fn(&str) -> Option<T>,
18) -> GeometryResult<T> {
19 let entity: &Entity = model.get(target).ok_or(GeometryError::MissingEntity {
20 referrer,
21 missing: target,
22 })?;
23 f(&entity.type_name).ok_or_else(|| GeometryError::WrongEntityType {
24 entity: target,
25 actual: entity.type_name.to_string(),
26 expected,
27 })
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CurveOnSurface {
34 Composite(EntityId),
36 PCurve(EntityId),
38 SurfaceCurve(EntityId),
40}
41
42impl CurveOnSurface {
43 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
45 classify(model, referrer, target, "IfcCurveOnSurface", |t| {
46 if is_a(t, "IFCCOMPOSITECURVEONSURFACE") {
49 Some(Self::Composite(target))
50 } else if is_a(t, "IFCPCURVE") {
51 Some(Self::PCurve(target))
52 } else if is_a(t, "IFCSURFACECURVE") {
53 Some(Self::SurfaceCurve(target))
54 } else {
55 None
56 }
57 })
58 }
59
60 pub fn id(&self) -> EntityId {
62 match self {
63 Self::Composite(id) | Self::PCurve(id) | Self::SurfaceCurve(id) => *id,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum CurveOrEdgeCurve {
75 Bounded(EntityId),
77 EdgeCurve(EntityId),
79}
80
81impl CurveOrEdgeCurve {
82 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
84 classify(model, referrer, target, "IfcCurveOrEdgeCurve", |t| {
85 if is_a(t, "IFCEDGECURVE") {
86 Some(Self::EdgeCurve(target))
87 } else if is_a(t, "IFCBOUNDEDCURVE") {
88 Some(Self::Bounded(target))
89 } else {
90 None
91 }
92 })
93 }
94
95 pub fn id(&self) -> EntityId {
97 match self {
98 Self::Bounded(id) | Self::EdgeCurve(id) => *id,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum GeometricSetSelect {
106 Curve(EntityId),
108 Point(EntityId),
110 Surface(EntityId),
112}
113
114impl GeometricSetSelect {
115 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
117 classify(model, referrer, target, "IfcGeometricSetSelect", |t| {
118 if is_a(t, "IFCCURVE") {
119 Some(Self::Curve(target))
120 } else if is_a(t, "IFCPOINT") {
121 Some(Self::Point(target))
122 } else if is_a(t, "IFCSURFACE") {
123 Some(Self::Surface(target))
124 } else {
125 None
126 }
127 })
128 }
129
130 pub fn id(&self) -> EntityId {
132 match self {
133 Self::Curve(id) | Self::Point(id) | Self::Surface(id) => *id,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum SurfaceOrFaceSurface {
142 FaceBasedSurfaceModel(EntityId),
144 FaceSurface(EntityId),
146 Surface(EntityId),
148}
149
150impl SurfaceOrFaceSurface {
151 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
153 classify(model, referrer, target, "IfcSurfaceOrFaceSurface", |t| {
154 if is_a(t, "IFCFACEBASEDSURFACEMODEL") {
155 Some(Self::FaceBasedSurfaceModel(target))
156 } else if is_a(t, "IFCFACESURFACE") {
157 Some(Self::FaceSurface(target))
158 } else if is_a(t, "IFCSURFACE") {
159 Some(Self::Surface(target))
160 } else {
161 None
162 }
163 })
164 }
165
166 pub fn id(&self) -> EntityId {
168 match self {
169 Self::FaceBasedSurfaceModel(id) | Self::FaceSurface(id) | Self::Surface(id) => *id,
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum PointOrVertexPoint {
177 Point(EntityId),
179 VertexPoint(EntityId),
181}
182
183impl PointOrVertexPoint {
184 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
186 classify(model, referrer, target, "IfcPointOrVertexPoint", |t| {
187 if is_a(t, "IFCVERTEXPOINT") {
188 Some(Self::VertexPoint(target))
189 } else if is_a(t, "IFCPOINT") {
190 Some(Self::Point(target))
191 } else {
192 None
193 }
194 })
195 }
196
197 pub fn id(&self) -> EntityId {
199 match self {
200 Self::Point(id) | Self::VertexPoint(id) => *id,
201 }
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum GridPlacementDirectionSelect {
209 Direction(EntityId),
211 GridIntersection(EntityId),
213}
214
215impl GridPlacementDirectionSelect {
216 pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
218 classify(
219 model,
220 referrer,
221 target,
222 "IfcGridPlacementDirectionSelect",
223 |t| {
224 if is_a(t, "IFCVIRTUALGRIDINTERSECTION") {
225 Some(Self::GridIntersection(target))
226 } else if is_a(t, "IFCDIRECTION") {
227 Some(Self::Direction(target))
228 } else {
229 None
230 }
231 },
232 )
233 }
234
235 pub fn id(&self) -> EntityId {
237 match self {
238 Self::Direction(id) | Self::GridIntersection(id) => *id,
239 }
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct LineIndex(Vec<usize>);
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub struct ArcIndex([usize; 3]);
259
260#[derive(Debug, Clone, PartialEq, Eq)]
262pub enum SegmentIndexSelect {
263 Line(LineIndex),
265 Arc(ArcIndex),
267}
268
269impl LineIndex {
270 pub fn as_zero_based(&self) -> &[usize] {
272 &self.0
273 }
274
275 pub fn len(&self) -> usize {
277 self.0.len()
278 }
279
280 pub fn is_empty(&self) -> bool {
282 self.0.is_empty()
283 }
284}
285
286impl ArcIndex {
287 pub fn as_zero_based(&self) -> [usize; 3] {
289 self.0
290 }
291}
292
293impl SegmentIndexSelect {
294 pub fn from_value(entity: EntityId, value: &Value) -> GeometryResult<Self> {
301 let (tag, inner) = match value {
302 Value::Typed { type_name, value } => (type_name.to_ascii_uppercase(), value.as_ref()),
303 _ => {
304 return Err(GeometryError::Degenerate {
305 entity,
306 type_name: "IFCSEGMENTINDEXSELECT".into(),
307 detail: "segment is not tagged IFCLINEINDEX or IFCARCINDEX, so its \
308 kind cannot be determined"
309 .into(),
310 })
311 }
312 };
313
314 let raw = inner.as_list().ok_or_else(|| GeometryError::Degenerate {
315 entity,
316 type_name: tag.clone(),
317 detail: "segment index is not a list".into(),
318 })?;
319
320 let mut indices = Vec::with_capacity(raw.len());
322 for value in raw {
323 let n = match value.unwrap_typed() {
324 Value::Integer(i) => *i,
325 other => {
326 return Err(GeometryError::Degenerate {
327 entity,
328 type_name: tag.clone(),
329 detail: format!("segment index is not an integer: {other:?}"),
330 })
331 }
332 };
333 if n < 1 {
334 return Err(GeometryError::Degenerate {
335 entity,
336 type_name: tag.clone(),
337 detail: format!("index {n} is not a positive integer (EXPRESS is 1-based)"),
338 });
339 }
340 indices.push((n - 1) as usize);
341 }
342
343 match tag.as_str() {
344 "IFCARCINDEX" => {
345 let three: [usize; 3] =
346 indices
347 .as_slice()
348 .try_into()
349 .map_err(|_| GeometryError::Degenerate {
350 entity,
351 type_name: tag.clone(),
352 detail: format!(
353 "IfcArcIndex requires exactly 3 indices, found {}",
354 indices.len()
355 ),
356 })?;
357 Ok(Self::Arc(ArcIndex(three)))
358 }
359 "IFCLINEINDEX" => {
360 if indices.len() < 2 {
361 return Err(GeometryError::Degenerate {
362 entity,
363 type_name: tag,
364 detail: format!(
365 "IfcLineIndex requires at least 2 indices, found {}",
366 indices.len()
367 ),
368 });
369 }
370 Ok(Self::Line(LineIndex(indices)))
371 }
372 other => Err(GeometryError::Degenerate {
373 entity,
374 type_name: other.to_string(),
375 detail: "not a member of IfcSegmentIndexSelect".into(),
376 }),
377 }
378 }
379
380 pub fn indices(&self) -> Vec<usize> {
382 match self {
383 Self::Line(l) => l.0.clone(),
384 Self::Arc(a) => a.0.to_vec(),
385 }
386 }
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
394pub struct DimensionCount(u8);
395
396impl DimensionCount {
397 pub fn new(count: i64) -> Option<Self> {
399 (1..=3).contains(&count).then_some(Self(count as u8))
400 }
401
402 pub fn get(&self) -> usize {
404 self.0 as usize
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 fn model_with(id: u64, type_name: &str) -> Model {
413 let mut model = Model::new();
414 model.insert(EntityId(id), Entity::new(type_name, vec![Value::Null; 4]));
415 model
416 }
417
418 fn tagged(tag: &str, values: &[i64]) -> Value {
419 Value::Typed {
420 type_name: tag.into(),
421 value: Box::new(Value::List(
422 values.iter().map(|v| Value::Integer(*v)).collect(),
423 )),
424 }
425 }
426
427 #[test]
429 fn one_based_express_indices_become_zero_based_exactly_once() {
430 let seg = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCLINEINDEX", &[1, 2, 3]))
431 .unwrap();
432 assert_eq!(
433 seg.indices(),
434 vec![0, 1, 2],
435 "IfcLineIndex((1,2,3)) addresses points 0,1,2"
436 );
437 }
438
439 #[test]
440 fn arc_indices_require_exactly_three_points() {
441 let ok = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCARCINDEX", &[3, 4, 5]));
442 assert_eq!(ok.unwrap().indices(), vec![2, 3, 4]);
443
444 let short = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCARCINDEX", &[3, 4]));
445 assert!(short.is_err(), "two indices cannot define an arc");
446 }
447
448 #[test]
449 fn line_indices_require_at_least_two_points() {
450 assert!(
451 SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCLINEINDEX", &[7])).is_err()
452 );
453 }
454
455 #[test]
457 fn a_zero_index_is_rejected_rather_than_underflowing() {
458 let err = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCLINEINDEX", &[0, 1]))
459 .unwrap_err();
460 assert!(err.to_string().contains("1-based"), "got {err}");
461 }
462
463 #[test]
465 fn an_untagged_segment_is_rejected_rather_than_guessed() {
466 let bare = Value::List(vec![Value::Integer(1), Value::Integer(2)]);
467 assert!(SegmentIndexSelect::from_value(EntityId(1), &bare).is_err());
468 }
469
470 #[test]
471 fn dimension_count_enforces_its_where_rule() {
472 assert_eq!(DimensionCount::new(3).map(|d| d.get()), Some(3));
473 assert_eq!(DimensionCount::new(1).map(|d| d.get()), Some(1));
474 assert_eq!(DimensionCount::new(0), None, "0 < SELF is required");
475 assert_eq!(DimensionCount::new(4), None, "SELF <= 3 is required");
476 assert_eq!(DimensionCount::new(-1), None);
477 }
478
479 #[test]
480 fn geometric_set_members_classify_by_family() {
481 let curve = model_with(5, "IFCPOLYLINE");
482 assert_eq!(
483 GeometricSetSelect::resolve(&curve, EntityId(1), EntityId(5)).unwrap(),
484 GeometricSetSelect::Curve(EntityId(5))
485 );
486 let surface = model_with(5, "IFCPLANE");
487 assert_eq!(
488 GeometricSetSelect::resolve(&surface, EntityId(1), EntityId(5)).unwrap(),
489 GeometricSetSelect::Surface(EntityId(5))
490 );
491 let point = model_with(5, "IFCCARTESIANPOINT");
492 assert_eq!(
493 GeometricSetSelect::resolve(&point, EntityId(1), EntityId(5)).unwrap(),
494 GeometricSetSelect::Point(EntityId(5))
495 );
496 }
497
498 #[test]
501 fn curve_on_surface_picks_the_most_derived_branch() {
502 let model = model_with(5, "IFCCOMPOSITECURVEONSURFACE");
503 assert_eq!(
504 CurveOnSurface::resolve(&model, EntityId(1), EntityId(5)).unwrap(),
505 CurveOnSurface::Composite(EntityId(5))
506 );
507 }
508 #[test]
513 fn a_face_surface_is_not_classified_as_a_plain_surface() {
514 let model = model_with(1, "IFCFACESURFACE");
515 let resolved = SurfaceOrFaceSurface::resolve(&model, EntityId(9), EntityId(1))
516 .expect("face surface resolves");
517 assert_eq!(resolved, SurfaceOrFaceSurface::FaceSurface(EntityId(1)));
518
519 let plain = model_with(2, "IFCPLANE");
520 let resolved = SurfaceOrFaceSurface::resolve(&plain, EntityId(9), EntityId(2))
521 .expect("plane resolves");
522 assert_eq!(resolved, SurfaceOrFaceSurface::Surface(EntityId(2)));
523 }
524
525 #[test]
527 fn a_non_surface_is_rejected_by_the_surface_select() {
528 let model = model_with(1, "IFCPOLYLINE");
529 let error = SurfaceOrFaceSurface::resolve(&model, EntityId(9), EntityId(1))
530 .expect_err("a curve is not a surface");
531 assert_eq!(error.entity(), Some(EntityId(1)));
532 }
533
534 #[test]
536 fn a_vertex_point_keeps_its_topological_identity() {
537 let model = model_with(1, "IFCVERTEXPOINT");
538 let resolved = PointOrVertexPoint::resolve(&model, EntityId(9), EntityId(1))
539 .expect("vertex point resolves");
540 assert_eq!(resolved, PointOrVertexPoint::VertexPoint(EntityId(1)));
541
542 let plain = model_with(2, "IFCCARTESIANPOINT");
543 let resolved = PointOrVertexPoint::resolve(&plain, EntityId(9), EntityId(2))
544 .expect("cartesian point resolves");
545 assert_eq!(resolved, PointOrVertexPoint::Point(EntityId(2)));
546 }
547
548 #[test]
550 fn an_edge_curve_is_not_classified_as_a_bounded_curve() {
551 let model = model_with(1, "IFCEDGECURVE");
552 let resolved = CurveOrEdgeCurve::resolve(&model, EntityId(9), EntityId(1))
553 .expect("edge curve resolves");
554 assert_eq!(resolved, CurveOrEdgeCurve::EdgeCurve(EntityId(1)));
555
556 let plain = model_with(2, "IFCPOLYLINE");
557 let resolved =
558 CurveOrEdgeCurve::resolve(&plain, EntityId(9), EntityId(2)).expect("polyline resolves");
559 assert_eq!(resolved, CurveOrEdgeCurve::Bounded(EntityId(2)));
560 }
561
562 #[test]
564 fn a_surface_is_rejected_by_the_curve_select() {
565 let model = model_with(1, "IFCPLANE");
566 let error = CurveOrEdgeCurve::resolve(&model, EntityId(9), EntityId(1))
567 .expect_err("a plane is not a curve");
568 assert_eq!(error.entity(), Some(EntityId(1)));
569 }
570}