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}