diff --git a/OpenNest.Api/NestResponse.cs b/OpenNest.Api/NestResponse.cs new file mode 100644 index 0000000..8f48014 --- /dev/null +++ b/OpenNest.Api/NestResponse.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text.Json; +using System.Threading.Tasks; +using OpenNest.IO; + +namespace OpenNest.Api; + +public class NestResponse +{ + public int SheetCount { get; init; } + public double Utilization { get; init; } + public TimeSpan CutTime { get; init; } + public TimeSpan Elapsed { get; init; } + public Nest Nest { get; init; } + public NestRequest Request { get; init; } + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + IncludeFields = true // Required for OpenNest.Geometry.Size (public fields) + }; + + public async Task SaveAsync(string path) + { + using var fs = new FileStream(path, FileMode.Create); + using var zip = new ZipArchive(fs, ZipArchiveMode.Create); + + // Write request.json + var requestEntry = zip.CreateEntry("request.json"); + await using (var stream = requestEntry.Open()) + { + await JsonSerializer.SerializeAsync(stream, Request, JsonOptions); + } + + // Write response.json (metrics only) + var metrics = new + { + SheetCount, + Utilization, + CutTimeTicks = CutTime.Ticks, + ElapsedTicks = Elapsed.Ticks + }; + var responseEntry = zip.CreateEntry("response.json"); + await using (var stream = responseEntry.Open()) + { + await JsonSerializer.SerializeAsync(stream, metrics, JsonOptions); + } + + // Write embedded nest.nest via NestWriter → MemoryStream → ZIP entry + var nestEntry = zip.CreateEntry("nest.nest"); + using var nestMs = new MemoryStream(); + var writer = new NestWriter(Nest); + writer.Write(nestMs); + nestMs.Position = 0; + await using (var stream = nestEntry.Open()) + { + await nestMs.CopyToAsync(stream); + } + } + + public static async Task LoadAsync(string path) + { + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read); + using var zip = new ZipArchive(fs, ZipArchiveMode.Read); + + // Read request.json + var requestEntry = zip.GetEntry("request.json"); + NestRequest request; + await using (var stream = requestEntry!.Open()) + { + request = await JsonSerializer.DeserializeAsync(stream, JsonOptions); + } + + // Read response.json + var responseEntry = zip.GetEntry("response.json"); + JsonElement metricsJson; + await using (var stream = responseEntry!.Open()) + { + metricsJson = await JsonSerializer.DeserializeAsync(stream, JsonOptions); + } + + // Read embedded nest.nest via NestReader(Stream) + var nestEntry = zip.GetEntry("nest.nest"); + Nest nest; + using (var nestMs = new MemoryStream()) + { + await using (var stream = nestEntry!.Open()) + { + await stream.CopyToAsync(nestMs); + } + nestMs.Position = 0; + var reader = new NestReader(nestMs); + nest = reader.Read(); + } + + return new NestResponse + { + SheetCount = metricsJson.GetProperty("sheetCount").GetInt32(), + Utilization = metricsJson.GetProperty("utilization").GetDouble(), + CutTime = TimeSpan.FromTicks(metricsJson.GetProperty("cutTimeTicks").GetInt64()), + Elapsed = TimeSpan.FromTicks(metricsJson.GetProperty("elapsedTicks").GetInt64()), + Nest = nest, + Request = request + }; + } +} diff --git a/OpenNest.Tests/Api/NestResponsePersistenceTests.cs b/OpenNest.Tests/Api/NestResponsePersistenceTests.cs new file mode 100644 index 0000000..5e3a026 --- /dev/null +++ b/OpenNest.Tests/Api/NestResponsePersistenceTests.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using OpenNest.Api; +using OpenNest.Geometry; + +namespace OpenNest.Tests.Api; + +public class NestResponsePersistenceTests +{ + [Fact] + public async Task SaveAsync_LoadAsync_RoundTrips() + { + var nest = new Nest("test-nest"); + var plate = new Plate(new Size(60, 120)); + nest.Plates.Add(plate); + + var request = new NestRequest + { + Parts = [new NestRequestPart { DxfPath = "test.dxf", Quantity = 5 }], + SheetSize = new Size(60, 120), + Material = "Steel", + Thickness = 0.125, + Spacing = 0.1 + }; + + var original = new NestResponse + { + SheetCount = 1, + Utilization = 0.75, + CutTime = TimeSpan.FromMinutes(12.5), + Elapsed = TimeSpan.FromSeconds(3.2), + Nest = nest, + Request = request + }; + + var path = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid()}.nestquote"); + + try + { + await original.SaveAsync(path); + var loaded = await NestResponse.LoadAsync(path); + + Assert.Equal(original.SheetCount, loaded.SheetCount); + Assert.Equal(original.Utilization, loaded.Utilization, precision: 4); + Assert.Equal(original.CutTime, loaded.CutTime); + Assert.Equal(original.Elapsed, loaded.Elapsed); + + Assert.Equal(original.Request.Material, loaded.Request.Material); + Assert.Equal(original.Request.Thickness, loaded.Request.Thickness); + Assert.Equal(original.Request.Parts.Count, loaded.Request.Parts.Count); + Assert.Equal(original.Request.Parts[0].DxfPath, loaded.Request.Parts[0].DxfPath); + Assert.Equal(original.Request.Parts[0].Quantity, loaded.Request.Parts[0].Quantity); + + Assert.NotNull(loaded.Nest); + Assert.Single(loaded.Nest.Plates); + } + finally + { + File.Delete(path); + } + } +}