From b115b9dcb01a9869dab051d31c656c3264c99991 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 18 Oct 2026 12:47:58 -0400 Subject: [PATCH] feat(ui): add Halloween, Christmas and 4th of July color schemes Built-in festive schemes theme the background, sheet and accents while keeping 12-color part palettes distinct, light enough for black part IDs and clear of the green etch stroke. They are only applied when chosen; there is no automatic seasonal switching. Co-Authored-By: Claude Opus 5.5 --- .../FestiveColorSchemeTests.cs | 56 +++++++++++ OpenNest/ColorSchemeRegistry.cs | 92 +++++++++++++++++++ docs/nesting-view-colors.md | 8 ++ 3 files changed, 156 insertions(+) create mode 100644 OpenNest.WinForms.Tests/FestiveColorSchemeTests.cs diff --git a/OpenNest.WinForms.Tests/FestiveColorSchemeTests.cs b/OpenNest.WinForms.Tests/FestiveColorSchemeTests.cs new file mode 100644 index 00000000..ce54ff88 --- /dev/null +++ b/OpenNest.WinForms.Tests/FestiveColorSchemeTests.cs @@ -0,0 +1,56 @@ +using System.Drawing; + +namespace OpenNest.WinForms.Tests; + +public class FestiveColorSchemeTests +{ + [Theory] + [InlineData("Halloween")] + [InlineData("Christmas")] + [InlineData("4th of July")] + public void FestiveSchemeKeepsPartsDistinctAndReadable(string name) + { + var scheme = ColorSchemeRegistry.Get(name); + + Assert.Equal(name, scheme.Name); + Assert.False(scheme.UseGoldenAngleColors); + Assert.True(scheme.PartColors.Length >= 12); + Assert.Equal(Color.Lime.ToArgb(), scheme.EtchColor.ToArgb()); + Assert.True(scheme.SelectedPartColor.IsEmpty); + + var colors = scheme.PartColors; + for (var i = 0; i < colors.Length; i++) + { + // Part IDs are drawn in black over the fill. + Assert.True(Luminance(colors[i]) >= 90, $"{name} part color {i} is too dark for black part IDs."); + Assert.True(Distance(colors[i], scheme.EtchColor) >= 120, $"{name} part color {i} is too close to the etch color."); + for (var j = i + 1; j < colors.Length; j++) + Assert.True(Distance(colors[i], colors[j]) >= 40, $"{name} part colors {i} and {j} are too similar."); + } + } + + [Fact] + public void FestiveSchemesRoundTripAndLeaveClassicAsDefault() + { + foreach (var name in new[] { "Halloween", "Christmas", "4th of July" }) + { + var scheme = ColorSchemeRegistry.Get(name); + var restored = ColorSchemeSerializer.Deserialize(ColorSchemeSerializer.Serialize(scheme)); + Assert.Equal(name, restored.Name); + Assert.Equal(scheme.BackgroundColor.ToArgb(), restored.BackgroundColor.ToArgb()); + Assert.Equal(scheme.PartColors.Select(c => c.ToArgb()), restored.PartColors.Select(c => c.ToArgb())); + } + + Assert.Equal("Classic", ColorSchemeRegistry.Get(null).Name); + } + + private static double Distance(Color a, Color b) + { + var dr = a.R - b.R; + var dg = a.G - b.G; + var db = a.B - b.B; + return System.Math.Sqrt(dr * dr + dg * dg + db * db); + } + + private static double Luminance(Color c) => 0.299 * c.R + 0.587 * c.G + 0.114 * c.B; +} diff --git a/OpenNest/ColorSchemeRegistry.cs b/OpenNest/ColorSchemeRegistry.cs index 5405e145..11dd54f8 100644 --- a/OpenNest/ColorSchemeRegistry.cs +++ b/OpenNest/ColorSchemeRegistry.cs @@ -19,6 +19,9 @@ namespace OpenNest ["Pastel"] = BuildPastel(), ["Dark"] = BuildDark(), ["Workshop"] = BuildWorkshop(), + ["Halloween"] = BuildHalloween(), + ["Christmas"] = BuildChristmas(), + ["4th of July"] = BuildFourthOfJuly(), }; private static List diskCache; @@ -135,6 +138,95 @@ namespace OpenNest PartColors = Enumerable.Range(0, 12).Select(PartColorPalette.GoldenAngle).ToArray(), }; + // Festive schemes theme the background, sheet and accents; their part palettes + // stay varied and light enough for black part IDs, and avoid the etch green. + private static ColorScheme BuildHalloween() => + new ColorScheme + { + Name = "Halloween", + BackgroundColor = Color.FromArgb(28, 18, 36), + LayoutOutlineColor = Color.FromArgb(110, 80, 130), + LayoutFillColor = Color.FromArgb(52, 48, 58), + BoundingBoxColor = Color.FromArgb(170, 120, 230), + RapidColor = Color.FromArgb(255, 140, 0), + OriginColor = Color.FromArgb(130, 110, 140), + EdgeSpacingColor = Color.FromArgb(80, 70, 92), + PreviewPartColor = Color.FromArgb(160, 255, 80), + PartColors = new[] + { + Color.FromArgb(255, 130, 30), // Pumpkin + Color.FromArgb(170, 110, 230), // Witch Purple + Color.FromArgb(255, 205, 60), // Candy Corn + Color.FromArgb(235, 235, 245), // Ghost + Color.FromArgb(220, 60, 60), // Blood Red + Color.FromArgb(130, 170, 230), // Moonlight + Color.FromArgb(225, 210, 170), // Bone + Color.FromArgb(230, 90, 200), // Potion + Color.FromArgb(60, 200, 180), // Cauldron Teal + Color.FromArgb(200, 120, 60), // Caramel Apple + Color.FromArgb(200, 170, 240), // Phantom Lavender + Color.FromArgb(255, 90, 40), // Ember + }, + }; + + private static ColorScheme BuildChristmas() => + new ColorScheme + { + Name = "Christmas", + BackgroundColor = Color.FromArgb(18, 58, 38), + LayoutOutlineColor = Color.FromArgb(190, 160, 80), + LayoutFillColor = Color.FromArgb(248, 248, 244), + BoundingBoxColor = Color.FromArgb(200, 40, 50), + RapidColor = Color.FromArgb(200, 150, 30), + OriginColor = Color.FromArgb(150, 130, 90), + EdgeSpacingColor = Color.FromArgb(215, 220, 215), + PreviewPartColor = Color.FromArgb(230, 60, 60), + PartColors = new[] + { + Color.FromArgb(215, 60, 60), // Holly Red + Color.FromArgb(80, 160, 90), // Pine + Color.FromArgb(230, 190, 70), // Gold + Color.FromArgb(150, 200, 235), // Ice + Color.FromArgb(190, 195, 205), // Silver + Color.FromArgb(240, 150, 160), // Candy Cane + Color.FromArgb(175, 120, 200), // Sugarplum + Color.FromArgb(150, 215, 170), // Mint + Color.FromArgb(190, 110, 70), // Cinnamon + Color.FromArgb(180, 50, 80), // Cranberry + Color.FromArgb(245, 225, 160), // Eggnog + Color.FromArgb(100, 150, 200), // Winter Sky + }, + }; + + private static ColorScheme BuildFourthOfJuly() => + new ColorScheme + { + Name = "4th of July", + BackgroundColor = Color.FromArgb(18, 28, 60), + LayoutOutlineColor = Color.FromArgb(110, 120, 150), + LayoutFillColor = Color.FromArgb(250, 250, 252), + BoundingBoxColor = Color.FromArgb(120, 150, 230), + RapidColor = Color.FromArgb(210, 40, 50), + OriginColor = Color.FromArgb(120, 130, 160), + EdgeSpacingColor = Color.FromArgb(205, 210, 225), + PreviewPartColor = Color.FromArgb(255, 190, 40), + PartColors = new[] + { + Color.FromArgb(220, 60, 60), // Old Glory Red + Color.FromArgb(110, 160, 230), // Liberty Blue + Color.FromArgb(250, 195, 60), // Firework Gold + Color.FromArgb(200, 205, 215), // Star Silver + Color.FromArgb(220, 90, 180), // Magenta Burst + Color.FromArgb(70, 190, 180), // Teal Burst + Color.FromArgb(240, 120, 110), // Coral + Color.FromArgb(150, 200, 240), // Sky + Color.FromArgb(250, 150, 60), // Sparkler + Color.FromArgb(170, 150, 230), // Twilight + Color.FromArgb(235, 150, 170), // Rose + Color.FromArgb(90, 120, 210), // Royal Blue + }, + }; + private static ColorScheme BuildClassic() => new ColorScheme { diff --git a/docs/nesting-view-colors.md b/docs/nesting-view-colors.md index 22991213..8a6a0866 100644 --- a/docs/nesting-view-colors.md +++ b/docs/nesting-view-colors.md @@ -36,6 +36,14 @@ classified as `Cut` remains a cut line. Drawing geometry, layer classifications, cutting plans and post-processor output are not changed by the renderer. The CAD conversion/program-editing views retain their own source/classification colors. +## Festive schemes + +**Halloween**, **Christmas** and **4th of July** are built-in schemes chosen like +any other; OpenNest never switches to them automatically. Their background, +sheet and accent colors carry the theme, while each 12-color part palette stays +varied, light enough for black part IDs and clear of the green etch stroke. +Etch, cut-outline and selection behavior match Classic. + ## Custom scheme files Scheme JSON files in `Schemes/` beside the application may specify `etchColor`