Skip to main content

roxlap_formats/
color.rs

1//! QE-B6 — the packed-colour newtype family.
2//!
3//! roxlap inherited **three distinct `u32` colour packings** from
4//! voxlap, and every API taking a bare `u32` invited mixing them up
5//! (the classic: passing `0xFF...` "opaque white" where the high byte
6//! is *brightness*, getting an over-bright voxel). The newtypes make
7//! each packing a distinct type, so a mix-up is a compile error:
8//!
9//! | Type | Packing | High byte means | Used by |
10//! |---|---|---|---|
11//! | [`VoxColor`] | `0xBB_RR_GG_BB` | baked brightness/AO (`0x80` neutral) | voxels: edits, kv6/vxl builders, colour queries |
12//! | [`Rgb`] | `0x00_RR_GG_BB` | nothing (must be 0) | tints, sky/fog/clear colours, colour→material map keys |
13//! | [`OverlayColor`] | `0xAA_RR_GG_BB` | real alpha | overlay lines (`Line3`) |
14//!
15//! All three are `#[repr(transparent)]` wrappers with a **public**
16//! `.0` field: on-disk formats and GPU buffers keep raw `u32`s, and
17//! wrapping/unwrapping at the boundary is free and explicit.
18
19/// A voxel colour in voxlap's packing: RGB in the low 24 bits, the
20/// baked **brightness/AO byte** on top — `0x80` is neutral ("unlit"),
21/// and lighting bakes rewrite it per voxel. NOT alpha: translucency
22/// is a material property (see the material palette), never a colour
23/// channel.
24#[repr(transparent)]
25#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
26pub struct VoxColor(pub u32);
27
28impl VoxColor {
29    /// A voxel colour at the neutral `0x80` brightness.
30    #[must_use]
31    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
32        Self(0x8000_0000 | ((r as u32) << 16) | ((g as u32) << 8) | b as u32)
33    }
34
35    /// Replace the brightness byte (bakes do this per voxel; hand-set
36    /// it only for pre-shaded art).
37    #[must_use]
38    pub const fn with_brightness(self, brightness: u8) -> Self {
39        Self((self.0 & 0x00ff_ffff) | ((brightness as u32) << 24))
40    }
41
42    /// The colour part as an [`Rgb`] (brightness stripped) — e.g. to
43    /// tint debris particles with the voxel colour they were carved
44    /// from.
45    #[must_use]
46    pub const fn rgb_part(self) -> Rgb {
47        Rgb(self.0 & 0x00ff_ffff)
48    }
49}
50
51/// A plain `0x00_RR_GG_BB` colour: sprite/actor/particle **tints**,
52/// the sky / fog / clear colours, and the colour keys of
53/// colour→material maps. No packing surprises — the high byte is
54/// unused and must stay zero.
55#[repr(transparent)]
56#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
57pub struct Rgb(pub u32);
58
59impl Rgb {
60    /// The no-op tint.
61    pub const WHITE: Self = Self(0x00ff_ffff);
62
63    /// Pack `r`/`g`/`b` into the low 24 bits; the high byte stays zero.
64    #[must_use]
65    pub const fn new(r: u8, g: u8, b: u8) -> Self {
66        Self(((r as u32) << 16) | ((g as u32) << 8) | b as u32)
67    }
68}
69
70/// An overlay colour with **real alpha**: `0xAA_RR_GG_BB` (`0xFF` =
71/// opaque). Used by the overlay-line API ([`Line3`]) — the one place
72/// in the engine where the high byte genuinely is opacity.
73///
74/// [`Line3`]: https://docs.rs/roxlap-render
75#[repr(transparent)]
76#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
77pub struct OverlayColor(pub u32);
78
79impl OverlayColor {
80    /// Pack an `0xAA_RR_GG_BB` word; `a` is real opacity (`0xff` =
81    /// opaque, `0` = invisible).
82    #[must_use]
83    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
84        Self(((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32)
85    }
86
87    /// Fully opaque.
88    #[must_use]
89    pub const fn opaque(r: u8, g: u8, b: u8) -> Self {
90        Self::rgba(r, g, b, 0xff)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn packings_match_their_wire_layouts() {
100        assert_eq!(VoxColor::rgb(0x4d, 0x8a, 0x3a).0, 0x804d_8a3a);
101        assert_eq!(
102            VoxColor::rgb(0x4d, 0x8a, 0x3a).with_brightness(0xff).0,
103            0xff4d_8a3a
104        );
105        assert_eq!(VoxColor(0x804d_8a3a).rgb_part(), Rgb(0x004d_8a3a));
106        assert_eq!(Rgb::new(0x8f, 0xbc, 0xd4).0, 0x008f_bcd4);
107        assert_eq!(OverlayColor::rgba(0xff, 0xd0, 0x40, 0xff).0, 0xffff_d040);
108        assert_eq!(
109            OverlayColor::opaque(1, 2, 3),
110            OverlayColor::rgba(1, 2, 3, 0xff)
111        );
112    }
113}