Skip to main content

ifc_structural/
action.rs

1//! Applied structural actions.
2
3use ifc_model::EntityId;
4use ifc_schema::SchemaVersion;
5
6use crate::error::{StructuralError, StructuralResult};
7use crate::view::Record;
8
9mod varying;
10
11/// Which `IfcStructuralActivity` application geometry an action carries.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ActionKind {
14    /// Point application: `IfcStructuralPointAction`.
15    Point,
16    /// Line application: `IfcStructuralCurveAction` or its `IfcStructuralLinearAction` subtype.
17    Curve,
18    /// Surface application: `IfcStructuralSurfaceAction` or its `IfcStructuralPlanarAction` subtype.
19    Surface,
20}
21
22/// Value of `IfcGlobalOrLocalEnum` naming the frame an action's magnitude/direction are given in.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum CoordinateSystem {
25    /// `GLOBAL_COORDS`.
26    Global,
27    /// `LOCAL_COORDS`.
28    Local,
29}
30
31/// Borrowed projection of an `IfcStructuralAction` (point, curve/linear, or surface/planar).
32///
33/// The IFC2X3-only varying subtypes `IfcStructuralLinearActionVarying` and
34/// `IfcStructuralPlanarActionVarying` classify as [`ActionKind::Curve`] and
35/// [`ActionKind::Surface`]; [`StructuralAction::is_varying`] tells them apart
36/// and [`StructuralAction::subsequent_applied_loads`] reads their load list.
37#[derive(Debug, Clone, Copy)]
38pub struct StructuralAction<'m, 's> {
39    record: Record<'m, 's>,
40    kind: ActionKind,
41}
42
43impl<'m, 's> StructuralAction<'m, 's> {
44    /// Classify `record`'s [`ActionKind`] from its declared IFC type.
45    ///
46    /// Fails with [`StructuralError::WrongType`] if the type is none of
47    /// `IfcStructuralPointAction`, `IfcStructuralCurveAction`/`IfcStructuralLinearAction`,
48    /// or `IfcStructuralSurfaceAction`/`IfcStructuralPlanarAction`.
49    pub(crate) fn from_record(record: Record<'m, 's>) -> StructuralResult<Self> {
50        let kind = if record
51            .schema
52            .is_a(&record.entity.type_name, "IfcStructuralPointAction")
53        {
54            ActionKind::Point
55        } else if record
56            .schema
57            .is_a(&record.entity.type_name, "IfcStructuralCurveAction")
58            || record
59                .schema
60                .is_a(&record.entity.type_name, "IfcStructuralLinearAction")
61        {
62            ActionKind::Curve
63        } else if record
64            .schema
65            .is_a(&record.entity.type_name, "IfcStructuralSurfaceAction")
66            || record
67                .schema
68                .is_a(&record.entity.type_name, "IfcStructuralPlanarAction")
69        {
70            ActionKind::Surface
71        } else {
72            return Err(StructuralError::WrongType {
73                id: record.id,
74                expected: "point, curve/linear or surface/planar structural action",
75                actual: record.entity.type_name.to_string(),
76            });
77        };
78        Ok(Self { record, kind })
79    }
80
81    #[must_use]
82    /// The `IfcStructuralAction` entity id.
83    pub fn id(&self) -> EntityId {
84        self.record.id
85    }
86
87    #[must_use]
88    /// Which application geometry this action carries.
89    pub fn kind(&self) -> ActionKind {
90        self.kind
91    }
92
93    /// `AppliedLoad`, the `IfcStructuralLoad` this action applies.
94    ///
95    /// Fails with [`StructuralError::WrongReferenceType`] if the referenced
96    /// load's type is not one of the subtypes this action's kind permits
97    /// (e.g. a point action must reference `IfcStructuralLoadSingleForce` or
98    /// `IfcStructuralLoadSingleDisplacement`), and with
99    /// [`StructuralError::SemanticViolation`] for the same schema-shape
100    /// checks `validate_semantics` performs.
101    pub fn applied_load(&self) -> StructuralResult<EntityId> {
102        self.validate_semantics()
103    }
104
105    /// `GlobalOrLocal`, always present on `IfcStructuralAction`.
106    pub fn coordinate_system(&self) -> StructuralResult<CoordinateSystem> {
107        self.validate_semantics()?;
108        self.coordinate_system_value()
109    }
110
111    fn coordinate_system_value(&self) -> StructuralResult<CoordinateSystem> {
112        match self
113            .record
114            .required_enum("GlobalOrLocal")?
115            .to_ascii_uppercase()
116            .as_str()
117        {
118            "GLOBAL_COORDS" => Ok(CoordinateSystem::Global),
119            "LOCAL_COORDS" => Ok(CoordinateSystem::Local),
120            _ => Err(StructuralError::InvalidValue {
121                entity: self.record.id,
122                attribute: "GlobalOrLocal",
123                expected: "IfcGlobalOrLocalEnum",
124            }),
125        }
126    }
127
128    /// `DestabilizingLoad`, mandatory in IFC2X3 (defaults to `Some`) and optional in IFC4/IFC4X3.
129    ///
130    /// `None` when the attribute does not exist for this type in this schema version.
131    pub fn destabilizing_load(&self) -> StructuralResult<Option<bool>> {
132        self.validate_semantics()?;
133        if !self.record.has_attribute("DestabilizingLoad") {
134            return Ok(None);
135        }
136        if self.record.schema.version() == Some(ifc_schema::SchemaVersion::Ifc2x3) {
137            return self.record.required_bool("DestabilizingLoad").map(Some);
138        }
139        self.record.optional_bool("DestabilizingLoad")
140    }
141
142    /// `CausedBy`, the `IfcStructuralReaction` that produced this action, when the attribute exists and is set.
143    pub fn caused_by(&self) -> StructuralResult<Option<EntityId>> {
144        self.validate_semantics()?;
145        if !self.record.has_attribute("CausedBy") {
146            return Ok(None);
147        }
148        self.record
149            .optional_ref("CausedBy", "IfcStructuralReaction")
150    }
151
152    /// `PredefinedType`, when the attribute exists for this action's IFC type.
153    ///
154    /// `IfcStructuralPointAction` has no `PredefinedType` attribute, so this
155    /// returns `None` for point actions regardless of schema version.
156    pub fn predefined_type(&self) -> StructuralResult<Option<&'m str>> {
157        self.validate_semantics()?;
158        if !self.record.has_attribute("PredefinedType") {
159            return Ok(None);
160        }
161        self.record.required_enum("PredefinedType").map(Some)
162    }
163
164    /// `ProjectedOrTrue`, mandatory on linear/planar actions in IFC2X3 and optional elsewhere.
165    pub fn projected_or_true(&self) -> StructuralResult<Option<&'m str>> {
166        self.validate_semantics()?;
167        if !self.record.has_attribute("ProjectedOrTrue") {
168            return Ok(None);
169        }
170        if self.record.schema.version() == Some(SchemaVersion::Ifc2x3)
171            && (self.is_linear() || self.is_planar())
172        {
173            return self.record.required_enum("ProjectedOrTrue").map(Some);
174        }
175        self.record.optional_enum("ProjectedOrTrue")
176    }
177
178    fn validate_semantics(&self) -> StructuralResult<EntityId> {
179        let applied_load = self.applied_load_value()?;
180        if self.kind == ActionKind::Point
181            || self.record.schema.version() == Some(SchemaVersion::Ifc2x3)
182        {
183            return Ok(applied_load);
184        }
185        let predefined_type = self.record.required_enum("PredefinedType")?;
186        self.record.require_object_type_if(
187            predefined_type.eq_ignore_ascii_case("USERDEFINED"),
188            "USERDEFINED structural action requires ObjectType",
189        )?;
190        if self.kind == ActionKind::Curve && predefined_type.eq_ignore_ascii_case("EQUIDISTANT") {
191            return Err(StructuralError::SemanticViolation {
192                entity: Some(self.record.id),
193                rule: "structural curve action PredefinedType must not be EQUIDISTANT",
194            });
195        }
196        if (self.is_linear() || self.is_planar()) && !predefined_type.eq_ignore_ascii_case("CONST")
197        {
198            return Err(StructuralError::SemanticViolation {
199                entity: Some(self.record.id),
200                rule: "linear and planar structural actions require CONST PredefinedType",
201            });
202        }
203        if self
204            .record
205            .optional_enum("ProjectedOrTrue")?
206            .is_some_and(|value| value.eq_ignore_ascii_case("PROJECTED_LENGTH"))
207            && self.coordinate_system_value()? != CoordinateSystem::Global
208        {
209            return Err(StructuralError::SemanticViolation {
210                entity: Some(self.record.id),
211                rule: "PROJECTED_LENGTH structural action requires GLOBAL_COORDS",
212            });
213        }
214        Ok(applied_load)
215    }
216
217    fn applied_load_value(&self) -> StructuralResult<EntityId> {
218        let (expected, members): (&'static str, &[&str]) = if self.kind == ActionKind::Point {
219            (
220                "point-action structural load",
221                &[
222                    "IfcStructuralLoadSingleForce",
223                    "IfcStructuralLoadSingleDisplacement",
224                ],
225            )
226        } else if self.is_linear() {
227            (
228                "linear-action structural load",
229                &[
230                    "IfcStructuralLoadLinearForce",
231                    "IfcStructuralLoadTemperature",
232                ],
233            )
234        } else if self.is_planar() {
235            (
236                "planar-action structural load",
237                &[
238                    "IfcStructuralLoadPlanarForce",
239                    "IfcStructuralLoadTemperature",
240                ],
241            )
242        } else {
243            return self.record.required_ref("AppliedLoad", "IfcStructuralLoad");
244        };
245        self.record
246            .required_ref_select("AppliedLoad", expected, members)
247    }
248
249    fn is_linear(&self) -> bool {
250        self.record
251            .schema
252            .is_a(&self.record.entity.type_name, "IfcStructuralLinearAction")
253    }
254
255    fn is_planar(&self) -> bool {
256        self.record
257            .schema
258            .is_a(&self.record.entity.type_name, "IfcStructuralPlanarAction")
259    }
260}