Skip to main content

ifc_material/authoring/
relationships.rs

1//! Authoring for the material relationship entities.
2//!
3//! Split from the parent module, which stages the material definitions
4//! themselves: materials, layers, profiles, constituents and their sets.
5//! This module stages the records that relate those definitions to each
6//! other and to the wider model -- properties, composition, external
7//! classification and presentation.
8//!
9//! Every SET here is `[1:?]` in the schema, so an empty aggregate is a
10//! malformed record rather than an under-specified one, and is refused.
11//!
12//! Release differences: IFC2X3 declares no `IfcMaterialRelationship` and an
13//! ABSTRACT `IfcMaterialProperties`, so both are refused for an IFC2X3 model;
14//! IFC4X3 renamed `IfcMaterialRelationship.Expression` to
15//! `MaterialExpression` at the same position.
16
17use ifc_model::{EntityId, Model, Transaction, Value};
18
19use super::{invalid, optional_text, refs, require_accepts, require_exists, require_type};
20use crate::error::MaterialResult;
21use crate::release::Release;
22
23/// Stage an `IfcMaterialProperties`. IFC4 onwards.
24///
25/// Properties attached to a material definition rather than to an
26/// occurrence: density, conductivity, and the rest of a datasheet.
27/// `Properties` is a `SET [1:?]`, so an empty set is malformed and
28/// refused rather than written as an empty aggregate.
29///
30/// `Material` accepts any `IfcMaterialDefinition` subtype the model's
31/// release declares, not only `IfcMaterial`: a layer, profile or
32/// constituent can carry its own properties.
33///
34/// # Errors
35///
36/// Refuses an empty property set, a `Material` reference whose target is
37/// not a material definition, and an IFC2X3 model
38/// ([`crate::MaterialError::EntityNotInSchema`]), whose
39/// `IfcMaterialProperties` is abstract.
40pub fn create_material_properties(
41    tx: &mut Transaction,
42    model: &Model,
43    name: Option<&str>,
44    description: Option<&str>,
45    properties: &[EntityId],
46    material: EntityId,
47) -> MaterialResult<EntityId> {
48    const ENTITY: &str = "IFCMATERIALPROPERTIES";
49    let release = Release::of(model);
50    release.require_entity(ENTITY, None)?;
51    if properties.is_empty() {
52        return Err(invalid(
53            ENTITY,
54            "Properties",
55            "expected at least one property",
56        ));
57    }
58    for property in properties {
59        require_exists(tx, model, *property)?;
60    }
61    require_accepts(tx, model, release, ENTITY, "Material", material)?;
62    let record = release.record(
63        ENTITY,
64        vec![
65            ("Name", optional_text(name)),
66            ("Description", optional_text(description)),
67            ("Properties", refs(properties)),
68            ("Material", Value::Ref(material)),
69        ],
70    )?;
71    Ok(tx.create(record))
72}
73
74/// Stage an `IfcMaterialRelationship`. IFC4 onwards.
75///
76/// Relates one material to the materials it is composed of or derived
77/// from: a concrete mix to its cement and aggregate. `expression` records
78/// the mix rule as authored prose (IFC4 `Expression`, IFC4X3
79/// `MaterialExpression`), not something this crate evaluates.
80///
81/// # Errors
82///
83/// Refuses an empty `RelatedMaterials` set (`SET [1:?]`), a relating
84/// material that is also among the related ones, any reference that is
85/// not an `IfcMaterial`, and an IFC2X3 model.
86pub fn create_material_relationship(
87    tx: &mut Transaction,
88    model: &Model,
89    name: Option<&str>,
90    description: Option<&str>,
91    relating: EntityId,
92    related: &[EntityId],
93    expression: Option<&str>,
94) -> MaterialResult<EntityId> {
95    const ENTITY: &str = "IFCMATERIALRELATIONSHIP";
96    let release = Release::of(model);
97    release.require_entity(ENTITY, None)?;
98    if related.is_empty() {
99        return Err(invalid(
100            ENTITY,
101            "RelatedMaterials",
102            "expected at least one related material",
103        ));
104    }
105    if related.contains(&relating) {
106        return Err(invalid(
107            ENTITY,
108            "RelatedMaterials",
109            "a material cannot be derived from itself",
110        ));
111    }
112    require_type(tx, model, release, relating, &["IFCMATERIAL"])?;
113    for material in related {
114        require_type(tx, model, release, *material, &["IFCMATERIAL"])?;
115    }
116    let record = release.record(
117        ENTITY,
118        vec![
119            ("Name", optional_text(name)),
120            ("Description", optional_text(description)),
121            ("RelatingMaterial", Value::Ref(relating)),
122            ("RelatedMaterials", refs(related)),
123            ("Expression", optional_text(expression)),
124        ],
125    )?;
126    Ok(tx.create(record))
127}
128
129/// Stage an `IfcMaterialClassificationRelationship`.
130///
131/// Classifies a material against external systems such as Uniclass or
132/// OmniClass. `MaterialClassifications` is a `SET [1:?]` (of
133/// `IfcClassificationSelect`, IFC2X3 `IfcClassificationNotationSelect`), so
134/// an unclassified relationship is refused rather than written empty.
135///
136/// # Errors
137///
138/// Refuses an empty classification set, and a `ClassifiedMaterial`
139/// that is not an `IfcMaterial`.
140pub fn create_material_classification_relationship(
141    tx: &mut Transaction,
142    model: &Model,
143    classifications: &[EntityId],
144    material: EntityId,
145) -> MaterialResult<EntityId> {
146    const ENTITY: &str = "IFCMATERIALCLASSIFICATIONRELATIONSHIP";
147    let release = Release::of(model);
148    release.require_entity(ENTITY, None)?;
149    if classifications.is_empty() {
150        return Err(invalid(
151            ENTITY,
152            "MaterialClassifications",
153            "expected at least one classification",
154        ));
155    }
156    for classification in classifications {
157        require_exists(tx, model, *classification)?;
158    }
159    require_type(tx, model, release, material, &["IFCMATERIAL"])?;
160    let record = release.record(
161        ENTITY,
162        vec![
163            ("MaterialClassifications", refs(classifications)),
164            ("ClassifiedMaterial", Value::Ref(material)),
165        ],
166    )?;
167    Ok(tx.create(record))
168}
169
170/// Stage an `IfcMaterialDefinitionRepresentation`.
171///
172/// Gives a material its presentation: the styled representations that
173/// say how it draws. The schema states OnlyStyledRepresentations (IFC2X3
174/// WR11), so every entry must be an `IfcStyledRepresentation` -- a surface
175/// style hung on a plain `IfcShapeRepresentation` parses and then renders
176/// as nothing.
177///
178/// # Errors
179///
180/// Refuses an empty representation list (`LIST [1:?]`), any entry that
181/// is not an `IfcStyledRepresentation`, and a `RepresentedMaterial`
182/// that is not an `IfcMaterial`.
183pub fn create_material_definition_representation(
184    tx: &mut Transaction,
185    model: &Model,
186    name: Option<&str>,
187    description: Option<&str>,
188    representations: &[EntityId],
189    material: EntityId,
190) -> MaterialResult<EntityId> {
191    const ENTITY: &str = "IFCMATERIALDEFINITIONREPRESENTATION";
192    let release = Release::of(model);
193    release.require_entity(ENTITY, None)?;
194    if representations.is_empty() {
195        return Err(invalid(
196            ENTITY,
197            "Representations",
198            "expected at least one styled representation",
199        ));
200    }
201    for representation in representations {
202        require_type(
203            tx,
204            model,
205            release,
206            *representation,
207            &["IFCSTYLEDREPRESENTATION"],
208        )?;
209    }
210    require_type(tx, model, release, material, &["IFCMATERIAL"])?;
211    let record = release.record(
212        ENTITY,
213        vec![
214            ("Name", optional_text(name)),
215            ("Description", optional_text(description)),
216            ("Representations", refs(representations)),
217            ("RepresentedMaterial", Value::Ref(material)),
218        ],
219    )?;
220    Ok(tx.create(record))
221}