feat(cutting-ui): plan and apply all plates from docked panel

This commit is contained in:
aj committed 2026-10-11 21:40:26 -04:00
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();
+263 -31
View File
@@ -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();
+14 -3
View File
@@ -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))
+1 -4
View File
@@ -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
View File
@@ -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