Skip to main content

ifc_style/
assignment.rs

1//! Style assignment, item binding, and deterministic cascade resolution.
2//!
3//! IFC2x3 `IfcPresentationStyleAssignment` wrappers are projected explicitly
4//! and flattened only by the resolver. IFC4/IFC4X3 direct style selections keep
5//! their original IDs.
6//!
7//! A unique direct item assignment wins over layer styles. Multiple direct
8//! assignments are an ambiguity error; layer candidates remain inspectable.
9
10use ifc_model::{EntityId, Value};
11
12use crate::error::{StyleError, StyleResult};
13use crate::view::Record;
14
15mod layer;
16mod resolution;
17mod styled_item;
18
19pub use layer::PresentationStyleAssignment;
20pub use resolution::{ResolvedStyle, StyleSource};
21pub use styled_item::StyledItem;
22
23/// One member of IFC2x3/IFC4's legacy presentation-style select.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum PresentationStyleMember {
26    /// A reference to an `IfcPresentationStyle`.
27    Style(EntityId),
28    /// The `IfcNullStyle.NULL` sentinel: this slot deliberately carries no style.
29    Null,
30}
31
32pub(crate) fn presentation_style_members(
33    record: Record<'_, '_>,
34    attribute: &'static str,
35    minimum: usize,
36) -> StyleResult<Vec<PresentationStyleMember>> {
37    let value = record.value(attribute)?;
38    let Value::List(items) = value.unwrap_typed() else {
39        return Err(record.invalid(attribute, value));
40    };
41    if items.len() < minimum {
42        return Err(StyleError::InvalidValue {
43            entity: record.entity.type_name.to_string(),
44            id: record.id,
45            attribute,
46            value: format!("{} member(s), expected at least {minimum}", items.len()),
47        });
48    }
49
50    items
51        .iter()
52        .map(|item| match item {
53            Value::Ref(target) => {
54                record.check_reference(*target, "IfcPresentationStyle")?;
55                Ok(PresentationStyleMember::Style(*target))
56            }
57            Value::Typed { type_name, value }
58                if type_name.eq_ignore_ascii_case("IFCNULLSTYLE")
59                    && matches!(value.as_ref(), Value::Enum(member) if member.eq_ignore_ascii_case("NULL")) =>
60            {
61                Ok(PresentationStyleMember::Null)
62            }
63            _ => Err(record.invalid(attribute, item)),
64        })
65        .collect()
66}