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