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