1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ActionKind {
16 Point,
18 Curve,
20 Surface,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CoordinateSystem {
27 Global,
29 Local,
31}
32
33#[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 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 pub fn id(&self) -> EntityId {
81 self.record.id
82 }
83
84 #[must_use]
85 pub fn kind(&self) -> ActionKind {
87 self.kind
88 }
89
90 pub fn applied_load(&self) -> StructuralResult<EntityId> {
99 self.validate_semantics()
100 }
101
102 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 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 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 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 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}