Skip to main content

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}