ifc_structural/load.rs
1//! Static structural load values and the reinforcement-area result.
2//!
3//! [`StaticLoad`] projects every `IfcStructuralLoadStatic` subtype declared
4//! by IFC2X3, IFC4 and IFC4X3, classified with `Schema::is_a` from the most
5//! specific subtype up, so a subtype is never read as its supertype's
6//! narrower attribute list. `IfcSurfaceReinforcementArea` is a sibling of
7//! `IfcStructuralLoadStatic` under `IfcStructuralLoadOrResult` (IFC4 and
8//! later), not a static load, so it has its own projection,
9//! [`SurfaceReinforcementArea`].
10
11use ifc_model::EntityId;
12
13use crate::error::{StructuralError, StructuralResult};
14use crate::view::Record;
15
16mod dynamic;
17mod reinforcement;
18
19pub use dynamic::LoadConfiguration;
20pub use reinforcement::SurfaceReinforcementArea;
21
22/// Which `IfcStructuralLoadStatic` subtype a static load value carries.
23///
24/// Each variant names the most specific subtype this crate reads; a record
25/// of an undeclared further subtype is classified as its nearest listed
26/// ancestor.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum LoadKind {
30 /// `IfcStructuralLoadSingleForce`: force/moment at a point.
31 SingleForce,
32 /// `IfcStructuralLoadSingleForceWarping`: a single force plus the
33 /// `WarpingMoment` (bimoment) of restrained warping.
34 SingleForceWarping,
35 /// `IfcStructuralLoadSingleDisplacement`: prescribed translations and
36 /// rotations at a point.
37 SingleDisplacement,
38 /// `IfcStructuralLoadSingleDisplacementDistortion`: a single
39 /// displacement plus the cross-section `Distortion`.
40 SingleDisplacementDistortion,
41 /// `IfcStructuralLoadLinearForce`: force/moment per unit length along a curve.
42 LinearForce,
43 /// `IfcStructuralLoadPlanarForce`: force per unit area over a surface.
44 PlanarForce,
45 /// `IfcStructuralLoadTemperature`: constant plus through-thickness temperature deltas.
46 Temperature,
47}
48
49/// Borrowed projection of an `IfcStructuralLoadStatic` subtype: single,
50/// linear or planar force, single displacement, or temperature.
51#[derive(Debug, Clone, Copy)]
52pub struct StaticLoad<'m, 's> {
53 record: Record<'m, 's>,
54 kind: LoadKind,
55}
56
57impl<'m, 's> StaticLoad<'m, 's> {
58 /// Classify `record`'s [`LoadKind`] from its declared IFC type.
59 ///
60 /// Classification uses `Schema::is_a`, most specific subtype first, so
61 /// `IfcStructuralLoadSingleForceWarping` is never read as a plain
62 /// single force. Fails with [`StructuralError::WrongType`] if the type
63 /// is not an `IfcStructuralLoadStatic` subtype this crate reads.
64 pub(crate) fn from_record(record: Record<'m, 's>) -> StructuralResult<Self> {
65 // Subtypes before their supertypes: the first match wins.
66 const ORDER: [(&str, LoadKind); 7] = [
67 (
68 "IfcStructuralLoadSingleForceWarping",
69 LoadKind::SingleForceWarping,
70 ),
71 ("IfcStructuralLoadSingleForce", LoadKind::SingleForce),
72 (
73 "IfcStructuralLoadSingleDisplacementDistortion",
74 LoadKind::SingleDisplacementDistortion,
75 ),
76 (
77 "IfcStructuralLoadSingleDisplacement",
78 LoadKind::SingleDisplacement,
79 ),
80 ("IfcStructuralLoadLinearForce", LoadKind::LinearForce),
81 ("IfcStructuralLoadPlanarForce", LoadKind::PlanarForce),
82 ("IfcStructuralLoadTemperature", LoadKind::Temperature),
83 ];
84 let Some(kind) = ORDER
85 .iter()
86 .find(|(ancestor, _)| record.schema.is_a(&record.entity.type_name, ancestor))
87 .map(|(_, kind)| *kind)
88 else {
89 return Err(StructuralError::WrongType {
90 id: record.id,
91 expected: "core static structural load",
92 actual: record.entity.type_name.to_string(),
93 });
94 };
95 Ok(Self { record, kind })
96 }
97
98 #[must_use]
99 /// The `IfcStructuralLoadStatic` entity id.
100 pub fn id(&self) -> EntityId {
101 self.record.id
102 }
103
104 #[must_use]
105 /// Which core static load subtype this value carries.
106 pub fn kind(&self) -> LoadKind {
107 self.kind
108 }
109
110 /// `Name`, inherited from `IfcStructuralLoad`. Legally absent.
111 pub fn name(&self) -> StructuralResult<Option<&'m str>> {
112 self.record.optional_text("Name")
113 }
114
115 /// The load's numeric components in schema-declared order for its [`LoadKind`]:
116 /// six force/moment axes for a single force, followed by `WarpingMoment`
117 /// for the warping form; three translations and three rotations
118 /// (`DisplacementX..Z`, `RotationalDisplacementRX..RZ`) for a single
119 /// displacement, followed by `Distortion` for the distortion form; six
120 /// per-length axes for a linear force; three per-area axes for a planar
121 /// force; or three temperature deltas (constant, through-Y, through-Z).
122 /// Each component is legally absent
123 /// individually; `IfcStructuralLoadTemperature` uses the underscored
124 /// `DeltaT_Constant`/`DeltaT_Y`/`DeltaT_Z` names in schemas where the
125 /// unscored `DeltaTConstant` attribute does not exist.
126 pub fn components(&self) -> StructuralResult<Vec<Option<f64>>> {
127 let names: &[&'static str] = match self.kind {
128 LoadKind::SingleForce => &[
129 "ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ",
130 ],
131 LoadKind::SingleForceWarping => &[
132 "ForceX",
133 "ForceY",
134 "ForceZ",
135 "MomentX",
136 "MomentY",
137 "MomentZ",
138 "WarpingMoment",
139 ],
140 LoadKind::SingleDisplacement => &[
141 "DisplacementX",
142 "DisplacementY",
143 "DisplacementZ",
144 "RotationalDisplacementRX",
145 "RotationalDisplacementRY",
146 "RotationalDisplacementRZ",
147 ],
148 LoadKind::SingleDisplacementDistortion => &[
149 "DisplacementX",
150 "DisplacementY",
151 "DisplacementZ",
152 "RotationalDisplacementRX",
153 "RotationalDisplacementRY",
154 "RotationalDisplacementRZ",
155 "Distortion",
156 ],
157 LoadKind::LinearForce => &[
158 "LinearForceX",
159 "LinearForceY",
160 "LinearForceZ",
161 "LinearMomentX",
162 "LinearMomentY",
163 "LinearMomentZ",
164 ],
165 LoadKind::PlanarForce => &["PlanarForceX", "PlanarForceY", "PlanarForceZ"],
166 LoadKind::Temperature => {
167 if self.record.has_attribute("DeltaTConstant") {
168 &["DeltaTConstant", "DeltaTY", "DeltaTZ"]
169 } else {
170 &["DeltaT_Constant", "DeltaT_Y", "DeltaT_Z"]
171 }
172 }
173 };
174 names
175 .iter()
176 .map(|name| self.record.optional_number(name))
177 .collect()
178 }
179}