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}