From 41460481d6dd47e19baf8ffff42d451e09e9ac2d Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 29 Oct 2026 00:58:14 -0400 Subject: [PATCH] fix(cutting): keep Plan Cutting's lead and tab edits Plan Cutting saved its settings only on a successful Apply, so a plate that would not plan lost them. Each open then laid the machine's leads back over the last parameters, and the cutting panel rebuilt parameters from scratch, dropping kerf and the machine/material/thickness stamp. Save the settings when the dialog is accepted, keep the panel's unexposed fields, and only apply the machine's leads when the last parameters were not already taken from that machine, material, grade and thickness. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Data/MachineCuttingDefaults.cs | 19 +++++++++ .../Data/MachineCuttingDefaultsTests.cs | 13 ++++++ .../Controls/CuttingPanelTests.cs | 39 ++++++++++++++++++ OpenNest/Controls/CuttingPanel.cs | 40 +++++++++++-------- OpenNest/Forms/CuttingInspectionPanel.cs | 2 + OpenNest/Forms/EditNestForm.cs | 2 +- OpenNest/Forms/MachineSettings.cs | 8 +++- docs/machine-cutting-defaults.md | 6 ++- 8 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 OpenNest.WinForms.Tests/Controls/CuttingPanelTests.cs diff --git a/OpenNest.Data/MachineCuttingDefaults.cs b/OpenNest.Data/MachineCuttingDefaults.cs index 787bacca..df7fb139 100644 --- a/OpenNest.Data/MachineCuttingDefaults.cs +++ b/OpenNest.Data/MachineCuttingDefaults.cs @@ -73,6 +73,25 @@ public static class MachineCuttingDefaults return true; } + /// + /// Whether are stamped with the machine, material, grade and + /// thickness would record for + /// the nest: they started from the machine's settings for it, and any lead edits since are + /// the user's choice for that same job, which the machine's settings must not replace. + /// + public static bool IsRecordedFor(CuttingParameters parameters, MachineConfig? machine, Nest nest) + { + ArgumentNullException.ThrowIfNull(parameters); + ArgumentNullException.ThrowIfNull(nest); + if (Find(machine, nest) == null) + return false; + var tolerance = ThicknessToleranceInches * Scale(Units.Inches, nest.Units); + return string.Equals(parameters.MachineName, machine!.Name, StringComparison.Ordinal) + && string.Equals(parameters.MaterialName, nest.Material.Name, StringComparison.Ordinal) + && string.Equals(parameters.Grade ?? "", nest.Material.Grade ?? "", StringComparison.Ordinal) + && System.Math.Abs(parameters.Thickness - nest.Thickness) <= tolerance; + } + /// /// Replaces the part clearance, overtravel, minimum segment and direction of /// with the machine's for the nest; the head-lock choice is kept. diff --git a/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs b/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs index 88b47aae..a6d96fbe 100644 --- a/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs +++ b/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs @@ -41,6 +41,19 @@ public class MachineCuttingDefaultsTests private static readonly LeadConfig LineArc = new() { Type = "Line + Arc", Length = 0.125, Angle = 90, Radius = 0.0625 }; + [Fact] + public void IsRecordedFor_KeepsLeadsEditedAfterTheMachineSettingsWereTaken() + { + var parameters = new CuttingParameters(); + Assert.True(MachineCuttingDefaults.ApplyTo(parameters, Machine(LineArc), NestOf(0.25, "A36"))); + parameters.ExternalLeadOut = new NoLeadOut(); + + Assert.True(MachineCuttingDefaults.IsRecordedFor(parameters, Machine(LineArc), NestOf(0.2505, "A36"))); + Assert.False(MachineCuttingDefaults.IsRecordedFor(parameters, Machine(LineArc), NestOf(0.25, "A572"))); + Assert.False(MachineCuttingDefaults.IsRecordedFor(parameters, Machine(LineArc), NestOf(0.5, "A36"))); + Assert.False(MachineCuttingDefaults.IsRecordedFor(new CuttingParameters(), Machine(LineArc), NestOf(0.25, "A36"))); + } + [Fact] public void ApplyTo_GivesEveryContourTheMachineLeadsAndStampsTheSource() { diff --git a/OpenNest.WinForms.Tests/Controls/CuttingPanelTests.cs b/OpenNest.WinForms.Tests/Controls/CuttingPanelTests.cs new file mode 100644 index 00000000..66e827e2 --- /dev/null +++ b/OpenNest.WinForms.Tests/Controls/CuttingPanelTests.cs @@ -0,0 +1,39 @@ +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Controls; + +namespace OpenNest.WinForms.Tests.Controls; + +public class CuttingPanelTests +{ + [Fact] + public void BuildParameters_KeepsTheSettingsThePanelHasNoFieldFor() + { + StaTestThread.Run(() => + { + using var panel = new CuttingPanel(); + panel.LoadFromParameters(new CuttingParameters + { + MachineName = "CL-980", + MaterialName = "Stainless Steel", + Grade = "304", + Thickness = 0.075, + Kerf = 0.008, + ExternalLeadIn = new ArcLeadIn { Radius = 0.0625 }, + ExternalLeadOut = new NoLeadOut(), + TabsEnabled = true, + TabConfig = new NormalTab { Size = 0.25 }, + }); + + var built = panel.BuildParameters(); + + // Without the stamp, the machine's leads would replace these edits on the next open. + Assert.Equal("CL-980", built.MachineName); + Assert.Equal("Stainless Steel", built.MaterialName); + Assert.Equal("304", built.Grade); + Assert.Equal(0.075, built.Thickness); + Assert.Equal(0.008, built.Kerf); + Assert.IsType(built.ExternalLeadOut); + Assert.True(built.TabsEnabled); + }, TimeSpan.FromSeconds(30), "The cutting panel did not build its parameters."); + } +} diff --git a/OpenNest/Controls/CuttingPanel.cs b/OpenNest/Controls/CuttingPanel.cs index 48f8e064..238d5958 100644 --- a/OpenNest/Controls/CuttingPanel.cs +++ b/OpenNest/Controls/CuttingPanel.cs @@ -2,6 +2,7 @@ using System; using System.Drawing; using System.Windows.Forms; using OpenNest.CNC.CuttingStrategy; +using OpenNest.IO; namespace OpenNest.Controls { @@ -57,6 +58,8 @@ namespace OpenNest.Controls private readonly NumericUpDown nudPierceClearance; private readonly NumericUpDown nudPierceDiameter; + private CuttingParameters loaded; + private readonly CheckBox chkRoundLeadInAngles; private readonly NumericUpDown nudLeadInAngleIncrement; @@ -294,30 +297,33 @@ namespace OpenNest.Controls base.Dispose(disposing); } + // Settings the panel has no field for (kerf, the machine/material/thickness stamp, + // sequencing) come back unchanged from the parameters last loaded. public CuttingParameters BuildParameters() { - return new CuttingParameters - { - ExternalLeadIn = BuildLeadIn(cboExternalLeadIn, pnlExternalLeadIn), - ExternalLeadOut = BuildLeadOut(cboExternalLeadOut, pnlExternalLeadOut), - InternalLeadIn = BuildLeadIn(cboInternalLeadIn, pnlInternalLeadIn), - InternalLeadOut = BuildLeadOut(cboInternalLeadOut, pnlInternalLeadOut), - TabsEnabled = chkTabsEnabled.Checked && rbTabAll.Checked, - TabConfig = new NormalTab { Size = (double)nudTabWidth.Value }, - PierceClearance = (double)nudPierceClearance.Value, - PierceDiameter = (double)nudPierceDiameter.Value, - RoundLeadInAngles = chkRoundLeadInAngles.Checked, - LeadInAngleIncrement = (double)nudLeadInAngleIncrement.Value, - SupportTabs = chkTabsEnabled.Checked && rbSupportTab.Checked, - NoBedTabMinWidth = (double)nudNoBedTabMin.Value, - NoBedTabMaxWidth = (double)nudNoBedTabMax.Value, - TabHeldMaxLength = (double)nudTabHeldMax.Value, - }; + var parameters = loaded == null ? new CuttingParameters() + : CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(loaded)); + parameters.ExternalLeadIn = BuildLeadIn(cboExternalLeadIn, pnlExternalLeadIn); + parameters.ExternalLeadOut = BuildLeadOut(cboExternalLeadOut, pnlExternalLeadOut); + parameters.InternalLeadIn = BuildLeadIn(cboInternalLeadIn, pnlInternalLeadIn); + parameters.InternalLeadOut = BuildLeadOut(cboInternalLeadOut, pnlInternalLeadOut); + parameters.TabsEnabled = chkTabsEnabled.Checked && rbTabAll.Checked; + parameters.TabConfig = new NormalTab { Size = (double)nudTabWidth.Value }; + parameters.PierceClearance = (double)nudPierceClearance.Value; + parameters.PierceDiameter = (double)nudPierceDiameter.Value; + parameters.RoundLeadInAngles = chkRoundLeadInAngles.Checked; + parameters.LeadInAngleIncrement = (double)nudLeadInAngleIncrement.Value; + parameters.SupportTabs = chkTabsEnabled.Checked && rbSupportTab.Checked; + parameters.NoBedTabMinWidth = (double)nudNoBedTabMin.Value; + parameters.NoBedTabMaxWidth = (double)nudNoBedTabMax.Value; + parameters.TabHeldMaxLength = (double)nudTabHeldMax.Value; + return parameters; } public void LoadFromParameters(CuttingParameters p) { suppressEvents = true; + loaded = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(p)); LoadLeadIn(cboExternalLeadIn, pnlExternalLeadIn, p.ExternalLeadIn); LoadLeadOut(cboExternalLeadOut, pnlExternalLeadOut, p.ExternalLeadOut); diff --git a/OpenNest/Forms/CuttingInspectionPanel.cs b/OpenNest/Forms/CuttingInspectionPanel.cs index 457b6643..465b6b98 100644 --- a/OpenNest/Forms/CuttingInspectionPanel.cs +++ b/OpenNest/Forms/CuttingInspectionPanel.cs @@ -1258,6 +1258,8 @@ internal sealed class CuttingInspectionPanel : UserControl dialog.LoadParameters(parameters); if (dialog.ShowDialog(this) != DialogResult.OK || closed || closeRequested) return; parameters = dialog.GetParameters(); + // Saved now, not only on Apply: a plate that will not plan must not lose the edit. + EditNestForm.SaveCuttingParameters(parameters); ShowSettings(); RestartPlanning(); } diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index ddffe263..8607740f 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -1356,7 +1356,7 @@ namespace OpenNest.Forms PlateView.SetAction(typeof(Actions.ActionLeadIn)); } - private static void SaveCuttingParameters(CuttingParameters parameters) + internal static void SaveCuttingParameters(CuttingParameters parameters) { var json = CuttingParametersSerializer.Serialize(parameters); Properties.Settings.Default.CuttingParametersJson = json; diff --git a/OpenNest/Forms/MachineSettings.cs b/OpenNest/Forms/MachineSettings.cs index d9eb82ce..4ec3271e 100644 --- a/OpenNest/Forms/MachineSettings.cs +++ b/OpenNest/Forms/MachineSettings.cs @@ -198,12 +198,16 @@ namespace OpenNest.Forms /// /// The cutting parameters a plate of without its own starts from: /// the last ones applied (or the built-in defaults), with the lead-ins, lead-outs and kerf - /// of the nest's machine for its material and thickness when the machine has them. + /// of the nest's machine for its material and thickness when the machine has them. Last + /// parameters already taken from that machine, material and thickness keep the user's + /// edits to them. /// public static CuttingParameters DefaultCuttingParameters(Nest nest) { var parameters = LastCuttingParameters(); - MachineCuttingDefaults.ApplyTo(parameters, FindMachine(nest.Machine), nest); + var machine = FindMachine(nest.Machine); + if (!MachineCuttingDefaults.IsRecordedFor(parameters, machine, nest)) + MachineCuttingDefaults.ApplyTo(parameters, machine, nest); return parameters; } diff --git a/docs/machine-cutting-defaults.md b/docs/machine-cutting-defaults.md index 1b4e5a29..14fce265 100644 --- a/docs/machine-cutting-defaults.md +++ b/docs/machine-cutting-defaults.md @@ -19,7 +19,11 @@ machine's units to the nest's. A plate with no cutting parameters of its own (Plan Cutting, Place Lead-in) starts from the last applied parameters, as before, with the machine's leads and kerf laid over them and the -machine, material, grade and thickness recorded on the parameters. A plate that already has +machine, material, grade and thickness recorded on the parameters. When the last parameters +already record the same machine, material, grade and thickness, the machine's settings are not +laid over them again, so lead and tab edits made for that job stick; the cutting panel keeps the +record (and the kerf) through an edit. Plan Cutting saves its settings as the last parameters as +soon as its settings dialog is accepted, not only on Apply. A plate that already has parameters keeps them: changing the machine, material or thickness does not rewrite leads the plate was planned with.