use std::collections::HashMap;
use crate::{MunsellError, Result};
use crate::iscc::IsccNbsColor;
use geo::CoordsIter;
pub struct MechanicalWedgeSystem {
wedge_containers: HashMap<String, Vec<IsccNbsColor>>,
hue_sequence: Vec<String>,
hue_to_position: HashMap<String, usize>,
}
impl MechanicalWedgeSystem {
pub fn new() -> Self {
let hue_sequence = Self::create_reference_hue_sequence();
let hue_to_position = Self::create_position_lookup(&hue_sequence);
let wedge_containers = Self::create_all_wedge_containers(&hue_sequence);
Self {
wedge_containers,
hue_sequence,
hue_to_position,
}
}
fn create_reference_hue_sequence() -> Vec<String> {
let families = ["R", "YR", "Y", "GY", "G", "BG", "B", "PB", "P", "RP"];
let mut sequence = Vec::with_capacity(100);
for family in &families {
for hue_num in 1..=10 {
sequence.push(format!("{}{}", hue_num, family));
}
}
sequence
}
fn create_position_lookup(sequence: &[String]) -> HashMap<String, usize> {
sequence.iter()
.enumerate()
.map(|(pos, hue)| (hue.clone(), pos))
.collect()
}
fn create_all_wedge_containers(sequence: &[String]) -> HashMap<String, Vec<IsccNbsColor>> {
let mut containers = HashMap::new();
for i in 0..sequence.len() {
let start_hue = &sequence[i];
let end_hue = &sequence[(i + 1) % sequence.len()]; let wedge_key = format!("{}→{}", start_hue, end_hue);
containers.insert(wedge_key, Vec::new());
}
containers
}
pub fn distribute_polygon(&mut self, polygon: IsccNbsColor) -> Result<()> {
let (start_hue, end_hue) = Self::parse_polygon_hue_range(&polygon)?;
let wedge_keys = self.find_wedges_in_range(&start_hue, &end_hue)?;
for wedge_key in wedge_keys {
if let Some(container) = self.wedge_containers.get_mut(&wedge_key) {
container.push(polygon.clone());
}
}
Ok(())
}
fn parse_polygon_hue_range(polygon: &IsccNbsColor) -> Result<(String, String)> {
Ok((polygon.hue_range.0.clone(), polygon.hue_range.1.clone()))
}
fn find_wedges_in_range(&self, start_hue: &str, end_hue: &str) -> Result<Vec<String>> {
let start_pos = self.hue_to_position.get(start_hue)
.ok_or_else(|| MunsellError::ConversionError {
message: format!("Unknown start hue: {}", start_hue)
})?;
let end_pos = self.hue_to_position.get(end_hue)
.ok_or_else(|| MunsellError::ConversionError {
message: format!("Unknown end hue: {}", end_hue)
})?;
let mut wedge_keys = Vec::new();
let mut current_pos = (*start_pos + 1) % self.hue_sequence.len();
loop {
let next_pos = (current_pos + 1) % self.hue_sequence.len();
let start_hue_at_pos = &self.hue_sequence[current_pos];
let end_hue_at_pos = &self.hue_sequence[next_pos];
wedge_keys.push(format!("{}→{}", start_hue_at_pos, end_hue_at_pos));
if current_pos == *end_pos {
break;
}
current_pos = next_pos;
}
Ok(wedge_keys)
}
pub fn find_all_colors_at_point(&self, hue: &str, value: f64, chroma: f64) -> Vec<u16> {
let wedge_key = match self.find_containing_wedge(hue) {
Some(key) => key,
None => return vec![],
};
let container = match self.wedge_containers.get(&wedge_key) {
Some(c) => c,
None => return vec![],
};
let mut matching_colors = Vec::new();
for polygon in container {
if self.point_in_polygon(value, chroma, polygon) {
matching_colors.push(polygon.color_number);
}
}
matching_colors.sort_unstable();
matching_colors.dedup();
matching_colors
}
#[inline]
pub fn classify_color(&self, hue: &str, value: f64, chroma: f64) -> Option<&IsccNbsColor> {
let wedge_key = self.find_containing_wedge(hue)?;
let container = self.wedge_containers.get(&wedge_key)?;
container.iter()
.find(|polygon| self.point_in_polygon(value, chroma, polygon))
}
#[inline]
fn find_containing_wedge(&self, hue: &str) -> Option<String> {
let (hue_number, hue_family) = self.parse_hue(hue).ok()?;
let wedge_number = if hue_number <= 0.0 || hue_number > 10.0 {
let normalized = if hue_number <= 0.0 {
(hue_number % 10.0 + 10.0) % 10.0
} else {
hue_number % 10.0
};
if normalized == 0.0 || normalized <= 1.0 { 1 } else { (normalized.ceil() as u8).min(10) }
} else {
(hue_number.ceil() as u8).max(1).min(10)
};
let wedge_hue = format!("{}{}", wedge_number, hue_family);
let wedge_pos = self.hue_to_position.get(&wedge_hue)?;
let wedge_end_pos = (*wedge_pos + 1) % self.hue_sequence.len();
let start_hue = &self.hue_sequence[*wedge_pos];
let end_hue = &self.hue_sequence[wedge_end_pos];
Some(format!("{}→{}", start_hue, end_hue))
}
#[inline]
fn parse_hue(&self, hue: &str) -> Result<(f64, String)> {
let hue = hue.trim();
let mut split_pos = 0;
for (i, c) in hue.char_indices() {
if c.is_alphabetic() {
split_pos = i;
break;
}
}
if split_pos == 0 {
return Err(MunsellError::ConversionError {
message: format!("Invalid hue format: {}", hue)
});
}
let number_str = &hue[..split_pos];
let family_str = &hue[split_pos..];
let number: f64 = number_str.parse()
.map_err(|_| MunsellError::ConversionError {
message: format!("Invalid hue number: {}", number_str)
})?;
Ok((number, family_str.to_string()))
}
pub fn find_wedge_for_hue(&self, hue: &str) -> Option<String> {
let (hue_num, family) = self.parse_hue(hue).ok()?;
let (start_ref, end_ref) = self.find_bracketing_hues(hue_num, &family)?;
Some(format!("{}→{}", start_ref, end_ref))
}
pub fn get_wedge_polygons(&self, wedge_key: &str) -> Option<&Vec<IsccNbsColor>> {
self.wedge_containers.get(wedge_key)
}
pub fn wedge_count(&self) -> usize {
self.wedge_containers.len()
}
fn find_bracketing_hues(&self, hue_num: f64, family: &str) -> Option<(String, String)> {
let start_num = hue_num.floor() as u32;
let end_num = if start_num == 10 { 1 } else { start_num + 1 };
if start_num == 10 {
let next_family = self.get_next_family(family)?;
Some((
format!("{}{}", start_num, family),
format!("{}{}", end_num, next_family)
))
} else {
Some((
format!("{}{}", start_num, family),
format!("{}{}", end_num, family)
))
}
}
#[inline]
fn get_next_family(&self, family: &str) -> Option<String> {
match family {
"R" => Some("YR".to_string()),
"YR" => Some("Y".to_string()),
"Y" => Some("GY".to_string()),
"GY" => Some("G".to_string()),
"G" => Some("BG".to_string()),
"BG" => Some("B".to_string()),
"B" => Some("PB".to_string()),
"PB" => Some("P".to_string()),
"P" => Some("RP".to_string()),
"RP" => Some("R".to_string()),
_ => None,
}
}
#[inline]
fn point_in_polygon(&self, value: f64, chroma: f64, polygon: &IsccNbsColor) -> bool {
use geo::Contains;
let point = geo::Point::new(chroma, value);
let is_inside = polygon.polygon.contains(&point);
if is_inside {
return true;
}
let (chroma_range, value_range) = self.get_polygon_ranges_at_point(value, chroma, polygon);
if let Some((c_min, c_max)) = chroma_range {
if let Some((v_min, v_max)) = value_range {
let in_chroma = if c_min == 0.0 {
chroma >= c_min && chroma <= c_max } else {
chroma > c_min && chroma <= c_max };
let in_value = if v_min == 0.0 {
value >= v_min && value <= v_max } else {
value > v_min && value <= v_max };
return in_chroma && in_value;
}
}
false
}
#[inline]
fn get_polygon_ranges_at_point(&self, value: f64, chroma: f64, polygon: &IsccNbsColor) -> (Option<(f64, f64)>, Option<(f64, f64)>) {
use geo::Coord;
let coords: Vec<Coord<f64>> = polygon.polygon.exterior().coords().cloned().collect();
let mut chroma_min = None::<f64>;
let mut chroma_max = None::<f64>;
let mut value_min = None::<f64>;
let mut value_max = None::<f64>;
for i in 0..coords.len() - 1 {
let p1 = coords[i];
let p2 = coords[i + 1];
if (p1.y <= value && p2.y >= value) || (p2.y <= value && p1.y >= value) {
if (p2.y - p1.y).abs() < 1e-10 {
let min_x = p1.x.min(p2.x);
let max_x = p1.x.max(p2.x);
chroma_min = Some(chroma_min.map_or(min_x, |m| m.min(min_x)));
chroma_max = Some(chroma_max.map_or(max_x, |m| m.max(max_x)));
} else {
let x = p1.x; chroma_min = Some(chroma_min.map_or(x, |m| m.min(x)));
chroma_max = Some(chroma_max.map_or(x, |m| m.max(x)));
}
}
if (p1.x <= chroma && p2.x >= chroma) || (p2.x <= chroma && p1.x >= chroma) {
if (p2.x - p1.x).abs() < 1e-10 {
let min_y = p1.y.min(p2.y);
let max_y = p1.y.max(p2.y);
value_min = Some(value_min.map_or(min_y, |m| m.min(min_y)));
value_max = Some(value_max.map_or(max_y, |m| m.max(max_y)));
} else {
let y = p1.y; value_min = Some(value_min.map_or(y, |m| m.min(y)));
value_max = Some(value_max.map_or(y, |m| m.max(y)));
}
}
}
let chroma_range = match (chroma_min, chroma_max) {
(Some(min), Some(max)) => Some((min, max)),
_ => None,
};
let value_range = match (value_min, value_max) {
(Some(min), Some(max)) => Some((min, max)),
_ => None,
};
(chroma_range, value_range)
}
fn _find_horizontal_segment_at_point(&self, value: f64, chroma: f64, polygon: &IsccNbsColor) -> Option<(f64, f64)> {
use geo::Coord;
let coords: Vec<Coord<f64>> = polygon.polygon.exterior().coords().cloned().collect();
for i in 0..coords.len() - 1 {
let p1 = coords[i];
let p2 = coords[i + 1];
if (p1.y - value).abs() < 1e-10 && (p2.y - value).abs() < 1e-10 {
let min_x = p1.x.min(p2.x);
let max_x = p1.x.max(p2.x);
if chroma >= min_x - 1e-10 && chroma <= max_x + 1e-10 {
return Some((min_x, max_x));
}
}
}
None
}
fn _find_vertical_segment_at_point(&self, value: f64, chroma: f64, polygon: &IsccNbsColor) -> Option<(f64, f64)> {
use geo::Coord;
let coords: Vec<Coord<f64>> = polygon.polygon.exterior().coords().cloned().collect();
for i in 0..coords.len() - 1 {
let p1 = coords[i];
let p2 = coords[i + 1];
if (p1.x - chroma).abs() < 1e-10 && (p2.x - chroma).abs() < 1e-10 {
let min_y = p1.y.min(p2.y);
let max_y = p1.y.max(p2.y);
if value >= min_y - 1e-10 && value <= max_y + 1e-10 {
return Some((min_y, max_y));
}
}
}
None
}
pub fn get_wedge_statistics(&self) -> WedgeStatistics {
let mut stats = WedgeStatistics::new();
for (wedge_key, container) in &self.wedge_containers {
stats.wedge_counts.insert(wedge_key.clone(), container.len());
stats.total_polygons += container.len();
}
stats.total_wedges = self.wedge_containers.len();
stats
}
pub fn debug_wedge_contents(&self, wedge_key: &str) -> Option<Vec<String>> {
if let Some(container) = self.wedge_containers.get(wedge_key) {
let contents = container.iter()
.map(|color| format!("Color {} (polygon: {} points)",
color.color_number,
color.polygon.exterior().coords_count()
))
.collect();
Some(contents)
} else {
None
}
}
pub fn debug_find_color(&self, color_number: u16) -> Vec<String> {
let mut found_wedges = Vec::new();
for (wedge_key, container) in &self.wedge_containers {
if container.iter().any(|color| color.color_number == color_number) {
found_wedges.push(wedge_key.clone());
}
}
found_wedges
}
pub fn debug_point_test(&self, wedge_key: &str, color_number: u16, value: f64, chroma: f64) -> Option<bool> {
if let Some(container) = self.wedge_containers.get(wedge_key) {
if let Some(color) = container.iter().find(|c| c.color_number == color_number) {
let result = self.point_in_polygon(value, chroma, color);
return Some(result);
}
}
None
}
pub fn debug_point_test_detailed(&self, wedge_key: &str, color_number: u16, value: f64, chroma: f64) -> Option<String> {
if let Some(container) = self.wedge_containers.get(wedge_key) {
if let Some(color) = container.iter().find(|c| c.color_number == color_number) {
let coord_count = color.polygon.exterior().coords_count();
let coord_points: Vec<_> = color.polygon.exterior().coords().collect();
let _test_point = geo::Point::new(chroma, value);
let result = self.point_in_polygon(value, chroma, color);
let debug_info = format!(
"Color {} in wedge {}\n\
Hue range: {} to {}\n\
Test point: (value={}, chroma={}) -> Point::new(x={}, y={}) [chroma=x, value=y]\n\
Polygon {} coordinates: {:?}\n\
Point-in-polygon result: {}",
color_number, wedge_key,
color.hue_range.0, color.hue_range.1,
value, chroma, chroma, value,
coord_count, coord_points,
result
);
return Some(debug_info);
}
}
None
}
pub fn validate_all_wedges(&self) -> WedgeValidationResults {
let mut results = WedgeValidationResults::new();
for (wedge_key, container) in &self.wedge_containers {
let wedge_result = self.validate_single_wedge(wedge_key, container);
results.wedge_results.insert(wedge_key.clone(), wedge_result);
}
results
}
fn validate_single_wedge(&self, _wedge_key: &str, container: &[IsccNbsColor]) -> SingleWedgeValidation {
let mut validation = SingleWedgeValidation::new();
validation.coverage_complete = self.check_wedge_coverage(container);
validation.gaps_detected = self.detect_wedge_gaps(container);
validation.intersections_detected = self.detect_wedge_intersections(container);
validation.polygon_count = container.len();
validation
}
fn check_wedge_coverage(&self, _container: &[IsccNbsColor]) -> bool {
true }
fn detect_wedge_gaps(&self, _container: &[IsccNbsColor]) -> Vec<String> {
Vec::new() }
fn detect_wedge_intersections(&self, _container: &[IsccNbsColor]) -> Vec<String> {
Vec::new() }
}
#[derive(Debug)]
pub struct WedgeStatistics {
pub total_wedges: usize,
pub total_polygons: usize,
pub wedge_counts: HashMap<String, usize>,
}
impl WedgeStatistics {
fn new() -> Self {
Self {
total_wedges: 0,
total_polygons: 0,
wedge_counts: HashMap::new(),
}
}
}
#[derive(Debug)]
pub struct WedgeValidationResults {
pub wedge_results: HashMap<String, SingleWedgeValidation>,
}
impl WedgeValidationResults {
fn new() -> Self {
Self {
wedge_results: HashMap::new(),
}
}
}
#[derive(Debug)]
pub struct SingleWedgeValidation {
pub polygon_count: usize,
pub coverage_complete: bool,
pub gaps_detected: Vec<String>,
pub intersections_detected: Vec<String>,
}
impl SingleWedgeValidation {
fn new() -> Self {
Self {
polygon_count: 0,
coverage_complete: false,
gaps_detected: Vec::new(),
intersections_detected: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hue_sequence_creation() {
let sequence = MechanicalWedgeSystem::create_reference_hue_sequence();
assert_eq!(sequence.len(), 100);
assert_eq!(sequence[0], "1R");
assert_eq!(sequence[1], "2R");
assert_eq!(sequence[9], "10R");
assert_eq!(sequence[10], "1YR");
assert_eq!(sequence[19], "10YR");
assert_eq!(sequence[99], "10RP");
}
#[test]
fn test_wedge_container_creation() {
let system = MechanicalWedgeSystem::new();
assert_eq!(system.wedge_containers.len(), 100);
assert!(system.wedge_containers.contains_key("1R→2R"));
assert!(system.wedge_containers.contains_key("10R→1YR"));
assert!(system.wedge_containers.contains_key("10RP→1R")); }
#[test]
fn test_hue_parsing() {
let system = MechanicalWedgeSystem::new();
let (num, family) = system.parse_hue("5R").unwrap();
assert_eq!(num, 5.0);
assert_eq!(family, "R");
let (num, family) = system.parse_hue("4.5YR").unwrap();
assert_eq!(num, 4.5);
assert_eq!(family, "YR");
let (num, family) = system.parse_hue("7PB").unwrap();
assert_eq!(num, 7.0);
assert_eq!(family, "PB");
}
#[test]
fn test_containing_wedge_range_based_rules() {
let system = MechanicalWedgeSystem::new();
assert_eq!(system.find_containing_wedge("0.0R"), Some("1R→2R".to_string()));
assert_eq!(system.find_containing_wedge("0.5R"), Some("1R→2R".to_string()));
assert_eq!(system.find_containing_wedge("1.0R"), Some("1R→2R".to_string()));
assert_eq!(system.find_containing_wedge("1.1R"), Some("2R→3R".to_string()));
assert_eq!(system.find_containing_wedge("1.5R"), Some("2R→3R".to_string()));
assert_eq!(system.find_containing_wedge("2.0R"), Some("2R→3R".to_string()));
assert_eq!(system.find_containing_wedge("4.5R"), Some("5R→6R".to_string()));
assert_eq!(system.find_containing_wedge("9.5R"), Some("10R→1YR".to_string()));
assert_eq!(system.find_containing_wedge("10.0R"), Some("10R→1YR".to_string()));
assert_eq!(system.find_containing_wedge("7.2YR"), Some("8YR→9YR".to_string()));
assert_eq!(system.find_containing_wedge("10.5R"), Some("1R→2R".to_string()));
assert_eq!(system.find_containing_wedge("11.0R"), Some("1R→2R".to_string()));
}
}