mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-15 13:31:41 -04:00
feat(cutting-ui): plan and apply all plates from docked panel
This commit is contained in:
1 parent
6ed7ab7392
commit
943b63937d
7 files changed
+500
-61
No files matched your search
@@ -61,14 +61,21 @@ public sealed class CuttingProposalInspection
|
||||
}
|
||||
|
||||
/// <summary>Recheck exact freshness before displaying, including every subsequent paint.</summary>
|
||||
public Plate RefreshPreview()
|
||||
private int previewIndex = -1;
|
||||
|
||||
public Plate RefreshPreview(int plateIndex = 0)
|
||||
{
|
||||
if (Proposal == null || CloseRequested)
|
||||
if (plateIndex != previewIndex)
|
||||
{
|
||||
Preview = null;
|
||||
previewIndex = plateIndex;
|
||||
}
|
||||
if (Proposal == null || CloseRequested || plateIndex < 0 || plateIndex >= Proposal.Plates.Count)
|
||||
return Preview = null;
|
||||
// BuildPreview is the authority; never clone a rejected program for presentation.
|
||||
if (Proposal.Plates[0].Result.Snapshot?.PlateState?.IsCurrent() != true)
|
||||
if (Proposal.Plates[plateIndex].Result.Snapshot?.PlateState?.IsCurrent() != true)
|
||||
return Preview = null;
|
||||
return Preview ??= Proposal.BuildPreview(0);
|
||||
return Preview ??= Proposal.BuildPreview(plateIndex);
|
||||
}
|
||||
|
||||
/// <summary>Zero-based proposed position for a one-based source diagnostic number.</summary>
|
||||
|
||||
@@ -139,6 +139,26 @@ public class CuttingProposalInspectionTests
|
||||
Assert.True(inspection.RequestClose());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BatchPreviewNavigatesToSecondPlateAndRejectsItsStaleState()
|
||||
{
|
||||
var first = Plate();
|
||||
var second = Plate();
|
||||
second.Parts[0].Offset(20, 0);
|
||||
var inspection = new CuttingProposalInspection();
|
||||
var work = inspection.BeginPlanning(CuttingPlanBatch.Capture([first, second], ExplicitContourTests.Parameters(), false));
|
||||
await work;
|
||||
inspection.CompletePlanning(work);
|
||||
var preview = inspection.RefreshPreview(1);
|
||||
Assert.NotNull(preview);
|
||||
Assert.Equal(second.Parts[1].Location, preview.Parts[0].Location);
|
||||
Assert.NotSame(preview, inspection.RefreshPreview(0));
|
||||
second.Quantity++;
|
||||
Assert.Null(inspection.RefreshPreview(1));
|
||||
Assert.NotNull(inspection.RefreshPreview(0));
|
||||
inspection.RequestClose();
|
||||
}
|
||||
|
||||
private static CuttingPlanBatch Capture(Plate plate) =>
|
||||
CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false);
|
||||
|
||||
|
||||
@@ -153,6 +153,174 @@ public class CuttingInspectionPanelTests
|
||||
}
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void AllPlatesNavigatesReadyAndBlockedWithoutPartialApply() => RunSta(() =>
|
||||
{
|
||||
using var editor = Editor();
|
||||
var second = editor.Nest.Plates[^1]; // Fill the managed empty sentinel; a new one is appended.
|
||||
second.Size = new OpenNest.Geometry.Size(100, 100);
|
||||
var locked = Square("locked", 1);
|
||||
var crossing = (OpenNest.CNC.Program)locked.Program.Clone();
|
||||
crossing.MoveTo(5, 5);
|
||||
Assert.True(locked.RestoreLeadInProgram(crossing, true));
|
||||
second.Parts.Add(locked);
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(editor.Nest.Plates.Count - 1); // Different empty sentinel.
|
||||
var original = editor.PlateView.Plate;
|
||||
OpenAll(editor);
|
||||
var panel = editor.CuttingInspection!;
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
Assert.Equal(2, panel.Proposal.Plates.Count);
|
||||
Assert.Equal(ScrollBars.Vertical, Field<TextBox>(panel, "resultLabel").ScrollBars);
|
||||
Assert.False(Field<Button>(panel, "applyButton").Enabled);
|
||||
Assert.True(editor.PlateView.HasProposalDisplay);
|
||||
Field<Button>(panel, "nextButton").PerformClick();
|
||||
Assert.False(editor.PlateView.HasProposalDisplay);
|
||||
Assert.Contains("no current preview", Field<Label>(panel, "previewLabel").Text);
|
||||
var grid = Field<DataGridView>(panel, "warningGrid");
|
||||
Assert.Equal(panel.Proposal.DiagnosticRows().Count, grid.Rows.Count);
|
||||
var row = grid.Rows.Cast<DataGridViewRow>().First(r => r.Tag is OpenNest.Engine.CuttingPlanning.CuttingPlanDiagnosticRow d && d.PlateIndex == 1);
|
||||
grid.ClearSelection();
|
||||
row.Selected = true; // SelectionChanged precedes CurrentCell.
|
||||
Assert.Contains("Plate 2", Field<Label>(panel, "previewLabel").Text);
|
||||
Assert.All(editor.Nest.Plates[0].Parts, p => Assert.False(p.HasManualLeadIns));
|
||||
panel.RequestClose();
|
||||
Assert.Same(original, editor.PlateView.Plate);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void WarningAcceptanceResetsOnReplanAndSettingsOrderChange() => RunSta(() =>
|
||||
{
|
||||
using var editor = Editor();
|
||||
var plate = editor.Nest.Plates[0];
|
||||
plate.Parts.Clear();
|
||||
var program = Square("fixture", 1).Program;
|
||||
program.MoveTo(2, 2); program.LineTo(4, 2); program.LineTo(4, 4);
|
||||
program.LineTo(2, 4); program.LineTo(2, 2);
|
||||
program.MoveTo(4, 2); program.LineTo(6, 2); program.LineTo(6, 4);
|
||||
program.LineTo(4, 4); program.LineTo(4, 2);
|
||||
plate.Parts.Add(new Part(new Drawing("touching contours", program), new Vector(1, 1)));
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
editor.OpenCuttingInspection();
|
||||
var panel = editor.CuttingInspection!;
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
var accept = Field<CheckBox>(panel, "acceptWarningsCheckBox");
|
||||
Assert.True(accept.Visible);
|
||||
Assert.False(accept.Checked);
|
||||
Assert.Contains("UNVERIFIED", Field<Label>(panel, "previewLabel").Text);
|
||||
accept.Checked = true;
|
||||
Assert.True(Field<Button>(panel, "applyButton").Enabled);
|
||||
panel.StartPlanning();
|
||||
Assert.False(accept.Checked);
|
||||
Assert.Empty(Field<DataGridView>(panel, "warningGrid").Rows.Cast<DataGridViewRow>());
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
accept.Checked = true;
|
||||
Field<CheckBox>(panel, "keepOrderCheckBox").Checked = true;
|
||||
Assert.False(accept.Checked);
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
Assert.False(Field<Button>(panel, "applyButton").Enabled);
|
||||
panel.RequestClose();
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void StaleApplyClearsPreviewAndAcceptanceWithoutChangingPrograms() => RunSta(() =>
|
||||
{
|
||||
using var editor = Editor();
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
editor.OpenCuttingInspection();
|
||||
var panel = editor.CuttingInspection!;
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
var plate = editor.PlateView.Plate;
|
||||
var programs = plate.Parts.Select(p => p.Program).ToArray();
|
||||
plate.Parts[0].Offset(0, 1);
|
||||
Field<Button>(panel, "applyButton").PerformClick();
|
||||
Assert.False(editor.PlateView.HasProposalDisplay);
|
||||
Assert.False(Field<Button>(panel, "applyButton").Enabled);
|
||||
Assert.False(Field<CheckBox>(panel, "acceptWarningsCheckBox").Checked);
|
||||
Assert.Contains("Replan", Field<TextBox>(panel, "resultLabel").Text);
|
||||
Assert.Equal(programs, plate.Parts.Select(p => p.Program));
|
||||
Assert.All(plate.Parts, p => Assert.False(p.HasManualLeadIns));
|
||||
panel.RequestClose();
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void ReadyApplyInstallsExactPreviewAndRefreshesLiveView() => RunSta(() =>
|
||||
{
|
||||
using var editor = Editor();
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
var plate = editor.PlateView.Plate;
|
||||
var original = plate.Parts.ToArray();
|
||||
editor.OpenCuttingInspection();
|
||||
var panel = editor.CuttingInspection!;
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
var proposed = panel.Proposal.Plates[0].Result.ProposedOrder
|
||||
.Select(part => original[part.SourceOrdinal]).ToArray();
|
||||
Assert.True(Field<Button>(panel, "applyButton").Enabled);
|
||||
Field<Button>(panel, "applyButton").PerformClick();
|
||||
Assert.Null(editor.CuttingInspection);
|
||||
Assert.False(editor.PlateView.InspectionOnly);
|
||||
Assert.False(editor.PlateView.HasProposalDisplay);
|
||||
Assert.Equal(proposed, plate.Parts);
|
||||
Assert.All(plate.Parts, part => Assert.True(part.HasManualLeadIns));
|
||||
Assert.NotNull(plate.CuttingParameters);
|
||||
Assert.Equal(plate.Parts, editor.PlateView.LayoutParts.Select(layout => layout.BasePart));
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void PresentationFailureCannotLeaveApplyOrWarningAcceptanceEnabled() => RunSta(() =>
|
||||
{
|
||||
using var editor = Editor();
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
editor.OpenCuttingInspection();
|
||||
var panel = editor.CuttingInspection!;
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
Assert.True(Field<Button>(panel, "applyButton").Enabled);
|
||||
var original = editor.PlateView.Plate.Parts.ToArray();
|
||||
panel.BeforePresent = () => throw new InvalidOperationException("synthetic presentation failure");
|
||||
panel.StartPlanning();
|
||||
PumpUntil(() => !panel.IsPlanning);
|
||||
Assert.False(Field<Button>(panel, "applyButton").Enabled);
|
||||
Assert.False(Field<CheckBox>(panel, "acceptWarningsCheckBox").Checked);
|
||||
Assert.False(editor.PlateView.HasProposalDisplay);
|
||||
Assert.Contains("Nothing can be applied", Field<TextBox>(panel, "resultLabel").Text);
|
||||
Assert.Equal(original, editor.PlateView.Plate.Parts);
|
||||
panel.RequestClose();
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void CommittedPlanStillClosesWhenSavingDefaultsFails() => RunSta(() =>
|
||||
{
|
||||
using var editor = Editor();
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
editor.OpenCuttingInspection();
|
||||
var panel = editor.CuttingInspection!;
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
Exception? error = null;
|
||||
panel.BeforePostApply = () => throw new InvalidOperationException("synthetic defaults failure");
|
||||
panel.OnPostApplyError = ex => error = ex;
|
||||
Field<Button>(panel, "applyButton").PerformClick();
|
||||
Assert.Equal(OpenNest.CNC.CuttingPlanning.CuttingCommitStatus.Applied, panel.CommitResult.Status);
|
||||
Assert.IsType<InvalidOperationException>(error);
|
||||
Assert.Null(editor.CuttingInspection);
|
||||
Assert.False(editor.PlateView.InspectionOnly);
|
||||
Assert.False(editor.PlateView.HasProposalDisplay);
|
||||
Assert.All(editor.PlateView.Plate.Parts, part => Assert.True(part.HasManualLeadIns));
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
private static void OpenAll(EditNestForm editor)
|
||||
{
|
||||
var method = typeof(EditNestForm).GetMethod("OpenCuttingInspection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
|
||||
method.Invoke(editor, method.GetParameters().Length == 0 ? [] : [true]);
|
||||
}
|
||||
|
||||
private static EditNestForm Editor()
|
||||
{
|
||||
var nest = new Nest();
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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.Diagnostics;
|
||||
@@ -10,46 +13,115 @@ using OpenNest.Engine.CuttingPlanning;
|
||||
|
||||
namespace OpenNest.Forms;
|
||||
|
||||
/// <summary>One-plate, read-only main-canvas cutting proposal inspection.</summary>
|
||||
/// <summary>Owner-thread planning and atomic Apply beside the read-only main canvas.</summary>
|
||||
internal sealed class CuttingInspectionPanel : UserControl
|
||||
{
|
||||
private readonly EditNestForm editor;
|
||||
private readonly PlateView view;
|
||||
private readonly Plate plate;
|
||||
private readonly CuttingParameters parameters;
|
||||
private CuttingParameters parameters;
|
||||
private readonly Plate[] plates;
|
||||
private readonly int[] plateNumbers;
|
||||
private int selectedPlate;
|
||||
private Plate preview;
|
||||
private bool populatingWarnings;
|
||||
private bool invalidated;
|
||||
private readonly SynchronizationContext context;
|
||||
private readonly (float Scale, PointF Origin) viewport;
|
||||
private readonly OverlapDisplayMode overlapDisplay;
|
||||
private readonly CuttingProposalInspection inspection = new();
|
||||
private readonly Button planButton = new() { Text = "Plan", AutoSize = true };
|
||||
private readonly Button planButton = new() { Text = "Replan", AutoSize = true };
|
||||
private readonly Button cancelButton = new() { Text = "Cancel / Close", AutoSize = true };
|
||||
private readonly Label resultLabel = new() { AutoSize = false, Dock = DockStyle.Fill };
|
||||
private readonly TextBox resultLabel = new()
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Multiline = true,
|
||||
ReadOnly = true,
|
||||
ScrollBars = ScrollBars.Vertical,
|
||||
WordWrap = true,
|
||||
MaxLength = int.MaxValue,
|
||||
AccessibleName = "Cutting plan outcome and remedies"
|
||||
};
|
||||
private readonly Button settingsButton = new() { Text = "Cutting Settings…", AutoSize = true };
|
||||
private readonly Button applyButton = new() { Text = "Apply", AutoSize = true, Enabled = false };
|
||||
private readonly Button previousButton = new() { Text = "Previous", AutoSize = true };
|
||||
private readonly Button nextButton = new() { Text = "Next", AutoSize = true };
|
||||
private readonly CheckBox keepOrderCheckBox = new() { Text = "Keep current part order", AutoSize = true };
|
||||
private readonly CheckBox acceptWarningsCheckBox = new() { Text = "Accept UNVERIFIED plan warnings (not posting consent)", AutoSize = true, Visible = false };
|
||||
private readonly Label settingsLabel = new() { AutoSize = true, MaximumSize = new Size(440, 0) };
|
||||
private readonly Label previewLabel = new() { AutoSize = true, MaximumSize = new Size(440, 0) };
|
||||
private readonly DataGridView warningGrid = new()
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ReadOnly = true,
|
||||
AllowUserToAddRows = false,
|
||||
AllowUserToDeleteRows = false,
|
||||
MultiSelect = false,
|
||||
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||||
RowHeadersVisible = false,
|
||||
AutoGenerateColumns = false
|
||||
};
|
||||
private ValidationInspectionScope scope;
|
||||
private bool closed;
|
||||
private int generation;
|
||||
|
||||
internal CuttingInspectionPanel(EditNestForm editor, CuttingParameters parameters)
|
||||
internal CuttingInspectionPanel(EditNestForm editor, CuttingParameters parameters, bool allPlates = false)
|
||||
{
|
||||
this.editor = editor;
|
||||
view = editor.PlateView;
|
||||
plate = view.Plate;
|
||||
var all = editor.Nest.Plates.ToList();
|
||||
plates = allPlates ? all.Where(p => p.Parts.Count > 0).ToArray() : [plate];
|
||||
if (plates.Length == 0)
|
||||
plates = [plate];
|
||||
plateNumbers = plates.Select(p => all.IndexOf(p) + 1).ToArray();
|
||||
selectedPlate = System.Math.Max(0, Array.IndexOf(plates, plate));
|
||||
this.parameters = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(parameters));
|
||||
context = SynchronizationContext.Current ?? new WindowsFormsSynchronizationContext();
|
||||
viewport = view.CaptureViewport();
|
||||
overlapDisplay = view.OverlapDisplay;
|
||||
var buttons = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true };
|
||||
buttons.Controls.AddRange([planButton, cancelButton]);
|
||||
Controls.Add(resultLabel);
|
||||
Controls.Add(buttons);
|
||||
var header = new FlowLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, FlowDirection = FlowDirection.TopDown, WrapContents = false };
|
||||
var buttons = new FlowLayoutPanel { AutoSize = true };
|
||||
buttons.Controls.AddRange([settingsButton, planButton, applyButton, cancelButton]);
|
||||
var navigation = new FlowLayoutPanel { AutoSize = true };
|
||||
navigation.Controls.AddRange([previousButton, nextButton]);
|
||||
header.Controls.AddRange([settingsLabel, keepOrderCheckBox, buttons, acceptWarningsCheckBox, navigation, previewLabel]);
|
||||
var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3 };
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 30));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 70));
|
||||
layout.Controls.Add(header, 0, 0);
|
||||
layout.Controls.Add(resultLabel, 0, 1);
|
||||
layout.Controls.Add(warningGrid, 0, 2);
|
||||
Controls.Add(layout);
|
||||
foreach (var name in new[] { "Plate", "Part", "With", "Warning / finding" })
|
||||
warningGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
HeaderText = name,
|
||||
Width = 48,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable,
|
||||
AutoSizeMode = name == "Warning / finding" ? DataGridViewAutoSizeColumnMode.Fill : DataGridViewAutoSizeColumnMode.None
|
||||
});
|
||||
Padding = new Padding(10);
|
||||
planButton.Click += (_, _) => StartPlanning();
|
||||
cancelButton.Click += (_, _) => RequestClose();
|
||||
resultLabel.Text = "Read-only proposal inspection. No live changes; Apply is not available in this view.";
|
||||
settingsButton.Click += (_, _) => EditSettings();
|
||||
keepOrderCheckBox.CheckedChanged += (_, _) => StartPlanning();
|
||||
acceptWarningsCheckBox.CheckedChanged += (_, _) => UpdateApply();
|
||||
applyButton.Click += (_, _) => Apply();
|
||||
previousButton.Click += (_, _) => ShowPlate(selectedPlate - 1);
|
||||
nextButton.Click += (_, _) => ShowPlate(selectedPlate + 1);
|
||||
warningGrid.SelectionChanged += (_, _) => NavigateFinding();
|
||||
ShowSettings();
|
||||
resultLabel.Text = "Read-only proposal inspection. Nothing live changes before Apply.";
|
||||
}
|
||||
|
||||
internal bool IsPlanning => inspection.IsRunning;
|
||||
internal Task PlanningTask { get; private set; } = Task.CompletedTask;
|
||||
internal Action<CancellationToken> BeforePlan { get; set; }
|
||||
internal Action BeforePresent { get; set; }
|
||||
internal Action BeforePostApply { get; set; }
|
||||
internal Action<Exception> OnPostApplyError { get; set; }
|
||||
internal CuttingPlanProposal Proposal => inspection.Proposal;
|
||||
|
||||
internal void Open()
|
||||
@@ -65,14 +137,13 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
{
|
||||
if (closed || inspection.IsRunning || inspection.CloseRequested)
|
||||
return;
|
||||
view.ClearProposalDisplay();
|
||||
planButton.Enabled = false;
|
||||
ClearResult();
|
||||
SetPlanning(true);
|
||||
resultLabel.Text = "Planning… Read-only; the live plate is unchanged.";
|
||||
try
|
||||
{
|
||||
// Capture on the owner thread; no worker recapture of the live plate or settings.
|
||||
var batch = CuttingPlanBatch.Capture([plate], parameters, false,
|
||||
[editor.Nest.Plates.IndexOf(plate) + 1]);
|
||||
var batch = CuttingPlanBatch.Capture(plates, parameters, keepOrderCheckBox.Checked, plateNumbers);
|
||||
var run = ++generation;
|
||||
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
PlanningTask = completion.Task;
|
||||
@@ -88,7 +159,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultLabel.Text = $"Unable to start planning: {ex.Message}. No current preview.";
|
||||
planButton.Enabled = true;
|
||||
SetPlanning(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +175,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
}
|
||||
if (closed || run != generation)
|
||||
return;
|
||||
planButton.Enabled = true;
|
||||
SetPlanning(false);
|
||||
if (work.IsFaulted)
|
||||
{
|
||||
resultLabel.Text = $"Planning failed: {work.Exception.GetBaseException().Message}. No current preview.";
|
||||
@@ -115,23 +186,37 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
resultLabel.Text = "Cancelled. No current preview; nothing changed.";
|
||||
return;
|
||||
}
|
||||
resultLabel.Text = string.Join(Environment.NewLine, work.Result.Describe(UnitsHelper.GetShortString(editor.Nest.Units)));
|
||||
var preview = inspection.RefreshPreview();
|
||||
if (preview == null)
|
||||
invalidated = false;
|
||||
resultLabel.Text = string.Join(Environment.NewLine, work.Result.Describe(UnitsHelper.GetShortString(editor.Nest.Units))
|
||||
.Where(line => !line.TrimStart().StartsWith("- ", StringComparison.Ordinal)
|
||||
&& !line.TrimStart().StartsWith("... and ", StringComparison.Ordinal)));
|
||||
populatingWarnings = true;
|
||||
try
|
||||
{
|
||||
resultLabel.Text += "\nNo current preview: the plate is stale, blocked or unsupported. Live plate shown.";
|
||||
return;
|
||||
foreach (var row in work.Result.DiagnosticRows())
|
||||
{
|
||||
var index = warningGrid.Rows.Add(row.PlateNumber, row.PartNumber, row.OtherPartNumber, row.Message);
|
||||
warningGrid.Rows[index].Tag = row;
|
||||
}
|
||||
warningGrid.ClearSelection();
|
||||
}
|
||||
resultLabel.Text = (work.Result.Plates[0].IsReady ? "PROPOSAL" : "UNVERIFIED PROPOSAL")
|
||||
+ " — numbered in proposed cutting order; read-only.\n" + resultLabel.Text;
|
||||
view.ShowProposalDisplay(preview, IsPreviewCurrent);
|
||||
view.ZoomToFit();
|
||||
finally { populatingWarnings = false; }
|
||||
acceptWarningsCheckBox.Visible = Proposal.RequiresWarningAcceptance;
|
||||
UpdateApply();
|
||||
BeforePresent?.Invoke();
|
||||
ShowPlate(selectedPlate);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
invalidated = true;
|
||||
preview = null;
|
||||
view.ClearProposalDisplay();
|
||||
resultLabel.Text = $"Unable to show the plan: {ex.Message}. No current preview.";
|
||||
planButton.Enabled = true;
|
||||
acceptWarningsCheckBox.Checked = false;
|
||||
acceptWarningsCheckBox.Visible = false;
|
||||
applyButton.Enabled = false;
|
||||
previewLabel.Text = "No current preview. Replan to continue.";
|
||||
resultLabel.Text = $"Unable to show the plan: {ex.Message}. Nothing can be applied until replanning.";
|
||||
SetPlanning(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -139,20 +224,165 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearResult()
|
||||
{
|
||||
invalidated = true;
|
||||
preview = null;
|
||||
view.ClearProposalDisplay();
|
||||
view.ClearValidationFinding();
|
||||
acceptWarningsCheckBox.Checked = false;
|
||||
acceptWarningsCheckBox.Visible = false;
|
||||
applyButton.Enabled = false;
|
||||
previewLabel.Text = "No current preview.";
|
||||
populatingWarnings = true;
|
||||
try { warningGrid.Rows.Clear(); }
|
||||
finally { populatingWarnings = false; }
|
||||
}
|
||||
|
||||
private void SetPlanning(bool running)
|
||||
{
|
||||
settingsButton.Enabled = keepOrderCheckBox.Enabled = planButton.Enabled = !running;
|
||||
previousButton.Enabled = !running && selectedPlate > 0;
|
||||
nextButton.Enabled = !running && selectedPlate < plates.Length - 1;
|
||||
acceptWarningsCheckBox.Enabled = !running;
|
||||
if (running) applyButton.Enabled = false;
|
||||
}
|
||||
|
||||
private void UpdateApply() => applyButton.Enabled = !invalidated && !IsPlanning
|
||||
&& (Proposal?.CanApply == true || Proposal?.CanApplyWithWarnings == true && acceptWarningsCheckBox.Checked);
|
||||
|
||||
private bool IsPreviewCurrent()
|
||||
{
|
||||
if (!closed && ReferenceEquals(view.Plate, plate) && inspection.RefreshPreview() != null)
|
||||
if (!closed && !invalidated && ReferenceEquals(view.Plate, plate)
|
||||
&& inspection.RefreshPreview(selectedPlate) != null)
|
||||
return true;
|
||||
resultLabel.Text = "No current preview: the plate changed. Replan to inspect it. Nothing applied.";
|
||||
invalidated = true;
|
||||
preview = null;
|
||||
acceptWarningsCheckBox.Checked = false;
|
||||
applyButton.Enabled = false;
|
||||
resultLabel.Text = previewLabel.Text = "No current preview: the plate changed. Replan to inspect it. Nothing applied.";
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ShowPlate(int index)
|
||||
{
|
||||
if (IsPlanning || invalidated || Proposal == null || index < 0 || index >= Proposal.Plates.Count)
|
||||
return;
|
||||
selectedPlate = index;
|
||||
view.ClearProposalDisplay();
|
||||
view.ClearValidationFinding();
|
||||
preview = inspection.RefreshPreview(index); // Only the guarded authority may clone programs.
|
||||
SetPlanning(false);
|
||||
if (preview == null)
|
||||
{
|
||||
previewLabel.Text = $"Plate {plateNumbers[index]}: no current preview — stale, blocked or unsupported. Live plate shown; source numbers are not canvas locations.";
|
||||
return;
|
||||
}
|
||||
previewLabel.Text = (Proposal.Plates[index].IsReady ? "PROPOSAL" : "UNVERIFIED PROPOSAL")
|
||||
+ $" — Plate {plateNumbers[index]} ({index + 1} of {plates.Length}), numbered in proposed cutting order; read-only.";
|
||||
view.ShowProposalDisplay(preview, IsPreviewCurrent);
|
||||
view.ZoomToFit();
|
||||
}
|
||||
|
||||
private void NavigateFinding()
|
||||
{
|
||||
if (populatingWarnings || invalidated || warningGrid.SelectedRows.Count == 0
|
||||
|| warningGrid.SelectedRows[0].Tag is not CuttingPlanDiagnosticRow row)
|
||||
return;
|
||||
ShowPlate(row.PlateIndex);
|
||||
if (preview == null || row.PartNumber is not int source)
|
||||
return;
|
||||
var position = CuttingProposalInspection.ProposedPosition(Proposal, row.PlateIndex, source);
|
||||
if (position == null)
|
||||
return;
|
||||
var bounds = preview.Parts[position.Value].BoundingBox;
|
||||
if (row.OtherPartNumber is int otherSource
|
||||
&& CuttingProposalInspection.ProposedPosition(Proposal, row.PlateIndex, otherSource) is int otherPosition)
|
||||
{
|
||||
var second = preview.Parts[otherPosition].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);
|
||||
view.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.";
|
||||
}
|
||||
|
||||
internal CuttingCommitResult CommitResult { get; private set; }
|
||||
|
||||
private void Apply()
|
||||
{
|
||||
if (!applyButton.Enabled || IsPlanning || invalidated || Proposal == null)
|
||||
return;
|
||||
// This handler runs on the owner thread; Apply independently rechecks all captured plates.
|
||||
CommitResult = Proposal.Apply(acceptWarningsCheckBox.Checked);
|
||||
if (CommitResult.Status == CuttingCommitStatus.Applied)
|
||||
{
|
||||
Exception postApplyError = null;
|
||||
try
|
||||
{
|
||||
BeforePostApply?.Invoke();
|
||||
editor.CompleteCuttingApply(parameters, CommitResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
postApplyError = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RequestClose(); // The commit already succeeded; never strand the inspection lock.
|
||||
}
|
||||
if (postApplyError != null)
|
||||
{
|
||||
if (OnPostApplyError != null)
|
||||
OnPostApplyError(postApplyError);
|
||||
else
|
||||
MessageBox.Show(editor, "The cutting plan was applied, but settings or the display did not refresh: "
|
||||
+ postApplyError.Message, "Plan Cutting", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ClearResult();
|
||||
resultLabel.Text = $"Nothing applied: {CommitResult.Message ?? CommitResult.Status.ToString()}. Replan to continue.";
|
||||
}
|
||||
|
||||
private void EditSettings()
|
||||
{
|
||||
if (IsPlanning || closed) return;
|
||||
using var dialog = new CuttingParametersDialog();
|
||||
dialog.LoadParameters(parameters);
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK) return;
|
||||
parameters = dialog.GetParameters();
|
||||
ShowSettings();
|
||||
StartPlanning();
|
||||
}
|
||||
|
||||
private void ShowSettings()
|
||||
{
|
||||
settingsLabel.Text = $"Lead-ins: outside {Describe(parameters.ExternalLeadIn)}, holes {Describe(parameters.InternalLeadIn)}, arcs/circles {Describe(parameters.ArcCircleLeadIn)}. Tabs {(parameters.TabsEnabled ? "on" : "off")}.";
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
internal void RequestClose()
|
||||
{
|
||||
if (closed)
|
||||
return;
|
||||
view.ClearProposalDisplay();
|
||||
planButton.Enabled = false;
|
||||
ClearResult();
|
||||
SetPlanning(true);
|
||||
cancelButton.Enabled = false;
|
||||
resultLabel.Text = "Cancelling… waiting for the worker. Nothing has changed.";
|
||||
if (inspection.RequestClose())
|
||||
@@ -169,7 +399,9 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
{
|
||||
view.InspectionCloseRequested = null;
|
||||
if (ReferenceEquals(view.Plate, plate))
|
||||
{
|
||||
view.OverlapDisplay = overlapDisplay;
|
||||
}
|
||||
}
|
||||
var ownedScope = scope;
|
||||
ownedScope?.Dispose();
|
||||
|
||||
@@ -278,20 +278,31 @@ namespace OpenNest.Forms
|
||||
}
|
||||
internal System.Action<System.Threading.CancellationToken> CuttingBeforePlan { get; set; }
|
||||
|
||||
internal void OpenCuttingInspection()
|
||||
internal void OpenCuttingInspection(bool allPlates = false)
|
||||
{
|
||||
if (CuttingInspection != null)
|
||||
return;
|
||||
PlateView.EndAction();
|
||||
var panel = new CuttingInspectionPanel(this, LoadOrDefaultParameters(PlateView.Plate.CuttingParameters))
|
||||
var panel = new CuttingInspectionPanel(this, LoadOrDefaultParameters(PlateView.Plate.CuttingParameters), allPlates)
|
||||
{
|
||||
BeforePlan = CuttingBeforePlan
|
||||
};
|
||||
CuttingInspection = panel;
|
||||
ShowSidePanel(panel, "Plan Cutting — Read-only Proposal");
|
||||
ShowSidePanel(panel, allPlates ? "Plan Cutting — All Plates" : "Plan Cutting — Read-only Proposal", 480);
|
||||
panel.Open();
|
||||
}
|
||||
|
||||
internal void CompleteCuttingApply(CuttingParameters parameters, OpenNest.CNC.CuttingPlanning.CuttingCommitResult commit)
|
||||
{
|
||||
if (commit.Status != OpenNest.CNC.CuttingPlanning.CuttingCommitStatus.Applied)
|
||||
return;
|
||||
SaveCuttingParameters(parameters);
|
||||
UpdatePlateList();
|
||||
if (commit.RefreshErrors.Count > 0)
|
||||
MessageBox.Show(this, "The cutting plan was applied, but part of the display did not refresh: "
|
||||
+ commit.RefreshErrors[0].Message, "Plan Cutting", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
internal void FinishCuttingInspection(CuttingInspectionPanel panel)
|
||||
{
|
||||
if (!ReferenceEquals(CuttingInspection, panel))
|
||||
|
||||
@@ -1952,10 +1952,7 @@ namespace OpenNest.Forms
|
||||
{
|
||||
foreach (var plateView in views)
|
||||
plateView.SetAction(typeof(ActionSelect));
|
||||
if (allPlates)
|
||||
editForm.PlanCutting(true, IsBusy, this);
|
||||
else
|
||||
editForm.OpenCuttingInspection();
|
||||
editForm.OpenCuttingInspection(allPlates);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
+23
-19
@@ -341,20 +341,25 @@ on an `Applied` result, not as a rollback.
|
||||
|
||||
## Desktop workflow
|
||||
|
||||
`Plate > Plan Cutting...` opens a read-only inspection panel beside the main plate canvas.
|
||||
It captures the active plate on the editor thread, plans on a worker, and shows the exact
|
||||
fresh, supported detached proposal in that canvas, including proposed part numbers and
|
||||
rapids. The live `Plate`, part order, programs and quantities remain unchanged; Escape,
|
||||
the panel X or Cancel restores the live view and prior viewport. Closing during planning
|
||||
waits for the worker before unlocking inspection. Stale or unsupported output has no
|
||||
proposal preview. This first panel slice does not offer Apply or settings editing: use
|
||||
`Nest > Plan Cutting (All Plates)...` for the existing modal Apply workflow. A displayed
|
||||
proposal is not a live-nest validation report or posting consent.
|
||||
`Plate > Plan Cutting...` opens a cutting inspection panel beside the main plate canvas;
|
||||
`Nest > Plan Cutting (All Plates)...` opens the same panel for every occupied plate.
|
||||
It captures scope on the editor thread, plans on a worker, and shows only a fresh,
|
||||
supported detached proposal in that canvas, including proposed part numbers and
|
||||
rapids. The live `Plate`, part order, programs and quantities remain unchanged
|
||||
until Apply. The summary and complete structured findings remain available while
|
||||
navigating plates; selecting a finding frames its affected part extent, not an
|
||||
invented contact point. Unsupported or stale output has no proposal preview.
|
||||
|
||||
`Nest > Plan Cutting (All Plates)...` plans every occupied plate in a dialog built on
|
||||
`OpenNest.Engine.CuttingPlanning.CuttingPlanBatch`:
|
||||
The panel shows the current cutting settings; editing them or switching Keep current
|
||||
part order starts a new plan and clears the previous warning acceptance. Apply uses
|
||||
the exact captured proposal, with an unchecked per-proposal acceptance required
|
||||
for UNVERIFIED results; a blocked plate disables the whole-batch Apply. A successful
|
||||
Apply persists the confirmed settings and refreshes the editor; a failed/stale Apply
|
||||
changes nothing and requires replanning. Escape, the panel X or Cancel restores the
|
||||
live view and prior viewport; closing during planning waits for the worker before
|
||||
unlocking inspection. Planning is not live-nest validation or posting consent.
|
||||
|
||||
- The dialog starts from the plate's cutting settings (or the last-used settings) and plans
|
||||
- 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
|
||||
whole-part order; either change replans. The settings are confirmed parameters: every
|
||||
unlocked part's lead-ins are regenerated. Locked parts keep their exact programs;
|
||||
@@ -386,8 +391,7 @@ proposal is not a live-nest validation report or posting consent.
|
||||
`NoSolutionWithinBudget` is retried once with the current part order, and the summary
|
||||
says the order was kept. Both are allowed 400 expansions per part (at least the
|
||||
default 20000), because both still plan contour order and entries for every part.
|
||||
- The dialog uses an owner-monitor-sized window (approximately 90% of the working area), like
|
||||
the database browser. The right panel keeps the batch outcome and plate headings above a
|
||||
- The docked panel keeps the batch outcome and plate headings above a
|
||||
read-only, unsorted warning grid with Plate, source Part, With, and full finding text. Every
|
||||
overlap issue/pair and every planner finding has its own selectable row, including findings
|
||||
hidden by the compact blocked-plate summary. Selecting a row switches to that plate's
|
||||
@@ -400,7 +404,7 @@ proposal is not a live-nest validation report or posting consent.
|
||||
with their status and findings. Unverified proposals retain every overlap and route
|
||||
warning in the selectable grid before the acceptance checkbox is used; they do
|
||||
not truncate later parts' warnings. Finding part numbers are the plate's current order, as the
|
||||
editor numbers them. The preview shows the active plate detached from the nest (quantity
|
||||
editor numbers them. The panel shows the selected plate detached from the nest (quantity
|
||||
zero, so drawing quantities do not change) in the proposed order with the proposed
|
||||
programs. Ready and best-effort proposals can be previewed only while the plate still
|
||||
matches capture. Best-effort previews are labelled `UNVERIFIED`. Refused program graphs
|
||||
@@ -412,13 +416,13 @@ proposal is not a live-nest validation report or posting consent.
|
||||
tolerance, so the source can be reviewed in the drawing editor. Reporting never
|
||||
closes gaps or changes the validation tolerance. A fully ready batch
|
||||
can apply immediately; an unverified batch requires the unchecked, per-proposal
|
||||
`I reviewed the warnings. Apply this unverified plan.` checkbox. Replanning clears it.
|
||||
`Accept UNVERIFIED plan warnings (not posting consent)` checkbox. Replanning clears it.
|
||||
Both use the same owned-program, freshness and rollback boundary. After it applies, each plate keeps its own copy of the
|
||||
confirmed settings, which also become the saved defaults. `Stale` keeps the dialog open
|
||||
confirmed settings, which also become the saved defaults. `Stale` keeps the panel open
|
||||
and asks for a replan; nothing changes.
|
||||
- Closing or cancelling while planning cancels the worker and keeps the dialog open until
|
||||
- Closing or cancelling while planning cancels the worker and keeps the panel locked until
|
||||
it stops. Planning and Apply refuse to start while a nesting or plate operation runs.
|
||||
The dialog plans with its own copy of the settings, and posts progress and results to
|
||||
The panel plans with its own copy of the settings, and posts progress and results to
|
||||
the thread it was created on rather than to whichever context is current.
|
||||
- `PlateView` follows `Plate.PartsReordered`: it redraws parts in the plate's order (the
|
||||
numbers it draws are the cutting order), rebuilds their graphics and marks the overlap
|
||||
|
||||
Reference in new issue
Block a user