Skip to main content

ifc_properties/pset/
reinforcement.rs

1//! Reinforcement, section, and profile property families.
2//!
3//! These five carry no WHERE rules. What constrains them is structural:
4//! required slots with no unset fallback, aggregate lower bounds of
5//! `[1:?]`, closed enumerations, and the measure type each numeric slot
6//! declares -- including `IfcCountMeasure`, which is an EXPRESS INTEGER.
7//!
8//! Only `IfcReinforcementDefinitionProperties` is an
9//! `IfcPropertySetDefinition` with the four `IfcRoot` slots. The other
10//! four are `IfcPropertyAbstraction` subtypes and start at their own
11//! first attribute.
12
13use ifc_model::{Entity, EntityId, Model, Transaction, Value};
14
15use crate::PropertyResult;
16
17use super::authoring::optional_text;
18use super::owned::{stage_ifc4, stage_owned, Rooted};
19use super::predefined::{invalid, measure, require_guid, Measure};
20const BAR_SURFACE: &[&str] = &["PLAIN", "TEXTURED"];
21
22/// Attributes of an `IfcReinforcementBarProperties`.
23///
24/// `total_cross_section_area` and `steel_grade` are required by the
25/// schema, so they are plain fields rather than options.
26#[derive(Debug, Clone, Copy)]
27pub struct ReinforcementBarDraft<'a> {
28    /// `TotalCrossSectionArea`, an area measure. Required.
29    pub total_cross_section_area: f64,
30    /// `SteelGrade`. Required.
31    pub steel_grade: &'a str,
32    /// `BarSurface`: `PLAIN` or `TEXTURED`.
33    pub bar_surface: Option<&'a str>,
34    /// `EffectiveDepth`, a length. May be negative.
35    pub effective_depth: Option<f64>,
36    /// `NominalBarDiameter`, a positive length.
37    pub nominal_bar_diameter: Option<f64>,
38    /// `BarCount`, an integer count.
39    pub bar_count: Option<i64>,
40}
41
42/// Stage an `IfcReinforcementBarProperties`.
43///
44/// # Errors
45///
46/// Refuses a blank steel grade, a non-finite or negative cross-section
47/// area, a bar surface outside the enumeration, and a measure that
48/// violates its schema measure type.
49pub fn add_reinforcement_bar_properties(
50    tx: &mut Transaction,
51    draft: ReinforcementBarDraft<'_>,
52) -> PropertyResult<EntityId> {
53    const ENTITY: &str = "IFCREINFORCEMENTBARPROPERTIES";
54
55    if draft.steel_grade.trim().is_empty() {
56        return Err(invalid(ENTITY, "SteelGrade", "blank"));
57    }
58    // An area is a magnitude: negative has no reading, and the slot is
59    // required, so there is no "unset" to fall back to.
60    if !draft.total_cross_section_area.is_finite() || draft.total_cross_section_area < 0.0 {
61        return Err(invalid(
62            ENTITY,
63            "TotalCrossSectionArea",
64            format!("{}", draft.total_cross_section_area),
65        ));
66    }
67    if let Some(surface) = draft.bar_surface {
68        if !BAR_SURFACE.contains(&surface) {
69            return Err(invalid(ENTITY, "BarSurface", surface));
70        }
71    }
72
73    let mut attributes = vec![Value::Null; 6];
74    attributes[0] = Value::Real(draft.total_cross_section_area);
75    attributes[1] = Value::Text(draft.steel_grade.into());
76    attributes[2] = draft
77        .bar_surface
78        .map_or(Value::Null, |s| Value::Enum(s.into()));
79    attributes[3] = measure(
80        ENTITY,
81        "EffectiveDepth",
82        draft.effective_depth,
83        Measure::Length,
84    )?;
85    attributes[4] = measure(
86        ENTITY,
87        "NominalBarDiameter",
88        draft.nominal_bar_diameter,
89        Measure::Positive,
90    )?;
91    // IfcCountMeasure is declared INTEGER in EXPRESS. Writing it as a
92    // real produces a record of the right arity that states the wrong
93    // type, which the structural validator does not catch.
94    attributes[5] = draft.bar_count.map_or(Value::Null, Value::Integer);
95
96    Ok(tx.create(Entity::new(ENTITY, attributes)))
97}
98
99const SECTION_TYPE: &[&str] = &["TAPERED", "UNIFORM"];
100
101/// Stage an `IfcSectionProperties`.
102///
103/// `section_type` is `TAPERED` or `UNIFORM`. A uniform section may still
104/// carry an end profile; the schema does not tie the two, and refusing
105/// the combination here would reject files it permits.
106///
107/// # Errors
108///
109/// Refuses a section type outside the enumeration.
110pub fn add_section_properties(
111    tx: &mut Transaction,
112    section_type: &str,
113    start_profile: EntityId,
114    end_profile: Option<EntityId>,
115) -> PropertyResult<EntityId> {
116    const ENTITY: &str = "IFCSECTIONPROPERTIES";
117
118    if !SECTION_TYPE.contains(&section_type) {
119        return Err(invalid(ENTITY, "SectionType", section_type));
120    }
121
122    let mut attributes = vec![Value::Null; 3];
123    attributes[0] = Value::Enum(section_type.into());
124    attributes[1] = Value::Ref(start_profile);
125    attributes[2] = end_profile.map_or(Value::Null, Value::Ref);
126
127    Ok(tx.create(Entity::new(ENTITY, attributes)))
128}
129
130const BAR_ROLE: &[&str] = &[
131    "ANCHORING",
132    "EDGE",
133    "LIGATURE",
134    "MAIN",
135    "PUNCHING",
136    "RING",
137    "SHEAR",
138    "STUD",
139    "USERDEFINED",
140    "NOTDEFINED",
141];
142
143/// Attributes of an `IfcSectionReinforcementProperties`.
144#[derive(Debug, Clone, Copy)]
145pub struct SectionReinforcementDraft<'a> {
146    /// `LongitudinalStartPosition`, a length. Required.
147    pub longitudinal_start_position: f64,
148    /// `LongitudinalEndPosition`, a length. Required.
149    pub longitudinal_end_position: f64,
150    /// `TransversePosition`, a length.
151    pub transverse_position: Option<f64>,
152    /// `ReinforcementRole`, from `IfcReinforcingBarRoleEnum`. Required.
153    pub reinforcement_role: &'a str,
154    /// `SectionDefinition`: an `IfcSectionProperties`. Required.
155    pub section_definition: EntityId,
156    /// `CrossSectionReinforcementDefinitions`: at least one
157    /// `IfcReinforcementBarProperties`.
158    pub cross_section_reinforcement_definitions: &'a [EntityId],
159}
160
161/// Stage an `IfcSectionReinforcementProperties`.
162///
163/// Both longitudinal positions are `IfcLengthMeasure`, which admits
164/// negatives, and the schema states no ordering between them: a section
165/// measured from a datum may legitimately start at a negative station.
166/// Only non-finite values are refused.
167///
168/// # Errors
169///
170/// Refuses a role outside the enumeration, a non-finite position, and an
171/// empty reinforcement-definition set, whose schema bound is `[1:?]`.
172pub fn add_section_reinforcement_properties(
173    tx: &mut Transaction,
174    draft: SectionReinforcementDraft<'_>,
175) -> PropertyResult<EntityId> {
176    const ENTITY: &str = "IFCSECTIONREINFORCEMENTPROPERTIES";
177
178    if !BAR_ROLE.contains(&draft.reinforcement_role) {
179        return Err(invalid(
180            ENTITY,
181            "ReinforcementRole",
182            draft.reinforcement_role,
183        ));
184    }
185    if draft.cross_section_reinforcement_definitions.is_empty() {
186        return Err(invalid(
187            ENTITY,
188            "CrossSectionReinforcementDefinitions",
189            "empty",
190        ));
191    }
192
193    let mut attributes = vec![Value::Null; 6];
194    attributes[0] = measure(
195        ENTITY,
196        "LongitudinalStartPosition",
197        Some(draft.longitudinal_start_position),
198        Measure::Length,
199    )?;
200    attributes[1] = measure(
201        ENTITY,
202        "LongitudinalEndPosition",
203        Some(draft.longitudinal_end_position),
204        Measure::Length,
205    )?;
206    attributes[2] = measure(
207        ENTITY,
208        "TransversePosition",
209        draft.transverse_position,
210        Measure::Length,
211    )?;
212    attributes[3] = Value::Enum(draft.reinforcement_role.into());
213    attributes[4] = Value::Ref(draft.section_definition);
214    attributes[5] = Value::List(
215        draft
216            .cross_section_reinforcement_definitions
217            .iter()
218            .copied()
219            .map(Value::Ref)
220            .collect(),
221    );
222
223    Ok(tx.create(Entity::new(ENTITY, attributes)))
224}
225
226/// Stage an `IfcReinforcementDefinitionProperties`.
227///
228/// This is the only member of the family that is an
229/// `IfcPropertySetDefinition`, so it carries the four `IfcRoot` slots and
230/// a GlobalId. The others are `IfcPropertyAbstraction` subtypes with no
231/// identity of their own.
232///
233/// Takes no model, so it writes the IFC4 layout with `OwnerHistory` `$`:
234/// valid IFC4 and IFC4X3, never valid IFC2X3. Use
235/// [`add_reinforcement_definition_properties_with_owner_history`] to
236/// write the model's declared release.
237///
238/// # Errors
239///
240/// Refuses a malformed GlobalId and an empty section-definition list,
241/// whose schema bound is `[1:?]`.
242pub fn add_reinforcement_definition_properties(
243    tx: &mut Transaction,
244    global_id: &str,
245    name: Option<&str>,
246    description: Option<&str>,
247    definition_type: Option<&str>,
248    reinforcement_section_definitions: &[EntityId],
249) -> PropertyResult<EntityId> {
250    let rooted = reinforcement_definition(
251        global_id,
252        (name, description),
253        definition_type,
254        reinforcement_section_definitions,
255    )?;
256    stage_ifc4(tx, rooted)
257}
258
259/// [`add_reinforcement_definition_properties`] in the model's declared
260/// release, with a caller-supplied `IfcOwnerHistory`, which IFC2X3
261/// requires (#202).
262///
263/// # Errors
264///
265/// Those of [`add_reinforcement_definition_properties`], and the release
266/// and owner-history refusals of
267/// [`add_door_lining_properties_with_owner_history`](crate::add_door_lining_properties_with_owner_history).
268/// Nothing is staged on an error.
269#[allow(clippy::too_many_arguments)]
270pub fn add_reinforcement_definition_properties_with_owner_history(
271    tx: &mut Transaction,
272    model: &Model,
273    global_id: &str,
274    name: Option<&str>,
275    description: Option<&str>,
276    definition_type: Option<&str>,
277    reinforcement_section_definitions: &[EntityId],
278    owner_history: EntityId,
279) -> PropertyResult<EntityId> {
280    let rooted = reinforcement_definition(
281        global_id,
282        (name, description),
283        definition_type,
284        reinforcement_section_definitions,
285    )?;
286    stage_owned(tx, model, rooted, owner_history)
287}
288
289fn reinforcement_definition<'a>(
290    global_id: &'a str,
291    (name, description): (Option<&'a str>, Option<&'a str>),
292    definition_type: Option<&str>,
293    reinforcement_section_definitions: &[EntityId],
294) -> PropertyResult<Rooted<'a>> {
295    const ENTITY: &str = "IFCREINFORCEMENTDEFINITIONPROPERTIES";
296
297    if reinforcement_section_definitions.is_empty() {
298        return Err(invalid(ENTITY, "ReinforcementSectionDefinitions", "empty"));
299    }
300    require_guid(ENTITY, global_id)?;
301    let sections = reinforcement_section_definitions.iter().copied();
302    let values = vec![
303        ("DefinitionType", optional_text(definition_type)),
304        (
305            "ReinforcementSectionDefinitions",
306            Value::List(sections.map(Value::Ref).collect()),
307        ),
308    ];
309    Ok(Rooted {
310        entity: ENTITY,
311        global_id,
312        name,
313        description,
314        values,
315    })
316}
317
318/// Stage an `IfcProfileProperties`.
319///
320/// `properties` are `IfcProperty` instances, not measures: this is the
321/// extended-property form, where the profile's characteristics are
322/// expressed as named properties rather than fixed typed slots.
323///
324/// # Errors
325///
326/// Refuses an empty property set, whose schema bound is `[1:?]`.
327pub fn add_profile_properties(
328    tx: &mut Transaction,
329    name: Option<&str>,
330    description: Option<&str>,
331    properties: &[EntityId],
332    profile_definition: EntityId,
333) -> PropertyResult<EntityId> {
334    const ENTITY: &str = "IFCPROFILEPROPERTIES";
335
336    if properties.is_empty() {
337        return Err(invalid(ENTITY, "Properties", "empty"));
338    }
339
340    let mut attributes = vec![Value::Null; 4];
341    attributes[0] = optional_text(name);
342    attributes[1] = optional_text(description);
343    attributes[2] = Value::List(properties.iter().copied().map(Value::Ref).collect());
344    attributes[3] = Value::Ref(profile_definition);
345
346    Ok(tx.create(Entity::new(ENTITY, attributes)))
347}