Skip to main content

brep_kernel/io/step/
styles.rs

1//! Geometry COLOUR on export — the ISO 10303-46 presentation entities that give
2//! an exported body or face the colour the document persisted for it.
3//!
4//! This is the WRITE half of `io/step_import/styles.rs`, and it writes exactly
5//! the chain that module reads:
6//!
7//! ```text
8//! MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',(styled …),ctx)
9//!   └ STYLED_ITEM('color',(#style),#item)            item = solid or face
10//!       └ PRESENTATION_STYLE_ASSIGNMENT((#usage))
11//!           └ SURFACE_STYLE_USAGE(.BOTH.,#side)
12//!               └ SURFACE_SIDE_STYLE('',(#fill))
13//!                   └ SURFACE_STYLE_FILL_AREA(#area)
14//!                       └ FILL_AREA_STYLE('',(#colour))
15//!                           └ FILL_AREA_STYLE_COLOUR('',#rgb)
16//!                               └ COLOUR_RGB('',r,g,b)
17//! ```
18//!
19//! `SURFACE_STYLE_FILL_AREA` (the SHADED colour) is written rather than
20//! `SURFACE_STYLE_RENDERING`: it is the form OpenCASCADE's XCAF reader takes
21//! first, the form the corpus fixtures overwhelmingly carry, and the only one
22//! the importer's fill-area lane needs. `.BOTH.` because the kernel shades one
23//! face with one colour and has no side-specific notion to express.
24//!
25//! **The style ENTITIES are shared with the PMI writer** — `write_colour_rgb`,
26//! [`write_style_assignment`] and [`write_styled_item`] here are the single
27//! producers, and `step/pmi.rs` writes its null style, its annotation colour and
28//! its mapped-shape styled item through them. Colour and annotation
29//! presentation stay independent otherwise: a file may carry either, both or
30//! neither, and nothing here reads the PMI block.
31//!
32//! # Where the colour comes from
33//!
34//! [`StepColors`] is the caller's map from SCENE NAME to an 8-bit sRGB triple —
35//! a body name (`Extrude1`, `ACOMP3:Extrude1`) or a face name
36//! (`Extrude1_PX`). The caller owns the policy: the app fills it from the
37//! document's persisted `color` metadata attribute, which is the same attribute
38//! a STEP import stamps and the Info window edits, and adds a name-derived
39//! colour only for a body the viewport is itself colouring by name hash. This
40//! module never invents a colour: a name with no entry gets no style, and an
41//! export with an empty map is byte-for-byte the export that ran before colours
42//! existed.
43//!
44//! The triple is 8-bit because `#RRGGBB` IS the persisted form
45//! (`io/appearance.rs`), so the round trip is exact: `n/255` prints through
46//! [`real`] and reads back through `round(c * 255) = n`.
47
48use std::collections::BTreeMap;
49
50use super::{id_list, real, ProductGeometry, StepWriter};
51
52/// Display colours to write as presentation styles, keyed by SCENE NAME.
53///
54/// Deterministic iteration (a `BTreeMap`) so a file's styled items come out in
55/// the same order every run; lookups are by exact name, with the occurrence
56/// prefixes a structured export resolves through supplied per product.
57#[derive(Clone, Debug, Default)]
58pub struct StepColors {
59    by_name: BTreeMap<String, [u8; 3]>,
60}
61
62impl StepColors {
63    pub fn new() -> Self {
64        Self::default()
65    }
66
67    /// Record one entity's colour. An empty name is ignored (no phantom entry).
68    pub fn set(&mut self, name: &str, rgb: [u8; 3]) {
69        if name.is_empty() {
70            return;
71        }
72        self.by_name.insert(name.to_string(), rgb);
73    }
74
75    /// Nothing to style — the whole writer short-circuits on it.
76    pub fn is_empty(&self) -> bool {
77        self.by_name.is_empty()
78    }
79
80    /// How many names carry a colour.
81    pub fn len(&self) -> usize {
82        self.by_name.len()
83    }
84
85    /// The colour for `name` under the first of `prefixes` that has one.
86    ///
87    /// `prefixes` is the occurrence namespaces a product's geometry is known by
88    /// in the document (`""` for the root, `"ACOMP3:"`, `"ACOMP5:ACOMP1:"` …).
89    /// A part placed several times has ONE written geometry, so it can carry
90    /// only one colour per entity: the first prefix in the caller's order wins,
91    /// the same "a shared part resolves to the one face" rule the PMI writer
92    /// documents for annotations.
93    fn resolve(&self, prefixes: &[String], name: &str) -> Option<[u8; 3]> {
94        prefixes.iter().find_map(|prefix| {
95            if prefix.is_empty() {
96                self.by_name.get(name).copied()
97            } else {
98                self.by_name.get(&format!("{prefix}{name}")).copied()
99            }
100        })
101    }
102}
103
104/// `COLOUR_RGB('',r,g,b)` — the ONE producer, shared with the PMI writer.
105/// Components are 0..=1 sRGB, which is what every consumer of an exchange
106/// colour treats them as (`io/appearance.rs` "Units").
107pub(crate) fn write_colour_rgb(
108    writer: &mut StepWriter,
109    rgb: [f64; 3],
110) -> Result<usize, String> {
111    Ok(writer.add(format!(
112        "COLOUR_RGB('',{},{},{})",
113        real(rgb[0])?,
114        real(rgb[1])?,
115        real(rgb[2])?
116    )))
117}
118
119/// `PRESENTATION_STYLE_ASSIGNMENT((…))` over `elements`, already rendered as a
120/// Part 21 list body (`#12` references, or an inline `NULL_STYLE(.NULL.)`).
121pub(crate) fn write_style_assignment(writer: &mut StepWriter, elements: &str) -> usize {
122    writer.add(format!("PRESENTATION_STYLE_ASSIGNMENT(({elements}))"))
123}
124
125/// `STYLED_ITEM(name,(#style),#item)` — one style assignment on one item.
126pub(crate) fn write_styled_item(
127    writer: &mut StepWriter,
128    name: &str,
129    style: usize,
130    item: usize,
131) -> usize {
132    writer.add(format!("STYLED_ITEM('{name}',(#{style}),#{item})"))
133}
134
135/// The styled items one product's geometry contributes: its coloured bodies
136/// first (in body order), then its coloured faces (in face order).
137///
138/// Bodies before faces is deliberate — it is the precedence a reader that keeps
139/// only one style per shell sees, and it matches the importer's own reading
140/// (a body colour is the body's, a face colour overrides it for that face).
141pub(crate) fn styled_items(
142    geometry: &ProductGeometry,
143    colors: &StepColors,
144    prefixes: &[String],
145) -> Vec<(usize, [u8; 3])> {
146    let mut items = Vec::new();
147    for (name, id) in &geometry.solids {
148        if let Some(rgb) = colors.resolve(prefixes, name) {
149            items.push((*id, rgb));
150        }
151    }
152    for (name, id) in &geometry.faces {
153        if let Some(rgb) = colors.resolve(prefixes, name) {
154            items.push((*id, rgb));
155        }
156    }
157    items
158}
159
160/// Write the file's geometry styles: one style chain per DISTINCT colour, one
161/// `STYLED_ITEM` per entity, and one
162/// `MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION` gathering them.
163///
164/// Returns how many styled items were written — `0` for an empty `items`, in
165/// which case NOTHING is emitted (not an empty presentation representation,
166/// which several readers reject and which would change every uncoloured file).
167pub(crate) fn write_geometry_styles(
168    writer: &mut StepWriter,
169    geometry_context: usize,
170    items: &[(usize, [u8; 3])],
171) -> Result<usize, String> {
172    if items.is_empty() {
173        return Ok(0);
174    }
175    // One chain per distinct colour, minted on first use so entity order
176    // follows `items` order. Keyed on the 8-bit triple, which IS the colour's
177    // persisted identity.
178    let mut assignments: BTreeMap<[u8; 3], usize> = BTreeMap::new();
179    let mut styled = Vec::with_capacity(items.len());
180    for (item, rgb) in items {
181        let assignment = match assignments.get(rgb) {
182            Some(id) => *id,
183            None => {
184                let id = write_surface_style(writer, *rgb)?;
185                assignments.insert(*rgb, id);
186                id
187            }
188        };
189        styled.push(write_styled_item(writer, "color", assignment, *item));
190    }
191    writer.add(format!(
192        "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',{},#{geometry_context})",
193        id_list(&styled)
194    ));
195    Ok(styled.len())
196}
197
198/// One colour's whole chain, `COLOUR_RGB` up to the
199/// `PRESENTATION_STYLE_ASSIGNMENT` a `STYLED_ITEM` names.
200fn write_surface_style(writer: &mut StepWriter, rgb: [u8; 3]) -> Result<usize, String> {
201    let colour = write_colour_rgb(
202        writer,
203        [
204            rgb[0] as f64 / 255.0,
205            rgb[1] as f64 / 255.0,
206            rgb[2] as f64 / 255.0,
207        ],
208    )?;
209    let fill_colour = writer.add(format!("FILL_AREA_STYLE_COLOUR('',#{colour})"));
210    let fill_area = writer.add(format!("FILL_AREA_STYLE('',(#{fill_colour}))"));
211    let fill_style = writer.add(format!("SURFACE_STYLE_FILL_AREA(#{fill_area})"));
212    let side_style = writer.add(format!("SURFACE_SIDE_STYLE('',(#{fill_style}))"));
213    let usage = writer.add(format!("SURFACE_STYLE_USAGE(.BOTH.,#{side_style})"));
214    Ok(write_style_assignment(writer, &format!("#{usage}")))
215}
216