Skip to main content

ifc_geometry/authoring/std_profile/
flanged_section.rs

1//! The flanged catalogue sections: I, L, T, U, Z, C and the asymmetric I.
2//!
3//! Split from the closed sections purely for file size; the shared
4//! helpers and the WHERE-rule discipline are identical.
5
6use ifc_model::{Entity, EntityId, Transaction, Value};
7
8use crate::error::GeometryError;
9use crate::slots::section_slot as slot;
10
11use super::super::require_finite;
12use super::{
13    non_negative, parameterized, positive, put_opt, rule, AsymmetricIDims, CShapeDims, FlangedDims,
14    IShapeDims, ProfileHeader,
15};
16
17/// The optional radii and slope an I-section may carry.
18///
19/// Grouped rather than passed positionally: five trailing `Option<f64>`
20/// arguments at a call site are indistinguishable from one another, and
21/// transposing two of them writes a valid record describing the wrong
22/// beam.
23#[derive(Debug, Clone, Copy, Default)]
24pub struct IShapeExtras {
25    /// `FilletRadius`: the web-to-flange transition.
26    pub fillet_radius: Option<f64>,
27    /// `FlangeEdgeRadius`: the rounding at the flange tip.
28    pub flange_edge_radius: Option<f64>,
29    /// `FlangeSlope`, in radians.
30    pub flange_slope: Option<f64>,
31}
32
33/// Stage an `IfcIShapeProfileDef`.
34///
35/// # Errors
36///
37/// Refuses a non-positive dimension, a negative radius, and each of the
38/// entity's three WHERE rules: `ValidFlangeThickness`
39/// (`2 * FlangeThickness < OverallDepth`), `ValidWebThickness`
40/// (`WebThickness < OverallWidth`), and `ValidFilletRadius`, which bounds
41/// the fillet by both the web clearance and the flange clearance.
42pub fn i_shape(
43    tx: &mut Transaction,
44    header: ProfileHeader<'_>,
45    dims: IShapeDims,
46    extras: IShapeExtras,
47) -> Result<EntityId, GeometryError> {
48    const T: &str = "IFCISHAPEPROFILEDEF";
49    let IShapeDims {
50        width,
51        depth,
52        web_thickness,
53        flange_thickness,
54    } = dims;
55    positive(T, "OverallWidth", width)?;
56    positive(T, "OverallDepth", depth)?;
57    positive(T, "WebThickness", web_thickness)?;
58    positive(T, "FlangeThickness", flange_thickness)?;
59    non_negative(T, "FilletRadius", extras.fillet_radius)?;
60    non_negative(T, "FlangeEdgeRadius", extras.flange_edge_radius)?;
61    if let Some(slope) = extras.flange_slope {
62        require_finite(T, "FlangeSlope", &[slope])?;
63    }
64    if 2.0 * flange_thickness >= depth {
65        return Err(rule(
66            T,
67            "FlangeThickness",
68            format!("2 * {flange_thickness} is not below OverallDepth {depth}"),
69        ));
70    }
71    if web_thickness >= width {
72        return Err(rule(
73            T,
74            "WebThickness",
75            format!("{web_thickness} is not below OverallWidth {width}"),
76        ));
77    }
78    if let Some(fillet) = extras.fillet_radius {
79        let web_room = (width - web_thickness) / 2.0;
80        let flange_room = (depth - 2.0 * flange_thickness) / 2.0;
81        if fillet > web_room || fillet > flange_room {
82            return Err(rule(
83                T,
84                "FilletRadius",
85                format!("{fillet} exceeds the room left by the web ({web_room}) or flanges ({flange_room})"),
86            ));
87        }
88    }
89    let mut attrs = parameterized(T, 10, header);
90    attrs[slot::I_WIDTH] = Value::Real(width);
91    attrs[slot::I_DEPTH] = Value::Real(depth);
92    attrs[slot::I_WEB] = Value::Real(web_thickness);
93    attrs[slot::I_FLANGE] = Value::Real(flange_thickness);
94    put_opt(&mut attrs, slot::I_FILLET, extras.fillet_radius);
95    put_opt(&mut attrs, slot::I_EDGE, extras.flange_edge_radius);
96    put_opt(&mut attrs, slot::I_SLOPE, extras.flange_slope);
97    Ok(tx.create(Entity::new(T, attrs)))
98}
99/// The optional radii and slope an L-section may carry.
100#[derive(Debug, Clone, Copy, Default)]
101pub struct LShapeExtras {
102    /// `Width`: absent means a equal-legged angle of side `Depth`.
103    pub width: Option<f64>,
104    /// `FilletRadius`: the inner corner.
105    pub fillet_radius: Option<f64>,
106    /// `EdgeRadius`: the leg tips.
107    pub edge_radius: Option<f64>,
108    /// `LegSlope`, in radians.
109    pub leg_slope: Option<f64>,
110}
111
112/// Stage an `IfcLShapeProfileDef`.
113///
114/// # Errors
115///
116/// Refuses a non-positive dimension, a negative radius, and the
117/// `ValidThickness` rule: the thickness must be below the depth, and
118/// below the width when one is stated. An equal-legged angle omits
119/// `Width`, so the width half of the rule only applies when present.
120pub fn l_shape(
121    tx: &mut Transaction,
122    header: ProfileHeader<'_>,
123    depth: f64,
124    thickness: f64,
125    extras: LShapeExtras,
126) -> Result<EntityId, GeometryError> {
127    const T: &str = "IFCLSHAPEPROFILEDEF";
128    positive(T, "Depth", depth)?;
129    positive(T, "Thickness", thickness)?;
130    if let Some(width) = extras.width {
131        positive(T, "Width", width)?;
132    }
133    non_negative(T, "FilletRadius", extras.fillet_radius)?;
134    non_negative(T, "EdgeRadius", extras.edge_radius)?;
135    if let Some(slope) = extras.leg_slope {
136        require_finite(T, "LegSlope", &[slope])?;
137    }
138    if thickness >= depth {
139        return Err(rule(
140            T,
141            "Thickness",
142            format!("{thickness} is not below Depth {depth}"),
143        ));
144    }
145    if let Some(width) = extras.width {
146        if thickness >= width {
147            return Err(rule(
148                T,
149                "Thickness",
150                format!("{thickness} is not below Width {width}"),
151            ));
152        }
153    }
154    let mut attrs = parameterized(T, 9, header);
155    attrs[slot::L_DEPTH] = Value::Real(depth);
156    attrs[slot::L_THICKNESS] = Value::Real(thickness);
157    put_opt(&mut attrs, slot::L_WIDTH, extras.width);
158    put_opt(&mut attrs, slot::L_FILLET, extras.fillet_radius);
159    put_opt(&mut attrs, slot::L_EDGE, extras.edge_radius);
160    put_opt(&mut attrs, slot::L_SLOPE, extras.leg_slope);
161    Ok(tx.create(Entity::new(T, attrs)))
162}
163/// The optional radii and slopes a T-section may carry.
164#[derive(Debug, Clone, Copy, Default)]
165pub struct TShapeExtras {
166    /// `FilletRadius`: the web-to-flange transition.
167    pub fillet_radius: Option<f64>,
168    /// `FlangeEdgeRadius`: the flange tips.
169    pub flange_edge_radius: Option<f64>,
170    /// `WebEdgeRadius`: the free end of the web.
171    pub web_edge_radius: Option<f64>,
172    /// `WebSlope`, in radians.
173    pub web_slope: Option<f64>,
174    /// `FlangeSlope`, in radians.
175    pub flange_slope: Option<f64>,
176}
177
178/// Stage an `IfcTShapeProfileDef`.
179///
180/// # Errors
181///
182/// Refuses a non-positive dimension, a negative radius, and the two
183/// WHERE rules: `FlangeThickness < Depth` and
184/// `WebThickness < FlangeWidth`.
185pub fn t_shape(
186    tx: &mut Transaction,
187    header: ProfileHeader<'_>,
188    dims: FlangedDims,
189    extras: TShapeExtras,
190) -> Result<EntityId, GeometryError> {
191    const T: &str = "IFCTSHAPEPROFILEDEF";
192    let FlangedDims {
193        depth,
194        flange_width,
195        web_thickness,
196        flange_thickness,
197    } = dims;
198    positive(T, "Depth", depth)?;
199    positive(T, "FlangeWidth", flange_width)?;
200    positive(T, "WebThickness", web_thickness)?;
201    positive(T, "FlangeThickness", flange_thickness)?;
202    non_negative(T, "FilletRadius", extras.fillet_radius)?;
203    non_negative(T, "FlangeEdgeRadius", extras.flange_edge_radius)?;
204    non_negative(T, "WebEdgeRadius", extras.web_edge_radius)?;
205    for (attribute, slope) in [
206        ("WebSlope", extras.web_slope),
207        ("FlangeSlope", extras.flange_slope),
208    ] {
209        if let Some(slope) = slope {
210            require_finite(T, attribute, &[slope])?;
211        }
212    }
213    if flange_thickness >= depth {
214        return Err(rule(
215            T,
216            "FlangeThickness",
217            format!("{flange_thickness} is not below Depth {depth}"),
218        ));
219    }
220    if web_thickness >= flange_width {
221        return Err(rule(
222            T,
223            "WebThickness",
224            format!("{web_thickness} is not below FlangeWidth {flange_width}"),
225        ));
226    }
227    let mut attrs = parameterized(T, 12, header);
228    attrs[slot::T_DEPTH] = Value::Real(depth);
229    attrs[slot::T_WIDTH] = Value::Real(flange_width);
230    attrs[slot::T_WEB] = Value::Real(web_thickness);
231    attrs[slot::T_FLANGE] = Value::Real(flange_thickness);
232    put_opt(&mut attrs, slot::T_FILLET, extras.fillet_radius);
233    put_opt(&mut attrs, slot::T_FLANGE_EDGE, extras.flange_edge_radius);
234    put_opt(&mut attrs, slot::T_WEB_EDGE, extras.web_edge_radius);
235    put_opt(&mut attrs, slot::T_WEB_SLOPE, extras.web_slope);
236    put_opt(&mut attrs, slot::T_FLANGE_SLOPE, extras.flange_slope);
237    Ok(tx.create(Entity::new(T, attrs)))
238}
239/// The optional radii and slope a U-section (channel) may carry.
240#[derive(Debug, Clone, Copy, Default)]
241pub struct UShapeExtras {
242    /// `FilletRadius`: the web-to-flange transition.
243    pub fillet_radius: Option<f64>,
244    /// `EdgeRadius`: the flange tips.
245    pub edge_radius: Option<f64>,
246    /// `FlangeSlope`, in radians.
247    pub flange_slope: Option<f64>,
248}
249
250/// Stage an `IfcUShapeProfileDef`.
251///
252/// # Errors
253///
254/// Refuses a non-positive dimension, a negative radius, and the two
255/// WHERE rules. Note the flange bound is `Depth / 2`, not `Depth`: a
256/// channel has two flanges, so a thickness at half the depth already
257/// closes the section.
258pub fn u_shape(
259    tx: &mut Transaction,
260    header: ProfileHeader<'_>,
261    dims: FlangedDims,
262    extras: UShapeExtras,
263) -> Result<EntityId, GeometryError> {
264    const T: &str = "IFCUSHAPEPROFILEDEF";
265    let FlangedDims {
266        depth,
267        flange_width,
268        web_thickness,
269        flange_thickness,
270    } = dims;
271    positive(T, "Depth", depth)?;
272    positive(T, "FlangeWidth", flange_width)?;
273    positive(T, "WebThickness", web_thickness)?;
274    positive(T, "FlangeThickness", flange_thickness)?;
275    non_negative(T, "FilletRadius", extras.fillet_radius)?;
276    non_negative(T, "EdgeRadius", extras.edge_radius)?;
277    if let Some(slope) = extras.flange_slope {
278        require_finite(T, "FlangeSlope", &[slope])?;
279    }
280    if flange_thickness >= depth / 2.0 {
281        return Err(rule(
282            T,
283            "FlangeThickness",
284            format!(
285                "{flange_thickness} is not below half the Depth ({})",
286                depth / 2.0
287            ),
288        ));
289    }
290    if web_thickness >= flange_width {
291        return Err(rule(
292            T,
293            "WebThickness",
294            format!("{web_thickness} is not below FlangeWidth {flange_width}"),
295        ));
296    }
297    let mut attrs = parameterized(T, 10, header);
298    attrs[slot::U_DEPTH] = Value::Real(depth);
299    attrs[slot::U_WIDTH] = Value::Real(flange_width);
300    attrs[slot::U_WEB] = Value::Real(web_thickness);
301    attrs[slot::U_FLANGE] = Value::Real(flange_thickness);
302    put_opt(&mut attrs, slot::U_FILLET, extras.fillet_radius);
303    put_opt(&mut attrs, slot::U_EDGE, extras.edge_radius);
304    put_opt(&mut attrs, slot::U_SLOPE, extras.flange_slope);
305    Ok(tx.create(Entity::new(T, attrs)))
306}
307
308/// The optional radii a Z-section may carry.
309#[derive(Debug, Clone, Copy, Default)]
310pub struct ZShapeExtras {
311    /// `FilletRadius`: the web-to-flange transitions.
312    pub fillet_radius: Option<f64>,
313    /// `EdgeRadius`: the flange tips.
314    pub edge_radius: Option<f64>,
315}
316
317/// Stage an `IfcZShapeProfileDef`.
318///
319/// # Errors
320///
321/// Refuses a non-positive dimension, a negative radius, and the
322/// `ValidFlangeThickness` rule (`FlangeThickness < Depth / 2`). A Z has
323/// two opposed flanges, so the bound is half the depth.
324pub fn z_shape(
325    tx: &mut Transaction,
326    header: ProfileHeader<'_>,
327    dims: FlangedDims,
328    extras: ZShapeExtras,
329) -> Result<EntityId, GeometryError> {
330    const T: &str = "IFCZSHAPEPROFILEDEF";
331    let FlangedDims {
332        depth,
333        flange_width,
334        web_thickness,
335        flange_thickness,
336    } = dims;
337    positive(T, "Depth", depth)?;
338    positive(T, "FlangeWidth", flange_width)?;
339    positive(T, "WebThickness", web_thickness)?;
340    positive(T, "FlangeThickness", flange_thickness)?;
341    non_negative(T, "FilletRadius", extras.fillet_radius)?;
342    non_negative(T, "EdgeRadius", extras.edge_radius)?;
343    if flange_thickness >= depth / 2.0 {
344        return Err(rule(
345            T,
346            "FlangeThickness",
347            format!(
348                "{flange_thickness} is not below half the Depth ({})",
349                depth / 2.0
350            ),
351        ));
352    }
353    let mut attrs = parameterized(T, 9, header);
354    attrs[slot::Z_DEPTH] = Value::Real(depth);
355    attrs[slot::Z_FLANGE_WIDTH] = Value::Real(flange_width);
356    attrs[slot::Z_WEB] = Value::Real(web_thickness);
357    attrs[slot::Z_FLANGE] = Value::Real(flange_thickness);
358    put_opt(&mut attrs, slot::Z_FILLET, extras.fillet_radius);
359    put_opt(&mut attrs, slot::Z_EDGE, extras.edge_radius);
360    Ok(tx.create(Entity::new(T, attrs)))
361}
362/// Stage an `IfcCShapeProfileDef`.
363///
364/// # Errors
365///
366/// Refuses a non-positive dimension, a negative radius, and the three
367/// WHERE rules: `Girth < Depth / 2`, `WallThickness` below half of both
368/// `Width` and `Depth`, and `InternalFilletRadius` within the room the
369/// wall leaves on both axes.
370pub fn c_shape(
371    tx: &mut Transaction,
372    header: ProfileHeader<'_>,
373    dims: CShapeDims,
374    internal_fillet_radius: Option<f64>,
375) -> Result<EntityId, GeometryError> {
376    const T: &str = "IFCCSHAPEPROFILEDEF";
377    let CShapeDims {
378        depth,
379        width,
380        wall_thickness,
381        girth,
382    } = dims;
383    positive(T, "Depth", depth)?;
384    positive(T, "Width", width)?;
385    positive(T, "WallThickness", wall_thickness)?;
386    positive(T, "Girth", girth)?;
387    non_negative(T, "InternalFilletRadius", internal_fillet_radius)?;
388    if girth >= depth / 2.0 {
389        return Err(rule(
390            T,
391            "Girth",
392            format!("{girth} is not below half the Depth ({})", depth / 2.0),
393        ));
394    }
395    if wall_thickness >= width / 2.0 || wall_thickness >= depth / 2.0 {
396        return Err(rule(
397            T,
398            "WallThickness",
399            format!(
400                "{wall_thickness} is not below half of both Width ({}) and Depth ({})",
401                width / 2.0,
402                depth / 2.0
403            ),
404        ));
405    }
406    if let Some(fillet) = internal_fillet_radius {
407        let across_width = width / 2.0 - wall_thickness;
408        let across_depth = depth / 2.0 - wall_thickness;
409        if fillet > across_width || fillet > across_depth {
410            return Err(rule(
411                T,
412                "InternalFilletRadius",
413                format!(
414                    "{fillet} exceeds the room the wall leaves ({across_width} by {across_depth})"
415                ),
416            ));
417        }
418    }
419    let mut attrs = parameterized(T, 8, header);
420    attrs[slot::C_DEPTH] = Value::Real(depth);
421    attrs[slot::C_WIDTH] = Value::Real(width);
422    attrs[slot::C_WALL] = Value::Real(wall_thickness);
423    attrs[slot::C_GIRTH] = Value::Real(girth);
424    put_opt(&mut attrs, slot::C_FILLET, internal_fillet_radius);
425    Ok(tx.create(Entity::new(T, attrs)))
426}
427/// The optional attributes an asymmetric I-section may carry.
428///
429/// `TopFlangeThickness` is optional in the schema; when absent the top
430/// flange is taken to match the bottom, and `ValidFlangeThickness` does
431/// not apply.
432#[derive(Debug, Clone, Copy, Default)]
433pub struct AsymmetricIExtras {
434    /// `TopFlangeThickness`.
435    pub top_flange_thickness: Option<f64>,
436    /// `BottomFlangeFilletRadius`.
437    pub bottom_flange_fillet_radius: Option<f64>,
438    /// `TopFlangeFilletRadius`.
439    pub top_flange_fillet_radius: Option<f64>,
440    /// `BottomFlangeEdgeRadius`.
441    pub bottom_flange_edge_radius: Option<f64>,
442    /// `TopFlangeEdgeRadius`.
443    pub top_flange_edge_radius: Option<f64>,
444    /// `BottomFlangeSlope`, in radians.
445    pub bottom_flange_slope: Option<f64>,
446    /// `TopFlangeSlope`, in radians.
447    pub top_flange_slope: Option<f64>,
448}
449
450/// Stage an `IfcAsymmetricIShapeProfileDef`.
451///
452/// # Errors
453///
454/// Refuses a non-positive dimension, a negative radius, and the four
455/// WHERE rules: the web must be narrower than both flanges, each fillet
456/// is bounded by its own flange's clearance, and the two flange
457/// thicknesses together must stay below the overall depth when a top
458/// thickness is stated.
459pub fn asymmetric_i_shape(
460    tx: &mut Transaction,
461    header: ProfileHeader<'_>,
462    dims: AsymmetricIDims,
463    extras: AsymmetricIExtras,
464) -> Result<EntityId, GeometryError> {
465    const T: &str = "IFCASYMMETRICISHAPEPROFILEDEF";
466    let AsymmetricIDims {
467        bottom_flange_width,
468        overall_depth,
469        web_thickness,
470        bottom_flange_thickness,
471        top_flange_width,
472    } = dims;
473    positive(T, "BottomFlangeWidth", bottom_flange_width)?;
474    positive(T, "OverallDepth", overall_depth)?;
475    positive(T, "WebThickness", web_thickness)?;
476    positive(T, "BottomFlangeThickness", bottom_flange_thickness)?;
477    positive(T, "TopFlangeWidth", top_flange_width)?;
478    if let Some(top) = extras.top_flange_thickness {
479        positive(T, "TopFlangeThickness", top)?;
480    }
481    non_negative(
482        T,
483        "BottomFlangeFilletRadius",
484        extras.bottom_flange_fillet_radius,
485    )?;
486    non_negative(T, "TopFlangeFilletRadius", extras.top_flange_fillet_radius)?;
487    non_negative(
488        T,
489        "BottomFlangeEdgeRadius",
490        extras.bottom_flange_edge_radius,
491    )?;
492    non_negative(T, "TopFlangeEdgeRadius", extras.top_flange_edge_radius)?;
493    for (attribute, slope) in [
494        ("BottomFlangeSlope", extras.bottom_flange_slope),
495        ("TopFlangeSlope", extras.top_flange_slope),
496    ] {
497        if let Some(slope) = slope {
498            require_finite(T, attribute, &[slope])?;
499        }
500    }
501    if web_thickness >= bottom_flange_width || web_thickness >= top_flange_width {
502        return Err(rule(
503            T,
504            "WebThickness",
505            format!(
506                "{web_thickness} is not below both flange widths ({bottom_flange_width}, {top_flange_width})"
507            ),
508        ));
509    }
510    if let Some(top) = extras.top_flange_thickness {
511        if bottom_flange_thickness + top >= overall_depth {
512            return Err(rule(
513                T,
514                "TopFlangeThickness",
515                format!(
516                    "{bottom_flange_thickness} + {top} is not below OverallDepth {overall_depth}"
517                ),
518            ));
519        }
520    }
521    for (attribute, fillet, width) in [
522        (
523            "BottomFlangeFilletRadius",
524            extras.bottom_flange_fillet_radius,
525            bottom_flange_width,
526        ),
527        (
528            "TopFlangeFilletRadius",
529            extras.top_flange_fillet_radius,
530            top_flange_width,
531        ),
532    ] {
533        if let Some(fillet) = fillet {
534            let room = (width - web_thickness) / 2.0;
535            if fillet > room {
536                return Err(rule(
537                    T,
538                    attribute,
539                    format!("{fillet} exceeds the flange clearance ({room})"),
540                ));
541            }
542        }
543    }
544    let mut attrs = parameterized(T, 15, header);
545    attrs[slot::AI_BOTTOM_WIDTH] = Value::Real(bottom_flange_width);
546    attrs[slot::AI_DEPTH] = Value::Real(overall_depth);
547    attrs[slot::AI_WEB] = Value::Real(web_thickness);
548    attrs[slot::AI_BOTTOM_FLANGE] = Value::Real(bottom_flange_thickness);
549    attrs[slot::AI_TOP_WIDTH] = Value::Real(top_flange_width);
550    put_opt(&mut attrs, slot::AI_TOP_FLANGE, extras.top_flange_thickness);
551    put_opt(
552        &mut attrs,
553        slot::AI_BOTTOM_FILLET,
554        extras.bottom_flange_fillet_radius,
555    );
556    put_opt(
557        &mut attrs,
558        slot::AI_TOP_FILLET,
559        extras.top_flange_fillet_radius,
560    );
561    put_opt(
562        &mut attrs,
563        slot::AI_BOTTOM_EDGE,
564        extras.bottom_flange_edge_radius,
565    );
566    put_opt(&mut attrs, slot::AI_TOP_EDGE, extras.top_flange_edge_radius);
567    put_opt(
568        &mut attrs,
569        slot::AI_BOTTOM_SLOPE,
570        extras.bottom_flange_slope,
571    );
572    put_opt(&mut attrs, slot::AI_TOP_SLOPE, extras.top_flange_slope);
573    Ok(tx.create(Entity::new(T, attrs)))
574}