Skip to main content

brep_kernel/io/
appearance.rs

1//! Imported appearance — the display colour an exchange format carries, in the
2//! kernel's own terms.
3//!
4//! **Where colour LIVES.** The kernel has no colour field on `BrepSolid`, and it
5//! does not need one: colour is scene metadata, keyed by the face/solid NAME in
6//! `feature_pipeline::scene_metadata`. This module is only the CARRIER between
7//! "the importer read a colour" and "the pipeline stamped it on a name" — it is
8//! deliberately not a storage layer, and nothing here is serialized.
9//!
10//! **The record shape (THE convention, one spelling everywhere).** A colour is
11//! stamped as a single string attribute on the entity's own metadata record:
12//!
13//! ```text
14//! { "color": "#RRGGBB" }
15//! ```
16//!
17//! * key [`COLOR_METADATA_KEY`] — `color`, US spelling, matching
18//!   `BREP_render/src/color.rs` and the caller-side colour params.
19//! * value — uppercase `#RRGGBB` sRGB hex, the form the info/metadata panel shows
20//!   and a human can type back. [`ImportedColor::to_hex`] is the ONE producer.
21//!
22//! The record rides on the FINAL, stamped name, so it is captured by
23//! `io/snapshot.rs` into the native IMPORT3D payload and restored with the
24//! geometry — an imported colour survives save/reload and the parts library for
25//! free (`docs/developer/kernel-plans/step-assembly-import.md` §3.8).
26//!
27//! **Precedence.** A face colour is stamped on the FACE name; a body colour on
28//! the BODY name only. A body colour is never fanned out onto its faces: the
29//! solid's own info window already surfaces it, and materializing it per face
30//! would make the face records claim a styling the file never authored.
31//!
32//! **Units.** Exchange formats give components as 0..1 doubles with no gamma
33//! statement; OCC, FreeCAD and every viewer that reads them treat the value as
34//! sRGB and scale it straight to 8 bits, so `round(c * 255)` is the conversion —
35//! no linear/sRGB transfer applied.
36
37/// The scene-metadata key an imported colour is stamped under. ONE spelling.
38pub const COLOR_METADATA_KEY: &str = "color";
39
40/// An imported display colour: sRGB components in 0..=1, kept as read.
41///
42/// The float form is preserved (rather than collapsing to 8-bit at read time) so
43/// a later viewport lane can use the exact authored value; the metadata record
44/// carries the [`Self::to_hex`] rendering of it.
45#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
46pub struct ImportedColor {
47    pub r: f64,
48    pub g: f64,
49    pub b: f64,
50}
51
52impl ImportedColor {
53    /// Clamp to the 0..=1 the format promises. A non-finite component reads as
54    /// 0 rather than poisoning the hex conversion.
55    pub fn new(r: f64, g: f64, b: f64) -> Self {
56        Self {
57            r: clamp_unit(r),
58            g: clamp_unit(g),
59            b: clamp_unit(b),
60        }
61    }
62
63    /// Uppercase `#RRGGBB` — the ONE rendering that reaches a metadata record.
64    pub fn to_hex(&self) -> String {
65        format!(
66            "#{:02X}{:02X}{:02X}",
67            channel(self.r),
68            channel(self.g),
69            channel(self.b)
70        )
71    }
72}
73
74fn clamp_unit(value: f64) -> f64 {
75    if value.is_finite() {
76        value.clamp(0.0, 1.0)
77    } else {
78        0.0
79    }
80}
81
82fn channel(value: f64) -> u8 {
83    (clamp_unit(value) * 255.0).round() as u8
84}
85
86/// One imported body's appearance: an optional body-wide colour plus an optional
87/// colour per FACE.
88///
89/// `faces` is POSITIONAL and parallel to the body's faces in shell/face order —
90/// the same walk `feature_pipeline::features::import3d::stamp_imported_names`
91/// uses — so index `i` is the i-th face of `solid.shells.iter().flat_map(faces)`.
92/// It is either empty (nothing read) or exactly as long as that face count; a
93/// producer that cannot guarantee the pairing must leave it EMPTY rather than
94/// emit a shorter or speculative list.
95#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
96pub struct BodyAppearance {
97    /// The whole body's colour, when the file styled the solid itself.
98    pub body: Option<ImportedColor>,
99    /// Per-face colour in shell/face order, or empty when none was read.
100    pub faces: Vec<Option<ImportedColor>>,
101}
102
103impl BodyAppearance {
104    /// Nothing to stamp: no body colour and no face carries one.
105    pub fn is_empty(&self) -> bool {
106        self.body.is_none() && self.faces.iter().all(Option::is_none)
107    }
108}
109
110// BREP private tests: a1fa8d400a8e2883