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}