Skip to main content

ifc_structural/condition/
mod.rs

1//! Typed structural boundary and connection conditions.
2//!
3//! Values are preserved as authored. Boolean stiffness selectors are not
4//! interpreted as solver constraints, and numeric values are not unit-converted.
5
6use ifc_model::{EntityId, Value};
7use ifc_schema::SchemaVersion;
8
9use crate::error::{StructuralError, StructuralResult};
10use crate::view::Record;
11
12mod rotation;
13mod translation;
14
15/// Three values ordered by the structural X, Y, and Z axes.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct AxisValues<T> {
18    /// X-axis value.
19    pub x: T,
20    /// Y-axis value.
21    pub y: T,
22    /// Z-axis value.
23    pub z: T,
24}
25
26impl<T: Default> Default for AxisValues<T> {
27    fn default() -> Self {
28        Self {
29            x: T::default(),
30            y: T::default(),
31            z: T::default(),
32        }
33    }
34}
35
36/// One authored IFC stiffness SELECT member.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub enum StiffnessValue {
39    /// IFC4/IFC4X3 `IfcBoolean`, preserved literally without interpreting it.
40    Boolean(bool),
41    /// One finite stiffness or subgrade-reaction measure.
42    Measure(f64),
43}
44
45/// Concrete boundary-condition family.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum BoundaryConditionKind {
48    /// `IfcBoundaryEdgeCondition`.
49    Edge,
50    /// `IfcBoundaryFaceCondition`.
51    Face,
52    /// `IfcBoundaryNodeCondition`.
53    Node,
54    /// `IfcBoundaryNodeConditionWarping`.
55    NodeWarping,
56}
57
58/// Strict borrowed projection of a concrete IFC boundary condition.
59#[derive(Debug, Clone, Copy)]
60pub struct BoundaryCondition<'m, 's> {
61    record: Record<'m, 's>,
62    kind: BoundaryConditionKind,
63}
64
65impl<'m, 's> BoundaryCondition<'m, 's> {
66    pub(crate) fn from_record(record: Record<'m, 's>) -> StructuralResult<Self> {
67        let kind = if record
68            .schema
69            .is_a(&record.entity.type_name, "IfcBoundaryNodeConditionWarping")
70        {
71            BoundaryConditionKind::NodeWarping
72        } else if record
73            .schema
74            .is_a(&record.entity.type_name, "IfcBoundaryNodeCondition")
75        {
76            BoundaryConditionKind::Node
77        } else if record
78            .schema
79            .is_a(&record.entity.type_name, "IfcBoundaryEdgeCondition")
80        {
81            BoundaryConditionKind::Edge
82        } else if record
83            .schema
84            .is_a(&record.entity.type_name, "IfcBoundaryFaceCondition")
85        {
86            BoundaryConditionKind::Face
87        } else {
88            return Err(StructuralError::WrongType {
89                id: record.id,
90                expected: "concrete IfcBoundaryCondition",
91                actual: record.entity.type_name.to_string(),
92            });
93        };
94        Ok(Self { record, kind })
95    }
96
97    /// Entity identifier in the shared model graph.
98    #[must_use]
99    pub fn id(&self) -> EntityId {
100        self.record.id
101    }
102
103    /// Concrete boundary-condition family.
104    #[must_use]
105    pub fn kind(&self) -> BoundaryConditionKind {
106        self.kind
107    }
108
109    /// Optional authored name.
110    pub fn name(&self) -> StructuralResult<Option<&'m str>> {
111        self.record.optional_text("Name")
112    }
113
114    /// Translational stiffness values in X/Y/Z order.
115    pub fn translational_stiffnesses(
116        &self,
117    ) -> StructuralResult<AxisValues<Option<StiffnessValue>>> {
118        let (current, legacy) = match self.kind {
119            BoundaryConditionKind::Edge => (
120                [
121                    "TranslationalStiffnessByLengthX",
122                    "TranslationalStiffnessByLengthY",
123                    "TranslationalStiffnessByLengthZ",
124                ],
125                [
126                    "LinearStiffnessByLengthX",
127                    "LinearStiffnessByLengthY",
128                    "LinearStiffnessByLengthZ",
129                ],
130            ),
131            BoundaryConditionKind::Face => (
132                [
133                    "TranslationalStiffnessByAreaX",
134                    "TranslationalStiffnessByAreaY",
135                    "TranslationalStiffnessByAreaZ",
136                ],
137                [
138                    "LinearStiffnessByAreaX",
139                    "LinearStiffnessByAreaY",
140                    "LinearStiffnessByAreaZ",
141                ],
142            ),
143            BoundaryConditionKind::Node | BoundaryConditionKind::NodeWarping => (
144                [
145                    "TranslationalStiffnessX",
146                    "TranslationalStiffnessY",
147                    "TranslationalStiffnessZ",
148                ],
149                ["LinearStiffnessX", "LinearStiffnessY", "LinearStiffnessZ"],
150            ),
151        };
152        self.stiffness_axes(current, legacy)
153    }
154
155    /// Rotational stiffness values in X/Y/Z order.
156    ///
157    /// Face conditions do not declare rotational values and return three `None`
158    /// entries rather than manufacturing an unsupported field.
159    pub fn rotational_stiffnesses(&self) -> StructuralResult<AxisValues<Option<StiffnessValue>>> {
160        if self.kind == BoundaryConditionKind::Face {
161            return Ok(AxisValues::default());
162        }
163        let names = if self.kind == BoundaryConditionKind::Edge {
164            [
165                "RotationalStiffnessByLengthX",
166                "RotationalStiffnessByLengthY",
167                "RotationalStiffnessByLengthZ",
168            ]
169        } else {
170            [
171                "RotationalStiffnessX",
172                "RotationalStiffnessY",
173                "RotationalStiffnessZ",
174            ]
175        };
176        self.stiffness_axes(names, names)
177    }
178
179    /// Optional warping stiffness for `IfcBoundaryNodeConditionWarping`.
180    pub fn warping_stiffness(&self) -> StructuralResult<Option<StiffnessValue>> {
181        if self.kind != BoundaryConditionKind::NodeWarping {
182            return Ok(None);
183        }
184        decode_stiffness(&self.record, "WarpingStiffness")
185    }
186
187    fn stiffness_axes(
188        &self,
189        current: [&'static str; 3],
190        legacy: [&'static str; 3],
191    ) -> StructuralResult<AxisValues<Option<StiffnessValue>>> {
192        let value = |index| {
193            let selected = if self.record.has_attribute(current[index]) {
194                current[index]
195            } else {
196                legacy[index]
197            };
198            decode_stiffness(&self.record, selected)
199        };
200        Ok(AxisValues {
201            x: value(0)?,
202            y: value(1)?,
203            z: value(2)?,
204        })
205    }
206}
207
208fn decode_stiffness(
209    record: &Record<'_, '_>,
210    attribute: &'static str,
211) -> StructuralResult<Option<StiffnessValue>> {
212    match record.value(attribute)?.unwrap_typed() {
213        Value::Null | Value::Derived => Ok(None),
214        Value::Bool(value) if record.schema.version() != Some(SchemaVersion::Ifc2x3) => {
215            Ok(Some(StiffnessValue::Boolean(*value)))
216        }
217        Value::Integer(value) => Ok(Some(StiffnessValue::Measure(*value as f64))),
218        Value::Real(value) if value.is_finite() => Ok(Some(StiffnessValue::Measure(*value))),
219        _ => Err(StructuralError::InvalidValue {
220            entity: record.id,
221            attribute,
222            expected: "finite stiffness measure or schema-supported boolean",
223        }),
224    }
225}
226
227/// Concrete structural connection-condition family.
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub enum ConnectionConditionKind {
230    /// `IfcFailureConnectionCondition`.
231    Failure,
232    /// `IfcSlippageConnectionCondition`.
233    Slippage,
234}
235
236/// Authored force limits for a failure connection condition.
237#[derive(Debug, Clone, Copy, PartialEq)]
238pub struct FailureLimits {
239    /// Tension limits in X/Y/Z order.
240    pub tension: AxisValues<Option<f64>>,
241    /// Compression limits in X/Y/Z order.
242    pub compression: AxisValues<Option<f64>>,
243}
244
245/// Strict borrowed projection of a concrete connection condition.
246#[derive(Debug, Clone, Copy)]
247pub struct ConnectionCondition<'m, 's> {
248    record: Record<'m, 's>,
249    kind: ConnectionConditionKind,
250}
251
252impl<'m, 's> ConnectionCondition<'m, 's> {
253    pub(crate) fn from_record(record: Record<'m, 's>) -> StructuralResult<Self> {
254        let kind = if record
255            .schema
256            .is_a(&record.entity.type_name, "IfcFailureConnectionCondition")
257        {
258            ConnectionConditionKind::Failure
259        } else if record
260            .schema
261            .is_a(&record.entity.type_name, "IfcSlippageConnectionCondition")
262        {
263            ConnectionConditionKind::Slippage
264        } else {
265            return Err(StructuralError::WrongType {
266                id: record.id,
267                expected: "concrete IfcStructuralConnectionCondition",
268                actual: record.entity.type_name.to_string(),
269            });
270        };
271        Ok(Self { record, kind })
272    }
273
274    /// Entity identifier in the shared model graph.
275    #[must_use]
276    pub fn id(&self) -> EntityId {
277        self.record.id
278    }
279
280    /// Concrete connection-condition family.
281    #[must_use]
282    pub fn kind(&self) -> ConnectionConditionKind {
283        self.kind
284    }
285
286    /// Optional authored name.
287    pub fn name(&self) -> StructuralResult<Option<&'m str>> {
288        self.record.optional_text("Name")
289    }
290
291    /// Failure limits, or `None` for a slippage condition.
292    pub fn failure_limits(&self) -> StructuralResult<Option<FailureLimits>> {
293        if self.kind != ConnectionConditionKind::Failure {
294            return Ok(None);
295        }
296        Ok(Some(FailureLimits {
297            tension: number_axes(
298                &self.record,
299                ["TensionFailureX", "TensionFailureY", "TensionFailureZ"],
300            )?,
301            compression: number_axes(
302                &self.record,
303                [
304                    "CompressionFailureX",
305                    "CompressionFailureY",
306                    "CompressionFailureZ",
307                ],
308            )?,
309        }))
310    }
311
312    /// Slippage lengths in X/Y/Z order, or `None` for a failure condition.
313    pub fn slippages(&self) -> StructuralResult<Option<AxisValues<Option<f64>>>> {
314        if self.kind != ConnectionConditionKind::Slippage {
315            return Ok(None);
316        }
317        Ok(Some(number_axes(
318            &self.record,
319            ["SlippageX", "SlippageY", "SlippageZ"],
320        )?))
321    }
322}
323
324fn number_axes(
325    record: &Record<'_, '_>,
326    names: [&'static str; 3],
327) -> StructuralResult<AxisValues<Option<f64>>> {
328    Ok(AxisValues {
329        x: record.optional_number(names[0])?,
330        y: record.optional_number(names[1])?,
331        z: record.optional_number(names[2])?,
332    })
333}