From 59d4c8d72707b9ae333a7340a5897db6f2f67dc3 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 31 Oct 2026 19:39:48 -0400 Subject: [PATCH] feat(machines): set spacing per thickness and save defaults from a nest Plate > Set as Default and Nest > Edit's Defaults tab only changed the open nest, so new nests never picked up the values. Set as Default now also saves the new-nest defaults, and the Defaults tab gains an opt-in "Use these defaults for new nests" check. Machine thicknesses can now set part and edge spacing. A nest takes them when its material, grade, thickness or machine changes and when a release creates it; BOM import seeds its groups from them. Spacing edited in the same dialog stays, and only an untouched trailing sheet follows. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Data/EdgeSpacingConfig.cs | 10 ++ OpenNest.Data/MachineCuttingDefaults.cs | 48 ++++++++ OpenNest.Data/ThicknessConfig.cs | 6 + .../Data/MachineCuttingDefaultsTests.cs | 114 ++++++++++++++++++ .../Forms/EditNestInfoFormTests.cs | 19 +++ .../Forms/MachineCutsFormsTests.cs | 51 ++++++++ OpenNest/Controls/ReleasePanel.cs | 2 +- OpenNest/Forms/BomImportForm.cs | 32 +++-- OpenNest/Forms/EditNestForm.cs | 10 +- OpenNest/Forms/EditNestInfoForm.Designer.cs | 13 ++ OpenNest/Forms/EditNestInfoForm.cs | 19 +++ OpenNest/Forms/MachineConfigForm.cs | 71 +++++++++++ OpenNest/Forms/MachineSettings.cs | 32 +++++ OpenNest/Forms/MainForm.Nest.cs | 6 +- OpenNest/Forms/MainForm.Plate.cs | 7 +- OpenNest/Forms/MainForm.Tools.cs | 22 +--- OpenNest/Forms/NewNestFactory.cs | 21 ++-- OpenNest/Forms/SavedNestsForm.cs | 2 +- docs/machine-cutting-defaults.md | 12 +- docs/shared-settings.md | 4 + 20 files changed, 457 insertions(+), 44 deletions(-) create mode 100644 OpenNest.Data/EdgeSpacingConfig.cs diff --git a/OpenNest.Data/EdgeSpacingConfig.cs b/OpenNest.Data/EdgeSpacingConfig.cs new file mode 100644 index 00000000..956bdfcc --- /dev/null +++ b/OpenNest.Data/EdgeSpacingConfig.cs @@ -0,0 +1,10 @@ +namespace OpenNest.Data; + +/// A thickness's distance kept from each sheet edge, in the machine's units. +public class EdgeSpacingConfig +{ + public double Left { get; set; } + public double Bottom { get; set; } + public double Right { get; set; } + public double Top { get; set; } +} diff --git a/OpenNest.Data/MachineCuttingDefaults.cs b/OpenNest.Data/MachineCuttingDefaults.cs index 22ff48f6..c3afd6da 100644 --- a/OpenNest.Data/MachineCuttingDefaults.cs +++ b/OpenNest.Data/MachineCuttingDefaults.cs @@ -1,4 +1,5 @@ using OpenNest.CNC.CuttingStrategy; +using OpenNest.Geometry; namespace OpenNest.Data; @@ -64,6 +65,53 @@ public static class MachineCuttingDefaults return true; } + /// + /// Gives the nest's plate defaults the part and edge spacing the machine has for its + /// material and thickness. Spacing the machine leaves unset stays as it was. Returns false, + /// changing nothing, when the machine sets neither. + /// + public static bool ApplySpacingTo(Nest nest, MachineConfig? machine) + { + ArgumentNullException.ThrowIfNull(nest); + var settings = Find(machine, nest); + if (settings?.PartSpacing == null && settings?.EdgeSpacing == null) + return false; + + var scale = Scale(machine!.Units, nest.Units); + if (settings.PartSpacing is { } partSpacing) + nest.PlateDefaults.PartSpacing = partSpacing * scale; + if (settings.EdgeSpacing is { } edge) + nest.PlateDefaults.EdgeSpacing = new Spacing(edge.Left * scale, edge.Bottom * scale, edge.Right * scale, edge.Top * scale); + return true; + } + + /// + /// Runs , then, when it changed the nest's material, grade or + /// thickness, applies the machine's spacing for them (). A + /// spacing the edit changed itself is the user's choice and stays. + /// + public static void ApplySpacingAfter(Nest nest, MachineConfig? machine, Action edit) + { + ArgumentNullException.ThrowIfNull(nest); + ArgumentNullException.ThrowIfNull(edit); + var job = (nest.Material?.Name, nest.Material?.Grade, nest.Thickness); + var plate = nest.PlateDefaults; + var (partBefore, edgeBefore) = (plate.PartSpacing, plate.EdgeSpacing); + + edit(); + + if (job == (nest.Material?.Name, nest.Material?.Grade, nest.Thickness)) + return; + + plate = nest.PlateDefaults; + var (partEdited, edgeEdited) = (plate.PartSpacing, plate.EdgeSpacing); + ApplySpacingTo(nest, machine); + if (partEdited != partBefore) + plate.PartSpacing = partEdited; + if (!edgeEdited.Equals(edgeBefore)) + plate.EdgeSpacing = edgeEdited; + } + /// /// The leads give, in the machine's units, for previewing them: /// a lead the machine leaves as it was (Line + Line, or Clean Hole on outside cuts) is none. diff --git a/OpenNest.Data/ThicknessConfig.cs b/OpenNest.Data/ThicknessConfig.cs index 6d674e8b..88b93ab9 100644 --- a/OpenNest.Data/ThicknessConfig.cs +++ b/OpenNest.Data/ThicknessConfig.cs @@ -26,4 +26,10 @@ public class ThicknessConfig public CutOffConfig CutOff { get; set; } = new(); public List PlateSizes { get; set; } = new(); + + /// The part spacing of nests in this thickness; null keeps the nest defaults'. + public double? PartSpacing { get; set; } + + /// The edge spacing of nests in this thickness; null keeps the nest defaults'. + public EdgeSpacingConfig? EdgeSpacing { get; set; } } diff --git a/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs b/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs index 5cb46e29..0bf281fb 100644 --- a/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs +++ b/OpenNest.Tests/Data/MachineCuttingDefaultsTests.cs @@ -1,5 +1,6 @@ using OpenNest.CNC.CuttingStrategy; using OpenNest.Data; +using OpenNest.Geometry; namespace OpenNest.Tests.Data; @@ -41,6 +42,119 @@ public class MachineCuttingDefaultsTests private static readonly LeadConfig LineArc = new() { Type = "Line + Arc", Length = 0.125, Angle = 90, Radius = 0.0625 }; + private static MachineConfig SpacedMachine(UnitSystem units = UnitSystem.Inches) + { + var machine = Machine(LineArc, units: units); + var thickness = machine.Materials[0].Thicknesses[0]; + thickness.PartSpacing = 0.5; + thickness.EdgeSpacing = new EdgeSpacingConfig { Left = 1, Bottom = 2, Right = 3, Top = 4 }; + return machine; + } + + private static Nest SpacedNest(double thickness = 0.25, Units units = Units.Inches) + { + var nest = NestOf(thickness, "A36", units); + nest.PlateDefaults.PartSpacing = 0.1; + nest.PlateDefaults.EdgeSpacing = new Spacing(0.2, 0.2, 0.2, 0.2); + return nest; + } + + [Fact] + public void ApplySpacingTo_SetsTheMachinesSpacing() + { + var nest = SpacedNest(); + + Assert.True(MachineCuttingDefaults.ApplySpacingTo(nest, SpacedMachine())); + + Assert.Equal(0.5, nest.PlateDefaults.PartSpacing); + Assert.Equal(new Spacing(1, 2, 3, 4), nest.PlateDefaults.EdgeSpacing); + } + + [Fact] + public void ApplySpacingTo_ConvertsToTheNestsUnits() + { + var nest = SpacedNest(6.35, Units.Millimeters); + + Assert.True(MachineCuttingDefaults.ApplySpacingTo(nest, SpacedMachine())); + + Assert.Equal(12.7, nest.PlateDefaults.PartSpacing, 6); + Assert.Equal(25.4, nest.PlateDefaults.EdgeSpacing.Left, 6); + } + + [Fact] + public void ApplySpacingTo_KeepsWhatTheThicknessLeavesUnset() + { + var machine = SpacedMachine(); + machine.Materials[0].Thicknesses[0].EdgeSpacing = null; + var nest = SpacedNest(); + + Assert.True(MachineCuttingDefaults.ApplySpacingTo(nest, machine)); + + Assert.Equal(0.5, nest.PlateDefaults.PartSpacing); + Assert.Equal(new Spacing(0.2, 0.2, 0.2, 0.2), nest.PlateDefaults.EdgeSpacing); + } + + [Fact] + public void ApplySpacingTo_WithoutSpacingOnFile_ChangesNothing() + { + var nest = SpacedNest(); + + Assert.False(MachineCuttingDefaults.ApplySpacingTo(nest, Machine(LineArc))); + Assert.False(MachineCuttingDefaults.ApplySpacingTo(nest, null)); + Assert.False(MachineCuttingDefaults.ApplySpacingTo(SpacedNest(0.5), SpacedMachine())); + + Assert.Equal(0.1, nest.PlateDefaults.PartSpacing); + } + + [Fact] + public void ApplySpacingAfter_FollowsAChangedThicknessOnly() + { + var nest = SpacedNest(0.5); + + MachineCuttingDefaults.ApplySpacingAfter(nest, SpacedMachine(), () => nest.Customer = "Acme"); + Assert.Equal(0.1, nest.PlateDefaults.PartSpacing); + + MachineCuttingDefaults.ApplySpacingAfter(nest, SpacedMachine(), () => nest.Thickness = 0.25); + Assert.Equal(0.5, nest.PlateDefaults.PartSpacing); + } + + [Fact] + public void ApplySpacingAfter_KeepsSpacingTheEditChanged() + { + var nest = SpacedNest(0.5); + + MachineCuttingDefaults.ApplySpacingAfter(nest, SpacedMachine(), () => + { + nest.Thickness = 0.25; + nest.PlateDefaults.PartSpacing = 0.3; + }); + + Assert.Equal(0.3, nest.PlateDefaults.PartSpacing); + Assert.Equal(new Spacing(1, 2, 3, 4), nest.PlateDefaults.EdgeSpacing); + } + + [Fact] + public void Spacing_SurvivesTheMachineFile() + { + var directory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + try + { + var provider = new LocalJsonProvider(directory); + var machine = SpacedMachine(); + provider.SaveMachine(machine); + + var thickness = provider.GetMachine(machine.Id)!.Materials[0].Thicknesses[0]; + + Assert.Equal(0.5, thickness.PartSpacing); + Assert.Equal(4, thickness.EdgeSpacing!.Top); + } + finally + { + if (Directory.Exists(directory)) + Directory.Delete(directory, true); + } + } + [Fact] public void IsRecordedFor_KeepsLeadsEditedAfterTheMachineSettingsWereTaken() { diff --git a/OpenNest.WinForms.Tests/Forms/EditNestInfoFormTests.cs b/OpenNest.WinForms.Tests/Forms/EditNestInfoFormTests.cs index 1f2be4a1..c9e57490 100644 --- a/OpenNest.WinForms.Tests/Forms/EditNestInfoFormTests.cs +++ b/OpenNest.WinForms.Tests/Forms/EditNestInfoFormTests.cs @@ -74,6 +74,25 @@ public class EditNestInfoFormTests }); } + [Fact] + public void NewNestDefaultsChoice_NeedsTheOfferAndTheCheck() + { + RunSta(() => + { + using var form = new EditNestInfoForm(); + var box = (CheckBox)form.Controls.Find("useForNewNestsBox", true).Single(); + + box.Checked = true; + Assert.False(form.UseForNewNests); + + form.OfferNewNestDefaults = true; + Assert.True(form.UseForNewNests); + + box.Checked = false; + Assert.False(form.UseForNewNests); + }); + } + private static Nest AcceptanceNest() { var nest = new Nest("Acceptance nest") diff --git a/OpenNest.WinForms.Tests/Forms/MachineCutsFormsTests.cs b/OpenNest.WinForms.Tests/Forms/MachineCutsFormsTests.cs index cd3f0e9a..8a930892 100644 --- a/OpenNest.WinForms.Tests/Forms/MachineCutsFormsTests.cs +++ b/OpenNest.WinForms.Tests/Forms/MachineCutsFormsTests.cs @@ -10,6 +10,57 @@ namespace OpenNest.WinForms.Tests.Forms; /// public class MachineCutsFormsTests { + [Fact] + public void SpacingTab_SetsOnlyTheCheckedSpacing() + { + RunSta(() => + { + var directory = Path.Combine(Path.GetTempPath(), "OpenNestTests", Guid.NewGuid().ToString()); + Directory.CreateDirectory(directory); + try + { + var provider = new LocalJsonProvider(directory); + provider.SaveMachine(new MachineConfig + { + Name = "Laser", + Materials = { new MaterialConfig { Name = "Mild Steel", Thicknesses = { new ThicknessConfig { Value = 0.25 } } } }, + }); + + using var form = new MachineConfigForm(provider); + var node = Descendants(Find(form, "_tree").Nodes).Single(n => n.Tag is ThicknessConfig); + typeof(MachineConfigForm) + .GetMethod("Tree_AfterSelect", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .Invoke(form, new object[] { null, new TreeViewEventArgs(node) }); + var shown = (ThicknessConfig)node.Tag; + + var page = All(form).Single().TabPages.Cast().Single(p => p.Text == "Spacing"); + var checks = All(page).ToList(); + var boxes = All(page).ToList(); + Assert.Equal(2, checks.Count); + Assert.Equal(5, boxes.Count); + Assert.All(boxes, b => Assert.False(b.Enabled)); + + checks[0].Checked = true; + boxes[0].Value = 0.5m; + Assert.Equal(0.5, shown.PartSpacing); + Assert.Null(shown.EdgeSpacing); + + checks[1].Checked = true; + boxes[4].Value = 2m; + Assert.Equal(2, shown.EdgeSpacing!.Top); + + checks[0].Checked = false; + checks[1].Checked = false; + Assert.Null(shown.PartSpacing); + Assert.Null(shown.EdgeSpacing); + } + finally + { + Directory.Delete(directory, true); + } + }); + } + [Fact] public void InsideCutsTab_GivesHolesTheirOwnLeads() { diff --git a/OpenNest/Controls/ReleasePanel.cs b/OpenNest/Controls/ReleasePanel.cs index 24577820..54966cd9 100644 --- a/OpenNest/Controls/ReleasePanel.cs +++ b/OpenNest/Controls/ReleasePanel.cs @@ -116,7 +116,7 @@ namespace OpenNest.Controls nestAfterReleaseCheckBox.Checked = Properties.Settings.Default.ReleaseNestAfter; nestAfterReleaseCheckBox.Click += NestAfterReleaseCheckBox_Click; ShowMessage = (text, buttons, icon) => MessageBox.Show(FindForm(), text, "Release", buttons, icon); - CreateNest = (group, _) => NewNestFactory.CreateAsync(FindForm(), group.Units); + CreateNest = (group, _) => NewNestFactory.CreateAsync(FindForm(), group); EditDrawing = edit => { using var form = new EditLibraryDrawingForm(edit); diff --git a/OpenNest/Forms/BomImportForm.cs b/OpenNest/Forms/BomImportForm.cs index b758e552..909a7a93 100644 --- a/OpenNest/Forms/BomImportForm.cs +++ b/OpenNest/Forms/BomImportForm.cs @@ -280,6 +280,7 @@ namespace OpenNest.Forms : _defaults.Size.Length; var groups = BomImportGroups.Build(_parts); + var machine = MachineSettings.DefaultMachine(_defaults, MachineSettings.LoadMachines()); var table = new DataTable(); table.Columns.Add("Material", typeof(string)); @@ -297,6 +298,7 @@ namespace OpenNest.Forms foreach (var group in groups) { var existing = _groupSettings.TryGetValue(group.Key, out var gs); + var defaults = GroupDefaults(group, machine); table.Rows.Add( group.Material, @@ -305,11 +307,11 @@ namespace OpenNest.Forms group.TotalQty, existing ? gs.PlateWidth : defaultWidth, existing ? gs.PlateLength : defaultLength, - existing ? gs.PartSpacing : _defaults.PartSpacing, - existing ? gs.EdgeLeft : _defaults.EdgeSpacing.Left, - existing ? gs.EdgeBottom : _defaults.EdgeSpacing.Bottom, - existing ? gs.EdgeRight : _defaults.EdgeSpacing.Right, - existing ? gs.EdgeTop : _defaults.EdgeSpacing.Top + existing ? gs.PartSpacing : defaults.PartSpacing, + existing ? gs.EdgeLeft : defaults.EdgeSpacing.Left, + existing ? gs.EdgeBottom : defaults.EdgeSpacing.Bottom, + existing ? gs.EdgeRight : defaults.EdgeSpacing.Right, + existing ? gs.EdgeTop : defaults.EdgeSpacing.Top ); } @@ -327,6 +329,15 @@ namespace OpenNest.Forms btnCreateNests.Enabled = table.Rows.Count > 0; } + // The saved defaults with the machine's spacing for the group's material and thickness. + private NestDefaults GroupDefaults(BomImportGroup group, MachineConfig machine) + { + var nest = new Nest { Thickness = group.Thickness, Material = new Material(group.Material) }; + _defaults.ApplyTo(nest); + MachineCuttingDefaults.ApplySpacingTo(nest, machine); + return NestDefaults.FromNest(nest); + } + private void SaveGroupSettings() { if (dgvGroups.DataSource is not DataTable table) @@ -416,15 +427,16 @@ namespace OpenNest.Forms { if (!_groupSettings.TryGetValue(group.Key, out var plate)) { + var defaults = GroupDefaults(group, machine); plate = new BomGroupPlateSettings { PlateWidth = defaultWidth, PlateLength = defaultLength, - PartSpacing = _defaults.PartSpacing, - EdgeLeft = _defaults.EdgeSpacing.Left, - EdgeBottom = _defaults.EdgeSpacing.Bottom, - EdgeRight = _defaults.EdgeSpacing.Right, - EdgeTop = _defaults.EdgeSpacing.Top, + PartSpacing = defaults.PartSpacing, + EdgeLeft = defaults.EdgeSpacing.Left, + EdgeBottom = defaults.EdgeSpacing.Bottom, + EdgeRight = defaults.EdgeSpacing.Right, + EdgeTop = defaults.EdgeSpacing.Top, }; } diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index 9e0f03ca..d373a717 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -826,7 +826,11 @@ namespace OpenNest.Forms public async Task ShowNestInfoEditorAsync() { - var form = new EditNestInfoForm { AllowRename = NestStorage.Settings.IsDatabaseMode }; + var form = new EditNestInfoForm + { + AllowRename = NestStorage.Settings.IsDatabaseMode, + OfferNewNestDefaults = true, + }; form.SetMaterialCatalog(MachineSettings.LoadMaterials()); form.LoadNestInfo(Nest); _ = ShowReleaseOriginAsync(form); @@ -843,13 +847,15 @@ namespace OpenNest.Forms using (var change = BeginChange("Nest Info")) { var before = UndoableNestInfo(); - PlateManager.ChangePlateDefaults(() => form.SaveNestInfo(Nest)); + MachineSettings.SaveNestInfo(PlateManager, form, Nest); if (UndoableNestInfo() != before) change.MarkChanged(); // The material, thickness or units may have changed. MachineSettings.ApplyCutOffDefaults(PlateView, Nest); change.Complete(); } + if (form.UseForNewNests) + MachineSettings.SaveNestDefaults(this, NestDefaults.FromNest(Nest), Nest.Machine); Text = Nest.Name; drawingListBox1.Units = Nest.Units; drawingListBox1.Invalidate(); diff --git a/OpenNest/Forms/EditNestInfoForm.Designer.cs b/OpenNest/Forms/EditNestInfoForm.Designer.cs index bdbebc20..62c99cdc 100644 --- a/OpenNest/Forms/EditNestInfoForm.Designer.cs +++ b/OpenNest/Forms/EditNestInfoForm.Designer.cs @@ -47,6 +47,7 @@ this.partSpacingBox = new OpenNest.Controls.NumericUpDown(); this.labelThk = new System.Windows.Forms.Label(); this.groupBox3 = new System.Windows.Forms.GroupBox(); + this.useForNewNestsBox = new System.Windows.Forms.CheckBox(); this.quadrantSelect1 = new OpenNest.Controls.QuadrantSelect(); this.tabControl1 = new System.Windows.Forms.TabControl(); this.tabPage1 = new System.Windows.Forms.TabPage(); @@ -631,6 +632,7 @@ // // tabPage2 // + this.tabPage2.Controls.Add(this.useForNewNestsBox); this.tabPage2.Controls.Add(this.groupBox3); this.tabPage2.Controls.Add(this.groupBox1); this.tabPage2.Controls.Add(this.tableLayoutPanel1); @@ -642,6 +644,16 @@ this.tabPage2.Text = "Defaults"; this.tabPage2.UseVisualStyleBackColor = true; // + // useForNewNestsBox + // + this.useForNewNestsBox.AutoSize = true; + this.useForNewNestsBox.Location = new System.Drawing.Point(12, 288); + this.useForNewNestsBox.Name = "useForNewNestsBox"; + this.useForNewNestsBox.TabIndex = 8; + this.useForNewNestsBox.Text = "Use these defaults for new nests"; + this.useForNewNestsBox.UseVisualStyleBackColor = true; + this.useForNewNestsBox.Visible = false; + // // tabPage3 // this.tabPage3.Controls.Add(this.notesBox); @@ -772,6 +784,7 @@ private Controls.NumericUpDown partSpacingBox; private Controls.BottomPanel bottomPanel1; private System.Windows.Forms.GroupBox groupBox3; + private System.Windows.Forms.CheckBox useForNewNestsBox; private Controls.QuadrantSelect quadrantSelect1; private System.Windows.Forms.Button applyButton; private System.Windows.Forms.Label releaseLabel; diff --git a/OpenNest/Forms/EditNestInfoForm.cs b/OpenNest/Forms/EditNestInfoForm.cs index 9fc517a9..4474c453 100644 --- a/OpenNest/Forms/EditNestInfoForm.cs +++ b/OpenNest/Forms/EditNestInfoForm.cs @@ -15,6 +15,7 @@ namespace OpenNest.Forms private DateTime dateLastModified; private List materialCatalog = new(); private string gradeMaterialKey = ""; + private bool offerNewNestDefaults; public EditNestInfoForm() { @@ -58,6 +59,24 @@ namespace OpenNest.Forms set { nameBox.ReadOnly = !value; } } + /// + /// Shows the Defaults tab's choice to also start new nests from these values. Hidden + /// by default: the tab otherwise edits only this nest. + /// + public bool OfferNewNestDefaults + { + // Not read back from the box: Visible is false once the dialog has closed. + get { return offerNewNestDefaults; } + set + { + offerNewNestDefaults = value; + useForNewNestsBox.Visible = value; + } + } + + /// The user chose to save the Defaults tab for new nests. + public bool UseForNewNests => OfferNewNestDefaults && useForNewNestsBox.Checked; + /// /// The drawing set release this nest came from (Database mode), shown beside the /// buttons; blank for none. diff --git a/OpenNest/Forms/MachineConfigForm.cs b/OpenNest/Forms/MachineConfigForm.cs index 7e763037..6c14cf00 100644 --- a/OpenNest/Forms/MachineConfigForm.cs +++ b/OpenNest/Forms/MachineConfigForm.cs @@ -621,6 +621,7 @@ namespace OpenNest.Forms tabs.TabPages.Add(CreateTab("Outside Cuts", BuildOutsideCutsTab(thickness))); tabs.TabPages.Add(CreateTab("Inside Cuts", BuildInsideCutsTab(thickness))); tabs.TabPages.Add(CreateTab("Cut Off", BuildCutOffTab(thickness.CutOff))); + tabs.TabPages.Add(CreateTab("Spacing", BuildSpacingTab(thickness))); tabs.TabPages.Add(CreateTab("Plate Sizes", BuildPlateSizesTab(thickness))); RememberTab(tabs, _thicknessTab, t => _thicknessTab = t); @@ -701,6 +702,76 @@ namespace OpenNest.Forms return panel; } + // Unchecked leaves the spacing to the nest defaults, so a thickness need not set both. + private Control BuildSpacingTab(ThicknessConfig thickness) + { + var layout = CreateDetailLayout(); + var row = 0; + + var partBox = new CheckBox + { + Text = "Use for this thickness", + AutoSize = true, + Checked = thickness.PartSpacing.HasValue, + }; + var part = CreateNumericBox(thickness.PartSpacing ?? 0, v => + { + thickness.PartSpacing = partBox.Checked ? v : null; + MarkDirty(); + }, 4, 1000, 0.0625m); + part.Enabled = partBox.Checked; + partBox.CheckedChanged += (s, e) => + { + part.Enabled = partBox.Checked; + thickness.PartSpacing = partBox.Checked ? (double)part.Value : null; + MarkDirty(); + }; + AddField(layout, ref row, "Part Spacing:", partBox); + AddField(layout, ref row, $"Spacing ({LengthUnitSuffix(null)}):", part); + + var edge = thickness.EdgeSpacing; + var edgeBox = new CheckBox + { + Text = "Use for this thickness", + AutoSize = true, + Checked = edge != null, + }; + var sides = new NumericUpDown[4]; + void StoreEdge() + { + thickness.EdgeSpacing = edgeBox.Checked + ? new EdgeSpacingConfig + { + Left = (double)sides[0].Value, + Bottom = (double)sides[1].Value, + Right = (double)sides[2].Value, + Top = (double)sides[3].Value, + } + : null; + MarkDirty(); + } + AddField(layout, ref row, "Edge Spacing:", edgeBox); + var values = new[] { edge?.Left ?? 0, edge?.Bottom ?? 0, edge?.Right ?? 0, edge?.Top ?? 0 }; + var labels = new[] { "Left", "Bottom", "Right", "Top" } + .Select(side => $"{side} ({LengthUnitSuffix(null)}):").ToArray(); + for (var i = 0; i < sides.Length; i++) + { + sides[i] = CreateNumericBox(values[i], _ => StoreEdge(), 4, 1000, 0.0625m); + sides[i].Enabled = edgeBox.Checked; + AddField(layout, ref row, labels[i], sides[i]); + } + edgeBox.CheckedChanged += (s, e) => + { + foreach (var side in sides) + side.Enabled = edgeBox.Checked; + StoreEdge(); + }; + + var panel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(10) }; + panel.Controls.Add(layout); + return panel; + } + private Control BuildOutsideCutsTab(ThicknessConfig thickness) { var editor = CreateLeadsEditor(CNC.CuttingStrategy.ContourType.External); diff --git a/OpenNest/Forms/MachineSettings.cs b/OpenNest/Forms/MachineSettings.cs index 90f65a23..3e058927 100644 --- a/OpenNest/Forms/MachineSettings.cs +++ b/OpenNest/Forms/MachineSettings.cs @@ -241,6 +241,14 @@ namespace OpenNest.Forms return null; } + /// + /// Saves a Nest Info dialog to : a changed material or thickness + /// takes the machine's spacing, and an untouched trailing sheet follows the new defaults. + /// + public static void SaveNestInfo(PlateManager manager, EditNestInfoForm form, Nest nest) => + manager.ChangePlateDefaults(() => MachineCuttingDefaults.ApplySpacingAfter( + nest, FindMachine(nest.Machine), () => form.SaveNestInfo(nest))); + /// /// Gives the cut-off clearance, overtravel, minimum segment and /// direction of the nest's machine for its material and thickness, when the machine has @@ -317,6 +325,30 @@ namespace OpenNest.Forms defaults.Save(NestDefaults.DefaultPath); } + /// Saves the nest defaults, telling the user when they could not be saved. + public static void SaveNestDefaults(IWin32Window owner, NestDefaults defaults) + { + try + { + SaveNestDefaults(defaults); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + MessageBox.Show(owner, "The nest defaults could not be saved.\n\n" + ex.Message, "Nest Defaults", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + /// + /// Makes values taken from an open nest the defaults new nests start from. The nest's + /// machine becomes the default; a nest without one keeps the saved choice. + /// + public static void SaveNestDefaults(IWin32Window owner, NestDefaults defaults, NestMachine machine) + { + defaults.MachineId = machine?.Id ?? LoadSavedNestDefaults(out _).MachineId; + SaveNestDefaults(owner, defaults); + } + private static SharedSettingRecord SharedNestDefaults() => SharedSettings.Find(SharedSettingKinds.NestDefaults, SharedSettingKinds.DefaultKey); } diff --git a/OpenNest/Forms/MainForm.Nest.cs b/OpenNest/Forms/MainForm.Nest.cs index bf786edd..1693192c 100644 --- a/OpenNest/Forms/MainForm.Nest.cs +++ b/OpenNest/Forms/MainForm.Nest.cs @@ -79,8 +79,9 @@ namespace OpenNest.Forms items.Add(new ToolStripMenuItem("(no machines; see Tools > Machine Configuration)") { Enabled = false }); } - // Assigning a machine also puts every sheet on that machine's support grid and gives - // the views its cut-off settings; plates' own cutting parameters are kept. + // Assigning a machine also puts every sheet on that machine's support grid, gives the + // views its cut-off settings and the nest its spacing; plates' own cutting parameters are + // kept, and of the sheets only an untouched trailing one takes the spacing. private void AssignMachine(MachineConfig machine) { var editForm = activeForm; @@ -101,6 +102,7 @@ namespace OpenNest.Forms MachineSettings.AssignMachine(nest, machine); foreach (var view in views) MachineSettings.ApplyCutOffDefaults(view, nest); + editForm.PlateManager.ChangePlateDefaults(() => MachineCuttingDefaults.ApplySpacingTo(nest, machine)); }); foreach (var view in views) view.Invalidate(); diff --git a/OpenNest/Forms/MainForm.Plate.cs b/OpenNest/Forms/MainForm.Plate.cs index f361fccd..6e46e9c3 100644 --- a/OpenNest/Forms/MainForm.Plate.cs +++ b/OpenNest/Forms/MainForm.Plate.cs @@ -9,7 +9,12 @@ namespace OpenNest.Forms { #region Plate Menu Events - private void SetAsNestDefault_Click(object sender, EventArgs e) => WithActive(f => f.SetCurrentPlateAsNestDefault()); + // Both this nest's next plates and every new nest start from the current plate. + private void SetAsNestDefault_Click(object sender, EventArgs e) + { + WithActive(f => f.SetCurrentPlateAsNestDefault()); + SaveCurrentAsDefaults_Click(sender, e); + } private void AddPlate_Click(object sender, EventArgs e) { diff --git a/OpenNest/Forms/MainForm.Tools.cs b/OpenNest/Forms/MainForm.Tools.cs index fb688e80..02d7e2b5 100644 --- a/OpenNest/Forms/MainForm.Tools.cs +++ b/OpenNest/Forms/MainForm.Tools.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Windows.Forms; using OpenNest.Actions; using OpenNest.Data; @@ -185,20 +184,7 @@ namespace OpenNest.Forms using (var form = new NestDefaultsForm(defaults, MachineSettings.LoadMachines(), SharedSettings.NestDefaultsSource())) { if (form.ShowDialog(this) == DialogResult.OK) - SaveNestDefaults(form.GetDefaults()); - } - } - - private void SaveNestDefaults(NestDefaults defaults) - { - try - { - MachineSettings.SaveNestDefaults(defaults); - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) - { - MessageBox.Show(this, "The nest defaults could not be saved.\n\n" + ex.Message, "Nest Defaults", - MessageBoxButtons.OK, MessageBoxIcon.Error); + MachineSettings.SaveNestDefaults(this, form.GetDefaults()); } } @@ -207,10 +193,8 @@ namespace OpenNest.Forms if (activeForm == null) return; - var defaults = NestDefaults.FromPlate(activeForm.Nest.Units, activeForm.PlateView.Plate); - // The nest's machine becomes the default; a nest without one keeps the saved choice. - defaults.MachineId = activeForm.Nest.Machine?.Id ?? MachineSettings.LoadSavedNestDefaults(out _).MachineId; - SaveNestDefaults(defaults); + MachineSettings.SaveNestDefaults(this, NestDefaults.FromPlate(activeForm.Nest.Units, activeForm.PlateView.Plate), + activeForm.Nest.Machine); } private void MachineConfig_Click(object sender, EventArgs e) diff --git a/OpenNest/Forms/NewNestFactory.cs b/OpenNest/Forms/NewNestFactory.cs index dd5735ef..a1973adc 100644 --- a/OpenNest/Forms/NewNestFactory.cs +++ b/OpenNest/Forms/NewNestFactory.cs @@ -19,13 +19,14 @@ namespace OpenNest.Forms { private static readonly TimeSpan ServerTimeout = TimeSpan.FromSeconds(3); - /// - /// The new nest's units, with the saved plate defaults converted to them (a drawing set - /// release group drawn in other units); null uses the defaults as saved. + /// + /// The drawing set release group the nest is for: its units, with the saved plate + /// defaults converted to them, its material and thickness, and the machine's spacing for + /// those. Null uses the defaults as saved. /// - public static async Task CreateAsync(IWin32Window owner, Units? units = null) + public static async Task CreateAsync(IWin32Window owner, SetReleaseGroup release = null) { - var nest = CreateDefaultNest(owner, units); + var nest = CreateDefaultNest(release); var now = DateTime.Now; nest.DateCreated = now; @@ -34,15 +35,21 @@ namespace OpenNest.Forms return nest; } - private static Nest CreateDefaultNest(IWin32Window owner, Units? units) + private static Nest CreateDefaultNest(SetReleaseGroup release) { var nest = new Nest(); var defaults = MachineSettings.LoadNestDefaults(); - (units is { } converted ? defaults.ConvertedTo(converted) : defaults).ApplyTo(nest); + (release != null ? defaults.ConvertedTo(release.Units) : defaults).ApplyTo(nest); // After the defaults so the rail and grid are converted to the new nest's units. var machine = MachineSettings.DefaultMachine(defaults, MachineSettings.LoadMachines()); MachineSettings.LoadClampSettings(machine).ApplyToNewNest(nest); MachineSettings.AssignMachine(nest, machine); + if (release != null) + { + nest.Material = new Material(release.MaterialName, release.MaterialGrade); + nest.Thickness = release.Thickness; + MachineCuttingDefaults.ApplySpacingTo(nest, machine); + } return nest; } diff --git a/OpenNest/Forms/SavedNestsForm.cs b/OpenNest/Forms/SavedNestsForm.cs index 54f8898c..b78e97bc 100644 --- a/OpenNest/Forms/SavedNestsForm.cs +++ b/OpenNest/Forms/SavedNestsForm.cs @@ -872,7 +872,7 @@ public sealed class SavedNestsForm : Form } // Same default propagation as Nest > Nest Info in the open editor. using var manager = new PlateManager(nest); - manager.ChangePlateDefaults(() => dialog.SaveNestInfo(nest)); + MachineSettings.SaveNestInfo(manager, dialog, nest); nest.DateLastModified = DateTime.Now; summaryLabel.Text = "Saving nest information..."; var bytes = await Task.Run(() => diff --git a/docs/machine-cutting-defaults.md b/docs/machine-cutting-defaults.md index 07459b4f..ff775efd 100644 --- a/docs/machine-cutting-defaults.md +++ b/docs/machine-cutting-defaults.md @@ -1,7 +1,7 @@ # Machine cutting defaults A nest's assigned machine (`Nest.Machine`, see [nest file format](nest-file-format.md)) -supplies the starting lead-ins, lead-outs, kerf and cut-off settings for the nest's +supplies the starting lead-ins, lead-outs, kerf, cut-off settings and spacing for the nest's material and thickness, from that machine's Tools > Machine Configuration entry (in Database mode, the server's [shared machines](shared-settings.md) once it holds them). `MachineCuttingDefaults` (OpenNest.Data) holds the mapping; the desktop applies it. @@ -47,6 +47,16 @@ which comes from the thickness). Types map as follows: Lead-outs map None, Line (length, angle) and Arc (radius); any other type is none. +## Spacing + +Each thickness can set a part spacing and an edge spacing (the Spacing tab), each optional: +one left unset stays at the nest defaults' value. The machine's spacing replaces the nest's +plate defaults when Nest > Edit or Nest Information changes the material, grade or thickness +(a spacing edited in the same dialog stays), when Nest > Machine changes the machine, and when a +drawing set release creates a nest. BOM import starts each group's row from it. File > New has +no material yet, so a new nest starts from the nest defaults. Plates are kept as they are, except +an untouched trailing empty sheet, which follows the plate defaults. + ## Cut-offs The plate view's part clearance, overtravel, minimum segment length and direction come from diff --git a/docs/shared-settings.md b/docs/shared-settings.md index f749735f..3bc5b867 100644 --- a/docs/shared-settings.md +++ b/docs/shared-settings.md @@ -13,6 +13,10 @@ building nests and posts the same way: File mode, the console, MCP and API front ends use only this PC's copies. +Nest defaults are saved by Tools > Nest Defaults, Tools > Save Current Plate as Defaults, +Plate > Set as Default, and Nest > Edit's Defaults tab with "Use these defaults for new nests" +checked. Without that check the tab changes only the open nest. + ## Which copy is used Each kind is read from the server once the server holds it, and from this PC's copy until