use crate::color::schemes::ColorScheme;
use crate::error::DotmaxError;
use crate::grid::Color;
#[derive(Debug, Clone)]
pub struct ColorSchemeBuilder {
name: String,
stops: Vec<(f32, Color)>,
}
impl ColorSchemeBuilder {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
stops: Vec::new(),
}
}
#[must_use]
pub fn add_color(mut self, intensity: f32, color: Color) -> Self {
self.stops.push((intensity, color));
self
}
pub fn build(mut self) -> Result<ColorScheme, DotmaxError> {
if self.stops.len() < 2 {
return Err(DotmaxError::InvalidColorScheme(
"at least 2 colors required".into(),
));
}
for &(intensity, _) in &self.stops {
if !(0.0..=1.0).contains(&intensity) {
return Err(DotmaxError::InvalidIntensity(intensity));
}
}
self.stops
.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
for window in self.stops.windows(2) {
if (window[0].0 - window[1].0).abs() < f32::EPSILON {
return Err(DotmaxError::InvalidColorScheme(
"duplicate intensity value".into(),
));
}
}
let colors: Vec<Color> = self.stops.into_iter().map(|(_, color)| color).collect();
ColorScheme::new(self.name, colors)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_new_creates_empty_builder() {
let builder = ColorSchemeBuilder::new("test");
assert_eq!(builder.name, "test");
assert!(builder.stops.is_empty());
}
#[test]
fn test_builder_new_accepts_string() {
let builder = ColorSchemeBuilder::new(String::from("owned_string"));
assert_eq!(builder.name, "owned_string");
}
#[test]
fn test_builder_debug_trait() {
let builder = ColorSchemeBuilder::new("debug_test");
let debug_str = format!("{:?}", builder);
assert!(debug_str.contains("ColorSchemeBuilder"));
assert!(debug_str.contains("debug_test"));
}
#[test]
fn test_builder_clone_trait() {
let builder = ColorSchemeBuilder::new("clone_test")
.add_color(0.0, Color::black())
.add_color(1.0, Color::white());
let cloned = builder.clone();
assert_eq!(cloned.name, "clone_test");
assert_eq!(cloned.stops.len(), 2);
}
#[test]
fn test_add_color_stores_intensity_and_color() {
let builder = ColorSchemeBuilder::new("test").add_color(0.5, Color::rgb(255, 0, 0));
assert_eq!(builder.stops.len(), 1);
assert_eq!(builder.stops[0].0, 0.5);
assert_eq!(builder.stops[0].1, Color::rgb(255, 0, 0));
}
#[test]
fn test_add_color_method_chaining() {
let builder = ColorSchemeBuilder::new("test")
.add_color(0.0, Color::black())
.add_color(0.5, Color::rgb(128, 128, 128))
.add_color(1.0, Color::white());
assert_eq!(builder.stops.len(), 3);
}
#[test]
fn test_add_color_multiple_stops() {
let builder = ColorSchemeBuilder::new("multi")
.add_color(0.0, Color::black())
.add_color(0.25, Color::rgb(64, 64, 64))
.add_color(0.5, Color::rgb(128, 128, 128))
.add_color(0.75, Color::rgb(192, 192, 192))
.add_color(1.0, Color::white());
assert_eq!(builder.stops.len(), 5);
}
#[test]
fn test_build_validates_empty_stops() {
let result = ColorSchemeBuilder::new("empty").build();
assert!(matches!(result, Err(DotmaxError::InvalidColorScheme(_))));
if let Err(DotmaxError::InvalidColorScheme(msg)) = result {
assert!(msg.contains("at least 2 colors"));
}
}
#[test]
fn test_build_validates_single_stop() {
let result = ColorSchemeBuilder::new("single")
.add_color(0.5, Color::white())
.build();
assert!(matches!(result, Err(DotmaxError::InvalidColorScheme(_))));
}
#[test]
fn test_build_validates_intensity_negative() {
let result = ColorSchemeBuilder::new("negative")
.add_color(-0.5, Color::black())
.add_color(1.0, Color::white())
.build();
assert!(matches!(result, Err(DotmaxError::InvalidIntensity(_))));
if let Err(DotmaxError::InvalidIntensity(val)) = result {
assert!(val < 0.0);
}
}
#[test]
fn test_build_validates_intensity_above_one() {
let result = ColorSchemeBuilder::new("above_one")
.add_color(0.0, Color::black())
.add_color(1.5, Color::white())
.build();
assert!(matches!(result, Err(DotmaxError::InvalidIntensity(_))));
if let Err(DotmaxError::InvalidIntensity(val)) = result {
assert!(val > 1.0);
}
}
#[test]
fn test_build_validates_duplicate_intensity() {
let result = ColorSchemeBuilder::new("duplicate")
.add_color(0.5, Color::black())
.add_color(0.5, Color::white())
.build();
assert!(matches!(result, Err(DotmaxError::InvalidColorScheme(_))));
if let Err(DotmaxError::InvalidColorScheme(msg)) = result {
assert!(msg.contains("duplicate"));
}
}
#[test]
fn test_build_with_valid_stops_two_colors() {
let result = ColorSchemeBuilder::new("two")
.add_color(0.0, Color::black())
.add_color(1.0, Color::white())
.build();
assert!(result.is_ok());
let scheme = result.unwrap();
assert_eq!(scheme.name(), "two");
assert_eq!(scheme.colors().len(), 2);
}
#[test]
fn test_build_with_valid_stops_three_colors() {
let result = ColorSchemeBuilder::new("three")
.add_color(0.0, Color::black())
.add_color(0.5, Color::rgb(128, 128, 128))
.add_color(1.0, Color::white())
.build();
assert!(result.is_ok());
assert_eq!(result.unwrap().colors().len(), 3);
}
#[test]
fn test_build_with_valid_stops_five_colors() {
let result = ColorSchemeBuilder::new("five")
.add_color(0.0, Color::rgb(0, 0, 0))
.add_color(0.25, Color::rgb(64, 0, 0))
.add_color(0.5, Color::rgb(128, 0, 0))
.add_color(0.75, Color::rgb(192, 0, 0))
.add_color(1.0, Color::rgb(255, 0, 0))
.build();
assert!(result.is_ok());
assert_eq!(result.unwrap().colors().len(), 5);
}
#[test]
fn test_build_with_valid_stops_ten_colors() {
let mut builder = ColorSchemeBuilder::new("ten");
for i in 0..10 {
let intensity = i as f32 / 9.0;
let gray = (i * 28) as u8;
builder = builder.add_color(intensity, Color::rgb(gray, gray, gray));
}
let result = builder.build();
assert!(result.is_ok());
assert_eq!(result.unwrap().colors().len(), 10);
}
#[test]
fn test_build_sorts_stops_by_intensity() {
let scheme = ColorSchemeBuilder::new("shuffled")
.add_color(1.0, Color::white())
.add_color(0.0, Color::black())
.add_color(0.5, Color::rgb(128, 128, 128))
.build()
.unwrap();
let colors = scheme.colors();
assert_eq!(colors[0], Color::black());
assert_eq!(colors[1], Color::rgb(128, 128, 128));
assert_eq!(colors[2], Color::white());
}
#[test]
fn test_build_sorts_complex_shuffled_order() {
let scheme = ColorSchemeBuilder::new("complex")
.add_color(0.75, Color::rgb(192, 192, 192))
.add_color(0.25, Color::rgb(64, 64, 64))
.add_color(1.0, Color::white())
.add_color(0.0, Color::black())
.add_color(0.5, Color::rgb(128, 128, 128))
.build()
.unwrap();
let colors = scheme.colors();
assert_eq!(colors.len(), 5);
assert_eq!(colors[0], Color::black()); assert_eq!(colors[1], Color::rgb(64, 64, 64)); assert_eq!(colors[2], Color::rgb(128, 128, 128)); assert_eq!(colors[3], Color::rgb(192, 192, 192)); assert_eq!(colors[4], Color::white()); }
#[test]
fn test_build_sorting_does_not_affect_interpolation() {
let scheme = ColorSchemeBuilder::new("interp_test")
.add_color(1.0, Color::rgb(255, 255, 255))
.add_color(0.0, Color::rgb(0, 0, 0))
.build()
.unwrap();
let black = scheme.sample(0.0);
let white = scheme.sample(1.0);
let gray = scheme.sample(0.5);
assert_eq!(black.r, 0);
assert_eq!(white.r, 255);
assert!(gray.r >= 127 && gray.r <= 128);
}
#[test]
fn test_built_scheme_sample_at_boundaries() {
let scheme = ColorSchemeBuilder::new("boundary")
.add_color(0.0, Color::rgb(0, 0, 0))
.add_color(1.0, Color::rgb(255, 255, 255))
.build()
.unwrap();
let black = scheme.sample(0.0);
let white = scheme.sample(1.0);
assert_eq!(black, Color::rgb(0, 0, 0));
assert_eq!(white, Color::rgb(255, 255, 255));
}
#[test]
fn test_built_scheme_sample_midpoint() {
let scheme = ColorSchemeBuilder::new("midpoint")
.add_color(0.0, Color::rgb(0, 0, 0))
.add_color(1.0, Color::rgb(255, 255, 255))
.build()
.unwrap();
let mid = scheme.sample(0.5);
assert!(mid.r >= 127 && mid.r <= 128);
assert!(mid.g >= 127 && mid.g <= 128);
assert!(mid.b >= 127 && mid.b <= 128);
}
#[test]
fn test_built_scheme_sample_at_color_stops() {
let scheme = ColorSchemeBuilder::new("stops")
.add_color(0.0, Color::rgb(255, 0, 0)) .add_color(0.5, Color::rgb(0, 255, 0)) .add_color(1.0, Color::rgb(0, 0, 255)) .build()
.unwrap();
let red = scheme.sample(0.0);
let green = scheme.sample(0.5);
let blue = scheme.sample(1.0);
assert_eq!(red, Color::rgb(255, 0, 0));
assert_eq!(green, Color::rgb(0, 255, 0));
assert_eq!(blue, Color::rgb(0, 0, 255));
}
#[test]
fn test_built_scheme_sample_between_stops() {
let scheme = ColorSchemeBuilder::new("between")
.add_color(0.0, Color::rgb(255, 0, 0)) .add_color(1.0, Color::rgb(0, 0, 255)) .build()
.unwrap();
let mid = scheme.sample(0.5);
assert!(mid.r > 100 && mid.r < 150); assert!(mid.b > 100 && mid.b < 150); assert_eq!(mid.g, 0); }
#[test]
fn test_built_scheme_sample_clamps_intensity() {
let scheme = ColorSchemeBuilder::new("clamp")
.add_color(0.0, Color::black())
.add_color(1.0, Color::white())
.build()
.unwrap();
let below = scheme.sample(-0.5);
assert_eq!(below, Color::black());
let above = scheme.sample(1.5);
assert_eq!(above, Color::white());
}
#[test]
fn test_comprehensive_builder_workflow() {
let scheme = ColorSchemeBuilder::new("sunset")
.add_color(0.0, Color::rgb(25, 25, 112)) .add_color(0.3, Color::rgb(255, 69, 0)) .add_color(0.5, Color::rgb(255, 140, 0)) .add_color(0.7, Color::rgb(255, 215, 0)) .add_color(1.0, Color::rgb(255, 255, 224)) .build()
.unwrap();
assert_eq!(scheme.name(), "sunset");
assert_eq!(scheme.colors().len(), 5);
let dawn = scheme.sample(0.0);
assert_eq!(dawn, Color::rgb(25, 25, 112));
let dusk = scheme.sample(1.0);
assert_eq!(dusk, Color::rgb(255, 255, 224));
let mid = scheme.sample(0.5);
assert_eq!(mid, Color::rgb(255, 140, 0));
let between = scheme.sample(0.15);
assert!(between.r > 100); }
#[test]
fn test_intensity_at_exact_boundaries() {
let result = ColorSchemeBuilder::new("exact")
.add_color(0.0, Color::black())
.add_color(1.0, Color::white())
.build();
assert!(result.is_ok());
}
#[test]
fn test_very_close_intensities_but_not_duplicate() {
let result = ColorSchemeBuilder::new("close")
.add_color(0.0, Color::black())
.add_color(0.001, Color::rgb(1, 1, 1))
.add_color(1.0, Color::white())
.build();
assert!(result.is_ok());
}
#[test]
fn test_name_with_special_characters() {
let result = ColorSchemeBuilder::new("my-scheme_v2.0")
.add_color(0.0, Color::black())
.add_color(1.0, Color::white())
.build();
assert!(result.is_ok());
assert_eq!(result.unwrap().name(), "my-scheme_v2.0");
}
#[test]
fn test_name_with_unicode() {
let result = ColorSchemeBuilder::new("日本語の名前")
.add_color(0.0, Color::black())
.add_color(1.0, Color::white())
.build();
assert!(result.is_ok());
assert_eq!(result.unwrap().name(), "日本語の名前");
}
#[test]
fn test_empty_name() {
let result = ColorSchemeBuilder::new("")
.add_color(0.0, Color::black())
.add_color(1.0, Color::white())
.build();
assert!(result.is_ok());
assert_eq!(result.unwrap().name(), "");
}
}