1use ifc_model::EntityId;
4use ifc_schema::SchemaVersion;
5
6use crate::error::{StructuralError, StructuralResult};
7use crate::view::Record;
8
9mod varying;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ActionKind {
14 Point,
16 Curve,
18 Surface,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum CoordinateSystem {
25 Global,
27 Local,
29}
30
31#[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 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 pub fn id(&self) -> EntityId {
84 self.record.id
85 }
86
87 #[must_use]
88 pub fn kind(&self) -> ActionKind {
90 self.kind
91 }
92
93 pub fn applied_load(&self) -> StructuralResult<EntityId> {
102 self.validate_semantics()
103 }
104
105 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 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 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 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 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}