Skip to main content

ifc_structural/
member.rs

1//! Idealized curve and surface structural members.
2
3use ifc_model::EntityId;
4use ifc_schema::SchemaVersion;
5
6use crate::error::{StructuralError, StructuralResult};
7use crate::view::Record;
8
9mod curve;
10mod surface;
11mod varying;
12
13/// Which `IfcStructuralMember` subtype (and mutable-cross-section variant) a member carries.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum MemberKind {
16    /// `IfcStructuralCurveMember` with constant axis properties.
17    Curve,
18    /// `IfcStructuralCurveMemberVarying`: axis properties vary along the curve.
19    CurveVarying,
20    /// `IfcStructuralSurfaceMember` with constant thickness.
21    Surface,
22    /// `IfcStructuralSurfaceMemberVarying`: thickness varies over the surface.
23    SurfaceVarying,
24}
25
26/// Borrowed projection of an `IfcStructuralMember` (curve or surface, constant or varying).
27#[derive(Debug, Clone, Copy)]
28pub struct Member<'m, 's> {
29    record: Record<'m, 's>,
30    kind: MemberKind,
31}
32
33impl<'m, 's> Member<'m, 's> {
34    /// Classify `record`'s [`MemberKind`] from its declared IFC type.
35    ///
36    /// Fails with [`StructuralError::WrongType`] if the type is not a subtype
37    /// of `IfcStructuralCurveMember` or `IfcStructuralSurfaceMember`.
38    pub(crate) fn from_record(record: Record<'m, 's>) -> StructuralResult<Self> {
39        let kind = if record
40            .schema
41            .is_a(&record.entity.type_name, "IfcStructuralCurveMemberVarying")
42        {
43            MemberKind::CurveVarying
44        } else if record.schema.is_a(
45            &record.entity.type_name,
46            "IfcStructuralSurfaceMemberVarying",
47        ) {
48            MemberKind::SurfaceVarying
49        } else if record
50            .schema
51            .is_a(&record.entity.type_name, "IfcStructuralCurveMember")
52        {
53            MemberKind::Curve
54        } else if record
55            .schema
56            .is_a(&record.entity.type_name, "IfcStructuralSurfaceMember")
57        {
58            MemberKind::Surface
59        } else {
60            return Err(StructuralError::WrongType {
61                id: record.id,
62                expected: "IfcStructuralCurveMember or IfcStructuralSurfaceMember",
63                actual: record.entity.type_name.to_string(),
64            });
65        };
66        Ok(Self { record, kind })
67    }
68
69    #[must_use]
70    /// The `IfcStructuralMember` entity id.
71    pub fn id(&self) -> EntityId {
72        self.record.id
73    }
74
75    #[must_use]
76    /// Which curve/surface, constant/varying subtype this member carries.
77    pub fn kind(&self) -> MemberKind {
78        self.kind
79    }
80
81    /// `Name`, inherited from `IfcRoot`. Legally absent.
82    pub fn name(&self) -> StructuralResult<Option<&'m str>> {
83        self.validate_semantics()?;
84        self.record.optional_text("Name")
85    }
86
87    /// `PredefinedType`, always mandatory on `IfcStructuralMember`.
88    ///
89    /// Fails with [`StructuralError::SemanticViolation`] if the value is
90    /// `USERDEFINED` but `ObjectType` is unset (IFC4/IFC4X3 only; IFC2X3
91    /// declares no `PredefinedType` semantic constraint here).
92    pub fn predefined_type(&self) -> StructuralResult<&'m str> {
93        self.validate_semantics()?;
94        self.record.required_enum("PredefinedType")
95    }
96
97    /// `Axis`, the local member axis direction. Only meaningful for curve
98    /// members; returns `None` for surface members or when the attribute is unset.
99    pub fn axis(&self) -> StructuralResult<Option<EntityId>> {
100        if !matches!(self.kind, MemberKind::Curve | MemberKind::CurveVarying)
101            || !self.record.has_attribute("Axis")
102        {
103            return Ok(None);
104        }
105        self.record.required_ref("Axis", "IfcDirection").map(Some)
106    }
107
108    /// `Thickness`, the constant surface thickness. Only meaningful for
109    /// surface members; returns `None` for curve members or when the
110    /// attribute is unset. Fails with [`StructuralError::SemanticViolation`]
111    /// if present but not a positive finite value.
112    pub fn thickness(&self) -> StructuralResult<Option<f64>> {
113        if !matches!(self.kind, MemberKind::Surface | MemberKind::SurfaceVarying)
114            || !self.record.has_attribute("Thickness")
115        {
116            return Ok(None);
117        }
118        let thickness = self.record.optional_number("Thickness")?;
119        if thickness.is_some_and(|value| !value.is_finite() || value <= 0.0) {
120            return Err(StructuralError::SemanticViolation {
121                entity: Some(self.record.id),
122                rule: "structural surface member Thickness must be positive and finite",
123            });
124        }
125        Ok(thickness)
126    }
127
128    fn validate_semantics(&self) -> StructuralResult<()> {
129        if self.record.schema.version() == Some(SchemaVersion::Ifc2x3) {
130            return Ok(());
131        }
132        let user_defined = self
133            .record
134            .required_enum("PredefinedType")?
135            .eq_ignore_ascii_case("USERDEFINED");
136        self.record.require_object_type_if(
137            user_defined,
138            "USERDEFINED structural member requires ObjectType",
139        )
140    }
141}