Skip to main content

ifc_geometry/
slots.rs

1//! Shared attribute access for typed geometry views.
2//!
3//! Every one of the 112 geometry entities reads positional attributes and must
4//! report the same way when one is missing or malformed. Without this, each
5//! view re-implements the same error construction and they drift.
6//!
7//! # The pattern
8//!
9//! A view is a newtype over `(EntityId, &Entity)`. It declares its slots as
10//! `mod slot` constants citing the EXPRESS declaration, then uses [`Slots`] to
11//! read them. Views own nothing and cost nothing to construct.
12
13use crate::error::{GeometryError, GeometryResult};
14use ifc_model::{Entity, EntityId, Model, Value};
15
16/// Attribute reader bound to one entity.
17///
18/// Carries the id and type name so every error is self-locating without the
19/// call site repeating them.
20#[derive(Debug, Clone, Copy)]
21pub struct Slots<'m> {
22    id: EntityId,
23    entity: &'m Entity,
24}
25
26impl<'m> Slots<'m> {
27    /// Wrap an entity for attribute access.
28    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
29        Self { id, entity }
30    }
31
32    /// The entity id.
33    pub fn id(&self) -> EntityId {
34        self.id
35    }
36
37    /// The underlying entity.
38    pub fn entity(&self) -> &'m Entity {
39        self.entity
40    }
41
42    /// The IFC type name.
43    pub fn type_name(&self) -> &'m str {
44        &self.entity.type_name
45    }
46
47    /// Raw attribute, `None` when absent or `$`.
48    ///
49    /// Treats a missing slot and an explicit `$` alike: both mean "not
50    /// provided", and real files disagree about which to write for trailing
51    /// optionals.
52    pub fn opt(&self, index: usize) -> Option<&'m Value> {
53        match self.entity.attribute(index) {
54            None | Some(Value::Null) => None,
55            other => other,
56        }
57    }
58
59    /// Required attribute.
60    pub fn req(&self, index: usize, name: &'static str) -> GeometryResult<&'m Value> {
61        self.opt(index)
62            .ok_or_else(|| GeometryError::MissingAttribute {
63                entity: self.id,
64                type_name: self.type_name().to_string(),
65                attribute: name,
66            })
67    }
68
69    /// Required real number, accepting an integer literal.
70    ///
71    /// STEP writes `0` where a real is declared often enough that rejecting it
72    /// would fail on conforming-in-practice files.
73    pub fn req_f64(&self, index: usize, name: &'static str) -> GeometryResult<f64> {
74        let value = self.req(index, name)?;
75        value
76            .unwrap_typed()
77            .as_f64()
78            .ok_or_else(|| self.kind_error(name, "a number", value))
79    }
80
81    /// Optional real number.
82    pub fn opt_f64(&self, index: usize) -> Option<f64> {
83        self.opt(index)?.unwrap_typed().as_f64()
84    }
85
86    /// Required integer.
87    pub fn req_i64(&self, index: usize, name: &'static str) -> GeometryResult<i64> {
88        let value = self.req(index, name)?;
89        match value.unwrap_typed() {
90            Value::Integer(i) => Ok(*i),
91            other => Err(self.kind_error(name, "an integer", other)),
92        }
93    }
94
95    /// Required entity reference.
96    pub fn req_ref(&self, index: usize, name: &'static str) -> GeometryResult<EntityId> {
97        let value = self.req(index, name)?;
98        value
99            .as_ref_id()
100            .ok_or_else(|| self.kind_error(name, "an entity reference", value))
101    }
102
103    /// Optional entity reference.
104    pub fn opt_ref(&self, index: usize) -> Option<EntityId> {
105        self.opt(index)?.as_ref_id()
106    }
107
108    /// Required list of reals, e.g. `Coordinates` on `IfcCartesianPoint`.
109    pub fn req_f64_list(&self, index: usize, name: &'static str) -> GeometryResult<Vec<f64>> {
110        let value = self.req(index, name)?;
111        let items = value
112            .as_list()
113            .ok_or_else(|| self.kind_error(name, "a list", value))?;
114        items
115            .iter()
116            .map(|v| {
117                v.unwrap_typed()
118                    .as_f64()
119                    .ok_or_else(|| self.kind_error(name, "a list of numbers", v))
120            })
121            .collect()
122    }
123
124    /// Required list of entity references.
125    pub fn req_ref_list(&self, index: usize, name: &'static str) -> GeometryResult<Vec<EntityId>> {
126        let value = self.req(index, name)?;
127        let items = value
128            .as_list()
129            .ok_or_else(|| self.kind_error(name, "a list", value))?;
130        items
131            .iter()
132            .map(|v| {
133                v.as_ref_id()
134                    .ok_or_else(|| self.kind_error(name, "a list of references", v))
135            })
136            .collect()
137    }
138
139    /// Optional list of entity references; absent becomes empty.
140    pub fn opt_ref_list(&self, index: usize) -> Vec<EntityId> {
141        self.opt(index)
142            .and_then(|v| v.as_list())
143            .map(|items| items.iter().filter_map(|v| v.as_ref_id()).collect())
144            .unwrap_or_default()
145    }
146
147    /// Enumeration token without its dots, e.g. `.CARTESIAN.` -> `CARTESIAN`.
148    /// A text attribute, absent when unset or not a string.
149    ///
150    /// Labels like RepresentationIdentifier are optional in the schema and
151    /// authors do omit them, so a missing value is data rather than an error.
152    pub fn opt_text(&self, index: usize) -> Option<String> {
153        match self.opt(index)?.unwrap_typed() {
154            Value::Text(text) => Some(text.to_string()),
155            _ => None,
156        }
157    }
158
159    /// The enumeration token at `index`, when the slot holds one.
160    ///
161    /// Returns `None` for an omitted slot and for a value that is present but
162    /// not an enumeration, so a caller cannot mistake a type error for an
163    /// authored absence.
164    pub fn opt_enum(&self, index: usize) -> Option<&'m str> {
165        match self.opt(index)? {
166            Value::Enum(e) => Some(e),
167            _ => None,
168        }
169    }
170
171    /// Boolean or logical value.
172    ///
173    /// Returns `None` for `.U.` (logical unknown), which is a real third state
174    /// in IFC and must not silently become `false`.
175    pub fn opt_bool(&self, index: usize) -> Option<bool> {
176        match self.opt(index)? {
177            Value::Bool(b) => Some(*b),
178            _ => None,
179        }
180    }
181
182    /// Required boolean.
183    pub fn req_bool(&self, index: usize, name: &'static str) -> GeometryResult<bool> {
184        let value = self.req(index, name)?;
185        match value {
186            Value::Bool(b) => Ok(*b),
187            other => Err(self.kind_error(name, "a boolean", other)),
188        }
189    }
190
191    /// Resolve a referenced entity, failing if it dangles.
192    pub fn resolve(&self, model: &'m Model, id: EntityId) -> GeometryResult<&'m Entity> {
193        model.get(id).ok_or(GeometryError::MissingEntity {
194            referrer: self.id,
195            missing: id,
196        })
197    }
198
199    /// Build a kind mismatch error naming what was actually found.
200    fn kind_error(
201        &self,
202        attribute: &'static str,
203        expected: &'static str,
204        found: &Value,
205    ) -> GeometryError {
206        GeometryError::WrongValueKind {
207            entity: self.id,
208            type_name: self.type_name().to_string(),
209            attribute,
210            expected,
211            found: describe(found),
212        }
213    }
214
215    /// Report a valid-but-unhandled entity.
216    pub fn unsupported(&self, detail: &'static str) -> GeometryError {
217        GeometryError::Unsupported {
218            entity: self.id,
219            type_name: self.type_name().to_string(),
220            detail,
221        }
222    }
223
224    /// Report geometry that cannot exist.
225    pub fn degenerate(&self, detail: impl Into<String>) -> GeometryError {
226        GeometryError::Degenerate {
227            entity: self.id,
228            type_name: self.type_name().to_string(),
229            detail: detail.into(),
230        }
231    }
232}
233
234/// Short human description of a value's kind, for error messages.
235fn describe(value: &Value) -> String {
236    match value {
237        Value::Null => "$".into(),
238        Value::Derived => "*".into(),
239        Value::Bool(b) => format!(".{}.", if *b { "T" } else { "F" }),
240        Value::LogicalUnknown => ".U.".into(),
241        Value::Integer(i) => format!("integer {i}"),
242        Value::Real(r) => format!("real {r}"),
243        Value::Text(t) => format!("text {t:?}"),
244        Value::Binary(_) => "binary".into(),
245        Value::Enum(e) => format!(".{e}."),
246        Value::Ref(id) => format!("reference {id}"),
247        Value::List(items) => format!("list of {}", items.len()),
248        Value::Typed { type_name, .. } => format!("{type_name}(...)"),
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    fn point() -> Entity {
257        Entity::new(
258            "IFCCARTESIANPOINT",
259            vec![Value::List(vec![
260                Value::Real(1.0),
261                Value::Real(2.0),
262                Value::Real(3.0),
263            ])],
264        )
265    }
266
267    #[test]
268    fn reads_a_coordinate_list() {
269        let e = point();
270        let s = Slots::new(EntityId(1), &e);
271        assert_eq!(
272            s.req_f64_list(0, "Coordinates").unwrap(),
273            vec![1.0, 2.0, 3.0]
274        );
275    }
276
277    #[test]
278    fn missing_required_attribute_names_the_entity_and_slot() {
279        let e = point();
280        let s = Slots::new(EntityId(7), &e);
281        let err = s.req(3, "Missing").unwrap_err();
282        assert!(err.to_string().contains("#7"));
283        assert!(err.to_string().contains("Missing"));
284    }
285
286    /// `$` and an absent slot mean the same thing to a consumer.
287    #[test]
288    fn explicit_null_reads_as_absent() {
289        let e = Entity::new("IFCTEST", vec![Value::Null, Value::Real(5.0)]);
290        let s = Slots::new(EntityId(1), &e);
291        assert!(s.opt(0).is_none(), "$ is absent");
292        assert!(s.opt(99).is_none(), "past the end is absent");
293        assert_eq!(s.opt_f64(1), Some(5.0));
294    }
295
296    /// A measure wrapper must not hide the number from a consumer.
297    #[test]
298    fn unwraps_typed_measures() {
299        let e = Entity::new(
300            "IFCCIRCLE",
301            vec![Value::Typed {
302                type_name: "IFCPOSITIVELENGTHMEASURE".into(),
303                value: Box::new(Value::Real(2.5)),
304            }],
305        );
306        let s = Slots::new(EntityId(1), &e);
307        assert_eq!(s.req_f64(0, "Radius").unwrap(), 2.5);
308    }
309
310    /// STEP writes `0` for a real often enough that rejecting it breaks files.
311    #[test]
312    fn integer_literal_is_accepted_where_a_real_is_declared() {
313        let e = Entity::new("IFCTEST", vec![Value::Integer(0)]);
314        let s = Slots::new(EntityId(1), &e);
315        assert_eq!(s.req_f64(0, "Depth").unwrap(), 0.0);
316    }
317
318    /// `.U.` is a third state and must not collapse into `false`.
319    #[test]
320    fn logical_unknown_is_not_false() {
321        let e = Entity::new("IFCTEST", vec![Value::LogicalUnknown]);
322        let s = Slots::new(EntityId(1), &e);
323        assert_eq!(s.opt_bool(0), None);
324    }
325
326    #[test]
327    fn wrong_kind_reports_what_was_actually_found() {
328        let e = Entity::new("IFCTEST", vec![Value::Text("nope".into())]);
329        let s = Slots::new(EntityId(1), &e);
330        let err = s.req_f64(0, "Radius").unwrap_err();
331        assert!(err.to_string().contains("text"), "got: {err}");
332    }
333}
334
335/// Attribute slots for the profile families, shared by both directions.
336///
337/// Lives here rather than in `lower/` because authoring needs the same
338/// numbers and must compile with `--no-default-features`, where `lower/` is
339/// absent (ADR 0011). One definition serves reading and writing, so a
340/// correction cannot land on one side only.
341///
342/// `IfcParameterizedProfileDef` contributes ProfileType, ProfileName and
343/// Position, so subtype attributes start at slot 3. Every index was read
344/// from the IFC4 ADD2 TC1 schema, not inferred.
345pub mod profile_slot {
346    /// `ProfileType`, `.AREA.` or `.CURVE.` -- declared by `IfcProfileDef`.
347    pub const PROFILE_TYPE: usize = 0;
348    /// `ProfileName`.
349    pub const PROFILE_NAME: usize = 1;
350    /// `Position : IfcAxis2Placement2D`.
351    pub const POSITION: usize = 2;
352    /// `IfcRectangleProfileDef.XDim`.
353    pub const X_DIM: usize = 3;
354    /// `IfcRectangleProfileDef.YDim`.
355    pub const Y_DIM: usize = 4;
356    /// `IfcCircleProfileDef.Radius`.
357    pub const RADIUS: usize = 3;
358    /// `IfcArbitraryClosedProfileDef.OuterCurve`.
359    pub const OUTER_CURVE: usize = 2;
360    /// `IfcArbitraryProfileDefWithVoids.InnerCurves`.
361    pub const INNER_CURVES: usize = 3;
362    /// `IfcCircleHollowProfileDef.WallThickness`.
363    pub const CIRCLE_WALL_THICKNESS: usize = 4;
364    /// `IfcRectangleHollowProfileDef.WallThickness`.
365    pub const RECT_WALL_THICKNESS: usize = 5;
366    /// `IfcRectangleHollowProfileDef.InnerFilletRadius`.
367    pub const RECT_INNER_RADIUS: usize = 6;
368    /// `IfcRectangleHollowProfileDef.OuterFilletRadius`.
369    pub const RECT_OUTER_RADIUS: usize = 7;
370    /// `IfcRoundedRectangleProfileDef.RoundingRadius`.
371    pub const ROUNDED_RECT_RADIUS: usize = 5;
372}
373
374/// Absolute attribute slots for the parameterized profile families.
375///
376/// Lives here, not beside the lowerer, because `lower` is behind the
377/// `lowering` feature while authoring is kernel-free: a writer that
378/// imported these from `lower` would not compile with the feature off.
379/// One definition, both directions.
380///
381/// Every index below was read from the IFC4 ADD2 TC1 schema, not inferred:
382/// `IfcParameterizedProfileDef` contributes ProfileType, ProfileName and
383/// Position, so subtype attributes start at slot 3.
384///
385/// Public because it is the schema itself, not an implementation detail:
386/// a caller assembling a profile record by hand needs the same indices.
387/// The kernel-free reader (`input::profile`) and the writers both index
388/// through these constants.
389pub mod section_slot {
390    // IfcIShapeProfileDef
391    /// `IfcIShapeProfileDef.OverallWidth`.
392    pub const I_WIDTH: usize = 3;
393    /// `IfcIShapeProfileDef.OverallDepth`.
394    pub const I_DEPTH: usize = 4;
395    /// `IfcIShapeProfileDef.WebThickness`.
396    pub const I_WEB: usize = 5;
397    /// `IfcIShapeProfileDef.FlangeThickness`.
398    pub const I_FLANGE: usize = 6;
399    /// `IfcIShapeProfileDef.FilletRadius`.
400    pub const I_FILLET: usize = 7;
401    /// `IfcIShapeProfileDef.FlangeEdgeRadius`.
402    pub const I_EDGE: usize = 8;
403    /// `IfcIShapeProfileDef.FlangeSlope`.
404    pub const I_SLOPE: usize = 9;
405
406    // IfcAsymmetricIShapeProfileDef
407    /// `IfcAsymmetricIShapeProfileDef.BottomFlangeWidth`.
408    pub const AI_BOTTOM_WIDTH: usize = 3;
409    /// `IfcAsymmetricIShapeProfileDef.OverallDepth`.
410    pub const AI_DEPTH: usize = 4;
411    /// `IfcAsymmetricIShapeProfileDef.WebThickness`.
412    pub const AI_WEB: usize = 5;
413    /// `IfcAsymmetricIShapeProfileDef.BottomFlangeThickness`.
414    pub const AI_BOTTOM_FLANGE: usize = 6;
415    /// `IfcAsymmetricIShapeProfileDef.BottomFlangeFilletRadius`.
416    pub const AI_BOTTOM_FILLET: usize = 7;
417    /// `IfcAsymmetricIShapeProfileDef.TopFlangeWidth`.
418    pub const AI_TOP_WIDTH: usize = 8;
419    /// `IfcAsymmetricIShapeProfileDef.TopFlangeThickness`.
420    pub const AI_TOP_FLANGE: usize = 9;
421    /// `IfcAsymmetricIShapeProfileDef.TopFlangeFilletRadius`.
422    pub const AI_TOP_FILLET: usize = 10;
423    /// `IfcAsymmetricIShapeProfileDef.BottomFlangeEdgeRadius`.
424    pub const AI_BOTTOM_EDGE: usize = 11;
425    /// `IfcAsymmetricIShapeProfileDef.BottomFlangeSlope`.
426    pub const AI_BOTTOM_SLOPE: usize = 12;
427    /// `IfcAsymmetricIShapeProfileDef.TopFlangeEdgeRadius`.
428    pub const AI_TOP_EDGE: usize = 13;
429    /// `IfcAsymmetricIShapeProfileDef.TopFlangeSlope`.
430    pub const AI_TOP_SLOPE: usize = 14;
431
432    // IfcLShapeProfileDef
433    /// `IfcLShapeProfileDef.Depth`.
434    pub const L_DEPTH: usize = 3;
435    /// `IfcLShapeProfileDef.Width`.
436    pub const L_WIDTH: usize = 4;
437    /// `IfcLShapeProfileDef.Thickness`.
438    pub const L_THICKNESS: usize = 5;
439    /// `IfcLShapeProfileDef.FilletRadius`.
440    pub const L_FILLET: usize = 6;
441    /// `IfcLShapeProfileDef.EdgeRadius`.
442    pub const L_EDGE: usize = 7;
443    /// `IfcLShapeProfileDef.LegSlope`.
444    pub const L_SLOPE: usize = 8;
445
446    // IfcTShapeProfileDef
447    /// `IfcTShapeProfileDef.Depth`.
448    pub const T_DEPTH: usize = 3;
449    /// `IfcTShapeProfileDef.FlangeWidth`.
450    pub const T_WIDTH: usize = 4;
451    /// `IfcTShapeProfileDef.WebThickness`.
452    pub const T_WEB: usize = 5;
453    /// `IfcTShapeProfileDef.FlangeThickness`.
454    pub const T_FLANGE: usize = 6;
455    /// `IfcTShapeProfileDef.FilletRadius`.
456    pub const T_FILLET: usize = 7;
457    /// `IfcTShapeProfileDef.FlangeEdgeRadius`.
458    pub const T_FLANGE_EDGE: usize = 8;
459    /// `IfcTShapeProfileDef.WebEdgeRadius`.
460    pub const T_WEB_EDGE: usize = 9;
461    /// `IfcTShapeProfileDef.WebSlope`.
462    pub const T_WEB_SLOPE: usize = 10;
463    /// `IfcTShapeProfileDef.FlangeSlope`.
464    pub const T_FLANGE_SLOPE: usize = 11;
465
466    // IfcUShapeProfileDef
467    /// `IfcUShapeProfileDef.Depth`.
468    pub const U_DEPTH: usize = 3;
469    /// `IfcUShapeProfileDef.FlangeWidth`.
470    pub const U_WIDTH: usize = 4;
471    /// `IfcUShapeProfileDef.WebThickness`.
472    pub const U_WEB: usize = 5;
473    /// `IfcUShapeProfileDef.FlangeThickness`.
474    pub const U_FLANGE: usize = 6;
475    /// `IfcUShapeProfileDef.FilletRadius`.
476    pub const U_FILLET: usize = 7;
477    /// `IfcUShapeProfileDef.EdgeRadius`.
478    pub const U_EDGE: usize = 8;
479    /// `IfcUShapeProfileDef.FlangeSlope`.
480    pub const U_SLOPE: usize = 9;
481
482    // IfcCShapeProfileDef
483    /// `IfcCShapeProfileDef.Depth`.
484    pub const C_DEPTH: usize = 3;
485    /// `IfcCShapeProfileDef.Width`.
486    pub const C_WIDTH: usize = 4;
487    /// `IfcCShapeProfileDef.WallThickness`.
488    pub const C_WALL: usize = 5;
489    /// `IfcCShapeProfileDef.Girth`.
490    pub const C_GIRTH: usize = 6;
491    /// `IfcCShapeProfileDef.InternalFilletRadius`.
492    pub const C_FILLET: usize = 7;
493
494    // IfcZShapeProfileDef
495    /// `IfcZShapeProfileDef.Depth`.
496    pub const Z_DEPTH: usize = 3;
497    /// `IfcZShapeProfileDef.FlangeWidth`.
498    pub const Z_FLANGE_WIDTH: usize = 4;
499    /// `IfcZShapeProfileDef.WebThickness`.
500    pub const Z_WEB: usize = 5;
501    /// `IfcZShapeProfileDef.FlangeThickness`.
502    pub const Z_FLANGE: usize = 6;
503    /// `IfcZShapeProfileDef.FilletRadius`.
504    pub const Z_FILLET: usize = 7;
505    /// `IfcZShapeProfileDef.EdgeRadius`.
506    pub const Z_EDGE: usize = 8;
507
508    // IfcEllipseProfileDef
509    /// `IfcEllipseProfileDef.SemiAxis1`.
510    pub const E_SEMI1: usize = 3;
511    /// `IfcEllipseProfileDef.SemiAxis2`.
512    pub const E_SEMI2: usize = 4;
513
514    // IfcTrapeziumProfileDef
515    /// `IfcTrapeziumProfileDef.BottomXDim`.
516    pub const TZ_BOTTOM: usize = 3;
517    /// `IfcTrapeziumProfileDef.TopXDim`.
518    pub const TZ_TOP: usize = 4;
519    /// `IfcTrapeziumProfileDef.YDim`.
520    pub const TZ_Y: usize = 5;
521    /// `IfcCenterLineProfileDef`: Curve at 2, Thickness at 3.
522    /// Thickness is the FULL width across the path, not a half-width.
523    pub const CL_CURVE: usize = 2;
524    /// Full width across the centre line.
525    pub const CL_THICKNESS: usize = 3;
526
527    /// `IfcTrapeziumProfileDef.TopXOffset`.
528    pub const TZ_OFFSET: usize = 6;
529
530    // IfcCompositeProfileDef / IfcDerivedProfileDef
531    /// `IfcCompositeProfileDef.Profiles`.
532    pub const COMPOSITE_PROFILES: usize = 2;
533    /// `IfcDerivedProfileDef.ParentProfile`.
534    pub const DERIVED_PARENT: usize = 2;
535    /// `IfcDerivedProfileDef.Operator`.
536    pub const DERIVED_OPERATOR: usize = 3;
537    /// `IfcCompositeProfileDef.Label`, after `Profiles`.
538    pub const COMPOSITE_LABEL: usize = 3;
539    /// `IfcDerivedProfileDef.Label`, after `Operator`; inherited unchanged by
540    /// `IfcMirroredProfileDef`.
541    pub const DERIVED_LABEL: usize = 4;
542}