mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 06:41:35 -05:00
fix(desktop): give each UI thread its own plate-drawing pens and brushes
ColorScheme.Default and LayoutPart kept single static pens, brushes and a font that every plate view and drawing preview painted with. GDI+ objects are not thread-safe, so two views painting at once on different UI threads (parallel WinForms test collections) failed in PlateRenderer.DrawPlate with "Object is currently in use elsewhere" and crashed the test host. The scheme now builds its pens and brushes per thread and rebuilds them after a color change; LayoutPart's selection, lead-in and label objects are thread-static. A regression test paints previews on four threads at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87672543cd
commit
dfa6ccb954
3 files changed
+215
-118
No files matched your search
@@ -0,0 +1,68 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Controls;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Controls;
|
||||
|
||||
public class ShapePreviewConcurrencyTests
|
||||
{
|
||||
private static Program Square(double size)
|
||||
{
|
||||
var program = new Program();
|
||||
program.Codes.Add(new RapidMove(0, 0));
|
||||
program.Codes.Add(new LinearMove(size, 0));
|
||||
program.Codes.Add(new LinearMove(size, size));
|
||||
program.Codes.Add(new LinearMove(0, size));
|
||||
program.Codes.Add(new LinearMove(0, 0));
|
||||
return program;
|
||||
}
|
||||
|
||||
// Parallel test collections paint previews on separate STA threads at once.
|
||||
// GDI+ objects are not thread-safe, so any pen, brush or font shared between
|
||||
// views fails with "Object is currently in use elsewhere".
|
||||
[Fact]
|
||||
public void PreviewsPaintAtTheSameTimeOnSeparateThreads()
|
||||
{
|
||||
const int threadCount = 4;
|
||||
const int paints = 200;
|
||||
var start = new Barrier(threadCount);
|
||||
var failures = new List<Exception>();
|
||||
var threads = new List<Thread>();
|
||||
|
||||
for (var t = 0; t < threadCount; t++)
|
||||
{
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
|
||||
using var preview = new ShapePreviewControl { Size = new Size(200, 200) };
|
||||
preview.ShowDrawing(new Drawing("square", Square(10)));
|
||||
preview.SetInfo("10 x 10", "1 part");
|
||||
using var bitmap = new Bitmap(200, 200);
|
||||
|
||||
start.SignalAndWait(TimeSpan.FromSeconds(10));
|
||||
for (var i = 0; i < paints; i++)
|
||||
preview.DrawToBitmap(bitmap, new Rectangle(0, 0, 200, 200));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lock (failures)
|
||||
failures.Add(ex);
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
};
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
threads.Add(thread);
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
foreach (var thread in threads)
|
||||
Assert.True(thread.Join(TimeSpan.FromMinutes(1)), "A preview thread did not finish.");
|
||||
|
||||
Assert.True(failures.Count == 0, string.Join(Environment.NewLine, failures));
|
||||
}
|
||||
}
|
||||
+104
-93
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -45,25 +47,110 @@ namespace OpenNest
|
||||
|
||||
#region Pens/Brushes
|
||||
|
||||
public Pen LayoutOutlinePen { get; private set; }
|
||||
// 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;
|
||||
|
||||
public Brush LayoutFillBrush { get; private set; }
|
||||
private DrawingTools Tools
|
||||
{
|
||||
get
|
||||
{
|
||||
var current = tools.Value;
|
||||
if (current == null || current.Version != toolsVersion)
|
||||
{
|
||||
current?.Dispose();
|
||||
current = new DrawingTools(this, toolsVersion);
|
||||
tools.Value = current;
|
||||
}
|
||||
|
||||
public Pen BoundingBoxPen { get; private set; }
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
public Pen RapidPen { get; private set; }
|
||||
public Pen LayoutOutlinePen => Tools.LayoutOutlinePen;
|
||||
|
||||
public Pen OriginPen { get; private set; }
|
||||
public Brush LayoutFillBrush => Tools.LayoutFillBrush;
|
||||
|
||||
public Pen EdgeSpacingPen { get; private set; }
|
||||
public Pen BoundingBoxPen => Tools.BoundingBoxPen;
|
||||
|
||||
public Pen PreviewPartPen { get; private set; }
|
||||
public Pen RapidPen => Tools.RapidPen;
|
||||
|
||||
public Brush PreviewPartBrush { get; private set; }
|
||||
public Pen OriginPen => Tools.OriginPen;
|
||||
|
||||
public Pen ActivePreviewPartPen { get; private set; }
|
||||
public Pen EdgeSpacingPen => Tools.EdgeSpacingPen;
|
||||
|
||||
public Brush ActivePreviewPartBrush { get; private set; }
|
||||
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
|
||||
|
||||
@@ -74,119 +161,43 @@ namespace OpenNest
|
||||
public Color LayoutOutlineColor
|
||||
{
|
||||
get { return layoutOutlineColor; }
|
||||
set
|
||||
{
|
||||
layoutOutlineColor = value;
|
||||
|
||||
if (LayoutOutlinePen != null)
|
||||
LayoutOutlinePen.Dispose();
|
||||
|
||||
LayoutOutlinePen = new Pen(value);
|
||||
}
|
||||
set { layoutOutlineColor = value; Interlocked.Increment(ref toolsVersion); }
|
||||
}
|
||||
|
||||
public Color LayoutFillColor
|
||||
{
|
||||
get { return layoutFillColor; }
|
||||
set
|
||||
{
|
||||
layoutFillColor = value;
|
||||
|
||||
if (LayoutFillBrush != null)
|
||||
LayoutFillBrush.Dispose();
|
||||
|
||||
LayoutFillBrush = new SolidBrush(value);
|
||||
}
|
||||
set { layoutFillColor = value; Interlocked.Increment(ref toolsVersion); }
|
||||
}
|
||||
|
||||
public Color BoundingBoxColor
|
||||
{
|
||||
get { return boundingBoxColor; }
|
||||
set
|
||||
{
|
||||
boundingBoxColor = value;
|
||||
|
||||
if (BoundingBoxPen != null)
|
||||
BoundingBoxPen.Dispose();
|
||||
|
||||
BoundingBoxPen = new Pen(value);
|
||||
}
|
||||
set { boundingBoxColor = value; Interlocked.Increment(ref toolsVersion); }
|
||||
}
|
||||
|
||||
public Color RapidColor
|
||||
{
|
||||
get { return rapidColor; }
|
||||
set
|
||||
{
|
||||
rapidColor = value;
|
||||
|
||||
if (RapidPen != null)
|
||||
RapidPen.Dispose();
|
||||
|
||||
RapidPen = new Pen(value)
|
||||
{
|
||||
DashPattern = new float[] { 10, 10 },
|
||||
DashCap = DashCap.Flat,
|
||||
};
|
||||
}
|
||||
set { rapidColor = value; Interlocked.Increment(ref toolsVersion); }
|
||||
}
|
||||
|
||||
public Color OriginColor
|
||||
{
|
||||
get { return originColor; }
|
||||
set
|
||||
{
|
||||
originColor = value;
|
||||
|
||||
if (OriginPen != null)
|
||||
OriginPen.Dispose();
|
||||
|
||||
OriginPen = new Pen(value);
|
||||
}
|
||||
set { originColor = value; Interlocked.Increment(ref toolsVersion); }
|
||||
}
|
||||
|
||||
public Color EdgeSpacingColor
|
||||
{
|
||||
get { return edgeSpacingColor; }
|
||||
set
|
||||
{
|
||||
edgeSpacingColor = value;
|
||||
|
||||
if (EdgeSpacingPen != null)
|
||||
EdgeSpacingPen.Dispose();
|
||||
|
||||
EdgeSpacingPen = new Pen(value)
|
||||
{
|
||||
DashPattern = new float[] { 3, 3 },
|
||||
DashCap = DashCap.Flat,
|
||||
};
|
||||
}
|
||||
set { edgeSpacingColor = value; Interlocked.Increment(ref toolsVersion); }
|
||||
}
|
||||
|
||||
public Color PreviewPartColor
|
||||
{
|
||||
get { return previewPartColor; }
|
||||
set
|
||||
{
|
||||
previewPartColor = value;
|
||||
|
||||
if (PreviewPartPen != null)
|
||||
PreviewPartPen.Dispose();
|
||||
|
||||
if (PreviewPartBrush != null)
|
||||
PreviewPartBrush.Dispose();
|
||||
|
||||
if (ActivePreviewPartPen != null)
|
||||
ActivePreviewPartPen.Dispose();
|
||||
|
||||
if (ActivePreviewPartBrush != null)
|
||||
ActivePreviewPartBrush.Dispose();
|
||||
|
||||
PreviewPartPen = new Pen(value, 1);
|
||||
PreviewPartBrush = new SolidBrush(Color.FromArgb(60, value));
|
||||
ActivePreviewPartPen = new Pen(Color.FromArgb(128, value), 1);
|
||||
ActivePreviewPartBrush = new SolidBrush(Color.FromArgb(30, value));
|
||||
}
|
||||
set { previewPartColor = value; Interlocked.Increment(ref toolsVersion); }
|
||||
}
|
||||
|
||||
#endregion Colors
|
||||
|
||||
+43
-25
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
@@ -11,11 +12,14 @@ namespace OpenNest
|
||||
{
|
||||
public class LayoutPart : IPart
|
||||
{
|
||||
private static Font programIdFont;
|
||||
private static Color selectedColor;
|
||||
private static Pen selectedPen;
|
||||
private static Brush selectedBrush;
|
||||
private static Pen leadInPen;
|
||||
private static Color selectedColor = Color.FromArgb(90, 150, 200, 255);
|
||||
|
||||
// GDI+ objects are not thread-safe, so views on different UI threads each draw
|
||||
// with their own; sharing one fails with "Object is currently in use elsewhere".
|
||||
[ThreadStatic] private static Font programIdFont;
|
||||
[ThreadStatic] private static Pen selectedPen;
|
||||
[ThreadStatic] private static Brush selectedBrush;
|
||||
[ThreadStatic] private static Pen leadInPen;
|
||||
|
||||
private Color color;
|
||||
private Color? colorOverride;
|
||||
@@ -33,13 +37,6 @@ namespace OpenNest
|
||||
|
||||
public readonly Part BasePart;
|
||||
|
||||
static LayoutPart()
|
||||
{
|
||||
programIdFont = new Font(SystemFonts.DefaultFont, FontStyle.Bold | FontStyle.Underline);
|
||||
SelectedColor = Color.FromArgb(90, 150, 200, 255);
|
||||
leadInPen = new Pen(Color.OrangeRed, 1.5f);
|
||||
}
|
||||
|
||||
private LayoutPart(Part part)
|
||||
{
|
||||
this.BasePart = part;
|
||||
@@ -105,15 +102,15 @@ namespace OpenNest
|
||||
RefreshColors();
|
||||
using var schemeSelectionBrush = IsSelected && !colorScheme.SelectedPartColor.IsEmpty
|
||||
? new SolidBrush(colorScheme.SelectedPartColor) : null;
|
||||
g.FillPath(muted ? Brushes.Gainsboro : schemeSelectionBrush ?? (IsSelected ? selectedBrush : brush),
|
||||
g.FillPath(muted ? Brushes.Gainsboro : schemeSelectionBrush ?? (IsSelected ? SelectedBrush : brush),
|
||||
MaterialPath);
|
||||
DrawDisplay(g, muted);
|
||||
DrawEtch(g, muted);
|
||||
// Keep real cuts visible even where an etch overlaps them.
|
||||
g.DrawPath(muted ? Pens.Gray : IsSelected && colorScheme.PartOutlineColor.IsEmpty ? selectedPen : pen, Path);
|
||||
g.DrawPath(muted ? Pens.Gray : IsSelected && colorScheme.PartOutlineColor.IsEmpty ? SelectedPen : pen, Path);
|
||||
|
||||
if (LeadInPath != null)
|
||||
g.DrawPath(muted ? Pens.Gray : leadInPen, LeadInPath);
|
||||
g.DrawPath(muted ? Pens.Gray : LeadInPen, LeadInPath);
|
||||
}
|
||||
|
||||
public void Draw(Graphics g, string id) => Draw(g, id, false);
|
||||
@@ -129,7 +126,7 @@ namespace OpenNest
|
||||
};
|
||||
g.DrawString(
|
||||
id,
|
||||
programIdFont,
|
||||
ProgramIdFont,
|
||||
muted ? Brushes.DimGray : Brushes.Black,
|
||||
_labelScreenPoint.X,
|
||||
_labelScreenPoint.Y,
|
||||
@@ -393,19 +390,40 @@ namespace OpenNest
|
||||
public static Color SelectedColor
|
||||
{
|
||||
get { return selectedColor; }
|
||||
set
|
||||
set { selectedColor = value; }
|
||||
}
|
||||
|
||||
private static Font ProgramIdFont =>
|
||||
programIdFont ??= new Font(SystemFonts.DefaultFont, FontStyle.Bold | FontStyle.Underline);
|
||||
|
||||
private static Pen LeadInPen => leadInPen ??= new Pen(Color.OrangeRed, 1.5f);
|
||||
|
||||
private static Brush SelectedBrush
|
||||
{
|
||||
get
|
||||
{
|
||||
selectedColor = value;
|
||||
if (selectedBrush is not SolidBrush current || current.Color != selectedColor)
|
||||
{
|
||||
selectedBrush?.Dispose();
|
||||
selectedBrush = new SolidBrush(selectedColor);
|
||||
}
|
||||
|
||||
if (selectedBrush != null)
|
||||
selectedBrush.Dispose();
|
||||
return selectedBrush;
|
||||
}
|
||||
}
|
||||
|
||||
selectedBrush = new SolidBrush(value);
|
||||
private static Pen SelectedPen
|
||||
{
|
||||
get
|
||||
{
|
||||
var color = ControlPaint.Dark(selectedColor);
|
||||
if (selectedPen == null || selectedPen.Color != color)
|
||||
{
|
||||
selectedPen?.Dispose();
|
||||
selectedPen = new Pen(color);
|
||||
}
|
||||
|
||||
if (selectedPen != null)
|
||||
selectedPen.Dispose();
|
||||
|
||||
selectedPen = new Pen(ControlPaint.Dark(value));
|
||||
return selectedPen;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user