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::{Entity, EntityId, Transaction, Value};
18use ifc_schema::Schema;
19
20use super::build_named;
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.
42///
43/// The kind selects the entity, and with it the attribute names the reader
44/// will look for. Face conditions declare no rotational or warping values
45/// and warping is exclusive to `NodeWarping`, so supplying either where the
46/// family does not declare it is refused rather than silently dropped: a
47/// caller who sets a rotational spring on a face has a modelling error, not
48/// a formatting one.
49pub fn stage_boundary_condition(
50    tx: &mut Transaction,
51    kind: BoundaryConditionKind,
52    draft: BoundaryConditionDraft<'_>,
53) -> StructuralResult<EntityId> {
54    let entity_type = entity_name(kind);
55    if kind == BoundaryConditionKind::Face && has_any(draft.rotational) {
56        return Err(crate::error::StructuralError::InvalidDraftValue {
57            entity_type,
58            attribute: "RotationalStiffness",
59            expected: "no rotational stiffness: face conditions do not declare one",
60        });
61    }
62    if draft.warping.is_some() && kind != BoundaryConditionKind::NodeWarping {
63        return Err(crate::error::StructuralError::InvalidDraftValue {
64            entity_type,
65            attribute: "WarpingStiffness",
66            expected: "no warping stiffness outside IfcBoundaryNodeConditionWarping",
67        });
68    }
69    for (axis, value) in axes(draft.translational).chain(axes(draft.rotational)) {
70        if let Some(StiffnessValue::Measure(measure)) = value {
71            if !measure.is_finite() {
72                return Err(crate::error::StructuralError::InvalidDraftValue {
73                    entity_type,
74                    attribute: axis,
75                    expected: "a finite stiffness measure",
76                });
77            }
78        }
79    }
80    let mut attributes = vec![optional_text(draft.name)];
81    attributes.extend(stiffness_triple(draft.translational, kind, true));
82    if kind != BoundaryConditionKind::Face {
83        attributes.extend(stiffness_triple(draft.rotational, kind, false));
84    }
85    if kind == BoundaryConditionKind::NodeWarping {
86        attributes.push(stiffness_value(draft.warping, kind, false));
87    }
88    Ok(tx.create(Entity::new(entity_type, attributes)))
89}
90
91fn entity_name(kind: BoundaryConditionKind) -> &'static str {
92    match kind {
93        BoundaryConditionKind::Edge => "IFCBOUNDARYEDGECONDITION",
94        BoundaryConditionKind::Face => "IFCBOUNDARYFACECONDITION",
95        BoundaryConditionKind::Node => "IFCBOUNDARYNODECONDITION",
96        BoundaryConditionKind::NodeWarping => "IFCBOUNDARYNODECONDITIONWARPING",
97    }
98}
99
100/// The measure each family uses. A node condition carries a stiffness, an
101/// edge condition stiffness per length, a face condition per area: writing
102/// the wrong wrapper states a different physical quantity.
103fn measure_name(kind: BoundaryConditionKind, translational: bool) -> &'static str {
104    match (kind, translational) {
105        (BoundaryConditionKind::Edge, true) => "IFCMODULUSOFTRANSLATIONALSUBGRADEREACTIONMEASURE",
106        (BoundaryConditionKind::Edge, false) => "IFCMODULUSOFROTATIONALSUBGRADEREACTIONMEASURE",
107        (BoundaryConditionKind::Face, _) => "IFCMODULUSOFSUBGRADEREACTIONMEASURE",
108        (_, true) => "IFCLINEARSTIFFNESSMEASURE",
109        (_, false) => "IFCROTATIONALSTIFFNESSMEASURE",
110    }
111}
112
113fn stiffness_value(
114    value: Option<StiffnessValue>,
115    kind: BoundaryConditionKind,
116    translational: bool,
117) -> Value {
118    match value {
119        None => Value::Null,
120        Some(StiffnessValue::Boolean(flag)) => Value::Bool(flag),
121        Some(StiffnessValue::Measure(measure)) => Value::Typed {
122            type_name: measure_name(kind, translational).into(),
123            value: Box::new(Value::Real(measure)),
124        },
125    }
126}
127
128fn stiffness_triple(
129    values: AxisValues<Option<StiffnessValue>>,
130    kind: BoundaryConditionKind,
131    translational: bool,
132) -> [Value; 3] {
133    [
134        stiffness_value(values.x, kind, translational),
135        stiffness_value(values.y, kind, translational),
136        stiffness_value(values.z, kind, translational),
137    ]
138}
139
140fn has_any(values: AxisValues<Option<StiffnessValue>>) -> bool {
141    values.x.is_some() || values.y.is_some() || values.z.is_some()
142}
143
144fn axes(
145    values: AxisValues<Option<StiffnessValue>>,
146) -> impl Iterator<Item = (&'static str, Option<StiffnessValue>)> {
147    [("X", values.x), ("Y", values.y), ("Z", values.z)].into_iter()
148}
149
150fn optional_text(value: Option<&str>) -> Value {
151    value.map_or(Value::Null, |text| Value::Text(text.into()))
152}
153
154/// Authored values for a connection condition.
155///
156/// The two families carry different measures -- failure limits are
157/// forces, slippage is a length -- so the kind selects both the entity
158/// and which of these fields is written.
159#[derive(Debug, Clone, Copy)]
160pub enum ConnectionConditionDraft {
161    /// `IfcFailureConnectionCondition`: the load at which the
162    /// connection gives way, per axis and sign.
163    Failure(FailureLimits),
164    /// `IfcSlippageConnectionCondition`: how far the connection moves
165    /// before it bears, per axis.
166    Slippage(AxisValues<Option<f64>>),
167}
168
169/// Stage an `IfcStructuralConnectionCondition` subtype.
170///
171/// `Name` is slot 0 in both families, so the measures start at slot 1.
172///
173/// # Errors
174///
175/// Refuses a non-finite measure. Every measure is optional: the schema
176/// states no limit rather than a zero one, and zero is a real value
177/// meaning the connection fails or slips under no load at all.
178pub fn stage_connection_condition(
179    tx: &mut Transaction,
180    schema: &Schema,
181    name: Option<&str>,
182    draft: ConnectionConditionDraft,
183) -> StructuralResult<EntityId> {
184    let (entity, values): (&'static str, Vec<(&'static str, Value)>) = match draft {
185        ConnectionConditionDraft::Failure(limits) => (
186            "IfcFailureConnectionCondition",
187            vec![
188                ("TensionFailureX", measure(limits.tension.x)),
189                ("TensionFailureY", measure(limits.tension.y)),
190                ("TensionFailureZ", measure(limits.tension.z)),
191                ("CompressionFailureX", measure(limits.compression.x)),
192                ("CompressionFailureY", measure(limits.compression.y)),
193                ("CompressionFailureZ", measure(limits.compression.z)),
194            ],
195        ),
196        ConnectionConditionDraft::Slippage(slippage) => (
197            "IfcSlippageConnectionCondition",
198            vec![
199                ("SlippageX", measure(slippage.x)),
200                ("SlippageY", measure(slippage.y)),
201                ("SlippageZ", measure(slippage.z)),
202            ],
203        ),
204    };
205    for (attribute, value) in &values {
206        if matches!(value, Value::Real(number) if !number.is_finite()) {
207            return Err(StructuralError::InvalidDraftValue {
208                entity_type: entity,
209                attribute,
210                expected: "finite measure or null",
211            });
212        }
213    }
214    let mut fields = vec![(
215        "Name",
216        name.map_or(Value::Null, |text| Value::Text(text.into())),
217    )];
218    fields.extend(values);
219    Ok(tx.create(build_named(schema, entity, fields)?))
220}
221
222/// An optional measure as a value.
223fn measure(value: Option<f64>) -> Value {
224    value.map_or(Value::Null, Value::Real)
225}