use super::modifier::ColorModifier;
use crate::semantic_overlay::MunsellSpec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum BaseColorSet {
Standard,
#[default]
Extended,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum OverlayMode {
Ignore,
#[default]
Include,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FormatOptions {
pub base_colors: BaseColorSet,
pub overlay_mode: OverlayMode,
}
impl FormatOptions {
pub fn new(base_colors: BaseColorSet, overlay_mode: OverlayMode) -> Self {
Self {
base_colors,
overlay_mode,
}
}
pub fn standard() -> Self {
Self {
base_colors: BaseColorSet::Standard,
overlay_mode: OverlayMode::Ignore,
}
}
pub fn extended() -> Self {
Self {
base_colors: BaseColorSet::Extended,
overlay_mode: OverlayMode::Ignore,
}
}
pub fn standard_with_overlays() -> Self {
Self {
base_colors: BaseColorSet::Standard,
overlay_mode: OverlayMode::Include,
}
}
pub fn extended_with_overlays() -> Self {
Self {
base_colors: BaseColorSet::Extended,
overlay_mode: OverlayMode::Include,
}
}
}
impl Default for FormatOptions {
fn default() -> Self {
Self {
base_colors: BaseColorSet::Extended,
overlay_mode: OverlayMode::Include,
}
}
}
#[derive(Debug, Clone)]
pub struct ColorCharacterization {
pub munsell: MunsellSpec,
pub iscc_nbs_number: u16,
pub iscc_base_color: String,
pub iscc_extended_name: String,
pub modifier: ColorModifier,
pub semantic_matches: Vec<String>,
pub nearest_semantic: Option<(String, f64)>,
pub shade: String,
}
impl ColorCharacterization {
pub fn describe(&self, options: &FormatOptions) -> String {
let color_name = match options.overlay_mode {
OverlayMode::Ignore => match options.base_colors {
BaseColorSet::Standard => &self.iscc_base_color,
BaseColorSet::Extended => &self.iscc_extended_name,
},
OverlayMode::Include => {
self.semantic_matches
.first()
.map(|s| s.as_str())
.or_else(|| self.nearest_semantic.as_ref().map(|(name, _)| name.as_str()))
.unwrap_or_else(|| match options.base_colors {
BaseColorSet::Standard => &self.iscc_base_color,
BaseColorSet::Extended => &self.iscc_extended_name,
})
}
};
self.modifier.format(color_name)
}
pub fn base_color(&self, options: &FormatOptions) -> &str {
match options.overlay_mode {
OverlayMode::Ignore => match options.base_colors {
BaseColorSet::Standard => &self.iscc_base_color,
BaseColorSet::Extended => &self.iscc_extended_name,
},
OverlayMode::Include => self
.semantic_matches
.first()
.map(|s| s.as_str())
.or_else(|| self.nearest_semantic.as_ref().map(|(name, _)| name.as_str()))
.unwrap_or_else(|| match options.base_colors {
BaseColorSet::Standard => &self.iscc_base_color,
BaseColorSet::Extended => &self.iscc_extended_name,
}),
}
}
pub fn has_semantic_match(&self) -> bool {
!self.semantic_matches.is_empty()
}
pub fn semantic_match_count(&self) -> usize {
self.semantic_matches.len()
}
pub fn to_string_default(&self) -> String {
self.describe(&FormatOptions::default())
}
}
impl std::fmt::Display for ColorCharacterization {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_string_default())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_characterization(
base: &str,
extended: &str,
modifier: ColorModifier,
semantic_matches: Vec<&str>,
nearest: Option<(&str, f64)>,
) -> ColorCharacterization {
ColorCharacterization {
munsell: MunsellSpec::new(28.0, 3.0, 8.0), iscc_nbs_number: 182,
iscc_base_color: base.to_string(),
iscc_extended_name: extended.to_string(),
modifier,
semantic_matches: semantic_matches.into_iter().map(String::from).collect(),
nearest_semantic: nearest.map(|(n, d)| (n.to_string(), d)),
shade: base.to_string(), }
}
#[test]
fn test_standard_ignore_overlay() {
let char = make_test_characterization(
"blue",
"sapphire",
ColorModifier::Dark,
vec!["navy"],
Some(("navy", 1.5)),
);
let opts = FormatOptions::new(BaseColorSet::Standard, OverlayMode::Ignore);
assert_eq!(char.describe(&opts), "dark blue"); }
#[test]
fn test_extended_ignore_overlay() {
let char = make_test_characterization(
"blue",
"sapphire",
ColorModifier::Dark,
vec!["navy"],
Some(("navy", 1.5)),
);
let opts = FormatOptions::new(BaseColorSet::Extended, OverlayMode::Ignore);
assert_eq!(char.describe(&opts), "dark sapphire"); }
#[test]
fn test_standard_include_overlay() {
let char = make_test_characterization(
"blue",
"sapphire",
ColorModifier::Dark,
vec!["navy"],
Some(("navy", 1.5)),
);
let opts = FormatOptions::new(BaseColorSet::Standard, OverlayMode::Include);
assert_eq!(char.describe(&opts), "dark navy"); }
#[test]
fn test_extended_include_overlay() {
let char = make_test_characterization(
"blue",
"sapphire",
ColorModifier::Dark,
vec!["navy"],
Some(("navy", 1.5)),
);
let opts = FormatOptions::new(BaseColorSet::Extended, OverlayMode::Include);
assert_eq!(char.describe(&opts), "dark navy"); }
#[test]
fn test_include_falls_back_to_nearest() {
let char = make_test_characterization(
"blue",
"sapphire",
ColorModifier::Vivid,
vec![], Some(("navy", 5.0)), );
let opts = FormatOptions::new(BaseColorSet::Extended, OverlayMode::Include);
assert_eq!(char.describe(&opts), "vivid navy"); }
#[test]
fn test_include_falls_back_to_iscc_nbs() {
let char = make_test_characterization(
"blue",
"sapphire",
ColorModifier::Vivid,
vec![], None, );
let opts = FormatOptions::new(BaseColorSet::Extended, OverlayMode::Include);
assert_eq!(char.describe(&opts), "vivid sapphire"); }
#[test]
fn test_format_options_presets() {
let char = make_test_characterization(
"blue", "sapphire", ColorModifier::Dark,
vec!["navy"],
Some(("navy", 1.5)),
);
assert_eq!(char.describe(&FormatOptions::standard()), "dark blue"); assert_eq!(char.describe(&FormatOptions::extended()), "dark sapphire"); assert_eq!(char.describe(&FormatOptions::standard_with_overlays()), "dark navy"); assert_eq!(char.describe(&FormatOptions::extended_with_overlays()), "dark navy"); }
#[test]
fn test_has_semantic_match() {
let with_match = make_test_characterization(
"blue", "blue", ColorModifier::Dark,
vec!["navy"], Some(("navy", 1.5)),
);
assert!(with_match.has_semantic_match());
assert_eq!(with_match.semantic_match_count(), 1);
let without_match = make_test_characterization(
"blue", "blue", ColorModifier::Vivid,
vec![], Some(("navy", 5.0)),
);
assert!(!without_match.has_semantic_match());
assert_eq!(without_match.semantic_match_count(), 0);
}
#[test]
fn test_display_uses_default() {
let char = make_test_characterization(
"blue", "blue", ColorModifier::Dark,
vec!["navy"], Some(("navy", 1.5)),
);
assert_eq!(format!("{}", char), "dark navy");
}
#[test]
fn test_base_color_method() {
let char = make_test_characterization(
"blue", "sapphire", ColorModifier::Dark,
vec!["navy"], Some(("navy", 1.5)),
);
assert_eq!(char.base_color(&FormatOptions::standard()), "blue");
assert_eq!(char.base_color(&FormatOptions::extended()), "sapphire");
assert_eq!(char.base_color(&FormatOptions::standard_with_overlays()), "navy");
assert_eq!(char.base_color(&FormatOptions::extended_with_overlays()), "navy");
}
}