mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-14 17:11:57 -04:00
refactor(cutting-ui): retire redundant planning and validation forms
This commit is contained in:
1 parent
f9b2e1282a
commit
d6ae706098
17 files changed
+268
-1829
No files matched your search
@@ -0,0 +1,47 @@
|
||||
using System.Drawing;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Controls;
|
||||
|
||||
public class ValidationOverlayTests
|
||||
{
|
||||
[Fact]
|
||||
public void RapidContactCrosshairIsDrawnAtContactAndClearedWithFinding() => RunSta(() =>
|
||||
{
|
||||
using var view = new PlateView();
|
||||
view.ShowValidationFinding(new PostVerificationFinding(PostVerificationKind.RapidCrossing, 1, 2, 1, "crossing")
|
||||
{
|
||||
RapidStart = new Vector(-1, 0),
|
||||
RapidEnd = new Vector(1, 0),
|
||||
ContactPoints = Array.AsReadOnly(new[] { Vector.Zero })
|
||||
});
|
||||
using var bitmap = new Bitmap(120, 120);
|
||||
using var graphics = Graphics.FromImage(bitmap);
|
||||
graphics.TranslateTransform(60, 60);
|
||||
graphics.Clear(Color.Black);
|
||||
view.ValidationOverlay.Draw(graphics);
|
||||
var pixel = bitmap.GetPixel(60, 65);
|
||||
Assert.True(pixel.R > 150 && pixel.B > 50 && pixel.G < 100);
|
||||
view.ClearValidationFinding();
|
||||
graphics.Clear(Color.Black);
|
||||
view.ValidationOverlay.Draw(graphics);
|
||||
Assert.Equal(Color.Black.ToArgb(), bitmap.GetPixel(60, 65).ToArgb());
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void PlateSwitchClearsAnimation() => RunSta(() =>
|
||||
{
|
||||
using var view = new PlateView();
|
||||
view.ShowValidationFinding(new PostVerificationFinding(PostVerificationKind.MissingLeadIn, 1, 1, null, "missing")
|
||||
{ Location = new Vector(3, 4) });
|
||||
Assert.True(view.ValidationOverlay.IsAnimating);
|
||||
view.Plate = new Plate();
|
||||
Assert.False(view.ValidationOverlay.IsAnimating);
|
||||
Assert.Null(view.ValidationOverlay.Finding);
|
||||
});
|
||||
|
||||
private static void RunSta(System.Action action) =>
|
||||
StaTestThread.Run(action, TimeSpan.FromSeconds(30), "Validation overlay test timed out.");
|
||||
}
|
||||
@@ -220,7 +220,20 @@ public class CuttingInspectionPanelTests
|
||||
Assert.False(accept.Checked);
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
Assert.False(Field<Button>(panel, "applyButton").Enabled);
|
||||
panel.RequestClose();
|
||||
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;
|
||||
Assert.Contains("affected part extent", Field<Label>(panel, "previewLabel").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]
|
||||
@@ -242,6 +255,10 @@ public class CuttingInspectionPanelTests
|
||||
Assert.Contains("Replan", Field<TextBox>(panel, "resultLabel").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();
|
||||
});
|
||||
|
||||
@@ -259,7 +276,18 @@ public class CuttingInspectionPanelTests
|
||||
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);
|
||||
@@ -304,7 +332,7 @@ public class CuttingInspectionPanelTests
|
||||
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.Equal(OpenNest.CNC.CuttingPlanning.CuttingCommitStatus.Applied, panel.CommitResult!.Status);
|
||||
Assert.IsType<InvalidOperationException>(error);
|
||||
Assert.Null(editor.CuttingInspection);
|
||||
Assert.False(editor.PlateView.InspectionOnly);
|
||||
@@ -312,6 +340,72 @@ public class CuttingInspectionPanelTests
|
||||
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();
|
||||
});
|
||||
|
||||
private sealed class MenuHost : MainForm
|
||||
{
|
||||
protected override void OnLoad(EventArgs e) { }
|
||||
}
|
||||
|
||||
private static T Field<T>(object target, string name) => Assert.IsType<T>(target.GetType()
|
||||
.GetField(name, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(target));
|
||||
|
||||
|
||||
@@ -1,348 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
// Shown forms and Application.OpenForms-based guards share this collection with the fill tests.
|
||||
[Collection("Fill operation lifetime")]
|
||||
public class CuttingPlanFormTests
|
||||
{
|
||||
[Fact]
|
||||
public void PlanThenApply_InstallsThePlanAndRedrawsTheEditorInPlateOrder() => RunSta(() =>
|
||||
{
|
||||
var (nest, view) = CreateView(Square("a", 12, 1), Square("b", 1, 1));
|
||||
using var editor = view;
|
||||
var plate = view.Plate;
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters());
|
||||
form.Show();
|
||||
WaitForPlan(form);
|
||||
|
||||
Assert.True(Control<Button>(form, "applyButton").Enabled);
|
||||
Assert.StartsWith("Ready to apply to 1 plate", Control<TextBox>(form, "summaryBox").Text);
|
||||
var preview = Field<PlateView>(form, "preview");
|
||||
Assert.True(preview.Visible);
|
||||
Assert.NotSame(plate, preview.Plate);
|
||||
Assert.Equal(2, preview.Plate.Parts.Count);
|
||||
Assert.All(preview.Plate.Parts, part => Assert.DoesNotContain(part, plate.Parts));
|
||||
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns)); // The preview is detached.
|
||||
|
||||
var reordered = 0;
|
||||
view.PartsReordered += (_, _) => reordered++;
|
||||
Invoke(form, "ApplyButton_Click", null, EventArgs.Empty);
|
||||
|
||||
Assert.Equal(DialogResult.OK, form.DialogResult);
|
||||
Assert.Equal(CuttingCommitStatus.Applied, form.CommitResult!.Status);
|
||||
Assert.All(plate.Parts, part => Assert.True(part.HasManualLeadIns));
|
||||
Assert.NotNull(plate.CuttingParameters);
|
||||
Assert.NotSame(form.ConfirmedParameters, plate.CuttingParameters);
|
||||
Assert.Equal(1, reordered);
|
||||
Assert.Equal(plate.Parts, view.LayoutParts.Select(layout => layout.BasePart));
|
||||
Assert.All(view.LayoutParts, layout => Assert.True(layout.IsDirty));
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void BlockedPlate_KeepsApplyDisabledAndShowsTheCurrentParts() => RunSta(() =>
|
||||
{
|
||||
var locked = Square("locked", 1, 1);
|
||||
var crossing = (OpenNest.CNC.Program)locked.Program.Clone();
|
||||
crossing.MoveTo(5, 5); // A locked crossing cannot be repaired by regeneration.
|
||||
Assert.True(locked.RestoreLeadInProgram(crossing, true));
|
||||
var (nest, view) = CreateView(locked);
|
||||
using var editor = view;
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters());
|
||||
form.Show();
|
||||
WaitForPlan(form);
|
||||
|
||||
Assert.False(Control<Button>(form, "applyButton").Enabled);
|
||||
var summary = Control<TextBox>(form, "summaryBox").Text;
|
||||
Assert.Contains("Plate 1: blocked", summary);
|
||||
var warnings = Control<DataGridView>(form, "warningGrid");
|
||||
Assert.Contains(warnings.Rows.Cast<DataGridViewRow>(), row =>
|
||||
row.Cells[3].Value?.ToString()?.Contains("Part 1 (locked)") == true);
|
||||
// A refused plate is never previewed; the editor already shows its part numbers.
|
||||
Assert.False(Field<PlateView>(form, "preview").Visible);
|
||||
Assert.Contains("no current, usable preview", Control<Label>(form, "previewLabel").Text);
|
||||
Assert.Null(view.Plate.CuttingParameters);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void ClosingWhilePlanning_CancelsAndWaitsForTheWorker() => RunSta(() =>
|
||||
{
|
||||
var (nest, view) = CreateView(Square("a", 1, 1), Square("b", 12, 1));
|
||||
using var editor = view;
|
||||
var programs = view.Plate.Parts.Select(part => part.Program).ToArray();
|
||||
using var entered = new ManualResetEventSlim();
|
||||
using var release = new ManualResetEventSlim();
|
||||
var cancelled = false;
|
||||
var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters())
|
||||
{
|
||||
// Hold the worker until the close request has been checked.
|
||||
BeforePlan = token =>
|
||||
{
|
||||
entered.Set();
|
||||
release.Wait(TimeSpan.FromSeconds(30));
|
||||
cancelled = token.IsCancellationRequested;
|
||||
},
|
||||
};
|
||||
try
|
||||
{
|
||||
form.Show();
|
||||
PumpUntil(() => form.IsPlanning, "planning to start");
|
||||
Assert.True(entered.Wait(TimeSpan.FromSeconds(30)), "The worker did not start.");
|
||||
|
||||
form.Close();
|
||||
|
||||
// The worker still holds the plan; the dialog stays up, cancelling, until it stops.
|
||||
Assert.False(form.IsDisposed);
|
||||
Assert.True(form.Visible);
|
||||
Assert.True(form.IsPlanning);
|
||||
Assert.False(Control<Button>(form, "cancelButton").Enabled);
|
||||
Assert.False(Control<Button>(form, "planButton").Enabled);
|
||||
release.Set();
|
||||
PumpUntil(() => form.IsDisposed, "the dialog to close after cancelling");
|
||||
Observe(form);
|
||||
Assert.True(cancelled);
|
||||
Assert.Equal(programs, view.Plate.Parts.Select(part => part.Program));
|
||||
Assert.All(view.Plate.Parts, part => Assert.False(part.HasManualLeadIns));
|
||||
Assert.Null(view.Plate.CuttingParameters);
|
||||
}
|
||||
finally
|
||||
{
|
||||
release.Set();
|
||||
form.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PlansWithItsOwnCopyOfTheSettings() => RunSta(() =>
|
||||
{
|
||||
var (nest, view) = CreateView(Square("a", 1, 1));
|
||||
using var editor = view;
|
||||
var parameters = Parameters();
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, parameters);
|
||||
|
||||
((LineLeadIn)parameters.ExternalLeadIn).Length = 5;
|
||||
|
||||
Assert.NotSame(parameters, form.ConfirmedParameters);
|
||||
Assert.Equal(0.3, ((LineLeadIn)form.ConfirmedParameters.ExternalLeadIn).Length);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void ApplyAfterALiveEdit_ChangesNothingAndReplanRecovers() => RunSta(() =>
|
||||
{
|
||||
var (nest, view) = CreateView(Square("a", 1, 1), Square("b", 12, 1));
|
||||
using var editor = view;
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters());
|
||||
form.Show();
|
||||
WaitForPlan(form);
|
||||
var programs = view.Plate.Parts.Select(part => part.Program).ToArray();
|
||||
|
||||
view.Plate.Parts[1].Offset(0, 1);
|
||||
Invoke(form, "ApplyButton_Click", null, EventArgs.Empty);
|
||||
|
||||
Assert.False(form.IsDisposed);
|
||||
Assert.Equal(DialogResult.None, form.DialogResult);
|
||||
Assert.Equal(CuttingCommitStatus.Stale, form.CommitResult!.Status);
|
||||
Assert.Contains("changed after planning", Control<TextBox>(form, "summaryBox").Text);
|
||||
Assert.False(Control<Button>(form, "applyButton").Enabled);
|
||||
Assert.Empty(Control<DataGridView>(form, "warningGrid").Rows.Cast<DataGridViewRow>());
|
||||
Assert.Equal(programs, view.Plate.Parts.Select(part => part.Program));
|
||||
Assert.Null(view.Plate.CuttingParameters);
|
||||
|
||||
Invoke(form, "PlanButton_Click", null, EventArgs.Empty);
|
||||
WaitForPlan(form);
|
||||
Assert.True(Control<Button>(form, "applyButton").Enabled);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void BusyEditor_DoesNotStartPlanningUntilItIsFree() => RunSta(() =>
|
||||
{
|
||||
var busy = true;
|
||||
var (nest, view) = CreateView(Square("a", 1, 1));
|
||||
using var editor = view;
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters(), () => busy);
|
||||
|
||||
Invoke(form, "StartPlanning");
|
||||
|
||||
Assert.False(form.IsPlanning);
|
||||
Assert.Null(form.Proposal);
|
||||
Assert.Contains("Wait for the current nesting or plate action", Control<TextBox>(form, "summaryBox").Text);
|
||||
busy = false;
|
||||
// Ending a DoEvents loop uninstalls WinForms' ambient context; planning started outside a
|
||||
// message loop must still report progress and results on the dialog's thread.
|
||||
Application.DoEvents();
|
||||
Invoke(form, "StartPlanning");
|
||||
WaitForPlan(form);
|
||||
Assert.True(Control<Button>(form, "applyButton").Enabled);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void AllPlates_SkipsEmptyPlatesAndRefusesTheBatchWhenOnePlateIsBlocked() => RunSta(() =>
|
||||
{
|
||||
var nest = new Nest();
|
||||
var first = nest.CreatePlate();
|
||||
first.Parts.Add(Square("open", 1, 1));
|
||||
var second = nest.CreatePlate();
|
||||
var locked = Square("locked", 1, 1);
|
||||
var crossing = (OpenNest.CNC.Program)locked.Program.Clone();
|
||||
crossing.MoveTo(5, 5);
|
||||
Assert.True(locked.RestoreLeadInProgram(crossing, true));
|
||||
second.Parts.Add(locked);
|
||||
nest.CreatePlate(); // An empty plate, like the editor's trailing plate.
|
||||
using var view = new PlateView { Plate = first };
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: true, Parameters());
|
||||
form.Show();
|
||||
WaitForPlan(form);
|
||||
|
||||
Assert.Equal("Plan Cutting — All Plates", form.Text);
|
||||
Assert.Equal(2, form.Proposal!.Plates.Count);
|
||||
var summary = Control<TextBox>(form, "summaryBox").Text;
|
||||
Assert.Contains("Plate 1: ready.", summary);
|
||||
Assert.Contains("Plate 2: blocked", summary);
|
||||
Assert.DoesNotContain("Plate 3", summary);
|
||||
Assert.False(Control<Button>(form, "applyButton").Enabled);
|
||||
Assert.All(first.Parts, part => Assert.False(part.HasManualLeadIns));
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void OnePlateDialog_ExcludesTheSentinelFromTheShownTotal() => RunSta(() =>
|
||||
{
|
||||
var (nest, view) = CreateView(Square("a", 1, 1));
|
||||
nest.CreatePlate(); // The editor's trailing empty new-plate sentinel.
|
||||
using var editor = view;
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters());
|
||||
|
||||
Assert.StartsWith("Plate 1 of 1, numbered in cutting order:", Control<Label>(form, "previewLabel").Text);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void SentinelActivePlate_IsLabeledNotNumberedBeyondTheTotal() => RunSta(() =>
|
||||
{
|
||||
var (nest, _) = CreateView(Square("a", 1, 1));
|
||||
var sentinel = nest.CreatePlate(); // trailing empty new-plate sentinel
|
||||
using var view = new PlateView { Plate = sentinel };
|
||||
using var editor = view;
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters());
|
||||
|
||||
var text = Control<Label>(form, "previewLabel").Text;
|
||||
Assert.StartsWith("New plate (empty)", text);
|
||||
Assert.DoesNotContain("Plate 2", text);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void UnverifiedPlan_RequiresAcceptanceAndReplanClearsIt() => RunSta(() =>
|
||||
{
|
||||
var program = SquareProgram();
|
||||
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);
|
||||
var part = new Part(new Drawing("touching contours", program), new Vector(1, 1));
|
||||
var (nest, view) = CreateView(part);
|
||||
using var editor = view;
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters());
|
||||
form.Show();
|
||||
WaitForPlan(form);
|
||||
var accept = Control<CheckBox>(form, "acceptWarningsCheckBox");
|
||||
Assert.True(accept.Visible);
|
||||
Assert.False(accept.Checked);
|
||||
Assert.False(Control<Button>(form, "applyButton").Enabled);
|
||||
Assert.True(Field<PlateView>(form, "preview").Visible);
|
||||
Assert.Contains("UNVERIFIED", Control<Label>(form, "previewLabel").Text);
|
||||
var grid = Control<DataGridView>(form, "warningGrid");
|
||||
Assert.NotEmpty(grid.Rows.Cast<DataGridViewRow>());
|
||||
Assert.Contains(grid.Rows.Cast<DataGridViewRow>(), row =>
|
||||
row.Cells[3].Value?.ToString()?.Contains("Material boundaries") == true);
|
||||
var partRow = grid.Rows.Cast<DataGridViewRow>().First(row => row.Cells[1].Value is int);
|
||||
partRow.Selected = true;
|
||||
Assert.Contains("affected part extent", Control<Label>(form, "previewLabel").Text);
|
||||
Assert.True(Field<PlateView>(form, "preview").Visible);
|
||||
Invoke(form, "ApplyButton_Click", null, EventArgs.Empty);
|
||||
Assert.Null(form.CommitResult);
|
||||
Assert.False(part.HasManualLeadIns);
|
||||
accept.Checked = true;
|
||||
Assert.True(Control<Button>(form, "applyButton").Enabled);
|
||||
Invoke(form, "PlanButton_Click", null, EventArgs.Empty);
|
||||
Assert.Empty(grid.Rows.Cast<DataGridViewRow>()); // No stale warning targets while replanning.
|
||||
WaitForPlan(form);
|
||||
Assert.NotEmpty(grid.Rows.Cast<DataGridViewRow>());
|
||||
Assert.False(accept.Checked);
|
||||
Assert.False(Control<Button>(form, "applyButton").Enabled);
|
||||
accept.Checked = true;
|
||||
Invoke(form, "ApplyButton_Click", null, EventArgs.Empty);
|
||||
Assert.Equal(CuttingCommitStatus.Applied, form.CommitResult!.Status);
|
||||
Assert.True(part.HasManualLeadIns);
|
||||
});
|
||||
|
||||
private static (Nest Nest, PlateView View) CreateView(params Part[] parts)
|
||||
{
|
||||
var nest = new Nest();
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Size = new Size(100, 100);
|
||||
foreach (var part in parts)
|
||||
plate.Parts.Add(part);
|
||||
return (nest, new PlateView { Plate = plate });
|
||||
}
|
||||
|
||||
private static CuttingParameters Parameters() => new()
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 45 },
|
||||
InternalLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 45 },
|
||||
ArcCircleLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 45 },
|
||||
ExternalLeadOut = new NoLeadOut(),
|
||||
InternalLeadOut = new NoLeadOut(),
|
||||
ArcCircleLeadOut = new NoLeadOut(),
|
||||
};
|
||||
|
||||
private static Part Square(string name, double x, double y) =>
|
||||
new(new Drawing(name, SquareProgram()), new Vector(x, y));
|
||||
|
||||
private static CNC.Program SquareProgram()
|
||||
{
|
||||
var program = new CNC.Program();
|
||||
program.MoveTo(0, 0);
|
||||
program.LineTo(0, 10);
|
||||
program.LineTo(10, 10);
|
||||
program.LineTo(10, 0);
|
||||
program.LineTo(0, 0);
|
||||
return program;
|
||||
}
|
||||
|
||||
private static void WaitForPlan(CuttingPlanForm form)
|
||||
{
|
||||
PumpUntil(() => form.Proposal != null && !form.IsPlanning, "the plan");
|
||||
Observe(form);
|
||||
}
|
||||
|
||||
// The planning task is already complete here; this rethrows anything its completion raised.
|
||||
private static void Observe(CuttingPlanForm form) => form.PlanningTask.GetAwaiter().GetResult();
|
||||
|
||||
private static void PumpUntil(Func<bool> condition, string what)
|
||||
{
|
||||
var until = DateTime.UtcNow.AddSeconds(60);
|
||||
while (!condition() && DateTime.UtcNow < until)
|
||||
{
|
||||
Application.DoEvents();
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
Assert.True(condition(), $"Timed out waiting for {what}.");
|
||||
}
|
||||
|
||||
private static T Control<T>(Form form, string name) where T : Control =>
|
||||
Assert.IsType<T>(form.Controls.Find(name, true).Single());
|
||||
|
||||
private static T Field<T>(object owner, string name) =>
|
||||
(T)owner.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(owner)!;
|
||||
|
||||
private static void Invoke(CuttingPlanForm form, string method, params object?[] arguments) =>
|
||||
typeof(CuttingPlanForm).GetMethod(method, BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.Invoke(form, arguments);
|
||||
|
||||
private static void RunSta(System.Action action) =>
|
||||
StaTestThread.Run(action, TimeSpan.FromMinutes(3), "The STA test did not complete.");
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using OpenNest.Forms;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
public class CuttingUiRetirementTests
|
||||
{
|
||||
[Fact]
|
||||
public void EditorAssemblyHasOnlyTheDockedInspectionEntryPoints()
|
||||
{
|
||||
var assembly = typeof(EditNestForm).Assembly;
|
||||
Assert.Null(assembly.GetType("OpenNest.Forms.CuttingPlanForm"));
|
||||
Assert.Null(assembly.GetType("OpenNest.Forms.ValidateNestForm"));
|
||||
Assert.Null(typeof(EditNestForm).GetMethod("PlanCutting"));
|
||||
// Non-editor consumers retain the shared analyzer, not an editor-dependent inspector.
|
||||
Assert.Contains(typeof(OpenNest.Diagnostics.PostVerificationAnalyzer).GetMethods(),
|
||||
method => method.Name == "Analyze" && method.GetParameters().Length > 0
|
||||
&& method.GetParameters()[0].ParameterType == typeof(Nest));
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,55 @@ public class CuttingValidationPanelTests
|
||||
}
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void OverlapNavigationKeepsAllPlateProgramsAndPosesStable() => RunSta(() =>
|
||||
{
|
||||
using var editor = RapidEditor();
|
||||
var plate = editor.Nest.Plates[1];
|
||||
plate.Parts.Add(new Part(plate.Parts[0].BaseDrawing) { Location = new Vector(2, 2) });
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
var parts = editor.Nest.Plates.SelectMany(p => p.Parts).ToArray();
|
||||
var programs = parts.Select(p => p.Program).ToArray();
|
||||
var codes = programs.Select(p => p.Codes.ToArray()).ToArray();
|
||||
var locations = parts.Select(p => p.Location).ToArray();
|
||||
var rotations = parts.Select(p => p.Rotation).ToArray();
|
||||
editor.OpenValidationInspection();
|
||||
var panel = editor.CuttingInspection!;
|
||||
Pump(() => panel.ValidationReport != null && !panel.IsPlanning);
|
||||
var grid = Field<DataGridView>(panel, "warningGrid");
|
||||
Assert.True(grid.ReadOnly);
|
||||
Assert.Equal(DataGridViewSelectionMode.FullRowSelect, grid.SelectionMode);
|
||||
Assert.All(grid.Columns.Cast<DataGridViewColumn>(), c => Assert.Equal(DataGridViewColumnSortMode.NotSortable, c.SortMode));
|
||||
Assert.True(Field<TextBox>(panel, "resultLabel").ReadOnly);
|
||||
var row = grid.Rows.Cast<DataGridViewRow>().Single(r => r.Tag is PostVerificationFinding { Kind: PostVerificationKind.Overlap });
|
||||
row.Selected = true;
|
||||
var view = editor.PlateView;
|
||||
Assert.Same(plate, view.Plate);
|
||||
Assert.NotNull(view.ValidationOverlay.Finding.Overlap);
|
||||
Assert.Contains("centroid", Field<TextBox>(panel, "resultLabel").Text);
|
||||
var scale = view.ViewScale;
|
||||
InvokeView(view, "OnMouseWheel", new MouseEventArgs(MouseButtons.None, 0, 100, 100, 120));
|
||||
Assert.True(view.ViewScale > scale);
|
||||
var viewport = view.CaptureViewport();
|
||||
InvokeView(view, "OnMouseMove", new MouseEventArgs(MouseButtons.Middle, 0, 120, 110, 0));
|
||||
Assert.NotEqual(viewport.Origin, view.CaptureViewport().Origin);
|
||||
view.SelectAll();
|
||||
InvokeView(view, "OnKeyDown", new KeyEventArgs(Keys.Delete));
|
||||
InvokeView(view, "ProcessDialogKey", Keys.Right);
|
||||
panel.RequestClose();
|
||||
Assert.Equal(parts, editor.Nest.Plates.SelectMany(p => p.Parts));
|
||||
Assert.Equal(programs, parts.Select(p => p.Program));
|
||||
for (var i = 0; i < programs.Length; i++) Assert.Equal(codes[i], programs[i].Codes.ToArray());
|
||||
Assert.Equal(locations, parts.Select(p => p.Location));
|
||||
Assert.Equal(rotations, parts.Select(p => p.Rotation));
|
||||
Assert.Null(view.ValidationOverlay.Finding);
|
||||
Assert.False(view.ValidationOverlay.IsAnimating);
|
||||
});
|
||||
|
||||
private static void InvokeView(Control target, string name, params object[] args) => target.GetType()
|
||||
.GetMethod(name, BindingFlags.NonPublic | BindingFlags.Instance)!.Invoke(target, args);
|
||||
|
||||
private static EditNestForm RapidEditor()
|
||||
{
|
||||
var nest = new Nest();
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
using System.Drawing;
|
||||
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.Forms;
|
||||
|
||||
public class ValidateNestFormTests
|
||||
{
|
||||
[Fact]
|
||||
public void RapidContactCrosshairIsDrawnAtContactAndClearedWithFinding()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
using var view = new PlateView();
|
||||
view.ShowValidationFinding(new PostVerificationFinding(PostVerificationKind.RapidCrossing,
|
||||
1, 2, 1, "crossing")
|
||||
{
|
||||
RapidStart = new Vector(-1, 0),
|
||||
RapidEnd = new Vector(1, 0),
|
||||
ContactPoints = Array.AsReadOnly(new[] { Vector.Zero })
|
||||
});
|
||||
using var bitmap = new Bitmap(120, 120);
|
||||
using var graphics = Graphics.FromImage(bitmap);
|
||||
graphics.TranslateTransform(60, 60);
|
||||
graphics.Clear(Color.Black);
|
||||
view.ValidationOverlay.Draw(graphics);
|
||||
// Off the horizontal rapid, on the contact's vertical crosshair.
|
||||
var pixel = bitmap.GetPixel(60, 65);
|
||||
Assert.True(pixel.R > 150 && pixel.B > 50 && pixel.G < 100);
|
||||
view.ClearValidationFinding();
|
||||
graphics.Clear(Color.Black);
|
||||
view.ValidationOverlay.Draw(graphics);
|
||||
Assert.Equal(Color.Black.ToArgb(), bitmap.GetPixel(60, 65).ToArgb());
|
||||
});
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Completed tasks on a dedicated STA thread.
|
||||
[Fact]
|
||||
public void GridNavigatesMainViewAndRestoresViewportOnDisposal()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var nest = new Nest("interactive inspection");
|
||||
var program = new CNC.Program();
|
||||
program.MoveTo(0, 0);
|
||||
program.LineTo(5, 0);
|
||||
program.LineTo(5, 5);
|
||||
program.LineTo(0, 5);
|
||||
program.LineTo(0, 0);
|
||||
nest.CreatePlate().Parts.Add(new Part(new Drawing("first", program)));
|
||||
var second = nest.CreatePlate();
|
||||
second.Parts.Add(new Part(new Drawing("second", program)));
|
||||
second.Parts.Add(new Part(second.Parts[0].BaseDrawing) { Location = new Vector(2, 2) });
|
||||
using var editor = new EditNestForm(nest);
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
var view = editor.PlateView;
|
||||
view.ZoomToArea(-10, -10, 40, 40);
|
||||
var viewport = view.CaptureViewport();
|
||||
view.OverlapDisplay = OverlapDisplayMode.Centroids;
|
||||
using (var form = new ValidateNestForm(editor,
|
||||
(value, token) => Task.FromResult(PostVerificationAnalyzer.Analyze(value, token))))
|
||||
{
|
||||
form.Show(editor);
|
||||
form.ValidateAsync().GetAwaiter().GetResult();
|
||||
Assert.True(form.TopLevel);
|
||||
Assert.False(form.Modal);
|
||||
Assert.True(editor.Enabled);
|
||||
Assert.True(view.Enabled);
|
||||
Assert.True(view.InspectionOnly);
|
||||
var grid = Find<DataGridView>(form, "warningGrid");
|
||||
Assert.True(grid.ReadOnly);
|
||||
Assert.Equal(DataGridViewSelectionMode.FullRowSelect, grid.SelectionMode);
|
||||
var row = grid.Rows.Cast<DataGridViewRow>().Single(row =>
|
||||
row.Tag is PostVerificationFinding { Kind: PostVerificationKind.Overlap });
|
||||
grid.CurrentCell = row.Cells[0];
|
||||
Assert.Same(second, view.Plate);
|
||||
Assert.Equal(1, editor.PlateManager.CurrentIndex);
|
||||
Assert.NotNull(view.ValidationOverlay.Finding.Overlap);
|
||||
Assert.True(view.ValidationOverlay.IsAnimating);
|
||||
using var bitmap = new Bitmap(view.Width, view.Height);
|
||||
view.DrawToBitmap(bitmap, view.ClientRectangle);
|
||||
Assert.Contains("centroid", Find<TextBox>(form, "reportBox").Text);
|
||||
view.SelectAll();
|
||||
var scale = view.ViewScale;
|
||||
Invoke(view, "OnMouseWheel", new MouseEventArgs(MouseButtons.None, 0, 100, 100, 120));
|
||||
Assert.True(view.ViewScale > scale);
|
||||
var pan = view.CaptureViewport();
|
||||
Invoke(view, "OnMouseMove", new MouseEventArgs(MouseButtons.Middle, 0, 120, 110, 0));
|
||||
Assert.NotEqual(pan.Origin, view.CaptureViewport().Origin);
|
||||
Invoke(view, "OnKeyDown", new KeyEventArgs(Keys.Delete));
|
||||
Invoke(view, "ProcessDialogKey", Keys.Right);
|
||||
Invoke(view, "OnMouseDown", new MouseEventArgs(MouseButtons.Middle, 1, 120, 110, 0));
|
||||
Invoke(view, "OnMouseUp", new MouseEventArgs(MouseButtons.Middle, 1, 120, 110, 0));
|
||||
Assert.Equal(2, second.Parts.Count);
|
||||
Assert.All(second.Parts, part => Assert.Equal(0, part.Rotation));
|
||||
}
|
||||
Assert.Same(nest.Plates[0], view.Plate);
|
||||
Assert.Equal(viewport, view.CaptureViewport());
|
||||
Assert.Equal(OverlapDisplayMode.Centroids, view.OverlapDisplay);
|
||||
Assert.Null(view.ValidationOverlay.Finding);
|
||||
Assert.False(view.ValidationOverlay.IsAnimating);
|
||||
Assert.False(view.InspectionOnly);
|
||||
Assert.Equal(new Vector(2, 2), second.Parts[1].Location);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModelessCancellationKeepsEditingLockedUntilWorkerStops()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var pending = new TaskCompletionSource<PostVerificationReport>();
|
||||
using var editor = new EditNestForm(new Nest("inspection"));
|
||||
editor.Show();
|
||||
using var form = new ValidateNestForm(editor, (_, _) => pending.Task);
|
||||
form.Show(editor);
|
||||
var validation = form.ValidateAsync();
|
||||
Assert.False(validation.IsCompleted);
|
||||
Assert.True(editor.Enabled);
|
||||
Assert.True(editor.PlateView.Enabled);
|
||||
Assert.True(editor.PlateView.InspectionOnly);
|
||||
form.Close();
|
||||
Assert.True(form.Visible);
|
||||
Assert.True(editor.PlateView.InspectionOnly);
|
||||
pending.SetCanceled();
|
||||
var timeout = System.Diagnostics.Stopwatch.StartNew();
|
||||
while (!form.IsDisposed && timeout.Elapsed < TimeSpan.FromSeconds(5))
|
||||
Application.DoEvents();
|
||||
Assert.True(form.IsDisposed);
|
||||
Assert.False(editor.PlateView.InspectionOnly);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlateSwitchClearsAnimation()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
using var view = new PlateView();
|
||||
view.ShowValidationFinding(new PostVerificationFinding(PostVerificationKind.MissingLeadIn,
|
||||
1, 1, null, "missing")
|
||||
{ Location = new Vector(3, 4) });
|
||||
Assert.True(view.ValidationOverlay.IsAnimating);
|
||||
view.Plate = new Plate();
|
||||
Assert.False(view.ValidationOverlay.IsAnimating);
|
||||
Assert.Null(view.ValidationOverlay.Finding);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChecksAllPlatesWithoutChangingProgramsOrPlacements()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var nest = new Nest("PEP inspection");
|
||||
var program = new CNC.Program();
|
||||
program.Codes.Add(new RapidMove(0, 0));
|
||||
program.Codes.Add(new LinearMove(5, 0));
|
||||
program.Codes.Add(new LinearMove(5, 5));
|
||||
program.Codes.Add(new LinearMove(0, 5));
|
||||
program.Codes.Add(new LinearMove(0, 0));
|
||||
var first = new Part(new Drawing("square", program));
|
||||
var second = new Part(first.BaseDrawing);
|
||||
nest.CreatePlate().Parts.Add(first);
|
||||
nest.CreatePlate().Parts.Add(second);
|
||||
var firstProgram = first.Program;
|
||||
var secondProgram = second.Program;
|
||||
var firstCodes = firstProgram.Codes.ToArray();
|
||||
var location = first.Location;
|
||||
var rotation = first.Rotation;
|
||||
using var form = new ValidateNestForm(nest,
|
||||
(value, token) => Task.FromResult(PostVerificationAnalyzer.Analyze(value, token)));
|
||||
|
||||
form.ValidateAsync().GetAwaiter().GetResult();
|
||||
|
||||
var report = Find<TextBox>(form, "reportBox");
|
||||
Assert.True(report.ReadOnly);
|
||||
Assert.Contains("Plate 1, part 1", report.Text);
|
||||
Assert.Contains("Plate 2, part 1", report.Text);
|
||||
Assert.DoesNotContain("Post processor", report.Text);
|
||||
Assert.Contains("Warnings found", Find<Label>(form, "summaryLabel").Text);
|
||||
Assert.Same(first, nest.Plates[0].Parts[0]);
|
||||
Assert.Same(second, nest.Plates[1].Parts[0]);
|
||||
Assert.Same(firstProgram, first.Program);
|
||||
Assert.Same(secondProgram, second.Program);
|
||||
Assert.Equal(firstCodes, first.Program.Codes.ToArray());
|
||||
Assert.Equal(location, first.Location);
|
||||
Assert.Equal(rotation, first.Rotation);
|
||||
Assert.Same(Find<Button>(form, "closeButton"), form.AcceptButton);
|
||||
Assert.Same(form.AcceptButton, form.CancelButton);
|
||||
Assert.Empty(form.Controls.Find("postButton", true));
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DistinguishesClearAndIncompleteChecks(bool incomplete)
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var nest = new Nest("inspection");
|
||||
if (incomplete)
|
||||
nest.CreatePlate().Parts.Add(new Part(new Drawing("invalid", new CNC.Program())));
|
||||
using var form = new ValidateNestForm(nest,
|
||||
(value, token) => Task.FromResult(PostVerificationAnalyzer.Analyze(value, token)));
|
||||
form.ValidateAsync().GetAwaiter().GetResult();
|
||||
Assert.Contains(incomplete ? "Validation incomplete" : "No warnings found",
|
||||
Find<Label>(form, "summaryLabel").Text);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailureIsReportedWithoutAnAllClear()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
using var form = new ValidateNestForm(new Nest("inspection"),
|
||||
(_, _) => Task.FromException<PostVerificationReport>(new InvalidOperationException("test failure")));
|
||||
form.ValidateAsync().GetAwaiter().GetResult();
|
||||
Assert.Contains("Validation failed", Find<Label>(form, "summaryLabel").Text);
|
||||
Assert.Contains("test failure", Find<TextBox>(form, "reportBox").Text);
|
||||
Assert.Equal("Close", Find<Button>(form, "closeButton").Text);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Cancel")]
|
||||
[InlineData("Escape")]
|
||||
[InlineData("Close")]
|
||||
public void RealModalCancellationKeepsOwnerDisabledUntilWorkerStops(string action)
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var pending = new TaskCompletionSource<PostVerificationReport>();
|
||||
using var owner = new Form();
|
||||
using var form = new ValidateNestForm(new Nest("inspection"), (_, _) => pending.Task);
|
||||
Exception? failure = null;
|
||||
owner.Show();
|
||||
using var timeout = new System.Windows.Forms.Timer { Interval = 10000 };
|
||||
timeout.Tick += (_, _) =>
|
||||
{
|
||||
failure ??= new TimeoutException("Modal cancellation did not finish.");
|
||||
pending.TrySetCanceled();
|
||||
form.Close();
|
||||
};
|
||||
form.Shown += (_, _) => form.BeginInvoke(new System.Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Assert.False(IsWindowEnabled(owner.Handle));
|
||||
if (action == "Cancel")
|
||||
Find<Button>(form, "closeButton").PerformClick();
|
||||
else if (action == "Escape")
|
||||
Invoke(form, "ProcessDialogKey", Keys.Escape);
|
||||
else
|
||||
form.Close();
|
||||
|
||||
// ShowDialog must stay active while the worker ignores cancellation.
|
||||
Assert.True(form.Visible);
|
||||
Assert.False(IsWindowEnabled(owner.Handle));
|
||||
Assert.False(Find<Button>(form, "closeButton").Enabled);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failure = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
pending.TrySetCanceled();
|
||||
}
|
||||
}));
|
||||
timeout.Start();
|
||||
var result = form.ShowDialog(owner);
|
||||
timeout.Stop();
|
||||
Assert.Equal(DialogResult.Cancel, result);
|
||||
Assert.True(IsWindowEnabled(owner.Handle));
|
||||
if (failure != null)
|
||||
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||
});
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
private static extern bool IsWindowEnabled(IntPtr window);
|
||||
|
||||
private static void Invoke(Control form, string method, params object?[] arguments) =>
|
||||
form.GetType().GetMethod(method, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
|
||||
.Invoke(form, arguments);
|
||||
|
||||
private static void RunSta(System.Action action) =>
|
||||
StaTestThread.Run(action, TimeSpan.FromSeconds(30), "STA validation test timed out.");
|
||||
|
||||
private static T Find<T>(Form form, string name) where T : Control =>
|
||||
Assert.IsType<T>(form.Controls.Find(name, true).Single());
|
||||
}
|
||||
@@ -431,6 +431,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
}
|
||||
|
||||
private void UpdateApply() => applyButton.Enabled = Mode == CuttingInspectionMode.Plan && !invalidated && !IsPlanning
|
||||
&& plates.Any(item => item.Parts.Count > 0)
|
||||
&& (Proposal?.CanApply == true || Proposal?.CanApplyWithWarnings == true && acceptWarningsCheckBox.Checked);
|
||||
|
||||
private bool IsPreviewCurrent()
|
||||
@@ -453,6 +454,13 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
selectedPlate = index;
|
||||
view.ClearProposalDisplay();
|
||||
view.ClearValidationFinding();
|
||||
if (plates[index].Parts.Count == 0)
|
||||
{
|
||||
preview = null;
|
||||
previewLabel.Text = "New plate (empty)";
|
||||
SetPlanning(false);
|
||||
return;
|
||||
}
|
||||
preview = inspection.RefreshPreview(index); // Only the guarded authority may clone programs.
|
||||
SetPlanning(false);
|
||||
if (preview == null)
|
||||
|
||||
-291
@@ -1,291 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.Forms;
|
||||
|
||||
partial class CuttingPlanForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
planning?.Cancel();
|
||||
components?.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
layout = new TableLayoutPanel();
|
||||
settingsPanel = new TableLayoutPanel();
|
||||
settingsLabel = new Label();
|
||||
settingsButton = new Button();
|
||||
optionsPanel = new FlowLayoutPanel();
|
||||
keepOrderCheckBox = new CheckBox();
|
||||
acceptWarningsCheckBox = new CheckBox();
|
||||
planButton = new Button();
|
||||
split = new SplitContainer();
|
||||
previewLayout = new TableLayoutPanel();
|
||||
previewLabel = new Label();
|
||||
previewPanel = new Panel();
|
||||
summaryBox = new TextBox();
|
||||
statusLabel = new Label();
|
||||
buttonsPanel = new FlowLayoutPanel();
|
||||
applyButton = new Button();
|
||||
cancelButton = new Button();
|
||||
layout.SuspendLayout();
|
||||
settingsPanel.SuspendLayout();
|
||||
optionsPanel.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)split).BeginInit();
|
||||
split.Panel1.SuspendLayout();
|
||||
split.Panel2.SuspendLayout();
|
||||
split.SuspendLayout();
|
||||
previewLayout.SuspendLayout();
|
||||
buttonsPanel.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// layout
|
||||
//
|
||||
layout.ColumnCount = 1;
|
||||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
layout.Controls.Add(settingsPanel, 0, 0);
|
||||
layout.Controls.Add(optionsPanel, 0, 1);
|
||||
layout.Controls.Add(split, 0, 2);
|
||||
layout.Controls.Add(statusLabel, 0, 3);
|
||||
layout.Controls.Add(acceptWarningsCheckBox, 0, 4);
|
||||
layout.Controls.Add(buttonsPanel, 0, 5);
|
||||
layout.Dock = DockStyle.Fill;
|
||||
layout.Name = "layout";
|
||||
layout.RowCount = 6;
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.TabIndex = 0;
|
||||
//
|
||||
// settingsPanel
|
||||
//
|
||||
settingsPanel.AutoSize = true;
|
||||
settingsPanel.ColumnCount = 2;
|
||||
settingsPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
settingsPanel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
settingsPanel.Controls.Add(settingsLabel, 0, 0);
|
||||
settingsPanel.Controls.Add(settingsButton, 1, 0);
|
||||
settingsPanel.Dock = DockStyle.Fill;
|
||||
settingsPanel.Margin = new Padding(0, 0, 0, 4);
|
||||
settingsPanel.Name = "settingsPanel";
|
||||
settingsPanel.RowCount = 1;
|
||||
settingsPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
settingsPanel.TabIndex = 0;
|
||||
//
|
||||
// settingsLabel
|
||||
//
|
||||
settingsLabel.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
settingsLabel.AutoSize = true;
|
||||
settingsLabel.Name = "settingsLabel";
|
||||
settingsLabel.TabIndex = 0;
|
||||
settingsLabel.Text = "Lead-ins:";
|
||||
//
|
||||
// settingsButton
|
||||
//
|
||||
settingsButton.AutoSize = true;
|
||||
settingsButton.Name = "settingsButton";
|
||||
settingsButton.Size = new Size(130, 30);
|
||||
settingsButton.TabIndex = 1;
|
||||
settingsButton.Text = "Cutting &Settings...";
|
||||
settingsButton.UseVisualStyleBackColor = true;
|
||||
settingsButton.Click += SettingsButton_Click;
|
||||
//
|
||||
// optionsPanel
|
||||
//
|
||||
optionsPanel.AutoSize = true;
|
||||
optionsPanel.Controls.Add(keepOrderCheckBox);
|
||||
optionsPanel.Controls.Add(planButton);
|
||||
optionsPanel.Dock = DockStyle.Fill;
|
||||
optionsPanel.Margin = new Padding(0, 0, 0, 4);
|
||||
optionsPanel.Name = "optionsPanel";
|
||||
optionsPanel.TabIndex = 1;
|
||||
optionsPanel.WrapContents = false;
|
||||
//
|
||||
// keepOrderCheckBox
|
||||
//
|
||||
keepOrderCheckBox.Anchor = AnchorStyles.Left;
|
||||
keepOrderCheckBox.AutoSize = true;
|
||||
keepOrderCheckBox.Margin = new Padding(3, 3, 12, 3);
|
||||
keepOrderCheckBox.Name = "keepOrderCheckBox";
|
||||
keepOrderCheckBox.TabIndex = 0;
|
||||
keepOrderCheckBox.Text = "&Keep the current part order";
|
||||
keepOrderCheckBox.UseVisualStyleBackColor = true;
|
||||
keepOrderCheckBox.CheckedChanged += KeepOrderCheckBox_CheckedChanged;
|
||||
//
|
||||
// planButton
|
||||
//
|
||||
planButton.AutoSize = true;
|
||||
planButton.Name = "planButton";
|
||||
planButton.Size = new Size(85, 30);
|
||||
planButton.TabIndex = 1;
|
||||
planButton.Text = "&Replan";
|
||||
planButton.UseVisualStyleBackColor = true;
|
||||
planButton.Click += PlanButton_Click;
|
||||
//
|
||||
// split
|
||||
//
|
||||
split.Dock = DockStyle.Fill;
|
||||
split.Name = "split";
|
||||
split.TabIndex = 2;
|
||||
//
|
||||
// split.Panel1
|
||||
//
|
||||
split.Panel1.Controls.Add(previewLayout);
|
||||
//
|
||||
// split.Panel2
|
||||
//
|
||||
split.Panel2.Controls.Add(summaryBox);
|
||||
//
|
||||
// previewLayout
|
||||
//
|
||||
previewLayout.ColumnCount = 1;
|
||||
previewLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
previewLayout.Controls.Add(previewLabel, 0, 0);
|
||||
previewLayout.Controls.Add(previewPanel, 0, 1);
|
||||
previewLayout.Dock = DockStyle.Fill;
|
||||
previewLayout.Margin = new Padding(0);
|
||||
previewLayout.Name = "previewLayout";
|
||||
previewLayout.RowCount = 2;
|
||||
previewLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
previewLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
previewLayout.TabIndex = 0;
|
||||
//
|
||||
// previewLabel
|
||||
//
|
||||
previewLabel.AutoSize = true;
|
||||
previewLabel.Dock = DockStyle.Fill;
|
||||
previewLabel.Name = "previewLabel";
|
||||
previewLabel.TabIndex = 0;
|
||||
previewLabel.Text = "Preview:";
|
||||
//
|
||||
// previewPanel
|
||||
//
|
||||
previewPanel.BorderStyle = BorderStyle.FixedSingle;
|
||||
previewPanel.Dock = DockStyle.Fill;
|
||||
previewPanel.Name = "previewPanel";
|
||||
previewPanel.TabIndex = 1;
|
||||
//
|
||||
// summaryBox
|
||||
//
|
||||
summaryBox.Dock = DockStyle.Fill;
|
||||
summaryBox.Multiline = true;
|
||||
summaryBox.Name = "summaryBox";
|
||||
summaryBox.ReadOnly = true;
|
||||
summaryBox.ScrollBars = ScrollBars.Vertical;
|
||||
summaryBox.TabIndex = 0;
|
||||
//
|
||||
// statusLabel
|
||||
//
|
||||
statusLabel.AutoSize = true;
|
||||
statusLabel.Dock = DockStyle.Fill;
|
||||
statusLabel.Margin = new Padding(3, 6, 3, 0);
|
||||
statusLabel.Name = "statusLabel";
|
||||
statusLabel.TabIndex = 3;
|
||||
//
|
||||
// acceptWarningsCheckBox
|
||||
//
|
||||
acceptWarningsCheckBox.AutoSize = true;
|
||||
acceptWarningsCheckBox.Dock = DockStyle.Fill;
|
||||
acceptWarningsCheckBox.Name = "acceptWarningsCheckBox";
|
||||
acceptWarningsCheckBox.Text = "I reviewed the warnings. Apply this unverified plan.";
|
||||
acceptWarningsCheckBox.Visible = false;
|
||||
acceptWarningsCheckBox.TabIndex = 4;
|
||||
acceptWarningsCheckBox.CheckedChanged += AcceptWarningsCheckBox_CheckedChanged;
|
||||
//
|
||||
// buttonsPanel
|
||||
//
|
||||
buttonsPanel.AutoSize = true;
|
||||
buttonsPanel.Controls.Add(cancelButton);
|
||||
buttonsPanel.Controls.Add(applyButton);
|
||||
buttonsPanel.Dock = DockStyle.Fill;
|
||||
buttonsPanel.FlowDirection = FlowDirection.RightToLeft;
|
||||
buttonsPanel.Margin = new Padding(0, 8, 0, 0);
|
||||
buttonsPanel.Name = "buttonsPanel";
|
||||
buttonsPanel.TabIndex = 5;
|
||||
buttonsPanel.WrapContents = false;
|
||||
//
|
||||
// applyButton
|
||||
//
|
||||
applyButton.AutoSize = true;
|
||||
applyButton.Enabled = false;
|
||||
applyButton.Name = "applyButton";
|
||||
applyButton.Size = new Size(85, 30);
|
||||
applyButton.TabIndex = 1;
|
||||
applyButton.Text = "&Apply";
|
||||
applyButton.UseVisualStyleBackColor = true;
|
||||
applyButton.Click += ApplyButton_Click;
|
||||
//
|
||||
// cancelButton
|
||||
//
|
||||
cancelButton.AutoSize = true;
|
||||
cancelButton.DialogResult = DialogResult.Cancel;
|
||||
cancelButton.Name = "cancelButton";
|
||||
cancelButton.Size = new Size(85, 30);
|
||||
cancelButton.TabIndex = 0;
|
||||
cancelButton.Text = "Cancel";
|
||||
cancelButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// CuttingPlanForm
|
||||
//
|
||||
AcceptButton = planButton;
|
||||
AutoScaleDimensions = new SizeF(7F, 15F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
CancelButton = cancelButton;
|
||||
ClientSize = new Size(940, 620);
|
||||
Controls.Add(layout);
|
||||
Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point);
|
||||
MinimizeBox = false;
|
||||
MinimumSize = new Size(700, 480);
|
||||
Name = "CuttingPlanForm";
|
||||
Padding = new Padding(12);
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Text = "Plan Cutting";
|
||||
layout.ResumeLayout(false);
|
||||
layout.PerformLayout();
|
||||
settingsPanel.ResumeLayout(false);
|
||||
settingsPanel.PerformLayout();
|
||||
optionsPanel.ResumeLayout(false);
|
||||
optionsPanel.PerformLayout();
|
||||
split.Panel1.ResumeLayout(false);
|
||||
split.Panel2.ResumeLayout(false);
|
||||
split.Panel2.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)split).EndInit();
|
||||
split.ResumeLayout(false);
|
||||
previewLayout.ResumeLayout(false);
|
||||
previewLayout.PerformLayout();
|
||||
buttonsPanel.ResumeLayout(false);
|
||||
buttonsPanel.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
private TableLayoutPanel layout;
|
||||
private TableLayoutPanel settingsPanel;
|
||||
private Label settingsLabel;
|
||||
private Button settingsButton;
|
||||
private FlowLayoutPanel optionsPanel;
|
||||
private CheckBox keepOrderCheckBox;
|
||||
private CheckBox acceptWarningsCheckBox;
|
||||
private Button planButton;
|
||||
private SplitContainer split;
|
||||
private TableLayoutPanel previewLayout;
|
||||
private Label previewLabel;
|
||||
private Panel previewPanel;
|
||||
private TextBox summaryBox;
|
||||
private Label statusLabel;
|
||||
private FlowLayoutPanel buttonsPanel;
|
||||
private Button applyButton;
|
||||
private Button cancelButton;
|
||||
}
|
||||
@@ -1,514 +0,0 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
|
||||
namespace OpenNest.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// Plans part order and lead-ins for the active plate or every plate, previews the result and
|
||||
/// applies it all-or-nothing through the cutting planner. Nothing live changes before Apply.
|
||||
/// </summary>
|
||||
public partial class CuttingPlanForm : Form
|
||||
{
|
||||
private readonly PlateView plateView;
|
||||
private readonly Plate activePlate;
|
||||
private readonly Plate[] plates;
|
||||
private readonly int[] plateNumbers;
|
||||
private readonly Func<bool> isOperationBusy;
|
||||
private readonly string unit;
|
||||
private readonly PlateView preview;
|
||||
private readonly DataGridView warningGrid;
|
||||
private bool populatingWarnings;
|
||||
private readonly SynchronizationContext uiContext;
|
||||
private readonly string previewText;
|
||||
private CuttingParameters parameters;
|
||||
private CancellationTokenSource planning;
|
||||
private CuttingPlanProposal proposal;
|
||||
private int generation;
|
||||
private bool closeRequested;
|
||||
|
||||
/// <param name="plateView">The editor's view of the active plate.</param>
|
||||
/// <param name="nest">The nest that owns the active plate.</param>
|
||||
/// <param name="allPlates">Plan every plate that has parts instead of only the active plate.</param>
|
||||
/// <param name="parameters">Cutting settings to start from; the form keeps its own copy.</param>
|
||||
/// <param name="isOperationBusy">Reports a nesting or plate operation that must finish first.</param>
|
||||
public CuttingPlanForm(PlateView plateView, Nest nest, bool allPlates, CuttingParameters parameters,
|
||||
Func<bool> isOperationBusy = null)
|
||||
{
|
||||
this.plateView = plateView ?? throw new ArgumentNullException(nameof(plateView));
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
ArgumentNullException.ThrowIfNull(parameters);
|
||||
// An owned copy: later edits to the caller's settings objects cannot change this plan.
|
||||
this.parameters = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(parameters));
|
||||
activePlate = plateView.Plate ?? throw new ArgumentException("An active plate is required.", nameof(plateView));
|
||||
var all = nest.Plates.ToList();
|
||||
if (!all.Contains(activePlate))
|
||||
throw new ArgumentException("The active plate is not in this nest.", nameof(nest));
|
||||
plates = allPlates ? all.Where(p => p.Parts.Count > 0).ToArray() : new[] { activePlate };
|
||||
if (plates.Length == 0)
|
||||
plates = new[] { activePlate };
|
||||
plateNumbers = plates.Select(p => all.IndexOf(p) + 1).ToArray();
|
||||
this.isOperationBusy = isOperationBusy;
|
||||
unit = UnitsHelper.GetShortString(nest.Units);
|
||||
InitializeComponent();
|
||||
StartPosition = FormStartPosition.Manual;
|
||||
warningGrid = new DataGridView
|
||||
{
|
||||
Name = "warningGrid",
|
||||
Dock = DockStyle.Fill,
|
||||
ReadOnly = true,
|
||||
AllowUserToAddRows = false,
|
||||
AllowUserToDeleteRows = false,
|
||||
MultiSelect = false,
|
||||
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||||
AutoGenerateColumns = false,
|
||||
RowHeadersVisible = false,
|
||||
BackgroundColor = SystemColors.Window,
|
||||
};
|
||||
warningGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
HeaderText = "Plate",
|
||||
Width = 58,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable
|
||||
});
|
||||
warningGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
HeaderText = "Part",
|
||||
Width = 58,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable
|
||||
});
|
||||
warningGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
HeaderText = "With",
|
||||
Width = 58,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable
|
||||
});
|
||||
warningGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
HeaderText = "Warning / finding",
|
||||
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable
|
||||
});
|
||||
warningGrid.SelectionChanged += WarningGrid_SelectionChanged;
|
||||
var findingsLayout = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1 };
|
||||
findingsLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 30));
|
||||
findingsLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 70));
|
||||
summaryBox.Dock = DockStyle.Fill;
|
||||
findingsLayout.Controls.Add(summaryBox, 0, 0);
|
||||
findingsLayout.Controls.Add(warningGrid, 0, 1);
|
||||
split.Panel2.Controls.Add(findingsLayout);
|
||||
// Keep the batch outcome visible; every individual diagnostic lives in the grid.
|
||||
summaryBox.MaxLength = int.MaxValue;
|
||||
uiContext = SynchronizationContext.Current ?? new WindowsFormsSynchronizationContext();
|
||||
|
||||
preview = new PlateView
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
AllowSelect = false,
|
||||
AllowDrop = false,
|
||||
DrawOrigin = false,
|
||||
DrawRapid = true,
|
||||
Cursor = Cursors.Default,
|
||||
TabStop = false,
|
||||
Visible = false,
|
||||
};
|
||||
preview.SizeChanged += (_, _) =>
|
||||
{
|
||||
if (preview.Visible)
|
||||
preview.ZoomToFit();
|
||||
};
|
||||
previewPanel.Controls.Add(preview);
|
||||
|
||||
if (allPlates)
|
||||
{
|
||||
Text = "Plan Cutting — All Plates";
|
||||
applyButton.Text = "&Apply to All Plates";
|
||||
}
|
||||
// Display-only numbering: the trailing empty new-plate sentinel is excluded from the
|
||||
// "of M" total; plateNumbers keep real collection positions for the batch summary.
|
||||
var displayCount = PlateDisplayNumbering.DisplayedPlateCount(all);
|
||||
var activeNumber = PlateDisplayNumbering.DisplayedPlateNumber(all, all.IndexOf(activePlate));
|
||||
previewText = activeNumber == null
|
||||
? "New plate (empty) has no parts; plates with parts are listed on the right."
|
||||
: plates.Contains(activePlate)
|
||||
? $"Plate {activeNumber} of {displayCount}, numbered in cutting order:"
|
||||
: $"Plate {activeNumber} has no parts; plates with parts are listed on the right.";
|
||||
previewLabel.Text = previewText;
|
||||
ShowSettings();
|
||||
}
|
||||
|
||||
/// <summary>The settings the operator confirmed; meaningful after an OK result.</summary>
|
||||
public CuttingParameters ConfirmedParameters => parameters;
|
||||
|
||||
/// <summary>The commit outcome of the last Apply, or null if Apply never ran.</summary>
|
||||
public CuttingCommitResult CommitResult { get; private set; }
|
||||
|
||||
internal Task PlanningTask { get; private set; } = Task.CompletedTask;
|
||||
|
||||
internal CuttingPlanProposal Proposal => proposal;
|
||||
|
||||
internal bool IsPlanning => planning != null;
|
||||
|
||||
/// <summary>Test seam: runs on the worker, before planning, with the run's cancellation token.</summary>
|
||||
internal Action<CancellationToken> BeforePlan { get; set; }
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
base.OnLoad(e);
|
||||
// Use the database browser's owner-monitor sizing, without maximizing a modal window.
|
||||
var area = Screen.FromControl(Owner ?? this).WorkingArea;
|
||||
var width = System.Math.Min(area.Width, System.Math.Max(MinimumSize.Width, area.Width * 9 / 10));
|
||||
var height = System.Math.Min(area.Height, System.Math.Max(MinimumSize.Height, area.Height * 9 / 10));
|
||||
Bounds = new Rectangle(area.Left + (area.Width - width) / 2,
|
||||
area.Top + (area.Height - height) / 2, width, height);
|
||||
// Min sizes and the splitter only fit once the container has its real size.
|
||||
split.Panel1MinSize = 200;
|
||||
split.Panel2MinSize = 220;
|
||||
var widest = split.Width - split.Panel2MinSize - split.SplitterWidth;
|
||||
if (widest > split.Panel1MinSize)
|
||||
split.SplitterDistance = System.Math.Clamp(split.Width * 3 / 5, split.Panel1MinSize, widest);
|
||||
}
|
||||
|
||||
protected override void OnShown(EventArgs e)
|
||||
{
|
||||
base.OnShown(e);
|
||||
StartPlanning();
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
if (planning != null)
|
||||
{
|
||||
// Keep the editor disabled until the worker has stopped; then close for real.
|
||||
e.Cancel = true;
|
||||
closeRequested = true;
|
||||
planning.Cancel();
|
||||
statusLabel.Text = "Cancelling…";
|
||||
cancelButton.Enabled = false;
|
||||
}
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
|
||||
private bool TryCheckCanChange(out string reason)
|
||||
{
|
||||
reason = null;
|
||||
if (isOperationBusy?.Invoke() == true || plateView.IsFillInProgress)
|
||||
reason = "Wait for the current nesting or plate action to finish, then replan.";
|
||||
else if (plateView.IsDisposed || !ReferenceEquals(plateView.Plate, activePlate))
|
||||
reason = "The active plate changed. Close this dialog and try again.";
|
||||
return reason == null;
|
||||
}
|
||||
|
||||
private void StartPlanning()
|
||||
{
|
||||
if (planning != null || IsDisposed)
|
||||
return; // One planning run at a time; inputs are disabled while it runs.
|
||||
ClearProposal();
|
||||
if (!TryCheckCanChange(out var reason))
|
||||
{
|
||||
ShowMessage(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
CuttingPlanBatch batch;
|
||||
try
|
||||
{
|
||||
batch = CuttingPlanBatch.Capture(plates, parameters, keepOrderCheckBox.Checked, plateNumbers);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Capture runs inside the dialog's message loop: report a failure, never crash the editor.
|
||||
ShowMessage($"Unable to start planning: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
var source = new CancellationTokenSource();
|
||||
var token = source.Token;
|
||||
planning = source;
|
||||
var run = ++generation;
|
||||
SetPlanning(true);
|
||||
summaryBox.Text = string.Empty;
|
||||
statusLabel.Text = "Planning…";
|
||||
var progress = new PostingProgress(uiContext, value =>
|
||||
{
|
||||
if (run == generation && ReferenceEquals(planning, source) && !IsDisposed)
|
||||
statusLabel.Text = ProgressText(value);
|
||||
});
|
||||
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
PlanningTask = completion.Task;
|
||||
var beforePlan = BeforePlan;
|
||||
Task.Run(() =>
|
||||
{
|
||||
beforePlan?.Invoke(token);
|
||||
return batch.Plan(progress, token);
|
||||
}).ContinueWith(
|
||||
work => uiContext.Post(_ => FinishPlanning(work, source, run, completion), null),
|
||||
CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
// Runs on the dialog's thread once the worker has stopped.
|
||||
private void FinishPlanning(Task<CuttingPlanProposal> work, CancellationTokenSource source, int run,
|
||||
TaskCompletionSource completion)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ReferenceEquals(planning, source))
|
||||
planning = null;
|
||||
source.Dispose();
|
||||
|
||||
if (run != generation || IsDisposed)
|
||||
return;
|
||||
SetPlanning(false);
|
||||
if (closeRequested)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
if (work.IsFaulted)
|
||||
ShowMessage($"Planning failed: {work.Exception.GetBaseException().Message}");
|
||||
else if (work.IsCanceled)
|
||||
ShowMessage("Planning was cancelled. Nothing has changed.");
|
||||
else
|
||||
ShowProposal(work.Result);
|
||||
}
|
||||
catch (Exception ex) when (!IsDisposed)
|
||||
{
|
||||
// A plan that cannot be presented must not leave a stale preview or an enabled Apply.
|
||||
ShowMessage($"Unable to show the plan: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
completion.TrySetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowProposal(CuttingPlanProposal result)
|
||||
{
|
||||
proposal = result;
|
||||
summaryBox.Text = string.Join(Environment.NewLine, result.Describe(unit)
|
||||
.Where(line => !line.TrimStart().StartsWith("- ", StringComparison.Ordinal)
|
||||
&& !line.TrimStart().StartsWith("... and ", StringComparison.Ordinal)));
|
||||
var warningRows = result.DiagnosticRows();
|
||||
populatingWarnings = true;
|
||||
try
|
||||
{
|
||||
warningGrid.Rows.Clear();
|
||||
foreach (var row in warningRows)
|
||||
{
|
||||
var display = warningGrid.Rows.Add(row.PlateNumber, row.PartNumber, row.OtherPartNumber,
|
||||
row.Message);
|
||||
warningGrid.Rows[display].Tag = row;
|
||||
}
|
||||
warningGrid.ClearSelection();
|
||||
}
|
||||
finally
|
||||
{
|
||||
populatingWarnings = false;
|
||||
}
|
||||
acceptWarningsCheckBox.Checked = false;
|
||||
acceptWarningsCheckBox.Visible = result.RequiresWarningAcceptance;
|
||||
statusLabel.Text = result.CanApply ? "Review the plan, then apply it."
|
||||
: result.RequiresWarningAcceptance ? "Unverified plan: review and accept the warnings to apply."
|
||||
: "Nothing can be applied.";
|
||||
applyButton.Enabled = result.CanApply;
|
||||
var index = Array.IndexOf(plates, activePlate);
|
||||
ShowPlate(index);
|
||||
}
|
||||
|
||||
private void ShowPlate(int index)
|
||||
{
|
||||
preview.Visible = false;
|
||||
if (proposal == null || index < 0 || index >= proposal.Plates.Count)
|
||||
return;
|
||||
var plate = proposal.BuildPreview(index);
|
||||
if (plate == null)
|
||||
{
|
||||
previewLabel.Text = $"Plate {plateNumbers[index]}: no current, usable preview. "
|
||||
+ "Part numbers refer to the editor; no location can be certified here.";
|
||||
return;
|
||||
}
|
||||
previewLabel.Text = proposal.Plates[index].IsReady
|
||||
? $"Plate {plateNumbers[index]} — preview, numbered in proposed cutting order:"
|
||||
: $"UNVERIFIED — Plate {plateNumbers[index]} — preview, numbered in proposed cutting order:";
|
||||
preview.Plate = plate;
|
||||
preview.Visible = true;
|
||||
preview.ZoomToFit();
|
||||
}
|
||||
|
||||
private void WarningGrid_SelectionChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (populatingWarnings || proposal == null || warningGrid.SelectedRows.Count == 0)
|
||||
return;
|
||||
// SelectionChanged precedes CurrentCell's update; SelectedRows is the new selection.
|
||||
if (warningGrid.SelectedRows[0].Tag is not CuttingPlanDiagnosticRow row)
|
||||
return;
|
||||
ShowPlate(row.PlateIndex);
|
||||
if (!preview.Visible || row.PartNumber is not int partNumber)
|
||||
{
|
||||
if (preview.Visible)
|
||||
previewLabel.Text += " No part location was provided for this warning.";
|
||||
return;
|
||||
}
|
||||
var plan = proposal.Plates[row.PlateIndex].Result;
|
||||
var matched = plan.ProposedOrder.Select((part, position) => (part, position))
|
||||
.FirstOrDefault(item => item.part.SourceOrdinal == partNumber - 1);
|
||||
if (matched.part == null)
|
||||
{
|
||||
previewLabel.Text += " This source part has no proposed preview location.";
|
||||
return;
|
||||
}
|
||||
var bounds = preview.Plate.Parts[matched.position].BoundingBox;
|
||||
if (row.OtherPartNumber is int otherNumber)
|
||||
{
|
||||
var other = plan.ProposedOrder.Select((part, position) => (part, position))
|
||||
.FirstOrDefault(item => item.part.SourceOrdinal == otherNumber - 1);
|
||||
if (other.part != null)
|
||||
{
|
||||
var second = preview.Plate.Parts[other.position].BoundingBox;
|
||||
var left = System.Math.Min(bounds.Left, second.Left);
|
||||
var bottom = System.Math.Min(bounds.Bottom, second.Bottom);
|
||||
bounds = new OpenNest.Geometry.Box(left, bottom,
|
||||
System.Math.Max(bounds.Right, second.Right) - left,
|
||||
System.Math.Max(bounds.Top, second.Top) - bottom);
|
||||
}
|
||||
}
|
||||
if (double.IsFinite(bounds.X) && double.IsFinite(bounds.Y)
|
||||
&& double.IsFinite(bounds.Length) && double.IsFinite(bounds.Width)
|
||||
&& bounds.Length > 0 && bounds.Width > 0)
|
||||
{
|
||||
var pad = System.Math.Max(0.5, System.Math.Max(bounds.Length, bounds.Width) * 0.12);
|
||||
preview.ZoomToArea(bounds.X - pad, bounds.Y - pad,
|
||||
bounds.Length + 2 * pad, bounds.Width + 2 * pad);
|
||||
}
|
||||
previewLabel.Text += " Showing affected part extent, not an exact warning point.";
|
||||
}
|
||||
|
||||
private void ClearProposal()
|
||||
{
|
||||
proposal = null;
|
||||
populatingWarnings = true;
|
||||
try
|
||||
{
|
||||
warningGrid.Rows.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
populatingWarnings = false;
|
||||
}
|
||||
acceptWarningsCheckBox.Checked = false;
|
||||
acceptWarningsCheckBox.Visible = false;
|
||||
applyButton.Enabled = false;
|
||||
preview.Visible = false;
|
||||
previewLabel.Text = previewText;
|
||||
}
|
||||
|
||||
private void ShowMessage(string message)
|
||||
{
|
||||
ClearProposal();
|
||||
statusLabel.Text = string.Empty;
|
||||
summaryBox.Text = message;
|
||||
}
|
||||
|
||||
private void SetPlanning(bool running)
|
||||
{
|
||||
settingsButton.Enabled = !running;
|
||||
keepOrderCheckBox.Enabled = !running;
|
||||
acceptWarningsCheckBox.Enabled = !running;
|
||||
planButton.Enabled = !running;
|
||||
if (running)
|
||||
applyButton.Enabled = false;
|
||||
}
|
||||
|
||||
private static string ProgressText(CuttingPlanProgress progress)
|
||||
{
|
||||
var plate = progress.PlateCount == 1
|
||||
? $"plate {progress.PlateNumber}"
|
||||
: $"plate {progress.PlateNumber} ({progress.PlateIndex + 1} of {progress.PlateCount})";
|
||||
return progress.Phase switch
|
||||
{
|
||||
CuttingPlanPhase.CheckingOverlap => $"Checking {plate} for overlapping parts…",
|
||||
CuttingPlanPhase.Reordering => $"Planning {plate}: searching for a part order…",
|
||||
_ => $"Planning {plate} with the current part order…",
|
||||
};
|
||||
}
|
||||
|
||||
private void ShowSettings()
|
||||
{
|
||||
var tabs = parameters.TabsEnabled ? "on" : "off";
|
||||
settingsLabel.Text = $"Lead-ins: outside {Describe(parameters.ExternalLeadIn)}, holes "
|
||||
+ $"{Describe(parameters.InternalLeadIn)}, arcs and circles {Describe(parameters.ArcCircleLeadIn)}. "
|
||||
+ $"Tabs {tabs}.";
|
||||
}
|
||||
|
||||
private static string Describe(LeadIn leadIn)
|
||||
{
|
||||
if (leadIn == null || leadIn is NoLeadIn)
|
||||
return "none";
|
||||
var name = leadIn.GetType().Name;
|
||||
if (name.EndsWith("LeadIn", StringComparison.Ordinal))
|
||||
name = name[..^"LeadIn".Length];
|
||||
return Regex.Replace(name, "(?<=[a-z])(?=[A-Z])", " ").ToLowerInvariant();
|
||||
}
|
||||
|
||||
private void SettingsButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new CuttingParametersDialog();
|
||||
dialog.LoadParameters(parameters);
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
parameters = dialog.GetParameters();
|
||||
ShowSettings();
|
||||
StartPlanning();
|
||||
}
|
||||
|
||||
private void KeepOrderCheckBox_CheckedChanged(object sender, EventArgs e) => StartPlanning();
|
||||
|
||||
private void PlanButton_Click(object sender, EventArgs e) => StartPlanning();
|
||||
|
||||
private sealed class PostingProgress(SynchronizationContext context, Action<CuttingPlanProgress> handler)
|
||||
: IProgress<CuttingPlanProgress>
|
||||
{
|
||||
public void Report(CuttingPlanProgress value) => context.Post(_ => handler(value), null);
|
||||
}
|
||||
|
||||
private bool CanApplyProposal => proposal?.CanApply == true
|
||||
|| proposal?.CanApplyWithWarnings == true && acceptWarningsCheckBox.Checked;
|
||||
|
||||
private void AcceptWarningsCheckBox_CheckedChanged(object sender, EventArgs e) =>
|
||||
applyButton.Enabled = planning == null && CanApplyProposal;
|
||||
|
||||
private void ApplyButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (planning != null || !CanApplyProposal)
|
||||
return;
|
||||
if (!TryCheckCanChange(out var reason))
|
||||
{
|
||||
ShowMessage(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
var commit = proposal.Apply(acceptWarningsCheckBox.Checked);
|
||||
CommitResult = commit;
|
||||
switch (commit.Status)
|
||||
{
|
||||
case CuttingCommitStatus.Applied:
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
return;
|
||||
case CuttingCommitStatus.Stale:
|
||||
ShowMessage("The nest changed after planning, so nothing was applied. Replan to continue.");
|
||||
return;
|
||||
default:
|
||||
ShowMessage($"Nothing was applied: {commit.Message ?? commit.Status.ToString()}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1009,31 +1009,6 @@ namespace OpenNest.Forms
|
||||
CalculateCurrentPlateCutTime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the cutting planner for the active plate or every plate. The planner applies its
|
||||
/// plan itself, all or nothing; on success the confirmed settings become the saved defaults.
|
||||
/// </summary>
|
||||
public bool PlanCutting(bool allPlates, Func<bool> isOperationBusy, IWin32Window owner)
|
||||
{
|
||||
if (CuttingInspection != null || PlateView?.Plate == null || Nest == null)
|
||||
return false;
|
||||
|
||||
// The form plans with its own copy; the plate's live settings stay plate state.
|
||||
var parameters = LoadOrDefaultParameters(PlateView.Plate.CuttingParameters);
|
||||
using var form = new CuttingPlanForm(PlateView, Nest, allPlates, parameters, isOperationBusy);
|
||||
if (form.ShowDialog(owner) != DialogResult.OK)
|
||||
return false;
|
||||
|
||||
SaveCuttingParameters(form.ConfirmedParameters);
|
||||
UpdatePlateList();
|
||||
if (form.CommitResult?.RefreshErrors.Count > 0)
|
||||
MessageBox.Show(owner,
|
||||
"The cutting plan was applied, but part of the display did not refresh: "
|
||||
+ form.CommitResult.RefreshErrors[0].Message,
|
||||
"Plan Cutting", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AssignLeadIns_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (PlateView?.Plate == null)
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Actions;
|
||||
using OpenNest.Collections;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Engine;
|
||||
@@ -1925,6 +1926,21 @@ namespace OpenNest.Forms
|
||||
|
||||
private void NestPlanCutting_Click(object sender, EventArgs e) => ShowCuttingPlan(allPlates: true);
|
||||
|
||||
internal bool IsCuttingPlanBusy(PlateView[] views) => nestingInProgress
|
||||
|| Application.OpenForms.OfType<NestProgressForm>().Any()
|
||||
|| views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true);
|
||||
|
||||
// Native tests capture this notice without opening a modal dialog in session zero.
|
||||
internal System.Action<string, string> CuttingPlanNotice { get; set; }
|
||||
|
||||
private void ShowCuttingPlanNotice(string message, string caption)
|
||||
{
|
||||
if (CuttingPlanNotice != null)
|
||||
CuttingPlanNotice(message, caption);
|
||||
else
|
||||
MessageBox.Show(this, message, caption, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private void ShowCuttingPlan(bool allPlates)
|
||||
{
|
||||
if (MdiChildren.OfType<EditNestForm>().Any(form => form.CuttingInspection != null && !ReferenceEquals(form, activeForm)))
|
||||
@@ -1938,12 +1954,9 @@ namespace OpenNest.Forms
|
||||
.Where(form => ReferenceEquals(form.Nest, nest))
|
||||
.Select(form => form.PlateView).ToArray();
|
||||
// A closed progress window can still have a fill awaiting completion/commit.
|
||||
bool IsBusy() => nestingInProgress || Application.OpenForms.OfType<NestProgressForm>().Any()
|
||||
|| views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true);
|
||||
if (IsBusy())
|
||||
if (IsCuttingPlanBusy(views))
|
||||
{
|
||||
MessageBox.Show(this, "Finish or cancel the current nesting or plate action first.",
|
||||
"Plan Cutting", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
ShowCuttingPlanNotice("Finish or cancel the current nesting or plate action first.", "Plan Cutting");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Diagnostics;
|
||||
|
||||
namespace OpenNest.Forms;
|
||||
|
||||
/// <summary>Modeless inspection; the editor scope blocks edits while leaving canvas navigation available.</summary>
|
||||
public sealed class ValidateNestForm : Form
|
||||
{
|
||||
private readonly Nest nest;
|
||||
private readonly ValidationInspectionScope inspection;
|
||||
private readonly Func<Nest, CancellationToken, Task<PostVerificationReport>> analyze;
|
||||
private readonly CancellationTokenSource cancellation = new();
|
||||
private readonly Label summaryLabel;
|
||||
private readonly TextBox reportBox;
|
||||
private readonly DataGridView warningGrid;
|
||||
private readonly EditNestForm editor;
|
||||
private readonly int originalPlate;
|
||||
private bool populating;
|
||||
private bool editorRestored;
|
||||
private readonly (float Scale, PointF Origin) originalViewport;
|
||||
private readonly OverlapDisplayMode originalOverlapDisplay;
|
||||
private readonly Button closeButton;
|
||||
private bool started;
|
||||
private bool analyzing;
|
||||
private bool closeRequested;
|
||||
private bool disposedResources;
|
||||
|
||||
public ValidateNestForm(Nest nest)
|
||||
: this(nest, (value, token) => Task.Run(() => PostVerificationAnalyzer.Analyze(value, token), token))
|
||||
{ }
|
||||
|
||||
public ValidateNestForm(EditNestForm editor)
|
||||
: this(editor, (value, token) => Task.Run(() => PostVerificationAnalyzer.Analyze(value, token), token))
|
||||
{ }
|
||||
|
||||
internal ValidateNestForm(EditNestForm editor, Func<Nest, CancellationToken, Task<PostVerificationReport>> analyze)
|
||||
: this(editor.Nest, analyze)
|
||||
{
|
||||
this.editor = editor;
|
||||
inspection = new ValidationInspectionScope(editor, this);
|
||||
originalPlate = editor.PlateManager.CurrentIndex;
|
||||
originalViewport = editor.PlateView.CaptureViewport();
|
||||
originalOverlapDisplay = editor.PlateView.OverlapDisplay;
|
||||
editor.PlateView.OverlapDisplay = OverlapDisplayMode.Off;
|
||||
StartPosition = FormStartPosition.Manual;
|
||||
MinimumSize = new Size(480, 440);
|
||||
ClientSize = new Size(560, 650);
|
||||
}
|
||||
|
||||
internal ValidateNestForm(Nest nest, Func<Nest, CancellationToken, Task<PostVerificationReport>> analyze)
|
||||
{
|
||||
this.nest = nest ?? throw new ArgumentNullException(nameof(nest));
|
||||
this.analyze = analyze ?? throw new ArgumentNullException(nameof(analyze));
|
||||
Text = "Validate Nest";
|
||||
Name = nameof(ValidateNestForm);
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
ClientSize = new Size(850, 560);
|
||||
MinimumSize = new Size(700, 440);
|
||||
ShowInTaskbar = true;
|
||||
MinimizeBox = true;
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
|
||||
var layout = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Padding = new Padding(12),
|
||||
ColumnCount = 1,
|
||||
RowCount = 4,
|
||||
};
|
||||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
summaryLabel = new Label
|
||||
{
|
||||
Name = "summaryLabel",
|
||||
AutoSize = true,
|
||||
Dock = DockStyle.Fill,
|
||||
Text = "Checking all plates: overlaps, missing lead-ins, and rapid crossings...",
|
||||
Margin = new Padding(3, 3, 3, 10),
|
||||
};
|
||||
var scopeLabel = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Dock = DockStyle.Fill,
|
||||
Text = "Select an issue to locate it in the main plate view. Its highlight pulses; unrelated parts appear gray.\r\n"
|
||||
+ "Minimum spacing, plate boundaries, and final post-processor output are not checked.",
|
||||
Margin = new Padding(3, 3, 3, 10),
|
||||
};
|
||||
reportBox = new TextBox
|
||||
{
|
||||
Name = "reportBox",
|
||||
Dock = DockStyle.Fill,
|
||||
Multiline = true,
|
||||
ReadOnly = true,
|
||||
ScrollBars = ScrollBars.Vertical,
|
||||
WordWrap = true,
|
||||
MaxLength = 0,
|
||||
AccessibleName = "Nest validation findings",
|
||||
Text = "Validation is running...",
|
||||
};
|
||||
warningGrid = new DataGridView
|
||||
{
|
||||
Name = "warningGrid",
|
||||
Dock = DockStyle.Fill,
|
||||
ReadOnly = true,
|
||||
AllowUserToAddRows = false,
|
||||
AllowUserToDeleteRows = false,
|
||||
MultiSelect = false,
|
||||
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||||
AutoGenerateColumns = false,
|
||||
RowHeadersVisible = false,
|
||||
BackgroundColor = SystemColors.Window,
|
||||
AccessibleName = "Nest validation findings",
|
||||
};
|
||||
foreach (var title in new[] { "Plate", "Part", "With", "Issue", "Finding" })
|
||||
warningGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
HeaderText = title,
|
||||
Width = title == "Issue" ? 115 : 48,
|
||||
AutoSizeMode = title == "Finding" ? DataGridViewAutoSizeColumnMode.Fill : DataGridViewAutoSizeColumnMode.None,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable,
|
||||
});
|
||||
warningGrid.CurrentCellChanged += (_, _) => ShowSelectedFinding();
|
||||
warningGrid.CellClick += (_, _) => ShowSelectedFinding();
|
||||
var findings = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1 };
|
||||
findings.RowStyles.Add(new RowStyle(SizeType.Percent, 65));
|
||||
findings.RowStyles.Add(new RowStyle(SizeType.Percent, 35));
|
||||
findings.Controls.Add(warningGrid, 0, 0);
|
||||
findings.Controls.Add(reportBox, 0, 1);
|
||||
closeButton = new Button
|
||||
{
|
||||
Name = "closeButton",
|
||||
Text = "Cancel",
|
||||
AutoSize = true,
|
||||
DialogResult = DialogResult.Cancel,
|
||||
};
|
||||
closeButton.Click += (_, _) => Close();
|
||||
var buttons = new FlowLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
AutoSize = true,
|
||||
FlowDirection = FlowDirection.RightToLeft,
|
||||
};
|
||||
buttons.Controls.Add(closeButton);
|
||||
layout.Controls.Add(summaryLabel, 0, 0);
|
||||
layout.Controls.Add(scopeLabel, 0, 1);
|
||||
layout.Controls.Add(findings, 0, 2);
|
||||
layout.Controls.Add(buttons, 0, 3);
|
||||
Controls.Add(layout);
|
||||
AcceptButton = closeButton;
|
||||
CancelButton = closeButton;
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
base.OnLoad(e);
|
||||
if (editor == null)
|
||||
return;
|
||||
var area = Screen.FromControl(editor).WorkingArea;
|
||||
Bounds = new Rectangle(area.Right - Width, area.Top, Width, System.Math.Min(Height, area.Height));
|
||||
}
|
||||
|
||||
private void ShowSelectedFinding()
|
||||
{
|
||||
if (editorRestored || populating || editor == null || editor.IsDisposed)
|
||||
return;
|
||||
var view = editor.PlateView;
|
||||
view.ClearValidationFinding();
|
||||
if (warningGrid.CurrentRow?.Tag is not PostVerificationFinding finding
|
||||
|| finding.PlateNumber < 1 || finding.PlateNumber > nest.Plates.Count)
|
||||
return;
|
||||
if (editor.PlateManager.CurrentIndex != finding.PlateNumber - 1)
|
||||
editor.PlateManager.LoadAt(finding.PlateNumber - 1);
|
||||
var location = finding.Location;
|
||||
if (location == null && finding.PartNumber is { } part && part > 0 && part <= view.Plate.Parts.Count)
|
||||
location = view.Plate.Parts[part - 1]?.BoundingBox?.Center;
|
||||
if (location is { } point && double.IsFinite(point.X) && double.IsFinite(point.Y))
|
||||
{
|
||||
var extent = finding.Overlap?.Bounds;
|
||||
var size = System.Math.Max(10, System.Math.Max(extent?.Length ?? 0, extent?.Width ?? 0) * 2);
|
||||
if (finding.RapidStart is { } start && finding.RapidEnd is { } end)
|
||||
size = System.Math.Max(size, start.DistanceTo(end) * 1.5);
|
||||
view.ZoomToArea(point.X - size / 2, point.Y - size / 2, size, size);
|
||||
// Keep the marker in the portion of the main canvas left of this floating inspector.
|
||||
var screen = view.RectangleToScreen(view.ClientRectangle);
|
||||
var available = System.Math.Clamp(Left - screen.Left, 0, view.Width);
|
||||
if (available > 100)
|
||||
view.CenterValidationPoint(point, new Point(available / 2, view.Height / 2));
|
||||
}
|
||||
else
|
||||
view.ZoomToFit();
|
||||
view.ShowValidationFinding(finding with { Location = location });
|
||||
ShowFindingDetails(finding);
|
||||
}
|
||||
|
||||
private void ShowFindingDetails(PostVerificationFinding finding)
|
||||
{
|
||||
var hint = finding.Kind switch
|
||||
{
|
||||
PostVerificationKind.Overlap => "Shaded material is the overlap. The ring marks its area centroid, which can lie between separate patches.",
|
||||
PostVerificationKind.RapidCrossing => finding.ContactPoints.Count > 0
|
||||
? "Crosshairs mark contacts with the completed contour. For travel along an edge, they mark the contact span's endpoints. The pulsing dashed orange line shows the rapid."
|
||||
: "The pulsing dashed orange line shows the rapid inside the completed contour; there is no boundary contact point to mark.",
|
||||
PostVerificationKind.MissingLeadIn => "The ring marks the cutting contour start where a lead-in is missing.",
|
||||
_ => "The ring marks the affected part's center when available; no exact problem location is known.",
|
||||
};
|
||||
reportBox.Text = $"Plate {finding.PlateNumber}, part {finding.PartNumber}: {finding.Message}"
|
||||
+ Environment.NewLine + Environment.NewLine + hint;
|
||||
}
|
||||
|
||||
protected override void OnFormClosed(FormClosedEventArgs e)
|
||||
{
|
||||
RestoreEditor();
|
||||
base.OnFormClosed(e);
|
||||
}
|
||||
|
||||
private void RestoreEditor()
|
||||
{
|
||||
if (editorRestored || editor == null || editor.IsDisposed)
|
||||
return;
|
||||
editorRestored = true;
|
||||
inspection?.Dispose();
|
||||
editor.PlateView.ClearValidationFinding();
|
||||
if (editor.PlateManager.CurrentIndex != originalPlate)
|
||||
editor.PlateManager.LoadAt(originalPlate);
|
||||
editor.PlateView.RestoreViewport(originalViewport);
|
||||
editor.PlateView.OverlapDisplay = originalOverlapDisplay;
|
||||
}
|
||||
|
||||
protected override async void OnShown(EventArgs e)
|
||||
{
|
||||
base.OnShown(e);
|
||||
await ValidateAsync();
|
||||
}
|
||||
|
||||
internal async Task ValidateAsync()
|
||||
{
|
||||
if (started)
|
||||
return;
|
||||
started = true;
|
||||
analyzing = true;
|
||||
try
|
||||
{
|
||||
var report = await analyze(nest, cancellation.Token);
|
||||
if (closeRequested || IsDisposed)
|
||||
return;
|
||||
populating = true;
|
||||
foreach (var finding in report.Findings)
|
||||
{
|
||||
var index = warningGrid.Rows.Add(finding.PlateNumber, finding.PartNumber, finding.OtherPartNumber,
|
||||
finding.Kind, finding.Message);
|
||||
warningGrid.Rows[index].Tag = finding;
|
||||
}
|
||||
populating = false;
|
||||
ShowSelectedFinding();
|
||||
reportBox.Text = report.ToDisplayText("Nest validation");
|
||||
if (editor != null && warningGrid.CurrentRow?.Tag is PostVerificationFinding selected)
|
||||
ShowFindingDetails(selected);
|
||||
reportBox.SelectionStart = 0;
|
||||
reportBox.SelectionLength = 0;
|
||||
summaryLabel.Text = report.Findings.Any(finding => finding.Kind == PostVerificationKind.Incomplete)
|
||||
? "Validation incomplete. Review the findings; this nest has not passed all checks."
|
||||
: report.HasWarnings ? "Warnings found. Review the findings below."
|
||||
: "Validation complete. No warnings found by these checks.";
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
closeRequested = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
summaryLabel.Text = "Validation failed. This nest has not passed the checks.";
|
||||
reportBox.Text = "Validation could not finish. Close this window and try again."
|
||||
+ Environment.NewLine + Environment.NewLine + ex.Message;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
analyzing = false;
|
||||
if (!IsDisposed)
|
||||
{
|
||||
closeButton.Text = "Close";
|
||||
if (closeRequested)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
if (analyzing)
|
||||
{
|
||||
closeRequested = true;
|
||||
cancellation.Cancel();
|
||||
closeButton.Enabled = false;
|
||||
summaryLabel.Text = "Canceling validation...";
|
||||
e.Cancel = true;
|
||||
}
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && !disposedResources)
|
||||
{
|
||||
disposedResources = true;
|
||||
RestoreEditor();
|
||||
inspection?.Dispose();
|
||||
cancellation.Cancel();
|
||||
cancellation.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,6 @@ internal sealed class ValidationInspectionScope : IDisposable
|
||||
private Form closingHost;
|
||||
private readonly bool previousInspection;
|
||||
|
||||
public ValidationInspectionScope(EditNestForm editor, Form inspector)
|
||||
: this(editor, inspector, null, inspector.Close)
|
||||
{
|
||||
}
|
||||
|
||||
public ValidationInspectionScope(EditNestForm editor, Control inspector, Control header, Action close)
|
||||
{
|
||||
view = editor.PlateView;
|
||||
|
||||
@@ -361,6 +361,8 @@ unlocking inspection. Planning is not live-nest validation or posting consent. T
|
||||
panel's **Validate Nest (live programs)** mode separately checks current installed
|
||||
programs across all plates and navigates typed findings on the live canvas; it
|
||||
never certifies an unapplied proposal. Posting still initiates its own fresh review.
|
||||
The former standalone planning dialog is retired; only the docked Plan/Validate
|
||||
panel is wired in the editor. Non-editor callers still use the shared analyzer.
|
||||
|
||||
- The panel starts from the plate's cutting settings (or the last-used settings) and plans
|
||||
at once. `Cutting Settings...` edits them and `Keep the current part order` fixes the
|
||||
|
||||
@@ -42,7 +42,9 @@ cancellation and waits for the worker to stop before restoring editing controls.
|
||||
Switching to Plan mode also waits for any validation worker; it does not promote
|
||||
a live validation report into a plan check. Closing the editor or main window
|
||||
joins the worker before continuing the close. Posting while inspection is open
|
||||
is refused; close the panel and use the fresh posting review.
|
||||
is refused; close the panel and use the fresh posting review. The former floating
|
||||
validator window is retired; non-editor checks remain available through the
|
||||
shared `PostVerificationAnalyzer` without an editor UI dependency.
|
||||
|
||||
## Posting workflow
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ removing or renaming required coverage requires an intentional manifest change.
|
||||
The whole suites still run, not just this manifest's subset.
|
||||
|
||||
The areas are BOM import, Nest Info material preservation, saved-nest browsing,
|
||||
nesting/commit policy, PDF export, the cutting dialog, automatic cutoffs,
|
||||
nesting/commit policy, PDF export, docked cutting planning and validation, automatic cutoffs,
|
||||
overlap/posting consent, UI lifetime, import/export failures, and MCP processes.
|
||||
The manifest is public test coverage only. Private tracker IDs, plans, customer
|
||||
data, and acceptance notes stay out of the repository and CI artifacts.
|
||||
|
||||
@@ -353,7 +353,7 @@
|
||||
},
|
||||
{
|
||||
"id": "cutting-planner",
|
||||
"title": "Current/all-plate cutting dialog",
|
||||
"title": "Docked cutting planning and live validation",
|
||||
"required_tests": [
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
@@ -372,42 +372,58 @@
|
||||
},
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingPlanFormTests.AllPlates_SkipsEmptyPlatesAndRefusesTheBatchWhenOnePlateIsBlocked",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingInspectionPanelTests.AllPlatesNavigatesReadyAndBlockedWithoutPartialApply",
|
||||
"minimum_cases": 1
|
||||
},
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingPlanFormTests.ApplyAfterALiveEdit_ChangesNothingAndReplanRecovers",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingInspectionPanelTests.StaleApplyClearsPreviewAndAcceptanceWithoutChangingPrograms",
|
||||
"minimum_cases": 1
|
||||
},
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingPlanFormTests.BlockedPlate_KeepsApplyDisabledAndShowsTheCurrentParts",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingInspectionPanelTests.WarningAcceptanceResetsOnReplanAndSettingsOrderChange",
|
||||
"minimum_cases": 1
|
||||
},
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingPlanFormTests.BusyEditor_DoesNotStartPlanningUntilItIsFree",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingInspectionPanelTests.CloseDuringWorkerRetainsPanelAndInspectionLockUntilCompletion",
|
||||
"minimum_cases": 1
|
||||
},
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingPlanFormTests.ClosingWhilePlanning_CancelsAndWaitsForTheWorker",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingInspectionPanelTests.ConstructorKeepsItsOwnCopyOfSettings",
|
||||
"minimum_cases": 1
|
||||
},
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingPlanFormTests.Constructor_PlansWithItsOwnCopyOfTheSettings",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingInspectionPanelTests.BusyMenuRefusesPlanningAndFreeMenuOpensDockedPanel",
|
||||
"minimum_cases": 1
|
||||
},
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingPlanFormTests.PlanThenApply_InstallsThePlanAndRedrawsTheEditorInPlateOrder",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingInspectionPanelTests.ReadyApplyInstallsExactPreviewAndRefreshesLiveView",
|
||||
"minimum_cases": 1
|
||||
},
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingValidationPanelTests.ValidateMenuUsesSharedPanelAndRealAllPlateFindingsRestoreOriginalView",
|
||||
"minimum_cases": 1
|
||||
},
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingValidationPanelTests.ExternalCloseJoinsValidationBeforeReleasingMdiGuard",
|
||||
"minimum_cases": 3
|
||||
},
|
||||
{
|
||||
"project": "OpenNest.WinForms.Tests",
|
||||
"method": "OpenNest.WinForms.Tests.Forms.CuttingValidationPanelTests.ModeSwitchJoinsBothWorkersAndDiscardsOldResultsWithoutUnlocking",
|
||||
"minimum_cases": 1
|
||||
}
|
||||
],
|
||||
"remaining_acceptance": [
|
||||
"Selected-part side-panel migration and legacy automatic-path retirement are separate unfinished work.",
|
||||
"Native operator check of docked panel readability, multi-plate navigation, Escape/X/Cancel, and final post review; automated form tests are not visual acceptance.",
|
||||
"Selected-part Auto Assign migration and legacy automatic-algorithm retirement are separate work.",
|
||||
"Actual job and NC/controller safety are not certified."
|
||||
]
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user