1use 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, 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 pub varying: Option<VaryingActionDraft>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
89#[non_exhaustive]
90pub struct VaryingActionDraft {
91 pub varying_applied_load_location: EntityId,
93 pub subsequent_applied_loads: Vec<EntityId>,
96}
97
98impl VaryingActionDraft {
99 #[must_use]
101 pub fn new(
102 varying_applied_load_location: EntityId,
103 subsequent_applied_loads: Vec<EntityId>,
104 ) -> Self {
105 Self {
106 varying_applied_load_location,
107 subsequent_applied_loads,
108 }
109 }
110}
111
112impl ActionDraft {
113 #[must_use]
115 pub fn new(
116 root: StructuralRootDraft,
117 applied_load: EntityId,
118 coordinate_system: CoordinateSystem,
119 kind: ActionDraftKind,
120 ) -> Self {
121 Self {
122 root,
123 applied_load,
124 coordinate_system,
125 destabilizing_load: None,
126 caused_by: None,
127 kind,
128 varying: None,
129 }
130 }
131
132 #[must_use]
135 pub fn destabilizing_load(mut self, value: bool) -> Self {
136 self.destabilizing_load = Some(value);
137 self
138 }
139
140 #[must_use]
143 pub fn caused_by(mut self, value: EntityId) -> Self {
144 self.caused_by = Some(value);
145 self
146 }
147
148 #[must_use]
151 pub fn varying(mut self, value: VaryingActionDraft) -> Self {
152 self.varying = Some(value);
153 self
154 }
155}
156
157pub fn stage_action(
175 tx: &mut Transaction,
176 model: &Model,
177 schema: &Schema,
178 draft: ActionDraft,
179) -> StructuralResult<EntityId> {
180 let varying = validate_varying(tx, model, schema, &draft)?;
181 validate_root(tx, model, schema, &draft.root)?;
182 let (entity_type, projected_or_true, load_members): (&str, Option<ProjectedOrTrue>, &[&str]) =
183 match draft.kind {
184 ActionDraftKind::Point => (
185 "IfcStructuralPointAction",
186 None,
187 &[
188 "IfcStructuralLoadSingleForce",
189 "IfcStructuralLoadSingleDisplacement",
190 ],
191 ),
192 ActionDraftKind::Linear { projected_or_true } => (
193 "IfcStructuralLinearAction",
194 projected_or_true,
195 &[
196 "IfcStructuralLoadLinearForce",
197 "IfcStructuralLoadTemperature",
198 ],
199 ),
200 ActionDraftKind::Planar { projected_or_true } => (
201 "IfcStructuralPlanarAction",
202 projected_or_true,
203 &[
204 "IfcStructuralLoadPlanarForce",
205 "IfcStructuralLoadTemperature",
206 ],
207 ),
208 ActionDraftKind::Curve {
209 projected_or_true, ..
210 } => (
211 "IfcStructuralCurveAction",
212 projected_or_true,
213 &[
214 "IfcStructuralLoadLinearForce",
215 "IfcStructuralLoadTemperature",
216 ],
217 ),
218 ActionDraftKind::Surface {
219 projected_or_true, ..
220 } => (
221 "IfcStructuralSurfaceAction",
222 projected_or_true,
223 &[
224 "IfcStructuralLoadPlanarForce",
225 "IfcStructuralLoadTemperature",
226 ],
227 ),
228 };
229 let entity_type = varying.as_ref().map_or(entity_type, |(name, _)| *name);
230 validate_ref_select(
231 tx,
232 model,
233 schema,
234 draft.applied_load,
235 "compatible structural load",
236 load_members,
237 )?;
238 validate_optional_ref(tx, model, schema, draft.caused_by, "IfcStructuralReaction")?;
239 if projected_or_true == Some(ProjectedOrTrue::ProjectedLength)
240 && draft.coordinate_system != CoordinateSystem::Global
241 {
242 return Err(StructuralError::SemanticViolation {
243 entity: None,
244 rule: "PROJECTED_LENGTH structural action requires GLOBAL_COORDS",
245 });
246 }
247 let attributes = schema.attributes(entity_type);
248
249 let destabilizing_required = attributes
250 .iter()
251 .find(|a| a.name.eq_ignore_ascii_case("DestabilizingLoad"))
252 .is_some_and(|attribute| !attribute.optional);
253 if destabilizing_required && draft.destabilizing_load.is_none() {
254 return Err(StructuralError::MissingRequired {
255 entity_type: entity_type.into(),
256 attribute: "DestabilizingLoad".into(),
257 });
258 }
259 let has_object_type = draft.root.object_type.is_some();
260 let mut fields = root_fields(draft.root);
261 fields.push(("AppliedLoad", Value::Ref(draft.applied_load)));
262 fields.push((
263 "GlobalOrLocal",
264 Value::Enum(match draft.coordinate_system {
265 CoordinateSystem::Global => "GLOBAL_COORDS".into(),
266 CoordinateSystem::Local => "LOCAL_COORDS".into(),
267 }),
268 ));
269 if attributes
270 .iter()
271 .any(|a| a.name.eq_ignore_ascii_case("DestabilizingLoad"))
272 {
273 fields.push((
274 "DestabilizingLoad",
275 draft.destabilizing_load.map_or(Value::Null, Value::Bool),
276 ));
277 }
278 if attributes
279 .iter()
280 .any(|a| a.name.eq_ignore_ascii_case("CausedBy"))
281 {
282 fields.push(("CausedBy", optional_ref(draft.caused_by)));
283 }
284 if attributes
285 .iter()
286 .any(|a| a.name.eq_ignore_ascii_case("ProjectedOrTrue"))
287 {
288 fields.push((
289 "ProjectedOrTrue",
290 projected_or_true.map_or(Value::Null, |value| Value::Enum(value.token().into())),
291 ));
292 }
293 if attributes
294 .iter()
295 .any(|a| a.name.eq_ignore_ascii_case("PredefinedType"))
296 {
297 let token = match draft.kind {
301 ActionDraftKind::Curve {
302 predefined_type, ..
303 }
304 | ActionDraftKind::Surface {
305 predefined_type, ..
306 } => predefined_type,
307 _ => "NOTDEFINED",
308 };
309 if token.eq_ignore_ascii_case("USERDEFINED") && !has_object_type {
310 return Err(StructuralError::SemanticViolation {
311 entity: None,
312 rule: "USERDEFINED PredefinedType requires an ObjectType",
313 });
314 }
315 if matches!(draft.kind, ActionDraftKind::Curve { .. })
321 && token.eq_ignore_ascii_case("EQUIDISTANT")
322 {
323 return Err(StructuralError::SemanticViolation {
324 entity: None,
325 rule: "IfcStructuralCurveAction.SuitablePredefinedType",
326 });
327 }
328 validate_activity_token(schema, entity_type, token)?;
332 fields.push(("PredefinedType", Value::Enum(token.into())));
333 }
334 if let Some((_, varying)) = varying {
335 fields.push((
336 "VaryingAppliedLoadLocation",
337 Value::Ref(varying.varying_applied_load_location),
338 ));
339 fields.push((
340 "SubsequentAppliedLoads",
341 Value::List(
342 varying
343 .subsequent_applied_loads
344 .into_iter()
345 .map(Value::Ref)
346 .collect(),
347 ),
348 ));
349 }
350 Ok(tx.create(build_named(schema, entity_type, fields)?))
351}
352
353fn validate_varying(
358 tx: &Transaction,
359 model: &Model,
360 schema: &Schema,
361 draft: &ActionDraft,
362) -> StructuralResult<Option<(&'static str, VaryingActionDraft)>> {
363 let Some(varying) = &draft.varying else {
364 return Ok(None);
365 };
366 let (entity_type, minimum, expected) = match draft.kind {
367 ActionDraftKind::Linear { .. } => (
368 "IfcStructuralLinearActionVarying",
369 1,
370 "LIST [1:?] of IfcStructuralLoad references",
371 ),
372 ActionDraftKind::Planar { .. } => (
373 "IfcStructuralPlanarActionVarying",
374 2,
375 "LIST [2:?] of IfcStructuralLoad references",
376 ),
377 _ => {
378 return Err(StructuralError::InvalidDraftValue {
379 entity_type: "IfcStructuralAction",
380 attribute: "SubsequentAppliedLoads",
381 expected: "a Linear or Planar action kind (only those have varying subtypes)",
382 })
383 }
384 };
385 if schema.entity(entity_type).is_none() {
386 return Err(StructuralError::EntityNotInSchema {
387 entity: entity_type,
388 schema: schema.name().to_owned(),
389 });
390 }
391 if varying.subsequent_applied_loads.len() < minimum {
392 return Err(StructuralError::InvalidDraftValue {
393 entity_type,
394 attribute: "SubsequentAppliedLoads",
395 expected,
396 });
397 }
398 validate_ref(
399 tx,
400 model,
401 schema,
402 varying.varying_applied_load_location,
403 "IfcShapeAspect",
404 )?;
405 for load in &varying.subsequent_applied_loads {
406 validate_ref(tx, model, schema, *load, "IfcStructuralLoad")?;
407 }
408 Ok(Some((entity_type, varying.clone())))
409}
410
411pub(super) fn validate_activity_token(
418 schema: &Schema,
419 entity_type: &'static str,
420 token: &str,
421) -> StructuralResult<()> {
422 let declared = schema
423 .attributes(entity_type)
424 .iter()
425 .find(|a| a.name.eq_ignore_ascii_case("PredefinedType"))
426 .and_then(|a| schema.type_def(&a.type_name))
427 .is_some_and(|def| {
428 matches!(&def.kind, TypeKind::Enumeration(values)
429 if values.iter().any(|v| v.eq_ignore_ascii_case(token)))
430 });
431 if declared {
432 return Ok(());
433 }
434 Err(StructuralError::InvalidDraftValue {
435 entity_type,
436 attribute: "PredefinedType",
437 expected: "a token this action's activity enum declares",
438 })
439}