Files
OpenNest/OpenNest.Desktop/ColorShade.cs
T
ajandClaude Opus 5.5 cc3711c11c refactor(desktop): move colour schemes into OpenNest.Desktop
ColorScheme, ColorSchemeRegistry, ColorSchemeSerializer and the Schemes
folder move into the toolkit-neutral library, keeping the OpenNest
namespace, so the Avalonia app can share the built-in palettes, the
scheme files and the recolour-on-open rule.

ColorScheme is now pure colour data with a Version that changes with
any colour. Its GDI+ pens and brushes move to GdiSchemeTools in the
WinForms project (still one set per thread, rebuilt after a colour
change), reached through scheme.Gdi(). The legacy part outline uses
ColorShade.Dark, a port of ControlPaint.Dark's HLS arithmetic; a
WinForms test compares the two across a colour sweep on Windows.

The registry no longer walks Application.OpenForms: Apply raises
Applied, and EditNestForm subscribes once to recolour every open nest
window as before. The saved scheme is read under the same
ActiveColorScheme setting (SavedSchemeName/ApplySaved for the shared
user-settings file); WinForms still reads it through Settings.Default.
Schemes/*.json now copies beside both desktop apps from OpenNest.Desktop.

Pure scheme tests move to OpenNest.Desktop.Tests, with new ones for
built-in round trips, scheme files, bad files, the saved name and the
recolour rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 21:23:48 -05:00

123 lines
4.8 KiB
C#

using System.Drawing;
namespace OpenNest
{
/// <summary>
/// The darker shade WinForms' <c>ControlPaint.Dark</c> gives a colour, computed without
/// WinForms so both desktop front ends outline parts alike. A port of the HLS arithmetic in
/// dotnet/winforms (ControlPaint.HLSColor, MIT licence), including its SystemColors.Control
/// special case.
/// </summary>
public static class ColorShade
{
private const int ShadowAdjustment = -333;
private const int HLSMax = 240;
private const int RGBMax = 255;
private const int Undefined = HLSMax * 2 / 3;
/// <summary>As <c>ControlPaint.Dark(color)</c>: halfway to the darkest shade.</summary>
public static Color Dark(Color color) => Dark(color, 0.5f);
/// <summary>As <c>ControlPaint.Dark(color, percOfDarkDark)</c>.</summary>
public static Color Dark(Color color, float percOfDarkDark)
{
// With the usual colour scheme, ControlDark/DarkDark is not exactly what the HLS
// arithmetic would give, so WinForms uses the system colours for Control.
if (color.ToKnownColor() == KnownColor.Control)
{
if (percOfDarkDark == 0.0f)
return SystemColors.ControlDark;
if (percOfDarkDark == 1.0f)
return SystemColors.ControlDarkDark;
var dark = SystemColors.ControlDark;
var darkDark = SystemColors.ControlDarkDark;
return Color.FromArgb(
(byte)(dark.R - (byte)((dark.R - darkDark.R) * percOfDarkDark)),
(byte)(dark.G - (byte)((dark.G - darkDark.G) * percOfDarkDark)),
(byte)(dark.B - (byte)((dark.B - darkDark.B) * percOfDarkDark)));
}
var (hue, luminosity, saturation) = ToHls(color);
var zeroLum = luminosity * (ShadowAdjustment + 1000) / 1000;
return FromHls(hue, zeroLum - (int)(zeroLum * percOfDarkDark), saturation);
}
private static (int Hue, int Luminosity, int Saturation) ToHls(Color color)
{
int r = color.R;
int g = color.G;
int b = color.B;
var max = System.Math.Max(System.Math.Max(r, g), b);
var min = System.Math.Min(System.Math.Min(r, g), b);
var sum = max + min;
var luminosity = ((sum * HLSMax) + RGBMax) / (2 * RGBMax);
var dif = max - min;
if (dif == 0)
return (Undefined, luminosity, 0);
var saturation = luminosity <= (HLSMax / 2)
? ((dif * HLSMax) + (sum / 2)) / sum
: ((dif * HLSMax) + (2 * RGBMax - sum) / 2) / (2 * RGBMax - sum);
var rDelta = (((max - r) * (HLSMax / 6)) + (dif / 2)) / dif;
var gDelta = (((max - g) * (HLSMax / 6)) + (dif / 2)) / dif;
var bDelta = (((max - b) * (HLSMax / 6)) + (dif / 2)) / dif;
int hue;
if (r == max)
hue = bDelta - gDelta;
else if (g == max)
hue = (HLSMax / 3) + rDelta - bDelta;
else
hue = (2 * HLSMax / 3) + gDelta - rDelta;
if (hue < 0)
hue += HLSMax;
if (hue > HLSMax)
hue -= HLSMax;
return (hue, luminosity, saturation);
}
private static Color FromHls(int hue, int luminosity, int saturation)
{
byte r, g, b;
if (saturation == 0)
{
r = g = b = (byte)(luminosity * RGBMax / HLSMax);
}
else
{
var magic2 = luminosity <= (HLSMax / 2)
? ((luminosity * (HLSMax + saturation)) + (HLSMax / 2)) / HLSMax
: luminosity + saturation - (((luminosity * saturation) + (HLSMax / 2)) / HLSMax);
var magic1 = 2 * luminosity - magic2;
r = (byte)((HueToRgb(magic1, magic2, hue + HLSMax / 3) * RGBMax + (HLSMax / 2)) / HLSMax);
g = (byte)((HueToRgb(magic1, magic2, hue) * RGBMax + (HLSMax / 2)) / HLSMax);
b = (byte)((HueToRgb(magic1, magic2, hue - HLSMax / 3) * RGBMax + (HLSMax / 2)) / HLSMax);
}
return Color.FromArgb(r, g, b);
}
private static int HueToRgb(int n1, int n2, int hue)
{
if (hue < 0)
hue += HLSMax;
if (hue > HLSMax)
hue -= HLSMax;
if (hue < (HLSMax / 6))
return n1 + (((n2 - n1) * hue + (HLSMax / 12)) / (HLSMax / 6));
if (hue < (HLSMax / 2))
return n2;
if (hue < (HLSMax * 2 / 3))
return n1 + (((n2 - n1) * ((HLSMax * 2 / 3) - hue) + (HLSMax / 12)) / (HLSMax / 6));
return n1;
}
}
}