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:
@@ -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; }
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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<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)
|
||||
var pd = dto.PlateDefaults;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
@@ -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;
|
||||
//
|
||||
@@ -522,6 +532,37 @@
|
||||
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
|
||||
//
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -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<NestStatus>())
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user