dotmax/color/scheme_builder.rs
1//! Builder pattern for creating custom color schemes.
2//!
3//! This module provides [`ColorSchemeBuilder`] for creating custom color schemes with
4//! fine-grained control over intensity-to-color mapping. Unlike the predefined schemes,
5//! custom schemes allow precise placement of color stops at specific intensity values.
6//!
7//! # Overview
8//!
9//! A color scheme maps intensity values (0.0 to 1.0) to colors. The builder pattern
10//! enables creating gradients with color stops at arbitrary intensity positions,
11//! automatically sorting and validating the configuration.
12//!
13//! # Examples
14//!
15//! ## Basic Builder Usage
16//!
17//! ```
18//! use dotmax::color::scheme_builder::ColorSchemeBuilder;
19//! use dotmax::Color;
20//!
21//! let scheme = ColorSchemeBuilder::new("fire")
22//! .add_color(0.0, Color::rgb(0, 0, 0)) // Black at 0%
23//! .add_color(0.3, Color::rgb(255, 0, 0)) // Red at 30%
24//! .add_color(0.7, Color::rgb(255, 165, 0)) // Orange at 70%
25//! .add_color(1.0, Color::rgb(255, 255, 0)) // Yellow at 100%
26//! .build()
27//! .unwrap();
28//!
29//! // Sample the scheme
30//! let color_at_half = scheme.sample(0.5);
31//! ```
32//!
33//! ## Colors in Any Order
34//!
35//! Colors can be added in any order - they are automatically sorted by intensity:
36//!
37//! ```
38//! use dotmax::color::scheme_builder::ColorSchemeBuilder;
39//! use dotmax::Color;
40//!
41//! let scheme = ColorSchemeBuilder::new("shuffled")
42//! .add_color(1.0, Color::white()) // Added last but highest intensity
43//! .add_color(0.0, Color::black()) // Added first but lowest intensity
44//! .add_color(0.5, Color::rgb(128, 128, 128)) // Middle
45//! .build()
46//! .unwrap();
47//!
48//! // Sampling works correctly regardless of insertion order
49//! assert_eq!(scheme.sample(0.0).r, 0); // Black
50//! assert_eq!(scheme.sample(1.0).r, 255); // White
51//! ```
52//!
53//! ## Validation Errors
54//!
55//! The builder validates the configuration and returns descriptive errors:
56//!
57//! ```
58//! use dotmax::color::scheme_builder::ColorSchemeBuilder;
59//! use dotmax::Color;
60//!
61//! // Error: Need at least 2 colors
62//! let result = ColorSchemeBuilder::new("single")
63//! .add_color(0.5, Color::white())
64//! .build();
65//! assert!(result.is_err());
66//!
67//! // Error: Intensity out of range
68//! let result = ColorSchemeBuilder::new("invalid")
69//! .add_color(-0.5, Color::black()) // Invalid!
70//! .add_color(1.5, Color::white()) // Invalid!
71//! .build();
72//! assert!(result.is_err());
73//! ```
74//!
75//! # Performance
76//!
77//! - Builder operations allocate during construction
78//! - Built schemes have identical performance to predefined schemes
79//! - Target: <100ns per `sample()` call on built schemes
80
81use crate::color::schemes::ColorScheme;
82use crate::error::DotmaxError;
83use crate::grid::Color;
84
85/// A builder for creating custom color schemes with intensity-based color stops.
86///
87/// `ColorSchemeBuilder` provides a fluent API for defining color gradients where
88/// each color is associated with a specific intensity value (0.0 to 1.0). The
89/// builder handles sorting, validation, and construction of the final [`ColorScheme`].
90///
91/// # Builder Pattern
92///
93/// The builder follows a standard pattern:
94/// 1. Create with [`ColorSchemeBuilder::new`]
95/// 2. Add colors with [`add_color`](ColorSchemeBuilder::add_color)
96/// 3. Build with [`build`](ColorSchemeBuilder::build)
97///
98/// # Examples
99///
100/// ```
101/// use dotmax::color::scheme_builder::ColorSchemeBuilder;
102/// use dotmax::Color;
103///
104/// // Create a "sunset" gradient
105/// let scheme = ColorSchemeBuilder::new("sunset")
106/// .add_color(0.0, Color::rgb(255, 100, 0)) // Orange
107/// .add_color(0.5, Color::rgb(255, 0, 100)) // Pink
108/// .add_color(1.0, Color::rgb(100, 0, 255)) // Purple
109/// .build()?;
110///
111/// // Use the scheme
112/// let mid_color = scheme.sample(0.5);
113/// # Ok::<(), dotmax::DotmaxError>(())
114/// ```
115#[derive(Debug, Clone)]
116pub struct ColorSchemeBuilder {
117 /// Human-readable name for the scheme
118 name: String,
119 /// Color stops as (intensity, color) pairs
120 stops: Vec<(f32, Color)>,
121}
122
123impl ColorSchemeBuilder {
124 /// Create a new color scheme builder with the given name.
125 ///
126 /// The builder starts with no color stops. Use [`add_color`](ColorSchemeBuilder::add_color)
127 /// to add color stops before calling [`build`](ColorSchemeBuilder::build).
128 ///
129 /// # Arguments
130 ///
131 /// * `name` - Human-readable name for the scheme (e.g., "fire", "ocean", "brand")
132 ///
133 /// # Examples
134 ///
135 /// ```
136 /// use dotmax::color::scheme_builder::ColorSchemeBuilder;
137 ///
138 /// let builder = ColorSchemeBuilder::new("my_gradient");
139 /// ```
140 #[must_use]
141 pub fn new(name: impl Into<String>) -> Self {
142 Self {
143 name: name.into(),
144 stops: Vec::new(),
145 }
146 }
147
148 /// Add a color stop at the specified intensity.
149 ///
150 /// Color stops define the gradient by mapping intensity values to colors.
151 /// Colors can be added in any order - they will be automatically sorted
152 /// by intensity when [`build`](ColorSchemeBuilder::build) is called.
153 ///
154 /// # Arguments
155 ///
156 /// * `intensity` - Intensity value from 0.0 (low) to 1.0 (high)
157 /// * `color` - The RGB color at this intensity
158 ///
159 /// # Returns
160 ///
161 /// Returns `self` for method chaining.
162 ///
163 /// # Note
164 ///
165 /// Intensity validation happens during [`build`](ColorSchemeBuilder::build),
166 /// not during `add_color`. This allows for flexible construction patterns.
167 ///
168 /// # Examples
169 ///
170 /// ```
171 /// use dotmax::color::scheme_builder::ColorSchemeBuilder;
172 /// use dotmax::Color;
173 ///
174 /// let builder = ColorSchemeBuilder::new("gradient")
175 /// .add_color(0.0, Color::black())
176 /// .add_color(0.5, Color::rgb(128, 128, 128))
177 /// .add_color(1.0, Color::white());
178 /// ```
179 #[must_use]
180 pub fn add_color(mut self, intensity: f32, color: Color) -> Self {
181 self.stops.push((intensity, color));
182 self
183 }
184
185 /// Build the color scheme, validating the configuration.
186 ///
187 /// This method validates all color stops and constructs the final [`ColorScheme`].
188 /// Color stops are automatically sorted by intensity in ascending order.
189 ///
190 /// # Validation Rules
191 ///
192 /// The following conditions result in errors:
193 ///
194 /// - **Less than 2 color stops**: Returns [`DotmaxError::InvalidColorScheme`]
195 /// - **Intensity out of range** (< 0.0 or > 1.0): Returns [`DotmaxError::InvalidIntensity`]
196 /// - **Duplicate intensity values**: Returns [`DotmaxError::InvalidColorScheme`]
197 ///
198 /// # Returns
199 ///
200 /// * `Ok(ColorScheme)` - A valid color scheme ready for use
201 /// * `Err(DotmaxError)` - If validation fails
202 ///
203 /// # Examples
204 ///
205 /// ```
206 /// use dotmax::color::scheme_builder::ColorSchemeBuilder;
207 /// use dotmax::Color;
208 ///
209 /// // Successful build
210 /// let scheme = ColorSchemeBuilder::new("valid")
211 /// .add_color(0.0, Color::black())
212 /// .add_color(1.0, Color::white())
213 /// .build()?;
214 ///
215 /// // Failed build: not enough colors
216 /// let result = ColorSchemeBuilder::new("invalid")
217 /// .add_color(0.5, Color::white())
218 /// .build();
219 /// assert!(result.is_err());
220 /// # Ok::<(), dotmax::DotmaxError>(())
221 /// ```
222 ///
223 /// # Errors
224 ///
225 /// Returns [`DotmaxError::InvalidColorScheme`] if:
226 /// - Fewer than 2 color stops are defined
227 /// - Two or more color stops have the same intensity value
228 ///
229 /// Returns [`DotmaxError::InvalidIntensity`] if:
230 /// - Any intensity value is less than 0.0 or greater than 1.0
231 pub fn build(mut self) -> Result<ColorScheme, DotmaxError> {
232 // Validate: at least 2 color stops required
233 if self.stops.len() < 2 {
234 return Err(DotmaxError::InvalidColorScheme(
235 "at least 2 colors required".into(),
236 ));
237 }
238
239 // Validate: all intensities in 0.0-1.0 range
240 for &(intensity, _) in &self.stops {
241 if !(0.0..=1.0).contains(&intensity) {
242 return Err(DotmaxError::InvalidIntensity(intensity));
243 }
244 }
245
246 // Sort by intensity ascending
247 self.stops
248 .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
249
250 // Validate: no duplicate intensity values
251 for window in self.stops.windows(2) {
252 if (window[0].0 - window[1].0).abs() < f32::EPSILON {
253 return Err(DotmaxError::InvalidColorScheme(
254 "duplicate intensity value".into(),
255 ));
256 }
257 }
258
259 // Extract colors in sorted order
260 let colors: Vec<Color> = self.stops.into_iter().map(|(_, color)| color).collect();
261
262 // Create the ColorScheme
263 // Note: ColorScheme::new validates non-empty, which we've already ensured
264 ColorScheme::new(self.name, colors)
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 // ========================================================================
273 // AC1: ColorSchemeBuilder Struct Tests
274 // ========================================================================
275
276 #[test]
277 fn test_builder_new_creates_empty_builder() {
278 let builder = ColorSchemeBuilder::new("test");
279 assert_eq!(builder.name, "test");
280 assert!(builder.stops.is_empty());
281 }
282
283 #[test]
284 fn test_builder_new_accepts_string() {
285 let builder = ColorSchemeBuilder::new(String::from("owned_string"));
286 assert_eq!(builder.name, "owned_string");
287 }
288
289 #[test]
290 fn test_builder_debug_trait() {
291 let builder = ColorSchemeBuilder::new("debug_test");
292 let debug_str = format!("{:?}", builder);
293 assert!(debug_str.contains("ColorSchemeBuilder"));
294 assert!(debug_str.contains("debug_test"));
295 }
296
297 #[test]
298 fn test_builder_clone_trait() {
299 let builder = ColorSchemeBuilder::new("clone_test")
300 .add_color(0.0, Color::black())
301 .add_color(1.0, Color::white());
302 let cloned = builder.clone();
303 assert_eq!(cloned.name, "clone_test");
304 assert_eq!(cloned.stops.len(), 2);
305 }
306
307 // ========================================================================
308 // AC2: Intensity-Based Color Stops Tests
309 // ========================================================================
310
311 #[test]
312 fn test_add_color_stores_intensity_and_color() {
313 let builder = ColorSchemeBuilder::new("test").add_color(0.5, Color::rgb(255, 0, 0));
314 assert_eq!(builder.stops.len(), 1);
315 assert_eq!(builder.stops[0].0, 0.5);
316 assert_eq!(builder.stops[0].1, Color::rgb(255, 0, 0));
317 }
318
319 #[test]
320 fn test_add_color_method_chaining() {
321 let builder = ColorSchemeBuilder::new("test")
322 .add_color(0.0, Color::black())
323 .add_color(0.5, Color::rgb(128, 128, 128))
324 .add_color(1.0, Color::white());
325 assert_eq!(builder.stops.len(), 3);
326 }
327
328 #[test]
329 fn test_add_color_multiple_stops() {
330 let builder = ColorSchemeBuilder::new("multi")
331 .add_color(0.0, Color::black())
332 .add_color(0.25, Color::rgb(64, 64, 64))
333 .add_color(0.5, Color::rgb(128, 128, 128))
334 .add_color(0.75, Color::rgb(192, 192, 192))
335 .add_color(1.0, Color::white());
336 assert_eq!(builder.stops.len(), 5);
337 }
338
339 // ========================================================================
340 // AC3: Validation Rules Tests
341 // ========================================================================
342
343 #[test]
344 fn test_build_validates_empty_stops() {
345 let result = ColorSchemeBuilder::new("empty").build();
346 assert!(matches!(result, Err(DotmaxError::InvalidColorScheme(_))));
347 if let Err(DotmaxError::InvalidColorScheme(msg)) = result {
348 assert!(msg.contains("at least 2 colors"));
349 }
350 }
351
352 #[test]
353 fn test_build_validates_single_stop() {
354 let result = ColorSchemeBuilder::new("single")
355 .add_color(0.5, Color::white())
356 .build();
357 assert!(matches!(result, Err(DotmaxError::InvalidColorScheme(_))));
358 }
359
360 #[test]
361 fn test_build_validates_intensity_negative() {
362 let result = ColorSchemeBuilder::new("negative")
363 .add_color(-0.5, Color::black())
364 .add_color(1.0, Color::white())
365 .build();
366 assert!(matches!(result, Err(DotmaxError::InvalidIntensity(_))));
367 if let Err(DotmaxError::InvalidIntensity(val)) = result {
368 assert!(val < 0.0);
369 }
370 }
371
372 #[test]
373 fn test_build_validates_intensity_above_one() {
374 let result = ColorSchemeBuilder::new("above_one")
375 .add_color(0.0, Color::black())
376 .add_color(1.5, Color::white())
377 .build();
378 assert!(matches!(result, Err(DotmaxError::InvalidIntensity(_))));
379 if let Err(DotmaxError::InvalidIntensity(val)) = result {
380 assert!(val > 1.0);
381 }
382 }
383
384 #[test]
385 fn test_build_validates_duplicate_intensity() {
386 let result = ColorSchemeBuilder::new("duplicate")
387 .add_color(0.5, Color::black())
388 .add_color(0.5, Color::white())
389 .build();
390 assert!(matches!(result, Err(DotmaxError::InvalidColorScheme(_))));
391 if let Err(DotmaxError::InvalidColorScheme(msg)) = result {
392 assert!(msg.contains("duplicate"));
393 }
394 }
395
396 #[test]
397 fn test_build_with_valid_stops_two_colors() {
398 let result = ColorSchemeBuilder::new("two")
399 .add_color(0.0, Color::black())
400 .add_color(1.0, Color::white())
401 .build();
402 assert!(result.is_ok());
403 let scheme = result.unwrap();
404 assert_eq!(scheme.name(), "two");
405 assert_eq!(scheme.colors().len(), 2);
406 }
407
408 #[test]
409 fn test_build_with_valid_stops_three_colors() {
410 let result = ColorSchemeBuilder::new("three")
411 .add_color(0.0, Color::black())
412 .add_color(0.5, Color::rgb(128, 128, 128))
413 .add_color(1.0, Color::white())
414 .build();
415 assert!(result.is_ok());
416 assert_eq!(result.unwrap().colors().len(), 3);
417 }
418
419 #[test]
420 fn test_build_with_valid_stops_five_colors() {
421 let result = ColorSchemeBuilder::new("five")
422 .add_color(0.0, Color::rgb(0, 0, 0))
423 .add_color(0.25, Color::rgb(64, 0, 0))
424 .add_color(0.5, Color::rgb(128, 0, 0))
425 .add_color(0.75, Color::rgb(192, 0, 0))
426 .add_color(1.0, Color::rgb(255, 0, 0))
427 .build();
428 assert!(result.is_ok());
429 assert_eq!(result.unwrap().colors().len(), 5);
430 }
431
432 #[test]
433 fn test_build_with_valid_stops_ten_colors() {
434 let mut builder = ColorSchemeBuilder::new("ten");
435 for i in 0..10 {
436 let intensity = i as f32 / 9.0;
437 let gray = (i * 28) as u8;
438 builder = builder.add_color(intensity, Color::rgb(gray, gray, gray));
439 }
440 let result = builder.build();
441 assert!(result.is_ok());
442 assert_eq!(result.unwrap().colors().len(), 10);
443 }
444
445 // ========================================================================
446 // AC4: Automatic Intensity Sorting Tests
447 // ========================================================================
448
449 #[test]
450 fn test_build_sorts_stops_by_intensity() {
451 // Add colors out of order
452 let scheme = ColorSchemeBuilder::new("shuffled")
453 .add_color(1.0, Color::white())
454 .add_color(0.0, Color::black())
455 .add_color(0.5, Color::rgb(128, 128, 128))
456 .build()
457 .unwrap();
458
459 // Colors should be sorted: black, gray, white
460 let colors = scheme.colors();
461 assert_eq!(colors[0], Color::black());
462 assert_eq!(colors[1], Color::rgb(128, 128, 128));
463 assert_eq!(colors[2], Color::white());
464 }
465
466 #[test]
467 fn test_build_sorts_complex_shuffled_order() {
468 let scheme = ColorSchemeBuilder::new("complex")
469 .add_color(0.75, Color::rgb(192, 192, 192))
470 .add_color(0.25, Color::rgb(64, 64, 64))
471 .add_color(1.0, Color::white())
472 .add_color(0.0, Color::black())
473 .add_color(0.5, Color::rgb(128, 128, 128))
474 .build()
475 .unwrap();
476
477 let colors = scheme.colors();
478 assert_eq!(colors.len(), 5);
479 assert_eq!(colors[0], Color::black()); // 0.0
480 assert_eq!(colors[1], Color::rgb(64, 64, 64)); // 0.25
481 assert_eq!(colors[2], Color::rgb(128, 128, 128)); // 0.5
482 assert_eq!(colors[3], Color::rgb(192, 192, 192)); // 0.75
483 assert_eq!(colors[4], Color::white()); // 1.0
484 }
485
486 #[test]
487 fn test_build_sorting_does_not_affect_interpolation() {
488 // Build scheme with colors in random order
489 let scheme = ColorSchemeBuilder::new("interp_test")
490 .add_color(1.0, Color::rgb(255, 255, 255))
491 .add_color(0.0, Color::rgb(0, 0, 0))
492 .build()
493 .unwrap();
494
495 // Interpolation should work correctly
496 let black = scheme.sample(0.0);
497 let white = scheme.sample(1.0);
498 let gray = scheme.sample(0.5);
499
500 assert_eq!(black.r, 0);
501 assert_eq!(white.r, 255);
502 assert!(gray.r >= 127 && gray.r <= 128);
503 }
504
505 // ========================================================================
506 // AC5: Integration with sample() Tests
507 // ========================================================================
508
509 #[test]
510 fn test_built_scheme_sample_at_boundaries() {
511 let scheme = ColorSchemeBuilder::new("boundary")
512 .add_color(0.0, Color::rgb(0, 0, 0))
513 .add_color(1.0, Color::rgb(255, 255, 255))
514 .build()
515 .unwrap();
516
517 let black = scheme.sample(0.0);
518 let white = scheme.sample(1.0);
519
520 assert_eq!(black, Color::rgb(0, 0, 0));
521 assert_eq!(white, Color::rgb(255, 255, 255));
522 }
523
524 #[test]
525 fn test_built_scheme_sample_midpoint() {
526 let scheme = ColorSchemeBuilder::new("midpoint")
527 .add_color(0.0, Color::rgb(0, 0, 0))
528 .add_color(1.0, Color::rgb(255, 255, 255))
529 .build()
530 .unwrap();
531
532 let mid = scheme.sample(0.5);
533 // Should be approximately 128 (gray)
534 assert!(mid.r >= 127 && mid.r <= 128);
535 assert!(mid.g >= 127 && mid.g <= 128);
536 assert!(mid.b >= 127 && mid.b <= 128);
537 }
538
539 #[test]
540 fn test_built_scheme_sample_at_color_stops() {
541 let scheme = ColorSchemeBuilder::new("stops")
542 .add_color(0.0, Color::rgb(255, 0, 0)) // Red
543 .add_color(0.5, Color::rgb(0, 255, 0)) // Green
544 .add_color(1.0, Color::rgb(0, 0, 255)) // Blue
545 .build()
546 .unwrap();
547
548 let red = scheme.sample(0.0);
549 let green = scheme.sample(0.5);
550 let blue = scheme.sample(1.0);
551
552 assert_eq!(red, Color::rgb(255, 0, 0));
553 assert_eq!(green, Color::rgb(0, 255, 0));
554 assert_eq!(blue, Color::rgb(0, 0, 255));
555 }
556
557 #[test]
558 fn test_built_scheme_sample_between_stops() {
559 let scheme = ColorSchemeBuilder::new("between")
560 .add_color(0.0, Color::rgb(255, 0, 0)) // Red
561 .add_color(1.0, Color::rgb(0, 0, 255)) // Blue
562 .build()
563 .unwrap();
564
565 let mid = scheme.sample(0.5);
566 // Should be purple-ish (mix of red and blue)
567 assert!(mid.r > 100 && mid.r < 150); // ~128
568 assert!(mid.b > 100 && mid.b < 150); // ~128
569 assert_eq!(mid.g, 0); // Green should stay 0
570 }
571
572 #[test]
573 fn test_built_scheme_sample_clamps_intensity() {
574 let scheme = ColorSchemeBuilder::new("clamp")
575 .add_color(0.0, Color::black())
576 .add_color(1.0, Color::white())
577 .build()
578 .unwrap();
579
580 // Clamped to 0.0
581 let below = scheme.sample(-0.5);
582 assert_eq!(below, Color::black());
583
584 // Clamped to 1.0
585 let above = scheme.sample(1.5);
586 assert_eq!(above, Color::white());
587 }
588
589 // ========================================================================
590 // AC7: Comprehensive Builder Workflow Test
591 // ========================================================================
592
593 #[test]
594 fn test_comprehensive_builder_workflow() {
595 // Create a custom "sunset" gradient
596 let scheme = ColorSchemeBuilder::new("sunset")
597 .add_color(0.0, Color::rgb(25, 25, 112)) // Dark blue
598 .add_color(0.3, Color::rgb(255, 69, 0)) // Red-orange
599 .add_color(0.5, Color::rgb(255, 140, 0)) // Orange
600 .add_color(0.7, Color::rgb(255, 215, 0)) // Gold
601 .add_color(1.0, Color::rgb(255, 255, 224)) // Light yellow
602 .build()
603 .unwrap();
604
605 // Verify scheme metadata
606 assert_eq!(scheme.name(), "sunset");
607 assert_eq!(scheme.colors().len(), 5);
608
609 // Verify sampling at various points
610 let dawn = scheme.sample(0.0);
611 assert_eq!(dawn, Color::rgb(25, 25, 112));
612
613 let dusk = scheme.sample(1.0);
614 assert_eq!(dusk, Color::rgb(255, 255, 224));
615
616 // Mid-range should be interpolated
617 let mid = scheme.sample(0.5);
618 assert_eq!(mid, Color::rgb(255, 140, 0)); // Exact stop
619
620 // Between stops
621 let between = scheme.sample(0.15);
622 assert!(between.r > 100); // Interpolating toward orange
623 }
624
625 // ========================================================================
626 // Edge Case Tests
627 // ========================================================================
628
629 #[test]
630 fn test_intensity_at_exact_boundaries() {
631 let result = ColorSchemeBuilder::new("exact")
632 .add_color(0.0, Color::black())
633 .add_color(1.0, Color::white())
634 .build();
635 assert!(result.is_ok());
636 }
637
638 #[test]
639 fn test_very_close_intensities_but_not_duplicate() {
640 let result = ColorSchemeBuilder::new("close")
641 .add_color(0.0, Color::black())
642 .add_color(0.001, Color::rgb(1, 1, 1))
643 .add_color(1.0, Color::white())
644 .build();
645 assert!(result.is_ok());
646 }
647
648 #[test]
649 fn test_name_with_special_characters() {
650 let result = ColorSchemeBuilder::new("my-scheme_v2.0")
651 .add_color(0.0, Color::black())
652 .add_color(1.0, Color::white())
653 .build();
654 assert!(result.is_ok());
655 assert_eq!(result.unwrap().name(), "my-scheme_v2.0");
656 }
657
658 #[test]
659 fn test_name_with_unicode() {
660 let result = ColorSchemeBuilder::new("日本語の名前")
661 .add_color(0.0, Color::black())
662 .add_color(1.0, Color::white())
663 .build();
664 assert!(result.is_ok());
665 assert_eq!(result.unwrap().name(), "日本語の名前");
666 }
667
668 #[test]
669 fn test_empty_name() {
670 let result = ColorSchemeBuilder::new("")
671 .add_color(0.0, Color::black())
672 .add_color(1.0, Color::white())
673 .build();
674 assert!(result.is_ok());
675 assert_eq!(result.unwrap().name(), "");
676 }
677}