diff --git a/OpenNest.Core/Nest.cs b/OpenNest.Core/Nest.cs
index cee1483..2f9d1ca 100644
--- a/OpenNest.Core/Nest.cs
+++ b/OpenNest.Core/Nest.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using OpenNest.Collections;
using OpenNest.Geometry;
@@ -38,6 +38,10 @@ namespace OpenNest
public string AssistGas { get; set; } = "";
+ public NestStatus Status { get; set; } = NestStatus.Quote;
+
+ public string MadeBy { get; set; } = "";
+
public double Thickness { get; set; }
public Material Material { get; set; }
diff --git a/OpenNest.Core/NestStatus.cs b/OpenNest.Core/NestStatus.cs
new file mode 100644
index 0000000..7dbde27
--- /dev/null
+++ b/OpenNest.Core/NestStatus.cs
@@ -0,0 +1,13 @@
+namespace OpenNest
+{
+ ///
+ /// Shop-floor workflow state of a saved nest, persisted as a string in
+ /// nest.json so unknown future values can fall back safely.
+ ///
+ public enum NestStatus
+ {
+ Quote,
+ ToBeCut,
+ HasBeenCut,
+ }
+}
diff --git a/OpenNest.IO/NestFormat.cs b/OpenNest.IO/NestFormat.cs
index d73cba6..39fa7db 100644
--- a/OpenNest.IO/NestFormat.cs
+++ b/OpenNest.IO/NestFormat.cs
@@ -24,6 +24,8 @@ namespace OpenNest.IO
public string DateLastModified { get; init; } = "";
public string Notes { get; init; } = "";
public string AssistGas { get; init; } = "";
+ public string Status { get; init; } = "Quote";
+ public string MadeBy { get; init; } = "";
public double Thickness { get; init; }
public MaterialDto Material { get; init; } = new();
public PlateDefaultsDto PlateDefaults { get; init; } = new();
diff --git a/OpenNest.IO/NestReader.cs b/OpenNest.IO/NestReader.cs
index bd9c072..40e3772 100644
--- a/OpenNest.IO/NestReader.cs
+++ b/OpenNest.IO/NestReader.cs
@@ -341,6 +341,11 @@ namespace OpenNest.IO
nest.DateLastModified = DateTime.Parse(dto.DateLastModified);
nest.Notes = dto.Notes;
nest.AssistGas = dto.AssistGas ?? "";
+ // Additive v2 fields: absent or unknown values keep the defaults.
+ nest.Status = Enum.TryParse(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)
var pd = dto.PlateDefaults;
diff --git a/OpenNest.IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs
index 2760de9..e74c83b 100644
--- a/OpenNest.IO/NestWriter.cs
+++ b/OpenNest.IO/NestWriter.cs
@@ -88,6 +88,8 @@ namespace OpenNest.IO
DateLastModified = nest.DateLastModified.ToString("o"),
Notes = nest.Notes ?? "",
AssistGas = nest.AssistGas ?? "",
+ Status = nest.Status.ToString(),
+ MadeBy = nest.MadeBy ?? "",
Thickness = nest.Thickness,
Material = new MaterialDto
{
diff --git a/OpenNest.Tests/IO/NestStatusSerializationTests.cs b/OpenNest.Tests/IO/NestStatusSerializationTests.cs
new file mode 100644
index 0000000..a1a7a9e
--- /dev/null
+++ b/OpenNest.Tests/IO/NestStatusSerializationTests.cs
@@ -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>(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>(options);
+ root[field] = JsonSerializer.Deserialize($"\"{value}\"");
+ return RewriteNestJson(archive, JsonSerializer.Serialize(root, options));
+ }
+}
diff --git a/OpenNest/Forms/EditNestInfoForm.Designer.cs b/OpenNest/Forms/EditNestInfoForm.Designer.cs
index bc86cf7..9887704 100644
--- a/OpenNest/Forms/EditNestInfoForm.Designer.cs
+++ b/OpenNest/Forms/EditNestInfoForm.Designer.cs
@@ -64,6 +64,10 @@
this.label5 = new System.Windows.Forms.Label();
this.labelMaterial = new System.Windows.Forms.Label();
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.tabPage3 = new System.Windows.Forms.TabPage();
this.notesBox = new System.Windows.Forms.TextBox();
@@ -417,16 +421,22 @@
this.tableLayoutPanel3.Controls.Add(this.textBox1, 1, 1);
this.tableLayoutPanel3.Controls.Add(this.textBox2, 1, 2);
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.Name = "tableLayoutPanel3";
- this.tableLayoutPanel3.RowCount = 7;
- 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, 14.28571F));
- 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, 14.28571F));
- 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, 14.28571F));
- this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 14.28571F));
+ this.tableLayoutPanel3.RowCount = 9;
+ 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.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.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.TabIndex = 0;
//
@@ -521,6 +531,37 @@
this.materialBox.Name = "materialBox";
this.materialBox.Size = new System.Drawing.Size(224, 24);
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
//
@@ -731,5 +772,9 @@
private System.Windows.Forms.Label label5;
private System.Windows.Forms.Label labelMaterial;
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;
}
}
\ No newline at end of file
diff --git a/OpenNest/Forms/EditNestInfoForm.cs b/OpenNest/Forms/EditNestInfoForm.cs
index 9a93770..f1d38af 100644
--- a/OpenNest/Forms/EditNestInfoForm.cs
+++ b/OpenNest/Forms/EditNestInfoForm.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Windows.Forms;
using OpenNest.Geometry;
using Timer = System.Timers.Timer;
@@ -18,6 +18,10 @@ namespace OpenNest.Forms
foreach (var name in PostProcessorMaterials.Names)
materialBox.Items.Add(name);
+ foreach (var status in Enum.GetValues())
+ statusBox.Items.Add(status);
+ statusBox.SelectedItem = NestStatus.Quote;
+
timer = new Timer
{
SynchronizingObject = this,
@@ -115,6 +119,18 @@ namespace OpenNest.Forms
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)
{
switch (units)
@@ -200,6 +216,8 @@ namespace OpenNest.Forms
DateLastModified = nest.DateLastModified;
Thickness = nest.Thickness;
MaterialName = nest.Material?.Name ?? "";
+ Status = nest.Status;
+ MadeBy = nest.MadeBy ?? "";
SizeString = nest.PlateDefaults.Size.ToString();
PartSpacing = nest.PlateDefaults.PartSpacing;
LeftSpacing = nest.PlateDefaults.EdgeSpacing.Left;
@@ -221,6 +239,8 @@ namespace OpenNest.Forms
nest.DateLastModified = DateLastModified;
nest.Thickness = Thickness;
nest.Material = new Material(MaterialName);
+ nest.Status = Status;
+ nest.MadeBy = MadeBy;
nest.PlateDefaults.Size = OpenNest.Geometry.Size.Parse(SizeString);
nest.PlateDefaults.PartSpacing = PartSpacing;
nest.PlateDefaults.EdgeSpacing = new Spacing(
diff --git a/docs/nest-file-format.md b/docs/nest-file-format.md
index 5c9d422..c094fc1 100644
--- a/docs/nest-file-format.md
+++ b/docs/nest-file-format.md
@@ -4,6 +4,14 @@
Old files remain readable. Older applications ignore the new cutting-state fields;
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/program-N` is drawing N's clean G-code program; optional