Skip to main content

ifc_material/authoring/
composites.rs

1//! Authoring for constituent and profile compositions.
2//!
3//! Split from the parent module, which stages materials, layers, lists and
4//! associations. Every entity staged here is IFC4 onwards: IFC2X3 declares
5//! no constituent, profile, or profile-set usage, so each function refuses
6//! an IFC2X3 model with [`crate::MaterialError::EntityNotInSchema`] before
7//! staging anything.
8
9use ifc_model::{EntityId, Model, Transaction, Value};
10
11use super::{invalid, optional_text, reals, refs, require_exists, require_type};
12use crate::release::Release;
13use crate::MaterialResult;
14
15/// Authored fields for `IfcMaterialConstituent`.
16#[derive(Debug, Clone, Copy)]
17pub struct ConstituentDraft<'a> {
18    /// `IfcMaterialConstituent.Name`, if given.
19    pub name: Option<&'a str>,
20    /// `IfcMaterialConstituent.Description`, if given.
21    pub description: Option<&'a str>,
22    /// `IfcMaterialConstituent.Material`, an `IfcMaterial` reference.
23    pub material: EntityId,
24    /// `IfcMaterialConstituent.Fraction`, if given. A ratio in `0.0..=1.0`.
25    pub fraction: Option<f64>,
26    /// `IfcMaterialConstituent.Category`, if given.
27    pub category: Option<&'a str>,
28}
29
30/// Stage an `IfcMaterialConstituent`. IFC4 onwards.
31///
32/// `Fraction` is an `IfcNormalisedRatioMeasure`: values outside `0..=1` are
33/// refused because a constituent cannot be a negative or >100% share of its
34/// set, and a wrong fraction silently misstates a composition.
35pub fn create_constituent(
36    tx: &mut Transaction,
37    model: &Model,
38    draft: ConstituentDraft<'_>,
39) -> MaterialResult<EntityId> {
40    const ENTITY: &str = "IFCMATERIALCONSTITUENT";
41    let release = Release::of(model);
42    release.require_entity(ENTITY, None)?;
43    if let Some(fraction) = draft.fraction {
44        if !fraction.is_finite() || !(0.0..=1.0).contains(&fraction) {
45            return Err(invalid(
46                ENTITY,
47                "Fraction",
48                "expected a normalised ratio in 0..=1",
49            ));
50        }
51    }
52    require_type(tx, model, release, draft.material, &["IFCMATERIAL"])?;
53    let record = release.record(
54        ENTITY,
55        vec![
56            ("Name", optional_text(draft.name)),
57            ("Description", optional_text(draft.description)),
58            ("Material", Value::Ref(draft.material)),
59            ("Fraction", draft.fraction.map_or(Value::Null, Value::Real)),
60            ("Category", optional_text(draft.category)),
61        ],
62    )?;
63    Ok(tx.create(record))
64}
65
66/// Stage an `IfcMaterialConstituentSet`. IFC4 onwards. Must name at least
67/// one constituent: an empty set describes no composition at all.
68pub fn create_constituent_set(
69    tx: &mut Transaction,
70    model: &Model,
71    constituents: &[EntityId],
72    name: Option<&str>,
73    description: Option<&str>,
74) -> MaterialResult<EntityId> {
75    const ENTITY: &str = "IFCMATERIALCONSTITUENTSET";
76    let release = Release::of(model);
77    release.require_entity(ENTITY, None)?;
78    if constituents.is_empty() {
79        return Err(invalid(
80            ENTITY,
81            "MaterialConstituents",
82            "expected at least one constituent",
83        ));
84    }
85    for &constituent in constituents {
86        require_type(tx, model, release, constituent, &["IFCMATERIALCONSTITUENT"])?;
87    }
88    let record = release.record(
89        ENTITY,
90        vec![
91            ("Name", optional_text(name)),
92            ("Description", optional_text(description)),
93            ("MaterialConstituents", refs(constituents)),
94        ],
95    )?;
96    Ok(tx.create(record))
97}
98
99/// Authored fields for `IfcMaterialProfile`.
100#[derive(Debug, Clone, Copy)]
101pub struct ProfileDraft<'a> {
102    /// `IfcMaterialProfile.Name`, if given.
103    pub name: Option<&'a str>,
104    /// `IfcMaterialProfile.Description`, if given.
105    pub description: Option<&'a str>,
106    /// `IfcMaterialProfile.Material`, an `IfcMaterial` reference, if given.
107    pub material: Option<EntityId>,
108    /// `IfcMaterialProfile.Profile`, an `IfcProfileDef` reference.
109    pub profile: EntityId,
110    /// `IfcMaterialProfile.Priority`, if given. Must be in `0..=100`.
111    pub priority: Option<i64>,
112    /// `IfcMaterialProfile.Category`, if given.
113    pub category: Option<&'a str>,
114}
115
116/// The profile attributes [`create_profile`] and
117/// [`create_profile_with_offsets`] share, after checking them.
118fn profile_values(
119    tx: &Transaction,
120    model: &Model,
121    release: Release<'_>,
122    entity: &'static str,
123    draft: &ProfileDraft<'_>,
124) -> MaterialResult<Vec<(&'static str, Value)>> {
125    release.require_entity(entity, None)?;
126    if let Some(priority) = draft.priority.filter(|value| !(0..=100).contains(value)) {
127        return Err(invalid(entity, "Priority", priority.to_string()));
128    }
129    if let Some(material) = draft.material {
130        require_type(tx, model, release, material, &["IFCMATERIAL"])?;
131    }
132    require_exists(tx, model, draft.profile)?;
133    Ok(vec![
134        ("Name", optional_text(draft.name)),
135        ("Description", optional_text(draft.description)),
136        ("Material", draft.material.map_or(Value::Null, Value::Ref)),
137        ("Profile", Value::Ref(draft.profile)),
138        (
139            "Priority",
140            draft.priority.map_or(Value::Null, Value::Integer),
141        ),
142        ("Category", optional_text(draft.category)),
143    ])
144}
145
146/// Stage an `IfcMaterialProfile`. IFC4 onwards.
147///
148/// The profile reference must exist; an arbitrary missing id here would
149/// produce a material profile with no cross-section.
150pub fn create_profile(
151    tx: &mut Transaction,
152    model: &Model,
153    draft: ProfileDraft<'_>,
154) -> MaterialResult<EntityId> {
155    const ENTITY: &str = "IFCMATERIALPROFILE";
156    let release = Release::of(model);
157    let values = profile_values(tx, model, release, ENTITY, &draft)?;
158    Ok(tx.create(release.record(ENTITY, values)?))
159}
160
161/// Stage an `IfcMaterialProfileSet`. IFC4 onwards. Must name at least one
162/// profile.
163pub fn create_profile_set(
164    tx: &mut Transaction,
165    model: &Model,
166    profiles: &[EntityId],
167    name: Option<&str>,
168    description: Option<&str>,
169    composite_profile: Option<EntityId>,
170) -> MaterialResult<EntityId> {
171    const ENTITY: &str = "IFCMATERIALPROFILESET";
172    let release = Release::of(model);
173    release.require_entity(ENTITY, None)?;
174    if profiles.is_empty() {
175        return Err(invalid(
176            ENTITY,
177            "MaterialProfiles",
178            "expected at least one profile",
179        ));
180    }
181    for &profile in profiles {
182        require_type(tx, model, release, profile, &["IFCMATERIALPROFILE"])?;
183    }
184    if let Some(composite) = composite_profile {
185        require_exists(tx, model, composite)?;
186    }
187    let record = release.record(
188        ENTITY,
189        vec![
190            ("Name", optional_text(name)),
191            ("Description", optional_text(description)),
192            ("MaterialProfiles", refs(profiles)),
193            (
194                "CompositeProfile",
195                composite_profile.map_or(Value::Null, Value::Ref),
196            ),
197        ],
198    )?;
199    Ok(tx.create(record))
200}
201
202/// Stage an `IfcMaterialProfileSetUsage`. IFC4 onwards.
203///
204/// `CardinalPoint` selects the cross-section reference point and is an
205/// `IfcCardinalPointReference` in `1..=9`; anything else names no point.
206pub fn create_profile_set_usage(
207    tx: &mut Transaction,
208    model: &Model,
209    for_profile_set: EntityId,
210    cardinal_point: Option<i64>,
211    reference_extent: Option<f64>,
212) -> MaterialResult<EntityId> {
213    const ENTITY: &str = "IFCMATERIALPROFILESETUSAGE";
214    let release = Release::of(model);
215    release.require_entity(ENTITY, None)?;
216    require_type(
217        tx,
218        model,
219        release,
220        for_profile_set,
221        &["IFCMATERIALPROFILESET"],
222    )?;
223    if let Some(point) = cardinal_point.filter(|value| !(1..=9).contains(value)) {
224        return Err(invalid(ENTITY, "CardinalPoint", point.to_string()));
225    }
226    let record = release.record(
227        ENTITY,
228        vec![
229            ("ForProfileSet", Value::Ref(for_profile_set)),
230            (
231                "CardinalPoint",
232                cardinal_point.map_or(Value::Null, Value::Integer),
233            ),
234            (
235                "ReferenceExtent",
236                reference_extent.map_or(Value::Null, Value::Real),
237            ),
238        ],
239    )?;
240    Ok(tx.create(record))
241}
242
243/// Stage an `IfcMaterialProfileWithOffsets`. IFC4 onwards.
244///
245/// The offset variant of [`create_profile`]. `OffsetValues` is an
246/// `ARRAY [1:2]`: two finite lengths, so a single value or three is
247/// not an under-specified profile but a malformed one.
248///
249/// # Errors
250///
251/// Refuses non-finite offsets, a priority outside `0..=100`, and a
252/// `Profile` or `Material` reference whose target is the wrong type.
253pub fn create_profile_with_offsets(
254    tx: &mut Transaction,
255    model: &Model,
256    draft: ProfileDraft<'_>,
257    offset_values: [f64; 2],
258) -> MaterialResult<EntityId> {
259    const ENTITY: &str = "IFCMATERIALPROFILEWITHOFFSETS";
260    let release = Release::of(model);
261    release.require_entity(ENTITY, None)?;
262    if offset_values.iter().any(|value| !value.is_finite()) {
263        return Err(invalid(ENTITY, "OffsetValues", "expected finite lengths"));
264    }
265    let mut values = profile_values(tx, model, release, ENTITY, &draft)?;
266    values.push(("OffsetValues", reals(&offset_values)));
267    Ok(tx.create(release.record(ENTITY, values)?))
268}
269
270/// Stage an `IfcMaterialProfileSetUsageTapering`. IFC4 onwards.
271///
272/// Five slots: three inherited, then its own two.
273///
274/// # Errors
275///
276/// Refuses a set that is not an `IfcMaterialProfileSet`, and a
277/// cardinal point outside 1..=9 at either end.
278pub fn create_profile_set_usage_tapering(
279    tx: &mut Transaction,
280    model: &Model,
281    for_profile_set: EntityId,
282    for_profile_end_set: EntityId,
283    cardinal_point: Option<i64>,
284    cardinal_end_point: Option<i64>,
285    reference_extent: Option<f64>,
286) -> MaterialResult<EntityId> {
287    const ENTITY: &str = "IFCMATERIALPROFILESETUSAGETAPERING";
288    let release = Release::of(model);
289    release.require_entity(ENTITY, None)?;
290    for set in [for_profile_set, for_profile_end_set] {
291        require_type(tx, model, release, set, &["IFCMATERIALPROFILESET"])?;
292    }
293    // Both ends carry the same 1..=9 cardinal point range.
294    for (point, attribute) in [
295        (cardinal_point, "CardinalPoint"),
296        (cardinal_end_point, "CardinalEndPoint"),
297    ] {
298        if let Some(value) = point.filter(|value| !(1..=9).contains(value)) {
299            return Err(invalid(ENTITY, attribute, value.to_string()));
300        }
301    }
302    let record = release.record(
303        ENTITY,
304        vec![
305            ("ForProfileSet", Value::Ref(for_profile_set)),
306            (
307                "CardinalPoint",
308                cardinal_point.map_or(Value::Null, Value::Integer),
309            ),
310            (
311                "ReferenceExtent",
312                reference_extent.map_or(Value::Null, Value::Real),
313            ),
314            ("ForProfileEndSet", Value::Ref(for_profile_end_set)),
315            (
316                "CardinalEndPoint",
317                cardinal_end_point.map_or(Value::Null, Value::Integer),
318            ),
319        ],
320    )?;
321    Ok(tx.create(record))
322}