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