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)]
20#[non_exhaustive]
21pub struct ColourRgbDraft<'a> {
22    /// The `Name` attribute, when supplied.
23    pub name: Option<&'a str>,
24    /// The `Red` channel; must be a finite value in `[0, 1]`.
25    pub red: f64,
26    /// The `Green` channel; must be a finite value in `[0, 1]`.
27    pub green: f64,
28    /// The `Blue` channel; must be a finite value in `[0, 1]`.
29    pub blue: f64,
30}
31
32impl<'a> ColourRgbDraft<'a> {
33    /// Starts a draft from its required `red`, `green`, `blue`; every other
34    /// field is unset.
35    #[must_use]
36    pub fn new(red: f64, green: f64, blue: f64) -> Self {
37        Self {
38            name: None,
39            red,
40            green,
41            blue,
42        }
43    }
44
45    /// Sets `name`.
46    ///
47    /// The `Name` attribute, when supplied.
48    #[must_use]
49    pub fn name(mut self, value: &'a str) -> Self {
50        self.name = Some(value);
51        self
52    }
53}
54
55/// Draft input for [`create_surface_style_shading`]: the writable attributes
56/// of a new `IfcSurfaceStyleShading`.
57#[derive(Debug, Clone, Copy)]
58#[non_exhaustive]
59pub struct SurfaceStyleShadingDraft {
60    /// The `SurfaceColour` reference to an `IfcColourRgb`.
61    pub surface_colour: EntityId,
62    /// The `Transparency` factor, when supplied; must be a finite value in
63    /// `[0, 1]`.
64    pub transparency: Option<f64>,
65}
66
67impl SurfaceStyleShadingDraft {
68    /// Starts a draft from its required `surface_colour`; every other field is
69    /// unset.
70    #[must_use]
71    pub fn new(surface_colour: EntityId) -> Self {
72        Self {
73            surface_colour,
74            transparency: None,
75        }
76    }
77
78    /// Sets `transparency`.
79    ///
80    /// The `Transparency` factor, when supplied; must be a finite value in
81    /// `[0, 1]`.
82    #[must_use]
83    pub fn transparency(mut self, value: f64) -> Self {
84        self.transparency = Some(value);
85        self
86    }
87}
88
89/// Draft input for [`create_surface_style`]: the writable attributes of a
90/// new `IfcSurfaceStyle`.
91#[derive(Debug, Clone)]
92#[non_exhaustive]
93pub struct SurfaceStyleDraft<'a> {
94    /// The `Name` attribute, when supplied.
95    pub name: Option<&'a str>,
96    /// The `Side` attribute.
97    pub side: crate::SurfaceSide,
98    /// The `Styles` elements: one to five references, no two from the same
99    /// surface-style element category (shading, lighting, refraction,
100    /// textures, externally defined).
101    pub elements: Vec<EntityId>,
102}
103
104impl<'a> SurfaceStyleDraft<'a> {
105    /// Starts a draft from its required `side`, `elements`; every other field
106    /// is unset.
107    #[must_use]
108    pub fn new(side: crate::SurfaceSide, elements: Vec<EntityId>) -> Self {
109        Self {
110            name: None,
111            side,
112            elements,
113        }
114    }
115
116    /// Sets `name`.
117    ///
118    /// The `Name` attribute, when supplied.
119    #[must_use]
120    pub fn name(mut self, value: &'a str) -> Self {
121        self.name = Some(value);
122        self
123    }
124}
125
126/// Draft input for [`create_styled_item`]: the writable attributes of a new
127/// `IfcStyledItem`.
128#[derive(Debug, Clone)]
129#[non_exhaustive]
130pub struct StyledItemDraft<'a> {
131    /// The `Item` reference to an `IfcRepresentationItem`, when supplied.
132    pub item: Option<EntityId>,
133    /// The `Styles` references; at least one is required. On IFC2x3 these
134    /// are wrapped in a staged `IfcPresentationStyleAssignment`.
135    pub styles: Vec<EntityId>,
136    /// The `Name` attribute, when supplied.
137    pub name: Option<&'a str>,
138}
139
140impl<'a> StyledItemDraft<'a> {
141    /// Starts a draft from its required `styles`; every other field is unset.
142    #[must_use]
143    pub fn new(styles: Vec<EntityId>) -> Self {
144        Self {
145            item: None,
146            styles,
147            name: None,
148        }
149    }
150
151    /// Sets `item`.
152    ///
153    /// The `Item` reference to an `IfcRepresentationItem`, when supplied.
154    #[must_use]
155    pub fn item(mut self, value: EntityId) -> Self {
156        self.item = Some(value);
157        self
158    }
159
160    /// Sets `name`.
161    ///
162    /// The `Name` attribute, when supplied.
163    #[must_use]
164    pub fn name(mut self, value: &'a str) -> Self {
165        self.name = Some(value);
166        self
167    }
168}
169
170/// Draft input for [`create_presentation_layer_with_style`]: the writable
171/// attributes of a new `IfcPresentationLayerWithStyle`.
172#[derive(Debug, Clone)]
173#[non_exhaustive]
174pub struct PresentationLayerDraft<'a> {
175    /// The `Name` attribute; must be non-empty.
176    pub name: &'a str,
177    /// The `Description` attribute, when supplied.
178    pub description: Option<&'a str>,
179    /// The `AssignedItems` references; at least one is required, and each
180    /// must resolve to an `IfcRepresentation` or `IfcRepresentationItem`.
181    pub assigned_items: Vec<EntityId>,
182    /// The `Identifier` attribute, when supplied.
183    pub identifier: Option<&'a str>,
184    /// The `LayerOn` attribute, when supplied.
185    pub layer_on: Option<bool>,
186    /// The `LayerFrozen` attribute, when supplied.
187    pub layer_frozen: Option<bool>,
188    /// The `LayerBlocked` attribute, when supplied.
189    pub layer_blocked: Option<bool>,
190    /// The `LayerStyles` references.
191    pub layer_styles: Vec<EntityId>,
192}
193
194impl<'a> PresentationLayerDraft<'a> {
195    /// Starts a draft from its required `name`, `assigned_items`; every other
196    /// field is unset.
197    #[must_use]
198    pub fn new(name: &'a str, assigned_items: Vec<EntityId>) -> Self {
199        Self {
200            name,
201            description: None,
202            assigned_items,
203            identifier: None,
204            layer_on: None,
205            layer_frozen: None,
206            layer_blocked: None,
207            layer_styles: Vec::new(),
208        }
209    }
210
211    /// Sets `description`.
212    ///
213    /// The `Description` attribute, when supplied.
214    #[must_use]
215    pub fn description(mut self, value: &'a str) -> Self {
216        self.description = Some(value);
217        self
218    }
219
220    /// Sets `identifier`.
221    ///
222    /// The `Identifier` attribute, when supplied.
223    #[must_use]
224    pub fn identifier(mut self, value: &'a str) -> Self {
225        self.identifier = Some(value);
226        self
227    }
228
229    /// Sets `layer_on`.
230    ///
231    /// The `LayerOn` attribute, when supplied.
232    #[must_use]
233    pub fn layer_on(mut self, value: bool) -> Self {
234        self.layer_on = Some(value);
235        self
236    }
237
238    /// Sets `layer_frozen`.
239    ///
240    /// The `LayerFrozen` attribute, when supplied.
241    #[must_use]
242    pub fn layer_frozen(mut self, value: bool) -> Self {
243        self.layer_frozen = Some(value);
244        self
245    }
246
247    /// Sets `layer_blocked`.
248    ///
249    /// The `LayerBlocked` attribute, when supplied.
250    #[must_use]
251    pub fn layer_blocked(mut self, value: bool) -> Self {
252        self.layer_blocked = Some(value);
253        self
254    }
255
256    /// Sets `layer_styles`.
257    ///
258    /// The `LayerStyles` references.
259    #[must_use]
260    pub fn layer_styles(mut self, value: Vec<EntityId>) -> Self {
261        self.layer_styles = value;
262        self
263    }
264}
265
266/// Stage a new `IfcColourRgb` in `tx`. Fails if any channel is not a finite
267/// value in `[0, 1]`.
268pub fn create_colour_rgb(
269    tx: &mut Transaction,
270    schema: &Schema,
271    draft: ColourRgbDraft<'_>,
272) -> StyleResult<EntityId> {
273    validate_ratio("IfcColourRgb", "Red", draft.red)?;
274    validate_ratio("IfcColourRgb", "Green", draft.green)?;
275    validate_ratio("IfcColourRgb", "Blue", draft.blue)?;
276    let mut values = Vec::new();
277    optional_text(&mut values, "Name", draft.name);
278    values.extend([
279        ("Red", Value::Real(draft.red)),
280        ("Green", Value::Real(draft.green)),
281        ("Blue", Value::Real(draft.blue)),
282    ]);
283    Ok(tx.create(build_named(schema, "IfcColourRgb", values)?))
284}
285
286/// Stage a new `IfcSurfaceStyleShading` in `tx`. Fails if `surface_colour`
287/// does not resolve to an `IfcColourRgb`, or `transparency` is not a finite
288/// value in `[0, 1]`.
289pub fn create_surface_style_shading(
290    tx: &mut Transaction,
291    model: &Model,
292    schema: &Schema,
293    draft: SurfaceStyleShadingDraft,
294) -> StyleResult<EntityId> {
295    validate_ref(tx, model, schema, draft.surface_colour, "IfcColourRgb")?;
296    if let Some(value) = draft.transparency {
297        validate_ratio("IfcSurfaceStyleShading", "Transparency", value)?;
298    }
299    let mut values = vec![("SurfaceColour", Value::Ref(draft.surface_colour))];
300    if let Some(value) = draft.transparency {
301        values.push(("Transparency", Value::Real(value)));
302    }
303    Ok(tx.create(build_named(schema, "IfcSurfaceStyleShading", values)?))
304}
305
306/// Stage a new `IfcSurfaceStyle` in `tx`. Fails if `elements` is empty,
307/// exceeds five members, contains two members from the same surface-style
308/// element category, or any member does not resolve to its expected type.
309pub fn create_surface_style(
310    tx: &mut Transaction,
311    model: &Model,
312    schema: &Schema,
313    draft: SurfaceStyleDraft<'_>,
314) -> StyleResult<EntityId> {
315    if draft.elements.is_empty() || draft.elements.len() > 5 {
316        return Err(invalid_authoring(
317            "IfcSurfaceStyle",
318            "Styles",
319            format!("expected 1..=5 elements, found {}", draft.elements.len()),
320        ));
321    }
322    let mut element_types = Vec::with_capacity(draft.elements.len());
323    for element in &draft.elements {
324        element_types.push(validate_surface_element(tx, model, schema, *element)?);
325    }
326    if let Some(category) =
327        duplicate_surface_element_category(schema, element_types.iter().map(String::as_str))
328    {
329        return Err(invalid_authoring(
330            "IfcSurfaceStyle",
331            "Styles",
332            format!("duplicate {category} category"),
333        ));
334    }
335    let mut values = Vec::new();
336    optional_text(&mut values, "Name", draft.name);
337    values.push(("Side", enumeration(draft.side.as_ifc())));
338    values.push((
339        "Styles",
340        Value::List(draft.elements.into_iter().map(Value::Ref).collect()),
341    ));
342    Ok(tx.create(build_named(schema, "IfcSurfaceStyle", values)?))
343}
344
345/// Stage a new `IfcStyledItem` in `tx`. Fails if `styles` is empty, `item`
346/// does not resolve to an `IfcRepresentationItem`, or a style does not
347/// resolve to its expected type for the target schema version.
348pub fn create_styled_item(
349    tx: &mut Transaction,
350    model: &Model,
351    schema: &Schema,
352    draft: StyledItemDraft<'_>,
353) -> StyleResult<EntityId> {
354    validate_optional_ref(tx, model, schema, draft.item, "IfcRepresentationItem")?;
355    if draft.styles.is_empty() {
356        return Err(invalid_authoring(
357            "IfcStyledItem",
358            "Styles",
359            "at least one style is required",
360        ));
361    }
362    for style in &draft.styles {
363        validate_ref(tx, model, schema, *style, "IfcPresentationStyle")?;
364    }
365
366    let mut staged = tx.clone();
367    let style_values = if schema.version() == Some(ifc_schema::SchemaVersion::Ifc2x3) {
368        let wrapper = build_named(
369            schema,
370            "IfcPresentationStyleAssignment",
371            vec![(
372                "Styles",
373                Value::List(draft.styles.into_iter().map(Value::Ref).collect()),
374            )],
375        )?;
376        vec![Value::Ref(staged.create(wrapper))]
377    } else {
378        draft.styles.into_iter().map(Value::Ref).collect()
379    };
380    let mut values = vec![("Styles", Value::List(style_values))];
381    optional_reference(&mut values, "Item", draft.item);
382    optional_text(&mut values, "Name", draft.name);
383    let id = staged.create(build_named(schema, "IfcStyledItem", values)?);
384    *tx = staged;
385    Ok(id)
386}
387
388/// Stage a new `IfcPresentationLayerWithStyle` in `tx`. Fails if `name` is
389/// empty, `assigned_items` is empty, or any assigned item or layer style
390/// does not resolve to its expected type.
391pub fn create_presentation_layer_with_style(
392    tx: &mut Transaction,
393    model: &Model,
394    schema: &Schema,
395    draft: PresentationLayerDraft<'_>,
396) -> StyleResult<EntityId> {
397    if draft.name.is_empty() {
398        return Err(invalid_authoring(
399            "IfcPresentationLayerWithStyle",
400            "Name",
401            "must not be empty",
402        ));
403    }
404    if draft.assigned_items.is_empty() {
405        return Err(invalid_authoring(
406            "IfcPresentationLayerWithStyle",
407            "AssignedItems",
408            "at least one item is required",
409        ));
410    }
411    for item in &draft.assigned_items {
412        validate_layered_item(tx, model, schema, *item)?;
413    }
414    for style in &draft.layer_styles {
415        validate_ref(tx, model, schema, *style, "IfcPresentationStyle")?;
416    }
417    let mut values = vec![
418        ("Name", text(draft.name)),
419        (
420            "AssignedItems",
421            Value::List(draft.assigned_items.into_iter().map(Value::Ref).collect()),
422        ),
423        (
424            "LayerStyles",
425            Value::List(draft.layer_styles.into_iter().map(Value::Ref).collect()),
426        ),
427    ];
428    optional_text(&mut values, "Description", draft.description);
429    optional_text(&mut values, "Identifier", draft.identifier);
430    if let Some(value) = draft.layer_on {
431        values.push(("LayerOn", Value::Bool(value)));
432    }
433    if let Some(value) = draft.layer_frozen {
434        values.push(("LayerFrozen", Value::Bool(value)));
435    }
436    if let Some(value) = draft.layer_blocked {
437        values.push(("LayerBlocked", Value::Bool(value)));
438    }
439    Ok(tx.create(build_named(
440        schema,
441        "IfcPresentationLayerWithStyle",
442        values,
443    )?))
444}
445
446fn staged_type(tx: &Transaction, model: &Model, target: EntityId) -> Option<String> {
447    tx.edits()
448        .iter()
449        .rev()
450        .find_map(|edit| match edit {
451            Edit::Create { id, entity } if *id == target => Some(entity.type_name.to_string()),
452            _ => None,
453        })
454        .or_else(|| model.get(target).map(|entity| entity.type_name.to_string()))
455}
456
457fn validate_surface_element(
458    tx: &Transaction,
459    model: &Model,
460    schema: &Schema,
461    target: EntityId,
462) -> StyleResult<String> {
463    let actual = staged_type(tx, model, target).ok_or(StyleError::DanglingReference {
464        source_id: EntityId(0),
465        target,
466    })?;
467    if SURFACE_STYLE_ELEMENT_MEMBERS
468        .iter()
469        .any(|member| schema.is_a(&actual, member))
470    {
471        Ok(actual)
472    } else {
473        Err(StyleError::ReferenceType {
474            target,
475            expected: "IfcSurfaceStyleElementSelect",
476            actual,
477        })
478    }
479}
480
481fn validate_layered_item(
482    tx: &Transaction,
483    model: &Model,
484    schema: &Schema,
485    target: EntityId,
486) -> StyleResult<()> {
487    let actual = staged_type(tx, model, target).ok_or(StyleError::DanglingReference {
488        source_id: EntityId(0),
489        target,
490    })?;
491    if schema.is_a(&actual, "IfcRepresentationItem") || schema.is_a(&actual, "IfcRepresentation") {
492        Ok(())
493    } else {
494        Err(StyleError::ReferenceType {
495            target,
496            expected: "IfcLayeredItem",
497            actual,
498        })
499    }
500}
501
502/// Authored fields for an `IfcSurfaceStyleRendering`.
503///
504/// The rendering form extends the shading form with the parameters a
505/// renderer needs. Every colour slot is an `IfcColourOrFactor`, so a
506/// caller states either an explicit colour or a factor of the surface
507/// colour -- the two are not interchangeable and the schema keeps both.
508#[derive(Debug, Clone, Copy)]
509#[non_exhaustive]
510pub struct SurfaceStyleRenderingDraft {
511    /// `SurfaceColour`, an `IfcColourRgb`.
512    pub surface_colour: EntityId,
513    /// `Transparency`, a normalised ratio.
514    pub transparency: Option<f64>,
515    /// `DiffuseColour`.
516    pub diffuse: Option<ColourOrFactor>,
517    /// `TransmissionColour`.
518    pub transmission: Option<ColourOrFactor>,
519    /// `DiffuseTransmissionColour`.
520    pub diffuse_transmission: Option<ColourOrFactor>,
521    /// `ReflectionColour`.
522    pub reflection: Option<ColourOrFactor>,
523    /// `SpecularColour`.
524    pub specular: Option<ColourOrFactor>,
525    /// `ReflectanceMethod`, an `IfcReflectanceMethodEnum` token.
526    pub reflectance_method: &'static str,
527}
528
529impl SurfaceStyleRenderingDraft {
530    /// Starts a draft from its required `surface_colour`, `reflectance_method`;
531    /// every other field is unset.
532    #[must_use]
533    pub fn new(surface_colour: EntityId, reflectance_method: &'static str) -> Self {
534        Self {
535            surface_colour,
536            transparency: None,
537            diffuse: None,
538            transmission: None,
539            diffuse_transmission: None,
540            reflection: None,
541            specular: None,
542            reflectance_method,
543        }
544    }
545
546    /// Sets `transparency`.
547    ///
548    /// `Transparency`, a normalised ratio.
549    #[must_use]
550    pub fn transparency(mut self, value: f64) -> Self {
551        self.transparency = Some(value);
552        self
553    }
554
555    /// Sets `diffuse`.
556    ///
557    /// `DiffuseColour`.
558    #[must_use]
559    pub fn diffuse(mut self, value: ColourOrFactor) -> Self {
560        self.diffuse = Some(value);
561        self
562    }
563
564    /// Sets `transmission`.
565    ///
566    /// `TransmissionColour`.
567    #[must_use]
568    pub fn transmission(mut self, value: ColourOrFactor) -> Self {
569        self.transmission = Some(value);
570        self
571    }
572
573    /// Sets `diffuse_transmission`.
574    ///
575    /// `DiffuseTransmissionColour`.
576    #[must_use]
577    pub fn diffuse_transmission(mut self, value: ColourOrFactor) -> Self {
578        self.diffuse_transmission = Some(value);
579        self
580    }
581
582    /// Sets `reflection`.
583    ///
584    /// `ReflectionColour`.
585    #[must_use]
586    pub fn reflection(mut self, value: ColourOrFactor) -> Self {
587        self.reflection = Some(value);
588        self
589    }
590
591    /// Sets `specular`.
592    ///
593    /// `SpecularColour`.
594    #[must_use]
595    pub fn specular(mut self, value: ColourOrFactor) -> Self {
596        self.specular = Some(value);
597        self
598    }
599}
600
601/// Stage an `IfcSurfaceStyleRendering`.
602///
603/// # Errors
604///
605/// Refuses a `surface_colour` or colour member that is not an
606/// `IfcColourRgb`, a factor or transparency outside `[0, 1]`, and a
607/// `reflectance_method` the schema does not declare.
608pub fn create_surface_style_rendering(
609    tx: &mut Transaction,
610    model: &Model,
611    schema: &Schema,
612    draft: SurfaceStyleRenderingDraft,
613) -> StyleResult<EntityId> {
614    const ENTITY: &str = "IfcSurfaceStyleRendering";
615    validate_ref(tx, model, schema, draft.surface_colour, "IfcColourRgb")?;
616    if let Some(value) = draft.transparency {
617        validate_ratio(ENTITY, "Transparency", value)?;
618    }
619    let mut values = vec![("SurfaceColour", Value::Ref(draft.surface_colour))];
620    if let Some(value) = draft.transparency {
621        values.push(("Transparency", Value::Real(value)));
622    }
623    for (attribute, member) in [
624        ("DiffuseColour", draft.diffuse),
625        ("TransmissionColour", draft.transmission),
626        ("DiffuseTransmissionColour", draft.diffuse_transmission),
627        ("ReflectionColour", draft.reflection),
628        ("SpecularColour", draft.specular),
629    ] {
630        let Some(member) = member else { continue };
631        let value = match member {
632            ColourOrFactor::Colour(id) => {
633                validate_ref(tx, model, schema, id, "IfcColourRgb")?;
634                Value::Ref(id)
635            }
636            ColourOrFactor::Factor(factor) => {
637                validate_ratio(ENTITY, attribute, factor)?;
638                Value::Real(factor)
639            }
640        };
641        values.push((attribute, value));
642    }
643    // A reflectance method the schema does not declare names a
644    // shading model the renderer cannot resolve.
645    let declared = schema
646        .attributes(ENTITY)
647        .iter()
648        .find(|a| a.name.eq_ignore_ascii_case("ReflectanceMethod"))
649        .and_then(|a| schema.type_def(&a.type_name))
650        .is_some_and(|def| match &def.kind {
651            ifc_schema::TypeKind::Enumeration(values) => values
652                .iter()
653                .any(|v| v.eq_ignore_ascii_case(draft.reflectance_method)),
654            _ => false,
655        });
656    if !declared {
657        return Err(invalid_authoring(
658            ENTITY,
659            "ReflectanceMethod",
660            draft.reflectance_method,
661        ));
662    }
663    values.push((
664        "ReflectanceMethod",
665        Value::Enum(draft.reflectance_method.into()),
666    ));
667    Ok(tx.create(build_named(schema, ENTITY, values)?))
668}