diff --git a/OpenNest.Core/Nest.cs b/OpenNest.Core/Nest.cs index 6fc60299..0d3c9063 100644 --- a/OpenNest.Core/Nest.cs +++ b/OpenNest.Core/Nest.cs @@ -57,6 +57,9 @@ namespace OpenNest public Units Units { get; set; } + /// The machine this nest is cut on, or null when none is assigned. + public NestMachine Machine { get; set; } + public DateTime DateCreated { get; set; } public DateTime DateLastModified { get; set; } diff --git a/OpenNest.Core/NestMachine.cs b/OpenNest.Core/NestMachine.cs new file mode 100644 index 00000000..20034942 --- /dev/null +++ b/OpenNest.Core/NestMachine.cs @@ -0,0 +1,21 @@ +using System; + +namespace OpenNest +{ + /// + /// The machine a nest is cut on: the id of its machine configuration plus the name it had, + /// so a nest still says which machine it was made for on a PC without that configuration. + /// + public sealed class NestMachine + { + public NestMachine(Guid id, string name) + { + Id = id; + Name = name ?? string.Empty; + } + + public Guid Id { get; } + + public string Name { get; } + } +} diff --git a/OpenNest.Data/MachineConfig.cs b/OpenNest.Data/MachineConfig.cs index d8f62785..7cf1671b 100644 --- a/OpenNest.Data/MachineConfig.cs +++ b/OpenNest.Data/MachineConfig.cs @@ -14,6 +14,12 @@ public class MachineConfig /// Acceleration limits; null until entered, when stands in. public MotionConfig? Motion { get; set; } + /// + /// The support grid nests assigned this machine rest on; null until saved from + /// Nest > Support Grid, when the legacy shared support-grid.json stands in. + /// + public SupportGridSettings? SupportGrid { get; set; } + public MotionConfig GetMotionOrPlaceholder() => Motion ?? MotionConfig.Placeholder(Units); public ThicknessConfig? GetParameters(string material, double thickness) => diff --git a/OpenNest.Data/NestDefaults.cs b/OpenNest.Data/NestDefaults.cs index 5c94a7d9..16a3d404 100644 --- a/OpenNest.Data/NestDefaults.cs +++ b/OpenNest.Data/NestDefaults.cs @@ -46,6 +46,12 @@ public sealed class NestDefaults public Spacing EdgeSpacing { get; set; } = new(1, 1, 1, 1); + /// + /// The machine configuration new nests are assigned, or null to use the first machine + /// by name. An id with no configuration on file falls back the same way. + /// + public Guid? MachineId { get; set; } + /// /// The built-in defaults used when no file exists and for every field /// that is missing or invalid. Matches the historical @@ -149,6 +155,10 @@ public sealed class NestDefaults ) defaults.EdgeSpacing = new Spacing(left, bottom, right, top); + if (dto.MachineId is { ValueKind: JsonValueKind.String } machineId + && Guid.TryParse(machineId.GetString(), out var machine) && machine != Guid.Empty) + defaults.MachineId = machine; + return defaults; } @@ -166,6 +176,7 @@ public sealed class NestDefaults Quadrant = plate.Quadrant, PartSpacing = plate.PartSpacing, EdgeSpacing = plate.EdgeSpacing, + MachineId = nest.Machine?.Id, }; } @@ -203,6 +214,7 @@ public sealed class NestDefaults PartSpacing = Convert(PartSpacing), EdgeSpacing = new Spacing(Convert(EdgeSpacing.Left), Convert(EdgeSpacing.Bottom), Convert(EdgeSpacing.Right), Convert(EdgeSpacing.Top)), + MachineId = MachineId, }; } @@ -239,6 +251,7 @@ public sealed class NestDefaults Right = EdgeSpacing.Right, Top = EdgeSpacing.Top, }, + MachineId = MachineId is { } machineId ? JsonSerializer.SerializeToElement(machineId.ToString()) : null, }; var json = JsonSerializer.Serialize(dto, JsonOptions); @@ -319,6 +332,9 @@ public sealed class NestDefaults public int? Quadrant { get; init; } public double? PartSpacing { get; init; } public SpacingDto? EdgeSpacing { get; init; } + // Raw JSON for the same reason as Units. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonElement? MachineId { get; init; } } private sealed record SizeDto diff --git a/OpenNest.IO.Tests/NestMachinePersistenceTests.cs b/OpenNest.IO.Tests/NestMachinePersistenceTests.cs new file mode 100644 index 00000000..2a6aa050 --- /dev/null +++ b/OpenNest.IO.Tests/NestMachinePersistenceTests.cs @@ -0,0 +1,104 @@ +using System.IO.Compression; +using System.Text; +using System.Text.Json.Nodes; +using OpenNest.Geometry; + +namespace OpenNest.IO.Tests; + +/// +/// The nest's assigned machine persists as top-level machine (id and name); +/// a nest without one writes no key, and a malformed one loads as none. +/// +public class NestMachinePersistenceTests +{ + private static readonly NestMachine Machine = new(Guid.Parse("6f1a3c2e-5b7d-4e8f-9a0b-1c2d3e4f5a6b"), "Fiber 1"); + + private static Nest NestWith(NestMachine machine) + { + var nest = new Nest("machine") { Machine = machine }; + nest.PlateDefaults.Size = new Size(48, 96); + nest.CreatePlate(); + return nest; + } + + private static MemoryStream Write(Nest nest) + { + var stream = new MemoryStream(); + Assert.True(new NestWriter(nest).Write(stream)); + stream.Position = 0; + return stream; + } + + private static JsonNode NestJson(MemoryStream zip) + { + zip.Position = 0; + using var archive = new ZipArchive(zip, ZipArchiveMode.Read, leaveOpen: true); + using var reader = new StreamReader(archive.GetEntry("nest.json")!.Open(), Encoding.UTF8); + var json = JsonNode.Parse(reader.ReadToEnd())!; + zip.Position = 0; + return json; + } + + private static MemoryStream Rewrite(MemoryStream zip, Action edit) + { + var json = NestJson(zip); + edit(json); + var copy = new MemoryStream(); + zip.Position = 0; + zip.CopyTo(copy); + using (var archive = new ZipArchive(copy, ZipArchiveMode.Update, leaveOpen: true)) + { + archive.GetEntry("nest.json")!.Delete(); + using var writer = new StreamWriter(archive.CreateEntry("nest.json").Open(), new UTF8Encoding(false)); + writer.Write(json.ToJsonString()); + } + copy.Position = 0; + return copy; + } + + [Fact] + public void MachineRoundTrips() + { + var reopened = new NestReader(Write(NestWith(Machine))).Read(); + + Assert.Equal(Machine.Id, reopened.Machine!.Id); + Assert.Equal("Fiber 1", reopened.Machine.Name); + } + + [Fact] + public void NestWithoutMachine_WritesNoKeyAndLoadsWithoutOne() + { + var zip = Write(NestWith(null)); + + Assert.False(NestJson(zip).AsObject().ContainsKey("machine")); + Assert.Null(new NestReader(zip).Read().Machine); + } + + [Theory] + [InlineData("\"Fiber 1\"")] + [InlineData("{\"id\":42,\"name\":\"Fiber 1\"}")] + [InlineData("{\"id\":\"not-a-guid\",\"name\":\"Fiber 1\"}")] + [InlineData("{\"id\":\"00000000-0000-0000-0000-000000000000\"}")] + [InlineData("{\"name\":\"Fiber 1\"}")] + public void MalformedMachine_LoadsAsNone(string machine) + { + var zip = Rewrite(Write(NestWith(Machine)), json => json["machine"] = JsonNode.Parse(machine)); + + var reopened = new NestReader(zip).Read(); + + Assert.Null(reopened.Machine); + Assert.Equal("machine", reopened.Name); + } + + [Fact] + public void MachineWithoutName_LoadsWithEmptyName() + { + var zip = Rewrite(Write(NestWith(Machine)), + json => json["machine"] = JsonNode.Parse($"{{\"id\":\"{Machine.Id}\",\"name\":7}}")); + + var reopened = new NestReader(zip).Read(); + + Assert.Equal(Machine.Id, reopened.Machine!.Id); + Assert.Equal("", reopened.Machine.Name); + } +} diff --git a/OpenNest.IO/NestFormat.cs b/OpenNest.IO/NestFormat.cs index d61a9425..346488a7 100644 --- a/OpenNest.IO/NestFormat.cs +++ b/OpenNest.IO/NestFormat.cs @@ -28,6 +28,15 @@ namespace OpenNest.IO public string AssistGas { get; init; } = ""; public string Status { get; init; } = "Quote"; public string MadeBy { get; init; } = ""; + + /// + /// The machine the nest is cut on. Omitted when none is assigned; older files and + /// older readers simply have none. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonConverter(typeof(LenientMachineConverter))] + public MachineDto Machine { get; init; } + public double Thickness { get; init; } public MaterialDto Material { get; init; } = new(); public PlateDefaultsDto PlateDefaults { get; init; } = new(); @@ -63,6 +72,43 @@ namespace OpenNest.IO public SupportBedDto SupportBed { get; init; } } + /// A machine configuration's id and the name it had when the nest was saved. + public record MachineDto + { + public Guid Id { get; init; } + public string Name { get; init; } = ""; + } + + /// + /// Reads a malformed or wrongly typed machine, or one without a valid id, as none instead + /// of failing the whole nest. A missing or wrongly typed name reads as empty. + /// + public sealed class LenientMachineConverter : JsonConverter + { + public override MachineDto Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + var machine = document.RootElement; + if (machine.ValueKind != JsonValueKind.Object + || !machine.TryGetProperty("id", out var id) || id.ValueKind != JsonValueKind.String + || !Guid.TryParse(id.GetString(), out var guid) || guid == Guid.Empty) + return null; + var name = machine.TryGetProperty("name", out var nameElement) && nameElement.ValueKind == JsonValueKind.String + ? nameElement.GetString() + : ""; + return new MachineDto { Id = guid, Name = name }; + } + + public override void Write(Utf8JsonWriter writer, MachineDto value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString("id", value.Id); + writer.WriteString("name", value.Name ?? ""); + writer.WriteEndObject(); + } + } + /// /// A support bed layout. names the layout: only grid, a regular /// grid of contact points, so far. Lengths are in the nest's units. diff --git a/OpenNest.IO/NestReader.cs b/OpenNest.IO/NestReader.cs index fcabf2d1..4590f407 100644 --- a/OpenNest.IO/NestReader.cs +++ b/OpenNest.IO/NestReader.cs @@ -539,6 +539,7 @@ namespace OpenNest.IO ? status : NestStatus.Quote; nest.MadeBy = dto.MadeBy ?? ""; + nest.Machine = dto.Machine is { } machine ? new NestMachine(machine.Id, machine.Name) : null; // Nest-level material and thickness (fall back to PlateDefaults for old files) var pd = dto.PlateDefaults; diff --git a/OpenNest.IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs index be2109f5..5719b43d 100644 --- a/OpenNest.IO/NestWriter.cs +++ b/OpenNest.IO/NestWriter.cs @@ -105,6 +105,9 @@ namespace OpenNest.IO AssistGas = nest.AssistGas ?? "", Status = nest.Status.ToString(), MadeBy = nest.MadeBy ?? "", + Machine = nest.Machine is { } machine + ? new MachineDto { Id = machine.Id, Name = machine.Name } + : null, Thickness = nest.Thickness, Material = new MaterialDto { diff --git a/OpenNest.Tests/Data/LocalJsonProviderTests.cs b/OpenNest.Tests/Data/LocalJsonProviderTests.cs index a8d46cbb..58fb421d 100644 --- a/OpenNest.Tests/Data/LocalJsonProviderTests.cs +++ b/OpenNest.Tests/Data/LocalJsonProviderTests.cs @@ -166,4 +166,44 @@ public class LocalJsonProviderTests : IDisposable Assert.NotNull(loaded); Assert.Equal(1, loaded.SchemaVersion); } + + [Fact] + public void SaveMachine_RoundTripsTheSupportGrid() + { + var provider = CreateProvider(); + var machine = new MachineConfig + { + Name = "Plasma", + SupportGrid = new SupportGridSettings + { + Enabled = true, + Units = Units.Millimeters, + PitchX = 50, + PitchY = 40, + OffsetX = -5, + StabilityMargin = 6, + }, + }; + + provider.SaveMachine(machine); + var grid = provider.GetMachine(machine.Id)!.SupportGrid!; + + Assert.True(grid.Enabled); + Assert.Equal(Units.Millimeters, grid.Units); + Assert.Equal(50, grid.PitchX); + Assert.Equal(40, grid.PitchY); + Assert.Equal(-5, grid.OffsetX); + Assert.Equal(6, grid.StabilityMargin); + } + + [Fact] + public void MachineWithoutSupportGrid_LoadsWithNone() + { + var provider = CreateProvider(); + var machine = new MachineConfig { Name = "Laser" }; + + provider.SaveMachine(machine); + + Assert.Null(provider.GetMachine(machine.Id)!.SupportGrid); + } } diff --git a/OpenNest.Tests/Data/NestDefaultsTests.cs b/OpenNest.Tests/Data/NestDefaultsTests.cs index c7dacd0d..ae14e3d0 100644 --- a/OpenNest.Tests/Data/NestDefaultsTests.cs +++ b/OpenNest.Tests/Data/NestDefaultsTests.cs @@ -307,4 +307,49 @@ public class NestDefaultsTests : IDisposable Assert.Equal(1, loaded.PartSpacing); Assert.Equal(new Spacing(1, 1, 1, 1), loaded.EdgeSpacing); } + + [Fact] + public void MachineId_RoundTrips() + { + var id = Guid.NewGuid(); + new NestDefaults { MachineId = id }.Save(_path); + + Assert.Equal(id, NestDefaults.Load(_path).MachineId); + } + + [Fact] + public void MachineId_Unset_IsNotWrittenAndLoadsAsNull() + { + new NestDefaults().Save(_path); + + Assert.DoesNotContain("machineId", File.ReadAllText(_path)); + Assert.Null(NestDefaults.Load(_path).MachineId); + } + + [Theory] + [InlineData("42")] + [InlineData("\"not-a-guid\"")] + [InlineData("\"00000000-0000-0000-0000-000000000000\"")] + public void MachineId_Invalid_LoadsAsNullAndKeepsOtherFields(string machineId) + { + File.WriteAllText(_path, $"{{\"partSpacing\": 4, \"machineId\": {machineId}}}"); + + var loaded = NestDefaults.Load(_path, out var status); + + Assert.Equal(NestDefaultsStatus.Ok, status); + Assert.Null(loaded.MachineId); + Assert.Equal(4, loaded.PartSpacing); + } + + [Fact] + public void FromNest_AndConvertedTo_KeepTheMachine() + { + var id = Guid.NewGuid(); + var nest = new Nest { Units = Units.Inches, Machine = new NestMachine(id, "Plasma") }; + + var defaults = NestDefaults.FromNest(nest); + + Assert.Equal(id, defaults.MachineId); + Assert.Equal(id, defaults.ConvertedTo(Units.Millimeters).MachineId); + } } diff --git a/OpenNest/Forms/BomImportForm.cs b/OpenNest/Forms/BomImportForm.cs index 4c798611..193b790f 100644 --- a/OpenNest/Forms/BomImportForm.cs +++ b/OpenNest/Forms/BomImportForm.cs @@ -415,7 +415,7 @@ namespace OpenNest.Forms var importErrors = new List(); // Read when the nests are created, warning (once per session) like New does. var clamps = MachineSettings.LoadClampSettings(this); - var supportGrid = MachineSettings.LoadSupportGridSettings(); + var machine = MachineSettings.DefaultMachine(_defaults, MachineSettings.LoadMachines()); foreach (var group in groups) { @@ -439,10 +439,10 @@ namespace OpenNest.Forms jobName, nest => { - // As New does: saved defaults, then the machine clamps in those units. + // As New does: saved defaults, then the machine clamps and grid in those units. _defaults.ApplyTo(nest); clamps.ApplyToNewNest(nest); - supportGrid.ApplyToNewNest(nest); + MachineSettings.AssignMachine(nest, machine); } ); importErrors.AddRange(result.Errors); diff --git a/OpenNest/Forms/MachineSettings.cs b/OpenNest/Forms/MachineSettings.cs index 4eaadf17..62a217ab 100644 --- a/OpenNest/Forms/MachineSettings.cs +++ b/OpenNest/Forms/MachineSettings.cs @@ -79,9 +79,57 @@ namespace OpenNest.Forms return settings; } - /// The persisted machine support grid; never throws, and loads as off when unreadable. - public static SupportGridSettings LoadSupportGridSettings() => - SupportGridSettings.Load(SupportGridSettings.DefaultPath); + /// + /// The machine new nests are assigned: the nest defaults' machine when it is on file, + /// otherwise the first by name, or null when there are none. + /// + public static MachineConfig DefaultMachine(NestDefaults defaults, IEnumerable machines) + { + var sorted = machines.OrderBy(m => m.Name, StringComparer.CurrentCultureIgnoreCase).ToList(); + return sorted.FirstOrDefault(m => m.Id == defaults?.MachineId) ?? sorted.FirstOrDefault(); + } + + /// The configuration of , or null when none is assigned or it is not on file. + public static MachineConfig FindMachine(NestMachine machine) + { + if (machine == null) + return null; + try + { + return CreateMachineProvider().GetMachine(machine.Id); + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + return null; + } + + /// + /// Assigns to (null for none) and gives + /// every sheet that machine's support grid, or none when the machine gives nests no grid. + /// + public static void AssignMachine(Nest nest, MachineConfig machine) + { + nest.Machine = machine == null ? null : new NestMachine(machine.Id, machine.Name); + var grid = LoadSupportGridSettings(machine); + nest.SetSupportBed(grid.Enabled ? grid.GridFor(nest.Units) : null); + } + + /// + /// The support grid of ; a machine without one saved yet, or no + /// machine, uses the legacy shared support-grid.json. Never throws, and loads as off when unreadable. + /// + public static SupportGridSettings LoadSupportGridSettings(MachineConfig machine) => + machine?.SupportGrid ?? SupportGridSettings.Load(SupportGridSettings.DefaultPath); + + /// Saves as the support grid of the machine with . + public static void SaveSupportGridSettings(Guid machineId, SupportGridSettings settings) + { + var provider = CreateMachineProvider(); + var machine = provider.GetMachine(machineId) + ?? throw new IOException("The machine configuration is no longer on file."); + machine.SupportGrid = settings; + provider.SaveMachine(machine); + } /// The single desktop entry point for the persisted machine clamp settings. public static ClampSettings LoadSavedClampSettings(out ClampSettingsStatus status) => diff --git a/OpenNest/Forms/MainForm.Designer.cs b/OpenNest/Forms/MainForm.Designer.cs index 7e403f5d..4c76eef8 100644 --- a/OpenNest/Forms/MainForm.Designer.cs +++ b/OpenNest/Forms/MainForm.Designer.cs @@ -93,6 +93,7 @@ namespace OpenNest.Forms mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem(); mnuNestClamps = new System.Windows.Forms.ToolStripMenuItem(); mnuNestSupportGrid = new System.Windows.Forms.ToolStripMenuItem(); + mnuNestMachine = new System.Windows.Forms.ToolStripMenuItem(); mnuNestImportDrawing = new System.Windows.Forms.ToolStripMenuItem(); mnuNestShapeLibrary = new System.Windows.Forms.ToolStripMenuItem(); mnuNestDrawingLibrary = new System.Windows.Forms.ToolStripMenuItem(); @@ -600,7 +601,7 @@ namespace OpenNest.Forms // // mnuNest // - mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestClamps, mnuNestSupportGrid, mnuNestImportDrawing, mnuNestShapeLibrary, mnuNestDrawingLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestClear, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestRemoveLeadIns }); + mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestMachine, mnuNestClamps, mnuNestSupportGrid, mnuNestImportDrawing, mnuNestShapeLibrary, mnuNestDrawingLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestClear, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestRemoveLeadIns }); mnuNest.Name = "mnuNest"; mnuNest.Size = new System.Drawing.Size(43, 20); mnuNest.Text = "&Nest"; @@ -612,6 +613,12 @@ namespace OpenNest.Forms mnuNestEdit.Size = new System.Drawing.Size(205, 22); mnuNestEdit.Text = "Edit"; mnuNestEdit.Click += EditNest_Click; + // + // mnuNestMachine + // + mnuNestMachine.Name = "mnuNestMachine"; + mnuNestMachine.Size = new System.Drawing.Size(205, 22); + mnuNestMachine.Text = "Machine"; // // mnuNestClamps // @@ -1378,6 +1385,7 @@ namespace OpenNest.Forms private System.Windows.Forms.ToolStripMenuItem mnuNestEdit; private System.Windows.Forms.ToolStripMenuItem mnuNestClamps; private System.Windows.Forms.ToolStripMenuItem mnuNestSupportGrid; + private System.Windows.Forms.ToolStripMenuItem mnuNestMachine; private System.Windows.Forms.ToolStripMenuItem mnuPlateEditClamps; private System.Windows.Forms.ToolStripMenuItem mnuNestImportDrawing; private System.Windows.Forms.ToolStripMenuItem mnuNestShapeLibrary; diff --git a/OpenNest/Forms/MainForm.Nest.cs b/OpenNest/Forms/MainForm.Nest.cs index 2b6a9bee..1541630d 100644 --- a/OpenNest/Forms/MainForm.Nest.cs +++ b/OpenNest/Forms/MainForm.Nest.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Windows.Forms; using OpenNest.Collections; +using OpenNest.Data; using OpenNest.Engine.Fill; using OpenNest.Forms.Commands; @@ -45,6 +46,56 @@ namespace OpenNest.Forms mnuNestDrawingLibrary.ToolTipText = databaseMode ? "" : NestStorage.DatabaseModeOnly; // Parts can be added without a plate change, so refresh the clear state on open. mnuNestClear.Enabled = activeForm?.PlateManager.CanClearAll ?? false; + BuildMachineMenu(); + } + + // Lists the machines on file, checking the active nest's; one assigned but no longer on + // file is shown checked and disabled so the nest still says what it was made for. + private void BuildMachineMenu() + { + mnuNestMachine.DropDownItems.Clear(); + var nest = activeForm?.Nest; + mnuNestMachine.Enabled = nest != null; + if (nest == null) + { + mnuNestMachine.DropDownItems.Add(new ToolStripMenuItem("(no nest open)") { Enabled = false }); + return; + } + + var machines = MachineSettings.LoadMachines() + .OrderBy(m => m.Name, StringComparer.CurrentCultureIgnoreCase) + .ToList(); + foreach (var machine in machines) + { + var item = new ToolStripMenuItem(machine.Name) { Checked = machine.Id == nest.Machine?.Id }; + item.Click += (_, _) => AssignMachine(machine); + mnuNestMachine.DropDownItems.Add(item); + } + if (nest.Machine is { } assigned && machines.All(m => m.Id != assigned.Id)) + mnuNestMachine.DropDownItems.Add(new ToolStripMenuItem($"{assigned.Name} (not on file)") { Checked = true, Enabled = false }); + if (mnuNestMachine.DropDownItems.Count == 0) + mnuNestMachine.DropDownItems.Add(new ToolStripMenuItem("(no machines; see Tools > Machine Configuration)") { Enabled = false }); + } + + // Assigning a machine also puts every sheet on that machine's support grid. + private void AssignMachine(MachineConfig machine) + { + var editForm = activeForm; + if (editForm == null || editForm.Nest.Machine?.Id == machine.Id) + return; + + var nest = editForm.Nest; + var views = ViewsOf(nest); + if (IsPlateWorkBusy(views)) + { + MessageBox.Show(this, "Finish or cancel the current nesting or plate action first.", + "Machine", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + MachineSettings.AssignMachine(nest, machine); + foreach (var view in views) + view.Invalidate(); } private async void EditNest_Click(object sender, EventArgs e) @@ -100,7 +151,12 @@ namespace OpenNest.Forms return; } - using var form = new SupportGridForm(nest.SupportBed, nest.Units, MachineSettings.LoadSupportGridSettings()); + var machine = MachineSettings.FindMachine(nest.Machine); + Action save = machine == null + ? null + : settings => MachineSettings.SaveSupportGridSettings(machine.Id, settings); + using var form = new SupportGridForm(nest.SupportBed, nest.Units, MachineSettings.LoadSupportGridSettings(machine), + machine?.Name ?? nest.Machine?.Name, save); if (form.ShowDialog(this) != DialogResult.OK) return; diff --git a/OpenNest/Forms/MainForm.Tools.cs b/OpenNest/Forms/MainForm.Tools.cs index 49389f65..76ea4b54 100644 --- a/OpenNest/Forms/MainForm.Tools.cs +++ b/OpenNest/Forms/MainForm.Tools.cs @@ -171,7 +171,7 @@ namespace OpenNest.Forms private void NestDefaults_Click(object sender, EventArgs e) { - using (var form = new NestDefaultsForm(MachineSettings.LoadSavedNestDefaults(out _))) + using (var form = new NestDefaultsForm(MachineSettings.LoadSavedNestDefaults(out _), MachineSettings.LoadMachines())) { if (form.ShowDialog(this) == DialogResult.OK) form.GetDefaults().Save(NestDefaults.DefaultPath); @@ -183,9 +183,10 @@ namespace OpenNest.Forms if (activeForm == null) return; - NestDefaults - .FromPlate(activeForm.Nest.Units, activeForm.PlateView.Plate) - .Save(NestDefaults.DefaultPath); + 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; + defaults.Save(NestDefaults.DefaultPath); } private void MachineConfig_Click(object sender, EventArgs e) diff --git a/OpenNest/Forms/NestDefaultsForm.Designer.cs b/OpenNest/Forms/NestDefaultsForm.Designer.cs index da644595..ed98ba57 100644 --- a/OpenNest/Forms/NestDefaultsForm.Designer.cs +++ b/OpenNest/Forms/NestDefaultsForm.Designer.cs @@ -47,6 +47,8 @@ namespace OpenNest.Forms this.edgeTopBox = new OpenNest.Controls.NumericUpDown(); this.quadrantLabel = new System.Windows.Forms.Label(); this.quadrantSelect1 = new OpenNest.Controls.QuadrantSelect(); + this.machineLabel = new System.Windows.Forms.Label(); + this.machineCombo = new System.Windows.Forms.ComboBox(); this.saveButton = new System.Windows.Forms.Button(); this.cancelButton = new System.Windows.Forms.Button(); ((System.ComponentModel.ISupportInitialize)(this.widthBox)).BeginInit(); @@ -82,19 +84,22 @@ namespace OpenNest.Forms this.tableLayoutPanel1.Controls.Add(this.edgeTopBox, 1, 7); this.tableLayoutPanel1.Controls.Add(this.quadrantLabel, 0, 8); this.tableLayoutPanel1.Controls.Add(this.quadrantSelect1, 1, 8); + this.tableLayoutPanel1.Controls.Add(this.machineLabel, 0, 9); + this.tableLayoutPanel1.Controls.Add(this.machineCombo, 1, 9); this.tableLayoutPanel1.Location = new System.Drawing.Point(12, 12); this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.RowCount = 9; - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F)); - this.tableLayoutPanel1.Size = new System.Drawing.Size(410, 260); + this.tableLayoutPanel1.RowCount = 10; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(410, 288); this.tableLayoutPanel1.TabIndex = 0; // // unitsLabel @@ -286,11 +291,30 @@ namespace OpenNest.Forms this.quadrantSelect1.Size = new System.Drawing.Size(120, 28); this.quadrantSelect1.TabIndex = 17; // + // machineLabel + // + this.machineLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.machineLabel.AutoSize = true; + this.machineLabel.Location = new System.Drawing.Point(3, 263); + this.machineLabel.Name = "machineLabel"; + this.machineLabel.Size = new System.Drawing.Size(100, 16); + this.machineLabel.TabIndex = 18; + this.machineLabel.Text = "Machine:"; + // + // machineCombo + // + this.machineCombo.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.machineCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.machineCombo.Location = new System.Drawing.Point(109, 259); + this.machineCombo.Name = "machineCombo"; + this.machineCombo.Size = new System.Drawing.Size(298, 24); + this.machineCombo.TabIndex = 19; + // // saveButton // this.saveButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.saveButton.DialogResult = System.Windows.Forms.DialogResult.OK; - this.saveButton.Location = new System.Drawing.Point(243, 285); + this.saveButton.Location = new System.Drawing.Point(243, 313); this.saveButton.Name = "saveButton"; this.saveButton.Size = new System.Drawing.Size(90, 28); this.saveButton.TabIndex = 1; @@ -301,7 +325,7 @@ namespace OpenNest.Forms // this.cancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.cancelButton.Location = new System.Drawing.Point(339, 285); + this.cancelButton.Location = new System.Drawing.Point(339, 313); this.cancelButton.Name = "cancelButton"; this.cancelButton.Size = new System.Drawing.Size(90, 28); this.cancelButton.TabIndex = 2; @@ -313,7 +337,7 @@ namespace OpenNest.Forms this.AcceptButton = this.saveButton; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; this.CancelButton = this.cancelButton; - this.ClientSize = new System.Drawing.Size(434, 322); + this.ClientSize = new System.Drawing.Size(434, 350); this.Controls.Add(this.tableLayoutPanel1); this.Controls.Add(this.saveButton); this.Controls.Add(this.cancelButton); @@ -358,6 +382,8 @@ namespace OpenNest.Forms private OpenNest.Controls.NumericUpDown edgeTopBox; private System.Windows.Forms.Label quadrantLabel; private OpenNest.Controls.QuadrantSelect quadrantSelect1; + private System.Windows.Forms.Label machineLabel; + private System.Windows.Forms.ComboBox machineCombo; private System.Windows.Forms.Button saveButton; private System.Windows.Forms.Button cancelButton; } diff --git a/OpenNest/Forms/NestDefaultsForm.cs b/OpenNest/Forms/NestDefaultsForm.cs index 52955ed8..cd904e24 100644 --- a/OpenNest/Forms/NestDefaultsForm.cs +++ b/OpenNest/Forms/NestDefaultsForm.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Windows.Forms; using OpenNest.Data; using OpenNest.Geometry; @@ -11,10 +13,26 @@ namespace OpenNest.Forms /// public partial class NestDefaultsForm : Form { - public NestDefaultsForm(NestDefaults defaults) + private sealed record MachineChoice(MachineConfig Machine) + { + public override string ToString() => Machine.Name; + } + + private readonly Guid? savedMachineId; + + /// The machines new nests can be assigned. + public NestDefaultsForm(NestDefaults defaults, IEnumerable machines) { InitializeComponent(); + savedMachineId = defaults.MachineId; + var sorted = machines.OrderBy(m => m.Name, StringComparer.CurrentCultureIgnoreCase).ToList(); + foreach (var machine in sorted) + machineCombo.Items.Add(new MachineChoice(machine)); + var selected = MachineSettings.DefaultMachine(defaults, sorted); + machineCombo.SelectedItem = machineCombo.Items.Cast().FirstOrDefault(c => c.Machine == selected); + machineCombo.Enabled = machineCombo.Items.Count > 0; + unitsCombo.SelectedIndex = defaults.Units == Units.Millimeters ? 1 : 0; widthBox.Value = Clamp(widthBox, defaults.Size.Width); lengthBox.Value = Clamp(lengthBox, defaults.Size.Length); @@ -42,6 +60,8 @@ namespace OpenNest.Forms (double)edgeRightBox.Value, (double)edgeTopBox.Value ), + // With no machines on file there is nothing to choose, so keep the saved choice. + MachineId = (machineCombo.SelectedItem as MachineChoice)?.Machine.Id ?? savedMachineId, }; } diff --git a/OpenNest/Forms/NewNestFactory.cs b/OpenNest/Forms/NewNestFactory.cs index 303e09f9..e0ba1cbc 100644 --- a/OpenNest/Forms/NewNestFactory.cs +++ b/OpenNest/Forms/NewNestFactory.cs @@ -10,7 +10,7 @@ using OpenNest.Properties; namespace OpenNest.Forms { /// - /// Creates File > New nests: machine defaults and clamps, then the year's next name + /// Creates File > New nests: nest defaults, clamps, the default machine and its support grid, then the year's next name /// (). In Database mode the nest server allocates the number so /// every PC shares one sequence; when it cannot, is the /// local fallback, kept above every number the server handed out and reset each year. @@ -39,9 +39,9 @@ namespace OpenNest.Forms var nest = new Nest(); var defaults = MachineSettings.LoadNestDefaults(); (units is { } converted ? defaults.ConvertedTo(converted) : defaults).ApplyTo(nest); - // After the defaults so the rail is converted to the new nest's units. + // After the defaults so the rail and grid are converted to the new nest's units. MachineSettings.LoadClampSettings(owner).ApplyToNewNest(nest); - MachineSettings.LoadSupportGridSettings().ApplyToNewNest(nest); + MachineSettings.AssignMachine(nest, MachineSettings.DefaultMachine(defaults, MachineSettings.LoadMachines())); return nest; } diff --git a/OpenNest/Forms/SupportGridForm.cs b/OpenNest/Forms/SupportGridForm.cs index 33d0f2ab..c6f216db 100644 --- a/OpenNest/Forms/SupportGridForm.cs +++ b/OpenNest/Forms/SupportGridForm.cs @@ -11,13 +11,13 @@ namespace OpenNest.Forms; /// Nest > Support Grid...: edits the laser grid of contact points every sheet of the current /// nest rests on, used to judge whether cut parts stay level and where support tabs go (see /// docs/support-tabs.md). OK applies it to the nest; Cancel discards the edits. Save as Machine -/// Defaults writes support-grid.json immediately and is not undone by Cancel. +/// Defaults writes the grid to the nest's machine configuration immediately and is not undone by Cancel. /// public sealed class SupportGridForm : Form { private readonly Units units; private readonly SupportGridSettings machineDefaults; - private readonly string settingsPath; + private readonly Action saveMachineDefaults; private readonly CheckBox useGridBox; private readonly NumericUpDown pitchXBox; private readonly NumericUpDown pitchYBox; @@ -28,11 +28,16 @@ public sealed class SupportGridForm : Form private readonly CheckBox applyToNewNestsBox; private readonly Label savedLabel; - public SupportGridForm(SupportBed bed, Units units, SupportGridSettings machineDefaults, string settingsPath = null) + /// The nest's machine, or null when none is assigned. + /// + /// Saves the grid as that machine's; null disables saving, when the nest has no machine on file. + /// + public SupportGridForm(SupportBed bed, Units units, SupportGridSettings machineDefaults, string machineName, + Action saveMachineDefaults) { this.units = units; this.machineDefaults = machineDefaults ?? new SupportGridSettings(); - this.settingsPath = settingsPath ?? SupportGridSettings.DefaultPath; + this.saveMachineDefaults = saveMachineDefaults; var grid = bed as SupportGrid; // A nest without a grid starts from the machine defaults, so enabling it is one click. var initial = grid != null @@ -86,7 +91,12 @@ public sealed class SupportGridForm : Form AddRow(values, "Stability margin:", marginBox, unit); layout.Controls.Add(values); - var defaultsGroup = new GroupBox { Text = "Machine defaults (support-grid.json)", AutoSize = true, Padding = new Padding(8) }; + var defaultsGroup = new GroupBox + { + Text = string.IsNullOrEmpty(machineName) ? "Machine defaults (no machine assigned)" : $"Machine defaults ({machineName})", + AutoSize = true, + Padding = new Padding(8), + }; var defaultsLayout = new FlowLayoutPanel { Dock = DockStyle.Fill, @@ -96,7 +106,7 @@ public sealed class SupportGridForm : Form }; applyToNewNestsBox = new CheckBox { - Text = "Give new nests this grid", + Text = "Give nests on this machine this grid", AutoSize = true, Checked = this.machineDefaults.Enabled, }; @@ -109,8 +119,12 @@ public sealed class SupportGridForm : Form { AutoSize = true, MaximumSize = new Size(410, 0), - Text = "Save as Machine Defaults writes the file immediately; Cancel below does not undo it.", + Text = saveMachineDefaults == null + ? "Assign this nest a machine on file in Nest > Machine to save its grid." + : "Save as Machine Defaults writes the machine configuration immediately; Cancel below does not undo it.", }; + applyToNewNestsBox.Enabled = saveMachineDefaults != null; + saveDefaultsButton.Enabled = saveMachineDefaults != null; defaultsLayout.Controls.Add(applyToNewNestsBox); defaultsLayout.Controls.Add(defaultsButtons); defaultsLayout.Controls.Add(savedLabel); @@ -172,14 +186,14 @@ public sealed class SupportGridForm : Form var settings = Read(); try { - settings.Save(settingsPath); + saveMachineDefaults(settings); savedLabel.Text = settings.Enabled - ? $"Saved {DateTime.Now:t}: new nests get this grid. Cancel does not undo this." - : $"Saved {DateTime.Now:t}: new nests get no grid. Cancel does not undo this."; + ? $"Saved {DateTime.Now:t}: nests on this machine get this grid. Cancel does not undo this." + : $"Saved {DateTime.Now:t}: nests on this machine get no grid. Cancel does not undo this."; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException) { - MessageBox.Show(this, $"The machine defaults could not be saved:\n{settingsPath}\n\n{ex.Message}", + MessageBox.Show(this, $"The machine defaults could not be saved:\n\n{ex.Message}", Text, MessageBoxButtons.OK, MessageBoxIcon.Error); } } diff --git a/docs/nest-file-format.md b/docs/nest-file-format.md index dbf069a4..f4eddbfa 100644 --- a/docs/nest-file-format.md +++ b/docs/nest-file-format.md @@ -13,6 +13,12 @@ Status and made-by are additive: files written before them load as Quote with an empty maker, and an unrecognized status value likewise falls back to Quote. Builds before `Draft` therefore open a Draft nest as Quote, and saving it there keeps Quote. +`machine` optionally names the machine the nest is cut on: `id` (its machine +configuration's GUID) and `name` (the configuration's name when assigned, so the nest +still says what it was made for on a PC without that configuration). The key is omitted +when no machine is assigned. A non-object, a missing, non-string, unparseable or empty +`id` loads as no machine; a missing or non-string `name` loads as empty. + ## Programs and placements - `programs/program-N` is drawing N's clean G-code program; optional diff --git a/docs/support-tabs.md b/docs/support-tabs.md index ef8060ff..89cfdfbb 100644 --- a/docs/support-tabs.md +++ b/docs/support-tabs.md @@ -23,9 +23,14 @@ Every bed also carries: The bed belongs to the nest (`Nest.SetSupportBed`); every plate follows it, including plates added later. It is display, analysis and tab placement only: never material, spacing or a -keep-out. Nest > Support Grid... edits it; Save as Machine Defaults writes -`%APPDATA%\OpenNest\support-grid.json`, which new nests receive while it is enabled. The -nest file stores it in `plateDefaults.supportBed` (see [nest file format](nest-file-format.md)). +keep-out. Nest > Support Grid... edits it; Save as Machine Defaults writes it to the nest's +machine configuration (`MachineConfig.SupportGrid`), which nests assigned that machine receive +while it is enabled: File > New and BOM import assign the Tools > Nest Defaults machine (the +first by name when none is chosen), and Nest > Machine reassigns a nest and replaces its grid +with the new machine's (none when that machine gives nests no grid). A machine with no grid +saved yet uses the legacy shared `%APPDATA%\OpenNest\support-grid.json`. The nest file stores +the bed in `plateDefaults.supportBed` and the machine in `machine` (see +[nest file format](nest-file-format.md)). ## The support check