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}