ifc_structural/authoring/
action.rs1use ifc_model::{EntityId, Model, Transaction, Value};
2use ifc_schema::{Schema, TypeKind};
3
4use super::item::{root_fields, validate_root, StructuralRootDraft};
5use super::{build_named, optional_ref, validate_optional_ref, validate_ref_select};
6use crate::action::CoordinateSystem;
7use crate::error::{StructuralError, StructuralResult};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ProjectedOrTrue {
12 ProjectedLength,
14 TrueLength,
16}
17
18impl ProjectedOrTrue {
19 fn token(self) -> &'static str {
20 match self {
21 Self::ProjectedLength => "PROJECTED_LENGTH",
22 Self::TrueLength => "TRUE_LENGTH",
23 }
24 }
25}
26
27#[derive(Debug, Clone)]
29pub enum ActionDraftKind {
30 Point,
32 Linear {
34 projected_or_true: Option<ProjectedOrTrue>,
36 },
37 Planar {
39 projected_or_true: Option<ProjectedOrTrue>,
41 },
42 Curve {
47 projected_or_true: Option<ProjectedOrTrue>,
49 predefined_type: &'static str,
51 },
52 Surface {
54 projected_or_true: Option<ProjectedOrTrue>,
56 predefined_type: &'static str,
58 },
59}
60
61#[derive(Debug, Clone)]
63#[non_exhaustive]
64pub struct ActionDraft {
65 pub root: StructuralRootDraft,
67 pub applied_load: EntityId,
69 pub coordinate_system: CoordinateSystem,
71 pub destabilizing_load: Option<bool>,
73 pub caused_by: Option<EntityId>,
75 pub kind: ActionDraftKind,
77}
78
79impl ActionDraft {
80 #[must_use]
82 pub fn new(
83 root: StructuralRootDraft,
84 applied_load: EntityId,
85 coordinate_system: CoordinateSystem,
86 kind: ActionDraftKind,
87 ) -> Self {
88 Self {
89 root,
90 applied_load,
91 coordinate_system,
92 destabilizing_load: None,
93 caused_by: None,
94 kind,
95 }
96 }
97
98 #[must_use]
101 pub fn destabilizing_load(mut self, value: bool) -> Self {
102 self.destabilizing_load = Some(value);
103 self
104 }
105
106 #[must_use]
109 pub fn caused_by(mut self, value: EntityId) -> Self {
110 self.caused_by = Some(value);
111 self
112 }
113}
114
115pub fn stage_action(
124 tx: &mut Transaction,
125 model: &Model,
126 schema: &Schema,
127 draft: ActionDraft,
128) -> StructuralResult<EntityId> {
129 validate_root(tx, model, schema, &draft.root)?;
130 let (entity_type, projected_or_true, load_members): (&str, Option<ProjectedOrTrue>, &[&str]) =
131 match draft.kind {
132 ActionDraftKind::Point => (
133 "IfcStructuralPointAction",
134 None,
135 &[
136 "IfcStructuralLoadSingleForce",
137 "IfcStructuralLoadSingleDisplacement",
138 ],
139 ),
140 ActionDraftKind::Linear { projected_or_true } => (
141 "IfcStructuralLinearAction",
142 projected_or_true,
143 &[
144 "IfcStructuralLoadLinearForce",
145 "IfcStructuralLoadTemperature",
146 ],
147 ),
148 ActionDraftKind::Planar { projected_or_true } => (
149 "IfcStructuralPlanarAction",
150 projected_or_true,
151 &[
152 "IfcStructuralLoadPlanarForce",
153 "IfcStructuralLoadTemperature",
154 ],
155 ),
156 ActionDraftKind::Curve {
157 projected_or_true, ..
158 } => (
159 "IfcStructuralCurveAction",
160 projected_or_true,
161 &[
162 "IfcStructuralLoadLinearForce",
163 "IfcStructuralLoadTemperature",
164 ],
165 ),
166 ActionDraftKind::Surface {
167 projected_or_true, ..
168 } => (
169 "IfcStructuralSurfaceAction",
170 projected_or_true,
171 &[
172 "IfcStructuralLoadPlanarForce",
173 "IfcStructuralLoadTemperature",
174 ],
175 ),
176 };
177 validate_ref_select(
178 tx,
179 model,
180 schema,
181 draft.applied_load,
182 "compatible structural load",
183 load_members,
184 )?;
185 validate_optional_ref(tx, model, schema, draft.caused_by, "IfcStructuralReaction")?;
186 if projected_or_true == Some(ProjectedOrTrue::ProjectedLength)
187 && draft.coordinate_system != CoordinateSystem::Global
188 {
189 return Err(StructuralError::SemanticViolation {
190 entity: None,
191 rule: "PROJECTED_LENGTH structural action requires GLOBAL_COORDS",
192 });
193 }
194 let attributes = schema.attributes(entity_type);
195
196 let destabilizing_required = attributes
197 .iter()
198 .find(|a| a.name.eq_ignore_ascii_case("DestabilizingLoad"))
199 .is_some_and(|attribute| !attribute.optional);
200 if destabilizing_required && draft.destabilizing_load.is_none() {
201 return Err(StructuralError::MissingRequired {
202 entity_type: entity_type.into(),
203 attribute: "DestabilizingLoad".into(),
204 });
205 }
206 let has_object_type = draft.root.object_type.is_some();
207 let mut fields = root_fields(draft.root);
208 fields.push(("AppliedLoad", Value::Ref(draft.applied_load)));
209 fields.push((
210 "GlobalOrLocal",
211 Value::Enum(match draft.coordinate_system {
212 CoordinateSystem::Global => "GLOBAL_COORDS".into(),
213 CoordinateSystem::Local => "LOCAL_COORDS".into(),
214 }),
215 ));
216 if attributes
217 .iter()
218 .any(|a| a.name.eq_ignore_ascii_case("DestabilizingLoad"))
219 {
220 fields.push((
221 "DestabilizingLoad",
222 draft.destabilizing_load.map_or(Value::Null, Value::Bool),
223 ));
224 }
225 if attributes
226 .iter()
227 .any(|a| a.name.eq_ignore_ascii_case("CausedBy"))
228 {
229 fields.push(("CausedBy", optional_ref(draft.caused_by)));
230 }
231 if attributes
232 .iter()
233 .any(|a| a.name.eq_ignore_ascii_case("ProjectedOrTrue"))
234 {
235 fields.push((
236 "ProjectedOrTrue",
237 projected_or_true.map_or(Value::Null, |value| Value::Enum(value.token().into())),
238 ));
239 }
240 if attributes
241 .iter()
242 .any(|a| a.name.eq_ignore_ascii_case("PredefinedType"))
243 {
244 let token = match draft.kind {
248 ActionDraftKind::Curve {
249 predefined_type, ..
250 }
251 | ActionDraftKind::Surface {
252 predefined_type, ..
253 } => predefined_type,
254 _ => "NOTDEFINED",
255 };
256 if token.eq_ignore_ascii_case("USERDEFINED") && !has_object_type {
257 return Err(StructuralError::SemanticViolation {
258 entity: None,
259 rule: "USERDEFINED PredefinedType requires an ObjectType",
260 });
261 }
262 if matches!(draft.kind, ActionDraftKind::Curve { .. })
268 && token.eq_ignore_ascii_case("EQUIDISTANT")
269 {
270 return Err(StructuralError::SemanticViolation {
271 entity: None,
272 rule: "IfcStructuralCurveAction.SuitablePredefinedType",
273 });
274 }
275 validate_activity_token(schema, entity_type, token)?;
279 fields.push(("PredefinedType", Value::Enum(token.into())));
280 }
281 Ok(tx.create(build_named(schema, entity_type, fields)?))
282}
283
284pub(super) fn validate_activity_token(
291 schema: &Schema,
292 entity_type: &'static str,
293 token: &str,
294) -> StructuralResult<()> {
295 let declared = schema
296 .attributes(entity_type)
297 .iter()
298 .find(|a| a.name.eq_ignore_ascii_case("PredefinedType"))
299 .and_then(|a| schema.type_def(&a.type_name))
300 .is_some_and(|def| {
301 matches!(&def.kind, TypeKind::Enumeration(values)
302 if values.iter().any(|v| v.eq_ignore_ascii_case(token)))
303 });
304 if declared {
305 return Ok(());
306 }
307 Err(StructuralError::InvalidDraftValue {
308 entity_type,
309 attribute: "PredefinedType",
310 expected: "a token this action's activity enum declares",
311 })
312}