@@ -42,9 +42,19 @@ public static class ThemePaletteResolver
4242 /// A <see cref="IReadOnlyList{T}"/> of hex color strings suitable for node fills.
4343 /// </returns>
4444 /// <exception cref="ArgumentNullException"><paramref name="theme"/> is <see langword="null"/>.</exception>
45- public static IReadOnlyList < string > ResolveEffectivePalette ( Theme theme )
45+ public static IReadOnlyList < string > ResolveEffectivePalette ( Theme theme ) =>
46+ ResolveEffectivePalette ( theme , DefaultPaletteSize ) ;
47+
48+ /// <summary>
49+ /// Returns a chromatic palette sized for the caller's repeated-element needs.
50+ /// </summary>
51+ /// <param name="theme">Source theme.</param>
52+ /// <param name="desiredCount">Number of entries to produce for fallback palettes.</param>
53+ public static IReadOnlyList < string > ResolveEffectivePalette ( Theme theme , int desiredCount )
4654 {
4755 ArgumentNullException . ThrowIfNull ( theme ) ;
56+ if ( desiredCount < 1 )
57+ throw new ArgumentOutOfRangeException ( nameof ( desiredCount ) , desiredCount , "desiredCount must be at least 1." ) ;
4858
4959 if ( theme . NodePalette is { Count : > 0 } &&
5060 ! ColorUtils . IsPaletteMonochrome ( theme . NodePalette , theme . BackgroundColor ) )
@@ -53,14 +63,14 @@ public static IReadOnlyList<string> ResolveEffectivePalette(Theme theme)
5363 // Build fallback from gradient stops when they are available and meaningful.
5464 if ( theme . UseBorderGradients && theme . BorderGradientStops is { Count : > 1 } )
5565 {
56- var gradientPalette = BuildPaletteFromGradientStops ( theme . BorderGradientStops , DefaultPaletteSize ) ;
66+ var gradientPalette = BuildPaletteFromGradientStops ( theme . BorderGradientStops , desiredCount ) ;
5767 if ( ! ColorUtils . IsPaletteMonochrome ( gradientPalette , theme . BackgroundColor ) )
5868 return gradientPalette ;
5969 }
6070
6171 // Fall back to hue-rotation derivation from the theme's semantic colors.
6272 bool isLightBackground = ColorUtils . IsLight ( theme . BackgroundColor ) ;
63- return BuildPaletteFromHueRotation ( theme . AccentColor , theme . SecondaryColor , DefaultPaletteSize , isLightBackground ) ;
73+ return BuildPaletteFromHueRotation ( theme . AccentColor , theme . SecondaryColor , desiredCount , isLightBackground ) ;
6474 }
6575
6676 // ── Private helpers ───────────────────────────────────────────────────────
@@ -146,6 +156,8 @@ public static string[] BuildRingColors(Theme theme, int ringCount, string center
146156 if ( ringCount < 1 )
147157 throw new ArgumentOutOfRangeException ( nameof ( ringCount ) , ringCount , "ringCount must be at least 1." ) ;
148158
159+ var chromaticPalette = ResolveEffectivePalette ( theme , Math . Max ( DefaultPaletteSize , ringCount * 2 ) ) ;
160+
149161 var colors = new List < string > ( ringCount )
150162 {
151163 outerColor ,
@@ -156,31 +168,44 @@ public static string[] BuildRingColors(Theme theme, int ringCount, string center
156168
157169 var candidatePool = new List < string >
158170 {
159- ColorUtils . Vibrant ( theme . SecondaryColor , 2.4 ) ,
160- ColorUtils . Vibrant ( theme . AccentColor , 2.4 ) ,
161- ColorUtils . Vibrant ( ColorUtils . Blend ( theme . SecondaryColor , theme . AccentColor , 0.5 ) , 2.6 ) ,
162- ColorUtils . Vibrant ( ColorUtils . Blend ( theme . AccentColor , theme . PrimaryColor , 0.35 ) , 2.5 ) ,
163- ColorUtils . Vibrant ( ColorUtils . Blend ( theme . PrimaryColor , theme . SecondaryColor , 0.25 ) , 2.5 ) ,
171+ ColorUtils . Vibrant ( theme . SecondaryColor , chromaticPalette [ 0 ] , 2.4 ) ,
172+ ColorUtils . Vibrant ( theme . AccentColor , chromaticPalette [ 1 % chromaticPalette . Count ] , 2.4 ) ,
173+ ColorUtils . Vibrant ( ColorUtils . Blend ( theme . SecondaryColor , theme . AccentColor , 0.5 ) , chromaticPalette [ 2 % chromaticPalette . Count ] , 2.6 ) ,
174+ ColorUtils . Vibrant ( ColorUtils . Blend ( theme . AccentColor , theme . PrimaryColor , 0.35 ) , chromaticPalette [ 3 % chromaticPalette . Count ] , 2.5 ) ,
175+ ColorUtils . Vibrant ( ColorUtils . Blend ( theme . PrimaryColor , theme . SecondaryColor , 0.25 ) , chromaticPalette [ 4 % chromaticPalette . Count ] , 2.5 ) ,
164176 ColorUtils . RotateHue ( theme . SecondaryColor , 34 , isLightBackground ) ,
165177 ColorUtils . RotateHue ( theme . AccentColor , - 34 , isLightBackground ) ,
166178 ColorUtils . RotateHue ( theme . PrimaryColor , 52 , isLightBackground ) ,
167179 } ;
168180
169181 if ( theme . NodePalette is { Count : > 0 } )
170182 {
171- foreach ( var paletteColor in theme . NodePalette )
183+ for ( int i = 0 ; i < theme . NodePalette . Count ; i ++ )
172184 {
173- candidatePool . Add ( ColorUtils . Vibrant ( paletteColor , 2.6 ) ) ;
174- candidatePool . Add ( ColorUtils . RotateHue ( paletteColor , 28 , isLightBackground ) ) ;
185+ string paletteColor = theme . NodePalette [ i ] ;
186+ string fallbackColor = chromaticPalette [ i % chromaticPalette . Count ] ;
187+ candidatePool . Add ( ColorUtils . Vibrant ( paletteColor , fallbackColor , 2.6 ) ) ;
188+ candidatePool . Add ( ColorUtils . RotateHue ( fallbackColor , 28 , isLightBackground ) ) ;
175189 }
176190 }
177191
192+ candidatePool . AddRange ( chromaticPalette ) ;
193+
178194 var distinctCandidates = candidatePool
179195 . Select ( color => ColorUtils . Blend ( color , theme . BackgroundColor , isLightBackground ? 0.06 : 0.10 ) )
180196 . Distinct ( StringComparer . OrdinalIgnoreCase )
181197 . Where ( color => ColorUtils . GetHueDistance ( color , outerColor ) >= 18 )
182198 . ToList ( ) ;
183199
200+ if ( ColorUtils . IsPaletteMonochrome ( distinctCandidates , theme . BackgroundColor ) )
201+ {
202+ distinctCandidates = chromaticPalette
203+ . Select ( color => ColorUtils . Blend ( color , theme . BackgroundColor , isLightBackground ? 0.06 : 0.10 ) )
204+ . Distinct ( StringComparer . OrdinalIgnoreCase )
205+ . Where ( color => ColorUtils . GetHueDistance ( color , outerColor ) >= 18 )
206+ . ToList ( ) ;
207+ }
208+
184209 while ( colors . Count < ringCount )
185210 {
186211 string ? nextColor = distinctCandidates
0 commit comments