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)]
46#[non_exhaustive]
47pub struct ReactionDraft<'a> {
48    /// `IfcRoot` attributes shared with other structural entities.
49    pub root: StructuralRootDraft,
50    /// `AppliedLoad`; the reaction force the analysis produced.
51    pub applied_load: EntityId,
52    /// `GlobalOrLocal`.
53    pub coordinate_system: CoordinateSystem,
54    /// Which subtype, and its PredefinedType where one applies.
55    pub kind: ReactionDraftKind<'a>,
56}
57
58impl<'a> ReactionDraft<'a> {
59    /// Starts a draft from its required fields; every other field is unset.
60    #[must_use]
61    pub fn new(
62        root: StructuralRootDraft,
63        applied_load: EntityId,
64        coordinate_system: CoordinateSystem,
65        kind: ReactionDraftKind<'a>,
66    ) -> Self {
67        Self {
68            root,
69            applied_load,
70            coordinate_system,
71            kind,
72        }
73    }
74}
75
76/// Stage an `IfcStructuralReaction` subtype.
77///
78/// # Errors
79///
80/// Refuses an `applied_load` that is not an `IfcStructuralLoad`, and a
81/// `predefined_type` the target schema does not declare for the
82/// subtype's own enum. The two typed forms draw from *different*
83/// enums, so a token valid on a curve may be undeclared on a surface.
84pub fn stage_reaction(
85    tx: &mut Transaction,
86    model: &Model,
87    schema: &Schema,
88    draft: ReactionDraft<'_>,
89) -> StructuralResult<EntityId> {
90    validate_root(tx, model, schema, &draft.root)?;
91    let (entity_type, predefined_type) = match draft.kind {
92        ReactionDraftKind::Point => ("IfcStructuralPointReaction", None),
93        ReactionDraftKind::Curve { predefined_type } => {
94            ("IfcStructuralCurveReaction", Some(predefined_type))
95        }
96        ReactionDraftKind::Surface { predefined_type } => {
97            ("IfcStructuralSurfaceReaction", Some(predefined_type))
98        }
99    };
100    validate_ref_select(
101        tx,
102        model,
103        schema,
104        draft.applied_load,
105        "structural load",
106        &["IfcStructuralLoad"],
107    )?;
108
109    let mut fields = root_fields(draft.root);
110    fields.push(("AppliedLoad", Value::Ref(draft.applied_load)));
111    fields.push((
112        "GlobalOrLocal",
113        Value::Enum(match draft.coordinate_system {
114            CoordinateSystem::Global => "GLOBAL_COORDS".into(),
115            CoordinateSystem::Local => "LOCAL_COORDS".into(),
116        }),
117    ));
118    if let Some(token) = predefined_type {
119        // The curve and surface forms draw from different activity
120        // enums with overlapping tokens, so the token is checked
121        // against the slot's own declared enum.
122        validate_activity_token(schema, entity_type, token)?;
123        fields.push(("PredefinedType", Value::Enum(token.into())));
124    }
125    Ok(tx.create(build_named(schema, entity_type, fields)?))
126}
127
128/// Authored fields for an `IfcStructuralResultGroup`.
129///
130/// A result group is an `IfcGroup`, not an `IfcProduct`: the schema
131/// declares no `ObjectPlacement` or `Representation`, so this draft has
132/// no fields for them rather than accepting and dropping them. Same
133/// reasoning as [`crate::LoadGroupDraft`].
134#[derive(Debug, Clone)]
135#[non_exhaustive]
136pub struct ResultGroupDraft {
137    /// `GlobalId`; must parse as a 22-character IFC GUID.
138    pub global_id: String,
139    /// `OwnerHistory`.
140    pub owner_history: Option<EntityId>,
141    /// `Name`.
142    pub name: Option<String>,
143    /// `Description`.
144    pub description: Option<String>,
145    /// `ObjectType`; required when `theory_type` is `USERDEFINED`.
146    pub object_type: Option<String>,
147    /// `TheoryType`, an `IfcAnalysisTheoryTypeEnum` token.
148    pub theory_type: String,
149    /// `ResultForLoadGroup`, an `IfcStructuralLoadGroup` reference.
150    pub result_for_load_group: Option<EntityId>,
151    /// `IsLinear`.
152    pub is_linear: bool,
153}
154
155impl ResultGroupDraft {
156    /// Starts a draft from its required fields; every other field is unset.
157    #[must_use]
158    pub fn new(
159        global_id: impl Into<String>,
160        theory_type: impl Into<String>,
161        is_linear: bool,
162    ) -> Self {
163        Self {
164            global_id: global_id.into(),
165            owner_history: None,
166            name: None,
167            description: None,
168            object_type: None,
169            theory_type: theory_type.into(),
170            result_for_load_group: None,
171            is_linear,
172        }
173    }
174
175    /// Sets `owner_history`: `OwnerHistory`.
176    #[must_use]
177    pub fn owner_history(mut self, value: EntityId) -> Self {
178        self.owner_history = Some(value);
179        self
180    }
181
182    /// Sets `name`: `Name`.
183    #[must_use]
184    pub fn name(mut self, value: impl Into<String>) -> Self {
185        self.name = Some(value.into());
186        self
187    }
188
189    /// Sets `description`: `Description`.
190    #[must_use]
191    pub fn description(mut self, value: impl Into<String>) -> Self {
192        self.description = Some(value.into());
193        self
194    }
195
196    /// Sets `object_type`: `ObjectType`; required when `theory_type` is
197    /// `USERDEFINED`.
198    #[must_use]
199    pub fn object_type(mut self, value: impl Into<String>) -> Self {
200        self.object_type = Some(value.into());
201        self
202    }
203
204    /// Sets `result_for_load_group`: `ResultForLoadGroup`, an
205    /// `IfcStructuralLoadGroup` reference.
206    #[must_use]
207    pub fn result_for_load_group(mut self, value: EntityId) -> Self {
208        self.result_for_load_group = Some(value);
209        self
210    }
211}
212
213/// Stage an `IfcStructuralResultGroup`.
214///
215/// # Errors
216///
217/// Refuses a `TheoryType` the schema does not declare, a
218/// `result_for_load_group` that is not an `IfcStructuralLoadGroup`, and a
219/// `USERDEFINED` theory with no `ObjectType` to name it (`HasObjectType`).
220pub fn stage_result_group(
221    tx: &mut Transaction,
222    model: &Model,
223    schema: &Schema,
224    draft: ResultGroupDraft,
225) -> StructuralResult<EntityId> {
226    const ENTITY: &str = "IfcStructuralResultGroup";
227    if Guid::parse(&draft.global_id).is_none() {
228        return Err(StructuralError::InvalidDraftValue {
229            entity_type: ENTITY,
230            attribute: "GlobalId",
231            expected: "a 22-character IFC GUID",
232        });
233    }
234    validate_optional_ref(tx, model, schema, draft.owner_history, "IfcOwnerHistory")?;
235    validate_optional_ref(
236        tx,
237        model,
238        schema,
239        draft.result_for_load_group,
240        "IfcStructuralLoadGroup",
241    )?;
242    validate_enum_token(schema, ENTITY, "TheoryType", &draft.theory_type)?;
243    // HasObjectType: a USERDEFINED theory names itself in ObjectType or
244    // says nothing at all.
245    if draft.theory_type.eq_ignore_ascii_case("USERDEFINED")
246        && draft
247            .object_type
248            .as_deref()
249            .is_none_or(|text| text.trim().is_empty())
250    {
251        return Err(StructuralError::SemanticViolation {
252            entity: None,
253            rule: "HasObjectType",
254        });
255    }
256    let fields = vec![
257        ("GlobalId", Value::Text(draft.global_id.into())),
258        ("OwnerHistory", optional_ref(draft.owner_history)),
259        ("Name", optional_text(draft.name)),
260        ("Description", optional_text(draft.description)),
261        ("ObjectType", optional_text(draft.object_type)),
262        ("TheoryType", Value::Enum(draft.theory_type.into())),
263        (
264            "ResultForLoadGroup",
265            optional_ref(draft.result_for_load_group),
266        ),
267        ("IsLinear", Value::Bool(draft.is_linear)),
268    ];
269    Ok(tx.create(build_named(schema, ENTITY, fields)?))
270}