Skip to main content

ifc_structural/authoring/
reaction.rs

1//! Authoring for structural reactions.
2//!
3//! A reaction is what the analysis produces: the force a support
4//! returns, as opposed to the action a designer applies. The crate
5//! could read all three forms and author none, so a result set could
6//! be inspected but never written back.
7//!
8//! The three forms share `IfcStructuralActivity`'s AppliedLoad and
9//! GlobalOrLocal, and differ only in whether they add a
10//! PredefinedType and which enum it draws from. The point form adds
11//! none, so its arity is one short of the other two.
12
13use ifc_model::guid::Guid;
14use ifc_model::{EntityId, Model, Transaction, Value};
15use ifc_schema::Schema;
16
17use super::action::validate_activity_token;
18use super::item::{root_fields, validate_root, StructuralRootDraft};
19use super::load_group::validate_enum_token;
20use super::{build_named, optional_ref, optional_text, validate_optional_ref, validate_ref_select};
21use crate::action::CoordinateSystem;
22use crate::error::StructuralError;
23use crate::error::StructuralResult;
24
25/// Which `IfcStructuralReaction` subtype to stage.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ReactionDraftKind<'a> {
28    /// `IfcStructuralPointReaction`: adds no PredefinedType.
29    Point,
30    /// `IfcStructuralCurveReaction`, typed by
31    /// `IfcStructuralCurveActivityTypeEnum`.
32    Curve {
33        /// `PredefinedType`. Required; the slot is not optional.
34        predefined_type: &'a str,
35    },
36    /// `IfcStructuralSurfaceReaction`, typed by
37    /// `IfcStructuralSurfaceActivityTypeEnum`.
38    Surface {
39        /// `PredefinedType`. Required; the slot is not optional.
40        predefined_type: &'a str,
41    },
42}
43
44/// Authored fields for one structural reaction.
45#[derive(Debug, Clone)]
46pub struct ReactionDraft<'a> {
47    /// `IfcRoot` attributes shared with other structural entities.
48    pub root: StructuralRootDraft,
49    /// `AppliedLoad`; the reaction force the analysis produced.
50    pub applied_load: EntityId,
51    /// `GlobalOrLocal`.
52    pub coordinate_system: CoordinateSystem,
53    /// Which subtype, and its PredefinedType where one applies.
54    pub kind: ReactionDraftKind<'a>,
55}
56
57/// Stage an `IfcStructuralReaction` subtype.
58///
59/// # Errors
60///
61/// Refuses an `applied_load` that is not an `IfcStructuralLoad`, and a
62/// `predefined_type` the target schema does not declare for the
63/// subtype's own enum. The two typed forms draw from *different*
64/// enums, so a token valid on a curve may be undeclared on a surface.
65pub fn stage_reaction(
66    tx: &mut Transaction,
67    model: &Model,
68    schema: &Schema,
69    draft: ReactionDraft<'_>,
70) -> StructuralResult<EntityId> {
71    validate_root(tx, model, schema, &draft.root)?;
72    let (entity_type, predefined_type) = match draft.kind {
73        ReactionDraftKind::Point => ("IfcStructuralPointReaction", None),
74        ReactionDraftKind::Curve { predefined_type } => {
75            ("IfcStructuralCurveReaction", Some(predefined_type))
76        }
77        ReactionDraftKind::Surface { predefined_type } => {
78            ("IfcStructuralSurfaceReaction", Some(predefined_type))
79        }
80    };
81    validate_ref_select(
82        tx,
83        model,
84        schema,
85        draft.applied_load,
86        "structural load",
87        &["IfcStructuralLoad"],
88    )?;
89
90    let mut fields = root_fields(draft.root);
91    fields.push(("AppliedLoad", Value::Ref(draft.applied_load)));
92    fields.push((
93        "GlobalOrLocal",
94        Value::Enum(match draft.coordinate_system {
95            CoordinateSystem::Global => "GLOBAL_COORDS".into(),
96            CoordinateSystem::Local => "LOCAL_COORDS".into(),
97        }),
98    ));
99    if let Some(token) = predefined_type {
100        // The curve and surface forms draw from different activity
101        // enums with overlapping tokens, so the token is checked
102        // against the slot's own declared enum.
103        validate_activity_token(schema, entity_type, token)?;
104        fields.push(("PredefinedType", Value::Enum(token.into())));
105    }
106    Ok(tx.create(build_named(schema, entity_type, fields)?))
107}
108
109/// Authored fields for an `IfcStructuralResultGroup`.
110///
111/// A result group is an `IfcGroup`, not an `IfcProduct`: the schema
112/// declares no `ObjectPlacement` or `Representation`, so this draft has
113/// no fields for them rather than accepting and dropping them. Same
114/// reasoning as [`crate::LoadGroupDraft`].
115#[derive(Debug, Clone)]
116pub struct ResultGroupDraft {
117    /// `GlobalId`; must parse as a 22-character IFC GUID.
118    pub global_id: String,
119    /// `OwnerHistory`.
120    pub owner_history: Option<EntityId>,
121    /// `Name`.
122    pub name: Option<String>,
123    /// `Description`.
124    pub description: Option<String>,
125    /// `ObjectType`; required when `theory_type` is `USERDEFINED`.
126    pub object_type: Option<String>,
127    /// `TheoryType`, an `IfcAnalysisTheoryTypeEnum` token.
128    pub theory_type: String,
129    /// `ResultForLoadGroup`, an `IfcStructuralLoadGroup` reference.
130    pub result_for_load_group: Option<EntityId>,
131    /// `IsLinear`.
132    pub is_linear: bool,
133}
134
135/// Stage an `IfcStructuralResultGroup`.
136///
137/// # Errors
138///
139/// Refuses a `TheoryType` the schema does not declare, a
140/// `result_for_load_group` that is not an `IfcStructuralLoadGroup`, and a
141/// `USERDEFINED` theory with no `ObjectType` to name it (`HasObjectType`).
142pub fn stage_result_group(
143    tx: &mut Transaction,
144    model: &Model,
145    schema: &Schema,
146    draft: ResultGroupDraft,
147) -> StructuralResult<EntityId> {
148    const ENTITY: &str = "IfcStructuralResultGroup";
149    if Guid::parse(&draft.global_id).is_none() {
150        return Err(StructuralError::InvalidDraftValue {
151            entity_type: ENTITY,
152            attribute: "GlobalId",
153            expected: "a 22-character IFC GUID",
154        });
155    }
156    validate_optional_ref(tx, model, schema, draft.owner_history, "IfcOwnerHistory")?;
157    validate_optional_ref(
158        tx,
159        model,
160        schema,
161        draft.result_for_load_group,
162        "IfcStructuralLoadGroup",
163    )?;
164    validate_enum_token(schema, ENTITY, "TheoryType", &draft.theory_type)?;
165    // HasObjectType: a USERDEFINED theory names itself in ObjectType or
166    // says nothing at all.
167    if draft.theory_type.eq_ignore_ascii_case("USERDEFINED")
168        && draft
169            .object_type
170            .as_deref()
171            .is_none_or(|text| text.trim().is_empty())
172    {
173        return Err(StructuralError::SemanticViolation {
174            entity: None,
175            rule: "HasObjectType",
176        });
177    }
178    let fields = vec![
179        ("GlobalId", Value::Text(draft.global_id.into())),
180        ("OwnerHistory", optional_ref(draft.owner_history)),
181        ("Name", optional_text(draft.name)),
182        ("Description", optional_text(draft.description)),
183        ("ObjectType", optional_text(draft.object_type)),
184        ("TheoryType", Value::Enum(draft.theory_type.into())),
185        (
186            "ResultForLoadGroup",
187            optional_ref(draft.result_for_load_group),
188        ),
189        ("IsLinear", Value::Bool(draft.is_linear)),
190    ];
191    Ok(tx.create(build_named(schema, ENTITY, fields)?))
192}