mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-14 03:25:07 -05:00
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:
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;
|
||||
}
|
||||
}
|
||||
@@ -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
-1
@@ -1,6 +1,6 @@
|
||||
using System.Drawing;
|
||||
|
||||
namespace OpenNest.WinForms.Tests;
|
||||
namespace OpenNest.Desktop.Tests;
|
||||
|
||||
public class FestiveColorSchemeTests
|
||||
{
|
||||
@@ -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.
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Reference in new issue
Block a user