feat(machines): assign each nest a machine and keep the support grid per machine

The support grid was one shared support-grid.json, off by default, and a nest
had no machine at all, so a plasma table and a fiber laser could not have
different grids, and nothing could pick machine-specific behavior later
(lead-ins differ between laser and plasma; tabbing depends on the grid).

- Nest.Machine (id + name) persists as top-level `machine` in nest.json;
  malformed values load as none.
- MachineConfig.SupportGrid holds each machine's grid; a machine without one
  still falls back to the legacy support-grid.json.
- New and BOM import assign the Tools > Nest Defaults machine (first by name
  when none is chosen) and give the nest that machine's grid.
- Nest > Machine reassigns a nest and swaps in the new machine's grid;
  Nest > Support Grid saves machine defaults to the nest's machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-23 10:17:28 -04:00
1 parent 993555dba6
commit 9c78b4ebec
21 files changed
+513 -44

No files matched your search

+3
View File
@@ -57,6 +57,9 @@ namespace OpenNest
public Units Units { get; set; }
/// <summary>The machine this nest is cut on, or null when none is assigned.</summary>
public NestMachine Machine { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateLastModified { get; set; }
+21
View File
@@ -0,0 +1,21 @@
using System;
namespace OpenNest
{
/// <summary>
/// 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.
/// </summary>
public sealed class NestMachine
{
public NestMachine(Guid id, string name)
{
Id = id;
Name = name ?? string.Empty;
}
public Guid Id { get; }
public string Name { get; }
}
}
+6
View File
@@ -14,6 +14,12 @@ public class MachineConfig
/// <summary>Acceleration limits; null until entered, when <see cref="GetMotionOrPlaceholder"/> stands in.</summary>
public MotionConfig? Motion { get; set; }
/// <summary>
/// The support grid nests assigned this machine rest on; null until saved from
/// Nest &gt; Support Grid, when the legacy shared support-grid.json stands in.
/// </summary>
public SupportGridSettings? SupportGrid { get; set; }
public MotionConfig GetMotionOrPlaceholder() => Motion ?? MotionConfig.Placeholder(Units);
public ThicknessConfig? GetParameters(string material, double thickness) =>
+16
View File
@@ -46,6 +46,12 @@ public sealed class NestDefaults
public Spacing EdgeSpacing { get; set; } = new(1, 1, 1, 1);
/// <summary>
/// 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.
/// </summary>
public Guid? MachineId { get; set; }
/// <summary>
/// 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
@@ -0,0 +1,104 @@
using System.IO.Compression;
using System.Text;
using System.Text.Json.Nodes;
using OpenNest.Geometry;
namespace OpenNest.IO.Tests;
/// <summary>
/// The nest's assigned machine persists as top-level <c>machine</c> (<c>id</c> and <c>name</c>);
/// a nest without one writes no key, and a malformed one loads as none.
/// </summary>
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<JsonNode> 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);
}
}
+46
View File
@@ -28,6 +28,15 @@ namespace OpenNest.IO
public string AssistGas { get; init; } = "";
public string Status { get; init; } = "Quote";
public string MadeBy { get; init; } = "";
/// <summary>
/// The machine the nest is cut on. Omitted when none is assigned; older files and
/// older readers simply have none.
/// </summary>
[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; }
}
/// <summary>A machine configuration's id and the name it had when the nest was saved.</summary>
public record MachineDto
{
public Guid Id { get; init; }
public string Name { get; init; } = "";
}
/// <summary>
/// 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.
/// </summary>
public sealed class LenientMachineConverter : JsonConverter<MachineDto>
{
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();
}
}
/// <summary>
/// A support bed layout. <see cref="Type"/> names the layout: only <c>grid</c>, a regular
/// grid of contact points, so far. Lengths are in the nest's units.
+1
View File
@@ -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;
+3
View File
@@ -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
{
@@ -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);
}
}
+45
View File
@@ -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);
}
}
+3 -3
View File
@@ -415,7 +415,7 @@ namespace OpenNest.Forms
var importErrors = new List<string>();
// 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);
+51 -3
View File
@@ -79,9 +79,57 @@ namespace OpenNest.Forms
return settings;
}
/// <summary>The persisted machine support grid; never throws, and loads as off when unreadable.</summary>
public static SupportGridSettings LoadSupportGridSettings() =>
SupportGridSettings.Load(SupportGridSettings.DefaultPath);
/// <summary>
/// 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.
/// </summary>
public static MachineConfig DefaultMachine(NestDefaults defaults, IEnumerable<MachineConfig> machines)
{
var sorted = machines.OrderBy(m => m.Name, StringComparer.CurrentCultureIgnoreCase).ToList();
return sorted.FirstOrDefault(m => m.Id == defaults?.MachineId) ?? sorted.FirstOrDefault();
}
/// <summary>The configuration of <paramref name="machine"/>, or null when none is assigned or it is not on file.</summary>
public static MachineConfig FindMachine(NestMachine machine)
{
if (machine == null)
return null;
try
{
return CreateMachineProvider().GetMachine(machine.Id);
}
catch (IOException) { }
catch (UnauthorizedAccessException) { }
return null;
}
/// <summary>
/// Assigns <paramref name="machine"/> to <paramref name="nest"/> (null for none) and gives
/// every sheet that machine's support grid, or none when the machine gives nests no grid.
/// </summary>
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);
}
/// <summary>
/// The support grid of <paramref name="machine"/>; a machine without one saved yet, or no
/// machine, uses the legacy shared support-grid.json. Never throws, and loads as off when unreadable.
/// </summary>
public static SupportGridSettings LoadSupportGridSettings(MachineConfig machine) =>
machine?.SupportGrid ?? SupportGridSettings.Load(SupportGridSettings.DefaultPath);
/// <summary>Saves <paramref name="settings"/> as the support grid of the machine with <paramref name="machineId"/>.</summary>
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);
}
/// <summary>The single desktop entry point for the persisted machine clamp settings.</summary>
public static ClampSettings LoadSavedClampSettings(out ClampSettingsStatus status) =>
+9 -1
View File
@@ -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;
+57 -1
View File
@@ -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<SupportGridSettings> 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;
+5 -4
View File
@@ -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)
+40 -14
View File
@@ -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;
}
+21 -1
View File
@@ -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
/// </summary>
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;
/// <param name="machines">The machines new nests can be assigned.</param>
public NestDefaultsForm(NestDefaults defaults, IEnumerable<MachineConfig> 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<MachineChoice>().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,
};
}
+3 -3
View File
@@ -10,7 +10,7 @@ using OpenNest.Properties;
namespace OpenNest.Forms
{
/// <summary>
/// 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
/// (<see cref="NestNames"/>). In Database mode the nest server allocates the number so
/// every PC shares one sequence; when it cannot, <see cref="Settings.NestNumber"/> 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;
}
+25 -11
View File
@@ -11,13 +11,13 @@ namespace OpenNest.Forms;
/// Nest &gt; 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.
/// </summary>
public sealed class SupportGridForm : Form
{
private readonly Units units;
private readonly SupportGridSettings machineDefaults;
private readonly string settingsPath;
private readonly Action<SupportGridSettings> 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)
/// <param name="machineName">The nest's machine, or null when none is assigned.</param>
/// <param name="saveMachineDefaults">
/// Saves the grid as that machine's; null disables saving, when the nest has no machine on file.
/// </param>
public SupportGridForm(SupportBed bed, Units units, SupportGridSettings machineDefaults, string machineName,
Action<SupportGridSettings> 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);
}
}
+6
View File
@@ -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
+8 -3
View File
@@ -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