Files
OpenNest/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs
T
ajandClaude Opus 5.5 d4269c1f06 refactor(cutting): merge circle lead settings into internal leads
Full circles now take InternalLeadIn/InternalLeadOut like every other
cutout; the ArcCircle lead settings and the cutting panel's Arc / Circle
tab are removed. Circle-specific geometry handling (radius clamp, angle
rounding, pierce zone) is unchanged.

Older nest files and desktop settings still load: their arcCircleLeadIn/
arcCircleLeadOut keys are ignored and no longer written, so re-planned
circles take the internal leads. Saved part programs are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:20:09 -04:00

978 lines
42 KiB
C#

using System.Windows.Forms;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Forms;
using OpenNest.Geometry;
namespace OpenNest.WinForms.Tests.Forms;
[Collection("Fill operation lifetime")]
public class CuttingInspectionPanelTests
{
[Fact]
public void OnePlatePreviewUsesMainCanvasAndCancelRestoresLiveView() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
var view = editor.PlateView;
var plate = view.Plate;
var programs = plate.Parts.Select(part => part.Program).ToArray();
view.ZoomToArea(-5, -5, 60, 60);
var viewport = view.CaptureViewport();
editor.OpenCuttingInspection();
var panel = Assert.IsType<CuttingInspectionPanel>(editor.CuttingInspection);
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.True(panel.Proposal.CanApply);
Assert.True(view.InspectionOnly);
Assert.True(view.HasProposalDisplay);
Assert.Same(plate, view.Plate);
Assert.NotSame(plate, view.DisplayPlate);
Assert.Equal(new[] { 1, 0 }, panel.Proposal.Plates[0].Result.ProposedOrder.Select(p => p.SourceOrdinal));
Assert.Equal(new[] { "near", "far" }, view.DisplayPlate.Parts.Select(p => p.BaseDrawing.Name));
Assert.Equal(programs, plate.Parts.Select(part => part.Program));
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns));
var key = Message.Create(IntPtr.Zero, 0, IntPtr.Zero, IntPtr.Zero);
Assert.True((bool)typeof(OpenNest.Controls.PlateView)
.GetMethod("ProcessCmdKey", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
.Invoke(view, [key, Keys.Escape])!);
Assert.Null(editor.CuttingInspection);
Assert.False(view.HasProposalDisplay);
Assert.False(view.InspectionOnly);
Assert.Same(plate, view.Plate);
Assert.Equal(viewport, view.CaptureViewport());
Assert.Equal(programs, plate.Parts.Select(part => part.Program));
});
[Fact]
public void CloseDuringWorkerRetainsPanelAndInspectionLockUntilCompletion() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
using var entered = new ManualResetEventSlim();
using var release = new ManualResetEventSlim();
editor.CuttingBeforePlan = token =>
{
entered.Set();
release.Wait(TimeSpan.FromSeconds(30));
Assert.True(token.IsCancellationRequested);
};
try
{
editor.OpenCuttingInspection();
var panel = Assert.IsType<CuttingInspectionPanel>(editor.CuttingInspection);
Assert.True(entered.Wait(TimeSpan.FromSeconds(30)));
panel.RequestClose();
Assert.True(panel.IsPlanning);
Assert.True(editor.PlateView.InspectionOnly);
Assert.True(editor.IsSidePanelVisible);
Assert.False(panel.IsDisposed);
release.Set();
PumpUntil(() => panel.IsDisposed);
Assert.False(editor.PlateView.InspectionOnly);
Assert.False(editor.PlateView.HasProposalDisplay);
Assert.Null(editor.CuttingInspection);
}
finally
{
release.Set();
}
});
[Fact]
public void DirectEditorDisposalWaitsForRunningWorker() => RunSta(() =>
{
var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
using var entered = new ManualResetEventSlim();
using var release = new ManualResetEventSlim();
editor.CuttingBeforePlan = token =>
{
entered.Set();
release.Wait(TimeSpan.FromSeconds(30));
Assert.True(token.IsCancellationRequested);
};
try
{
editor.OpenCuttingInspection();
Assert.True(entered.Wait(TimeSpan.FromSeconds(30)));
var panel = Assert.IsType<CuttingInspectionPanel>(editor.CuttingInspection);
editor.Dispose();
Assert.False(editor.IsDisposed);
Assert.True(editor.PlateView.InspectionOnly);
Assert.False(panel.IsDisposed);
release.Set();
PumpUntil(() => editor.IsDisposed);
Assert.True(panel.IsDisposed);
}
finally
{
release.Set();
editor.Dispose();
}
});
[Fact]
public void DirectMdiHostDisposalWaitsForRunningWorker() => RunSta(() =>
{
var host = new MainForm();
var editor = Editor();
using var entered = new ManualResetEventSlim();
using var release = new ManualResetEventSlim();
try
{
host.Show();
editor.MdiParent = host;
editor.Show();
editor.PlateManager.LoadAt(0);
editor.CuttingBeforePlan = token =>
{
entered.Set();
release.Wait(TimeSpan.FromSeconds(30));
Assert.True(token.IsCancellationRequested);
};
editor.OpenCuttingInspection();
Assert.True(entered.Wait(TimeSpan.FromSeconds(30)));
var panel = Assert.IsType<CuttingInspectionPanel>(editor.CuttingInspection);
host.Dispose();
Assert.False(host.IsDisposed);
Assert.False(editor.IsDisposed);
Assert.True(editor.PlateView.InspectionOnly);
Assert.False(panel.IsDisposed);
release.Set();
PumpUntil(() => host.IsDisposed);
Assert.True(panel.IsDisposed);
}
finally
{
release.Set();
host.Dispose();
}
});
[Fact]
public void AllPlatesNavigatesReadyAndBlockedWithoutPartialApply() => RunSta(() =>
{
using var editor = Editor();
var second = editor.Nest.Plates[^1]; // Fill the managed empty sentinel; a new one is appended.
second.Size = new OpenNest.Geometry.Size(100, 100);
// Overlapping material blocks the plate (a locked program's own crossing only warns).
second.Parts.Add(Square("first", 1));
second.Parts.Add(Square("second", 5));
editor.Show();
editor.PlateManager.LoadAt(editor.Nest.Plates.Count - 1); // Different empty sentinel.
var original = editor.PlateView.Plate;
OpenAll(editor);
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Equal(2, panel.Proposal.Plates.Count);
Assert.Contains("Can't apply", Field<Label>(panel, "statusLabel").Text);
Assert.False(Field<Button>(panel, "applyButton").Enabled);
Assert.True(editor.PlateView.HasProposalDisplay);
Field<Button>(panel, "nextButton").PerformClick();
Assert.Same(second, editor.PlateView.Plate); // Refusal shows the requested live plate, not the opening plate.
Assert.False(editor.PlateView.HasProposalDisplay);
Assert.Contains("blocked", Field<Label>(panel, "previewLabel").Text);
var grid = Field<DataGridView>(panel, "warningGrid");
Assert.Equal(panel.Proposal.DiagnosticRows().Count, grid.Rows.Count);
var row = grid.Rows.Cast<DataGridViewRow>().First(r => r.Tag is OpenNest.Engine.CuttingPlanning.CuttingPlanDiagnosticRow d && d.PlateIndex == 1);
grid.ClearSelection();
row.Selected = true; // SelectionChanged precedes CurrentCell.
Assert.Contains("Plate 2", Field<Label>(panel, "previewLabel").Text);
Assert.All(editor.Nest.Plates[0].Parts, p => Assert.False(p.HasManualLeadIns));
panel.RequestClose();
Assert.Same(original, editor.PlateView.Plate);
});
[Fact]
public void BlockedOverlapRowShadesTheOverlapOnTheLivePlate() => RunSta(() =>
{
using var editor = Editor();
var plate = editor.Nest.Plates[0];
plate.Parts.Add(Square("overlapping", 5));
editor.Show();
editor.PlateManager.LoadAt(0);
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.False(Field<Button>(panel, "applyButton").Enabled);
Assert.Contains("Can't apply", Field<Label>(panel, "statusLabel").Text);
Assert.Contains("parts overlap", Field<Label>(panel, "previewLabel").Text);
var view = editor.PlateView;
Assert.False(view.HasProposalDisplay);
var grid = Field<DataGridView>(panel, "warningGrid");
var row = grid.Rows.Cast<DataGridViewRow>().First(r =>
r.Tag is OpenNest.Engine.CuttingPlanning.CuttingPlanDiagnosticRow { Witness.Overlap: not null });
grid.ClearSelection();
row.Selected = true;
Assert.Same(plate, view.Plate);
Assert.NotNull(view.ValidationOverlay.Finding?.Overlap);
Assert.Contains("overlapping material", Field<Label>(panel, "detailLabel").Text);
panel.RequestClose();
Assert.Null(view.ValidationOverlay.Finding);
});
[Fact]
public void PartsTabListsEveryPartAndFlagsOnlyThoseWithIssues() => RunSta(() =>
{
using var editor = Editor();
var plate = editor.Nest.Plates[0];
plate.Parts.Add(Square("overlapping", 5));
editor.Show();
editor.PlateManager.LoadAt(0);
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
var issues = Field<DataGridView>(panel, "warningGrid");
var parts = Field<DataGridView>(panel, "partsGrid");
Assert.Equal(plate.Parts.Count, parts.Rows.Count);
var flaggedPartNumbers = issues.Rows.Cast<DataGridViewRow>()
.SelectMany(r => r.Tag is OpenNest.Engine.CuttingPlanning.CuttingPlanDiagnosticRow d
? new[] { d.PartNumber, d.OtherPartNumber }.OfType<int>() : [])
.ToHashSet();
Assert.NotEmpty(flaggedPartNumbers);
foreach (DataGridViewRow row in parts.Rows)
{
var partNumber = (int)row.Cells[1].Value!;
var status = (string)row.Cells[2].Value!;
Assert.Equal(!flaggedPartNumbers.Contains(partNumber), status == "Passed");
}
var flaggedRow = parts.Rows.Cast<DataGridViewRow>().First(r => flaggedPartNumbers.Contains((int)r.Cells[1].Value!));
parts.ClearSelection();
flaggedRow.Selected = true; // Cross-selects the matching Issues row and its marker.
Assert.NotNull(editor.PlateView.ValidationOverlay.Finding?.Overlap);
panel.RequestClose();
});
[Fact]
public void PlannedCutOffsKeepTheCutOffLineStyleOnTheProposal() => RunSta(() =>
{
using var editor = Editor();
editor.Size = new System.Drawing.Size(1300, 800);
editor.Show();
editor.PlateManager.LoadAt(0);
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
var view = editor.PlateView;
var baseline = CutOffPixels(view);
Field<CheckBox>(panel, "autoCutOffsCheckBox").Checked = true;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.True(view.HasProposalDisplay);
Assert.Contains(view.DisplayPlate.Parts, part => part.BaseDrawing.IsCutOff);
Assert.True(CutOffPixels(view) > baseline + 50, "Planned cut-offs should draw as orange-red dashes.");
panel.RequestClose();
static int CutOffPixels(OpenNest.Controls.PlateView view)
{
using var bitmap = new System.Drawing.Bitmap(view.Width, view.Height);
view.DrawToBitmap(bitmap, new System.Drawing.Rectangle(0, 0, view.Width, view.Height));
var count = 0;
for (var x = 0; x < bitmap.Width; x++)
for (var y = 0; y < bitmap.Height; y++)
{
var pixel = bitmap.GetPixel(x, y);
if (pixel.R > 220 && pixel.G is > 40 and < 110 && pixel.B < 40)
count++;
}
return count;
}
});
[Fact]
public void NextAndPreviousShowTheSelectedReadyPlateNotTheOpeningPlate() => RunSta(() =>
{
using var editor = Editor();
var first = editor.Nest.Plates[0];
var second = editor.Nest.Plates[^1]; // Fill the managed sentinel.
second.Size = new OpenNest.Geometry.Size(100, 100);
second.Parts.Add(Square("other plate", 40));
editor.Show();
editor.PlateManager.LoadAt(0);
OpenAll(editor);
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Equal(2, panel.Proposal.Plates.Count);
Assert.Same(first, editor.PlateView.Plate);
Assert.True(editor.PlateView.HasProposalDisplay);
Assert.Contains(editor.PlateView.DisplayPlate.Parts, part => part.BaseDrawing.Name == "near");
Field<Button>(panel, "nextButton").PerformClick();
Assert.Same(second, editor.PlateView.Plate);
Assert.True(editor.PlateView.HasProposalDisplay);
Assert.NotSame(second, editor.PlateView.DisplayPlate);
Assert.Single(editor.PlateView.DisplayPlate.Parts);
Assert.Equal("other plate", editor.PlateView.DisplayPlate.Parts[0].BaseDrawing.Name);
Assert.Contains("Plate 2", Field<Label>(panel, "previewLabel").Text);
Field<Button>(panel, "previousButton").PerformClick();
Assert.Same(first, editor.PlateView.Plate);
Assert.True(editor.PlateView.HasProposalDisplay);
Assert.Contains(editor.PlateView.DisplayPlate.Parts, part => part.BaseDrawing.Name == "near");
panel.RequestClose();
Assert.Same(first, editor.PlateView.Plate);
});
[Fact]
public void WarningAcceptanceResetsOnReplanAndSettingsOrderChange() => RunSta(() =>
{
using var editor = Editor();
var plate = editor.Nest.Plates[0];
plate.Parts.Clear();
var program = Square("fixture", 1).Program;
program.MoveTo(2, 2); program.LineTo(4, 2); program.LineTo(4, 4);
program.LineTo(2, 4); program.LineTo(2, 2);
program.MoveTo(4, 2); program.LineTo(6, 2); program.LineTo(6, 4);
program.LineTo(4, 4); program.LineTo(4, 2);
plate.Parts.Add(new Part(new Drawing("touching contours", program), new Vector(1, 1)));
editor.Show();
editor.PlateManager.LoadAt(0);
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
var accept = Field<CheckBox>(panel, "acceptWarningsCheckBox");
Assert.True(accept.Visible);
Assert.False(accept.Checked);
Assert.Contains("UNVERIFIED", Field<Label>(panel, "previewLabel").Text);
accept.Checked = true;
Assert.True(Field<Button>(panel, "applyButton").Enabled);
panel.StartPlanning();
Assert.False(accept.Checked);
Assert.Empty(Field<DataGridView>(panel, "warningGrid").Rows.Cast<DataGridViewRow>());
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
accept.Checked = true;
Field<CheckBox>(panel, "keepOrderCheckBox").Checked = true;
Assert.False(accept.Checked);
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.False(Field<Button>(panel, "applyButton").Enabled);
var grid = Field<DataGridView>(panel, "warningGrid");
Assert.NotEmpty(grid.Rows.Cast<DataGridViewRow>());
var partRow = grid.Rows.Cast<DataGridViewRow>().First(r => r.Cells[1].Value is int);
grid.ClearSelection();
partRow.Selected = true;
if (partRow.Tag is OpenNest.Engine.CuttingPlanning.CuttingPlanDiagnosticRow { Witness.Location: not null })
Assert.NotNull(editor.PlateView.ValidationOverlay.Finding);
else
Assert.Contains("affected part", Field<Label>(panel, "detailLabel").Text);
Field<Button>(panel, "applyButton").PerformClick();
Assert.Null(panel.CommitResult);
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns));
accept.Checked = true;
Field<Button>(panel, "applyButton").PerformClick();
Assert.Equal(OpenNest.CNC.CuttingPlanning.CuttingCommitStatus.Applied, panel.CommitResult!.Status);
Assert.All(plate.Parts, part => Assert.True(part.HasManualLeadIns));
Assert.Null(editor.CuttingInspection);
});
[Fact]
public void StaleApplyClearsPreviewAndAcceptanceWithoutChangingPrograms() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
var plate = editor.PlateView.Plate;
var programs = plate.Parts.Select(p => p.Program).ToArray();
plate.Parts[0].Offset(0, 1);
Field<Button>(panel, "applyButton").PerformClick();
Assert.False(editor.PlateView.HasProposalDisplay);
Assert.False(Field<Button>(panel, "applyButton").Enabled);
Assert.False(Field<CheckBox>(panel, "acceptWarningsCheckBox").Checked);
Assert.Contains("Replan", Field<Label>(panel, "statusLabel").Text);
Assert.Equal(programs, plate.Parts.Select(p => p.Program));
Assert.All(plate.Parts, p => Assert.False(p.HasManualLeadIns));
panel.StartPlanning();
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.True(Field<Button>(panel, "applyButton").Enabled);
Assert.True(editor.PlateView.HasProposalDisplay);
panel.RequestClose();
});
[Fact]
public void ReadyApplyInstallsExactPreviewAndRefreshesLiveView() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
var plate = editor.PlateView.Plate;
var original = plate.Parts.ToArray();
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
var proposed = panel.Proposal.Plates[0].Result.ProposedOrder
.Select(part => original[part.SourceOrdinal]).ToArray();
Assert.True(Field<Button>(panel, "applyButton").Enabled);
var parameters = Field<CuttingParameters>(panel, "parameters");
var reordered = 0;
editor.PlateView.PartsReordered += (_, _) => reordered++;
Field<Button>(panel, "applyButton").PerformClick();
Assert.Equal(1, reordered);
Assert.NotSame(parameters, plate.CuttingParameters);
foreach (var layout in editor.PlateView.LayoutParts)
{
if (layout.IsDirty)
layout.Update(editor.PlateView);
Assert.True(layout.LeadInPath?.PointCount > 0);
}
Assert.Null(editor.CuttingInspection);
Assert.False(editor.PlateView.InspectionOnly);
Assert.False(editor.PlateView.HasProposalDisplay);
Assert.Equal(proposed, plate.Parts);
Assert.All(plate.Parts, part => Assert.True(part.HasManualLeadIns));
Assert.NotNull(plate.CuttingParameters);
Assert.Equal(plate.Parts, editor.PlateView.LayoutParts.Select(layout => layout.BasePart));
});
[Fact]
public void PresentationFailureCannotLeaveApplyOrWarningAcceptanceEnabled() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.True(Field<Button>(panel, "applyButton").Enabled);
var original = editor.PlateView.Plate.Parts.ToArray();
panel.BeforePresent = () => throw new InvalidOperationException("synthetic presentation failure");
panel.StartPlanning();
PumpUntil(() => !panel.IsPlanning);
Assert.False(Field<Button>(panel, "applyButton").Enabled);
Assert.False(Field<CheckBox>(panel, "acceptWarningsCheckBox").Checked);
Assert.False(editor.PlateView.HasProposalDisplay);
Assert.Contains("Nothing can be applied", Field<Label>(panel, "statusLabel").Text);
Assert.Equal(original, editor.PlateView.Plate.Parts);
panel.RequestClose();
});
[Fact]
public void CommittedPlanStillClosesWhenSavingDefaultsFails() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Exception? error = null;
panel.BeforePostApply = () => throw new InvalidOperationException("synthetic defaults failure");
panel.OnPostApplyError = ex => error = ex;
Field<Button>(panel, "applyButton").PerformClick();
Assert.Equal(OpenNest.CNC.CuttingPlanning.CuttingCommitStatus.Applied, panel.CommitResult!.Status);
Assert.IsType<InvalidOperationException>(error);
Assert.Null(editor.CuttingInspection);
Assert.False(editor.PlateView.InspectionOnly);
Assert.False(editor.PlateView.HasProposalDisplay);
Assert.All(editor.PlateView.Plate.Parts, part => Assert.True(part.HasManualLeadIns));
});
[Fact]
public void ConstructorKeepsItsOwnCopyOfSettings() => RunSta(() =>
{
using var editor = Editor();
var parameters = Parameters();
using var panel = new CuttingInspectionPanel(editor, parameters);
((LineLeadIn)parameters.ExternalLeadIn).Length = 5;
var captured = Field<CuttingParameters>(panel, "parameters");
Assert.NotSame(parameters, captured);
Assert.Equal(0.3, ((LineLeadIn)captured.ExternalLeadIn).Length);
});
[Fact]
public void EmptySentinelIsNotNumberedAsAnOccupiedPlate() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(editor.Nest.Plates.Count - 1);
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Equal("New plate (empty)", Field<Label>(panel, "previewLabel").Text);
Assert.False(editor.PlateView.HasProposalDisplay);
panel.RequestClose();
});
[Fact]
public void BusyMenuRefusesPlanningAndFreeMenuOpensDockedPanel() => RunSta(() =>
{
using var host = new MenuHost();
using var editor = Editor();
host.Show();
editor.MdiParent = host;
editor.Show();
editor.Activate();
editor.PlateManager.LoadAt(0);
var busy = typeof(MainForm).GetField("nestingInProgress", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
var show = typeof(MainForm).GetMethod("ShowCuttingPlan", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
busy.SetValue(host, true);
Assert.True(host.IsCuttingPlanBusy([editor.PlateView]));
string? notice = null;
host.CuttingPlanNotice = (message, caption) =>
{
Assert.Equal("Plan Cutting", caption);
notice = message;
};
show.Invoke(host, [false]); // Exercise the real guarded menu path, not only its predicate.
Assert.Contains("Finish or cancel", notice);
Assert.Null(editor.CuttingInspection);
Assert.False(editor.PlateView.InspectionOnly);
busy.SetValue(host, false);
Assert.False(host.IsCuttingPlanBusy([editor.PlateView]));
// A completed DoEvents loop removes the ambient context; the panel still owns its dispatcher.
Application.DoEvents();
show.Invoke(host, [false]);
var panel = Assert.IsType<CuttingInspectionPanel>(editor.CuttingInspection);
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.True(Field<Button>(panel, "applyButton").Enabled);
panel.RequestClose();
});
[Fact]
public void AutomaticCutOffsAreStagedForThePlanAndRemovedOnCancelOrUntick() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
var plate = editor.PlateView.Plate;
var original = plate.Parts.ToArray();
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Empty(plate.CutOffs);
Field<CheckBox>(panel, "autoCutOffsCheckBox").Checked = true;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.NotNull(panel.StagedCutOffs);
Assert.NotEmpty(plate.CutOffs);
Assert.Equal(plate.CutOffs.Count, plate.Parts.Count(p => p.BaseDrawing.IsCutOff));
Assert.Equal(plate.CutOffs.Count, panel.Proposal.Plates[0].Result.ProposedOrder.Count(p => p.IsCutOff));
Assert.False(Field<Control>(panel, "existingWorkBanner").Visible); // Staged lines are not "existing".
Field<CheckBox>(panel, "autoCutOffsCheckBox").Checked = false;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Null(panel.StagedCutOffs);
Assert.Empty(plate.CutOffs);
Assert.Equal(original, plate.Parts);
Field<CheckBox>(panel, "autoCutOffsCheckBox").Checked = true;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.NotEmpty(plate.CutOffs);
panel.RequestClose();
Assert.Null(editor.CuttingInspection);
Assert.Empty(plate.CutOffs);
Assert.Equal(original, plate.Parts);
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns));
});
[Fact]
public void TogglingScrapCutOffsWhilePlanningCancelsAndRestartsWithTheNewValue() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
var plate = editor.PlateView.Plate;
using var entered = new ManualResetEventSlim();
using var release = new ManualResetEventSlim();
editor.CuttingBeforePlan = _ =>
{
entered.Set();
release.Wait(TimeSpan.FromSeconds(30));
};
try
{
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => entered.IsSet);
Assert.True(panel.IsPlanning);
var checkBox = Field<CheckBox>(panel, "autoCutOffsCheckBox");
Assert.True(checkBox.Enabled); // stays selectable instead of being disabled like the other controls
checkBox.Checked = true; // toggled mid-plan: cancels the run in progress and restarts
Assert.True(checkBox.Enabled);
release.Set();
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.NotNull(panel.StagedCutOffs);
Assert.NotEmpty(plate.CutOffs);
}
finally
{
release.Set();
}
});
[Fact]
public void PlanningInputsStayEnabledAndChangingThemMidPlanRestartsWithTheNewValue() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
using var entered = new ManualResetEventSlim();
using var release = new ManualResetEventSlim();
var runs = 0;
editor.CuttingBeforePlan = _ =>
{
if (Interlocked.Increment(ref runs) > 1) return;
entered.Set();
release.Wait(TimeSpan.FromSeconds(30));
};
try
{
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => entered.IsSet);
Assert.True(panel.IsPlanning);
Assert.Matches(@"^Planning.*\[\d+s\]$", Field<Label>(panel, "statusLabel").Text);
foreach (var name in new[] { "settingsButton", "cutOffSettingsButton", "planButton" })
Assert.True(Field<Button>(panel, name).Enabled, name);
var keepOrder = Field<CheckBox>(panel, "keepOrderCheckBox");
Assert.True(keepOrder.Enabled);
keepOrder.Checked = true; // cancels the run in progress and restarts
release.Set();
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Equal(2, runs);
Assert.True(panel.Proposal.Plates[0].KeptCurrentOrder || !panel.Proposal.Plates[0].OrderChanged);
panel.RequestClose();
}
finally
{
release.Set();
}
});
[Fact]
public void CutOffSettingsMidPlanOpenOnceTheRunCancelsThenReplan() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
using var entered = new ManualResetEventSlim();
using var release = new ManualResetEventSlim();
var runs = 0;
editor.CuttingBeforePlan = _ =>
{
if (Interlocked.Increment(ref runs) > 1) return;
entered.Set();
release.Wait(TimeSpan.FromSeconds(30));
};
try
{
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => entered.IsSet);
var opened = 0;
panel.ShowCutOffSettings = _ => { opened++; return DialogResult.Cancel; };
Field<Button>(panel, "cutOffSettingsButton").PerformClick();
Assert.Equal(0, opened); // waits for the running plan to cancel first
release.Set();
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Equal(1, opened);
Assert.Equal(2, runs);
panel.RequestClose();
}
finally
{
release.Set();
}
});
[Theory]
[InlineData(0, "0s")]
[InlineData(20, "20s")]
[InlineData(65, "1m 05s")]
[InlineData(3725, "62m 05s")]
public void ElapsedPlanningTimeIsShownInSecondsThenMinutes(int seconds, string expected) =>
Assert.Equal(expected, CuttingInspectionPanel.FormatElapsed(TimeSpan.FromSeconds(seconds)));
[Fact]
public void ApplyKeepsTheStagedAutomaticCutOffsInThePlannedOrder() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
var plate = editor.PlateView.Plate;
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Field<CheckBox>(panel, "autoCutOffsCheckBox").Checked = true;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
var staged = panel.StagedCutOffs!.Definitions.ToArray();
var planned = panel.Proposal.Plates[0].Result.ProposedOrder.Select(p => p.SourcePart).ToArray();
Field<Button>(panel, "applyButton").PerformClick();
Assert.Equal(OpenNest.CNC.CuttingPlanning.CuttingCommitStatus.Applied, panel.CommitResult!.Status);
Assert.Null(editor.CuttingInspection);
Assert.Equal(staged, plate.CutOffs);
Assert.Equal(planned, plate.Parts);
Assert.All(plate.Parts.Where(p => !p.BaseDrawing.IsCutOff), part => Assert.True(part.HasManualLeadIns));
});
[Fact]
public void KeptOrderPlansWithStagedCutOffsPlacedBeforeThePartsTheyCross() => RunSta(() =>
{
// A 50 in wide part: every 35 in strip line crosses it, so a strip appended after it
// would conflict with the kept order.
var nest = new Nest();
var plate = nest.CreatePlate();
plate.Size = new OpenNest.Geometry.Size(100, 100);
plate.CuttingParameters = Parameters();
var program = new OpenNest.CNC.Program();
program.MoveTo(0, 0);
program.LineTo(0, 10);
program.LineTo(50, 10);
program.LineTo(50, 0);
program.LineTo(0, 0);
var wide = new Part(new Drawing("wide", program), new Vector(1, 1));
var small = Square("small", 60);
plate.Parts.Add(wide);
plate.Parts.Add(small);
using var editor = new EditNestForm(nest);
editor.Show();
editor.PlateManager.LoadAt(0);
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Field<CheckBox>(panel, "keepOrderCheckBox").Checked = true;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Field<CheckBox>(panel, "autoCutOffsCheckBox").Checked = true;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
var strip = plate.Parts.First(p => p.BaseDrawing.IsCutOff);
Assert.True(plate.Parts.IndexOf(strip) < plate.Parts.IndexOf(wide));
Assert.True(plate.Parts.IndexOf(wide) < plate.Parts.IndexOf(small));
var planned = Assert.Single(panel.Proposal.Plates);
Assert.True(planned.IsReady, string.Join("\n", panel.Proposal.Describe("in")));
Assert.True(Field<Button>(panel, "applyButton").Enabled);
panel.RequestClose();
Assert.Equal(new[] { wide, small }, plate.Parts);
});
[Fact]
public void ExistingLeadInsShowTheRedBannerAndClearRecalculatesFromScratch() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
var plate = editor.PlateView.Plate;
editor.OpenCuttingInspection();
var first = editor.CuttingInspection!;
PumpUntil(() => first.Proposal != null && !first.IsPlanning);
Assert.False(Field<Control>(first, "existingWorkBanner").Visible);
Field<Button>(first, "applyButton").PerformClick();
Assert.All(plate.Parts, part => Assert.True(part.HasManualLeadIns));
plate.CutOffs.Add(new CutOff(new Vector(60, 0), CutOffAxis.Vertical));
plate.RegenerateCutOffs(editor.PlateView.CutOffSettings);
var real = plate.Parts.Where(p => !p.BaseDrawing.IsCutOff).ToArray();
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
var banner = Field<Control>(panel, "existingWorkBanner");
Assert.True(banner.Visible);
Assert.Contains("ALREADY APPLIED: lead-ins on 2 parts and 1 cut-off on plate 1.",
Field<Label>(panel, "existingWorkLabel").Text);
string? asked = null;
panel.ConfirmClearExisting = message => { asked = message; return false; };
Field<Button>(panel, "clearExistingButton").PerformClick();
Assert.Contains("cannot be undone", asked);
Assert.Single(plate.CutOffs);
Assert.All(real, part => Assert.True(part.HasManualLeadIns));
panel.ConfirmClearExisting = _ => true;
Field<Button>(panel, "clearExistingButton").PerformClick();
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Empty(plate.CutOffs);
Assert.Equal(real, plate.Parts);
Assert.All(real, part => Assert.False(part.HasManualLeadIns));
Assert.False(banner.Visible);
Assert.True(panel.Proposal.CanApply);
panel.RequestClose();
});
[Fact]
public void ClearingWhilePlanningCancelsTheRunThenClearsAndReplans() => RunSta(() =>
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
var plate = editor.PlateView.Plate;
editor.OpenCuttingInspection();
var first = editor.CuttingInspection!;
PumpUntil(() => first.Proposal != null && !first.IsPlanning);
Field<Button>(first, "applyButton").PerformClick();
Assert.All(plate.Parts, part => Assert.True(part.HasManualLeadIns));
using var entered = new ManualResetEventSlim();
using var release = new ManualResetEventSlim();
var runs = 0;
var leadsDuringRuns = new List<bool>();
editor.CuttingBeforePlan = _ =>
{
leadsDuringRuns.Add(plate.Parts.Any(part => part.HasManualLeadIns));
if (Interlocked.Increment(ref runs) > 1) return;
entered.Set();
release.Wait(TimeSpan.FromSeconds(30));
};
try
{
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => entered.IsSet);
Assert.True(panel.IsPlanning);
var button = Field<Button>(panel, "clearExistingButton");
Assert.True(button.Enabled);
panel.ConfirmClearExisting = _ => true;
button.PerformClick();
// The clear waits for the running plan to cancel; the plates are untouched until then.
Assert.All(plate.Parts, part => Assert.True(part.HasManualLeadIns));
release.Set();
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Equal([true, false], leadsDuringRuns);
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns));
Assert.False(Field<Control>(panel, "existingWorkBanner").Visible);
Assert.True(panel.Proposal.CanApply);
panel.RequestClose();
}
finally
{
release.Set();
}
});
[Fact]
public void CutOffSettingsOpensTheAutomaticCutOffDialogAndReplansWithItsChoice() => RunSta(() =>
{
var remembered = (System.Collections.IDictionary)typeof(CuttingInspectionPanel)
.GetField("lastCutOffOptions", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!
.GetValue(null)!;
try
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
var plate = editor.PlateView.Plate;
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Equal(35, panel.CutOffOptions.Spacing);
panel.ShowCutOffSettings = form => DialogResult.Cancel;
Field<Button>(panel, "cutOffSettingsButton").PerformClick();
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.False(Field<CheckBox>(panel, "autoCutOffsCheckBox").Checked);
Assert.Equal(35, panel.CutOffOptions.Spacing);
Assert.Empty(plate.CutOffs);
AutomaticCutOffForm? shown = null;
panel.ShowCutOffSettings = form =>
{
shown = form;
Assert.Equal(35, double.Parse(form.Controls.Find("spacingBox", true).Single().Text));
form.Controls.Find("spacingBox", true).Single().Text = "30";
form.Controls.Find("minimumTailBox", true).Single().Text = "10";
typeof(AutomaticCutOffForm).GetMethod("ApplyButton_Click",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
.Invoke(form, [null, EventArgs.Empty]);
Assert.Empty(plate.CutOffs); // The dialog only chooses settings.
return form.DialogResult;
};
Field<Button>(panel, "cutOffSettingsButton").PerformClick();
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.NotNull(shown);
Assert.Equal((30.0, 10.0), (panel.CutOffOptions.Spacing, panel.CutOffOptions.MinimumTailLength));
Assert.True(Field<CheckBox>(panel, "autoCutOffsCheckBox").Checked);
Assert.Contains("30 in strips", Field<CheckBox>(panel, "autoCutOffsCheckBox").Text);
Assert.NotNull(panel.StagedCutOffs);
Assert.NotEmpty(plate.CutOffs);
Assert.Equal(plate.CutOffs.Count, panel.Proposal.Plates[0].Result.ProposedOrder.Count(p => p.IsCutOff));
panel.RequestClose();
Assert.Empty(plate.CutOffs);
editor.OpenCuttingInspection();
var reopened = editor.CuttingInspection!;
Assert.Equal(30, reopened.CutOffOptions.Spacing); // Remembered for this session.
PumpUntil(() => reopened.Proposal != null && !reopened.IsPlanning);
reopened.RequestClose();
}
finally
{
remembered.Clear();
}
});
private sealed class MenuHost : MainForm
{
protected override void OnLoad(EventArgs e) { }
}
private static T Field<T>(object target, string name) => Assert.IsAssignableFrom<T>(target.GetType()
.GetField(name, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(target));
private static void OpenAll(EditNestForm editor)
{
var method = typeof(EditNestForm).GetMethod("OpenCuttingInspection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
method.Invoke(editor, method.GetParameters().Length == 0 ? [] : [true]);
}
private static EditNestForm Editor()
{
var nest = new Nest();
var plate = nest.CreatePlate();
plate.Size = new OpenNest.Geometry.Size(100, 100);
plate.CuttingParameters = Parameters();
plate.Parts.Add(Square("far", 12));
plate.Parts.Add(Square("near", 1));
return new EditNestForm(nest);
}
private static Part Square(string name, double x)
{
var program = new OpenNest.CNC.Program();
program.MoveTo(0, 0);
program.LineTo(0, 10);
program.LineTo(10, 10);
program.LineTo(10, 0);
program.LineTo(0, 0);
return new Part(new Drawing(name, program), new Vector(x, 1));
}
private static CuttingParameters Parameters() => new()
{
ExternalLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 45 },
InternalLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 45 },
ExternalLeadOut = new NoLeadOut(),
InternalLeadOut = new NoLeadOut(),
};
private static void PumpUntil(Func<bool> condition)
{
var until = DateTime.UtcNow.AddSeconds(30);
while (!condition() && DateTime.UtcNow < until)
{
Application.DoEvents();
Thread.Sleep(10);
}
Assert.True(condition(), "Timed out waiting for cutting inspection.");
}
private static void RunSta(System.Action action) =>
StaTestThread.Run(action, TimeSpan.FromSeconds(45), "STA cutting inspection test timed out.");
}