From 943b63937dea8ddc92d06a96215b0becdf596ffd Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 11 Oct 2026 21:40:26 -0400 Subject: [PATCH] feat(cutting-ui): plan and apply all plates from docked panel --- .../CuttingProposalInspection.cs | 15 +- .../CuttingProposalInspectionTests.cs | 20 ++ .../Forms/CuttingInspectionPanelTests.cs | 168 ++++++++++ OpenNest/Forms/CuttingInspectionPanel.cs | 294 ++++++++++++++++-- OpenNest/Forms/EditNestForm.cs | 17 +- OpenNest/Forms/MainForm.cs | 5 +- docs/cutting-planner.md | 42 +-- 7 files changed, 500 insertions(+), 61 deletions(-) diff --git a/OpenNest.Engine/CuttingPlanning/CuttingProposalInspection.cs b/OpenNest.Engine/CuttingPlanning/CuttingProposalInspection.cs index 551e8c2..0742349 100644 --- a/OpenNest.Engine/CuttingPlanning/CuttingProposalInspection.cs +++ b/OpenNest.Engine/CuttingPlanning/CuttingProposalInspection.cs @@ -61,14 +61,21 @@ public sealed class CuttingProposalInspection } /// Recheck exact freshness before displaying, including every subsequent paint. - 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); } /// Zero-based proposed position for a one-based source diagnostic number. diff --git a/OpenNest.Tests/CuttingPlanning/CuttingProposalInspectionTests.cs b/OpenNest.Tests/CuttingPlanning/CuttingProposalInspectionTests.cs index ba881bb..c1decae 100644 --- a/OpenNest.Tests/CuttingPlanning/CuttingProposalInspectionTests.cs +++ b/OpenNest.Tests/CuttingPlanning/CuttingProposalInspectionTests.cs @@ -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); diff --git a/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs b/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs index 698fbb1..ef10401 100644 --- a/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs +++ b/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs @@ -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(panel, "resultLabel").ScrollBars); + Assert.False(Field