Skip to main content

ifc_structural/result/
reaction.rs

1//! Borrowed structural-reaction projections.
2
3use ifc_model::EntityId;
4
5use crate::action::CoordinateSystem;
6use crate::error::{StructuralError, StructuralResult};
7use crate::view::Record;
8
9/// Concrete structural-reaction family.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ReactionKind {
12    /// `IfcStructuralPointReaction`.
13    Point,
14    /// IFC4+ `IfcStructuralCurveReaction`.
15    Curve,
16    /// IFC4+ `IfcStructuralSurfaceReaction`.
17    Surface,
18}
19
20/// Borrowed authored structural reaction.
21///
22/// The referenced load is returned literally. This type does not compute,
23/// transform, combine, or solve reaction values.
24#[derive(Debug, Clone, Copy)]
25pub struct Reaction<'m, 's> {
26    record: Record<'m, 's>,
27    kind: ReactionKind,
28}
29
30impl<'m, 's> Reaction<'m, 's> {
31    pub(crate) fn from_record(record: Record<'m, 's>) -> StructuralResult<Self> {
32        let kind = if record
33            .schema
34            .is_a(&record.entity.type_name, "IfcStructuralPointReaction")
35        {
36            ReactionKind::Point
37        } else if record
38            .schema
39            .is_a(&record.entity.type_name, "IfcStructuralCurveReaction")
40        {
41            ReactionKind::Curve
42        } else if record
43            .schema
44            .is_a(&record.entity.type_name, "IfcStructuralSurfaceReaction")
45        {
46            ReactionKind::Surface
47        } else {
48            return Err(StructuralError::WrongType {
49                id: record.id,
50                expected: "concrete IfcStructuralReaction",
51                actual: record.entity.type_name.to_string(),
52            });
53        };
54        Ok(Self { record, kind })
55    }
56
57    /// Entity identifier in the shared model graph.
58    #[must_use]
59    pub fn id(&self) -> EntityId {
60        self.record.id
61    }
62
63    /// Concrete reaction family.
64    #[must_use]
65    pub fn kind(&self) -> ReactionKind {
66        self.kind
67    }
68
69    /// Optional authored name after semantic validation.
70    pub fn name(&self) -> StructuralResult<Option<&'m str>> {
71        self.validate_semantics()?;
72        self.record.optional_text("Name")
73    }
74
75    /// Authored applied-load/result record.
76    pub fn applied_load(&self) -> StructuralResult<EntityId> {
77        self.validate_semantics()
78    }
79
80    /// Authored global/local coordinate selector.
81    pub fn coordinate_system(&self) -> StructuralResult<CoordinateSystem> {
82        self.validate_semantics()?;
83        match self
84            .record
85            .required_enum("GlobalOrLocal")?
86            .to_ascii_uppercase()
87            .as_str()
88        {
89            "GLOBAL_COORDS" => Ok(CoordinateSystem::Global),
90            "LOCAL_COORDS" => Ok(CoordinateSystem::Local),
91            _ => Err(StructuralError::InvalidValue {
92                entity: self.record.id,
93                attribute: "GlobalOrLocal",
94                expected: "IfcGlobalOrLocalEnum",
95            }),
96        }
97    }
98
99    /// IFC4+ curve/surface predefined type, absent on point reactions.
100    pub fn predefined_type(&self) -> StructuralResult<Option<&'m str>> {
101        self.validate_semantics()?;
102        if !self.record.has_attribute("PredefinedType") {
103            return Ok(None);
104        }
105        self.record.required_enum("PredefinedType").map(Some)
106    }
107
108    fn validate_semantics(&self) -> StructuralResult<EntityId> {
109        let applied_load = if self.kind == ReactionKind::Point {
110            self.record.required_ref_select(
111                "AppliedLoad",
112                "point-reaction structural load",
113                &[
114                    "IfcStructuralLoadSingleForce",
115                    "IfcStructuralLoadSingleDisplacement",
116                ],
117            )?
118        } else {
119            self.record
120                .required_ref("AppliedLoad", "IfcStructuralLoad")?
121        };
122        if self.record.has_attribute("PredefinedType") {
123            let predefined_type = self.record.required_enum("PredefinedType")?;
124            self.record.require_object_type_if(
125                predefined_type.eq_ignore_ascii_case("USERDEFINED"),
126                "USERDEFINED structural reaction requires ObjectType",
127            )?;
128            if self.kind == ReactionKind::Curve
129                && (predefined_type.eq_ignore_ascii_case("SINUS")
130                    || predefined_type.eq_ignore_ascii_case("PARABOLA"))
131            {
132                return Err(StructuralError::SemanticViolation {
133                    entity: Some(self.record.id),
134                    rule: "IfcStructuralCurveReaction.SuitablePredefinedType",
135                });
136            }
137        }
138        Ok(applied_load)
139    }
140}