diff --git a/OpenNest.Data/NestSheets.cs b/OpenNest.Data/NestSheets.cs index e0b6a5ad..263ea411 100644 --- a/OpenNest.Data/NestSheets.cs +++ b/OpenNest.Data/NestSheets.cs @@ -10,6 +10,9 @@ public sealed class NestSheets public int Revision { get; init; } + /// The nest's units, which every size in is stored in. + public Units Units { get; init; } + /// One entry per distinct sheet size, ordered by width, then length. public IReadOnlyList Sheets { get; init; } = Array.Empty(); diff --git a/OpenNest.Server.Tests/NestSheetsTests.cs b/OpenNest.Server.Tests/NestSheetsTests.cs index 3a53c497..3352cfaa 100644 --- a/OpenNest.Server.Tests/NestSheetsTests.cs +++ b/OpenNest.Server.Tests/NestSheetsTests.cs @@ -236,6 +236,53 @@ public sealed class NestSheetsTests Assert.All(sheets.Plates.Skip(1), p => Assert.Null(p.ExpectedRemnant)); } + [Fact] + public async Task Units_AreTheNestsAndSizesStayAsStored() + { + using var factory = new ServerFactory(); + using var client = factory.CreateClient(); + var nest = NestApiTests.SyntheticNest(); + nest.Units = Units.Millimeters; + var id = await Create(client, nest); + + var json = await client.GetStringAsync($"/api/nests/{id}/sheets"); + + using var document = JsonDocument.Parse(json); + Assert.Equal("millimeters", document.RootElement.GetProperty("units").GetString()); + var sheets = JsonSerializer.Deserialize(json, Options)!; + Assert.Equal(Units.Millimeters, sheets.Units); + Assert.Equal([(20.0, 30.0, 1)], Sizes(sheets)); + } + + [Fact] + public async Task InchNest_ReportsInches() + { + using var factory = new ServerFactory(); + using var client = factory.CreateClient(); + var nest = NestApiTests.SyntheticNest(); + nest.Units = Units.Inches; + + var sheets = await Sheets(client, nest); + + Assert.Equal(Units.Inches, sheets.Units); + } + + // NestReader keeps the default units when the field is absent or unrecognized. + [Theory] + [InlineData("")] + [InlineData("\"units\":\"furlongs\",")] + public async Task AbsentOrUnknownUnits_AreInches(string unitsField) + { + using var factory = new ServerFactory(); + using var client = factory.CreateClient(); + var archive = TestNests.Zip("nest.json", "{" + unitsField + "\"drawings\":[],\"plates\":[]}"); + var id = await Create(client, archive); + + var sheets = (await client.GetFromJsonAsync($"/api/nests/{id}/sheets", Options))!; + + Assert.Equal(Units.Inches, sheets.Units); + } + [Fact] public async Task UnknownNest_Returns404() { diff --git a/OpenNest.Server/NestArchive.cs b/OpenNest.Server/NestArchive.cs index 3ef27e21..27da30d8 100644 --- a/OpenNest.Server/NestArchive.cs +++ b/OpenNest.Server/NestArchive.cs @@ -106,10 +106,14 @@ internal static class NestArchive }) .ToList(); + // NestReader: absent or unknown units keep the nest default, inches. + var units = Enum.TryParse(nest.Units, ignoreCase: true, out var parsed) ? parsed : Units.Inches; + sheets = new NestSheets { NestId = record.Id, Revision = record.Revision, + Units = units, Sheets = sizes, Parts = parts, Plates = platesOut, @@ -178,6 +182,7 @@ internal static class NestArchive private sealed class NestJson { + public string? Units { get; set; } public double Thickness { get; set; } public MaterialJson? Material { get; set; } = new(); public PlateDefaultsJson? PlateDefaults { get; set; } diff --git a/docs/nest-storage.md b/docs/nest-storage.md index f4224bc2..ca165409 100644 --- a/docs/nest-storage.md +++ b/docs/nest-storage.md @@ -126,8 +126,11 @@ Restore appends a new snapshot, never rewrites its source or intervening history `/sheets` lists what a nest needs cut, for inventory holds (MaterialLedger). It skips the trailing empty new-plate slot, as `plateCount` does. Sheets group by -size with `width` the shorter side, sizes as stored (unrounded), `quantity` the -sum of those plates' quantities, ordered by width then length. `parts` has one +size with `width` the shorter side, sizes as stored (unrounded, in the nest's +`units`: `inches` or `millimeters`, inches when the archive has none or an +unknown value, as `NestReader` reads it), `quantity` the sum of those plates' +quantities, ordered by width then length. Callers that work in one unit, such +as MaterialLedger's inches, must convert using `units`. `parts` has one entry per drawing, ordered by name: `required` is the drawing's required quantity, `nested` its parts on each plate times that plate's quantity; parts whose drawing is missing are skipped.