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)]
63pub struct ActionDraft {
64    /// `IfcRoot` attributes shared with other staged structural entities.
65    pub root: StructuralRootDraft,
66    /// `AppliedLoad`; must reference a load type compatible with `kind`.
67    pub applied_load: EntityId,
68    /// `GlobalOrLocal`.
69    pub coordinate_system: CoordinateSystem,
70    /// `DestabilizingLoad`; required when the target schema (IFC2X3) declares it mandatory.
71    pub destabilizing_load: Option<bool>,
72    /// `CausedBy`; only staged when the target schema declares the attribute.
73    pub caused_by: Option<EntityId>,
74    /// Which `IfcStructuralAction` subtype to create, and its subtype-specific attributes.
75    pub kind: ActionDraftKind,
76}
77
78/// Stage an `IfcStructuralAction` create edit on `tx`.
79///
80/// Fails with [`StructuralError::WrongReferenceType`] if `draft.applied_load`
81/// is not one of the load types `kind` permits, [`StructuralError::SemanticViolation`]
82/// if `ProjectedOrTrue` is `PROJECTED_LENGTH` while `coordinate_system` is not
83/// `Global`, and [`StructuralError::MissingRequired`] if `destabilizing_load`
84/// is unset while the target schema requires it. Returns the id staged for
85/// the new entity.
86pub fn stage_action(
87    tx: &mut Transaction,
88    model: &Model,
89    schema: &Schema,
90    draft: ActionDraft,
91) -> StructuralResult<EntityId> {
92    validate_root(tx, model, schema, &draft.root)?;
93    let (entity_type, projected_or_true, load_members): (&str, Option<ProjectedOrTrue>, &[&str]) =
94        match draft.kind {
95            ActionDraftKind::Point => (
96                "IfcStructuralPointAction",
97                None,
98                &[
99                    "IfcStructuralLoadSingleForce",
100                    "IfcStructuralLoadSingleDisplacement",
101                ],
102            ),
103            ActionDraftKind::Linear { projected_or_true } => (
104                "IfcStructuralLinearAction",
105                projected_or_true,
106                &[
107                    "IfcStructuralLoadLinearForce",
108                    "IfcStructuralLoadTemperature",
109                ],
110            ),
111            ActionDraftKind::Planar { projected_or_true } => (
112                "IfcStructuralPlanarAction",
113                projected_or_true,
114                &[
115                    "IfcStructuralLoadPlanarForce",
116                    "IfcStructuralLoadTemperature",
117                ],
118            ),
119            ActionDraftKind::Curve {
120                projected_or_true, ..
121            } => (
122                "IfcStructuralCurveAction",
123                projected_or_true,
124                &[
125                    "IfcStructuralLoadLinearForce",
126                    "IfcStructuralLoadTemperature",
127                ],
128            ),
129            ActionDraftKind::Surface {
130                projected_or_true, ..
131            } => (
132                "IfcStructuralSurfaceAction",
133                projected_or_true,
134                &[
135                    "IfcStructuralLoadPlanarForce",
136                    "IfcStructuralLoadTemperature",
137                ],
138            ),
139        };
140    validate_ref_select(
141        tx,
142        model,
143        schema,
144        draft.applied_load,
145        "compatible structural load",
146        load_members,
147    )?;
148    validate_optional_ref(tx, model, schema, draft.caused_by, "IfcStructuralReaction")?;
149    if projected_or_true == Some(ProjectedOrTrue::ProjectedLength)
150        && draft.coordinate_system != CoordinateSystem::Global
151    {
152        return Err(StructuralError::SemanticViolation {
153            entity: None,
154            rule: "PROJECTED_LENGTH structural action requires GLOBAL_COORDS",
155        });
156    }
157    let attributes = schema.attributes(entity_type);
158
159    let destabilizing_required = attributes
160        .iter()
161        .find(|a| a.name.eq_ignore_ascii_case("DestabilizingLoad"))
162        .is_some_and(|attribute| !attribute.optional);
163    if destabilizing_required && draft.destabilizing_load.is_none() {
164        return Err(StructuralError::MissingRequired {
165            entity_type: entity_type.into(),
166            attribute: "DestabilizingLoad".into(),
167        });
168    }
169    let has_object_type = draft.root.object_type.is_some();
170    let mut fields = root_fields(draft.root);
171    fields.push(("AppliedLoad", Value::Ref(draft.applied_load)));
172    fields.push((
173        "GlobalOrLocal",
174        Value::Enum(match draft.coordinate_system {
175            CoordinateSystem::Global => "GLOBAL_COORDS".into(),
176            CoordinateSystem::Local => "LOCAL_COORDS".into(),
177        }),
178    ));
179    if attributes
180        .iter()
181        .any(|a| a.name.eq_ignore_ascii_case("DestabilizingLoad"))
182    {
183        fields.push((
184            "DestabilizingLoad",
185            draft.destabilizing_load.map_or(Value::Null, Value::Bool),
186        ));
187    }
188    if attributes
189        .iter()
190        .any(|a| a.name.eq_ignore_ascii_case("CausedBy"))
191    {
192        fields.push(("CausedBy", optional_ref(draft.caused_by)));
193    }
194    if attributes
195        .iter()
196        .any(|a| a.name.eq_ignore_ascii_case("ProjectedOrTrue"))
197    {
198        fields.push((
199            "ProjectedOrTrue",
200            projected_or_true.map_or(Value::Null, |value| Value::Enum(value.token().into())),
201        ));
202    }
203    if attributes
204        .iter()
205        .any(|a| a.name.eq_ignore_ascii_case("PredefinedType"))
206    {
207        // IFC4 makes PredefinedType mandatory on the curve and surface
208        // forms. USERDEFINED without an ObjectType names nothing, which
209        // is the same trap the element types carry.
210        let token = match draft.kind {
211            ActionDraftKind::Curve {
212                predefined_type, ..
213            }
214            | ActionDraftKind::Surface {
215                predefined_type, ..
216            } => predefined_type,
217            _ => "NOTDEFINED",
218        };
219        if token.eq_ignore_ascii_case("USERDEFINED") && !has_object_type {
220            return Err(StructuralError::SemanticViolation {
221                entity: None,
222                rule: "USERDEFINED PredefinedType requires an ObjectType",
223            });
224        }
225        // SuitablePredefinedType: the curve form alone excludes
226        // EQUIDISTANT. The token exists in the enum because the
227        // activity-type enum is shared with curve *reactions*, where
228        // equidistant results are meaningful; an applied action
229        // cannot be equidistant.
230        if matches!(draft.kind, ActionDraftKind::Curve { .. })
231            && token.eq_ignore_ascii_case("EQUIDISTANT")
232        {
233            return Err(StructuralError::SemanticViolation {
234                entity: None,
235                rule: "IfcStructuralCurveAction.SuitablePredefinedType",
236            });
237        }
238        // The token must be one the target schema declares for this
239        // attribute: a surface token on a curve action resolves to a
240        // slot that accepts it structurally and means nothing.
241        validate_activity_token(schema, entity_type, token)?;
242        fields.push(("PredefinedType", Value::Enum(token.into())));
243    }
244    Ok(tx.create(build_named(schema, entity_type, fields)?))
245}
246
247/// Refuse a `PredefinedType` token the schema does not declare
248/// for `entity_type`.
249///
250/// The curve and surface forms carry different activity enums
251/// with overlapping tokens (`CONST`, `DISCRETE`), so a wrong-form
252/// token is not always visibly wrong.
253pub(super) fn validate_activity_token(
254    schema: &Schema,
255    entity_type: &'static str,
256    token: &str,
257) -> StructuralResult<()> {
258    let declared = schema
259        .attributes(entity_type)
260        .iter()
261        .find(|a| a.name.eq_ignore_ascii_case("PredefinedType"))
262        .and_then(|a| schema.type_def(&a.type_name))
263        .is_some_and(|def| {
264            matches!(&def.kind, TypeKind::Enumeration(values)
265                if values.iter().any(|v| v.eq_ignore_ascii_case(token)))
266        });
267    if declared {
268        return Ok(());
269    }
270    Err(StructuralError::InvalidDraftValue {
271        entity_type,
272        attribute: "PredefinedType",
273        expected: "a token this action's activity enum declares",
274    })
275}