ifc_geometry/authoring/std_profile.rs
1//! Standard parameterized sections: the catalogue profiles.
2//!
3//! These are the sections a structural catalogue names -- an IPE beam,
4//! an angle, a channel, a square hollow tube. Each is a fixed list of
5//! dimensions, so authoring one is writing numbers into slots; no
6//! curve construction and no evaluator is involved.
7//!
8//! Slots come from [`crate::slots::section_slot`], the same
9//! constants the profile reader uses. A layout correction lands once and
10//! serves both directions.
11//!
12//! # Refusals are schema rules, not taste
13//!
14//! IFC declares the load-bearing dimensions as
15//! `IfcPositiveLengthMeasure`, so zero is invalid rather than merely
16//! degenerate, and each entity carries WHERE rules relating those
17//! dimensions -- a web thinner than the flange it joins, a girth under
18//! half the depth. This module enforces them at authoring time because
19//! `ifc-validate` does not implement profile WHERE rules: a section
20//! violating them is a record that parses and describes an impossible
21//! shape.
22//!
23//! Fillet and edge radii are `IfcNonNegativeLengthMeasure`, where zero
24//! is legal and means a sharp corner. They are checked for
25//! non-negativity, never for positivity.
26
27use ifc_model::{EntityId, Value};
28
29use crate::error::GeometryError;
30use crate::slots::profile_slot as base;
31
32use super::profile::ProfileType;
33use super::{invalid, require_finite};
34/// The two attributes every parameterized profile carries.
35///
36/// Bundled so a section's own dimensions are not separated from each
37/// other by two unrelated arguments at the call site.
38#[derive(Debug, Clone, Copy, Default)]
39pub struct ProfileHeader<'a> {
40 /// `ProfileName`.
41 pub name: Option<&'a str>,
42 /// `Position : IfcAxis2Placement2D`.
43 pub position: Option<EntityId>,
44}
45
46/// The four required dimensions of an I-section.
47#[derive(Debug, Clone, Copy)]
48pub struct IShapeDims {
49 /// `OverallWidth`.
50 pub width: f64,
51 /// `OverallDepth`.
52 pub depth: f64,
53 /// `WebThickness`.
54 pub web_thickness: f64,
55 /// `FlangeThickness`.
56 pub flange_thickness: f64,
57}
58
59/// The required dimensions of a T, U or Z section.
60///
61/// The three share a shape: a depth, a flange width, and the two
62/// thicknesses. They differ only in which WHERE rules bound them.
63#[derive(Debug, Clone, Copy)]
64pub struct FlangedDims {
65 /// `Depth`.
66 pub depth: f64,
67 /// `FlangeWidth`.
68 pub flange_width: f64,
69 /// `WebThickness`.
70 pub web_thickness: f64,
71 /// `FlangeThickness`.
72 pub flange_thickness: f64,
73}
74
75/// The required dimensions of a C-section.
76#[derive(Debug, Clone, Copy)]
77pub struct CShapeDims {
78 /// `Depth`.
79 pub depth: f64,
80 /// `Width`.
81 pub width: f64,
82 /// `WallThickness`.
83 pub wall_thickness: f64,
84 /// `Girth`.
85 pub girth: f64,
86}
87
88/// The required dimensions of an asymmetric I-section.
89#[derive(Debug, Clone, Copy)]
90pub struct AsymmetricIDims {
91 /// `BottomFlangeWidth`.
92 pub bottom_flange_width: f64,
93 /// `OverallDepth`.
94 pub overall_depth: f64,
95 /// `WebThickness`.
96 pub web_thickness: f64,
97 /// `BottomFlangeThickness`.
98 pub bottom_flange_thickness: f64,
99 /// `TopFlangeWidth`.
100 pub top_flange_width: f64,
101}
102
103/// The required dimensions of a rectangular hollow section.
104#[derive(Debug, Clone, Copy)]
105pub struct RectangleHollowDims {
106 /// `XDim`.
107 pub x_dim: f64,
108 /// `YDim`.
109 pub y_dim: f64,
110 /// `WallThickness`.
111 pub wall_thickness: f64,
112}
113
114/// The two optional fillet radii of a rectangular hollow section.
115#[derive(Debug, Clone, Copy, Default)]
116pub struct RectangleHollowFillets {
117 /// `InnerFilletRadius`.
118 pub inner: Option<f64>,
119 /// `OuterFilletRadius`.
120 pub outer: Option<f64>,
121}
122
123/// The required dimensions of a trapezium.
124#[derive(Debug, Clone, Copy)]
125pub struct TrapeziumDims {
126 /// `BottomXDim`.
127 pub bottom_x_dim: f64,
128 /// `TopXDim`.
129 pub top_x_dim: f64,
130 /// `YDim`.
131 pub y_dim: f64,
132 /// `TopXOffset`, which may be negative.
133 pub top_x_offset: f64,
134}
135
136/// Fill the three `IfcParameterizedProfileDef` slots every section shares.
137///
138/// `ProfileType` is always `.AREA.`: each entity in this module bounds a
139/// closed region, so `.CURVE.` would contradict the declaration.
140pub(super) fn parameterized(
141 type_name: &'static str,
142 arity: usize,
143 header: ProfileHeader<'_>,
144) -> Vec<Value> {
145 let mut attrs = vec![Value::Null; arity];
146 debug_assert!(!type_name.is_empty());
147 attrs[base::PROFILE_TYPE] = Value::Enum(ProfileType::Area.token().into());
148 attrs[base::PROFILE_NAME] = header.name.map_or(Value::Null, |n| Value::Text(n.into()));
149 attrs[base::POSITION] = header.position.map_or(Value::Null, Value::Ref);
150 attrs
151}
152
153/// Require a strictly positive, finite `IfcPositiveLengthMeasure`.
154pub(super) fn positive(
155 type_name: &'static str,
156 attribute: &'static str,
157 value: f64,
158) -> Result<(), GeometryError> {
159 require_finite(type_name, attribute, &[value])?;
160 if value <= 0.0 {
161 return Err(invalid(
162 type_name,
163 attribute,
164 format!("expected a positive length, got {value}"),
165 ));
166 }
167 Ok(())
168}
169
170/// Require a finite, non-negative `IfcNonNegativeLengthMeasure`.
171///
172/// Zero is accepted: a fillet radius of zero is a sharp corner, which is
173/// a real section, not a degenerate one.
174pub(super) fn non_negative(
175 type_name: &'static str,
176 attribute: &'static str,
177 value: Option<f64>,
178) -> Result<(), GeometryError> {
179 let Some(value) = value else { return Ok(()) };
180 require_finite(type_name, attribute, &[value])?;
181 if value < 0.0 {
182 return Err(invalid(
183 type_name,
184 attribute,
185 format!("expected a non-negative length, got {value}"),
186 ));
187 }
188 Ok(())
189}
190
191/// Refuse a WHERE-rule violation.
192pub(super) fn rule(
193 type_name: &'static str,
194 attribute: &'static str,
195 detail: String,
196) -> GeometryError {
197 invalid(type_name, attribute, detail)
198}
199
200/// Write an optional length into its slot when present.
201pub(super) fn put_opt(attrs: &mut [Value], index: usize, value: Option<f64>) {
202 if let Some(value) = value {
203 attrs[index] = Value::Real(value);
204 }
205}
206
207mod closed_section;
208mod flanged_section;
209
210pub use closed_section::{
211 circle_hollow_profile, ellipse_profile, rectangle_hollow_profile, trapezium_profile,
212};
213pub use flanged_section::{
214 asymmetric_i_shape, c_shape, i_shape, l_shape, t_shape, u_shape, z_shape, AsymmetricIExtras,
215 IShapeExtras, LShapeExtras, TShapeExtras, UShapeExtras, ZShapeExtras,
216};