mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-07 11:55:44 -05:00
feat(ui): draw library previews in one shared style from the color scheme
The Shape Library, drawing library and drawing set preview each drew parts their own way (hardcoded green fill, steel-blue fallback, black outlines with gray etch). They now share a library style on ColorScheme (background, fill, outline, etch and bend colors) that ignores drawing colors, follows the active scheme (Dark gets a dark variant) and round-trips through saved schemes, defaulting when absent. Nest part colors are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dcb0a5c519
commit
e18304066d
12 files changed
+172
-34
No files matched your search
@@ -0,0 +1,68 @@
|
||||
using System.Drawing;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Controls;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Controls;
|
||||
|
||||
public class LibraryPreviewStyleTests
|
||||
{
|
||||
private const int ViewSize = 200;
|
||||
|
||||
[Fact]
|
||||
public void LibraryColorsRoundTripAndOlderSchemesKeepTheDefaults()
|
||||
{
|
||||
var scheme = ColorSchemeRegistry.Get("Dark");
|
||||
var restored = ColorSchemeSerializer.Deserialize(ColorSchemeSerializer.Serialize(scheme));
|
||||
|
||||
Assert.Equal(scheme.LibraryBackgroundColor.ToArgb(), restored.LibraryBackgroundColor.ToArgb());
|
||||
Assert.Equal(scheme.LibraryFillColor.ToArgb(), restored.LibraryFillColor.ToArgb());
|
||||
Assert.Equal(scheme.LibraryOutlineColor.ToArgb(), restored.LibraryOutlineColor.ToArgb());
|
||||
Assert.Equal(scheme.LibraryEtchColor.ToArgb(), restored.LibraryEtchColor.ToArgb());
|
||||
Assert.Equal(scheme.LibraryBendColor.ToArgb(), restored.LibraryBendColor.ToArgb());
|
||||
|
||||
var legacy = ColorSchemeSerializer.Deserialize("{\"name\":\"Old\",\"backgroundColor\":\"#101010\"}");
|
||||
var defaults = new ColorScheme();
|
||||
Assert.Equal(defaults.LibraryBackgroundColor.ToArgb(), legacy.LibraryBackgroundColor.ToArgb());
|
||||
Assert.Equal(defaults.LibraryFillColor.ToArgb(), legacy.LibraryFillColor.ToArgb());
|
||||
Assert.Equal(defaults.LibraryBendColor.ToArgb(), legacy.LibraryBendColor.ToArgb());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShapePreviewFillsWithTheLibraryStyleNotTheDrawingColor() => ArchUnitsTests.RunSta(() =>
|
||||
{
|
||||
using var preview = new ShapePreviewControl { Size = new Size(ViewSize, ViewSize) };
|
||||
preview.ShowDrawing(RedSquare());
|
||||
|
||||
AssertLibraryStyle(preview);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void DrawingSetPreviewFillsWithTheLibraryStyleNotTheDrawingColor() => ArchUnitsTests.RunSta(() =>
|
||||
{
|
||||
using var preview = new DrawingSetPreviewControl { Size = new Size(ViewSize, ViewSize) };
|
||||
preview.ShowDrawings(new[] { (RedSquare(), "") });
|
||||
|
||||
AssertLibraryStyle(preview);
|
||||
});
|
||||
|
||||
private static void AssertLibraryStyle(PlateView preview)
|
||||
{
|
||||
using var bitmap = new Bitmap(ViewSize, ViewSize);
|
||||
preview.DrawToBitmap(bitmap, new Rectangle(0, 0, ViewSize, ViewSize));
|
||||
var scheme = ColorScheme.Default;
|
||||
|
||||
Assert.Equal(scheme.LibraryBackgroundColor.ToArgb(), preview.BackColor.ToArgb());
|
||||
Assert.Equal(scheme.LibraryFillColor.ToArgb(), bitmap.GetPixel(ViewSize / 2, ViewSize / 2 + 10).ToArgb());
|
||||
}
|
||||
|
||||
private static Drawing RedSquare()
|
||||
{
|
||||
var program = new Program();
|
||||
program.Codes.Add(new RapidMove(0, 0));
|
||||
program.Codes.Add(new LinearMove(10, 0));
|
||||
program.Codes.Add(new LinearMove(10, 10));
|
||||
program.Codes.Add(new LinearMove(0, 10));
|
||||
program.Codes.Add(new LinearMove(0, 0));
|
||||
return new Drawing("square", program) { Color = Color.Red };
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,18 @@ namespace OpenNest
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -72,6 +72,11 @@ namespace OpenNest
|
||||
d.PartOutlineColor = scheme.PartOutlineColor;
|
||||
d.SelectedPartColor = scheme.SelectedPartColor;
|
||||
d.EtchColor = scheme.EtchColor;
|
||||
d.LibraryBackgroundColor = scheme.LibraryBackgroundColor;
|
||||
d.LibraryFillColor = scheme.LibraryFillColor;
|
||||
d.LibraryOutlineColor = scheme.LibraryOutlineColor;
|
||||
d.LibraryEtchColor = scheme.LibraryEtchColor;
|
||||
d.LibraryBendColor = scheme.LibraryBendColor;
|
||||
|
||||
Drawing.PartColors = scheme.PartColors;
|
||||
Drawing.UseGoldenAngleColors = scheme.UseGoldenAngleColors;
|
||||
@@ -333,6 +338,11 @@ namespace OpenNest
|
||||
OriginColor = Color.FromArgb(120, 120, 130),
|
||||
EdgeSpacingColor = Color.FromArgb(90, 90, 100),
|
||||
PreviewPartColor = Color.FromArgb(255, 170, 60),
|
||||
LibraryBackgroundColor = Color.FromArgb(30, 30, 34),
|
||||
LibraryFillColor = Color.FromArgb(70, 76, 86),
|
||||
LibraryOutlineColor = Color.FromArgb(205, 210, 220),
|
||||
LibraryEtchColor = Color.FromArgb(120, 230, 120),
|
||||
LibraryBendColor = Color.FromArgb(100, 180, 255),
|
||||
PartColors = new[]
|
||||
{
|
||||
Color.FromArgb(255, 85, 85), // Neon Red
|
||||
|
||||
@@ -29,6 +29,11 @@ namespace OpenNest
|
||||
PartOutlineColor = scheme.PartOutlineColor.IsEmpty ? null : ToHex(scheme.PartOutlineColor),
|
||||
SelectedPartColor = scheme.SelectedPartColor.IsEmpty ? null : ToHex(scheme.SelectedPartColor),
|
||||
EtchColor = ToHex(scheme.EtchColor),
|
||||
LibraryBackgroundColor = ToHex(scheme.LibraryBackgroundColor),
|
||||
LibraryFillColor = ToHex(scheme.LibraryFillColor),
|
||||
LibraryOutlineColor = ToHex(scheme.LibraryOutlineColor),
|
||||
LibraryEtchColor = ToHex(scheme.LibraryEtchColor),
|
||||
LibraryBendColor = ToHex(scheme.LibraryBendColor),
|
||||
PartColors = scheme.PartColors.Select(ToHex).ToArray(),
|
||||
UseGoldenAngleColors = scheme.UseGoldenAngleColors,
|
||||
};
|
||||
@@ -41,7 +46,7 @@ namespace OpenNest
|
||||
JsonSerializer.Deserialize<ColorSchemeDto>(json, JsonOptions)
|
||||
?? throw new JsonException("ColorScheme JSON was null");
|
||||
|
||||
return new ColorScheme
|
||||
var scheme = new ColorScheme
|
||||
{
|
||||
Name = dto.Name ?? "Unnamed",
|
||||
BackgroundColor = FromHex(dto.BackgroundColor),
|
||||
@@ -58,8 +63,19 @@ namespace OpenNest
|
||||
PartColors = (dto.PartColors ?? new string[0]).Select(FromHex).ToArray(),
|
||||
UseGoldenAngleColors = dto.UseGoldenAngleColors,
|
||||
};
|
||||
|
||||
// Schemes saved before the library style keep its defaults.
|
||||
scheme.LibraryBackgroundColor = FromHexOr(dto.LibraryBackgroundColor, scheme.LibraryBackgroundColor);
|
||||
scheme.LibraryFillColor = FromHexOr(dto.LibraryFillColor, scheme.LibraryFillColor);
|
||||
scheme.LibraryOutlineColor = FromHexOr(dto.LibraryOutlineColor, scheme.LibraryOutlineColor);
|
||||
scheme.LibraryEtchColor = FromHexOr(dto.LibraryEtchColor, scheme.LibraryEtchColor);
|
||||
scheme.LibraryBendColor = FromHexOr(dto.LibraryBendColor, scheme.LibraryBendColor);
|
||||
return scheme;
|
||||
}
|
||||
|
||||
private static Color FromHexOr(string hex, Color fallback) =>
|
||||
string.IsNullOrWhiteSpace(hex) ? fallback : FromHex(hex);
|
||||
|
||||
private static string ToHex(Color c) =>
|
||||
"#" + c.R.ToString("X2") + c.G.ToString("X2") + c.B.ToString("X2");
|
||||
|
||||
@@ -102,6 +118,11 @@ namespace OpenNest
|
||||
public string PartOutlineColor { get; set; }
|
||||
public string SelectedPartColor { get; set; }
|
||||
public string EtchColor { get; set; }
|
||||
public string LibraryBackgroundColor { get; set; }
|
||||
public string LibraryFillColor { get; set; }
|
||||
public string LibraryOutlineColor { get; set; }
|
||||
public string LibraryEtchColor { get; set; }
|
||||
public string LibraryBendColor { get; set; }
|
||||
public string[] PartColors { get; set; }
|
||||
public bool UseGoldenAngleColors { get; set; }
|
||||
}
|
||||
|
||||
@@ -514,7 +514,6 @@ namespace OpenNest.Controls
|
||||
previewBox.AllowPan = false;
|
||||
previewBox.AllowSelect = false;
|
||||
previewBox.AllowZoom = false;
|
||||
previewBox.BackColor = System.Drawing.Color.White;
|
||||
previewBox.Cursor = System.Windows.Forms.Cursors.Default;
|
||||
previewBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
previewBox.Name = "previewBox";
|
||||
|
||||
@@ -8,7 +8,7 @@ using OpenNest.Geometry;
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows every drawing of a set side by side at one scale, outlined without fill, packed by
|
||||
/// Shows every drawing of a set side by side at one scale in the library style, packed by
|
||||
/// <see cref="DrawingSetPreviewLayout"/> with a label under each and the info lines above.
|
||||
/// The layout is repacked to fit whenever the control is resized; the wheel zooms, the
|
||||
/// middle button pans and a middle double-click fits the set again.
|
||||
@@ -29,7 +29,7 @@ namespace OpenNest.Controls
|
||||
AllowSelect = false;
|
||||
AllowZoom = true;
|
||||
AllowDrop = false;
|
||||
BackColor = Color.White;
|
||||
BackColor = ColorScheme.Default.LibraryBackgroundColor;
|
||||
// A large set in millimeters fits at well under the default floor of 0.3 px per unit.
|
||||
ViewScaleMin = 1e-5f;
|
||||
}
|
||||
@@ -138,28 +138,13 @@ namespace OpenNest.Controls
|
||||
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||
|
||||
e.Graphics.TranslateTransform(origin.X, origin.Y);
|
||||
PaintOutlines(e.Graphics);
|
||||
Renderer.DrawLibraryParts(e.Graphics, ColorScheme.Default);
|
||||
PaintLabels(e.Graphics);
|
||||
e.Graphics.ResetTransform();
|
||||
|
||||
PaintInfo(e.Graphics);
|
||||
}
|
||||
|
||||
// Outlines only: cuts in black, etch marks dashed in gray.
|
||||
private void PaintOutlines(Graphics g)
|
||||
{
|
||||
using var etchPen = new Pen(Color.Gray) { DashStyle = DashStyle.Dash };
|
||||
foreach (var part in LayoutParts)
|
||||
{
|
||||
if (part.IsDirty)
|
||||
part.Update(this);
|
||||
if (part.EtchPath?.PointCount > 0)
|
||||
g.DrawPath(etchPen, part.EtchPath);
|
||||
if (part.Path?.PointCount > 0)
|
||||
g.DrawPath(Pens.Black, part.Path);
|
||||
}
|
||||
}
|
||||
|
||||
private void PaintLabels(Graphics g)
|
||||
{
|
||||
if (layout == null)
|
||||
@@ -172,13 +157,14 @@ namespace OpenNest.Controls
|
||||
Trimming = StringTrimming.EllipsisCharacter,
|
||||
FormatFlags = StringFormatFlags.NoWrap,
|
||||
};
|
||||
using var text = new SolidBrush(ColorScheme.Default.LibraryOutlineColor);
|
||||
|
||||
foreach (var placement in layout.Placements)
|
||||
{
|
||||
var corner = PointWorldToGraph(placement.CellX, placement.LabelTop);
|
||||
var rect = new RectangleF(corner.X, corner.Y + 1,
|
||||
LengthWorldToGui(placement.CellWidth), System.Math.Max(LengthWorldToGui(layout.LabelHeight), Font.Height));
|
||||
g.DrawString(items[placement.Index].Label, Font, Brushes.Black, rect, format);
|
||||
g.DrawString(items[placement.Index].Label, Font, text, rect, format);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,12 +177,13 @@ namespace OpenNest.Controls
|
||||
Trimming = StringTrimming.EllipsisCharacter,
|
||||
FormatFlags = StringFormatFlags.NoWrap,
|
||||
};
|
||||
using var text = new SolidBrush(ColorScheme.Default.LibraryOutlineColor);
|
||||
|
||||
foreach (var line in infoLines)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
g.DrawString(line, Font, Brushes.Black, new RectangleF(4, y, System.Math.Max(Width - 8, 1), lineHeight), format);
|
||||
g.DrawString(line, Font, text, new RectangleF(4, y, System.Math.Max(Width - 8, 1), lineHeight), format);
|
||||
y += lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,22 @@ namespace OpenNest.Controls
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws every part in <paramref name="scheme"/>'s library style with its bend lines,
|
||||
/// without ids, selection or validation states.
|
||||
/// </summary>
|
||||
public void DrawLibraryParts(Graphics g, ColorScheme scheme)
|
||||
{
|
||||
using var bendPen = new Pen(scheme.LibraryBendColor, 1.5f) { DashStyle = DashStyle.Dash };
|
||||
foreach (var part in view.DisplayLayoutParts)
|
||||
{
|
||||
if (part.IsDirty)
|
||||
part.UpdateView(view);
|
||||
part.DrawLibrary(g, scheme);
|
||||
DrawBendLines(g, part.BasePart, bendPen);
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawParts(Graphics g)
|
||||
{
|
||||
var viewBounds = view.GetViewBounds();
|
||||
@@ -439,6 +455,13 @@ namespace OpenNest.Controls
|
||||
{
|
||||
DashStyle = System.Drawing.Drawing2D.DashStyle.Dash,
|
||||
};
|
||||
DrawBendLines(g, part, bendPen);
|
||||
}
|
||||
|
||||
private void DrawBendLines(Graphics g, Part part, Pen bendPen)
|
||||
{
|
||||
if (part.BaseDrawing.Bends == null)
|
||||
return;
|
||||
|
||||
foreach (var bend in part.BaseDrawing.Bends)
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace OpenNest.Controls
|
||||
AllowSelect = false;
|
||||
AllowZoom = false;
|
||||
AllowDrop = false;
|
||||
BackColor = Color.White;
|
||||
BackColor = ColorScheme.Default.LibraryBackgroundColor;
|
||||
}
|
||||
|
||||
public void SetInfo(params string[] lines)
|
||||
@@ -55,7 +55,7 @@ namespace OpenNest.Controls
|
||||
|
||||
e.Graphics.TranslateTransform(origin.X, origin.Y);
|
||||
Renderer.DrawPlate(e.Graphics);
|
||||
Renderer.DrawParts(e.Graphics);
|
||||
Renderer.DrawLibraryParts(e.Graphics, ColorScheme.Default);
|
||||
e.Graphics.ResetTransform();
|
||||
|
||||
PaintInfo(e.Graphics);
|
||||
@@ -68,12 +68,13 @@ namespace OpenNest.Controls
|
||||
|
||||
var lineHeight = Font.GetHeight(g) + 1;
|
||||
var y = 4f;
|
||||
using var text = new SolidBrush(ColorScheme.Default.LibraryOutlineColor);
|
||||
|
||||
foreach (var line in infoLines)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
g.DrawString(line, Font, Brushes.Black, 4, y);
|
||||
g.DrawString(line, Font, text, 4, y);
|
||||
y += lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -202,7 +202,6 @@ namespace OpenNest.Forms
|
||||
previewBox.AllowPan = false;
|
||||
previewBox.AllowSelect = false;
|
||||
previewBox.AllowZoom = false;
|
||||
previewBox.BackColor = System.Drawing.Color.White;
|
||||
colorScheme1.BackgroundColor = System.Drawing.Color.DarkGray;
|
||||
colorScheme1.BoundingBoxColor = System.Drawing.Color.FromArgb(128, 128, 255);
|
||||
colorScheme1.EdgeSpacingColor = System.Drawing.Color.FromArgb(180, 180, 180);
|
||||
|
||||
@@ -27,9 +27,6 @@ namespace OpenNest.Forms
|
||||
private const int TileColumns = 3;
|
||||
private const int LVM_SETICONSPACING = 0x1000 + 53;
|
||||
|
||||
// The fill the preview gives a drawing without a color, so tiles match it.
|
||||
private static readonly Color ThumbnailFill = Color.FromArgb(130, 204, 130);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
@@ -125,6 +122,8 @@ namespace OpenNest.Forms
|
||||
ImageSize = new Size(ThumbnailSize, ThumbnailSize),
|
||||
};
|
||||
shapeListView.LargeImageList = images;
|
||||
shapeListView.BackColor = ColorScheme.Default.LibraryBackgroundColor;
|
||||
shapeListView.ForeColor = ColorScheme.Default.LibraryOutlineColor;
|
||||
shapeListView.HandleCreated += (_, _) => ApplyTileSpacing();
|
||||
Disposed += (_, _) => images.Dispose();
|
||||
|
||||
@@ -161,9 +160,10 @@ namespace OpenNest.Forms
|
||||
// GetImage draws without antialiasing; render at twice the size and
|
||||
// downsample so curved edges stay smooth at thumbnail scale.
|
||||
var renderSize = new Size(ThumbnailSize * 2, ThumbnailSize * 2);
|
||||
using var brush = new SolidBrush(ThumbnailFill);
|
||||
using var pen = new Pen(ColorScheme.Default.GetPartOutlineColor(ThumbnailFill), 2f);
|
||||
using var large = program.GetImage(renderSize, pen, brush);
|
||||
var scheme = ColorScheme.Default;
|
||||
using var brush = new SolidBrush(scheme.LibraryFillColor);
|
||||
using var pen = new Pen(scheme.LibraryOutlineColor, 2f);
|
||||
using var large = program.GetImage(renderSize, pen, brush, scheme.LibraryEtchColor);
|
||||
using var graphics = Graphics.FromImage(thumbnail);
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
|
||||
@@ -108,7 +108,8 @@ namespace OpenNest
|
||||
this Program pgm,
|
||||
System.Drawing.Size size,
|
||||
Pen pen,
|
||||
Brush brush
|
||||
Brush brush,
|
||||
Color? etchColor = null
|
||||
)
|
||||
{
|
||||
var img = new Bitmap(size.Width, size.Height);
|
||||
@@ -152,7 +153,7 @@ namespace OpenNest
|
||||
if (pen == null)
|
||||
pen = Pens.Black;
|
||||
|
||||
using var etchPen = new Pen(ColorScheme.Default.EtchColor, 1.5f);
|
||||
using var etchPen = new Pen(etchColor ?? ColorScheme.Default.EtchColor, 1.5f);
|
||||
graphics.DrawPath(etchPen, etchPath);
|
||||
// A coincident cut must remain visible over an etch.
|
||||
graphics.DrawPath(pen, path);
|
||||
|
||||
@@ -143,6 +143,23 @@ namespace OpenNest
|
||||
g.DrawPath(muted ? Pens.Gray : LeadInPen, LeadInPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the part in the scheme's library style, ignoring its drawing color: library
|
||||
/// previews show drawings that are not in the nest yet.
|
||||
/// </summary>
|
||||
internal void DrawLibrary(Graphics g, ColorScheme scheme)
|
||||
{
|
||||
using var fill = new SolidBrush(scheme.LibraryFillColor);
|
||||
using var outline = new Pen(scheme.LibraryOutlineColor);
|
||||
using var etch = new Pen(scheme.LibraryEtchColor, 1.5f);
|
||||
g.FillPath(fill, MaterialPath);
|
||||
DrawDisplay(g, false);
|
||||
if (EtchPath?.PointCount > 0)
|
||||
g.DrawPath(etch, EtchPath);
|
||||
// Keep real cuts visible even where an etch overlaps them.
|
||||
g.DrawPath(outline, Path);
|
||||
}
|
||||
|
||||
public void Draw(Graphics g, string id) => Draw(g, id, false);
|
||||
|
||||
internal void Draw(Graphics g, string id, bool muted)
|
||||
|
||||
Reference in new issue
Block a user