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