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