feat(core,io): persist nest status and made-by in nest files

Add NestStatus (Quote/ToBeCut/HasBeenCut) plus MadeBy on Nest, written as
additive camelCase nest.json fields with PascalCase enum strings matching
the units convention. Legacy files and unknown status values fall back to
Quote. The nest info dialog gains a Status dropdown and Made By box.
This commit is contained in:
aj
2026-09-29 16:21:17 -04:00
parent b15c4cf6c6
commit ef2f9aa714
9 changed files with 282 additions and 10 deletions
+5 -1
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Collections; using OpenNest.Collections;
using OpenNest.Geometry; using OpenNest.Geometry;
@@ -38,6 +38,10 @@ namespace OpenNest
public string AssistGas { get; set; } = ""; public string AssistGas { get; set; } = "";
public NestStatus Status { get; set; } = NestStatus.Quote;
public string MadeBy { get; set; } = "";
public double Thickness { get; set; } public double Thickness { get; set; }
public Material Material { get; set; } public Material Material { get; set; }
+13
View File
@@ -0,0 +1,13 @@
namespace OpenNest
{
/// <summary>
/// Shop-floor workflow state of a saved nest, persisted as a string in
/// <c>nest.json</c> so unknown future values can fall back safely.
/// </summary>
public enum NestStatus
{
Quote,
ToBeCut,
HasBeenCut,
}
}
+2
View File
@@ -24,6 +24,8 @@ namespace OpenNest.IO
public string DateLastModified { get; init; } = ""; public string DateLastModified { get; init; } = "";
public string Notes { get; init; } = ""; public string Notes { get; init; } = "";
public string AssistGas { get; init; } = ""; public string AssistGas { get; init; } = "";
public string Status { get; init; } = "Quote";
public string MadeBy { get; init; } = "";
public double Thickness { get; init; } public double Thickness { get; init; }
public MaterialDto Material { get; init; } = new(); public MaterialDto Material { get; init; } = new();
public PlateDefaultsDto PlateDefaults { get; init; } = new(); public PlateDefaultsDto PlateDefaults { get; init; } = new();
+5
View File
@@ -341,6 +341,11 @@ namespace OpenNest.IO
nest.DateLastModified = DateTime.Parse(dto.DateLastModified); nest.DateLastModified = DateTime.Parse(dto.DateLastModified);
nest.Notes = dto.Notes; nest.Notes = dto.Notes;
nest.AssistGas = dto.AssistGas ?? ""; nest.AssistGas = dto.AssistGas ?? "";
// Additive v2 fields: absent or unknown values keep the defaults.
nest.Status = Enum.TryParse<NestStatus>(dto.Status, true, out var status)
? status
: NestStatus.Quote;
nest.MadeBy = dto.MadeBy ?? "";
// Nest-level material and thickness (fall back to PlateDefaults for old files) // Nest-level material and thickness (fall back to PlateDefaults for old files)
var pd = dto.PlateDefaults; var pd = dto.PlateDefaults;
+2
View File
@@ -88,6 +88,8 @@ namespace OpenNest.IO
DateLastModified = nest.DateLastModified.ToString("o"), DateLastModified = nest.DateLastModified.ToString("o"),
Notes = nest.Notes ?? "", Notes = nest.Notes ?? "",
AssistGas = nest.AssistGas ?? "", AssistGas = nest.AssistGas ?? "",
Status = nest.Status.ToString(),
MadeBy = nest.MadeBy ?? "",
Thickness = nest.Thickness, Thickness = nest.Thickness,
Material = new MaterialDto Material = new MaterialDto
{ {
@@ -0,0 +1,173 @@
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.Json;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Tests.IO;
public class NestStatusSerializationTests
{
[Fact]
public void RoundTrip_StatusAndMadeBy_Preserved()
{
var nest = CreateMinimalNest();
nest.Status = NestStatus.ToBeCut;
nest.MadeBy = "AJ";
var loaded = RoundTrip(nest);
Assert.Equal(NestStatus.ToBeCut, loaded.Status);
Assert.Equal("AJ", loaded.MadeBy);
}
[Fact]
public void RoundTrip_DefaultStatus_IsQuote()
{
var loaded = RoundTrip(CreateMinimalNest());
Assert.Equal(NestStatus.Quote, loaded.Status);
Assert.Equal("", loaded.MadeBy);
}
[Fact]
public void RoundTrip_HasBeenCut_Preserved()
{
var nest = CreateMinimalNest();
nest.Status = NestStatus.HasBeenCut;
var loaded = RoundTrip(nest);
Assert.Equal(NestStatus.HasBeenCut, loaded.Status);
}
[Fact]
public void Status_SerializedAsEnumString()
{
var nest = CreateMinimalNest();
nest.Status = NestStatus.ToBeCut;
nest.MadeBy = "Rex";
using var ms = new MemoryStream();
new NestWriter(nest).Write(ms);
var dtoJson = ReadNestJson(ms);
using var doc = JsonDocument.Parse(dtoJson);
// Enum strings follow the existing units convention (PascalCase).
Assert.Equal("ToBeCut", doc.RootElement.GetProperty("status").GetString());
Assert.Equal("Rex", doc.RootElement.GetProperty("madeBy").GetString());
}
[Fact]
public void LegacyFile_WithoutStatusFields_LoadsDefaults()
{
var nest = CreateMinimalNest();
using var ms = new MemoryStream();
new NestWriter(nest).Write(ms);
// Rebuild the archive with the additive fields stripped from nest.json.
var rebuilt = StripFields(ms, "status", "madeBy");
var loaded = new NestReader(rebuilt).Read();
Assert.Equal(NestStatus.Quote, loaded.Status);
Assert.Equal("", loaded.MadeBy);
}
[Fact]
public void File_WithUnknownStatusValue_LoadsQuote()
{
var nest = CreateMinimalNest();
using var ms = new MemoryStream();
new NestWriter(nest).Write(ms);
var rebuilt = ReplaceField(ms, "status", "Banana");
var loaded = new NestReader(rebuilt).Read();
Assert.Equal(NestStatus.Quote, loaded.Status);
}
private static Nest CreateMinimalNest()
{
var program = new OpenNest.CNC.Program();
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(10, 0)));
var drawing = new Drawing("Part", program);
var nest = new Nest { Name = "Test" };
nest.Drawings.Add(drawing);
var plate = new Plate(new Size(100, 100));
plate.Parts.Add(new Part(drawing, new Vector(0, 0)));
nest.Plates.Add(plate);
return nest;
}
private static Nest RoundTrip(Nest nest)
{
var ms = new MemoryStream();
new NestWriter(nest).Write(ms);
ms.Position = 0;
return new NestReader(ms).Read();
}
private static string ReadNestJson(MemoryStream archive)
{
archive.Position = 0;
using var zip = new ZipArchive(archive, ZipArchiveMode.Read, leaveOpen: true);
using var reader = new StreamReader(
zip.GetEntry("nest.json").Open(), Encoding.UTF8);
return reader.ReadToEnd();
}
private static MemoryStream RewriteNestJson(MemoryStream archive, string json)
{
var output = new MemoryStream();
archive.Position = 0;
using (var source = new ZipArchive(archive, ZipArchiveMode.Read, leaveOpen: true))
using (var target = new ZipArchive(output, ZipArchiveMode.Create, leaveOpen: true))
{
foreach (var entry in source.Entries)
{
var newEntry = target.CreateEntry(entry.FullName);
using var sourceStream = entry.Open();
using var targetStream = newEntry.Open();
if (entry.FullName == "nest.json")
{
using var writer = new StreamWriter(targetStream, Encoding.UTF8);
writer.Write(json);
}
else
{
sourceStream.CopyTo(targetStream);
}
}
}
output.Position = 0;
return output;
}
private static MemoryStream StripFields(MemoryStream archive, params string[] fields)
{
using var doc = JsonDocument.Parse(ReadNestJson(archive));
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var root = doc.RootElement.Deserialize<Dictionary<string, JsonElement>>(options);
foreach (var field in fields)
root.Remove(field);
return RewriteNestJson(archive, JsonSerializer.Serialize(root, options));
}
private static MemoryStream ReplaceField(MemoryStream archive, string field, string value)
{
using var doc = JsonDocument.Parse(ReadNestJson(archive));
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var root = doc.RootElement.Deserialize<Dictionary<string, JsonElement>>(options);
root[field] = JsonSerializer.Deserialize<JsonElement>($"\"{value}\"");
return RewriteNestJson(archive, JsonSerializer.Serialize(root, options));
}
}
+53 -8
View File
@@ -64,6 +64,10 @@
this.label5 = new System.Windows.Forms.Label(); this.label5 = new System.Windows.Forms.Label();
this.labelMaterial = new System.Windows.Forms.Label(); this.labelMaterial = new System.Windows.Forms.Label();
this.materialBox = new System.Windows.Forms.ComboBox(); this.materialBox = new System.Windows.Forms.ComboBox();
this.labelStatus = new System.Windows.Forms.Label();
this.statusBox = new System.Windows.Forms.ComboBox();
this.labelMadeBy = new System.Windows.Forms.Label();
this.madeByBox = new System.Windows.Forms.TextBox();
this.tabPage2 = new System.Windows.Forms.TabPage(); this.tabPage2 = new System.Windows.Forms.TabPage();
this.tabPage3 = new System.Windows.Forms.TabPage(); this.tabPage3 = new System.Windows.Forms.TabPage();
this.notesBox = new System.Windows.Forms.TextBox(); this.notesBox = new System.Windows.Forms.TextBox();
@@ -417,16 +421,22 @@
this.tableLayoutPanel3.Controls.Add(this.textBox1, 1, 1); this.tableLayoutPanel3.Controls.Add(this.textBox1, 1, 1);
this.tableLayoutPanel3.Controls.Add(this.textBox2, 1, 2); this.tableLayoutPanel3.Controls.Add(this.textBox2, 1, 2);
this.tableLayoutPanel3.Controls.Add(this.label5, 0, 6); this.tableLayoutPanel3.Controls.Add(this.label5, 0, 6);
this.tableLayoutPanel3.Controls.Add(this.labelStatus, 0, 7);
this.tableLayoutPanel3.Controls.Add(this.statusBox, 1, 7);
this.tableLayoutPanel3.Controls.Add(this.labelMadeBy, 0, 8);
this.tableLayoutPanel3.Controls.Add(this.madeByBox, 1, 8);
this.tableLayoutPanel3.Location = new System.Drawing.Point(6, 6); this.tableLayoutPanel3.Location = new System.Drawing.Point(6, 6);
this.tableLayoutPanel3.Name = "tableLayoutPanel3"; this.tableLayoutPanel3.Name = "tableLayoutPanel3";
this.tableLayoutPanel3.RowCount = 7; this.tableLayoutPanel3.RowCount = 9;
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 14.28571F)); this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 14.28571F)); this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 14.28571F)); this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 14.28571F)); this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 14.28571F)); this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 14.28571F)); this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 14.28571F)); this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
this.tableLayoutPanel3.Size = new System.Drawing.Size(362, 280); this.tableLayoutPanel3.Size = new System.Drawing.Size(362, 280);
this.tableLayoutPanel3.TabIndex = 0; this.tableLayoutPanel3.TabIndex = 0;
// //
@@ -522,6 +532,37 @@
this.materialBox.Size = new System.Drawing.Size(224, 24); this.materialBox.Size = new System.Drawing.Size(224, 24);
this.materialBox.TabIndex = 11; this.materialBox.TabIndex = 11;
// //
// labelStatus
//
this.labelStatus.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
this.labelStatus.AutoSize = true;
this.labelStatus.Name = "labelStatus";
this.labelStatus.TabIndex = 12;
this.labelStatus.Text = "Status :";
//
// statusBox
//
this.statusBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
this.statusBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.statusBox.FormattingEnabled = true;
this.statusBox.Name = "statusBox";
this.statusBox.TabIndex = 13;
//
// labelMadeBy
//
this.labelMadeBy.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
this.labelMadeBy.AutoSize = true;
this.labelMadeBy.Name = "labelMadeBy";
this.labelMadeBy.TabIndex = 14;
this.labelMadeBy.Text = "Made By :";
//
// madeByBox
//
this.madeByBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
this.madeByBox.Name = "madeByBox";
this.madeByBox.Size = new System.Drawing.Size(224, 22);
this.madeByBox.TabIndex = 15;
//
// label3 // label3
// //
this.label3.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); this.label3.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
@@ -731,5 +772,9 @@
private System.Windows.Forms.Label label5; private System.Windows.Forms.Label label5;
private System.Windows.Forms.Label labelMaterial; private System.Windows.Forms.Label labelMaterial;
private System.Windows.Forms.ComboBox materialBox; private System.Windows.Forms.ComboBox materialBox;
private System.Windows.Forms.Label labelStatus;
private System.Windows.Forms.ComboBox statusBox;
private System.Windows.Forms.Label labelMadeBy;
private System.Windows.Forms.TextBox madeByBox;
} }
} }
+21 -1
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
using OpenNest.Geometry; using OpenNest.Geometry;
using Timer = System.Timers.Timer; using Timer = System.Timers.Timer;
@@ -18,6 +18,10 @@ namespace OpenNest.Forms
foreach (var name in PostProcessorMaterials.Names) foreach (var name in PostProcessorMaterials.Names)
materialBox.Items.Add(name); materialBox.Items.Add(name);
foreach (var status in Enum.GetValues<NestStatus>())
statusBox.Items.Add(status);
statusBox.SelectedItem = NestStatus.Quote;
timer = new Timer timer = new Timer
{ {
SynchronizingObject = this, SynchronizingObject = this,
@@ -115,6 +119,18 @@ namespace OpenNest.Forms
set { materialBox.Text = value; } set { materialBox.Text = value; }
} }
public NestStatus Status
{
get { return statusBox.SelectedItem is NestStatus status ? status : NestStatus.Quote; }
set { statusBox.SelectedItem = value; }
}
public string MadeBy
{
get { return madeByBox.Text; }
set { madeByBox.Text = value; }
}
public void SetUnits(Units units) public void SetUnits(Units units)
{ {
switch (units) switch (units)
@@ -200,6 +216,8 @@ namespace OpenNest.Forms
DateLastModified = nest.DateLastModified; DateLastModified = nest.DateLastModified;
Thickness = nest.Thickness; Thickness = nest.Thickness;
MaterialName = nest.Material?.Name ?? ""; MaterialName = nest.Material?.Name ?? "";
Status = nest.Status;
MadeBy = nest.MadeBy ?? "";
SizeString = nest.PlateDefaults.Size.ToString(); SizeString = nest.PlateDefaults.Size.ToString();
PartSpacing = nest.PlateDefaults.PartSpacing; PartSpacing = nest.PlateDefaults.PartSpacing;
LeftSpacing = nest.PlateDefaults.EdgeSpacing.Left; LeftSpacing = nest.PlateDefaults.EdgeSpacing.Left;
@@ -221,6 +239,8 @@ namespace OpenNest.Forms
nest.DateLastModified = DateLastModified; nest.DateLastModified = DateLastModified;
nest.Thickness = Thickness; nest.Thickness = Thickness;
nest.Material = new Material(MaterialName); nest.Material = new Material(MaterialName);
nest.Status = Status;
nest.MadeBy = MadeBy;
nest.PlateDefaults.Size = OpenNest.Geometry.Size.Parse(SizeString); nest.PlateDefaults.Size = OpenNest.Geometry.Size.Parse(SizeString);
nest.PlateDefaults.PartSpacing = PartSpacing; nest.PlateDefaults.PartSpacing = PartSpacing;
nest.PlateDefaults.EdgeSpacing = new Spacing( nest.PlateDefaults.EdgeSpacing = new Spacing(
+8
View File
@@ -4,6 +4,14 @@
Old files remain readable. Older applications ignore the new cutting-state fields; Old files remain readable. Older applications ignore the new cutting-state fields;
opening and saving with an older application loses that state. opening and saving with an older application loses that state.
## Job metadata
`nest.json` carries the workflow fields shown in the nest info dialog:
`name`, `customer`, `dateCreated`, `dateLastModified`, `notes`, `material`,
`thickness`, plus `status` (`"Quote"`, `"ToBeCut"`, `"HasBeenCut"`) and `madeBy`.
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.
## Programs and placements ## Programs and placements
- `programs/program-N` is drawing N's clean G-code program; optional - `programs/program-N` is drawing N's clean G-code program; optional