Skip to main content

ifc_structural/authoring/
item.rs

1use ifc_model::guid::Guid;
2use ifc_model::{EntityId, Model, Transaction, Value};
3use ifc_schema::Schema;
4
5use super::{build_named, optional_ref, optional_text, validate_optional_ref, validate_ref};
6use crate::error::{StructuralError, StructuralResult};
7
8/// Staged `IfcRoot`-level attributes shared by every staged structural entity.
9#[derive(Debug, Clone)]
10pub struct StructuralRootDraft {
11    /// `GlobalId`; must parse as a 22-character IFC GUID.
12    pub global_id: String,
13    /// `OwnerHistory`, validated against the model/transaction if present.
14    pub owner_history: Option<EntityId>,
15    /// `Name`.
16    pub name: Option<String>,
17    /// `Description`.
18    pub description: Option<String>,
19    /// `ObjectType`; required non-blank on entities whose predefined type is `USERDEFINED`.
20    pub object_type: Option<String>,
21    /// `ObjectPlacement`, an `IfcObjectPlacement` reference.
22    pub object_placement: Option<EntityId>,
23    /// `Representation`, an `IfcProductRepresentation` reference.
24    pub representation: Option<EntityId>,
25}
26
27impl Default for StructuralRootDraft {
28    fn default() -> Self {
29        Self {
30            global_id: "0000000000000000000000".into(),
31            owner_history: None,
32            name: None,
33            description: None,
34            object_type: None,
35            object_placement: None,
36            representation: None,
37        }
38    }
39}
40
41/// Value of `IfcStructuralCurveMemberTypeEnum`/`IfcStructuralSurfaceMemberTypeEnum` naming a member's structural role.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum MemberPredefinedType {
44    /// `RIGID_JOINED_MEMBER` (curve members only).
45    RigidJoinedMember,
46    /// `PIN_JOINED_MEMBER` (curve members only).
47    PinJoinedMember,
48    /// `CABLE` (curve members only).
49    Cable,
50    /// `TENSION_MEMBER` (curve members only).
51    TensionMember,
52    /// `COMPRESSION_MEMBER` (curve members only).
53    CompressionMember,
54    /// `BENDING_ELEMENT` (surface members only).
55    BendingElement,
56    /// `MEMBRANE_ELEMENT` (surface members only).
57    MembraneElement,
58    /// `SHELL` (surface members only); requires `Thickness` to be set.
59    Shell,
60    /// `USERDEFINED`: a custom role named by `ObjectType`.
61    UserDefined,
62    /// `NOTDEFINED`: no structural role classification given.
63    NotDefined,
64}
65
66impl MemberPredefinedType {
67    fn token(self) -> &'static str {
68        match self {
69            Self::RigidJoinedMember => "RIGID_JOINED_MEMBER",
70            Self::PinJoinedMember => "PIN_JOINED_MEMBER",
71            Self::Cable => "CABLE",
72            Self::TensionMember => "TENSION_MEMBER",
73            Self::CompressionMember => "COMPRESSION_MEMBER",
74            Self::BendingElement => "BENDING_ELEMENT",
75            Self::MembraneElement => "MEMBRANE_ELEMENT",
76            Self::Shell => "SHELL",
77            Self::UserDefined => "USERDEFINED",
78            Self::NotDefined => "NOTDEFINED",
79        }
80    }
81}
82
83/// Staged member subtype for [`MemberDraft::kind`].
84#[derive(Debug, Clone)]
85pub enum MemberDraftKind {
86    /// Stages an `IfcStructuralCurveMember`.
87    Curve {
88        /// `PredefinedType`; must be a curve-member role (rigid/pin-joined, cable, tension/compression, user/not defined).
89        predefined_type: MemberPredefinedType,
90        /// `Axis`; required when the target schema declares the attribute for this type.
91        axis: Option<EntityId>,
92        /// Stage `IfcStructuralCurveMemberVarying` instead.
93        ///
94        /// The varying subtype adds no attribute: it declares that the
95        /// axis properties change along the member, which the analysis
96        /// side reads from the type name alone. Same slots, different
97        /// claim about the member.
98        varying: bool,
99    },
100    /// Stages an `IfcStructuralSurfaceMember`.
101    Surface {
102        /// `PredefinedType`; must be a surface-member role (bending/membrane element, shell, user/not defined).
103        predefined_type: MemberPredefinedType,
104        /// `Thickness`; must be a positive finite value, and mandatory when `predefined_type` is `Shell`.
105        thickness: Option<f64>,
106        /// Stage `IfcStructuralSurfaceMemberVarying` instead.
107        ///
108        /// `Thickness` on the varying form is the value at a reference
109        /// point, not a constant; the variation itself is carried by an
110        /// attached property set, so the slot layout is unchanged.
111        varying: bool,
112    },
113}
114
115/// Staged fields for creating an `IfcStructuralMember` via [`stage_member`].
116#[derive(Debug, Clone)]
117pub struct MemberDraft {
118    /// `IfcRoot` attributes shared with other staged structural entities.
119    pub root: StructuralRootDraft,
120    /// Which `IfcStructuralMember` subtype to create, and its subtype-specific attributes.
121    pub kind: MemberDraftKind,
122}
123
124/// Staged connection subtype for [`ConnectionDraft::kind`].
125#[derive(Debug, Clone)]
126pub enum ConnectionDraftKind {
127    /// Stages an `IfcStructuralPointConnection`.
128    Point {
129        /// `AppliedCondition`, an `IfcBoundaryCondition` reference.
130        applied_condition: Option<EntityId>,
131        /// `ConditionCoordinateSystem`, an `IfcAxis2Placement3D` reference.
132        condition_coordinate_system: Option<EntityId>,
133    },
134    /// Stages an `IfcStructuralCurveConnection`.
135    Curve {
136        /// `AppliedCondition`, an `IfcBoundaryCondition` reference.
137        applied_condition: Option<EntityId>,
138        /// `Axis`; required when the target schema declares the attribute for this type.
139        axis: Option<EntityId>,
140    },
141    /// Stages an `IfcStructuralSurfaceConnection`.
142    Surface {
143        /// `AppliedCondition`, an `IfcBoundaryCondition` reference.
144        applied_condition: Option<EntityId>,
145    },
146}
147
148/// Staged fields for creating an `IfcStructuralConnection` via [`stage_connection`].
149#[derive(Debug, Clone)]
150pub struct ConnectionDraft {
151    /// `IfcRoot` attributes shared with other staged structural entities.
152    pub root: StructuralRootDraft,
153    /// Which `IfcStructuralConnection` subtype to create, and its subtype-specific attributes.
154    pub kind: ConnectionDraftKind,
155}
156
157pub(super) fn validate_root(
158    tx: &Transaction,
159    model: &Model,
160    schema: &Schema,
161    root: &StructuralRootDraft,
162) -> StructuralResult<()> {
163    if Guid::parse(&root.global_id).is_none() {
164        return Err(StructuralError::InvalidGlobalId);
165    }
166    validate_optional_ref(tx, model, schema, root.owner_history, "IfcOwnerHistory")?;
167    validate_optional_ref(
168        tx,
169        model,
170        schema,
171        root.object_placement,
172        "IfcObjectPlacement",
173    )?;
174    validate_optional_ref(
175        tx,
176        model,
177        schema,
178        root.representation,
179        "IfcProductRepresentation",
180    )?;
181    Ok(())
182}
183
184pub(super) fn root_fields(root: StructuralRootDraft) -> Vec<(&'static str, Value)> {
185    vec![
186        ("GlobalId", Value::Text(root.global_id.into())),
187        ("OwnerHistory", optional_ref(root.owner_history)),
188        ("Name", optional_text(root.name)),
189        ("Description", optional_text(root.description)),
190        ("ObjectType", optional_text(root.object_type)),
191        ("ObjectPlacement", optional_ref(root.object_placement)),
192        ("Representation", optional_ref(root.representation)),
193    ]
194}
195
196/// Stage an `IfcStructuralMember` create edit on `tx`.
197///
198/// Fails with [`StructuralError::InvalidDraftValue`] if `predefined_type`
199/// does not belong to the curve/surface role set matching `kind`, or if
200/// `Thickness` is set but not positive and finite;
201/// [`StructuralError::SemanticViolation`] if `predefined_type` is
202/// `UserDefined` without a non-blank `object_type`; [`StructuralError::MissingRequired`]
203/// if `Axis` is required by the schema but unset, or `predefined_type` is
204/// `Shell` without a `Thickness`; and [`StructuralError::UnsupportedAttribute`]
205/// if `axis` is set but the target schema declares no `Axis` attribute for
206/// this type. Returns the id staged for the new entity.
207pub fn stage_member(
208    tx: &mut Transaction,
209    model: &Model,
210    schema: &Schema,
211    draft: MemberDraft,
212) -> StructuralResult<EntityId> {
213    validate_root(tx, model, schema, &draft.root)?;
214    let (entity_type, predefined_type, axis, thickness, curve) = match draft.kind {
215        MemberDraftKind::Curve {
216            predefined_type,
217            axis,
218            varying,
219        } => (
220            if varying {
221                "IfcStructuralCurveMemberVarying"
222            } else {
223                "IfcStructuralCurveMember"
224            },
225            predefined_type,
226            axis,
227            None,
228            true,
229        ),
230        MemberDraftKind::Surface {
231            predefined_type,
232            thickness,
233            varying,
234        } => (
235            if varying {
236                "IfcStructuralSurfaceMemberVarying"
237            } else {
238                "IfcStructuralSurfaceMember"
239            },
240            predefined_type,
241            None,
242            thickness,
243            false,
244        ),
245    };
246    let token = predefined_type.token();
247    let valid = if curve {
248        matches!(
249            predefined_type,
250            MemberPredefinedType::RigidJoinedMember
251                | MemberPredefinedType::PinJoinedMember
252                | MemberPredefinedType::Cable
253                | MemberPredefinedType::TensionMember
254                | MemberPredefinedType::CompressionMember
255                | MemberPredefinedType::UserDefined
256                | MemberPredefinedType::NotDefined
257        )
258    } else {
259        matches!(
260            predefined_type,
261            MemberPredefinedType::BendingElement
262                | MemberPredefinedType::MembraneElement
263                | MemberPredefinedType::Shell
264                | MemberPredefinedType::UserDefined
265                | MemberPredefinedType::NotDefined
266        )
267    };
268    if !valid {
269        return Err(StructuralError::InvalidDraftValue {
270            entity_type,
271            attribute: "PredefinedType",
272            expected: "member-kind enum value",
273        });
274    }
275    if predefined_type == MemberPredefinedType::UserDefined
276        && draft.root.object_type.as_deref().is_none_or(str::is_empty)
277    {
278        return Err(StructuralError::SemanticViolation {
279            entity: None,
280            rule: "USERDEFINED structural member requires ObjectType",
281        });
282    }
283    let has_axis = schema
284        .attributes(entity_type)
285        .iter()
286        .any(|attribute| attribute.name.eq_ignore_ascii_case("Axis"));
287    let validated_axis = if has_axis {
288        let target = axis.ok_or(StructuralError::MissingRequired {
289            entity_type: entity_type.into(),
290            attribute: "Axis".into(),
291        })?;
292        validate_ref(tx, model, schema, target, "IfcDirection")?;
293        Some(target)
294    } else if axis.is_some() {
295        return Err(StructuralError::UnsupportedAttribute {
296            entity_type: entity_type.into(),
297            attribute: "Axis".into(),
298        });
299    } else {
300        None
301    };
302    if thickness.is_some_and(|value| !value.is_finite() || value <= 0.0) {
303        return Err(StructuralError::InvalidDraftValue {
304            entity_type,
305            attribute: "Thickness",
306            expected: "positive finite thickness or null",
307        });
308    }
309    if predefined_type == MemberPredefinedType::Shell && thickness.is_none() {
310        return Err(StructuralError::InvalidDraftValue {
311            entity_type,
312            attribute: "Thickness",
313            expected: "SHELL requires positive finite Thickness",
314        });
315    }
316    let mut fields = root_fields(draft.root);
317    fields.push(("PredefinedType", Value::Enum(token.into())));
318    if let Some(axis) = validated_axis {
319        fields.push(("Axis", Value::Ref(axis)));
320    }
321    if schema
322        .attributes(entity_type)
323        .iter()
324        .any(|a| a.name.eq_ignore_ascii_case("Thickness"))
325    {
326        fields.push(("Thickness", thickness.map_or(Value::Null, Value::Real)));
327    }
328    Ok(tx.create(build_named(schema, entity_type, fields)?))
329}
330
331/// Stage an `IfcStructuralConnection` create edit on `tx`.
332///
333/// Fails with [`StructuralError::MissingRequired`] if `Axis` is required by
334/// the schema but unset, and with [`StructuralError::UnsupportedAttribute`]
335/// if `axis` is set but the target schema declares no `Axis` attribute for
336/// this type. Returns the id staged for the new entity.
337pub fn stage_connection(
338    tx: &mut Transaction,
339    model: &Model,
340    schema: &Schema,
341    draft: ConnectionDraft,
342) -> StructuralResult<EntityId> {
343    validate_root(tx, model, schema, &draft.root)?;
344    let (entity_type, applied_condition, axis, coordinate_system) = match draft.kind {
345        ConnectionDraftKind::Point {
346            applied_condition,
347            condition_coordinate_system,
348        } => (
349            "IfcStructuralPointConnection",
350            applied_condition,
351            None,
352            condition_coordinate_system,
353        ),
354        ConnectionDraftKind::Curve {
355            applied_condition,
356            axis,
357        } => (
358            "IfcStructuralCurveConnection",
359            applied_condition,
360            axis,
361            None,
362        ),
363        ConnectionDraftKind::Surface { applied_condition } => (
364            "IfcStructuralSurfaceConnection",
365            applied_condition,
366            None,
367            None,
368        ),
369    };
370    validate_optional_ref(tx, model, schema, applied_condition, "IfcBoundaryCondition")?;
371    validate_optional_ref(tx, model, schema, coordinate_system, "IfcAxis2Placement3D")?;
372    // The two curve forms name this slot differently: a member carries
373    // `Axis`, a connection `AxisDirection`. Hardcoding either name makes
374    // the other unauthorable -- an absent axis fails the required-attribute
375    // check, and a supplied one fails as unsupported.
376    let axis_attribute = ["Axis", "AxisDirection"].into_iter().find(|candidate| {
377        schema
378            .attributes(entity_type)
379            .iter()
380            .any(|attribute| attribute.name.eq_ignore_ascii_case(candidate))
381    });
382    let validated_axis = if let Some(axis_attribute) = axis_attribute {
383        let target = axis.ok_or_else(|| StructuralError::MissingRequired {
384            entity_type: entity_type.into(),
385            attribute: axis_attribute.into(),
386        })?;
387        validate_ref(tx, model, schema, target, "IfcDirection")?;
388        Some((axis_attribute, target))
389    } else if axis.is_some() {
390        return Err(StructuralError::UnsupportedAttribute {
391            entity_type: entity_type.into(),
392            attribute: "Axis".into(),
393        });
394    } else {
395        None
396    };
397    let mut fields = root_fields(draft.root);
398    fields.push(("AppliedCondition", optional_ref(applied_condition)));
399    if let Some((axis_attribute, axis)) = validated_axis {
400        fields.push((axis_attribute, Value::Ref(axis)));
401    }
402    if schema
403        .attributes(entity_type)
404        .iter()
405        .any(|a| a.name.eq_ignore_ascii_case("ConditionCoordinateSystem"))
406    {
407        fields.push(("ConditionCoordinateSystem", optional_ref(coordinate_system)));
408    }
409    Ok(tx.create(build_named(schema, entity_type, fields)?))
410}