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}