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