Skip to main content

ifc_occurrence/
draft.rs

1//! The authored fields of an occurrence.
2//!
3//! Beyond the attributes every occurrence shares, a few classes require
4//! more in IFC2X3 TC1 than in IFC4 ADD2 TC1 and IFC4X3 ADD2, and the draft
5//! carries them so those records can be authored (#214). From
6//! `references/ifc-spec`:
7//!
8//! ```text
9//! IFC2X3  IfcRamp / IfcRoof / IfcStair
10//!           ShapeType : IfcRampTypeEnum / IfcRoofTypeEnum / IfcStairTypeEnum;
11//! IFC2X3  IfcReinforcingBar
12//!           NominalDiameter : IfcPositiveLengthMeasure;
13//!           CrossSectionArea : IfcAreaMeasure;
14//!           BarRole : IfcReinforcingBarRoleEnum;
15//! IFC2X3  IfcTendon
16//!           NominalDiameter : IfcPositiveLengthMeasure;
17//!           CrossSectionArea : IfcAreaMeasure;
18//! IFC2X3  IfcReinforcingMesh
19//!           Longitudinal/TransverseBarNominalDiameter : IfcPositiveLengthMeasure;
20//!           Longitudinal/TransverseBarCrossSectionArea : IfcAreaMeasure;
21//!           Longitudinal/TransverseBarSpacing : IfcPositiveLengthMeasure;
22//! ```
23//!
24//! IFC4 and IFC4X3 declare the reinforcement measures `OPTIONAL` and have
25//! no `ShapeType` or `BarRole`. A value is written wherever the bound
26//! release declares the attribute on the class and refused where it does
27//! not.
28
29use ifc_model::{EntityId, Value};
30
31use crate::error::{OccurrenceError, OccurrenceResult};
32use crate::release::Layout;
33
34/// Attributes of an occurrence: those every class shares, and the few that
35/// one class requires on top.
36///
37/// The struct is `#[non_exhaustive]`: build it with
38/// [`OccurrenceDraft::new`] and the setters, so a field a later release
39/// needs can be added without breaking callers.
40#[derive(Debug, Clone, Copy, Default)]
41#[non_exhaustive]
42pub struct OccurrenceDraft<'a> {
43    /// `Name`.
44    pub name: Option<&'a str>,
45    /// `Description`.
46    pub description: Option<&'a str>,
47    /// `ObjectType`; required when `PredefinedType` is `USERDEFINED`.
48    pub object_type: Option<&'a str>,
49    /// `ObjectPlacement`.
50    pub placement: Option<EntityId>,
51    /// `Representation`.
52    pub representation: Option<EntityId>,
53    /// `Tag`, slot 7 on every class in this catalogue.
54    pub tag: Option<&'a str>,
55    /// `ShapeType` of an IFC2X3 `IfcRamp`, `IfcRoof` or `IfcStair`, a token
56    /// of that release's enumeration; required there.
57    pub shape_type: Option<&'a str>,
58    /// `NominalDiameter` of an `IfcReinforcingBar` or `IfcTendon`, an
59    /// `IfcPositiveLengthMeasure`; required in IFC2X3.
60    pub nominal_diameter: Option<f64>,
61    /// `CrossSectionArea` of an `IfcReinforcingBar` or `IfcTendon`, an
62    /// `IfcAreaMeasure`; required in IFC2X3.
63    pub cross_section_area: Option<f64>,
64    /// `BarRole` of an IFC2X3 `IfcReinforcingBar`, an
65    /// `IfcReinforcingBarRoleEnum` token; required there.
66    pub bar_role: Option<&'a str>,
67    /// The longitudinal bars of an `IfcReinforcingMesh`; required in
68    /// IFC2X3.
69    pub longitudinal_bars: Option<MeshBars>,
70    /// The transverse bars of an `IfcReinforcingMesh`; required in IFC2X3.
71    pub transverse_bars: Option<MeshBars>,
72}
73
74/// One bar direction of an `IfcReinforcingMesh`: its
75/// `...BarNominalDiameter`, `...BarCrossSectionArea` and `...BarSpacing`.
76#[derive(Debug, Clone, Copy, PartialEq)]
77#[non_exhaustive]
78pub struct MeshBars {
79    /// `...BarNominalDiameter`, an `IfcPositiveLengthMeasure`.
80    pub nominal_diameter: f64,
81    /// `...BarCrossSectionArea`, an `IfcAreaMeasure`.
82    pub cross_section_area: f64,
83    /// `...BarSpacing`, an `IfcPositiveLengthMeasure`.
84    pub spacing: f64,
85}
86
87impl MeshBars {
88    /// The three measures of one bar direction.
89    #[must_use]
90    pub const fn new(nominal_diameter: f64, cross_section_area: f64, spacing: f64) -> Self {
91        Self {
92            nominal_diameter,
93            cross_section_area,
94            spacing,
95        }
96    }
97}
98
99impl<'a> OccurrenceDraft<'a> {
100    /// Starts an empty draft with every attribute unset.
101    #[must_use]
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Sets `Name`.
107    #[must_use]
108    pub fn name(mut self, value: &'a str) -> Self {
109        self.name = Some(value);
110        self
111    }
112
113    /// Sets `Description`.
114    #[must_use]
115    pub fn description(mut self, value: &'a str) -> Self {
116        self.description = Some(value);
117        self
118    }
119
120    /// Sets `ObjectType`.
121    #[must_use]
122    pub fn object_type(mut self, value: &'a str) -> Self {
123        self.object_type = Some(value);
124        self
125    }
126
127    /// Sets `ObjectPlacement`.
128    #[must_use]
129    pub fn placement(mut self, value: EntityId) -> Self {
130        self.placement = Some(value);
131        self
132    }
133
134    /// Sets `Representation`.
135    #[must_use]
136    pub fn representation(mut self, value: EntityId) -> Self {
137        self.representation = Some(value);
138        self
139    }
140
141    /// Sets `Tag`.
142    #[must_use]
143    pub fn tag(mut self, value: &'a str) -> Self {
144        self.tag = Some(value);
145        self
146    }
147
148    /// Sets the IFC2X3 `ShapeType`.
149    #[must_use]
150    pub fn shape_type(mut self, value: &'a str) -> Self {
151        self.shape_type = Some(value);
152        self
153    }
154
155    /// Sets `NominalDiameter`.
156    #[must_use]
157    pub fn nominal_diameter(mut self, value: f64) -> Self {
158        self.nominal_diameter = Some(value);
159        self
160    }
161
162    /// Sets `CrossSectionArea`.
163    #[must_use]
164    pub fn cross_section_area(mut self, value: f64) -> Self {
165        self.cross_section_area = Some(value);
166        self
167    }
168
169    /// Sets the IFC2X3 `BarRole`.
170    #[must_use]
171    pub fn bar_role(mut self, value: &'a str) -> Self {
172        self.bar_role = Some(value);
173        self
174    }
175
176    /// Sets the longitudinal bars of a mesh.
177    #[must_use]
178    pub fn longitudinal_bars(mut self, value: MeshBars) -> Self {
179        self.longitudinal_bars = Some(value);
180        self
181    }
182
183    /// Sets the transverse bars of a mesh.
184    #[must_use]
185    pub fn transverse_bars(mut self, value: MeshBars) -> Self {
186        self.transverse_bars = Some(value);
187        self
188    }
189}
190
191/// How a measure is checked.
192#[derive(Clone, Copy)]
193enum Measure {
194    /// `IfcPositiveLengthMeasure`: finite and greater than zero.
195    PositiveLength,
196    /// `IfcAreaMeasure`: finite.
197    Area,
198}
199
200/// The class-specific values of `draft`, named by attribute and checked.
201///
202/// A token must be a member of the enumeration `layout` declares for the
203/// attribute on `entity` ([`OccurrenceError::UnknownToken`]); a token for
204/// an attribute `entity` does not declare there is
205/// [`OccurrenceError::AuthoringNotInSchema`], as is a measure, through
206/// [`Layout::named_record`]. Unset fields yield no value, so a record
207/// without them is laid out exactly as before.
208pub(crate) fn specific_values(
209    layout: Layout,
210    entity: &'static str,
211    draft: &OccurrenceDraft<'_>,
212) -> OccurrenceResult<Vec<(&'static str, Value)>> {
213    let mut values = Vec::new();
214    for (attribute, token) in [("ShapeType", draft.shape_type), ("BarRole", draft.bar_role)] {
215        let Some(token) = token else {
216            continue;
217        };
218        let Some(members) = layout.members(entity, attribute) else {
219            return Err(OccurrenceError::AuthoringNotInSchema {
220                entity,
221                attribute,
222                schema: layout.version(),
223            });
224        };
225        if !members.contains(&token) {
226            return Err(OccurrenceError::UnknownToken {
227                entity,
228                attribute,
229                token: token.into(),
230            });
231        }
232        values.push((attribute, Value::Enum(token.into())));
233    }
234
235    let mut measures = vec![
236        (
237            "NominalDiameter",
238            draft.nominal_diameter,
239            Measure::PositiveLength,
240        ),
241        ("CrossSectionArea", draft.cross_section_area, Measure::Area),
242    ];
243    for (bars, [diameter, area, spacing]) in [
244        (
245            draft.longitudinal_bars,
246            [
247                "LongitudinalBarNominalDiameter",
248                "LongitudinalBarCrossSectionArea",
249                "LongitudinalBarSpacing",
250            ],
251        ),
252        (
253            draft.transverse_bars,
254            [
255                "TransverseBarNominalDiameter",
256                "TransverseBarCrossSectionArea",
257                "TransverseBarSpacing",
258            ],
259        ),
260    ] {
261        measures.extend([
262            (
263                diameter,
264                bars.map(|b| b.nominal_diameter),
265                Measure::PositiveLength,
266            ),
267            (area, bars.map(|b| b.cross_section_area), Measure::Area),
268            (spacing, bars.map(|b| b.spacing), Measure::PositiveLength),
269        ]);
270    }
271    for (attribute, value, measure) in measures {
272        let Some(value) = value else {
273            continue;
274        };
275        let admitted = match measure {
276            Measure::PositiveLength => value.is_finite() && value > 0.0,
277            Measure::Area => value.is_finite(),
278        };
279        if !admitted {
280            return Err(OccurrenceError::InvalidMeasure {
281                entity,
282                attribute,
283                value: format!("{value:?}"),
284            });
285        }
286        values.push((attribute, Value::Real(value)));
287    }
288    Ok(values)
289}