use num_traits::{Float, ToPrimitive};
use std::{any::type_name, fmt::Display};
use thiserror::Error;
mod colour_map;
mod colour_parsing;
mod conversion;
mod interpolation;
mod numeric;
pub use colour_map::ColourMapError;
pub use colour_parsing::ColourParsingError;
pub use conversion::ConversionError;
pub use interpolation::InterpolationError;
pub use numeric::NumericError;
#[derive(Error, Debug)]
pub enum ChromaticError {
#[error("Invalid colour: {0}")]
InvalidColour(String),
#[error("Colour parsing error: {0}")]
ColourParsing(#[from] ColourParsingError),
#[error("Colour conversion error: {0}")]
ColourConversion(#[from] ConversionError),
#[error("Interpolation error: {0}")]
Interpolation(#[from] InterpolationError),
#[error("Colour map error: {0}")]
ColourMap(#[from] ColourMapError),
#[error("Math error: {0}")]
Math(#[from] NumericError),
#[error("Display error: {0}")]
Display(String),
}
pub type Result<T> = std::result::Result<T, ChromaticError>;
impl From<ChromaticError> for std::fmt::Error {
fn from(_: ChromaticError) -> Self {
Self
}
}
pub fn safe_constant<S: Copy + Send + Sync + Display + ToPrimitive, T: Send + Sync + Float>(value: S) -> Result<T> {
T::from(value).ok_or_else(|| {
NumericError::TypeConversionFailed {
from: type_name::<S>().to_string(),
to: type_name::<T>().to_string(),
reason: format!("Failed to convert constant: {value}"),
}
.into()
})
}
pub fn validate_component_range<T: Float + Send + Sync>(value: T, name: &str, min: T, max: T) -> Result<()> {
if value < min || value > max {
return Err(ChromaticError::InvalidColour(format!(
"{} component ({}) must be between {} and {}",
name,
value.to_f64().unwrap_or(f64::NAN),
min.to_f64().unwrap_or(f64::NAN),
max.to_f64().unwrap_or(f64::NAN)
)));
}
Ok(())
}
pub fn validate_unit_component<T: Float + Send + Sync>(value: T, name: &str) -> Result<()> {
validate_component_range(value, name, T::zero(), T::one())
}
pub fn validate_interpolation_factor<T: Float + Send + Sync>(t: T) -> Result<()> {
if t < T::zero() || t > T::one() {
return Err(InterpolationError::InvalidInterpolationFactor {
factor: t.to_f64().unwrap_or(f64::NAN),
}
.into());
}
Ok(())
}
pub fn component_to_u8<T: Float + Send + Sync>(value: T, name: &str, scale_factor: T) -> Result<u8> {
let scaled = (value * scale_factor).round();
scaled.to_u8().ok_or_else(|| {
NumericError::TypeConversionFailed {
from: type_name::<T>().to_string(),
to: "u8".to_string(),
reason: format!(
"{} value {} is outside u8 range [0, 255]",
name,
scaled.to_f64().unwrap_or(f64::NAN)
),
}
.into()
})
}
pub fn u8_to_component<T: Float + Send + Sync>(value: u8, scale_factor: T) -> Result<T> {
let converted = safe_constant::<u8, T>(value)?;
Ok(converted / scale_factor)
}
pub fn parse_hex_component(hex: &str, component_name: &str) -> Result<u8> {
u8::from_str_radix(hex, 16).map_err(|source| {
ColourParsingError::HexParseError {
component: component_name.to_string(),
source,
}
.into()
})
}
pub fn normalize_hue<T: Float + Send + Sync>(mut hue: T) -> Result<T> {
const MAX_ITERATIONS: usize = 1000;
let f360 = safe_constant(360.0)?;
let mut iterations = 0;
while hue >= f360 && iterations < MAX_ITERATIONS {
hue = hue - f360;
iterations += 1;
}
iterations = 0;
while hue < T::zero() && iterations < MAX_ITERATIONS {
hue = hue + f360;
iterations += 1;
}
if iterations >= MAX_ITERATIONS {
return Err(NumericError::InvalidMathOperation(format!(
"Hue normalization failed: value too large ({})",
hue.to_f64().unwrap_or(f64::NAN)
))
.into());
}
Ok(hue)
}
pub fn format_terminal_color<T: Float + Send + Sync>(red: T, green: T, blue: T, symbol: char) -> Result<String> {
let scale = safe_constant::<i32, T>(255)?;
let r = component_to_u8(red, "red", scale)?;
let g = component_to_u8(green, "green", scale)?;
let b = component_to_u8(blue, "blue", scale)?;
Ok(format!("\x1b[38;2;{r};{g};{b}m{symbol}\x1b[0m"))
}