mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-13 03:31:09 -05:00
feat(avalonia): draw plates in the saved colour scheme
The Avalonia app applies the colour scheme saved as ActiveColorScheme in the shared user-settings file when the main window opens, and recolours a nest's drawings from it when opening the nest, as WinForms does. The plate canvas draws the background, sheet, edge spacing, etch, part outlines and selection from the active scheme through SkiaSchemePalette, a cache of Skia colours rebuilt when the scheme's colours change, instead of hard-coded colours. Part outlines follow the scheme's outline colour or the fill darkened as WinForms darkens it. There is no Options screen to change the scheme yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cc3711c11c
commit
641e07b766
4 files changed
+117
-22
No files matched your search
@@ -16,18 +16,13 @@ using Vector = OpenNest.Geometry.Vector;
|
||||
namespace OpenNest.Avalonia.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="Scheme"/>: 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.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The colours drawn with: the application's active scheme unless set.</summary>
|
||||
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<PartItem>();
|
||||
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<PartItem> items;
|
||||
private readonly SkiaSchemePalette palette;
|
||||
|
||||
public PlateDrawOperation(Rect bounds, Box plateRect, Spacing? edgeSpacing, SKMatrix matrix,
|
||||
List<PartItem> items)
|
||||
List<PartItem> 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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SkiaSharp;
|
||||
using DrawingColor = System.Drawing.Color;
|
||||
|
||||
namespace OpenNest.Avalonia.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// 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; <see cref="For"/> returns the
|
||||
/// cached palette until the scheme's colours change.
|
||||
/// </summary>
|
||||
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<ColorScheme, SkiaSchemePalette> Cache = new();
|
||||
|
||||
private readonly DrawingColor partOutline;
|
||||
private readonly ConcurrentDictionary<SKColor, SKColor> 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; }
|
||||
|
||||
/// <summary>The palette for <paramref name="scheme"/>'s current colours.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A part's outline for its fill: the scheme's outline colour, or the fill darkened.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user