Skip to main content

ifc_material/layer/
definition.rs

1//! Material layer definitions, including authored offsets.
2//!
3//! IFC2X3 `IfcMaterialLayer` is `(Material, LayerThickness, IsVentilated)`
4//! with a positive thickness; IFC4 and IFC4X3 append `Name`, `Description`,
5//! `Category` and `Priority` and allow a zero thickness. An accessor for an
6//! attribute the bound release lacks fails with `NotInSchema`.
7
8use ifc_model::{Entity, EntityId};
9
10use crate::release::Release;
11use crate::view::{
12    borrowed_entity, optional_integer, optional_logical, optional_ref, optional_text,
13    required_enum, required_number, required_number_array_2, MaterialView,
14};
15use crate::{LayerSetDirection, LogicalValue, MaterialError, MaterialResult};
16
17borrowed_entity!(MaterialLayer, "IFCMATERIALLAYER");
18borrowed_entity!(MaterialLayerWithOffsets, "IFCMATERIALLAYERWITHOFFSETS");
19
20/// `LayerThickness` of a layer record, checked against the bound release's
21/// declared measure: IFC2X3 `IfcPositiveLengthMeasure` (> 0), IFC4 and
22/// IFC4X3 `IfcNonNegativeLengthMeasure` (>= 0).
23pub(crate) fn layer_thickness(
24    entity_type: &'static str,
25    id: EntityId,
26    entity: &Entity,
27    release: Release<'_>,
28) -> MaterialResult<f64> {
29    let (slot, declared) = release.attribute(entity_type, id, "LayerThickness")?;
30    let value = required_number(entity_type, id, entity, slot, "LayerThickness")?;
31    let positive = declared
32        .type_name
33        .eq_ignore_ascii_case("IfcPositiveLengthMeasure");
34    if value < 0.0 || (positive && value == 0.0) {
35        return Err(MaterialError::InvalidValue {
36            entity: entity_type,
37            id,
38            attribute: "LayerThickness",
39            value: if positive {
40                "expected a positive length".to_owned()
41            } else {
42                "expected a non-negative length".to_owned()
43            },
44        });
45    }
46    Ok(value)
47}
48
49macro_rules! layer_accessors {
50    ($type:ident, $ifc_name:literal) => {
51        impl<'m> $type<'m> {
52            /// `Material`, the associated `IfcMaterial`, if given.
53            pub fn material_id(self) -> MaterialResult<Option<EntityId>> {
54                optional_ref(
55                    $ifc_name,
56                    self.id(),
57                    self.entity(),
58                    self.slot("Material")?,
59                    "Material",
60                )
61            }
62
63            /// `LayerThickness`. Required; non-negative in IFC4 and IFC4X3,
64            /// strictly positive in IFC2X3.
65            pub fn thickness(self) -> MaterialResult<f64> {
66                layer_thickness($ifc_name, self.id(), self.entity(), self.release())
67            }
68
69            /// `IsVentilated`, if given.
70            pub fn is_ventilated(self) -> MaterialResult<Option<LogicalValue>> {
71                optional_logical(
72                    $ifc_name,
73                    self.id(),
74                    self.entity(),
75                    self.slot("IsVentilated")?,
76                    "IsVentilated",
77                )
78            }
79
80            /// `Name`, if given. `NotInSchema` for IFC2X3.
81            pub fn name(self) -> MaterialResult<Option<&'m str>> {
82                optional_text(
83                    $ifc_name,
84                    self.id(),
85                    self.entity(),
86                    self.slot("Name")?,
87                    "Name",
88                )
89            }
90
91            /// `Description`, if given. `NotInSchema` for IFC2X3.
92            pub fn description(self) -> MaterialResult<Option<&'m str>> {
93                optional_text(
94                    $ifc_name,
95                    self.id(),
96                    self.entity(),
97                    self.slot("Description")?,
98                    "Description",
99                )
100            }
101
102            /// `Category`, if given. `NotInSchema` for IFC2X3.
103            pub fn category(self) -> MaterialResult<Option<&'m str>> {
104                optional_text(
105                    $ifc_name,
106                    self.id(),
107                    self.entity(),
108                    self.slot("Category")?,
109                    "Category",
110                )
111            }
112
113            /// `Priority`, if given. Must be in `0..=100`. `NotInSchema` for
114            /// IFC2X3.
115            pub fn priority(self) -> MaterialResult<Option<i64>> {
116                let value = optional_integer(
117                    $ifc_name,
118                    self.id(),
119                    self.entity(),
120                    self.slot("Priority")?,
121                    "Priority",
122                )?;
123                if value.is_some_and(|value| !(0..=100).contains(&value)) {
124                    return Err(MaterialError::InvalidValue {
125                        entity: $ifc_name,
126                        id: self.id(),
127                        attribute: "Priority",
128                        value: "expected an integer in 0..=100".to_owned(),
129                    });
130                }
131                Ok(value)
132            }
133        }
134    };
135}
136layer_accessors!(MaterialLayer, "IFCMATERIALLAYER");
137layer_accessors!(MaterialLayerWithOffsets, "IFCMATERIALLAYERWITHOFFSETS");
138
139impl MaterialLayerWithOffsets<'_> {
140    /// `IfcMaterialLayerWithOffsets.OffsetDirection`. Required.
141    pub fn offset_direction(self) -> MaterialResult<LayerSetDirection> {
142        let token = required_enum(
143            "IFCMATERIALLAYERWITHOFFSETS",
144            self.id(),
145            self.entity(),
146            self.slot("OffsetDirection")?,
147            "OffsetDirection",
148        )?;
149        LayerSetDirection::parse(token).ok_or_else(|| MaterialError::InvalidValue {
150            entity: "IFCMATERIALLAYERWITHOFFSETS",
151            id: self.id(),
152            attribute: "OffsetDirection",
153            value: token.to_owned(),
154        })
155    }
156
157    /// `IfcMaterialLayerWithOffsets.OffsetValues`. Required, a 2-element
158    /// list.
159    pub fn offset_values(self) -> MaterialResult<[f64; 2]> {
160        required_number_array_2(
161            "IFCMATERIALLAYERWITHOFFSETS",
162            self.id(),
163            self.entity(),
164            self.slot("OffsetValues")?,
165            "OffsetValues",
166        )
167    }
168}
169
170impl<'m> MaterialView<'m> {
171    /// Iterates every `IfcMaterialLayer` instance in the model.
172    pub fn layers(self) -> impl Iterator<Item = MaterialLayer<'m>> + 'm {
173        let release = self.release();
174        self.model()
175            .of_type("IFCMATERIALLAYER")
176            .map(move |(id, entity)| MaterialLayer::from_known(id, entity, release))
177    }
178
179    /// Iterates every `IfcMaterialLayerWithOffsets` instance in the model.
180    pub fn layers_with_offsets(self) -> impl Iterator<Item = MaterialLayerWithOffsets<'m>> + 'm {
181        let release = self.release();
182        self.model()
183            .of_type("IFCMATERIALLAYERWITHOFFSETS")
184            .map(move |(id, entity)| MaterialLayerWithOffsets::from_known(id, entity, release))
185    }
186}