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)]
17#[non_exhaustive]
18pub struct ConstituentDraft<'a> {
19    /// `IfcMaterialConstituent.Name`, if given.
20    pub name: Option<&'a str>,
21    /// `IfcMaterialConstituent.Description`, if given.
22    pub description: Option<&'a str>,
23    /// `IfcMaterialConstituent.Material`, an `IfcMaterial` reference.
24    pub material: EntityId,
25    /// `IfcMaterialConstituent.Fraction`, if given. A ratio in `0.0..=1.0`.
26    pub fraction: Option<f64>,
27    /// `IfcMaterialConstituent.Category`, if given.
28    pub category: Option<&'a str>,
29}
30
31impl<'a> ConstituentDraft<'a> {
32    /// Starts a draft for a constituent made of `material`.
33    #[must_use]
34    pub const fn new(material: EntityId) -> Self {
35        Self {
36            name: None,
37            description: None,
38            material,
39            fraction: None,
40            category: None,
41        }
42    }
43
44    /// Sets `Name`.
45    #[must_use]
46    pub const fn name(mut self, value: &'a str) -> Self {
47        self.name = Some(value);
48        self
49    }
50
51    /// Sets `Description`.
52    #[must_use]
53    pub const fn description(mut self, value: &'a str) -> Self {
54        self.description = Some(value);
55        self
56    }
57
58    /// Sets `Fraction`, a ratio in `0.0..=1.0`.
59    #[must_use]
60    pub const fn fraction(mut self, value: f64) -> Self {
61        self.fraction = Some(value);
62        self
63    }
64
65    /// Sets `Category`.
66    #[must_use]
67    pub const fn category(mut self, value: &'a str) -> Self {
68        self.category = Some(value);
69        self
70    }
71}
72
73/// Stage an `IfcMaterialConstituent`. IFC4 onwards.
74///
75/// `Fraction` is an `IfcNormalisedRatioMeasure`: values outside `0..=1` are
76/// refused because a constituent cannot be a negative or >100% share of its
77/// set, and a wrong fraction silently misstates a composition.
78pub fn create_constituent(
79    tx: &mut Transaction,
80    model: &Model,
81    draft: ConstituentDraft<'_>,
82) -> MaterialResult<EntityId> {
83    const ENTITY: &str = "IFCMATERIALCONSTITUENT";
84    let release = Release::of(model);
85    release.require_entity(ENTITY, None)?;
86    if let Some(fraction) = draft.fraction {
87        if !fraction.is_finite() || !(0.0..=1.0).contains(&fraction) {
88            return Err(invalid(
89                ENTITY,
90                "Fraction",
91                "expected a normalised ratio in 0..=1",
92            ));
93        }
94    }
95    require_type(tx, model, release, draft.material, &["IFCMATERIAL"])?;
96    let record = release.record(
97        ENTITY,
98        vec![
99            ("Name", optional_text(draft.name)),
100            ("Description", optional_text(draft.description)),
101            ("Material", Value::Ref(draft.material)),
102            ("Fraction", draft.fraction.map_or(Value::Null, Value::Real)),
103            ("Category", optional_text(draft.category)),
104        ],
105    )?;
106    Ok(tx.create(record))
107}
108
109/// Stage an `IfcMaterialConstituentSet`. IFC4 onwards. Must name at least
110/// one constituent: an empty set describes no composition at all.
111pub fn create_constituent_set(
112    tx: &mut Transaction,
113    model: &Model,
114    constituents: &[EntityId],
115    name: Option<&str>,
116    description: Option<&str>,
117) -> MaterialResult<EntityId> {
118    const ENTITY: &str = "IFCMATERIALCONSTITUENTSET";
119    let release = Release::of(model);
120    release.require_entity(ENTITY, None)?;
121    if constituents.is_empty() {
122        return Err(invalid(
123            ENTITY,
124            "MaterialConstituents",
125            "expected at least one constituent",
126        ));
127    }
128    for &constituent in constituents {
129        require_type(tx, model, release, constituent, &["IFCMATERIALCONSTITUENT"])?;
130    }
131    let record = release.record(
132        ENTITY,
133        vec![
134            ("Name", optional_text(name)),
135            ("Description", optional_text(description)),
136            ("MaterialConstituents", refs(constituents)),
137        ],
138    )?;
139    Ok(tx.create(record))
140}
141
142/// Authored fields for `IfcMaterialProfile`.
143#[derive(Debug, Clone, Copy)]
144#[non_exhaustive]
145pub struct ProfileDraft<'a> {
146    /// `IfcMaterialProfile.Name`, if given.
147    pub name: Option<&'a str>,
148    /// `IfcMaterialProfile.Description`, if given.
149    pub description: Option<&'a str>,
150    /// `IfcMaterialProfile.Material`, an `IfcMaterial` reference, if given.
151    pub material: Option<EntityId>,
152    /// `IfcMaterialProfile.Profile`, an `IfcProfileDef` reference.
153    pub profile: EntityId,
154    /// `IfcMaterialProfile.Priority`, if given. Must be in `0..=100`.
155    pub priority: Option<i64>,
156    /// `IfcMaterialProfile.Category`, if given.
157    pub category: Option<&'a str>,
158}
159
160impl<'a> ProfileDraft<'a> {
161    /// Starts a draft for a material profile of `profile`, an
162    /// `IfcProfileDef` reference.
163    #[must_use]
164    pub const fn new(profile: EntityId) -> Self {
165        Self {
166            name: None,
167            description: None,
168            material: None,
169            profile,
170            priority: None,
171            category: None,
172        }
173    }
174
175    /// Sets `Name`.
176    #[must_use]
177    pub const fn name(mut self, value: &'a str) -> Self {
178        self.name = Some(value);
179        self
180    }
181
182    /// Sets `Description`.
183    #[must_use]
184    pub const fn description(mut self, value: &'a str) -> Self {
185        self.description = Some(value);
186        self
187    }
188
189    /// Sets `Material`, an `IfcMaterial` reference.
190    #[must_use]
191    pub const fn material(mut self, value: EntityId) -> Self {
192        self.material = Some(value);
193        self
194    }
195
196    /// Sets `Priority`, in `0..=100`.
197    #[must_use]
198    pub const fn priority(mut self, value: i64) -> Self {
199        self.priority = Some(value);
200        self
201    }
202
203    /// Sets `Category`.
204    #[must_use]
205    pub const fn category(mut self, value: &'a str) -> Self {
206        self.category = Some(value);
207        self
208    }
209}
210
211/// The profile attributes [`create_profile`] and
212/// [`create_profile_with_offsets`] share, after checking them.
213fn profile_values(
214    tx: &Transaction,
215    model: &Model,
216    release: Release<'_>,
217    entity: &'static str,
218    draft: &ProfileDraft<'_>,
219) -> MaterialResult<Vec<(&'static str, Value)>> {
220    release.require_entity(entity, None)?;
221    if let Some(priority) = draft.priority.filter(|value| !(0..=100).contains(value)) {
222        return Err(invalid(entity, "Priority", priority.to_string()));
223    }
224    if let Some(material) = draft.material {
225        require_type(tx, model, release, material, &["IFCMATERIAL"])?;
226    }
227    require_exists(tx, model, draft.profile)?;
228    Ok(vec![
229        ("Name", optional_text(draft.name)),
230        ("Description", optional_text(draft.description)),
231        ("Material", draft.material.map_or(Value::Null, Value::Ref)),
232        ("Profile", Value::Ref(draft.profile)),
233        (
234            "Priority",
235            draft.priority.map_or(Value::Null, Value::Integer),
236        ),
237        ("Category", optional_text(draft.category)),
238    ])
239}
240
241/// Stage an `IfcMaterialProfile`. IFC4 onwards.
242///
243/// The profile reference must exist; an arbitrary missing id here would
244/// produce a material profile with no cross-section.
245pub fn create_profile(
246    tx: &mut Transaction,
247    model: &Model,
248    draft: ProfileDraft<'_>,
249) -> MaterialResult<EntityId> {
250    const ENTITY: &str = "IFCMATERIALPROFILE";
251    let release = Release::of(model);
252    let values = profile_values(tx, model, release, ENTITY, &draft)?;
253    Ok(tx.create(release.record(ENTITY, values)?))
254}
255
256/// Stage an `IfcMaterialProfileSet`. IFC4 onwards. Must name at least one
257/// profile.
258pub fn create_profile_set(
259    tx: &mut Transaction,
260    model: &Model,
261    profiles: &[EntityId],
262    name: Option<&str>,
263    description: Option<&str>,
264    composite_profile: Option<EntityId>,
265) -> MaterialResult<EntityId> {
266    const ENTITY: &str = "IFCMATERIALPROFILESET";
267    let release = Release::of(model);
268    release.require_entity(ENTITY, None)?;
269    if profiles.is_empty() {
270        return Err(invalid(
271            ENTITY,
272            "MaterialProfiles",
273            "expected at least one profile",
274        ));
275    }
276    for &profile in profiles {
277        require_type(tx, model, release, profile, &["IFCMATERIALPROFILE"])?;
278    }
279    if let Some(composite) = composite_profile {
280        require_exists(tx, model, composite)?;
281    }
282    let record = release.record(
283        ENTITY,
284        vec![
285            ("Name", optional_text(name)),
286            ("Description", optional_text(description)),
287            ("MaterialProfiles", refs(profiles)),
288            (
289                "CompositeProfile",
290                composite_profile.map_or(Value::Null, Value::Ref),
291            ),
292        ],
293    )?;
294    Ok(tx.create(record))
295}
296
297/// Stage an `IfcMaterialProfileSetUsage`. IFC4 onwards.
298///
299/// `CardinalPoint` selects the cross-section reference point and is an
300/// `IfcCardinalPointReference` in `1..=9`; anything else names no point.
301pub fn create_profile_set_usage(
302    tx: &mut Transaction,
303    model: &Model,
304    for_profile_set: EntityId,
305    cardinal_point: Option<i64>,
306    reference_extent: Option<f64>,
307) -> MaterialResult<EntityId> {
308    const ENTITY: &str = "IFCMATERIALPROFILESETUSAGE";
309    let release = Release::of(model);
310    release.require_entity(ENTITY, None)?;
311    require_type(
312        tx,
313        model,
314        release,
315        for_profile_set,
316        &["IFCMATERIALPROFILESET"],
317    )?;
318    if let Some(point) = cardinal_point.filter(|value| !(1..=9).contains(value)) {
319        return Err(invalid(ENTITY, "CardinalPoint", point.to_string()));
320    }
321    let record = release.record(
322        ENTITY,
323        vec![
324            ("ForProfileSet", Value::Ref(for_profile_set)),
325            (
326                "CardinalPoint",
327                cardinal_point.map_or(Value::Null, Value::Integer),
328            ),
329            (
330                "ReferenceExtent",
331                reference_extent.map_or(Value::Null, Value::Real),
332            ),
333        ],
334    )?;
335    Ok(tx.create(record))
336}
337
338/// Stage an `IfcMaterialProfileWithOffsets`. IFC4 onwards.
339///
340/// The offset variant of [`create_profile`]. `OffsetValues` is an
341/// `ARRAY [1:2]`: two finite lengths, so a single value or three is
342/// not an under-specified profile but a malformed one.
343///
344/// # Errors
345///
346/// Refuses non-finite offsets, a priority outside `0..=100`, and a
347/// `Profile` or `Material` reference whose target is the wrong type.
348pub fn create_profile_with_offsets(
349    tx: &mut Transaction,
350    model: &Model,
351    draft: ProfileDraft<'_>,
352    offset_values: [f64; 2],
353) -> MaterialResult<EntityId> {
354    const ENTITY: &str = "IFCMATERIALPROFILEWITHOFFSETS";
355    let release = Release::of(model);
356    release.require_entity(ENTITY, None)?;
357    if offset_values.iter().any(|value| !value.is_finite()) {
358        return Err(invalid(ENTITY, "OffsetValues", "expected finite lengths"));
359    }
360    let mut values = profile_values(tx, model, release, ENTITY, &draft)?;
361    values.push(("OffsetValues", reals(&offset_values)));
362    Ok(tx.create(release.record(ENTITY, values)?))
363}
364
365/// Stage an `IfcMaterialProfileSetUsageTapering`. IFC4 onwards.
366///
367/// Five slots: three inherited, then its own two.
368///
369/// # Errors
370///
371/// Refuses a set that is not an `IfcMaterialProfileSet`, and a
372/// cardinal point outside 1..=9 at either end.
373pub fn create_profile_set_usage_tapering(
374    tx: &mut Transaction,
375    model: &Model,
376    for_profile_set: EntityId,
377    for_profile_end_set: EntityId,
378    cardinal_point: Option<i64>,
379    cardinal_end_point: Option<i64>,
380    reference_extent: Option<f64>,
381) -> MaterialResult<EntityId> {
382    const ENTITY: &str = "IFCMATERIALPROFILESETUSAGETAPERING";
383    let release = Release::of(model);
384    release.require_entity(ENTITY, None)?;
385    for set in [for_profile_set, for_profile_end_set] {
386        require_type(tx, model, release, set, &["IFCMATERIALPROFILESET"])?;
387    }
388    // Both ends carry the same 1..=9 cardinal point range.
389    for (point, attribute) in [
390        (cardinal_point, "CardinalPoint"),
391        (cardinal_end_point, "CardinalEndPoint"),
392    ] {
393        if let Some(value) = point.filter(|value| !(1..=9).contains(value)) {
394            return Err(invalid(ENTITY, attribute, value.to_string()));
395        }
396    }
397    let record = release.record(
398        ENTITY,
399        vec![
400            ("ForProfileSet", Value::Ref(for_profile_set)),
401            (
402                "CardinalPoint",
403                cardinal_point.map_or(Value::Null, Value::Integer),
404            ),
405            (
406                "ReferenceExtent",
407                reference_extent.map_or(Value::Null, Value::Real),
408            ),
409            ("ForProfileEndSet", Value::Ref(for_profile_end_set)),
410            (
411                "CardinalEndPoint",
412                cardinal_end_point.map_or(Value::Null, Value::Integer),
413            ),
414        ],
415    )?;
416    Ok(tx.create(record))
417}