pub const COLOR_METADATA_KEY: &str = "color";
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ImportedColor {
pub r: f64,
pub g: f64,
pub b: f64,
}
impl ImportedColor {
pub fn new(r: f64, g: f64, b: f64) -> Self {
Self {
r: clamp_unit(r),
g: clamp_unit(g),
b: clamp_unit(b),
}
}
pub fn to_hex(&self) -> String {
format!(
"#{:02X}{:02X}{:02X}",
channel(self.r),
channel(self.g),
channel(self.b)
)
}
}
fn clamp_unit(value: f64) -> f64 {
if value.is_finite() {
value.clamp(0.0, 1.0)
} else {
0.0
}
}
fn channel(value: f64) -> u8 {
(clamp_unit(value) * 255.0).round() as u8
}
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct BodyAppearance {
pub body: Option<ImportedColor>,
pub faces: Vec<Option<ImportedColor>>,
}
impl BodyAppearance {
pub fn is_empty(&self) -> bool {
self.body.is_none() && self.faces.iter().all(Option::is_none)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_is_round_to_nearest_8_bit_srgb() {
assert_eq!(
ImportedColor::new(0.800000010877, 0.800000010877, 0.800000010877).to_hex(),
"#CCCCCC"
);
assert_eq!(
ImportedColor::new(0.541176494856, 0.890196087049, 0.631372563332).to_hex(),
"#8AE3A1"
);
assert_eq!(ImportedColor::new(1.0, 0.0, 0.0).to_hex(), "#FF0000");
assert_eq!(ImportedColor::new(0.0, 0.0, 0.0).to_hex(), "#000000");
}
#[test]
fn out_of_range_and_non_finite_components_clamp() {
assert_eq!(ImportedColor::new(2.0, -1.0, f64::NAN).to_hex(), "#FF0000");
}
#[test]
fn empty_appearance_is_reported_empty() {
assert!(BodyAppearance::default().is_empty());
assert!(BodyAppearance {
body: None,
faces: vec![None, None],
}
.is_empty());
assert!(!BodyAppearance {
body: None,
faces: vec![None, Some(ImportedColor::new(1.0, 0.0, 0.0))],
}
.is_empty());
}
}