Skip to main content

ifc_structural/authoring/
condition.rs

1//! Authoring for boundary conditions.
2//!
3//! A structural connection without a boundary condition is unsolved: the
4//! condition is what states whether a support is pinned, fixed, sprung or
5//! free. The crate could read all four families and author none of them.
6//!
7//! Each family declares its own attribute names -- node conditions use
8//! `TranslationalStiffnessX`, edge conditions `...ByLengthX`, face
9//! conditions `...ByAreaX` -- so the entity type chosen here decides how
10//! the reader resolves every value. Writing node attributes under an edge
11//! entity would leave every stiffness unreadable, which is why the kind
12//! selects the entity and the slot order in one place.
13//!
14//! `IfcBoundaryCondition` contributes `Name` as slot 0, so the stiffness
15//! values start at slot 1 in every family.
16
17use ifc_model::{EntityId, Transaction, Value};
18use ifc_schema::Schema;
19
20use super::{build_named, stiffness};
21
22use crate::condition::{AxisValues, BoundaryConditionKind, FailureLimits, StiffnessValue};
23use crate::error::{StructuralError, StructuralResult};
24
25/// Authored fields for a boundary condition.
26#[derive(Debug, Clone, Copy, Default)]
27#[non_exhaustive]
28pub struct BoundaryConditionDraft<'a> {
29    /// `IfcBoundaryCondition.Name`, if given.
30    pub name: Option<&'a str>,
31    /// Translational stiffness per axis. `None` leaves the axis unset,
32    /// which the reader reports as absent rather than zero -- an unset
33    /// support is not a free one.
34    pub translational: AxisValues<Option<StiffnessValue>>,
35    /// Rotational stiffness per axis. Ignored for face conditions, which
36    /// declare no rotational attributes.
37    pub rotational: AxisValues<Option<StiffnessValue>>,
38    /// Warping stiffness. Only `NodeWarping` declares it.
39    pub warping: Option<StiffnessValue>,
40}
41
42impl<'a> BoundaryConditionDraft<'a> {
43    /// Starts an empty draft; every field is unset.
44    #[must_use]
45    pub fn new() -> Self {
46        Self {
47            name: None,
48            translational: Default::default(),
49            rotational: Default::default(),
50            warping: None,
51        }
52    }
53
54    /// Sets `name`: `IfcBoundaryCondition.Name`, if given.
55    #[must_use]
56    pub fn name(mut self, value: &'a str) -> Self {
57        self.name = Some(value);
58        self
59    }
60
61    /// Sets `translational`: Translational stiffness per axis. `None` leaves
62    /// the axis unset, which the reader reports as absent rather than zero --
63    /// an unset support is not a free one.
64    #[must_use]
65    pub fn translational(mut self, value: AxisValues<Option<StiffnessValue>>) -> Self {
66        self.translational = value;
67        self
68    }
69
70    /// Sets `rotational`: Rotational stiffness per axis. Ignored for face
71    /// conditions, which declare no rotational attributes.
72    #[must_use]
73    pub fn rotational(mut self, value: AxisValues<Option<StiffnessValue>>) -> Self {
74        self.rotational = value;
75        self
76    }
77
78    /// Sets `warping`: Warping stiffness. Only `NodeWarping` declares it.
79    #[must_use]
80    pub fn warping(mut self, value: StiffnessValue) -> Self {
81        self.warping = Some(value);
82        self
83    }
84}
85
86/// Stage a boundary condition of the given family, in the IFC4 form.
87///
88/// Equivalent to [`stage_boundary_condition_in`] with the bundled IFC4
89/// table. IFC4 and IFC4X3 declare the same stiffness SELECTs, so the record
90/// is correct in both. It is **not** correct in IFC2X3, whose stiffness
91/// attributes are plain measures that take no wrapper and admit no boolean:
92/// IFC2X3 callers use [`stage_boundary_condition_in`] with
93/// [`ifc_schema::ifc2x3`] (#200).
94///
95/// # Errors
96///
97/// As [`stage_boundary_condition_in`].
98pub fn stage_boundary_condition(
99    tx: &mut Transaction,
100    kind: BoundaryConditionKind,
101    draft: BoundaryConditionDraft<'_>,
102) -> StructuralResult<EntityId> {
103    stage_boundary_condition_in(tx, ifc_schema::ifc4(), kind, draft)
104}
105
106/// Stage a boundary condition of the given family in `schema`'s release.
107///
108/// The kind selects the entity, and with it the attribute names the reader
109/// will look for. Face conditions declare no rotational or warping values
110/// and warping is exclusive to `NodeWarping`, so supplying either where the
111/// family does not declare it is refused rather than silently dropped: a
112/// caller who sets a rotational spring on a face has a modelling error, not
113/// a formatting one.
114///
115/// The record is laid out by `schema`'s attribute names (IFC2X3 still calls
116/// the translational values `LinearStiffness...`), and each value is written
117/// in the form its declared type requires in that release (#200, #201):
118///
119/// ```text
120/// IFC2X3        LinearStiffnessX : IfcLinearStiffnessMeasure      1.5
121/// IFC4, IFC4X3  TranslationalStiffnessX : IfcTranslationalStiffnessSelect
122///                 a measure  IFCLINEARSTIFFNESSMEASURE(1.5)
123///                 a boolean  IFCBOOLEAN(.T.)
124/// ```
125///
126/// # Errors
127///
128/// [`StructuralError::InvalidDraftValue`] for a value the family does not
129/// declare, a non-finite measure, or a boolean where the release's declared
130/// type admits none (every IFC2X3 stiffness).
131/// [`StructuralError::UnsupportedAttribute`] or
132/// [`StructuralError::UnsupportedSchema`] if `schema` does not declare the
133/// family's attributes. Nothing is staged on error.
134pub fn stage_boundary_condition_in(
135    tx: &mut Transaction,
136    schema: &Schema,
137    kind: BoundaryConditionKind,
138    draft: BoundaryConditionDraft<'_>,
139) -> StructuralResult<EntityId> {
140    let entity_type = stiffness::entity_name(kind);
141    if kind == BoundaryConditionKind::Face && has_any(draft.rotational) {
142        return Err(StructuralError::InvalidDraftValue {
143            entity_type,
144            attribute: "RotationalStiffness",
145            expected: "no rotational stiffness: face conditions do not declare one",
146        });
147    }
148    if draft.warping.is_some() && kind != BoundaryConditionKind::NodeWarping {
149        return Err(StructuralError::InvalidDraftValue {
150            entity_type,
151            attribute: "WarpingStiffness",
152            expected: "no warping stiffness outside IfcBoundaryNodeConditionWarping",
153        });
154    }
155    for (axis, value) in axes(draft.translational).chain(axes(draft.rotational)) {
156        if let Some(StiffnessValue::Measure(measure)) = value {
157            if !measure.is_finite() {
158                return Err(StructuralError::InvalidDraftValue {
159                    entity_type,
160                    attribute: axis,
161                    expected: "a finite stiffness measure",
162                });
163            }
164        }
165    }
166    let mut fields = vec![("Name", optional_text(draft.name))];
167    fields.extend(stiffness::triple(schema, kind, true, draft.translational)?);
168    if kind != BoundaryConditionKind::Face {
169        fields.extend(stiffness::triple(schema, kind, false, draft.rotational)?);
170    }
171    if kind == BoundaryConditionKind::NodeWarping {
172        fields.push(stiffness::warping(schema, draft.warping)?);
173    }
174    Ok(tx.create(build_named(schema, entity_type, fields)?))
175}
176
177fn has_any(values: AxisValues<Option<StiffnessValue>>) -> bool {
178    values.x.is_some() || values.y.is_some() || values.z.is_some()
179}
180
181fn axes(
182    values: AxisValues<Option<StiffnessValue>>,
183) -> impl Iterator<Item = (&'static str, Option<StiffnessValue>)> {
184    [("X", values.x), ("Y", values.y), ("Z", values.z)].into_iter()
185}
186
187fn optional_text(value: Option<&str>) -> Value {
188    value.map_or(Value::Null, |text| Value::Text(text.into()))
189}
190
191/// Authored values for a connection condition.
192///
193/// The two families carry different measures -- failure limits are
194/// forces, slippage is a length -- so the kind selects both the entity
195/// and which of these fields is written.
196#[derive(Debug, Clone, Copy)]
197pub enum ConnectionConditionDraft {
198    /// `IfcFailureConnectionCondition`: the load at which the
199    /// connection gives way, per axis and sign.
200    Failure(FailureLimits),
201    /// `IfcSlippageConnectionCondition`: how far the connection moves
202    /// before it bears, per axis.
203    Slippage(AxisValues<Option<f64>>),
204}
205
206/// Stage an `IfcStructuralConnectionCondition` subtype.
207///
208/// `Name` is slot 0 in both families, so the measures start at slot 1.
209///
210/// # Errors
211///
212/// Refuses a non-finite measure. Every measure is optional: the schema
213/// states no limit rather than a zero one, and zero is a real value
214/// meaning the connection fails or slips under no load at all.
215pub fn stage_connection_condition(
216    tx: &mut Transaction,
217    schema: &Schema,
218    name: Option<&str>,
219    draft: ConnectionConditionDraft,
220) -> StructuralResult<EntityId> {
221    let (entity, values): (&'static str, Vec<(&'static str, Value)>) = match draft {
222        ConnectionConditionDraft::Failure(limits) => (
223            "IfcFailureConnectionCondition",
224            vec![
225                ("TensionFailureX", measure(limits.tension.x)),
226                ("TensionFailureY", measure(limits.tension.y)),
227                ("TensionFailureZ", measure(limits.tension.z)),
228                ("CompressionFailureX", measure(limits.compression.x)),
229                ("CompressionFailureY", measure(limits.compression.y)),
230                ("CompressionFailureZ", measure(limits.compression.z)),
231            ],
232        ),
233        ConnectionConditionDraft::Slippage(slippage) => (
234            "IfcSlippageConnectionCondition",
235            vec![
236                ("SlippageX", measure(slippage.x)),
237                ("SlippageY", measure(slippage.y)),
238                ("SlippageZ", measure(slippage.z)),
239            ],
240        ),
241    };
242    for (attribute, value) in &values {
243        if matches!(value, Value::Real(number) if !number.is_finite()) {
244            return Err(StructuralError::InvalidDraftValue {
245                entity_type: entity,
246                attribute,
247                expected: "finite measure or null",
248            });
249        }
250    }
251    let mut fields = vec![(
252        "Name",
253        name.map_or(Value::Null, |text| Value::Text(text.into())),
254    )];
255    fields.extend(values);
256    Ok(tx.create(build_named(schema, entity, fields)?))
257}
258
259/// An optional measure as a value.
260fn measure(value: Option<f64>) -> Value {
261    value.map_or(Value::Null, Value::Real)
262}