diff --git a/OpenNest.Avalonia/Controls/PlateCanvas.cs b/OpenNest.Avalonia/Controls/PlateCanvas.cs
index 5704acad..f39e98f9 100644
--- a/OpenNest.Avalonia/Controls/PlateCanvas.cs
+++ b/OpenNest.Avalonia/Controls/PlateCanvas.cs
@@ -16,18 +16,13 @@ using Vector = OpenNest.Geometry.Vector;
namespace OpenNest.Avalonia.Controls
{
///
- /// Draws one plate with Skia: pans with a left or middle drag, zooms at the cursor with the
- /// wheel, fits on a double click, and highlights the part under the cursor.
+ /// Draws one plate with Skia in the colours of : pans with a left or
+ /// middle drag, zooms at the cursor with the wheel, fits on a double click, and highlights the
+ /// part under the cursor.
///
public sealed class PlateCanvas : Control
{
- private static readonly SKColor BackgroundColor = new(169, 169, 169);
- private static readonly SKColor PlateFillColor = new(245, 245, 245);
- private static readonly SKColor PlateOutlineColor = new(128, 128, 128);
- private static readonly SKColor EdgeSpacingColor = new(180, 180, 180);
private static readonly SKColor LeadColor = new(255, 0, 0);
- private static readonly SKColor EtchColor = new(0, 150, 60);
- private static readonly SKColor SelectedColor = new(150, 200, 255, 90 * 255 / 100);
// Paths are built once per program and only read afterwards, so the render thread may draw
// them while the UI thread builds others. Keyed by reference: a rotation replaces the program.
@@ -52,6 +47,9 @@ namespace OpenNest.Avalonia.Controls
}
}
+ /// The colours drawn with: the application's active scheme unless set.
+ public ColorScheme Scheme { get; set; } = ColorScheme.Default;
+
public Part HoveredPart { get; private set; }
public Part SelectedPart { get; private set; }
@@ -131,21 +129,23 @@ namespace OpenNest.Avalonia.Controls
if (fitPending && Fit())
global::Avalonia.Threading.Dispatcher.UIThread.Post(() => ViewChanged?.Invoke(this, EventArgs.Empty));
+ var palette = SkiaSchemePalette.For(Scheme ?? ColorScheme.Default);
var items = new List();
if (plate != null)
{
for (var i = 0; i < plate.Parts.Count; i++)
{
var part = plate.Parts[i];
+ var color = ToSkColor(part.BaseDrawing.Color);
items.Add(new PartItem(PathsFor(part), part.Location, part.BoundingBox,
- ToSkColor(part.BaseDrawing.Color), (i + 1).ToString(),
+ color, palette.PartOutline(color), (i + 1).ToString(),
ReferenceEquals(part, SelectedPart), ReferenceEquals(part, HoveredPart),
part.BaseDrawing.IsCutOff, part.HasManualLeadIns));
}
}
context.Custom(new PlateDrawOperation(new Rect(Bounds.Size), plate == null ? null : PlateRect(plate),
- plate?.EdgeSpacing, WorldMatrix, items));
+ plate?.EdgeSpacing, WorldMatrix, items, palette));
}
private static SKColor ToSkColor(System.Drawing.Color color) =>
@@ -257,7 +257,7 @@ namespace OpenNest.Avalonia.Controls
}
private sealed record PartItem(ProgramPaths Paths, Vector Location, Box Bounds, SKColor Color,
- string Label, bool Selected, bool Hovered, bool IsCutOff, bool ShowLeads);
+ SKColor Outline, string Label, bool Selected, bool Hovered, bool IsCutOff, bool ShowLeads);
private sealed class PlateDrawOperation : ICustomDrawOperation
{
@@ -265,10 +265,12 @@ namespace OpenNest.Avalonia.Controls
private readonly Spacing? edgeSpacing;
private readonly SKMatrix matrix;
private readonly List items;
+ private readonly SkiaSchemePalette palette;
public PlateDrawOperation(Rect bounds, Box plateRect, Spacing? edgeSpacing, SKMatrix matrix,
- List items)
+ List items, SkiaSchemePalette palette)
{
+ this.palette = palette;
Bounds = bounds;
this.plateRect = plateRect;
this.edgeSpacing = edgeSpacing;
@@ -295,7 +297,7 @@ namespace OpenNest.Avalonia.Controls
var canvas = api.SkCanvas;
canvas.Save();
canvas.ClipRect(new SKRect(0, 0, (float)Bounds.Width, (float)Bounds.Height));
- canvas.Clear(BackgroundColor);
+ canvas.Clear(palette.Background);
if (plateRect != null)
Draw(canvas);
@@ -315,16 +317,16 @@ namespace OpenNest.Avalonia.Controls
var plate = new SKRect((float)plateRect.Left, (float)plateRect.Bottom,
(float)plateRect.Right, (float)plateRect.Top);
- fill.Color = PlateFillColor;
+ fill.Color = palette.PlateFill;
canvas.DrawRect(plate, fill);
if (edgeSpacing is { } spacing)
{
- stroke.Color = EdgeSpacingColor;
+ stroke.Color = palette.EdgeSpacing;
canvas.DrawRect(new SKRect(plate.Left + (float)spacing.Left,
plate.Top + (float)spacing.Bottom, plate.Right - (float)spacing.Right,
plate.Bottom - (float)spacing.Top), stroke);
}
- stroke.Color = PlateOutlineColor;
+ stroke.Color = palette.PlateOutline;
canvas.DrawRect(plate, stroke);
var view = canvas.LocalClipBounds;
@@ -337,13 +339,14 @@ namespace OpenNest.Avalonia.Controls
canvas.Save();
canvas.Translate((float)item.Location.X, (float)item.Location.Y);
- fill.Color = item.Selected ? SelectedColor
+ fill.Color = item.Selected ? palette.SelectedFill
: item.Hovered ? Lighten(item.Color) : item.Color;
canvas.DrawPath(item.Paths.Fill, fill);
- stroke.Color = EtchColor;
+ stroke.Color = palette.Etch;
canvas.DrawPath(item.Paths.Etch, stroke);
- stroke.Color = item.Selected ? new SKColor(30, 90, 200) : Darken(item.Color);
+ // Keep real cuts visible even where an etch overlaps them.
+ stroke.Color = item.Selected ? palette.SelectedOutline : item.Outline;
canvas.DrawPath(item.Paths.Cut, stroke);
// As on the desktop, only manually placed lead-ins are shown.
if (item.ShowLeads)
@@ -377,9 +380,6 @@ namespace OpenNest.Avalonia.Controls
}
}
- private static SKColor Darken(SKColor c) =>
- new((byte)(c.Red * 0.55), (byte)(c.Green * 0.55), (byte)(c.Blue * 0.55), 255);
-
private static SKColor Lighten(SKColor c) =>
new((byte)(c.Red + (255 - c.Red) / 2), (byte)(c.Green + (255 - c.Green) / 2),
(byte)(c.Blue + (255 - c.Blue) / 2), c.Alpha);
diff --git a/OpenNest.Avalonia/MainWindow.axaml.cs b/OpenNest.Avalonia/MainWindow.axaml.cs
index 6a6828b7..fe89c532 100644
--- a/OpenNest.Avalonia/MainWindow.axaml.cs
+++ b/OpenNest.Avalonia/MainWindow.axaml.cs
@@ -50,6 +50,8 @@ namespace OpenNest.Avalonia
var engineWarning = EngineSelection.LoadSavedSelection();
if (!string.IsNullOrEmpty(engineWarning))
notice = engineWarning;
+ // The colour scheme chosen in the WinForms app's options, from the shared user settings.
+ ColorSchemeRegistry.ApplySaved(Data.UserSettingsFile.Read(Data.UserSettingsFile.DefaultPath));
tabs.Changed += (_, _) => ShowActiveTab();
KeyBindings.Add(new KeyBinding
{
@@ -99,6 +101,8 @@ namespace OpenNest.Avalonia
var reader = new NestReader(file);
var document = new Document { Nest = reader.Read() };
warnings = reader.Warnings;
+ // The view follows the application's scheme, not the one the file was saved under.
+ ColorSchemeRegistry.RecolorDrawings(document.Nest);
return document;
});
if (!views.ContainsKey(tab))
diff --git a/OpenNest.Avalonia/Rendering/SkiaSchemePalette.cs b/OpenNest.Avalonia/Rendering/SkiaSchemePalette.cs
new file mode 100644
index 00000000..23294324
--- /dev/null
+++ b/OpenNest.Avalonia/Rendering/SkiaSchemePalette.cs
@@ -0,0 +1,79 @@
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+using SkiaSharp;
+using DrawingColor = System.Drawing.Color;
+
+namespace OpenNest.Avalonia.Rendering
+{
+ ///
+ /// A colour scheme's colours as Skia colours, for the plate canvas. Immutable, so the render
+ /// thread may read one while the UI thread changes the scheme; returns the
+ /// cached palette until the scheme's colours change.
+ ///
+ internal sealed class SkiaSchemePalette
+ {
+ // The WinForms view's translucent selection fill, used when the scheme sets none.
+ private static readonly DrawingColor LegacySelectedColor = DrawingColor.FromArgb(90, 150, 200, 255);
+
+ private static readonly ConditionalWeakTable Cache = new();
+
+ private readonly DrawingColor partOutline;
+ private readonly ConcurrentDictionary outlines = new();
+
+ private SkiaSchemePalette(ColorScheme scheme)
+ {
+ partOutline = scheme.PartOutlineColor;
+ Version = scheme.Version;
+ Background = ToSk(scheme.BackgroundColor);
+ PlateFill = ToSk(scheme.LayoutFillColor);
+ PlateOutline = ToSk(scheme.LayoutOutlineColor);
+ EdgeSpacing = ToSk(scheme.EdgeSpacingColor);
+ Etch = ToSk(scheme.EtchColor);
+ var selected = scheme.SelectedPartColor.IsEmpty ? LegacySelectedColor : scheme.SelectedPartColor;
+ SelectedFill = ToSk(selected);
+ // As WinForms: the scheme's outline, else the legacy selection fill darkened.
+ SelectedOutline = ToSk(scheme.PartOutlineColor.IsEmpty
+ ? ColorShade.Dark(LegacySelectedColor)
+ : scheme.PartOutlineColor);
+ }
+
+ private int Version { get; }
+
+ public SKColor Background { get; }
+
+ public SKColor PlateFill { get; }
+
+ public SKColor PlateOutline { get; }
+
+ public SKColor EdgeSpacing { get; }
+
+ public SKColor Etch { get; }
+
+ public SKColor SelectedFill { get; }
+
+ public SKColor SelectedOutline { get; }
+
+ /// The palette for 's current colours.
+ public static SkiaSchemePalette For(ColorScheme scheme)
+ {
+ lock (Cache)
+ {
+ if (!Cache.TryGetValue(scheme, out var palette) || palette.Version != scheme.Version)
+ {
+ palette = new SkiaSchemePalette(scheme);
+ Cache.AddOrUpdate(scheme, palette);
+ }
+
+ return palette;
+ }
+ }
+
+ /// A part's outline for its fill: the scheme's outline colour, or the fill darkened.
+ public SKColor PartOutline(SKColor fill) =>
+ outlines.GetOrAdd(fill, f => ToSk(partOutline.IsEmpty
+ ? ColorShade.Dark(DrawingColor.FromArgb(f.Alpha, f.Red, f.Green, f.Blue))
+ : partOutline));
+
+ public static SKColor ToSk(DrawingColor color) => new(color.R, color.G, color.B, color.A);
+ }
+}
diff --git a/docs/nesting-view-colors.md b/docs/nesting-view-colors.md
index 32461606..1e6cb933 100644
--- a/docs/nesting-view-colors.md
+++ b/docs/nesting-view-colors.md
@@ -63,6 +63,18 @@ fill hue falls in the etch green band. Selected rendering has separate bitmap te
Custom drawing colors are not automatically contrast-corrected. Existing scheme
names, saved choices, custom palettes and geometry remain unchanged.
+## Shared code and the Avalonia app
+
+The schemes, their JSON format, the `Schemes/` folder and the recolor rule live in
+`OpenNest.Desktop` (`ColorScheme`, `ColorSchemeRegistry`, `ColorSchemeSerializer`,
+and `ColorShade`, a port of `ControlPaint.Dark` for the legacy part outline), so
+both desktop apps share them; `Schemes/*.json` in that project copies beside both
+apps. WinForms draws with per-thread GDI+ pens from `GdiSchemeTools`; the Avalonia
+plate canvas draws with Skia colours from `SkiaSchemePalette`. The Avalonia app
+applies the scheme saved as `ActiveColorScheme` in the shared user-settings file
+when it starts, and recolors a nest's drawings when it opens one; it has no
+Options screen to change the scheme yet.
+
## Verification
On Windows, run: