feat(plateview): show material overlap areas
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
using System;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
|
||||||
|
namespace OpenNest.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cheap ordered identity/pose check, not a geometry hash. In-place geometry editors must
|
||||||
|
/// explicitly invalidate before mutating. Capture and match only on the model's UI thread.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OverlapGeometryStamp
|
||||||
|
{
|
||||||
|
private readonly Plate plate;
|
||||||
|
private readonly Entry[] entries;
|
||||||
|
|
||||||
|
private OverlapGeometryStamp(Plate plate)
|
||||||
|
{
|
||||||
|
this.plate = plate;
|
||||||
|
entries = new Entry[plate.Parts.Count];
|
||||||
|
for (var i = 0; i < entries.Length; i++)
|
||||||
|
entries[i] = new Entry(plate.Parts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OverlapGeometryStamp Capture(Plate plate) => new(plate);
|
||||||
|
|
||||||
|
public bool Matches(Plate current)
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(plate, current) || current.Parts.Count != entries.Length)
|
||||||
|
return false;
|
||||||
|
for (var i = 0; i < entries.Length; i++)
|
||||||
|
if (!entries[i].Matches(current.Parts[i]))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct Entry
|
||||||
|
{
|
||||||
|
private readonly Part part;
|
||||||
|
private readonly Drawing drawing;
|
||||||
|
private readonly Program placedProgram;
|
||||||
|
private readonly Program cleanProgram;
|
||||||
|
private readonly long x, y, rotation;
|
||||||
|
private readonly bool isCutOff;
|
||||||
|
|
||||||
|
public Entry(Part part)
|
||||||
|
{
|
||||||
|
this.part = part;
|
||||||
|
drawing = part.BaseDrawing;
|
||||||
|
placedProgram = part.Program;
|
||||||
|
cleanProgram = drawing.Program;
|
||||||
|
x = BitConverter.DoubleToInt64Bits(part.Location.X);
|
||||||
|
y = BitConverter.DoubleToInt64Bits(part.Location.Y);
|
||||||
|
rotation = BitConverter.DoubleToInt64Bits(part.Rotation);
|
||||||
|
isCutOff = drawing.IsCutOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Matches(Part current) =>
|
||||||
|
ReferenceEquals(part, current)
|
||||||
|
&& ReferenceEquals(drawing, current.BaseDrawing)
|
||||||
|
&& ReferenceEquals(placedProgram, current.Program)
|
||||||
|
&& ReferenceEquals(cleanProgram, current.BaseDrawing.Program)
|
||||||
|
&& x == BitConverter.DoubleToInt64Bits(current.Location.X)
|
||||||
|
&& y == BitConverter.DoubleToInt64Bits(current.Location.Y)
|
||||||
|
&& rotation == BitConverter.DoubleToInt64Bits(current.Rotation)
|
||||||
|
&& isCutOff == current.BaseDrawing.IsCutOff;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Diagnostics;
|
||||||
|
|
||||||
|
public enum OverlapDisplayMode { Off, Areas }
|
||||||
|
public enum OverlapCheckStatus { NotChecked, Checking, Current, Incomplete, Failed, Canceled, Stale }
|
||||||
|
|
||||||
|
/// <summary>UI-thread lifecycle policy, independent of workers, GDI and view transforms.</summary>
|
||||||
|
public sealed class OverlapReportState
|
||||||
|
{
|
||||||
|
private OverlapGeometryStamp stamp;
|
||||||
|
private int uncheckedPartCount;
|
||||||
|
|
||||||
|
public long Generation { get; private set; }
|
||||||
|
public OverlapCheckStatus Status { get; private set; } = OverlapCheckStatus.NotChecked;
|
||||||
|
public OverlapDisplayMode DisplayMode { get; set; } = OverlapDisplayMode.Areas;
|
||||||
|
public PlateOverlapReport Report { get; private set; }
|
||||||
|
public bool IsRunning => Status == OverlapCheckStatus.Checking;
|
||||||
|
|
||||||
|
public string Message => Status switch
|
||||||
|
{
|
||||||
|
OverlapCheckStatus.Checking => "Checking overlaps…",
|
||||||
|
OverlapCheckStatus.Current => Report.Pairs.Count == 0
|
||||||
|
? "No material overlaps detected" : $"Overlaps: {Report.Pairs.Count} pairs",
|
||||||
|
OverlapCheckStatus.Incomplete => $"Overlap check incomplete: {Report.Pairs.Count} overlapping pairs; "
|
||||||
|
+ $"{uncheckedPartCount} parts could not be checked",
|
||||||
|
OverlapCheckStatus.Failed => "Overlap check failed — run Check Overlaps again",
|
||||||
|
OverlapCheckStatus.Canceled => "Overlap check canceled",
|
||||||
|
OverlapCheckStatus.Stale => "Overlap check out of date — run Check Overlaps again",
|
||||||
|
_ => "Overlaps: not checked"
|
||||||
|
};
|
||||||
|
|
||||||
|
public long Begin(Plate plate)
|
||||||
|
{
|
||||||
|
Clear(OverlapCheckStatus.Checking);
|
||||||
|
stamp = OverlapGeometryStamp.Capture(plate);
|
||||||
|
DisplayMode = OverlapDisplayMode.Areas;
|
||||||
|
return Generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryPublish(long generation, Plate plate, PlateOverlapReport report)
|
||||||
|
{
|
||||||
|
if (!CanComplete(generation, plate))
|
||||||
|
return false;
|
||||||
|
Report = report;
|
||||||
|
uncheckedPartCount = CountUncheckedParts(report.Issues);
|
||||||
|
Status = report.IsComplete ? OverlapCheckStatus.Current : OverlapCheckStatus.Incomplete;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryFail(long generation, Plate plate)
|
||||||
|
{
|
||||||
|
if (!CanComplete(generation, plate))
|
||||||
|
return false;
|
||||||
|
Clear(OverlapCheckStatus.Failed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanComplete(long generation, Plate plate) =>
|
||||||
|
generation == Generation && IsRunning && EnsureFresh(plate);
|
||||||
|
|
||||||
|
public bool EnsureFresh(Plate plate)
|
||||||
|
{
|
||||||
|
if (stamp == null)
|
||||||
|
return false;
|
||||||
|
if (stamp.Matches(plate))
|
||||||
|
return true;
|
||||||
|
Invalidate();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invalidate()
|
||||||
|
{
|
||||||
|
if (Status is OverlapCheckStatus.Checking or OverlapCheckStatus.Current or OverlapCheckStatus.Incomplete)
|
||||||
|
Clear(OverlapCheckStatus.Stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
if (IsRunning)
|
||||||
|
Clear(OverlapCheckStatus.Canceled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset() => Clear(OverlapCheckStatus.NotChecked);
|
||||||
|
|
||||||
|
private void Clear(OverlapCheckStatus status)
|
||||||
|
{
|
||||||
|
Generation++;
|
||||||
|
Report = null;
|
||||||
|
uncheckedPartCount = 0;
|
||||||
|
stamp = null;
|
||||||
|
Status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int CountUncheckedParts(IEnumerable<PlateOverlapIssue> issues)
|
||||||
|
{
|
||||||
|
var ids = new HashSet<int>();
|
||||||
|
foreach (var issue in issues)
|
||||||
|
{
|
||||||
|
ids.Add(issue.PartAId);
|
||||||
|
if (issue.PartBId.HasValue)
|
||||||
|
ids.Add(issue.PartBId.Value);
|
||||||
|
}
|
||||||
|
return ids.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Diagnostics;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Diagnostics;
|
||||||
|
|
||||||
|
public class OverlapReportStateTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RestartRejectsOldSuccessAndFailureWithoutDisturbingNewRequest()
|
||||||
|
{
|
||||||
|
var plate = PlateWithParts();
|
||||||
|
var state = new OverlapReportState();
|
||||||
|
var old = state.Begin(plate);
|
||||||
|
var current = state.Begin(plate);
|
||||||
|
Assert.False(state.TryPublish(old, plate, Analyze(plate)));
|
||||||
|
Assert.False(state.TryFail(old, plate));
|
||||||
|
Assert.Equal(OverlapCheckStatus.Checking, state.Status);
|
||||||
|
Assert.True(state.TryPublish(current, plate, Analyze(plate)));
|
||||||
|
Assert.Equal(OverlapCheckStatus.Current, state.Status);
|
||||||
|
Assert.Equal("Overlaps: 1 pairs", state.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("move-x")]
|
||||||
|
[InlineData("move-y")]
|
||||||
|
[InlineData("rotate")]
|
||||||
|
[InlineData("reorder")]
|
||||||
|
[InlineData("replace-part")]
|
||||||
|
[InlineData("replace-drawing")]
|
||||||
|
[InlineData("placed-program")]
|
||||||
|
[InlineData("clean-program")]
|
||||||
|
[InlineData("add")]
|
||||||
|
[InlineData("remove")]
|
||||||
|
[InlineData("plate")]
|
||||||
|
public void ExactOrderedStampRejectsEditsBeforePublishAndAfterCompletion(string edit)
|
||||||
|
{
|
||||||
|
foreach (var publishFirst in new[] { false, true })
|
||||||
|
{
|
||||||
|
var plate = PlateWithParts();
|
||||||
|
var state = new OverlapReportState();
|
||||||
|
var request = state.Begin(plate);
|
||||||
|
var report = Analyze(plate);
|
||||||
|
if (publishFirst)
|
||||||
|
Assert.True(state.TryPublish(request, plate, report));
|
||||||
|
var part = plate.Parts[0];
|
||||||
|
switch (edit)
|
||||||
|
{
|
||||||
|
case "move-x": part.Offset(1e-10, 0); break;
|
||||||
|
case "move-y": part.Offset(0, 1e-10); break;
|
||||||
|
case "rotate": part.Rotate(0.01); break;
|
||||||
|
case "reorder": (plate.Parts[0], plate.Parts[1]) = (plate.Parts[1], plate.Parts[0]); break;
|
||||||
|
case "replace-part": plate.Parts[0] = part.CloneAtOffset(new Vector()); break;
|
||||||
|
case "replace-drawing": plate.Parts[0] = Rectangle(); break;
|
||||||
|
case "placed-program": part.Update(); break;
|
||||||
|
case "clean-program": part.BaseDrawing.Program = (Program)part.BaseDrawing.Program.Clone(); break;
|
||||||
|
case "add": plate.Parts.Add(Rectangle()); break;
|
||||||
|
case "remove": plate.Parts.RemoveAt(0); break;
|
||||||
|
case "plate":
|
||||||
|
var replacement = new Plate();
|
||||||
|
replacement.Parts.AddRange(plate.Parts);
|
||||||
|
plate = replacement;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Assert.False(state.EnsureFresh(plate));
|
||||||
|
Assert.False(state.TryPublish(request, plate, report));
|
||||||
|
Assert.Null(state.Report);
|
||||||
|
Assert.Equal(OverlapCheckStatus.Stale, state.Status);
|
||||||
|
Assert.Equal("Overlap check out of date — run Check Overlaps again", state.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CancelFailureAndIncompleteAreNeverClear()
|
||||||
|
{
|
||||||
|
var plate = new Plate();
|
||||||
|
var state = new OverlapReportState();
|
||||||
|
var request = state.Begin(plate);
|
||||||
|
state.Cancel();
|
||||||
|
Assert.False(state.TryPublish(request, plate, Analyze(plate)));
|
||||||
|
Assert.Equal(OverlapCheckStatus.Canceled, state.Status);
|
||||||
|
Assert.Null(state.Report);
|
||||||
|
request = state.Begin(plate);
|
||||||
|
Assert.True(state.TryFail(request, plate));
|
||||||
|
Assert.Equal(OverlapCheckStatus.Failed, state.Status);
|
||||||
|
Assert.Contains("failed", state.Message);
|
||||||
|
Assert.Null(state.Report);
|
||||||
|
|
||||||
|
plate.Parts.Add(new Part(new Drawing("open", new Program())));
|
||||||
|
request = state.Begin(plate);
|
||||||
|
Assert.True(state.TryPublish(request, plate, Analyze(plate)));
|
||||||
|
Assert.Equal(OverlapCheckStatus.Incomplete, state.Status);
|
||||||
|
Assert.Equal("Overlap check incomplete: 0 overlapping pairs; 1 parts could not be checked", state.Message);
|
||||||
|
state.Invalidate();
|
||||||
|
Assert.Equal(OverlapCheckStatus.Stale, state.Status);
|
||||||
|
Assert.Null(state.Report);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IssueCountIncludesBothPairIdsAndDeduplicatesAcrossAllIssues()
|
||||||
|
{
|
||||||
|
var issues = new[]
|
||||||
|
{
|
||||||
|
new PlateOverlapIssue(2, null, "input"),
|
||||||
|
new PlateOverlapIssue(2, 4, "pair"),
|
||||||
|
new PlateOverlapIssue(4, 7, "pair"),
|
||||||
|
new PlateOverlapIssue(7, null, "input")
|
||||||
|
};
|
||||||
|
Assert.Equal(3, OverlapReportState.CountUncheckedParts(issues));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisplayAndNonGeometryChangesRetainReportAndGeneration()
|
||||||
|
{
|
||||||
|
var plate = PlateWithParts();
|
||||||
|
var state = new OverlapReportState();
|
||||||
|
Assert.Equal(OverlapDisplayMode.Areas, state.DisplayMode);
|
||||||
|
var request = state.Begin(plate);
|
||||||
|
var report = Analyze(plate);
|
||||||
|
Assert.True(state.TryPublish(request, plate, report));
|
||||||
|
state.DisplayMode = OverlapDisplayMode.Off;
|
||||||
|
plate.Parts[0].BaseDrawing.Name = "renamed";
|
||||||
|
plate.Quantity = 2;
|
||||||
|
plate.PartSpacing = 123;
|
||||||
|
Assert.True(state.EnsureFresh(plate));
|
||||||
|
Assert.Same(report, state.Report);
|
||||||
|
Assert.Equal(request, state.Generation);
|
||||||
|
state.DisplayMode = OverlapDisplayMode.Areas;
|
||||||
|
Assert.Same(report, state.Report);
|
||||||
|
Assert.Equal(request, state.Generation);
|
||||||
|
state.DisplayMode = OverlapDisplayMode.Off;
|
||||||
|
state.Begin(plate);
|
||||||
|
Assert.Equal(OverlapDisplayMode.Areas, state.DisplayMode);
|
||||||
|
Assert.Null(state.Report);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NewPlateResetsStateButRetainsDocumentDisplayPreference()
|
||||||
|
{
|
||||||
|
var plate = PlateWithParts();
|
||||||
|
var state = new OverlapReportState();
|
||||||
|
var request = state.Begin(plate);
|
||||||
|
state.DisplayMode = OverlapDisplayMode.Off;
|
||||||
|
state.Reset();
|
||||||
|
Assert.Equal(OverlapCheckStatus.NotChecked, state.Status);
|
||||||
|
Assert.Null(state.Report);
|
||||||
|
Assert.Equal(OverlapDisplayMode.Off, state.DisplayMode);
|
||||||
|
Assert.False(state.TryPublish(request, plate, Analyze(plate)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CutoffSlotsRemainPartOfTheOrderedIdentityStamp()
|
||||||
|
{
|
||||||
|
var plate = PlateWithParts();
|
||||||
|
var cutoff = Rectangle();
|
||||||
|
cutoff.BaseDrawing.IsCutOff = true;
|
||||||
|
plate.Parts.Insert(0, cutoff);
|
||||||
|
var state = new OverlapReportState();
|
||||||
|
Assert.True(state.TryPublish(state.Begin(plate), plate, Analyze(plate)));
|
||||||
|
var pair = Assert.Single(state.Report.Pairs);
|
||||||
|
Assert.Equal((1, 2), (pair.PartAId, pair.PartBId));
|
||||||
|
plate.Parts.RemoveAt(0);
|
||||||
|
Assert.False(state.EnsureFresh(plate));
|
||||||
|
Assert.Null(state.Report);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InPlaceGeometryEditorsMustExplicitlyInvalidate()
|
||||||
|
{
|
||||||
|
var plate = PlateWithParts();
|
||||||
|
var state = new OverlapReportState();
|
||||||
|
var request = state.Begin(plate);
|
||||||
|
var report = Analyze(plate);
|
||||||
|
// Deliberately not serialized or converted by the stamp.
|
||||||
|
plate.Parts[0].BaseDrawing.Program.Codes.Clear();
|
||||||
|
Assert.True(state.EnsureFresh(plate));
|
||||||
|
state.Invalidate();
|
||||||
|
Assert.False(state.TryPublish(request, plate, report));
|
||||||
|
Assert.Equal(OverlapCheckStatus.Stale, state.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyCompleteReportAloneCanSayClear()
|
||||||
|
{
|
||||||
|
var plate = new Plate();
|
||||||
|
var state = new OverlapReportState();
|
||||||
|
Assert.Equal(OverlapCheckStatus.NotChecked, state.Status);
|
||||||
|
Assert.True(state.TryPublish(state.Begin(plate), plate, Analyze(plate)));
|
||||||
|
Assert.Equal("No material overlaps detected", state.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlateOverlapReport Analyze(Plate plate) => PlateOverlapAnalyzer.Analyze(plate.Parts.ToArray());
|
||||||
|
|
||||||
|
private static Plate PlateWithParts()
|
||||||
|
{
|
||||||
|
var plate = new Plate();
|
||||||
|
plate.Parts.Add(Rectangle());
|
||||||
|
plate.Parts.Add(Rectangle());
|
||||||
|
return plate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Part Rectangle()
|
||||||
|
{
|
||||||
|
var program = new Program(Mode.Absolute);
|
||||||
|
program.Codes.Add(new RapidMove(0, 0));
|
||||||
|
program.Codes.Add(new LinearMove(4, 0));
|
||||||
|
program.Codes.Add(new LinearMove(4, 4));
|
||||||
|
program.Codes.Add(new LinearMove(0, 4));
|
||||||
|
program.Codes.Add(new LinearMove(0, 0));
|
||||||
|
return new Part(new Drawing("same name", program));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Controls;
|
||||||
|
using OpenNest.Diagnostics;
|
||||||
|
using OpenNest.Forms;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.WinForms.Tests;
|
||||||
|
|
||||||
|
[CollectionDefinition("Overlap overlay", DisableParallelization = true)]
|
||||||
|
public class OverlapOverlayCollection;
|
||||||
|
|
||||||
|
[Collection("Overlap overlay")]
|
||||||
|
public class PlateOverlapOverlayTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RestartAndCancelRejectLateCompletionsOnTheUiThread() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var run = new OverlayRun();
|
||||||
|
var uiThread = Environment.CurrentManagedThreadId;
|
||||||
|
run.View.OverlapStateChanged += (_, _) => Assert.Equal(uiThread, Environment.CurrentManagedThreadId);
|
||||||
|
var first = run.Start();
|
||||||
|
var old = run.Next();
|
||||||
|
var second = run.Start();
|
||||||
|
var current = run.Next();
|
||||||
|
Assert.True(old.Token.IsCancellationRequested);
|
||||||
|
old.Complete();
|
||||||
|
run.Pump(first);
|
||||||
|
Assert.Equal(OverlapCheckStatus.Checking, run.View.OverlapStatus);
|
||||||
|
current.Complete();
|
||||||
|
run.Pump(second);
|
||||||
|
Assert.Equal(OverlapCheckStatus.Current, run.View.OverlapStatus);
|
||||||
|
Assert.Single(run.View.OverlapReport.Pairs);
|
||||||
|
|
||||||
|
var canceled = run.Start();
|
||||||
|
var late = run.Next();
|
||||||
|
run.View.CancelOverlapCheck();
|
||||||
|
Assert.True(late.Token.IsCancellationRequested);
|
||||||
|
late.Complete();
|
||||||
|
run.Pump(canceled);
|
||||||
|
Assert.Equal(OverlapCheckStatus.Canceled, run.View.OverlapStatus);
|
||||||
|
Assert.Null(run.View.OverlapReport);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("move")]
|
||||||
|
[InlineData("add")]
|
||||||
|
[InlineData("remove")]
|
||||||
|
[InlineData("replace")]
|
||||||
|
[InlineData("reorder")]
|
||||||
|
[InlineData("editor")]
|
||||||
|
[InlineData("plate")]
|
||||||
|
public void EditOrPlateSwitchRejectsPendingReport(string edit) => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var run = new OverlayRun();
|
||||||
|
var task = run.Start();
|
||||||
|
var work = run.Next();
|
||||||
|
var plate = run.View.Plate;
|
||||||
|
switch (edit)
|
||||||
|
{
|
||||||
|
case "move": plate.Parts[0].Offset(1e-10, 0); break;
|
||||||
|
case "add": plate.Parts.Add(Rectangle()); break;
|
||||||
|
case "remove": plate.Parts.RemoveAt(0); break;
|
||||||
|
case "replace": plate.Parts[0] = Rectangle(); break;
|
||||||
|
case "reorder": (plate.Parts[0], plate.Parts[1]) = (plate.Parts[1], plate.Parts[0]); break;
|
||||||
|
case "editor": run.View.InvalidateOverlapCheck(); break;
|
||||||
|
case "plate": run.View.Plate = new Plate(); break;
|
||||||
|
}
|
||||||
|
if (edit != "move")
|
||||||
|
Assert.True(work.Token.IsCancellationRequested);
|
||||||
|
work.Complete();
|
||||||
|
run.Pump(task);
|
||||||
|
Assert.True(work.Token.IsCancellationRequested);
|
||||||
|
Assert.Null(run.View.OverlapReport);
|
||||||
|
Assert.Equal(edit == "plate" ? OverlapCheckStatus.NotChecked : OverlapCheckStatus.Stale,
|
||||||
|
run.View.OverlapStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PaintChecksFreshnessEvenWhenDisplayIsOff() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var run = new OverlayRun();
|
||||||
|
run.Finish();
|
||||||
|
using var image = new Bitmap(240, 240);
|
||||||
|
using var graphics = Graphics.FromImage(image);
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
Assert.NotNull(run.View.OverlapOverlay.CachedPath);
|
||||||
|
run.View.OverlapDisplay = OverlapDisplayMode.Off;
|
||||||
|
run.View.Plate.Parts[0].Offset(1e-10, 0);
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
||||||
|
Assert.Null(run.View.OverlapReport);
|
||||||
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisplayPanZoomAndPreviewDoNotAnalyzeOrChangeCommittedParts() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var run = new OverlayRun();
|
||||||
|
var parts = run.View.Plate.Parts.ToArray();
|
||||||
|
var locations = parts.Select(p => p.Location).ToArray();
|
||||||
|
var programs = parts.Select(p => p.Program).ToArray();
|
||||||
|
var quantities = parts.Select(p => p.BaseDrawing.Quantity.Nested).ToArray();
|
||||||
|
run.View.SelectAll();
|
||||||
|
var selection = run.View.SelectedParts.ToArray();
|
||||||
|
run.View.SetActiveParts(new List<Part> { Rectangle() });
|
||||||
|
run.View.OverlapDisplay = OverlapDisplayMode.Off;
|
||||||
|
run.Finish();
|
||||||
|
Assert.Equal(OverlapDisplayMode.Areas, run.View.OverlapDisplay);
|
||||||
|
Assert.Single(run.View.OverlapReport.Pairs); // Preview is not a third input.
|
||||||
|
var report = run.View.OverlapReport;
|
||||||
|
using var image = new Bitmap(240, 240);
|
||||||
|
using var graphics = Graphics.FromImage(image);
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
var path = run.View.OverlapOverlay.CachedPath;
|
||||||
|
Assert.Equal(FillMode.Winding, path.FillMode);
|
||||||
|
run.View.Pan(35, -20);
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
||||||
|
run.View.OverlapDisplay = OverlapDisplayMode.Off;
|
||||||
|
run.View.OverlapDisplay = OverlapDisplayMode.Areas;
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
||||||
|
run.View.ZoomToPoint(new Vector(), 2);
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
Assert.NotSame(path, run.View.OverlapOverlay.CachedPath);
|
||||||
|
Assert.Same(report, run.View.OverlapReport);
|
||||||
|
Assert.Equal(1, run.Calls);
|
||||||
|
Assert.Equal(parts, run.View.Plate.Parts.ToArray());
|
||||||
|
Assert.Equal(locations, parts.Select(p => p.Location));
|
||||||
|
Assert.Equal(programs, parts.Select(p => p.Program));
|
||||||
|
Assert.Equal(quantities, parts.Select(p => p.BaseDrawing.Quantity.Nested));
|
||||||
|
Assert.Equal(selection, run.View.SelectedParts);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TripleOverlapUsesOneFillAndGraphCoordinatesDoNotDoublePan() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var run = new OverlayRun();
|
||||||
|
run.View.Plate.Parts.Clear();
|
||||||
|
run.View.Plate.Parts.Add(Rectangle());
|
||||||
|
run.View.Plate.Parts.Add(Rectangle(2));
|
||||||
|
run.View.Plate.Parts.Add(Rectangle(3));
|
||||||
|
run.Finish();
|
||||||
|
run.View.ZoomToPoint(new Vector(), 20);
|
||||||
|
run.View.SetOrigin(60, 220);
|
||||||
|
using var image = new Bitmap(300, 300);
|
||||||
|
using var graphics = Graphics.FromImage(image);
|
||||||
|
graphics.Clear(Color.White);
|
||||||
|
graphics.TranslateTransform(60, 220);
|
||||||
|
var transform = graphics.Transform.Elements;
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
Assert.Equal(transform, graphics.Transform.Elements); // Label restores graph space.
|
||||||
|
Assert.Equal(image.GetPixel(110, 180), image.GetPixel(130, 180)); // Double/triple material, same alpha.
|
||||||
|
Assert.NotEqual(Color.White.ToArgb(), image.GetPixel(130, 180).ToArgb());
|
||||||
|
Assert.Equal(Color.White.ToArgb(), image.GetPixel(70, 180).ToArgb());
|
||||||
|
});
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void HandleDestructionOrDisposalNeverResurrectsRequests(bool dispose) => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var run = new OverlayRun();
|
||||||
|
run.Finish();
|
||||||
|
using var image = new Bitmap(100, 100);
|
||||||
|
using var graphics = Graphics.FromImage(image);
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
Assert.NotNull(run.View.OverlapOverlay.CachedPath);
|
||||||
|
var task = run.Start();
|
||||||
|
var work = run.Next();
|
||||||
|
if (dispose)
|
||||||
|
run.View.Dispose();
|
||||||
|
else
|
||||||
|
run.View.Recreate();
|
||||||
|
Assert.True(work.Token.IsCancellationRequested);
|
||||||
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
||||||
|
work.Complete();
|
||||||
|
run.Pump(task);
|
||||||
|
Assert.Null(run.View.OverlapReport);
|
||||||
|
Assert.False(run.View.IsOverlapCheckRunning);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompletedPathIsReleasedOnHandleRecreationAndCannotReappear() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var run = new OverlayRun();
|
||||||
|
run.Finish();
|
||||||
|
using var image = new Bitmap(100, 100);
|
||||||
|
using var graphics = Graphics.FromImage(image);
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
Assert.NotNull(run.View.OverlapOverlay.CachedPath);
|
||||||
|
run.View.Recreate();
|
||||||
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
||||||
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
||||||
|
Assert.Null(run.View.OverlapReport);
|
||||||
|
Assert.Equal(1, run.Calls);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FailureAndIncompleteAreVisibleAndNeverReportedAsClear() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var run = new OverlayRun();
|
||||||
|
var task = run.Start();
|
||||||
|
run.Next().Fail();
|
||||||
|
run.Pump(task);
|
||||||
|
Assert.Equal(OverlapCheckStatus.Failed, run.View.OverlapStatus);
|
||||||
|
Assert.Contains("failed", run.View.Status);
|
||||||
|
Assert.Null(run.View.OverlapReport);
|
||||||
|
run.View.Plate.Parts.Add(new Part(new Drawing("invalid", new Program())));
|
||||||
|
run.Finish();
|
||||||
|
Assert.Equal(OverlapCheckStatus.Incomplete, run.View.OverlapStatus);
|
||||||
|
Assert.Contains("incomplete: 1 overlapping pairs; 1 parts", run.View.Status);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SnapshotKeepsOriginalLabelsWhenLiveNamesChange() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var run = new OverlayRun();
|
||||||
|
var task = run.Start();
|
||||||
|
var work = run.Next();
|
||||||
|
run.View.Plate.Parts[0].BaseDrawing.Name = "new label";
|
||||||
|
work.Complete();
|
||||||
|
run.Pump(task);
|
||||||
|
Assert.Equal("same name", Assert.Single(run.View.OverlapReport.Pairs).PartAName);
|
||||||
|
Assert.Equal(OverlapCheckStatus.Current, run.View.OverlapStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("add")]
|
||||||
|
[InlineData("remove")]
|
||||||
|
[InlineData("replace")]
|
||||||
|
[InlineData("clear")]
|
||||||
|
public void CollectionEventsImmediatelyDiscardCurrentReportAndPath(string edit) => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var run = new OverlayRun();
|
||||||
|
run.Finish();
|
||||||
|
using var image = new Bitmap(100, 100);
|
||||||
|
using var graphics = Graphics.FromImage(image);
|
||||||
|
run.View.OverlapOverlay.Draw(graphics);
|
||||||
|
Assert.NotNull(run.View.OverlapOverlay.CachedPath);
|
||||||
|
switch (edit)
|
||||||
|
{
|
||||||
|
case "add": run.View.Plate.Parts.Add(Rectangle()); break;
|
||||||
|
case "remove": run.View.Plate.Parts.RemoveAt(0); break;
|
||||||
|
case "replace": run.View.Plate.Parts[0] = Rectangle(); break;
|
||||||
|
case "clear": run.View.Plate.Parts.Clear(); break;
|
||||||
|
}
|
||||||
|
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
||||||
|
Assert.Null(run.View.OverlapReport);
|
||||||
|
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MenusFollowMdiActivationDocumentDisplayAndRunningState() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var host = new MenuHost();
|
||||||
|
host.Show();
|
||||||
|
var check = Menu(host, "mnuOverlapCheckActive");
|
||||||
|
var cancel = Menu(host, "mnuOverlapCancel");
|
||||||
|
var off = Menu(host, "mnuOverlapOff");
|
||||||
|
var areas = Menu(host, "mnuOverlapAreas");
|
||||||
|
Assert.False(check.Enabled);
|
||||||
|
Assert.False(cancel.Enabled);
|
||||||
|
Assert.False(off.Enabled);
|
||||||
|
Assert.False(areas.Enabled);
|
||||||
|
Assert.All(new[] { check, cancel, off, areas }, item => Assert.Equal(Keys.None, item.ShortcutKeys));
|
||||||
|
|
||||||
|
using var first = new EditNestForm(new Nest("first")) { MdiParent = host };
|
||||||
|
first.Show();
|
||||||
|
Assert.True(check.Enabled);
|
||||||
|
Assert.True(areas.Checked);
|
||||||
|
off.PerformClick();
|
||||||
|
Assert.Equal(OverlapDisplayMode.Off, first.OverlapDisplay);
|
||||||
|
using var second = new EditNestForm(new Nest("second")) { MdiParent = host };
|
||||||
|
second.Show();
|
||||||
|
Assert.True(areas.Checked);
|
||||||
|
Assert.False(off.Checked);
|
||||||
|
first.Activate();
|
||||||
|
Assert.True(off.Checked);
|
||||||
|
Assert.False(areas.Checked);
|
||||||
|
Assert.Equal(OverlapDisplayMode.Areas, second.OverlapDisplay);
|
||||||
|
|
||||||
|
var previous = SynchronizationContext.Current;
|
||||||
|
var context = new PumpContext();
|
||||||
|
using var pending = new BlockingCollection<Work>();
|
||||||
|
SynchronizationContext.SetSynchronizationContext(context);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
first.PlateView.OverlapOverlay.Analyze = (snapshot, token) =>
|
||||||
|
{
|
||||||
|
var request = new Work(snapshot, token);
|
||||||
|
pending.Add(request);
|
||||||
|
return request.WaitForResult();
|
||||||
|
};
|
||||||
|
var task = first.CheckOverlapsAsync();
|
||||||
|
Assert.True(pending.TryTake(out var request, TimeSpan.FromSeconds(15)));
|
||||||
|
Assert.True(cancel.Enabled);
|
||||||
|
Assert.True(areas.Checked); // Checking from Off selects Areas.
|
||||||
|
cancel.PerformClick();
|
||||||
|
Assert.False(cancel.Enabled);
|
||||||
|
Assert.True(request!.Token.IsCancellationRequested);
|
||||||
|
request.Complete();
|
||||||
|
context.Pump(task);
|
||||||
|
Assert.Equal(OverlapCheckStatus.Canceled, first.PlateView.OverlapStatus);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SynchronizationContext.SetSynchronizationContext(previous);
|
||||||
|
}
|
||||||
|
first.Dispose();
|
||||||
|
second.Dispose();
|
||||||
|
Assert.False(check.Enabled);
|
||||||
|
Assert.False(cancel.Enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
private static ToolStripMenuItem Menu(MainForm host, string name) =>
|
||||||
|
(ToolStripMenuItem)host.MainMenuStrip!.Items.Find(name, true).Single();
|
||||||
|
|
||||||
|
private sealed class MenuHost : MainForm
|
||||||
|
{
|
||||||
|
// Do not run startup migrations/automatic new-document creation or save test window settings.
|
||||||
|
protected override void OnLoad(EventArgs e) { }
|
||||||
|
protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestView : PlateView
|
||||||
|
{
|
||||||
|
public void Pan(float x, float y) { origin.X += x; origin.Y += y; }
|
||||||
|
public void SetOrigin(float x, float y) => origin = new PointF(x, y);
|
||||||
|
public void Recreate() => RecreateHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class OverlayRun : IDisposable
|
||||||
|
{
|
||||||
|
private readonly PumpContext context = new();
|
||||||
|
private readonly SynchronizationContext? previous;
|
||||||
|
private readonly BlockingCollection<Work> work = new();
|
||||||
|
public TestView View { get; } = new();
|
||||||
|
public int Calls;
|
||||||
|
|
||||||
|
public OverlayRun()
|
||||||
|
{
|
||||||
|
View.CreateControl();
|
||||||
|
previous = SynchronizationContext.Current;
|
||||||
|
SynchronizationContext.SetSynchronizationContext(context);
|
||||||
|
View.Plate.Parts.Add(Rectangle());
|
||||||
|
View.Plate.Parts.Add(Rectangle(1));
|
||||||
|
View.OverlapOverlay.Analyze = (snapshot, token) =>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref Calls);
|
||||||
|
var request = new Work(snapshot, token);
|
||||||
|
work.Add(request);
|
||||||
|
return request.WaitForResult();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Start() => View.CheckOverlapsAsync();
|
||||||
|
public Work Next()
|
||||||
|
{
|
||||||
|
Assert.True(work.TryTake(out var result, TimeSpan.FromSeconds(15)), "Worker did not start.");
|
||||||
|
return result!;
|
||||||
|
}
|
||||||
|
public void Pump(Task task) => context.Pump(task);
|
||||||
|
public void Finish()
|
||||||
|
{
|
||||||
|
var task = Start();
|
||||||
|
Next().Complete();
|
||||||
|
Pump(task);
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
View.Dispose();
|
||||||
|
SynchronizationContext.SetSynchronizationContext(previous);
|
||||||
|
work.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Work(PlateOverlapSnapshot snapshot, CancellationToken token)
|
||||||
|
{
|
||||||
|
public CancellationToken Token { get; } = token;
|
||||||
|
public TaskCompletionSource<PlateOverlapReport> Result { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
// Only called by Task.Run workers; the STA supplies the result and pumps continuations.
|
||||||
|
public PlateOverlapReport WaitForResult() => Result.Task.GetAwaiter().GetResult();
|
||||||
|
public void Complete() => Result.SetResult(PlateOverlapAnalyzer.Analyze(snapshot));
|
||||||
|
public void Fail() => Result.SetException(new InvalidOperationException("test failure"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PumpContext : SynchronizationContext
|
||||||
|
{
|
||||||
|
private readonly BlockingCollection<System.Action> callbacks = new();
|
||||||
|
public override void Post(SendOrPostCallback callback, object? state) => callbacks.Add(() => callback(state));
|
||||||
|
public void Pump(Task task)
|
||||||
|
{
|
||||||
|
while (!task.IsCompleted)
|
||||||
|
{
|
||||||
|
Assert.True(callbacks.TryTake(out var callback, TimeSpan.FromSeconds(15)), "UI continuation did not arrive.");
|
||||||
|
callback!();
|
||||||
|
}
|
||||||
|
task.GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Part Rectangle(double x = 0)
|
||||||
|
{
|
||||||
|
var program = new Program(Mode.Absolute);
|
||||||
|
program.Codes.Add(new RapidMove(0, 0));
|
||||||
|
program.Codes.Add(new LinearMove(4, 0));
|
||||||
|
program.Codes.Add(new LinearMove(4, 4));
|
||||||
|
program.Codes.Add(new LinearMove(0, 4));
|
||||||
|
program.Codes.Add(new LinearMove(0, 0));
|
||||||
|
return new Part(new Drawing("same name", program), new Vector(x, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunSta(System.Action action)
|
||||||
|
{
|
||||||
|
Exception? failure = null;
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try { action(); }
|
||||||
|
catch (Exception ex) { failure = ex; }
|
||||||
|
})
|
||||||
|
{ IsBackground = true };
|
||||||
|
thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
thread.Start();
|
||||||
|
Assert.True(thread.Join(TimeSpan.FromSeconds(60)), "The STA test did not complete.");
|
||||||
|
if (failure != null)
|
||||||
|
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OpenNest.Collections;
|
||||||
|
using OpenNest.Diagnostics;
|
||||||
|
|
||||||
|
namespace OpenNest.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI-thread owner of manual overlap requests and their cached display geometry.
|
||||||
|
/// Workers see only owned analyzer snapshots, never the live plate or view.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class OverlapOverlayController : IDisposable
|
||||||
|
{
|
||||||
|
private readonly PlateView view;
|
||||||
|
private readonly OverlapReportState state = new();
|
||||||
|
private ObservableList<Part> observedParts;
|
||||||
|
private CancellationTokenSource cancellation;
|
||||||
|
private GraphicsPath path;
|
||||||
|
private PlateOverlapReport pathReport;
|
||||||
|
private float pathScale;
|
||||||
|
private bool disposed;
|
||||||
|
|
||||||
|
public OverlapOverlayController(PlateView view) => this.view = view;
|
||||||
|
|
||||||
|
public event EventHandler StateChanged;
|
||||||
|
public OverlapCheckStatus Status => state.Status;
|
||||||
|
public PlateOverlapReport Report => state.Report;
|
||||||
|
public bool IsRunning => state.IsRunning;
|
||||||
|
|
||||||
|
// Deterministic worker seam for STA lifecycle tests. Capture always stays on the UI thread.
|
||||||
|
internal Func<PlateOverlapSnapshot, CancellationToken, PlateOverlapReport> Analyze { get; set; }
|
||||||
|
= PlateOverlapAnalyzer.Analyze;
|
||||||
|
internal GraphicsPath CachedPath => path;
|
||||||
|
|
||||||
|
public OverlapDisplayMode DisplayMode
|
||||||
|
{
|
||||||
|
get => state.DisplayMode;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (state.DisplayMode == value)
|
||||||
|
return;
|
||||||
|
state.DisplayMode = value;
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPlate(Plate plate)
|
||||||
|
{
|
||||||
|
Unsubscribe();
|
||||||
|
CancelWorker();
|
||||||
|
state.Reset();
|
||||||
|
ReleasePath();
|
||||||
|
observedParts = plate?.Parts;
|
||||||
|
if (observedParts != null)
|
||||||
|
{
|
||||||
|
observedParts.ItemAdded += PartAdded;
|
||||||
|
observedParts.ItemRemoved += PartRemoved;
|
||||||
|
observedParts.ItemChanged += PartChanged;
|
||||||
|
}
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CheckAsync()
|
||||||
|
{
|
||||||
|
if (!CanUseView || view.Plate == null)
|
||||||
|
return;
|
||||||
|
if (view.InvokeRequired)
|
||||||
|
throw new InvalidOperationException("Overlap checks must be started on the UI thread.");
|
||||||
|
|
||||||
|
CancelWorker();
|
||||||
|
var plate = view.Plate;
|
||||||
|
var generation = state.Begin(plate);
|
||||||
|
var source = new CancellationTokenSource();
|
||||||
|
cancellation = source;
|
||||||
|
var token = source.Token;
|
||||||
|
ReleasePath();
|
||||||
|
NotifyChanged();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Do not include PreviewManager parts. Capture owns clean geometry and stable names.
|
||||||
|
var snapshot = PlateOverlapAnalyzer.Capture(plate.Parts.ToArray(), token);
|
||||||
|
var analyze = Analyze;
|
||||||
|
var report = await Task.Run(() => analyze(snapshot, token), token);
|
||||||
|
if (!CanUseView || generation != state.Generation || !ReferenceEquals(plate, view.Plate))
|
||||||
|
return;
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
state.Cancel();
|
||||||
|
else if (!state.TryPublish(generation, view.Plate, report))
|
||||||
|
CancelWorker(); // A pose/reference mismatch discovered at publication is stale.
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (CanUseView && generation == state.Generation)
|
||||||
|
{
|
||||||
|
state.Cancel();
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Overlap check failed: {ex}");
|
||||||
|
if (CanUseView && generation == state.Generation)
|
||||||
|
{
|
||||||
|
if (!state.TryFail(generation, view.Plate))
|
||||||
|
CancelWorker();
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(cancellation, source))
|
||||||
|
cancellation = null;
|
||||||
|
source.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanUseView => !disposed && !view.IsDisposed && !view.Disposing && view.IsHandleCreated;
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
if (!state.IsRunning)
|
||||||
|
return;
|
||||||
|
CancelWorker();
|
||||||
|
state.Cancel();
|
||||||
|
ReleasePath();
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invalidate()
|
||||||
|
{
|
||||||
|
var generation = state.Generation;
|
||||||
|
state.Invalidate();
|
||||||
|
if (generation == state.Generation)
|
||||||
|
return;
|
||||||
|
CancelWorker();
|
||||||
|
ReleasePath();
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseHandle()
|
||||||
|
{
|
||||||
|
CancelWorker();
|
||||||
|
state.Cancel();
|
||||||
|
state.Invalidate();
|
||||||
|
ReleasePath();
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelWorker()
|
||||||
|
{
|
||||||
|
// The awaiting request disposes its own source after the worker exits.
|
||||||
|
var source = cancellation;
|
||||||
|
cancellation = null;
|
||||||
|
source?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyChanged()
|
||||||
|
{
|
||||||
|
if (disposed || view.IsDisposed || view.Disposing)
|
||||||
|
return;
|
||||||
|
view.Status = state.Message;
|
||||||
|
view.Invalidate();
|
||||||
|
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PartAdded(object sender, ItemAddedEventArgs<Part> e) => Invalidate();
|
||||||
|
private void PartRemoved(object sender, ItemRemovedEventArgs<Part> e) => Invalidate();
|
||||||
|
private void PartChanged(object sender, ItemChangedEventArgs<Part> e) => Invalidate();
|
||||||
|
|
||||||
|
private void Unsubscribe()
|
||||||
|
{
|
||||||
|
if (observedParts == null)
|
||||||
|
return;
|
||||||
|
observedParts.ItemAdded -= PartAdded;
|
||||||
|
observedParts.ItemRemoved -= PartRemoved;
|
||||||
|
observedParts.ItemChanged -= PartChanged;
|
||||||
|
observedParts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw(Graphics graphics)
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
return;
|
||||||
|
var generation = state.Generation;
|
||||||
|
state.EnsureFresh(view.Plate);
|
||||||
|
if (generation != state.Generation)
|
||||||
|
{
|
||||||
|
CancelWorker();
|
||||||
|
ReleasePath();
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.DisplayMode == OverlapDisplayMode.Areas && state.Report != null)
|
||||||
|
{
|
||||||
|
EnsurePath();
|
||||||
|
if (path.PointCount > 0)
|
||||||
|
{
|
||||||
|
using var brush = new SolidBrush(Color.FromArgb(100, 255, 0, 80));
|
||||||
|
// A single winding fill is the union: triple overlap is not painted darker.
|
||||||
|
graphics.FillPath(brush, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DrawStateLabel(graphics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsurePath()
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(pathReport, state.Report) && pathScale == view.ViewScale && path != null)
|
||||||
|
return;
|
||||||
|
ReleasePath();
|
||||||
|
var next = new GraphicsPath(FillMode.Winding);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var pair in state.Report.Pairs)
|
||||||
|
{
|
||||||
|
foreach (var region in pair.Regions)
|
||||||
|
{
|
||||||
|
var vertices = region.Vertices;
|
||||||
|
var points = new PointF[vertices.Count - 1]; // Analyzer repeats the closing vertex.
|
||||||
|
var signedArea = 0.0;
|
||||||
|
var start = vertices[0];
|
||||||
|
for (var i = 0; i < points.Length; i++)
|
||||||
|
{
|
||||||
|
var a = vertices[i] - start;
|
||||||
|
var b = vertices[i + 1] - start;
|
||||||
|
signedArea += a.X * b.Y - b.X * a.Y;
|
||||||
|
// The host has already translated by origin: graph, not control coordinates.
|
||||||
|
points[i] = view.PointWorldToGraph(vertices[i]);
|
||||||
|
}
|
||||||
|
if (signedArea < 0)
|
||||||
|
Array.Reverse(points);
|
||||||
|
next.AddPolygon(points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path = next;
|
||||||
|
pathReport = state.Report;
|
||||||
|
pathScale = view.ViewScale;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
next.Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawStateLabel(Graphics graphics)
|
||||||
|
{
|
||||||
|
var saved = graphics.Save();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
graphics.ResetTransform();
|
||||||
|
var text = state.Message + (state.DisplayMode == OverlapDisplayMode.Off ? " (display off)" : "");
|
||||||
|
var size = graphics.MeasureString(text, view.Font);
|
||||||
|
using var background = new SolidBrush(Color.FromArgb(235, Color.White));
|
||||||
|
graphics.FillRectangle(background, 6, 6, size.Width + 8, size.Height + 6);
|
||||||
|
graphics.DrawString(text, view.Font, Brushes.DarkRed, 10, 9);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
graphics.Restore(saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleasePath()
|
||||||
|
{
|
||||||
|
path?.Dispose();
|
||||||
|
path = null;
|
||||||
|
pathReport = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
return;
|
||||||
|
disposed = true;
|
||||||
|
Unsubscribe();
|
||||||
|
CancelWorker();
|
||||||
|
state.Reset();
|
||||||
|
ReleasePath();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
|||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using OpenNest.Actions;
|
using OpenNest.Actions;
|
||||||
using OpenNest.Collections;
|
using OpenNest.Collections;
|
||||||
|
using OpenNest.Diagnostics;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Engine.Jobs.Placement;
|
using OpenNest.Engine.Jobs.Placement;
|
||||||
@@ -23,6 +24,7 @@ namespace OpenNest.Controls
|
|||||||
public class PlateView : DrawControl
|
public class PlateView : DrawControl
|
||||||
{
|
{
|
||||||
private readonly Timer redrawTimer;
|
private readonly Timer redrawTimer;
|
||||||
|
private readonly OverlapOverlayController overlapOverlay;
|
||||||
|
|
||||||
private string status;
|
private string status;
|
||||||
private Plate plate;
|
private Plate plate;
|
||||||
@@ -86,6 +88,7 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
public PlateView(ColorScheme colorScheme)
|
public PlateView(ColorScheme colorScheme)
|
||||||
{
|
{
|
||||||
|
overlapOverlay = new OverlapOverlayController(this);
|
||||||
Plate = new Plate(60, 120);
|
Plate = new Plate(60, 120);
|
||||||
origin = new PointF();
|
origin = new PointF();
|
||||||
parts = new List<LayoutPart>();
|
parts = new List<LayoutPart>();
|
||||||
@@ -208,9 +211,28 @@ namespace OpenNest.Controls
|
|||||||
foreach (var part in plate.Parts)
|
foreach (var part in plate.Parts)
|
||||||
parts.Add(LayoutPart.Create(part, this));
|
parts.Add(LayoutPart.Create(part, this));
|
||||||
|
|
||||||
|
overlapOverlay.SetPlate(plate);
|
||||||
actionManager?.OnPlateChanged();
|
actionManager?.OnPlateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal OverlapOverlayController OverlapOverlay => overlapOverlay;
|
||||||
|
public OverlapCheckStatus OverlapStatus => overlapOverlay.Status;
|
||||||
|
public PlateOverlapReport OverlapReport => overlapOverlay.Report;
|
||||||
|
public bool IsOverlapCheckRunning => overlapOverlay.IsRunning;
|
||||||
|
public OverlapDisplayMode OverlapDisplay
|
||||||
|
{
|
||||||
|
get => overlapOverlay.DisplayMode;
|
||||||
|
set => overlapOverlay.DisplayMode = value;
|
||||||
|
}
|
||||||
|
public event EventHandler OverlapStateChanged
|
||||||
|
{
|
||||||
|
add => overlapOverlay.StateChanged += value;
|
||||||
|
remove => overlapOverlay.StateChanged -= value;
|
||||||
|
}
|
||||||
|
public Task CheckOverlapsAsync() => overlapOverlay.CheckAsync();
|
||||||
|
public void CancelOverlapCheck() => overlapOverlay.Cancel();
|
||||||
|
public void InvalidateOverlapCheck() => overlapOverlay.Invalidate();
|
||||||
|
|
||||||
public string Status
|
public string Status
|
||||||
{
|
{
|
||||||
get { return status; }
|
get { return status; }
|
||||||
@@ -491,6 +513,7 @@ namespace OpenNest.Controls
|
|||||||
renderer.DrawCutOffs(e.Graphics);
|
renderer.DrawCutOffs(e.Graphics);
|
||||||
renderer.DrawActiveWorkArea(e.Graphics);
|
renderer.DrawActiveWorkArea(e.Graphics);
|
||||||
renderer.DrawDebugRemnants(e.Graphics);
|
renderer.DrawDebugRemnants(e.Graphics);
|
||||||
|
DrawOverlapOverlay(e.Graphics);
|
||||||
|
|
||||||
base.OnPaint(e);
|
base.OnPaint(e);
|
||||||
|
|
||||||
@@ -515,12 +538,22 @@ namespace OpenNest.Controls
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawOverlapOverlay(Graphics graphics) => overlapOverlay.Draw(graphics);
|
||||||
|
|
||||||
protected override void OnHandleDestroyed(EventArgs e)
|
protected override void OnHandleDestroyed(EventArgs e)
|
||||||
{
|
{
|
||||||
|
overlapOverlay.ReleaseHandle();
|
||||||
base.OnHandleDestroyed(e);
|
base.OnHandleDestroyed(e);
|
||||||
actionManager.Cleanup();
|
actionManager.Cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
overlapOverlay?.Dispose();
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
public override void Refresh()
|
public override void Refresh()
|
||||||
{
|
{
|
||||||
parts.ForEach(p => p.Update(this));
|
parts.ForEach(p => p.Update(this));
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using OpenNest.Api;
|
|||||||
using OpenNest.CNC.CuttingStrategy;
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
using OpenNest.Collections;
|
using OpenNest.Collections;
|
||||||
using OpenNest.Controls;
|
using OpenNest.Controls;
|
||||||
|
using OpenNest.Diagnostics;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
using OpenNest.Engine.Sequencing;
|
using OpenNest.Engine.Sequencing;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
@@ -26,6 +27,15 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
public readonly Document Document;
|
public readonly Document Document;
|
||||||
public readonly PlateView PlateView;
|
public readonly PlateView PlateView;
|
||||||
|
|
||||||
|
public System.Threading.Tasks.Task CheckOverlapsAsync() => PlateView.CheckOverlapsAsync();
|
||||||
|
public void CancelOverlapCheck() => PlateView.CancelOverlapCheck();
|
||||||
|
public OverlapDisplayMode OverlapDisplay
|
||||||
|
{
|
||||||
|
get => PlateView.OverlapDisplay;
|
||||||
|
set => PlateView.OverlapDisplay = value;
|
||||||
|
}
|
||||||
|
|
||||||
public readonly PlateManager PlateManager;
|
public readonly PlateManager PlateManager;
|
||||||
|
|
||||||
public Nest Nest => Document.Nest;
|
public Nest Nest => Document.Nest;
|
||||||
@@ -897,6 +907,8 @@ namespace OpenNest.Forms
|
|||||||
program => NestWriter.GetProgramText(program) + "\0" + NestWriter.GetSubProgramsText(program)
|
program => NestWriter.GetProgramText(program) + "\0" + NestWriter.GetSubProgramsText(program)
|
||||||
);
|
);
|
||||||
var converter = new CadConverterForm();
|
var converter = new CadConverterForm();
|
||||||
|
// LoadDrawings can edit live programs even when the dialog is canceled.
|
||||||
|
PlateView.InvalidateOverlapCheck();
|
||||||
converter.LoadDrawings(Nest.Drawings);
|
converter.LoadDrawings(Nest.Drawings);
|
||||||
|
|
||||||
if (converter.ShowDialog() != DialogResult.OK)
|
if (converter.ShowDialog() != DialogResult.OK)
|
||||||
|
|||||||
Generated
+52
-1
@@ -48,6 +48,12 @@
|
|||||||
toolStripMenuItem4 = new System.Windows.Forms.ToolStripSeparator();
|
toolStripMenuItem4 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
mnuEditSelectAll = new System.Windows.Forms.ToolStripMenuItem();
|
mnuEditSelectAll = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuView = new System.Windows.Forms.ToolStripMenuItem();
|
mnuView = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
mnuViewOverlapCheck = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
mnuOverlapCheckActive = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
mnuOverlapCancel = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
mnuOverlapDisplay = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
mnuOverlapOff = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
mnuOverlapAreas = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuViewDrawRapids = new System.Windows.Forms.ToolStripMenuItem();
|
mnuViewDrawRapids = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuViewDrawPiercePoints = new System.Windows.Forms.ToolStripMenuItem();
|
mnuViewDrawPiercePoints = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuViewDrawBounds = new System.Windows.Forms.ToolStripMenuItem();
|
mnuViewDrawBounds = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
@@ -318,10 +324,49 @@
|
|||||||
//
|
//
|
||||||
// mnuView
|
// mnuView
|
||||||
//
|
//
|
||||||
mnuView.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuViewDrawRapids, mnuViewDrawPiercePoints, mnuViewDrawBounds, mnuViewDrawOffset, mnuViewDrawCutDirection, toolStripMenuItem5, mnuViewZoomTo, mnuViewZoomIn, mnuViewZoomOut });
|
mnuView.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuViewDrawRapids, mnuViewDrawPiercePoints, mnuViewDrawBounds, mnuViewDrawOffset, mnuViewDrawCutDirection, mnuViewOverlapCheck, toolStripMenuItem5, mnuViewZoomTo, mnuViewZoomIn, mnuViewZoomOut });
|
||||||
mnuView.Name = "mnuView";
|
mnuView.Name = "mnuView";
|
||||||
mnuView.Size = new System.Drawing.Size(44, 20);
|
mnuView.Size = new System.Drawing.Size(44, 20);
|
||||||
mnuView.Text = "&View";
|
mnuView.Text = "&View";
|
||||||
|
mnuView.DropDownOpening += OverlapMenu_Opening;
|
||||||
|
//
|
||||||
|
// mnuViewOverlapCheck
|
||||||
|
//
|
||||||
|
mnuViewOverlapCheck.Name = "mnuViewOverlapCheck";
|
||||||
|
mnuViewOverlapCheck.Text = "Overlap Check";
|
||||||
|
mnuViewOverlapCheck.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuOverlapCheckActive, mnuOverlapCancel, mnuOverlapDisplay });
|
||||||
|
mnuViewOverlapCheck.DropDownOpening += OverlapMenu_Opening;
|
||||||
|
//
|
||||||
|
// mnuOverlapCheckActive
|
||||||
|
//
|
||||||
|
mnuOverlapCheckActive.Name = "mnuOverlapCheckActive";
|
||||||
|
mnuOverlapCheckActive.Text = "Check Active Plate";
|
||||||
|
mnuOverlapCheckActive.Click += CheckOverlaps_Click;
|
||||||
|
//
|
||||||
|
// mnuOverlapCancel
|
||||||
|
//
|
||||||
|
mnuOverlapCancel.Name = "mnuOverlapCancel";
|
||||||
|
mnuOverlapCancel.Text = "Cancel Check";
|
||||||
|
mnuOverlapCancel.Click += CancelOverlapCheck_Click;
|
||||||
|
//
|
||||||
|
// mnuOverlapDisplay
|
||||||
|
//
|
||||||
|
mnuOverlapDisplay.Name = "mnuOverlapDisplay";
|
||||||
|
mnuOverlapDisplay.Text = "Display";
|
||||||
|
mnuOverlapDisplay.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuOverlapOff, mnuOverlapAreas });
|
||||||
|
mnuOverlapDisplay.DropDownOpening += OverlapMenu_Opening;
|
||||||
|
//
|
||||||
|
// mnuOverlapOff
|
||||||
|
//
|
||||||
|
mnuOverlapOff.Name = "mnuOverlapOff";
|
||||||
|
mnuOverlapOff.Text = "Off";
|
||||||
|
mnuOverlapOff.Click += OverlapOff_Click;
|
||||||
|
//
|
||||||
|
// mnuOverlapAreas
|
||||||
|
//
|
||||||
|
mnuOverlapAreas.Name = "mnuOverlapAreas";
|
||||||
|
mnuOverlapAreas.Text = "Areas";
|
||||||
|
mnuOverlapAreas.Click += OverlapAreas_Click;
|
||||||
//
|
//
|
||||||
// mnuViewDrawRapids
|
// mnuViewDrawRapids
|
||||||
//
|
//
|
||||||
@@ -1241,6 +1286,12 @@
|
|||||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem4;
|
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem4;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuEditSelectAll;
|
private System.Windows.Forms.ToolStripMenuItem mnuEditSelectAll;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuView;
|
private System.Windows.Forms.ToolStripMenuItem mnuView;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuViewOverlapCheck;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuOverlapCheckActive;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuOverlapCancel;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuOverlapDisplay;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuOverlapOff;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuOverlapAreas;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawRapids;
|
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawRapids;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawPiercePoints;
|
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawPiercePoints;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawBounds;
|
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawBounds;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using System.Windows.Forms;
|
|||||||
using OpenNest.Actions;
|
using OpenNest.Actions;
|
||||||
using OpenNest.Collections;
|
using OpenNest.Collections;
|
||||||
using OpenNest.Data;
|
using OpenNest.Data;
|
||||||
|
using OpenNest.Diagnostics;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
using OpenNest.Engine.BestFit;
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
@@ -47,6 +48,7 @@ namespace OpenNest.Forms
|
|||||||
public MainForm()
|
public MainForm()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
UpdateOverlapMenu();
|
||||||
LoadSettings();
|
LoadSettings();
|
||||||
|
|
||||||
var renderer = new ToolStripRenderer(ToolbarTheme.Toolbar);
|
var renderer = new ToolStripRenderer(ToolbarTheme.Toolbar);
|
||||||
@@ -438,6 +440,7 @@ namespace OpenNest.Forms
|
|||||||
activeForm.PlateView.MouseMove -= PlateView_MouseMove;
|
activeForm.PlateView.MouseMove -= PlateView_MouseMove;
|
||||||
activeForm.PlateView.MouseClick -= PlateView_MouseClick;
|
activeForm.PlateView.MouseClick -= PlateView_MouseClick;
|
||||||
activeForm.PlateView.StatusChanged -= PlateView_StatusChanged;
|
activeForm.PlateView.StatusChanged -= PlateView_StatusChanged;
|
||||||
|
activeForm.PlateView.OverlapStateChanged -= OverlapStateChanged;
|
||||||
activeForm.PlateView.SelectionChanged -= PlateView_SelectionChanged;
|
activeForm.PlateView.SelectionChanged -= PlateView_SelectionChanged;
|
||||||
activeForm.PlateView.PartAdded -= PlateView_PartAdded;
|
activeForm.PlateView.PartAdded -= PlateView_PartAdded;
|
||||||
activeForm.PlateView.PartRemoved -= PlateView_PartRemoved;
|
activeForm.PlateView.PartRemoved -= PlateView_PartRemoved;
|
||||||
@@ -450,6 +453,7 @@ namespace OpenNest.Forms
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeForm = ActiveMdiChild as EditNestForm;
|
activeForm = ActiveMdiChild as EditNestForm;
|
||||||
|
UpdateOverlapMenu();
|
||||||
|
|
||||||
EnableCheck();
|
EnableCheck();
|
||||||
UpdatePlateStatus();
|
UpdatePlateStatus();
|
||||||
@@ -465,6 +469,7 @@ namespace OpenNest.Forms
|
|||||||
UpdateLocationMode();
|
UpdateLocationMode();
|
||||||
UpdateSelectionStatus();
|
UpdateSelectionStatus();
|
||||||
activeForm.PlateView.StatusChanged += PlateView_StatusChanged;
|
activeForm.PlateView.StatusChanged += PlateView_StatusChanged;
|
||||||
|
activeForm.PlateView.OverlapStateChanged += OverlapStateChanged;
|
||||||
activeForm.PlateView.SelectionChanged += PlateView_SelectionChanged;
|
activeForm.PlateView.SelectionChanged += PlateView_SelectionChanged;
|
||||||
activeForm.PlateView.PartAdded += PlateView_PartAdded;
|
activeForm.PlateView.PartAdded += PlateView_PartAdded;
|
||||||
activeForm.PlateView.PartRemoved += PlateView_PartRemoved;
|
activeForm.PlateView.PartRemoved += PlateView_PartRemoved;
|
||||||
@@ -658,6 +663,43 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
#region View Menu Events
|
#region View Menu Events
|
||||||
|
|
||||||
|
private void OverlapStateChanged(object sender, EventArgs e) => UpdateOverlapMenu();
|
||||||
|
private void OverlapMenu_Opening(object sender, EventArgs e) => UpdateOverlapMenu();
|
||||||
|
|
||||||
|
private void UpdateOverlapMenu()
|
||||||
|
{
|
||||||
|
var hasPlate = activeForm != null && !activeForm.IsDisposed && activeForm.PlateView.Plate != null;
|
||||||
|
mnuViewOverlapCheck.Enabled = hasPlate;
|
||||||
|
mnuOverlapCheckActive.Enabled = hasPlate;
|
||||||
|
mnuOverlapCancel.Enabled = hasPlate && activeForm.PlateView.IsOverlapCheckRunning;
|
||||||
|
mnuOverlapDisplay.Enabled = hasPlate;
|
||||||
|
mnuOverlapOff.Enabled = hasPlate;
|
||||||
|
mnuOverlapAreas.Enabled = hasPlate;
|
||||||
|
mnuOverlapOff.Checked = hasPlate && activeForm.OverlapDisplay == OverlapDisplayMode.Off;
|
||||||
|
mnuOverlapAreas.Checked = hasPlate && activeForm.OverlapDisplay == OverlapDisplayMode.Areas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void CheckOverlaps_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var form = activeForm;
|
||||||
|
if (form != null)
|
||||||
|
await form.CheckOverlapsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelOverlapCheck_Click(object sender, EventArgs e) => activeForm?.CancelOverlapCheck();
|
||||||
|
|
||||||
|
private void OverlapOff_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (activeForm != null)
|
||||||
|
activeForm.OverlapDisplay = OverlapDisplayMode.Off;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OverlapAreas_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (activeForm != null)
|
||||||
|
activeForm.OverlapDisplay = OverlapDisplayMode.Areas;
|
||||||
|
}
|
||||||
|
|
||||||
private void ToggleDrawRapids_Click(object sender, EventArgs e)
|
private void ToggleDrawRapids_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (activeForm == null)
|
if (activeForm == null)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("OpenNest.WinForms.Tests")]
|
||||||
@@ -12,6 +12,7 @@ A Windows desktop application for CNC nesting — imports DXF drawings, arranges
|
|||||||
- **Import / export** — DXF & DWG parts (ACadSharp), Excel BOMs, bend-line detection, built-in parametric shapes; export DXF or post-processed G-code.
|
- **Import / export** — DXF & DWG parts (ACadSharp), Excel BOMs, bend-line detection, built-in parametric shapes; export DXF or post-processed G-code.
|
||||||
- **Nesting** — pluggable whole-job engines (Default, Strip, Vertical/Horizontal Remnant, StockLadder, plus DLL plugins), NFP-based interlocking pair evaluation, gravity compaction, rotation sweeps, multi-plate/multi-material jobs.
|
- **Nesting** — pluggable whole-job engines (Default, Strip, Vertical/Horizontal Remnant, StockLadder, plus DLL plugins), NFP-based interlocking pair evaluation, gravity compaction, rotation sweeps, multi-plate/multi-material jobs.
|
||||||
- **Plate operations** — manual sheet cut-offs, [automatic scrap cutoffs with unused-tail preservation](docs/automatic-scrap-cutoffs.md), oversized-part splitting (straight, weld-gap tabs, spike-groove), interactive editing, and spacing-aware pushes that can slide along or away from touching parts.
|
- **Plate operations** — manual sheet cut-offs, [automatic scrap cutoffs with unused-tail preservation](docs/automatic-scrap-cutoffs.md), oversized-part splitting (straight, weld-gap tabs, spike-groove), interactive editing, and spacing-aware pushes that can slide along or away from touching parts.
|
||||||
|
- **Visual overlap check** — manually highlight shared material on the active plate, including containment and cutouts, through View > Overlap Check. [Usage and limitations](docs/geometry/visual-overlap-check.md).
|
||||||
- **CNC output** — configurable lead-ins/outs and tabs, contour editing, user-defined G-code variables (`$name` → `#200+` machine variables), plugin post-processors (Cincinnati CL-707/800/900/940/CLX included).
|
- **CNC output** — configurable lead-ins/outs and tabs, contour editing, user-defined G-code variables (`$name` → `#200+` machine variables), plugin post-processors (Cincinnati CL-707/800/900/940/CLX included).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
# Material-overlap polygon diagnostics
|
# Visual material-overlap check
|
||||||
|
|
||||||
|
## Desktop use
|
||||||
|
|
||||||
|
Choose **View > Overlap Check > Check Active Plate** to check committed parts on
|
||||||
|
this plate. Shared material is shaded red/magenta without changing the nest,
|
||||||
|
selection, cutting paths, or export behavior. Cutoffs and temporary preview parts
|
||||||
|
are excluded. **Cancel Check** discards the running request. **Display > Off /
|
||||||
|
Areas** changes visibility without rerunning analysis; checking from Off shows
|
||||||
|
Areas. Display preferences belong to the current document and are not saved.
|
||||||
|
|
||||||
|
A persistent label distinguishes unchecked, checking, current, incomplete, stale,
|
||||||
|
canceled, and failed checks. Only a completed, current, fully checked report can
|
||||||
|
say **No material overlaps detected**. An incomplete check retains known overlaps
|
||||||
|
and states how many distinct parts could not be checked. Pair counts are not
|
||||||
|
fragment counts. This diagnostic checks shared material, not minimum spacing,
|
||||||
|
plate edges, or cutting-path crossings.
|
||||||
|
|
||||||
|
Edits clear the overlay and require another explicit check. Plate changes reset
|
||||||
|
the check. Pan, zoom, selection, and display changes do not rerun geometry.
|
||||||
|
Drawing-editor loading invalidates before loading, even if the dialog is later
|
||||||
|
canceled. There is no automatic check during dragging or export.
|
||||||
|
|
||||||
|
## Analysis API
|
||||||
|
|
||||||
`OpenNest.Diagnostics.PlateOverlapAnalyzer` in OpenNest.Core checks a group of placed
|
`OpenNest.Diagnostics.PlateOverlapAnalyzer` in OpenNest.Core checks a group of placed
|
||||||
parts and returns the shared polygon areas for each overlapping pair. This is the
|
parts and returns the shared polygon areas for each overlapping pair. Existing
|
||||||
cross-platform analysis foundation for a future PlateView overlay; it does not add
|
|
||||||
a menu command, painting, centroids, or freshness management yet. Existing
|
|
||||||
`Part.Intersects`, `PartOverlapChecker`, `Plate.HasOverlappingParts`, engine
|
`Part.Intersects`, `PartOverlapChecker`, `Plate.HasOverlappingParts`, engine
|
||||||
validators, and CLI entry points are unchanged. A separate shared-triangulator fix
|
validators, and CLI entry points are unchanged. A separate shared-triangulator fix
|
||||||
uses translation-stable winding, correcting missed clockwise outlines/holes far
|
uses translation-stable winding, correcting missed clockwise outlines/holes far
|
||||||
@@ -121,10 +142,33 @@ slivers are below its reporting policy. Floating-point coordinates still have
|
|||||||
finite resolution. Contact and fragment thresholds are unchanged; only the shared
|
finite resolution. Contact and fragment thresholds are unchanged; only the shared
|
||||||
triangulator's winding arithmetic was stabilized in the prerequisite fix.
|
triangulator's winding arithmetic was stabilized in the prerequisite fix.
|
||||||
|
|
||||||
Next integration/hardening: add PlateView request generations and stale-result
|
## Desktop lifecycle and rendering
|
||||||
invalidation before rendering; measure real-plate capture/analysis cost and
|
|
||||||
cancellation latency before adding cached triangulations or background capture;
|
`OverlapReportState` and `OverlapGeometryStamp` in Core hold the testable request
|
||||||
add area-weighted centroids separately if the UI needs them.
|
policy. The stamp compares ordered part identities, exact pose scalars, drawing
|
||||||
|
and program references, cutoff status, and plate identity. It is not a geometry
|
||||||
|
hash: any new editor that mutates a clean program in place must call
|
||||||
|
`PlateView.InvalidateOverlapCheck()` before loading/mutation. Current live clean
|
||||||
|
program editing goes through `EditNestForm.EditDrawingsInConverter_Click`;
|
||||||
|
metadata-only edits do not change material. In-place hole-program edits require
|
||||||
|
the same explicit invalidation.
|
||||||
|
|
||||||
|
`OverlapOverlayController` owns UI-thread captures, background analysis, request
|
||||||
|
generations, cancellation, and the GDI display cache. It checks freshness before
|
||||||
|
publication and painting. Handle destruction/disposal cancels work and releases
|
||||||
|
paths; old completions cannot replace a newer report. Snapshot conversion and
|
||||||
|
clipping never run in paint or mouse-move handlers.
|
||||||
|
|
||||||
|
PlateView draws the controller overlay after work-area/debug-remnant drawing and
|
||||||
|
before action paint subscribers and hover tooltips. One consistently wound path
|
||||||
|
is filled once, avoiding fragment outlines, internal triangulation seams, and
|
||||||
|
darker triple coverage. World-to-graph conversion excludes pan, because PlateView
|
||||||
|
already applies origin translation. Paths are rebuilt for report/scale changes,
|
||||||
|
not ordinary repaints or panning. The state label saves/restores graphics state.
|
||||||
|
|
||||||
|
Next hardening: measure real-plate capture/analysis cost and cancellation latency
|
||||||
|
before adding cached triangulations or background capture. Cancellation cannot
|
||||||
|
interrupt the interior of an existing kernel operation.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
@@ -136,5 +180,21 @@ inputs, snapshot isolation, read-only output, cutting-program independence, and
|
|||||||
cancellation. Run:
|
cancellation. Run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dotnet test OpenNest.Tests/OpenNest.Tests.csproj --filter FullyQualifiedName~PlateOverlapAnalyzerTests
|
dotnet test OpenNest.Tests/OpenNest.Tests.csproj --filter 'FullyQualifiedName~PlateOverlapAnalyzerTests|FullyQualifiedName~OverlapReportStateTests'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`OverlapReportStateTests` verifies request supersession, exact pose/reference
|
||||||
|
freshness, stale clearing, cancellation, and incomplete-versus-clear messaging.
|
||||||
|
`OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs` adds STA worker/publication,
|
||||||
|
menu/MDI, path-cache, uniform-fill pixel, and control-lifetime checks. Run those
|
||||||
|
on Windows:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dotnet test OpenNest.WinForms.Tests/OpenNest.WinForms.Tests.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux can cross-build with `-p:EnableWindowsTargeting=true`, but that does not
|
||||||
|
execute Windows tests or verify appearance, DPI, or interaction. On Windows,
|
||||||
|
check partial overlap, containment, inside-hole placement, pan/zoom and quadrant
|
||||||
|
alignment, stale clearing during edits/plate switches, converter cancellation,
|
||||||
|
and repeated check/toggle/close cycles without GDI/disposed-control errors.
|
||||||
|
|||||||
Reference in New Issue
Block a user