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>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-11 21:23:48 -05:00
1 parent a7302d3224
commit cc3711c11c
22 files changed
+830 -291

No files matched your search

@@ -0,0 +1,244 @@
using System.Drawing;
using OpenNest.Data;
namespace OpenNest.Desktop.Tests;
// Applying a scheme changes ColorScheme.Default and the drawing palette for the whole process.
[Collection(ColorSchemeState.Collection)]
public class ColorSchemeRegistryTests : IDisposable
{
private static readonly string[] BuiltInNames =
["Classic", "Pastel", "Dark", "Workshop", "Halloween", "Christmas", "4th of July"];
private readonly ColorSchemeState saved = new();
private readonly string directory = Path.Combine(Path.GetTempPath(), "opennest-schemes-" + Guid.NewGuid().ToString("N"));
public ColorSchemeRegistryTests() => Directory.CreateDirectory(directory);
public void Dispose()
{
saved.Restore();
Directory.Delete(directory, true);
}
[Fact]
public void BuiltInSchemesAreListedInOrder()
{
ColorSchemeRegistry.SchemesDirectory = directory;
Assert.Equal(BuiltInNames, ColorSchemeRegistry.AllSchemes.Select(s => s.Name));
}
[Theory]
[InlineData("Classic")]
[InlineData("Pastel")]
[InlineData("Dark")]
[InlineData("Workshop")]
[InlineData("Halloween")]
[InlineData("Christmas")]
[InlineData("4th of July")]
public void EveryBuiltInSchemeRoundTripsUnchanged(string name)
{
var scheme = ColorSchemeRegistry.Get(name);
var json = ColorSchemeSerializer.Serialize(scheme);
var restored = ColorSchemeSerializer.Deserialize(json);
Assert.Equal(Snapshot(scheme), Snapshot(restored));
Assert.Equal(json, ColorSchemeSerializer.Serialize(restored));
}
[Fact]
public void ASchemeFileInTheSchemesFolderLoads()
{
File.WriteAllText(Path.Combine(directory, "shop.json"), """
{
"name": "Shop Floor",
"backgroundColor": "#102030",
"layoutOutlineColor": "#405060",
"layoutFillColor": "#F0F0E0",
"boundingBoxColor": "#8080FF",
"rapidColor": "#1E90FF",
"originColor": "#808080",
"edgeSpacingColor": "#B4B4B4",
"previewPartColor": "#FF8C00",
"partOutlineColor": "#202020",
"partColors": ["#AA0000", "#00AA00"]
}
""");
ColorSchemeRegistry.SchemesDirectory = directory;
var scheme = ColorSchemeRegistry.Get("shop floor");
Assert.Equal("Shop Floor", scheme.Name);
Assert.Equal(Color.FromArgb(16, 32, 48), scheme.BackgroundColor);
Assert.Equal(Color.FromArgb(32, 32, 32), scheme.PartOutlineColor);
Assert.Equal([Color.FromArgb(170, 0, 0), Color.FromArgb(0, 170, 0)], scheme.PartColors);
// Missing entries keep their defaults, as WinForms reads them.
Assert.Equal(Color.Lime, scheme.EtchColor);
Assert.True(scheme.SelectedPartColor.IsEmpty);
Assert.Equal(new ColorScheme().LibraryFillColor, scheme.LibraryFillColor);
Assert.Equal(BuiltInNames.Append("Shop Floor"), ColorSchemeRegistry.AllSchemes.Select(s => s.Name));
}
[Fact]
public void BadSchemeFilesAreSkippedAndTheirNamesFallBackToClassic()
{
File.WriteAllText(Path.Combine(directory, "broken.json"), "{ \"name\": \"Broken\", ");
File.WriteAllText(Path.Combine(directory, "badhex.json"), "{ \"name\": \"Bad Hex\", \"backgroundColor\": \"#GG0000\" }");
File.WriteAllText(Path.Combine(directory, "null.json"), "null");
File.WriteAllText(Path.Combine(directory, "classic.json"), "{ \"name\": \"classic\", \"backgroundColor\": \"#FF0000\" }");
File.WriteAllText(Path.Combine(directory, "notes.txt"), "{ \"name\": \"Text\" }");
ColorSchemeRegistry.SchemesDirectory = directory;
Assert.Equal(BuiltInNames, ColorSchemeRegistry.AllSchemes.Select(s => s.Name));
Assert.Equal("Classic", ColorSchemeRegistry.Get("Broken").Name);
Assert.Equal("Classic", ColorSchemeRegistry.Get("Bad Hex").Name);
// A file cannot replace a built-in scheme.
Assert.Equal(Color.DarkGray, ColorSchemeRegistry.Get("Classic").BackgroundColor);
Assert.Equal("Classic", ColorSchemeRegistry.Get(null).Name);
Assert.Equal("Classic", ColorSchemeRegistry.Get(" ").Name);
}
[Fact]
public void AMissingSchemesFolderListsOnlyTheBuiltIns()
{
ColorSchemeRegistry.SchemesDirectory = Path.Combine(directory, "missing");
Assert.Equal(BuiltInNames, ColorSchemeRegistry.AllSchemes.Select(s => s.Name));
}
[Fact]
public void TheSavedSchemeNameResolvesFromTheSharedUserSettings()
{
var path = Path.Combine(directory, "user-settings.json");
UserSettingsFile.Write(path, new Dictionary<string, string>
{
["MainWindowState"] = "Normal",
[ColorSchemeRegistry.SettingName] = "Halloween",
});
var applied = ColorSchemeRegistry.ApplySaved(UserSettingsFile.Read(path));
Assert.Equal("ActiveColorScheme", ColorSchemeRegistry.SettingName);
Assert.Equal("Halloween", applied.Name);
Assert.Equal("Halloween", ColorScheme.Default.Name);
Assert.Equal(applied.BackgroundColor, ColorScheme.Default.BackgroundColor);
Assert.Equal(applied.PartColors, Drawing.PartColors);
}
[Theory]
[InlineData(null, "Classic")]
[InlineData("", "Classic")]
[InlineData("dark", "Dark")]
[InlineData("Gone Scheme", "Classic")]
public void TheSavedSchemeFallsBackToClassic(string? savedName, string expected)
{
var settings = new Dictionary<string, string>();
if (savedName != null)
settings[ColorSchemeRegistry.SettingName] = savedName;
Assert.Equal(savedName ?? "Classic", ColorSchemeRegistry.SavedSchemeName(settings));
Assert.Equal(expected, ColorSchemeRegistry.ApplySaved(settings).Name);
Assert.Equal(expected, ColorScheme.Default.Name);
}
[Fact]
public void ApplyingCopiesTheSchemeIntoTheDefaultAndRaisesApplied()
{
var workshop = ColorSchemeRegistry.Get("Workshop");
var defaultVersion = ColorScheme.Default.Version;
ColorScheme? raised = null;
EventHandler<ColorScheme> handler = (_, scheme) => raised = scheme;
ColorSchemeRegistry.Applied += handler;
try
{
ColorSchemeRegistry.Apply(workshop);
}
finally
{
ColorSchemeRegistry.Applied -= handler;
}
Assert.Same(workshop, raised);
Assert.Equal(Snapshot(workshop), Snapshot(ColorScheme.Default));
Assert.NotEqual(defaultVersion, ColorScheme.Default.Version);
Assert.True(Drawing.UseGoldenAngleColors);
Assert.Equal(PartColorPalette.GoldenAngle(30), Drawing.GetPartColor(30));
}
[Fact]
public void RecolorKeepsCustomColorsInTheirSlotsAndSkipsCutoffs()
{
ColorSchemeRegistry.Apply(ColorSchemeRegistry.Get("Classic"));
var nest = new Nest("Recolor");
var first = new Drawing("First") { Color = Color.Coral };
var custom = new Drawing("Custom") { Color = Color.Red, HasCustomColor = true };
var cutoff = new Drawing("Cutoff") { IsCutOff = true, Color = Color.Gray };
var third = new Drawing("Third") { Color = Color.Coral };
nest.Drawings.Add(first);
nest.Drawings.Add(custom);
nest.Drawings.Add(cutoff);
nest.Drawings.Add(third);
ColorSchemeRegistry.RecolorDrawings(nest);
var classic = ColorSchemeRegistry.Get("Classic").PartColors;
Assert.Equal(classic[0], first.Color);
Assert.Equal(Color.Red, custom.Color);
Assert.Equal(Color.Gray, cutoff.Color);
Assert.Equal(classic[2], third.Color);
}
[Fact]
public void RecolorFollowsTheActiveSchemesPalette()
{
var nest = new Nest("Recolor");
var drawings = Enumerable.Range(0, 14).Select(i => new Drawing($"D{i}") { Color = Color.Coral }).ToList();
foreach (var drawing in drawings)
nest.Drawings.Add(drawing);
ColorSchemeRegistry.Apply(ColorSchemeRegistry.Get("Dark"));
ColorSchemeRegistry.RecolorDrawings(nest);
var dark = ColorSchemeRegistry.Get("Dark").PartColors;
Assert.Equal(Enumerable.Range(0, 14).Select(i => dark[i % dark.Length]), drawings.Select(d => d.Color));
ColorSchemeRegistry.Apply(ColorSchemeRegistry.Get("Workshop"));
ColorSchemeRegistry.RecolorDrawings(nest);
Assert.Equal(Enumerable.Range(0, 14).Select(PartColorPalette.GoldenAngle), drawings.Select(d => d.Color));
}
private static string Snapshot(ColorScheme s) => string.Join(";",
s.Name, s.BackgroundColor.ToArgb(), s.LayoutOutlineColor.ToArgb(), s.LayoutFillColor.ToArgb(),
s.BoundingBoxColor.ToArgb(), s.RapidColor.ToArgb(), s.OriginColor.ToArgb(), s.EdgeSpacingColor.ToArgb(),
s.PreviewPartColor.ToArgb(), s.PartOutlineColor.IsEmpty ? "empty" : s.PartOutlineColor.ToArgb().ToString(),
s.SelectedPartColor.IsEmpty ? "empty" : s.SelectedPartColor.ToArgb().ToString(), s.EtchColor.ToArgb(),
s.LibraryBackgroundColor.ToArgb(), s.LibraryFillColor.ToArgb(), s.LibraryOutlineColor.ToArgb(),
s.LibraryEtchColor.ToArgb(), s.LibraryBendColor.ToArgb(), s.UseGoldenAngleColors,
string.Join(",", s.PartColors.Select(c => c.ToArgb())));
}
[CollectionDefinition(ColorSchemeState.Collection, DisableParallelization = true)]
public sealed class ColorSchemeStateCollection
{
}
/// <summary>The process-wide scheme state a test changes, restored afterwards.</summary>
public sealed class ColorSchemeState
{
public const string Collection = "Colour scheme state";
private readonly ColorScheme previous =
ColorSchemeSerializer.Deserialize(ColorSchemeSerializer.Serialize(ColorScheme.Default));
private readonly Color[] previousPartColors = Drawing.PartColors;
private readonly bool previousGoldenAngle = Drawing.UseGoldenAngleColors;
private readonly string previousDirectory = ColorSchemeRegistry.SchemesDirectory;
public void Restore()
{
ColorSchemeRegistry.Apply(previous);
Drawing.PartColors = previousPartColors;
Drawing.UseGoldenAngleColors = previousGoldenAngle;
ColorSchemeRegistry.SchemesDirectory = previousDirectory;
}
}
+39
View File
@@ -0,0 +1,39 @@
using System.Drawing;
namespace OpenNest.Desktop.Tests;
public class ColorShadeTests
{
// Expected values from an independent transcription of WinForms' ControlPaint.HLSColor;
// OpenNest.WinForms.Tests compares against ControlPaint.Dark itself on Windows.
[Theory]
[InlineData(255, 127, 80, 113, 31, 0)]
[InlineData(0, 0, 0, 0, 0, 0)]
[InlineData(255, 255, 255, 85, 85, 85)]
[InlineData(205, 92, 92, 77, 23, 23)]
[InlineData(128, 128, 128, 42, 42, 42)]
[InlineData(90, 150, 200, 23, 50, 72)]
[InlineData(255, 0, 0, 85, 0, 0)]
[InlineData(12, 34, 250, 2, 11, 85)]
public void DarkMatchesTheWinFormsShade(int r, int g, int b, int er, int eg, int eb)
{
Assert.Equal(Color.FromArgb(er, eg, eb), ColorShade.Dark(Color.FromArgb(r, g, b)));
}
[Fact]
public void DarkIgnoresAlphaAndReturnsAnOpaqueColor()
{
// The WinForms selection fill: alpha 90 over (150, 200, 255).
Assert.Equal(Color.FromArgb(0, 65, 136), ColorShade.Dark(Color.FromArgb(90, 150, 200, 255)));
}
[Fact]
public void SchemesWithoutAPartOutlineDarkenTheFill()
{
var classic = ColorSchemeRegistry.Get("Classic");
var workshop = ColorSchemeRegistry.Get("Workshop");
Assert.Equal(Color.FromArgb(113, 31, 0), classic.GetPartOutlineColor(Color.Coral));
Assert.Equal(Color.FromArgb(48, 48, 48), workshop.GetPartOutlineColor(Color.Coral));
}
}
@@ -1,6 +1,6 @@
using System.Drawing;
namespace OpenNest.WinForms.Tests;
namespace OpenNest.Desktop.Tests;
public class FestiveColorSchemeTests
{
+178
View File
@@ -0,0 +1,178 @@
using System.Drawing;
using System.Threading;
namespace OpenNest
{
/// <summary>
/// A colour scheme's colours: the plate view, its parts and the library previews. Pure data
/// shared by both desktop front ends; each draws with its own cache of pens or paints, which
/// it rebuilds when <see cref="Version"/> changes.
/// </summary>
public sealed class ColorScheme
{
private int version;
private Color backgroundColor;
private Color layoutOutlineColor;
private Color layoutFillColor;
private Color boundingBoxColor;
private Color rapidColor;
private Color originColor;
private Color edgeSpacingColor;
private Color previewPartColor;
private Color[] partColors = Drawing.PartColors;
private bool useGoldenAngleColors;
private Color partOutlineColor = Color.Empty;
private Color selectedPartColor = Color.Empty;
private Color etchColor = Color.Lime;
private Color libraryBackgroundColor = Color.White;
private Color libraryFillColor = Color.FromArgb(214, 219, 226);
private Color libraryOutlineColor = Color.FromArgb(64, 70, 80);
private Color libraryEtchColor = Color.FromArgb(0, 150, 60);
private Color libraryBendColor = Color.FromArgb(40, 100, 210);
public string Name { get; set; } = "Unnamed";
/// <summary>
/// Changes whenever a colour does, so a front end's pen or paint cache knows to rebuild.
/// </summary>
public int Version => Volatile.Read(ref version);
public Color[] PartColors
{
get => partColors;
set => Set(ref partColors, value);
}
public bool UseGoldenAngleColors
{
get => useGoldenAngleColors;
set => Set(ref useGoldenAngleColors, value);
}
// Empty preserves the legacy outline derived from each part's fill.
public Color PartOutlineColor
{
get => partOutlineColor;
set => Set(ref partOutlineColor, value);
}
// Empty preserves the legacy translucent selection fill.
public Color SelectedPartColor
{
get => selectedPartColor;
set => Set(ref selectedPartColor, value);
}
public Color EtchColor
{
get => etchColor;
set => Set(ref etchColor, value);
}
// Library previews (Shape Library, drawing library, drawing set preview) show drawings
// that are not in the nest yet, so they share one style instead of nest part colors.
public Color LibraryBackgroundColor
{
get => libraryBackgroundColor;
set => Set(ref libraryBackgroundColor, value);
}
public Color LibraryFillColor
{
get => libraryFillColor;
set => Set(ref libraryFillColor, value);
}
public Color LibraryOutlineColor
{
get => libraryOutlineColor;
set => Set(ref libraryOutlineColor, value);
}
public Color LibraryEtchColor
{
get => libraryEtchColor;
set => Set(ref libraryEtchColor, value);
}
public Color LibraryBendColor
{
get => libraryBendColor;
set => Set(ref libraryBendColor, value);
}
/// <summary>
/// The scheme's part outline or, when it has none, the legacy outline: the fill darkened
/// as WinForms' <c>ControlPaint.Dark</c> does.
/// </summary>
public Color GetPartOutlineColor(Color fill) =>
PartOutlineColor.IsEmpty ? ColorShade.Dark(fill) : PartOutlineColor;
public static readonly ColorScheme Default = new ColorScheme
{
Name = "Classic",
BackgroundColor = Color.DarkGray,
LayoutOutlineColor = Color.Gray,
LayoutFillColor = Color.WhiteSmoke,
BoundingBoxColor = Color.FromArgb(128, 128, 255),
RapidColor = Color.DodgerBlue,
OriginColor = Color.Gray,
EdgeSpacingColor = Color.FromArgb(180, 180, 180),
PreviewPartColor = Color.FromArgb(255, 140, 0),
};
public Color BackgroundColor
{
get => backgroundColor;
set => Set(ref backgroundColor, value);
}
public Color LayoutOutlineColor
{
get => layoutOutlineColor;
set => Set(ref layoutOutlineColor, value);
}
public Color LayoutFillColor
{
get => layoutFillColor;
set => Set(ref layoutFillColor, value);
}
public Color BoundingBoxColor
{
get => boundingBoxColor;
set => Set(ref boundingBoxColor, value);
}
public Color RapidColor
{
get => rapidColor;
set => Set(ref rapidColor, value);
}
public Color OriginColor
{
get => originColor;
set => Set(ref originColor, value);
}
public Color EdgeSpacingColor
{
get => edgeSpacingColor;
set => Set(ref edgeSpacingColor, value);
}
public Color PreviewPartColor
{
get => previewPartColor;
set => Set(ref previewPartColor, value);
}
private void Set<T>(ref T field, T value)
{
field = value;
Interlocked.Increment(ref version);
}
}
}
@@ -3,14 +3,21 @@ using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using OpenNest.Forms;
using OpenNest.Properties;
namespace OpenNest
{
/// <summary>
/// The built-in colour schemes plus any scheme JSON files in <see cref="SchemesDirectory"/>,
/// and the active scheme, which lives in <see cref="ColorScheme.Default"/>.
/// </summary>
public static class ColorSchemeRegistry
{
/// <summary>The user setting that names the chosen scheme, as the WinForms app stores it.</summary>
public const string SettingName = "ActiveColorScheme";
/// <summary>The scheme used when none is chosen, or the chosen one no longer exists.</summary>
public const string DefaultSchemeName = "Classic";
private static readonly Dictionary<string, ColorScheme> builtIns = new(
StringComparer.OrdinalIgnoreCase
)
@@ -25,6 +32,27 @@ namespace OpenNest
};
private static List<ColorScheme> diskCache;
private static string schemesDirectory = Path.Combine(AppContext.BaseDirectory, "Schemes");
/// <summary>
/// Raised after <see cref="Apply"/> copies a scheme into <see cref="ColorScheme.Default"/>
/// and the drawing palette, so each front end can recolour its open nests and repaint.
/// </summary>
public static event EventHandler<ColorScheme> Applied;
/// <summary>
/// The folder of scheme JSON files: Schemes beside the application. A file that does not
/// parse is skipped, and one named like a built-in scheme never replaces it.
/// </summary>
public static string SchemesDirectory
{
get => schemesDirectory;
set
{
schemesDirectory = value;
Refresh();
}
}
public static IEnumerable<ColorScheme> AllSchemes
{
@@ -40,19 +68,27 @@ namespace OpenNest
public static ColorScheme Get(string name)
{
if (string.IsNullOrWhiteSpace(name))
return builtIns["Classic"];
return builtIns[DefaultSchemeName];
var hit = AllSchemes.FirstOrDefault(s =>
string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase)
);
return hit ?? builtIns["Classic"];
return hit ?? builtIns[DefaultSchemeName];
}
public static void ApplyActiveFromSettings()
/// <summary>
/// The scheme name saved in the user settings (<see cref="SettingName"/>), or the default
/// scheme's name when none is saved, as the WinForms setting defaults.
/// </summary>
public static string SavedSchemeName(IReadOnlyDictionary<string, string> settings) =>
settings != null && settings.TryGetValue(SettingName, out var name) ? name : DefaultSchemeName;
/// <summary>Applies the scheme saved in the user settings.</summary>
public static ColorScheme ApplySaved(IReadOnlyDictionary<string, string> settings)
{
var name = Settings.Default.ActiveColorScheme;
var scheme = Get(name);
var scheme = Get(SavedSchemeName(settings));
Apply(scheme);
return scheme;
}
public static void Apply(ColorScheme scheme)
@@ -81,20 +117,13 @@ namespace OpenNest
Drawing.PartColors = scheme.PartColors;
Drawing.UseGoldenAngleColors = scheme.UseGoldenAngleColors;
RecolorOpenNests();
}
private static void RecolorOpenNests()
{
foreach (Form f in Application.OpenForms)
{
if (f is EditNestForm enf)
RecolorDrawings(enf.Nest);
}
Applied?.Invoke(null, scheme);
}
/// <summary>
/// Gives each non-cutoff drawing its palette color by position. Custom colors are kept,
/// Gives each non-cutoff drawing its palette color by position: on opening a nest, so the
/// view follows the application's scheme rather than the one the file was saved under,
/// and when a scheme is applied. Custom colors are kept,
/// but still take their slot so the other drawings' colors don't shift around them.
/// </summary>
public static void RecolorDrawings(Nest nest)
@@ -112,8 +141,8 @@ namespace OpenNest
private static IEnumerable<ColorScheme> LoadDiskSchemes()
{
var dir = Path.Combine(AppContext.BaseDirectory, "Schemes");
if (!Directory.Exists(dir))
var dir = schemesDirectory;
if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir))
yield break;
foreach (var path in Directory.GetFiles(dir, "*.json"))
File renamed without changes.
+122
View File
@@ -0,0 +1,122 @@
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;
}
}
}
+6
View File
@@ -15,4 +15,10 @@
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
<ProjectReference Include="..\OpenNest.Reporting\OpenNest.Reporting.csproj" />
</ItemGroup>
<ItemGroup>
<!-- Colour scheme files; they copy beside both desktop apps, which read them from Schemes. -->
<None Update="Schemes\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
File renamed without changes.
@@ -35,7 +35,7 @@ public class OutlinedPlateDisplayTests
view.Renderer.DrawPlate(g);
var fill = ((SolidBrush)view.ColorScheme.LayoutFillBrush).Color;
var fill = ((SolidBrush)view.ColorScheme.Gdi().LayoutFillBrush).Color;
Assert.Equal(Color.White.ToArgb(), image.GetPixel(75, 25).ToArgb()); // the missing corner
Assert.Equal(fill.ToArgb(), image.GetPixel(25, 75).ToArgb()); // the sheet
Assert.Equal(fill.ToArgb(), image.GetPixel(75, 75).ToArgb()); // the arm
@@ -6,36 +6,6 @@ namespace OpenNest.WinForms.Tests;
[Collection("Fill operation lifetime")]
public class CustomDrawingColorTests
{
[Fact]
public void RecolorKeepsCustomColorsInTheirSlotsAndSkipsCutoffs()
{
var previous = ColorSchemeSerializer.Deserialize(ColorSchemeSerializer.Serialize(ColorScheme.Default));
try
{
ColorSchemeRegistry.Apply(ColorSchemeRegistry.Get("Classic"));
var nest = new Nest("Recolor");
var first = new Drawing("First") { Color = Color.Coral };
var custom = new Drawing("Custom") { Color = Color.Red, HasCustomColor = true };
var cutoff = new Drawing("Cutoff") { IsCutOff = true, Color = Color.Gray };
var third = new Drawing("Third") { Color = Color.Coral };
nest.Drawings.Add(first);
nest.Drawings.Add(custom);
nest.Drawings.Add(cutoff);
nest.Drawings.Add(third);
ColorSchemeRegistry.RecolorDrawings(nest);
Assert.Equal(Drawing.GetPartColor(0), first.Color);
Assert.Equal(Color.Red, custom.Color);
Assert.Equal(Color.Gray, cutoff.Color);
Assert.Equal(Drawing.GetPartColor(2), third.Color);
}
finally
{
ColorSchemeRegistry.Apply(previous);
}
}
[Fact]
public void DrawingPropertiesMarksOnlyAChangedColorAsCustom() => StaTestThread.Run(() =>
{
@@ -0,0 +1,57 @@
using System.Drawing;
using System.Windows.Forms;
namespace OpenNest.WinForms.Tests;
public class GdiSchemeToolsTests
{
[Fact]
public void SharedShadeMatchesControlPaintDark()
{
var colors = new List<Color> { SystemColors.Control, Color.Coral, Color.White, Color.Black };
for (var r = 0; r < 256; r += 15)
{
for (var g = 0; g < 256; g += 17)
{
for (var b = 0; b < 256; b += 13)
colors.Add(Color.FromArgb(r, g, b));
}
}
Assert.All(colors, color => Assert.Equal(ControlPaint.Dark(color), ColorShade.Dark(color)));
Assert.All(colors, color => Assert.Equal(ControlPaint.Dark(color, 0.25f), ColorShade.Dark(color, 0.25f)));
}
[Fact]
public void PensFollowTheSchemeAndAreRebuiltAfterAColorChange()
{
var scheme = ColorSchemeSerializer.Deserialize(ColorSchemeSerializer.Serialize(ColorSchemeRegistry.Get("Classic")));
var tools = scheme.Gdi();
Assert.Same(tools, scheme.Gdi());
Assert.Equal(scheme.RapidColor.ToArgb(), tools.RapidPen.Color.ToArgb());
Assert.Equal(Color.FromArgb(30, scheme.PreviewPartColor).ToArgb(),
((SolidBrush)tools.ActivePreviewPartBrush).Color.ToArgb());
scheme.LayoutFillColor = Color.Coral;
var rebuilt = scheme.Gdi();
Assert.NotSame(tools, rebuilt);
Assert.Equal(Color.Coral.ToArgb(), ((SolidBrush)rebuilt.LayoutFillBrush).Color.ToArgb());
}
[Fact]
public void EachThreadDrawsWithItsOwnPens()
{
var scheme = ColorSchemeRegistry.Get("Workshop");
var here = scheme.Gdi();
GdiSchemeTools? other = null;
var thread = new Thread(() => other = scheme.Gdi());
thread.Start();
thread.Join();
Assert.NotNull(other);
Assert.NotSame(here, other);
Assert.NotSame(here.LayoutOutlinePen, other.LayoutOutlinePen);
}
}
-217
View File
@@ -1,217 +0,0 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Threading;
namespace OpenNest
{
public sealed class ColorScheme
{
private Color layoutOutlineColor;
private Color layoutFillColor;
private Color boundingBoxColor;
private Color rapidColor;
private Color originColor;
private Color edgeSpacingColor;
private Color previewPartColor;
public string Name { get; set; } = "Unnamed";
public Color[] PartColors { get; set; } = Drawing.PartColors;
public bool UseGoldenAngleColors { get; set; }
// Empty preserves the legacy outline derived from each part's fill.
public Color PartOutlineColor { get; set; } = Color.Empty;
// Empty preserves the legacy translucent selection fill.
public Color SelectedPartColor { get; set; } = Color.Empty;
public Color EtchColor { get; set; } = Color.Lime;
// Library previews (Shape Library, drawing library, drawing set preview) show drawings
// that are not in the nest yet, so they share one style instead of nest part colors.
public Color LibraryBackgroundColor { get; set; } = Color.White;
public Color LibraryFillColor { get; set; } = Color.FromArgb(214, 219, 226);
public Color LibraryOutlineColor { get; set; } = Color.FromArgb(64, 70, 80);
public Color LibraryEtchColor { get; set; } = Color.FromArgb(0, 150, 60);
public Color LibraryBendColor { get; set; } = Color.FromArgb(40, 100, 210);
public Color GetPartOutlineColor(Color fill) =>
PartOutlineColor.IsEmpty ? System.Windows.Forms.ControlPaint.Dark(fill) : PartOutlineColor;
public static readonly ColorScheme Default = new ColorScheme
{
Name = "Classic",
BackgroundColor = Color.DarkGray,
LayoutOutlineColor = Color.Gray,
LayoutFillColor = Color.WhiteSmoke,
BoundingBoxColor = Color.FromArgb(128, 128, 255),
RapidColor = Color.DodgerBlue,
OriginColor = Color.Gray,
EdgeSpacingColor = Color.FromArgb(180, 180, 180),
PreviewPartColor = Color.FromArgb(255, 140, 0),
};
#region Pens/Brushes
// GDI+ objects are not thread-safe, and views on different UI threads share a
// scheme (Default), so each thread draws with its own set, rebuilt after a color
// change. Two views painting at once otherwise fail with "Object is currently in
// use elsewhere".
private readonly ThreadLocal<DrawingTools> tools = new();
private int toolsVersion;
private DrawingTools Tools
{
get
{
var current = tools.Value;
if (current == null || current.Version != toolsVersion)
{
current?.Dispose();
current = new DrawingTools(this, toolsVersion);
tools.Value = current;
}
return current;
}
}
public Pen LayoutOutlinePen => Tools.LayoutOutlinePen;
public Brush LayoutFillBrush => Tools.LayoutFillBrush;
public Pen BoundingBoxPen => Tools.BoundingBoxPen;
public Pen RapidPen => Tools.RapidPen;
public Pen OriginPen => Tools.OriginPen;
public Pen EdgeSpacingPen => Tools.EdgeSpacingPen;
public Pen PreviewPartPen => Tools.PreviewPartPen;
public Brush PreviewPartBrush => Tools.PreviewPartBrush;
public Pen ActivePreviewPartPen => Tools.ActivePreviewPartPen;
public Brush ActivePreviewPartBrush => Tools.ActivePreviewPartBrush;
private sealed class DrawingTools : IDisposable
{
public DrawingTools(ColorScheme scheme, int version)
{
Version = version;
LayoutOutlinePen = new Pen(scheme.layoutOutlineColor);
LayoutFillBrush = new SolidBrush(scheme.layoutFillColor);
BoundingBoxPen = new Pen(scheme.boundingBoxColor);
RapidPen = new Pen(scheme.rapidColor)
{
DashPattern = new float[] { 10, 10 },
DashCap = DashCap.Flat,
};
OriginPen = new Pen(scheme.originColor);
EdgeSpacingPen = new Pen(scheme.edgeSpacingColor)
{
DashPattern = new float[] { 3, 3 },
DashCap = DashCap.Flat,
};
PreviewPartPen = new Pen(scheme.previewPartColor, 1);
PreviewPartBrush = new SolidBrush(Color.FromArgb(60, scheme.previewPartColor));
ActivePreviewPartPen = new Pen(Color.FromArgb(128, scheme.previewPartColor), 1);
ActivePreviewPartBrush = new SolidBrush(Color.FromArgb(30, scheme.previewPartColor));
}
public int Version { get; }
public Pen LayoutOutlinePen { get; }
public Brush LayoutFillBrush { get; }
public Pen BoundingBoxPen { get; }
public Pen RapidPen { get; }
public Pen OriginPen { get; }
public Pen EdgeSpacingPen { get; }
public Pen PreviewPartPen { get; }
public Brush PreviewPartBrush { get; }
public Pen ActivePreviewPartPen { get; }
public Brush ActivePreviewPartBrush { get; }
public void Dispose()
{
LayoutOutlinePen.Dispose();
LayoutFillBrush.Dispose();
BoundingBoxPen.Dispose();
RapidPen.Dispose();
OriginPen.Dispose();
EdgeSpacingPen.Dispose();
PreviewPartPen.Dispose();
PreviewPartBrush.Dispose();
ActivePreviewPartPen.Dispose();
ActivePreviewPartBrush.Dispose();
}
}
#endregion Pens/Brushes
#region Colors
public Color BackgroundColor { get; set; }
public Color LayoutOutlineColor
{
get { return layoutOutlineColor; }
set { layoutOutlineColor = value; Interlocked.Increment(ref toolsVersion); }
}
public Color LayoutFillColor
{
get { return layoutFillColor; }
set { layoutFillColor = value; Interlocked.Increment(ref toolsVersion); }
}
public Color BoundingBoxColor
{
get { return boundingBoxColor; }
set { boundingBoxColor = value; Interlocked.Increment(ref toolsVersion); }
}
public Color RapidColor
{
get { return rapidColor; }
set { rapidColor = value; Interlocked.Increment(ref toolsVersion); }
}
public Color OriginColor
{
get { return originColor; }
set { originColor = value; Interlocked.Increment(ref toolsVersion); }
}
public Color EdgeSpacingColor
{
get { return edgeSpacingColor; }
set { edgeSpacingColor = value; Interlocked.Increment(ref toolsVersion); }
}
public Color PreviewPartColor
{
get { return previewPartColor; }
set { previewPartColor = value; Interlocked.Increment(ref toolsVersion); }
}
#endregion Colors
}
}
+8 -8
View File
@@ -92,14 +92,14 @@ namespace OpenNest.Controls
plateRect.Y -= plateRect.Height;
edgeSpacingRect.Y -= edgeSpacingRect.Height;
g.FillRectangle(view.ColorScheme.LayoutFillBrush, plateRect);
g.FillRectangle(view.ColorScheme.Gdi().LayoutFillBrush, plateRect);
var viewBounds = view.GetViewBounds();
if (!edgeSpacingRect.Contains(viewBounds))
{
g.DrawRectangle(
view.ColorScheme.EdgeSpacingPen,
view.ColorScheme.Gdi().EdgeSpacingPen,
edgeSpacingRect.X,
edgeSpacingRect.Y,
edgeSpacingRect.Width,
@@ -108,7 +108,7 @@ namespace OpenNest.Controls
}
g.DrawRectangle(
view.ColorScheme.LayoutOutlinePen,
view.ColorScheme.Gdi().LayoutOutlinePen,
plateRect.X,
plateRect.Y,
plateRect.Width,
@@ -259,7 +259,7 @@ namespace OpenNest.Controls
sheet.AddPolygon(ToGraph(outline.Points, origin));
foreach (var obstacle in outline.Obstacles)
sheet.AddPolygon(ToGraph(obstacle, origin));
g.FillPath(view.ColorScheme.LayoutFillBrush, sheet);
g.FillPath(view.ColorScheme.Gdi().LayoutFillBrush, sheet);
if (outline.Obstacles.Count > 0)
{
@@ -277,10 +277,10 @@ namespace OpenNest.Controls
System.Math.Max(plate.EdgeSpacing.Top, plate.EdgeSpacing.Bottom));
if (edge > 0)
foreach (var path in outline.Usable(edge, origin))
g.DrawPolygon(view.ColorScheme.EdgeSpacingPen,
g.DrawPolygon(view.ColorScheme.Gdi().EdgeSpacingPen,
path.Select(p => view.PointWorldToGraph(p.x, p.y)).ToArray());
g.DrawPath(view.ColorScheme.LayoutOutlinePen, sheet);
g.DrawPath(view.ColorScheme.Gdi().LayoutOutlinePen, sheet);
}
private PointF[] ToGraph(IReadOnlyList<Vector> points, Vector origin) =>
@@ -684,7 +684,7 @@ namespace OpenNest.Controls
private void DrawRapids(Graphics g)
{
var pen = view.ColorScheme.RapidPen;
var pen = view.ColorScheme.Gdi().RapidPen;
foreach (var segment in RapidEnumerator.Enumerate(view.DisplayPlate.Parts))
DrawLine(g, segment.From, segment.To, pen);
}
@@ -782,7 +782,7 @@ namespace OpenNest.Controls
g.DrawLine(pen, point1, point2);
}
private void DrawBox(Graphics g, Box box) => DrawBox(g, box, view.ColorScheme.BoundingBoxPen);
private void DrawBox(Graphics g, Box box) => DrawBox(g, box, view.ColorScheme.Gdi().BoundingBoxPen);
private void DrawBox(Graphics g, Box box, Pen pen)
{
+2 -2
View File
@@ -805,8 +805,8 @@ namespace OpenNest.Controls
if (DrawOrigin)
{
e.Graphics.DrawLine(ColorScheme.OriginPen, origin.X, 0, origin.X, Height);
e.Graphics.DrawLine(ColorScheme.OriginPen, 0, origin.Y, Width, origin.Y);
e.Graphics.DrawLine(ColorScheme.Gdi().OriginPen, origin.X, 0, origin.X, Height);
e.Graphics.DrawLine(ColorScheme.Gdi().OriginPen, 0, origin.Y, Width, origin.Y);
}
e.Graphics.TranslateTransform(origin.X, origin.Y);
+2 -2
View File
@@ -15,9 +15,9 @@ namespace OpenNest.Controls
public IReadOnlyList<LayoutPart> PreviewParts => activeParts;
public Brush PreviewBrush => view.ColorScheme.ActivePreviewPartBrush;
public Brush PreviewBrush => view.ColorScheme.Gdi().ActivePreviewPartBrush;
public Pen PreviewPen => view.ColorScheme.ActivePreviewPartPen;
public Pen PreviewPen => view.ColorScheme.Gdi().ActivePreviewPartPen;
public void SetActiveParts(List<Part> parts)
{
+2 -2
View File
@@ -161,7 +161,7 @@ namespace OpenNest.Controls
var sheetHeight = (float)(tile.Sheet.Width * scale);
var left = x + (width - sheetWidth) / 2;
var sheetRect = new RectangleF(left, baseline - sheetHeight, sheetWidth, sheetHeight);
g.FillRectangle(colorScheme.LayoutFillBrush, sheetRect);
g.FillRectangle(colorScheme.Gdi().LayoutFillBrush, sheetRect);
var state = g.Save();
// World Y grows up; the sheet's lower-left corner sits at the rectangle's bottom-left.
@@ -177,7 +177,7 @@ namespace OpenNest.Controls
}
g.Restore(state);
g.DrawRectangle(colorScheme.LayoutOutlinePen, sheetRect.X, sheetRect.Y, sheetRect.Width, sheetRect.Height);
g.DrawRectangle(colorScheme.Gdi().LayoutOutlinePen, sheetRect.X, sheetRect.Y, sheetRect.Width, sheetRect.Height);
var caption = new Rectangle(x, (int)baseline + 4, width, CaptionHeight);
TextRenderer.DrawText(g, tile.Title + "\n" + tile.Detail, Font, caption, ForeColor,
TextFormatFlags.HorizontalCenter | TextFormatFlags.Top);
+14
View File
@@ -453,6 +453,20 @@ namespace OpenNest.Forms
};
}
// Applying a colour scheme recolours the drawings of every open nest window. Subscribed
// once, before any nest window can exist.
static EditNestForm()
{
ColorSchemeRegistry.Applied += (_, _) =>
{
foreach (Form f in Application.OpenForms)
{
if (f is EditNestForm enf)
ColorSchemeRegistry.RecolorDrawings(enf.Nest);
}
};
}
public EditNestForm(Nest nest)
: this()
{
+1 -1
View File
@@ -162,7 +162,7 @@ namespace OpenNest.Forms
Shown += (_, _) => statusLabel1.Text = engineWarning;
OptionsForm.ApplyDisabledStrategies();
ColorSchemeRegistry.ApplyActiveFromSettings();
ColorSchemeRegistry.Apply(ColorSchemeRegistry.Get(Settings.Default.ActiveColorScheme));
foreach (var name in EngineSelection.UiEngineNames)
engineComboBox.Items.Add(name);
+1 -1
View File
@@ -141,7 +141,7 @@ namespace OpenNest.Forms
Settings.Default.Save();
ApplyDisabledStrategies();
ColorSchemeRegistry.ApplyActiveFromSettings();
ColorSchemeRegistry.Apply(ColorSchemeRegistry.Get(Settings.Default.ActiveColorScheme));
foreach (Form f in Application.OpenForms)
f.Invalidate(invalidateChildren: true);
+102
View File
@@ -0,0 +1,102 @@
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.CompilerServices;
using System.Threading;
namespace OpenNest
{
/// <summary>
/// The GDI+ pens and brushes the WinForms views draw a <see cref="ColorScheme"/> with.
/// </summary>
/// <remarks>
/// GDI+ objects are not thread-safe, and views on different UI threads share a scheme
/// (<see cref="ColorScheme.Default"/>), so each thread draws with its own set, rebuilt after
/// a colour change. Two views painting at once otherwise fail with "Object is currently in
/// use elsewhere".
/// </remarks>
public sealed class GdiSchemeTools
{
private static readonly ConditionalWeakTable<ColorScheme, ThreadLocal<GdiSchemeTools>> Cache = new();
private GdiSchemeTools(ColorScheme scheme)
{
Version = scheme.Version;
LayoutOutlinePen = new Pen(scheme.LayoutOutlineColor);
LayoutFillBrush = new SolidBrush(scheme.LayoutFillColor);
BoundingBoxPen = new Pen(scheme.BoundingBoxColor);
RapidPen = new Pen(scheme.RapidColor)
{
DashPattern = new float[] { 10, 10 },
DashCap = DashCap.Flat,
};
OriginPen = new Pen(scheme.OriginColor);
EdgeSpacingPen = new Pen(scheme.EdgeSpacingColor)
{
DashPattern = new float[] { 3, 3 },
DashCap = DashCap.Flat,
};
PreviewPartPen = new Pen(scheme.PreviewPartColor, 1);
PreviewPartBrush = new SolidBrush(Color.FromArgb(60, scheme.PreviewPartColor));
ActivePreviewPartPen = new Pen(Color.FromArgb(128, scheme.PreviewPartColor), 1);
ActivePreviewPartBrush = new SolidBrush(Color.FromArgb(30, scheme.PreviewPartColor));
}
/// <summary>This thread's pens and brushes for <paramref name="scheme"/>, current with its colours.</summary>
public static GdiSchemeTools For(ColorScheme scheme)
{
var perThread = Cache.GetValue(scheme, _ => new ThreadLocal<GdiSchemeTools>());
var current = perThread.Value;
if (current == null || current.Version != scheme.Version)
{
current?.Release();
current = new GdiSchemeTools(scheme);
perThread.Value = current;
}
return current;
}
private int Version { get; }
public Pen LayoutOutlinePen { get; }
public Brush LayoutFillBrush { get; }
public Pen BoundingBoxPen { get; }
public Pen RapidPen { get; }
public Pen OriginPen { get; }
public Pen EdgeSpacingPen { get; }
public Pen PreviewPartPen { get; }
public Brush PreviewPartBrush { get; }
public Pen ActivePreviewPartPen { get; }
public Brush ActivePreviewPartBrush { get; }
// Only the cache that made them releases them, once this thread has built newer ones.
private void Release()
{
LayoutOutlinePen.Dispose();
LayoutFillBrush.Dispose();
BoundingBoxPen.Dispose();
RapidPen.Dispose();
OriginPen.Dispose();
EdgeSpacingPen.Dispose();
PreviewPartPen.Dispose();
PreviewPartBrush.Dispose();
ActivePreviewPartPen.Dispose();
ActivePreviewPartBrush.Dispose();
}
}
public static class GdiSchemeToolsExtensions
{
/// <summary>This thread's GDI+ pens and brushes for the scheme.</summary>
public static GdiSchemeTools Gdi(this ColorScheme scheme) => GdiSchemeTools.For(scheme);
}
}
-5
View File
@@ -17,11 +17,6 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="Schemes\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />