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.