diff --git a/OpenNest.WinForms.Tests/Controls/LibraryPreviewStyleTests.cs b/OpenNest.WinForms.Tests/Controls/LibraryPreviewStyleTests.cs new file mode 100644 index 00000000..ca90df01 --- /dev/null +++ b/OpenNest.WinForms.Tests/Controls/LibraryPreviewStyleTests.cs @@ -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 }; + } +} diff --git a/OpenNest/ColorScheme.cs b/OpenNest/ColorScheme.cs index 1ecf7df9..3496008a 100644 --- a/OpenNest/ColorScheme.cs +++ b/OpenNest/ColorScheme.cs @@ -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; diff --git a/OpenNest/ColorSchemeRegistry.cs b/OpenNest/ColorSchemeRegistry.cs index 6bc4413f..75844c13 100644 --- a/OpenNest/ColorSchemeRegistry.cs +++ b/OpenNest/ColorSchemeRegistry.cs @@ -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 diff --git a/OpenNest/ColorSchemeSerializer.cs b/OpenNest/ColorSchemeSerializer.cs index 2eb1f42e..d4db785f 100644 --- a/OpenNest/ColorSchemeSerializer.cs +++ b/OpenNest/ColorSchemeSerializer.cs @@ -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(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; } } diff --git a/OpenNest/Controls/DrawingLibraryView.Designer.cs b/OpenNest/Controls/DrawingLibraryView.Designer.cs index 0a76554f..c2a46ccc 100644 --- a/OpenNest/Controls/DrawingLibraryView.Designer.cs +++ b/OpenNest/Controls/DrawingLibraryView.Designer.cs @@ -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"; diff --git a/OpenNest/Controls/DrawingSetPreviewControl.cs b/OpenNest/Controls/DrawingSetPreviewControl.cs index e7bbe214..f726042a 100644 --- a/OpenNest/Controls/DrawingSetPreviewControl.cs +++ b/OpenNest/Controls/DrawingSetPreviewControl.cs @@ -8,7 +8,7 @@ using OpenNest.Geometry; namespace OpenNest.Controls { /// - /// 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 /// 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; } } diff --git a/OpenNest/Controls/PlateRenderer.cs b/OpenNest/Controls/PlateRenderer.cs index c3ad1307..bed86480 100644 --- a/OpenNest/Controls/PlateRenderer.cs +++ b/OpenNest/Controls/PlateRenderer.cs @@ -109,6 +109,22 @@ namespace OpenNest.Controls ); } + /// + /// Draws every part in 's library style with its bend lines, + /// without ids, selection or validation states. + /// + 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) { diff --git a/OpenNest/Controls/ShapePreviewControl.cs b/OpenNest/Controls/ShapePreviewControl.cs index 03580652..03d03548 100644 --- a/OpenNest/Controls/ShapePreviewControl.cs +++ b/OpenNest/Controls/ShapePreviewControl.cs @@ -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; } } diff --git a/OpenNest/Forms/ShapeLibraryForm.Designer.cs b/OpenNest/Forms/ShapeLibraryForm.Designer.cs index e6ac2c46..3a06d6a5 100644 --- a/OpenNest/Forms/ShapeLibraryForm.Designer.cs +++ b/OpenNest/Forms/ShapeLibraryForm.Designer.cs @@ -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); diff --git a/OpenNest/Forms/ShapeLibraryForm.cs b/OpenNest/Forms/ShapeLibraryForm.cs index 0bbe25c4..1e6a954a 100644 --- a/OpenNest/Forms/ShapeLibraryForm.cs +++ b/OpenNest/Forms/ShapeLibraryForm.cs @@ -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; diff --git a/OpenNest/GraphicsHelper.cs b/OpenNest/GraphicsHelper.cs index 3b62817b..a6745c2c 100644 --- a/OpenNest/GraphicsHelper.cs +++ b/OpenNest/GraphicsHelper.cs @@ -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); diff --git a/OpenNest/LayoutPart.cs b/OpenNest/LayoutPart.cs index 43dec85d..8c5bb413 100644 --- a/OpenNest/LayoutPart.cs +++ b/OpenNest/LayoutPart.cs @@ -143,6 +143,23 @@ namespace OpenNest g.DrawPath(muted ? Pens.Gray : LeadInPen, LeadInPath); } + /// + /// Draws the part in the scheme's library style, ignoring its drawing color: library + /// previews show drawings that are not in the nest yet. + /// + 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)