Skip to main content

ifc_structural/authoring/
action.rs

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_select};
6use crate::action::CoordinateSystem;
7use crate::error::{StructuralError, StructuralResult};
8
9/// Value of `IfcProjectedOrTrueLengthEnum` naming how a linear/planar action's magnitude is measured.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ProjectedOrTrue {
12    /// `PROJECTED_LENGTH`: magnitude given per unit of the projected length/area.
13    ProjectedLength,
14    /// `TRUE_LENGTH`: magnitude given per unit of the true (unprojected) length/area.
15    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/// Staged action subtype for [`ActionDraft::kind`].
28#[derive(Debug, Clone)]
29pub enum ActionDraftKind {
30    /// Stages an `IfcStructuralPointAction`.
31    Point,
32    /// Stages an `IfcStructuralLinearAction`.
33    Linear {
34        /// `ProjectedOrTrue`; staged only when the target schema declares the attribute.
35        projected_or_true: Option<ProjectedOrTrue>,
36    },
37    /// Stages an `IfcStructuralPlanarAction`.
38    Planar {
39        /// `ProjectedOrTrue`; staged only when the target schema declares the attribute.
40        projected_or_true: Option<ProjectedOrTrue>,
41    },
42    /// Stages an `IfcStructuralCurveAction`.
43    ///
44    /// IFC4 replaced the linear/planar pair with curve/surface forms that
45    /// additionally carry a mandatory `PredefinedType`.
46    Curve {
47        /// `ProjectedOrTrue`; staged only when the target schema declares it.
48        projected_or_true: Option<ProjectedOrTrue>,
49        /// `PredefinedType`, an `IfcStructuralCurveActivityTypeEnum` token.
50        predefined_type: &'static str,
51    },
52    /// Stages an `IfcStructuralSurfaceAction`.
53    Surface {
54        /// `ProjectedOrTrue`; staged only when the target schema declares it.
55        projected_or_true: Option<ProjectedOrTrue>,
56        /// `PredefinedType`, an `IfcStructuralSurfaceActivityTypeEnum` token.
57        predefined_type: &'static str,
58    },
59}
60
61/// Staged fields for creating an `IfcStructuralAction` via [`stage_action`].
62#[derive(Debug, Clone)]
63#[non_exhaustive]
64pub struct ActionDraft {
65    /// `IfcRoot` attributes shared with other staged structural entities.
66    pub root: StructuralRootDraft,
67    /// `AppliedLoad`; must reference a load type compatible with `kind`.
68    pub applied_load: EntityId,
69    /// `GlobalOrLocal`.
70    pub coordinate_system: CoordinateSystem,
71    /// `DestabilizingLoad`; required when the target schema (IFC2X3) declares it mandatory.
72    pub destabilizing_load: Option<bool>,
73    /// `CausedBy`; only staged when the target schema declares the attribute.
74    pub caused_by: Option<EntityId>,
75    /// Which `IfcStructuralAction` subtype to create, and its subtype-specific attributes.
76    pub kind: ActionDraftKind,
77}
78
79impl ActionDraft {
80    /// Starts a draft from its required fields; every other field is unset.
81    #[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    /// Sets `destabilizing_load`: `DestabilizingLoad`; required when the target
99    /// schema (IFC2X3) declares it mandatory.
100    #[must_use]
101    pub fn destabilizing_load(mut self, value: bool) -> Self {
102        self.destabilizing_load = Some(value);
103        self
104    }
105
106    /// Sets `caused_by`: `CausedBy`; only staged when the target schema
107    /// declares the attribute.
108    #[must_use]
109    pub fn caused_by(mut self, value: EntityId) -> Self {
110        self.caused_by = Some(value);
111        self
112    }
113}
114
115/// Stage an `IfcStructuralAction` create edit on `tx`.
116///
117/// Fails with [`StructuralError::WrongReferenceType`] if `draft.applied_load`
118/// is not one of the load types `kind` permits, [`StructuralError::SemanticViolation`]
119/// if `ProjectedOrTrue` is `PROJECTED_LENGTH` while `coordinate_system` is not
120/// `Global`, and [`StructuralError::MissingRequired`] if `destabilizing_load`
121/// is unset while the target schema requires it. Returns the id staged for
122/// the new entity.
123pub 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        // IFC4 makes PredefinedType mandatory on the curve and surface
245        // forms. USERDEFINED without an ObjectType names nothing, which
246        // is the same trap the element types carry.
247        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        // SuitablePredefinedType: the curve form alone excludes
263        // EQUIDISTANT. The token exists in the enum because the
264        // activity-type enum is shared with curve *reactions*, where
265        // equidistant results are meaningful; an applied action
266        // cannot be equidistant.
267        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        // The token must be one the target schema declares for this
276        // attribute: a surface token on a curve action resolves to a
277        // slot that accepts it structurally and means nothing.
278        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
284/// Refuse a `PredefinedType` token the schema does not declare
285/// for `entity_type`.
286///
287/// The curve and surface forms carry different activity enums
288/// with overlapping tokens (`CONST`, `DISCRETE`), so a wrong-form
289/// token is not always visibly wrong.
290pub(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}