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