1use serde::{Deserialize, Serialize};
4use std::fmt;
5use crate::error::{MunsellError, Result};
6use crate::semantic_overlay::{self, MunsellSpec};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct RgbColor {
11 pub r: u8,
13 pub g: u8,
15 pub b: u8,
17}
18
19impl RgbColor {
20 pub fn new(r: u8, g: u8, b: u8) -> Self {
36 Self { r, g, b }
37 }
38
39 pub fn from_array(rgb: [u8; 3]) -> Self {
54 Self {
55 r: rgb[0],
56 g: rgb[1],
57 b: rgb[2],
58 }
59 }
60
61 pub fn to_array(self) -> [u8; 3] {
75 [self.r, self.g, self.b]
76 }
77
78 pub fn is_grayscale(self) -> bool {
94 self.r == self.g && self.g == self.b
95 }
96}
97
98impl fmt::Display for RgbColor {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 write!(f, "RGB({}, {}, {})", self.r, self.g, self.b)
101 }
102}
103
104impl From<[u8; 3]> for RgbColor {
105 fn from(rgb: [u8; 3]) -> Self {
106 Self::from_array(rgb)
107 }
108}
109
110impl From<RgbColor> for [u8; 3] {
111 fn from(color: RgbColor) -> Self {
112 color.to_array()
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct MunsellColor {
119 pub notation: String,
121 pub hue: Option<String>,
123 pub value: f64,
125 pub chroma: Option<f64>,
127}
128
129impl MunsellColor {
130 pub fn new_chromatic(hue: String, value: f64, chroma: f64) -> Self {
146 let notation = format!("{} {:.1}/{:.1}", hue, value, chroma);
147 Self {
148 notation,
149 hue: Some(hue),
150 value,
151 chroma: Some(chroma),
152 }
153 }
154
155 pub fn new_neutral(value: f64) -> Self {
169 let notation = if value == 0.0 {
170 "N 0.0".to_string()
171 } else {
172 format!("N {:.1}/", value)
173 };
174 Self {
175 notation,
176 hue: None,
177 value,
178 chroma: None,
179 }
180 }
181
182 pub fn from_notation(notation: &str) -> Result<Self> {
203 let notation = notation.trim();
204
205 if notation.starts_with("N ") {
207 let value_part = notation.strip_prefix("N ").unwrap().trim_end_matches('/');
208 let value = value_part.parse::<f64>().map_err(|_| MunsellError::InvalidNotation {
209 notation: notation.to_string(),
210 reason: "Invalid value component in neutral color".to_string(),
211 })?;
212
213 if !(0.0..=10.0).contains(&value) {
214 return Err(MunsellError::InvalidNotation {
215 notation: notation.to_string(),
216 reason: "Value must be between 0.0 and 10.0".to_string(),
217 });
218 }
219
220 return Ok(Self {
222 notation: notation.to_string(),
223 hue: None,
224 value,
225 chroma: None,
226 });
227 }
228
229 let parts: Vec<&str> = notation.split_whitespace().collect();
231 if parts.len() != 2 {
232 return Err(MunsellError::InvalidNotation {
233 notation: notation.to_string(),
234 reason: "Expected format: 'HUE VALUE/CHROMA' or 'N VALUE/'".to_string(),
235 });
236 }
237
238 let hue = parts[0].to_string();
239
240 if !is_valid_hue_format(&hue) {
242 return Err(MunsellError::InvalidNotation {
243 notation: notation.to_string(),
244 reason: "Invalid hue format. Expected format like '5R', '2.5YR', etc.".to_string(),
245 });
246 }
247
248 let value_chroma = parts[1];
249
250 if !value_chroma.contains('/') {
251 return Err(MunsellError::InvalidNotation {
252 notation: notation.to_string(),
253 reason: "Missing '/' separator between value and chroma".to_string(),
254 });
255 }
256
257 let value_chroma_parts: Vec<&str> = value_chroma.split('/').collect();
258 if value_chroma_parts.len() != 2 {
259 return Err(MunsellError::InvalidNotation {
260 notation: notation.to_string(),
261 reason: "Invalid value/chroma format".to_string(),
262 });
263 }
264
265 let value = value_chroma_parts[0].parse::<f64>().map_err(|_| MunsellError::InvalidNotation {
266 notation: notation.to_string(),
267 reason: "Invalid value component".to_string(),
268 })?;
269
270 let chroma = value_chroma_parts[1].parse::<f64>().map_err(|_| MunsellError::InvalidNotation {
271 notation: notation.to_string(),
272 reason: "Invalid chroma component".to_string(),
273 })?;
274
275 if !(0.0..=10.0).contains(&value) {
276 return Err(MunsellError::InvalidNotation {
277 notation: notation.to_string(),
278 reason: "Value must be between 0.0 and 10.0".to_string(),
279 });
280 }
281
282 if chroma < 0.0 {
283 return Err(MunsellError::InvalidNotation {
284 notation: notation.to_string(),
285 reason: "Chroma must be non-negative".to_string(),
286 });
287 }
288
289 Ok(Self::new_chromatic(hue, value, chroma))
290 }
291
292 pub fn is_neutral(&self) -> bool {
308 self.hue.is_none() || self.chroma.is_none()
309 }
310
311 pub fn is_chromatic(&self) -> bool {
327 !self.is_neutral()
328 }
329
330 pub fn hue_family(&self) -> Option<String> {
346 self.hue.as_ref().map(|h| {
347 h.chars().filter(|c| c.is_alphabetic()).collect()
349 })
350 }
351
352 pub fn to_munsell_spec(&self) -> Option<MunsellSpec> {
366 if self.is_neutral() {
367 Some(MunsellSpec::neutral(self.value))
369 } else {
370 let hue = self.hue.as_ref()?;
371 let chroma = self.chroma?;
372 let hue_number = semantic_overlay::parse_hue_to_number(hue)?;
373 Some(MunsellSpec::new(hue_number, self.value, chroma))
374 }
375 }
376
377 pub fn semantic_overlay(&self) -> Option<&'static str> {
397 let spec = self.to_munsell_spec()?;
398 semantic_overlay::semantic_overlay(&spec)
399 }
400
401 pub fn matching_overlays(&self) -> Vec<&'static str> {
421 match self.to_munsell_spec() {
422 Some(spec) => semantic_overlay::matching_overlays(&spec),
423 None => Vec::new(),
424 }
425 }
426
427 pub fn matches_overlay(&self, overlay_name: &str) -> bool {
445 match self.to_munsell_spec() {
446 Some(spec) => semantic_overlay::matches_overlay(&spec, overlay_name),
447 None => false,
448 }
449 }
450
451 pub fn closest_overlay(&self) -> Option<(&'static str, f64)> {
471 let spec = self.to_munsell_spec()?;
472 semantic_overlay::closest_overlay(&spec)
473 }
474}
475
476impl fmt::Display for MunsellColor {
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 write!(f, "{}", self.notation)
479 }
480}
481
482#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
484pub struct IsccNbsName {
485 pub color_number: u16,
487 pub descriptor: String,
489 pub color_name: String,
491 pub modifier: Option<String>,
493 pub revised_name: String,
495 pub shade: String,
497}
498
499impl IsccNbsName {
500 pub fn new(
523 color_number: u16,
524 descriptor: String,
525 color_name: String,
526 modifier: Option<String>,
527 revised_color: String,
528 ) -> Self {
529 let revised_name = Self::apply_naming_rules(&color_name, &modifier, &revised_color);
531 let shade = Self::extract_shade(&revised_name);
532
533 Self {
534 color_number,
535 descriptor,
536 color_name,
537 modifier,
538 revised_name,
539 shade,
540 }
541 }
542
543 fn apply_naming_rules(color_name: &str, modifier: &Option<String>, revised_color: &str) -> String {
545 match modifier.as_deref() {
546 None => {
547 if color_name == "white" || color_name == "black" {
549 return color_name.to_string();
550 }
551 revised_color.to_string()
552 }
553 Some(mod_str) => {
554 if mod_str == "-ish white" {
556 format!("{}ish white", apply_ish_rules(color_name))
558 } else if mod_str == "-ish gray" {
559 format!("{}ish gray", apply_ish_rules(color_name))
561 } else if mod_str.starts_with("dark -ish") {
562 let base_mod = mod_str.strip_prefix("dark -ish ").unwrap_or("");
564 format!("dark {}ish {}", apply_ish_rules(color_name), base_mod)
565 } else {
566 format!("{} {}", mod_str, revised_color)
568 }
569 }
570 }
571 }
572
573 fn extract_shade(revised_name: &str) -> String {
575 revised_name
576 .split_whitespace()
577 .last()
578 .unwrap_or(revised_name)
579 .to_string()
580 }
581}
582
583fn apply_ish_rules(color_name: &str) -> String {
585 match color_name {
586 "red" => "reddish".to_string(), "olive" => "olive".to_string(), other => format!("{}ish", other),
589 }
590}
591
592impl fmt::Display for IsccNbsName {
593 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594 write!(f, "{}", self.descriptor)
595 }
596}
597
598#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
600pub struct MunsellPoint {
601 pub hue1: String,
603 pub hue2: String,
605 pub chroma: f64,
607 pub value: f64,
609 pub is_open_chroma: bool,
611}
612
613impl MunsellPoint {
614 pub fn new(hue1: String, hue2: String, chroma: f64, value: f64, is_open_chroma: bool) -> Self {
638 Self {
639 hue1,
640 hue2,
641 chroma,
642 value,
643 is_open_chroma,
644 }
645 }
646
647 pub fn parse_chroma(chroma_str: &str) -> (f64, bool) {
668 if chroma_str.starts_with('>') {
669 let value = chroma_str[1..].parse::<f64>().unwrap_or(15.0);
670 (value, true)
671 } else {
672 let value = chroma_str.parse::<f64>().unwrap_or(0.0);
673 (value, false)
674 }
675 }
676}
677
678#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
680pub struct IsccNbsPolygon {
681 pub color_number: u16,
683 pub descriptor: String,
685 pub color_name: String,
687 pub modifier: Option<String>,
689 pub revised_color: String,
691 pub points: Vec<MunsellPoint>,
693}
694
695impl IsccNbsPolygon {
696 pub fn new(
726 color_number: u16,
727 descriptor: String,
728 color_name: String,
729 modifier: Option<String>,
730 revised_color: String,
731 points: Vec<MunsellPoint>,
732 ) -> Self {
733 Self {
734 color_number,
735 descriptor,
736 color_name,
737 modifier,
738 revised_color,
739 points,
740 }
741 }
742
743 pub fn contains_point(&self, munsell: &MunsellColor) -> bool {
751 if munsell.is_neutral() {
753 return self.contains_neutral_point(munsell.value);
754 }
755
756 let hue = munsell.hue.as_ref().unwrap();
757 let value = munsell.value;
758 let chroma = munsell.chroma.unwrap_or(0.0);
759
760 let hue_degrees = parse_hue_to_degrees(hue);
762
763 self.is_point_in_polygon(hue_degrees, value, chroma)
765 }
766
767 fn contains_neutral_point(&self, value: f64) -> bool {
769 self.points.iter().any(|point| {
772 point.chroma <= 1.0 && (point.value - value).abs() <= 1.0
773 })
774 }
775
776 fn is_point_in_polygon(&self, hue_degrees: f64, value: f64, chroma: f64) -> bool {
778 let mut hue_ranges: Vec<(f64, f64)> = Vec::new();
780 let mut vc_points: Vec<(f64, f64)> = Vec::new();
781
782 for point in &self.points {
784 let hue1_deg = parse_hue_to_degrees(&point.hue1);
785 let hue2_deg = parse_hue_to_degrees(&point.hue2);
786 hue_ranges.push((hue1_deg, hue2_deg));
787 vc_points.push((point.value, point.chroma));
788 }
789
790 let hue_in_range = hue_ranges.iter().any(|(h1, h2)| {
792 is_hue_in_circular_range(hue_degrees, *h1, *h2)
793 });
794
795 if !hue_in_range {
796 return false;
797 }
798
799 ray_casting_point_in_polygon(value, chroma, &vc_points)
801 }
802}
803
804fn parse_hue_to_degrees(hue: &str) -> f64 {
806 let hue_families = [
807 ("R", 0.0), ("YR", 36.0), ("Y", 72.0), ("GY", 108.0), ("G", 144.0),
808 ("BG", 180.0), ("B", 216.0), ("PB", 252.0), ("P", 288.0), ("RP", 324.0)
809 ];
810
811 let family = hue_families
813 .iter()
814 .find(|(fam, _)| hue.ends_with(fam))
815 .map(|(_, deg)| *deg)
816 .unwrap_or(0.0);
817
818 let number_str = hue.chars()
820 .take_while(|c| c.is_ascii_digit() || *c == '.')
821 .collect::<String>();
822
823 let number = number_str.parse::<f64>().unwrap_or(5.0);
824
825 family + (number - 5.0) * 3.6
827}
828
829fn is_hue_in_circular_range(hue: f64, start: f64, end: f64) -> bool {
831 let normalized_hue = hue % 360.0;
832 let normalized_start = start % 360.0;
833 let normalized_end = end % 360.0;
834
835 if normalized_start <= normalized_end {
836 normalized_hue >= normalized_start && normalized_hue <= normalized_end
837 } else {
838 normalized_hue >= normalized_start || normalized_hue <= normalized_end
840 }
841}
842
843fn ray_casting_point_in_polygon(test_x: f64, test_y: f64, vertices: &[(f64, f64)]) -> bool {
845 let mut inside = false;
846 let n = vertices.len();
847
848 if n < 3 {
849 return false;
850 }
851
852 let mut j = n - 1;
853 for i in 0..n {
854 let (xi, yi) = vertices[i];
855 let (xj, yj) = vertices[j];
856
857 if ((yi > test_y) != (yj > test_y)) &&
858 (test_x < (xj - xi) * (test_y - yi) / (yj - yi) + xi) {
859 inside = !inside;
860 }
861 j = i;
862 }
863
864 inside
865}
866
867fn is_valid_hue_format(hue: &str) -> bool {
869 let mut valid_families = ["R", "YR", "Y", "GY", "G", "BG", "B", "PB", "P", "RP"];
871 valid_families.sort_by_key(|s| std::cmp::Reverse(s.len()));
872
873 let family = valid_families.iter()
875 .find(|&&family| hue.ends_with(family));
876
877 let family = match family {
878 Some(f) => f,
879 None => return false,
880 };
881
882 let numeric_part = hue.strip_suffix(family).unwrap_or("");
884
885 if numeric_part.is_empty() {
887 return false;
888 }
889
890 match numeric_part.parse::<f64>() {
892 Ok(num) => num >= 0.0 && num <= 10.0,
893 Err(_) => false,
894 }
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900
901 #[test]
902 fn test_rgb_color() {
903 let color = RgbColor::new(255, 128, 64);
904 assert_eq!(color.r, 255);
905 assert_eq!(color.g, 128);
906 assert_eq!(color.b, 64);
907 assert!(!color.is_grayscale());
908
909 let gray = RgbColor::new(128, 128, 128);
910 assert!(gray.is_grayscale());
911 }
912
913 #[test]
914 fn test_munsell_color_chromatic() {
915 let color = MunsellColor::new_chromatic("5R".to_string(), 4.0, 14.0);
916 assert_eq!(color.notation, "5R 4.0/14.0");
917 assert!(!color.is_neutral());
918 assert!(color.is_chromatic());
919 assert_eq!(color.hue_family(), Some("R".to_string()));
920 }
921
922 #[test]
923 fn test_munsell_color_neutral() {
924 let color = MunsellColor::new_neutral(5.6);
925 assert_eq!(color.notation, "N 5.6/");
926 assert!(color.is_neutral());
927 assert!(!color.is_chromatic());
928 assert_eq!(color.hue_family(), None);
929 }
930
931 #[test]
932 fn test_munsell_parsing() {
933 let color = MunsellColor::from_notation("5R 4.0/14.0").unwrap();
934 assert_eq!(color.hue, Some("5R".to_string()));
935 assert_eq!(color.value, 4.0);
936 assert_eq!(color.chroma, Some(14.0));
937
938 let gray = MunsellColor::from_notation("N 5.6/").unwrap();
939 assert!(gray.is_neutral());
940 assert_eq!(gray.value, 5.6);
941 }
942
943 #[test]
944 fn test_rgb_color_edge_cases() {
945 let black = RgbColor::new(0, 0, 0);
947 assert!(black.is_grayscale());
948 assert_eq!(black.to_array(), [0, 0, 0]);
949
950 let white = RgbColor::new(255, 255, 255);
951 assert!(white.is_grayscale());
952 assert_eq!(white.to_array(), [255, 255, 255]);
953
954 for i in 0..=255 {
956 let gray = RgbColor::new(i, i, i);
957 assert!(gray.is_grayscale());
958 }
959
960 let red = RgbColor::new(255, 0, 0);
962 assert!(!red.is_grayscale());
963
964 let green = RgbColor::new(0, 255, 0);
965 assert!(!green.is_grayscale());
966
967 let blue = RgbColor::new(0, 0, 255);
968 assert!(!blue.is_grayscale());
969 }
970
971 #[test]
972 fn test_munsell_color_edge_cases() {
973 let zero_chroma = MunsellColor::new_chromatic("5R".to_string(), 5.0, 0.0);
975 assert_eq!(zero_chroma.notation, "5R 5.0/0.0");
976 assert!(zero_chroma.is_chromatic());
977
978 let high_chroma = MunsellColor::new_chromatic("5R".to_string(), 5.0, 20.0);
980 assert_eq!(high_chroma.notation, "5R 5.0/20.0");
981
982 let min_value = MunsellColor::new_chromatic("5R".to_string(), 0.0, 10.0);
984 assert_eq!(min_value.value, 0.0);
985
986 let max_value = MunsellColor::new_chromatic("5R".to_string(), 10.0, 10.0);
987 assert_eq!(max_value.value, 10.0);
988 }
989
990 #[test]
991 fn test_munsell_color_neutral_edge_cases() {
992 let black_neutral = MunsellColor::new_neutral(0.0);
994 assert_eq!(black_neutral.notation, "N 0.0");
995 assert!(black_neutral.is_neutral());
996 assert!(!black_neutral.is_chromatic());
997
998 let white_neutral = MunsellColor::new_neutral(10.0);
999 assert_eq!(white_neutral.notation, "N 10.0/");
1000
1001 let mid_neutral = MunsellColor::new_neutral(5.5);
1003 assert_eq!(mid_neutral.notation, "N 5.5/");
1004 }
1005
1006 #[test]
1007 fn test_munsell_parsing_variants() {
1008 let hue_families = ["R", "YR", "Y", "GY", "G", "BG", "B", "PB", "P", "RP"];
1010 for family in &hue_families {
1011 let notation = format!("5{} 5.0/10.0", family);
1012 let color = MunsellColor::from_notation(¬ation).unwrap();
1013 assert_eq!(color.hue_family(), Some(family.to_string()));
1014 assert_eq!(color.value, 5.0);
1015 assert_eq!(color.chroma, Some(10.0));
1016 }
1017
1018 for hue_num in [2.5, 5.0, 7.5, 10.0] {
1020 let notation = format!("{}R 5.0/10.0", hue_num);
1021 let color = MunsellColor::from_notation(¬ation).unwrap();
1022 assert!(color.hue.as_ref().unwrap().contains("R"));
1023 }
1024
1025 let precise = MunsellColor::from_notation("5.5R 6.25/12.75").unwrap();
1027 assert_eq!(precise.value, 6.25);
1028 assert_eq!(precise.chroma, Some(12.75));
1029 }
1030
1031 #[test]
1032 fn test_munsell_parsing_invalid_cases() {
1033 assert!(MunsellColor::from_notation("").is_err());
1035 assert!(MunsellColor::from_notation("invalid").is_err());
1036 assert!(MunsellColor::from_notation("5X 5.0/10.0").is_err()); assert!(MunsellColor::from_notation("R 5.0/10.0").is_err()); assert!(MunsellColor::from_notation("5R /10.0").is_err()); assert!(MunsellColor::from_notation("5R 5.0/").is_err()); assert!(MunsellColor::from_notation("5R -1.0/10.0").is_err()); assert!(MunsellColor::from_notation("5R 5.0/-1.0").is_err()); assert!(MunsellColor::from_notation("N /").is_err()); assert!(MunsellColor::from_notation("N 5.0/10.0").is_err()); }
1045
1046 #[test]
1047 fn test_munsell_color_display() {
1048 let chromatic = MunsellColor::new_chromatic("5R".to_string(), 4.0, 14.0);
1049 assert_eq!(format!("{}", chromatic), "5R 4.0/14.0");
1050
1051 let neutral = MunsellColor::new_neutral(5.6);
1052 assert_eq!(format!("{}", neutral), "N 5.6/");
1053 }
1054
1055 #[test]
1056 fn test_munsell_color_debug() {
1057 let color = MunsellColor::new_chromatic("5R".to_string(), 4.0, 14.0);
1058 let debug_str = format!("{:?}", color);
1059 assert!(debug_str.contains("MunsellColor"));
1060 assert!(debug_str.contains("5R"));
1061 assert!(debug_str.contains("4"));
1062 assert!(debug_str.contains("14"));
1063 }
1064
1065 #[test]
1066 fn test_munsell_color_clone() {
1067 let original = MunsellColor::new_chromatic("5R".to_string(), 4.0, 14.0);
1068 let cloned = original.clone();
1069 assert_eq!(original.notation, cloned.notation);
1070 assert_eq!(original.hue, cloned.hue);
1071 assert_eq!(original.value, cloned.value);
1072 assert_eq!(original.chroma, cloned.chroma);
1073 }
1074
1075 #[test]
1076 fn test_rgb_color_display() {
1077 let color = RgbColor::new(255, 128, 64);
1078 assert_eq!(format!("{}", color), "RGB(255, 128, 64)");
1079 }
1080
1081 #[test]
1082 fn test_rgb_color_debug() {
1083 let color = RgbColor::new(255, 128, 64);
1084 let debug_str = format!("{:?}", color);
1085 assert!(debug_str.contains("RgbColor"));
1086 assert!(debug_str.contains("255"));
1087 assert!(debug_str.contains("128"));
1088 assert!(debug_str.contains("64"));
1089 }
1090
1091 #[test]
1092 fn test_rgb_color_clone() {
1093 let original = RgbColor::new(255, 128, 64);
1094 let cloned = original.clone();
1095 assert_eq!(original.r, cloned.r);
1096 assert_eq!(original.g, cloned.g);
1097 assert_eq!(original.b, cloned.b);
1098 }
1099
1100 #[test]
1101 fn test_rgb_color_equality() {
1102 let color1 = RgbColor::new(255, 128, 64);
1103 let color2 = RgbColor::new(255, 128, 64);
1104 let color3 = RgbColor::new(255, 128, 65);
1105
1106 assert_eq!(color1, color2);
1107 assert_ne!(color1, color3);
1108 }
1109
1110 #[test]
1111 fn test_munsell_color_equality() {
1112 let color1 = MunsellColor::new_chromatic("5R".to_string(), 4.0, 14.0);
1113 let color2 = MunsellColor::new_chromatic("5R".to_string(), 4.0, 14.0);
1114 let color3 = MunsellColor::new_chromatic("5R".to_string(), 4.0, 14.1);
1115
1116 assert_eq!(color1, color2);
1117 assert_ne!(color1, color3);
1118 }
1119
1120 #[test]
1121 fn test_munsell_point_functionality() {
1122 let point = MunsellPoint {
1123 hue1: "5R".to_string(),
1124 hue2: "7R".to_string(),
1125 value: 6.0,
1126 chroma: 12.0,
1127 is_open_chroma: false,
1128 };
1129
1130 assert_eq!(point.hue1, "5R");
1131 assert_eq!(point.hue2, "7R");
1132 assert_eq!(point.value, 6.0);
1133 assert_eq!(point.chroma, 12.0);
1134 assert!(!point.is_open_chroma);
1135
1136 let cloned = point.clone();
1138 assert_eq!(point.hue1, cloned.hue1);
1139 assert_eq!(point.hue2, cloned.hue2);
1140 assert_eq!(point.value, cloned.value);
1141 assert_eq!(point.chroma, cloned.chroma);
1142 assert_eq!(point.is_open_chroma, cloned.is_open_chroma);
1143 }
1144
1145 #[test]
1146 fn test_iscc_nbs_name_functionality() {
1147 let name = IsccNbsName {
1148 color_number: 34,
1149 descriptor: "Strong".to_string(),
1150 color_name: "Red".to_string(),
1151 modifier: None,
1152 revised_name: "Strong Red".to_string(),
1153 shade: "Red".to_string(),
1154 };
1155
1156 assert_eq!(name.color_number, 34);
1157 assert_eq!(name.color_name, "Red");
1158 assert_eq!(name.revised_name, "Strong Red");
1159
1160 let cloned = name.clone();
1162 assert_eq!(name.color_number, cloned.color_number);
1163 assert_eq!(name.color_name, cloned.color_name);
1164 assert_eq!(name.revised_name, cloned.revised_name);
1165 }
1166
1167 #[test]
1168 fn test_iscc_nbs_polygon_functionality() {
1169 let polygon = IsccNbsPolygon {
1170 color_number: 34,
1171 descriptor: "Strong".to_string(),
1172 color_name: "Red".to_string(),
1173 modifier: None,
1174 revised_color: "Strong Red".to_string(),
1175 points: vec![
1176 MunsellPoint {
1177 hue1: "5R".to_string(),
1178 hue2: "7R".to_string(),
1179 value: 5.0,
1180 chroma: 10.0,
1181 is_open_chroma: false,
1182 }
1183 ],
1184 };
1185
1186 assert_eq!(polygon.color_number, 34);
1187 assert_eq!(polygon.color_name, "Red");
1188 assert_eq!(polygon.revised_color, "Strong Red");
1189 assert_eq!(polygon.points.len(), 1);
1190
1191 let cloned = polygon.clone();
1193 assert_eq!(polygon.color_number, cloned.color_number);
1194 assert_eq!(polygon.color_name, cloned.color_name);
1195 assert_eq!(polygon.revised_color, cloned.revised_color);
1196 assert_eq!(polygon.points.len(), cloned.points.len());
1197 }
1198
1199 #[test]
1200 fn test_munsell_color_to_munsell_spec() {
1201 let chromatic = MunsellColor::new_chromatic("5R".to_string(), 4.0, 14.0);
1203 let spec = chromatic.to_munsell_spec();
1204 assert!(spec.is_some());
1205 let spec = spec.unwrap();
1206 assert_eq!(spec.value, 4.0);
1207 assert_eq!(spec.chroma, 14.0);
1208 assert!((spec.hue_number - 2.0).abs() < 0.01);
1210
1211 let neutral = MunsellColor::new_neutral(5.0);
1213 let spec = neutral.to_munsell_spec();
1214 assert!(spec.is_some());
1215 let spec = spec.unwrap();
1216 assert_eq!(spec.value, 5.0);
1217 assert_eq!(spec.chroma, 0.0);
1218 }
1219
1220 #[test]
1221 fn test_munsell_color_semantic_overlay() {
1222 let teal = MunsellColor::new_chromatic("5BG".to_string(), 5.0, 8.0);
1225
1226 assert!(teal.to_munsell_spec().is_some());
1228
1229 let closest = teal.closest_overlay();
1231 assert!(closest.is_some());
1232 let (name, _distance) = closest.unwrap();
1233 assert!(!name.is_empty());
1235 }
1236
1237 #[test]
1238 fn test_munsell_color_matching_overlays() {
1239 let color = MunsellColor::new_chromatic("5R".to_string(), 5.0, 10.0);
1241 let matches = color.matching_overlays();
1242 assert!(matches.len() >= 0);
1244
1245 let neutral = MunsellColor::new_neutral(5.0);
1247 let matches = neutral.matching_overlays();
1248 assert!(matches.len() <= 2);
1250 }
1251
1252 #[test]
1253 fn test_munsell_color_matches_overlay() {
1254 let color = MunsellColor::new_chromatic("5BG".to_string(), 5.0, 8.0);
1256
1257 let _ = color.matches_overlay("teal");
1260 let _ = color.matches_overlay("TEAL");
1261 let _ = color.matches_overlay("Teal");
1262
1263 assert!(!color.matches_overlay("nonexistent"));
1265 }
1266
1267 #[test]
1268 fn test_munsell_color_closest_overlay() {
1269 let colors = [
1271 MunsellColor::new_chromatic("5R".to_string(), 5.0, 10.0),
1272 MunsellColor::new_chromatic("5Y".to_string(), 7.0, 6.0),
1273 MunsellColor::new_chromatic("5B".to_string(), 4.0, 8.0),
1274 MunsellColor::new_chromatic("5P".to_string(), 3.0, 10.0),
1275 ];
1276
1277 for color in &colors {
1278 let result = color.closest_overlay();
1279 assert!(result.is_some(), "closest_overlay should return Some for {}", color);
1280 let (name, distance) = result.unwrap();
1281 assert!(!name.is_empty());
1282 assert!(distance >= 0.0);
1283 }
1284
1285 let neutral = MunsellColor::new_neutral(5.0);
1287 let result = neutral.closest_overlay();
1288 assert!(result.is_some());
1289 }
1290}