Skip to main content

ifc_style/authoring/
surface.rs

1//! Authoring colours, surface styles, and presentation layers.
2//!
3//! Split from the module root: these writers form one concern --
4//! how a surface looks -- and the root was over the monolith limit.
5
6use ifc_model::{Edit, EntityId, Model, Transaction, Value};
7use ifc_schema::Schema;
8
9use super::{
10    build_named, enumeration, invalid_authoring, optional_reference, optional_text, text,
11    validate_optional_ref, validate_ratio, validate_ref,
12};
13use crate::colour::ColourOrFactor;
14use crate::error::{StyleError, StyleResult};
15use crate::surface_style::{duplicate_surface_element_category, SURFACE_STYLE_ELEMENT_MEMBERS};
16
17/// Draft input for [`create_colour_rgb`]: the writable attributes of a new
18/// `IfcColourRgb`.
19#[derive(Debug, Clone, Copy)]
20pub struct ColourRgbDraft<'a> {
21    /// The `Name` attribute, when supplied.
22    pub name: Option<&'a str>,
23    /// The `Red` channel; must be a finite value in `[0, 1]`.
24    pub red: f64,
25    /// The `Green` channel; must be a finite value in `[0, 1]`.
26    pub green: f64,
27    /// The `Blue` channel; must be a finite value in `[0, 1]`.
28    pub blue: f64,
29}
30
31/// Draft input for [`create_surface_style_shading`]: the writable attributes
32/// of a new `IfcSurfaceStyleShading`.
33#[derive(Debug, Clone, Copy)]
34pub struct SurfaceStyleShadingDraft {
35    /// The `SurfaceColour` reference to an `IfcColourRgb`.
36    pub surface_colour: EntityId,
37    /// The `Transparency` factor, when supplied; must be a finite value in
38    /// `[0, 1]`.
39    pub transparency: Option<f64>,
40}
41
42/// Draft input for [`create_surface_style`]: the writable attributes of a
43/// new `IfcSurfaceStyle`.
44#[derive(Debug, Clone)]
45pub struct SurfaceStyleDraft<'a> {
46    /// The `Name` attribute, when supplied.
47    pub name: Option<&'a str>,
48    /// The `Side` attribute.
49    pub side: crate::SurfaceSide,
50    /// The `Styles` elements: one to five references, no two from the same
51    /// surface-style element category (shading, lighting, refraction,
52    /// textures, externally defined).
53    pub elements: Vec<EntityId>,
54}
55
56/// Draft input for [`create_styled_item`]: the writable attributes of a new
57/// `IfcStyledItem`.
58#[derive(Debug, Clone)]
59pub struct StyledItemDraft<'a> {
60    /// The `Item` reference to an `IfcRepresentationItem`, when supplied.
61    pub item: Option<EntityId>,
62    /// The `Styles` references; at least one is required. On IFC2x3 these
63    /// are wrapped in a staged `IfcPresentationStyleAssignment`.
64    pub styles: Vec<EntityId>,
65    /// The `Name` attribute, when supplied.
66    pub name: Option<&'a str>,
67}
68
69/// Draft input for [`create_presentation_layer_with_style`]: the writable
70/// attributes of a new `IfcPresentationLayerWithStyle`.
71#[derive(Debug, Clone)]
72pub struct PresentationLayerDraft<'a> {
73    /// The `Name` attribute; must be non-empty.
74    pub name: &'a str,
75    /// The `Description` attribute, when supplied.
76    pub description: Option<&'a str>,
77    /// The `AssignedItems` references; at least one is required, and each
78    /// must resolve to an `IfcRepresentation` or `IfcRepresentationItem`.
79    pub assigned_items: Vec<EntityId>,
80    /// The `Identifier` attribute, when supplied.
81    pub identifier: Option<&'a str>,
82    /// The `LayerOn` attribute, when supplied.
83    pub layer_on: Option<bool>,
84    /// The `LayerFrozen` attribute, when supplied.
85    pub layer_frozen: Option<bool>,
86    /// The `LayerBlocked` attribute, when supplied.
87    pub layer_blocked: Option<bool>,
88    /// The `LayerStyles` references.
89    pub layer_styles: Vec<EntityId>,
90}
91
92/// Stage a new `IfcColourRgb` in `tx`. Fails if any channel is not a finite
93/// value in `[0, 1]`.
94pub fn create_colour_rgb(
95    tx: &mut Transaction,
96    schema: &Schema,
97    draft: ColourRgbDraft<'_>,
98) -> StyleResult<EntityId> {
99    validate_ratio("IfcColourRgb", "Red", draft.red)?;
100    validate_ratio("IfcColourRgb", "Green", draft.green)?;
101    validate_ratio("IfcColourRgb", "Blue", draft.blue)?;
102    let mut values = Vec::new();
103    optional_text(&mut values, "Name", draft.name);
104    values.extend([
105        ("Red", Value::Real(draft.red)),
106        ("Green", Value::Real(draft.green)),
107        ("Blue", Value::Real(draft.blue)),
108    ]);
109    Ok(tx.create(build_named(schema, "IfcColourRgb", values)?))
110}
111
112/// Stage a new `IfcSurfaceStyleShading` in `tx`. Fails if `surface_colour`
113/// does not resolve to an `IfcColourRgb`, or `transparency` is not a finite
114/// value in `[0, 1]`.
115pub fn create_surface_style_shading(
116    tx: &mut Transaction,
117    model: &Model,
118    schema: &Schema,
119    draft: SurfaceStyleShadingDraft,
120) -> StyleResult<EntityId> {
121    validate_ref(tx, model, schema, draft.surface_colour, "IfcColourRgb")?;
122    if let Some(value) = draft.transparency {
123        validate_ratio("IfcSurfaceStyleShading", "Transparency", value)?;
124    }
125    let mut values = vec![("SurfaceColour", Value::Ref(draft.surface_colour))];
126    if let Some(value) = draft.transparency {
127        values.push(("Transparency", Value::Real(value)));
128    }
129    Ok(tx.create(build_named(schema, "IfcSurfaceStyleShading", values)?))
130}
131
132/// Stage a new `IfcSurfaceStyle` in `tx`. Fails if `elements` is empty,
133/// exceeds five members, contains two members from the same surface-style
134/// element category, or any member does not resolve to its expected type.
135pub fn create_surface_style(
136    tx: &mut Transaction,
137    model: &Model,
138    schema: &Schema,
139    draft: SurfaceStyleDraft<'_>,
140) -> StyleResult<EntityId> {
141    if draft.elements.is_empty() || draft.elements.len() > 5 {
142        return Err(invalid_authoring(
143            "IfcSurfaceStyle",
144            "Styles",
145            format!("expected 1..=5 elements, found {}", draft.elements.len()),
146        ));
147    }
148    let mut element_types = Vec::with_capacity(draft.elements.len());
149    for element in &draft.elements {
150        element_types.push(validate_surface_element(tx, model, schema, *element)?);
151    }
152    if let Some(category) =
153        duplicate_surface_element_category(schema, element_types.iter().map(String::as_str))
154    {
155        return Err(invalid_authoring(
156            "IfcSurfaceStyle",
157            "Styles",
158            format!("duplicate {category} category"),
159        ));
160    }
161    let mut values = Vec::new();
162    optional_text(&mut values, "Name", draft.name);
163    values.push(("Side", enumeration(draft.side.as_ifc())));
164    values.push((
165        "Styles",
166        Value::List(draft.elements.into_iter().map(Value::Ref).collect()),
167    ));
168    Ok(tx.create(build_named(schema, "IfcSurfaceStyle", values)?))
169}
170
171/// Stage a new `IfcStyledItem` in `tx`. Fails if `styles` is empty, `item`
172/// does not resolve to an `IfcRepresentationItem`, or a style does not
173/// resolve to its expected type for the target schema version.
174pub fn create_styled_item(
175    tx: &mut Transaction,
176    model: &Model,
177    schema: &Schema,
178    draft: StyledItemDraft<'_>,
179) -> StyleResult<EntityId> {
180    validate_optional_ref(tx, model, schema, draft.item, "IfcRepresentationItem")?;
181    if draft.styles.is_empty() {
182        return Err(invalid_authoring(
183            "IfcStyledItem",
184            "Styles",
185            "at least one style is required",
186        ));
187    }
188    for style in &draft.styles {
189        validate_ref(tx, model, schema, *style, "IfcPresentationStyle")?;
190    }
191
192    let mut staged = tx.clone();
193    let style_values = if schema.version() == Some(ifc_schema::SchemaVersion::Ifc2x3) {
194        let wrapper = build_named(
195            schema,
196            "IfcPresentationStyleAssignment",
197            vec![(
198                "Styles",
199                Value::List(draft.styles.into_iter().map(Value::Ref).collect()),
200            )],
201        )?;
202        vec![Value::Ref(staged.create(wrapper))]
203    } else {
204        draft.styles.into_iter().map(Value::Ref).collect()
205    };
206    let mut values = vec![("Styles", Value::List(style_values))];
207    optional_reference(&mut values, "Item", draft.item);
208    optional_text(&mut values, "Name", draft.name);
209    let id = staged.create(build_named(schema, "IfcStyledItem", values)?);
210    *tx = staged;
211    Ok(id)
212}
213
214/// Stage a new `IfcPresentationLayerWithStyle` in `tx`. Fails if `name` is
215/// empty, `assigned_items` is empty, or any assigned item or layer style
216/// does not resolve to its expected type.
217pub fn create_presentation_layer_with_style(
218    tx: &mut Transaction,
219    model: &Model,
220    schema: &Schema,
221    draft: PresentationLayerDraft<'_>,
222) -> StyleResult<EntityId> {
223    if draft.name.is_empty() {
224        return Err(invalid_authoring(
225            "IfcPresentationLayerWithStyle",
226            "Name",
227            "must not be empty",
228        ));
229    }
230    if draft.assigned_items.is_empty() {
231        return Err(invalid_authoring(
232            "IfcPresentationLayerWithStyle",
233            "AssignedItems",
234            "at least one item is required",
235        ));
236    }
237    for item in &draft.assigned_items {
238        validate_layered_item(tx, model, schema, *item)?;
239    }
240    for style in &draft.layer_styles {
241        validate_ref(tx, model, schema, *style, "IfcPresentationStyle")?;
242    }
243    let mut values = vec![
244        ("Name", text(draft.name)),
245        (
246            "AssignedItems",
247            Value::List(draft.assigned_items.into_iter().map(Value::Ref).collect()),
248        ),
249        (
250            "LayerStyles",
251            Value::List(draft.layer_styles.into_iter().map(Value::Ref).collect()),
252        ),
253    ];
254    optional_text(&mut values, "Description", draft.description);
255    optional_text(&mut values, "Identifier", draft.identifier);
256    if let Some(value) = draft.layer_on {
257        values.push(("LayerOn", Value::Bool(value)));
258    }
259    if let Some(value) = draft.layer_frozen {
260        values.push(("LayerFrozen", Value::Bool(value)));
261    }
262    if let Some(value) = draft.layer_blocked {
263        values.push(("LayerBlocked", Value::Bool(value)));
264    }
265    Ok(tx.create(build_named(
266        schema,
267        "IfcPresentationLayerWithStyle",
268        values,
269    )?))
270}
271
272fn staged_type(tx: &Transaction, model: &Model, target: EntityId) -> Option<String> {
273    tx.edits()
274        .iter()
275        .rev()
276        .find_map(|edit| match edit {
277            Edit::Create { id, entity } if *id == target => Some(entity.type_name.to_string()),
278            _ => None,
279        })
280        .or_else(|| model.get(target).map(|entity| entity.type_name.to_string()))
281}
282
283fn validate_surface_element(
284    tx: &Transaction,
285    model: &Model,
286    schema: &Schema,
287    target: EntityId,
288) -> StyleResult<String> {
289    let actual = staged_type(tx, model, target).ok_or(StyleError::DanglingReference {
290        source_id: EntityId(0),
291        target,
292    })?;
293    if SURFACE_STYLE_ELEMENT_MEMBERS
294        .iter()
295        .any(|member| schema.is_a(&actual, member))
296    {
297        Ok(actual)
298    } else {
299        Err(StyleError::ReferenceType {
300            target,
301            expected: "IfcSurfaceStyleElementSelect",
302            actual,
303        })
304    }
305}
306
307fn validate_layered_item(
308    tx: &Transaction,
309    model: &Model,
310    schema: &Schema,
311    target: EntityId,
312) -> StyleResult<()> {
313    let actual = staged_type(tx, model, target).ok_or(StyleError::DanglingReference {
314        source_id: EntityId(0),
315        target,
316    })?;
317    if schema.is_a(&actual, "IfcRepresentationItem") || schema.is_a(&actual, "IfcRepresentation") {
318        Ok(())
319    } else {
320        Err(StyleError::ReferenceType {
321            target,
322            expected: "IfcLayeredItem",
323            actual,
324        })
325    }
326}
327
328/// Authored fields for an `IfcSurfaceStyleRendering`.
329///
330/// The rendering form extends the shading form with the parameters a
331/// renderer needs. Every colour slot is an `IfcColourOrFactor`, so a
332/// caller states either an explicit colour or a factor of the surface
333/// colour -- the two are not interchangeable and the schema keeps both.
334#[derive(Debug, Clone, Copy)]
335pub struct SurfaceStyleRenderingDraft {
336    /// `SurfaceColour`, an `IfcColourRgb`.
337    pub surface_colour: EntityId,
338    /// `Transparency`, a normalised ratio.
339    pub transparency: Option<f64>,
340    /// `DiffuseColour`.
341    pub diffuse: Option<ColourOrFactor>,
342    /// `TransmissionColour`.
343    pub transmission: Option<ColourOrFactor>,
344    /// `DiffuseTransmissionColour`.
345    pub diffuse_transmission: Option<ColourOrFactor>,
346    /// `ReflectionColour`.
347    pub reflection: Option<ColourOrFactor>,
348    /// `SpecularColour`.
349    pub specular: Option<ColourOrFactor>,
350    /// `ReflectanceMethod`, an `IfcReflectanceMethodEnum` token.
351    pub reflectance_method: &'static str,
352}
353
354/// Stage an `IfcSurfaceStyleRendering`.
355///
356/// # Errors
357///
358/// Refuses a `surface_colour` or colour member that is not an
359/// `IfcColourRgb`, a factor or transparency outside `[0, 1]`, and a
360/// `reflectance_method` the schema does not declare.
361pub fn create_surface_style_rendering(
362    tx: &mut Transaction,
363    model: &Model,
364    schema: &Schema,
365    draft: SurfaceStyleRenderingDraft,
366) -> StyleResult<EntityId> {
367    const ENTITY: &str = "IfcSurfaceStyleRendering";
368    validate_ref(tx, model, schema, draft.surface_colour, "IfcColourRgb")?;
369    if let Some(value) = draft.transparency {
370        validate_ratio(ENTITY, "Transparency", value)?;
371    }
372    let mut values = vec![("SurfaceColour", Value::Ref(draft.surface_colour))];
373    if let Some(value) = draft.transparency {
374        values.push(("Transparency", Value::Real(value)));
375    }
376    for (attribute, member) in [
377        ("DiffuseColour", draft.diffuse),
378        ("TransmissionColour", draft.transmission),
379        ("DiffuseTransmissionColour", draft.diffuse_transmission),
380        ("ReflectionColour", draft.reflection),
381        ("SpecularColour", draft.specular),
382    ] {
383        let Some(member) = member else { continue };
384        let value = match member {
385            ColourOrFactor::Colour(id) => {
386                validate_ref(tx, model, schema, id, "IfcColourRgb")?;
387                Value::Ref(id)
388            }
389            ColourOrFactor::Factor(factor) => {
390                validate_ratio(ENTITY, attribute, factor)?;
391                Value::Real(factor)
392            }
393        };
394        values.push((attribute, value));
395    }
396    // A reflectance method the schema does not declare names a
397    // shading model the renderer cannot resolve.
398    let declared = schema
399        .attributes(ENTITY)
400        .iter()
401        .find(|a| a.name.eq_ignore_ascii_case("ReflectanceMethod"))
402        .and_then(|a| schema.type_def(&a.type_name))
403        .is_some_and(|def| match &def.kind {
404            ifc_schema::TypeKind::Enumeration(values) => values
405                .iter()
406                .any(|v| v.eq_ignore_ascii_case(draft.reflectance_method)),
407            _ => false,
408        });
409    if !declared {
410        return Err(invalid_authoring(
411            ENTITY,
412            "ReflectanceMethod",
413            draft.reflectance_method,
414        ));
415    }
416    values.push((
417        "ReflectanceMethod",
418        Value::Enum(draft.reflectance_method.into()),
419    ));
420    Ok(tx.create(build_named(schema, ENTITY, values)?))
421}