use serde::{Deserialize, Serialize};
use std::fmt;
use super::munsell::MunsellColor;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IsccNbsName {
pub color_number: u16,
pub descriptor: String,
pub color_name: String,
pub modifier: Option<String>,
pub revised_name: String,
pub shade: String,
}
impl IsccNbsName {
pub fn new(
color_number: u16,
descriptor: String,
color_name: String,
modifier: Option<String>,
revised_color: String,
) -> Self {
let revised_name = Self::apply_naming_rules(&color_name, &modifier, &revised_color);
let shade = Self::extract_shade(&revised_name);
Self {
color_number,
descriptor,
color_name,
modifier,
revised_name,
shade,
}
}
fn apply_naming_rules(
color_name: &str,
modifier: &Option<String>,
revised_color: &str,
) -> String {
match modifier.as_deref() {
None => {
if color_name == "white" || color_name == "black" {
return color_name.to_string();
}
revised_color.to_string()
}
Some(mod_str) => {
if mod_str == "-ish white" {
format!("{}ish white", apply_ish_rules(color_name))
} else if mod_str == "-ish gray" {
format!("{}ish gray", apply_ish_rules(color_name))
} else if mod_str.starts_with("dark -ish") {
let base_mod = mod_str.strip_prefix("dark -ish ").unwrap_or("");
format!("dark {}ish {}", apply_ish_rules(color_name), base_mod)
} else {
format!("{} {}", mod_str, revised_color)
}
}
}
}
fn extract_shade(revised_name: &str) -> String {
revised_name
.split_whitespace()
.last()
.unwrap_or(revised_name)
.to_string()
}
}
fn apply_ish_rules(color_name: &str) -> String {
match color_name {
"red" => "reddish".to_string(), "olive" => "olive".to_string(), other => format!("{}ish", other),
}
}
impl fmt::Display for IsccNbsName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.descriptor)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MunsellPoint {
pub hue1: String,
pub hue2: String,
pub chroma: f64,
pub value: f64,
pub is_open_chroma: bool,
}
impl MunsellPoint {
pub fn new(hue1: String, hue2: String, chroma: f64, value: f64, is_open_chroma: bool) -> Self {
Self {
hue1,
hue2,
chroma,
value,
is_open_chroma,
}
}
pub fn parse_chroma(chroma_str: &str) -> (f64, bool) {
if chroma_str.starts_with('>') {
let value = chroma_str[1..].parse::<f64>().unwrap_or(15.0);
(value, true)
} else {
let value = chroma_str.parse::<f64>().unwrap_or(0.0);
(value, false)
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IsccNbsPolygon {
pub color_number: u16,
pub descriptor: String,
pub color_name: String,
pub modifier: Option<String>,
pub revised_color: String,
pub points: Vec<MunsellPoint>,
}
impl IsccNbsPolygon {
pub fn new(
color_number: u16,
descriptor: String,
color_name: String,
modifier: Option<String>,
revised_color: String,
points: Vec<MunsellPoint>,
) -> Self {
Self {
color_number,
descriptor,
color_name,
modifier,
revised_color,
points,
}
}
pub fn contains_point(&self, munsell: &MunsellColor) -> bool {
if munsell.is_neutral() {
return self.contains_neutral_point(munsell.value);
}
let hue = munsell.hue.as_ref().unwrap();
let value = munsell.value;
let chroma = munsell.chroma.unwrap_or(0.0);
let hue_degrees = parse_hue_to_degrees(hue);
self.is_point_in_polygon(hue_degrees, value, chroma)
}
fn contains_neutral_point(&self, value: f64) -> bool {
self.points.iter().any(|point| {
point.chroma <= 1.0 && (point.value - value).abs() <= 1.0
})
}
fn is_point_in_polygon(&self, hue_degrees: f64, value: f64, chroma: f64) -> bool {
let mut hue_ranges: Vec<(f64, f64)> = Vec::new();
let mut vc_points: Vec<(f64, f64)> = Vec::new();
for point in &self.points {
let hue1_deg = parse_hue_to_degrees(&point.hue1);
let hue2_deg = parse_hue_to_degrees(&point.hue2);
hue_ranges.push((hue1_deg, hue2_deg));
vc_points.push((point.value, point.chroma));
}
let hue_in_range = hue_ranges.iter().any(|(h1, h2)| {
is_hue_in_circular_range(hue_degrees, *h1, *h2)
});
if !hue_in_range {
return false;
}
ray_casting_point_in_polygon(value, chroma, &vc_points)
}
}
pub(crate) fn parse_hue_to_degrees(hue: &str) -> f64 {
let hue_families = [
("R", 0.0), ("YR", 36.0), ("Y", 72.0), ("GY", 108.0), ("G", 144.0),
("BG", 180.0), ("B", 216.0), ("PB", 252.0), ("P", 288.0), ("RP", 324.0),
];
let family = hue_families
.iter()
.find(|(fam, _)| hue.ends_with(fam))
.map(|(_, deg)| *deg)
.unwrap_or(0.0);
let number_str = hue.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect::<String>();
let number = number_str.parse::<f64>().unwrap_or(5.0);
family + (number - 5.0) * 3.6
}
pub(crate) fn is_hue_in_circular_range(hue: f64, start: f64, end: f64) -> bool {
let normalized_hue = hue % 360.0;
let normalized_start = start % 360.0;
let normalized_end = end % 360.0;
if normalized_start <= normalized_end {
normalized_hue >= normalized_start && normalized_hue <= normalized_end
} else {
normalized_hue >= normalized_start || normalized_hue <= normalized_end
}
}
pub(crate) fn ray_casting_point_in_polygon(
test_x: f64,
test_y: f64,
vertices: &[(f64, f64)],
) -> bool {
let mut inside = false;
let n = vertices.len();
if n < 3 {
return false;
}
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = vertices[i];
let (xj, yj) = vertices[j];
if ((yi > test_y) != (yj > test_y))
&& (test_x < (xj - xi) * (test_y - yi) / (yj - yi) + xi)
{
inside = !inside;
}
j = i;
}
inside
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_munsell_point_functionality() {
let point = MunsellPoint {
hue1: "5R".to_string(),
hue2: "7R".to_string(),
value: 6.0,
chroma: 12.0,
is_open_chroma: false,
};
assert_eq!(point.hue1, "5R");
assert_eq!(point.hue2, "7R");
assert_eq!(point.value, 6.0);
assert_eq!(point.chroma, 12.0);
assert!(!point.is_open_chroma);
let cloned = point.clone();
assert_eq!(point.hue1, cloned.hue1);
assert_eq!(point.hue2, cloned.hue2);
assert_eq!(point.value, cloned.value);
assert_eq!(point.chroma, cloned.chroma);
assert_eq!(point.is_open_chroma, cloned.is_open_chroma);
}
#[test]
fn test_iscc_nbs_name_functionality() {
let name = IsccNbsName {
color_number: 34,
descriptor: "Strong".to_string(),
color_name: "Red".to_string(),
modifier: None,
revised_name: "Strong Red".to_string(),
shade: "Red".to_string(),
};
assert_eq!(name.color_number, 34);
assert_eq!(name.color_name, "Red");
assert_eq!(name.revised_name, "Strong Red");
let cloned = name.clone();
assert_eq!(name.color_number, cloned.color_number);
assert_eq!(name.color_name, cloned.color_name);
assert_eq!(name.revised_name, cloned.revised_name);
}
#[test]
fn test_iscc_nbs_polygon_functionality() {
let polygon = IsccNbsPolygon {
color_number: 34,
descriptor: "Strong".to_string(),
color_name: "Red".to_string(),
modifier: None,
revised_color: "Strong Red".to_string(),
points: vec![
MunsellPoint {
hue1: "5R".to_string(),
hue2: "7R".to_string(),
value: 5.0,
chroma: 10.0,
is_open_chroma: false,
}
],
};
assert_eq!(polygon.color_number, 34);
assert_eq!(polygon.color_name, "Red");
assert_eq!(polygon.revised_color, "Strong Red");
assert_eq!(polygon.points.len(), 1);
let cloned = polygon.clone();
assert_eq!(polygon.color_number, cloned.color_number);
assert_eq!(polygon.color_name, cloned.color_name);
assert_eq!(polygon.revised_color, cloned.revised_color);
assert_eq!(polygon.points.len(), cloned.points.len());
}
}