Files
OpenNest/OpenNest.Api/NestResponse.cs
T
ajandClaude Opus 5.5 bc363cc904 feat(engine): name every unplaced part and why
NestPipelineResult.Unplaced lists each requirement's unplaced copies with a
typed NestUnplacedReason, counted from the returned placements: no stock fits
at the sampled rotations, reserved in a cutout of an unplaced frame, stock
exhausted, plate limit, no position found, or proposal not keepable. Desktop
Auto Nest, Console --autonest and MCP autonest_plate/autonest_job report them;
the API returns NestResponse.Unplaced, persisted from response schema 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:51:24 -04:00

211 lines
9.5 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Jobs;
using OpenNest.IO;
namespace OpenNest.Api;
/// <summary>Stable fulfillment metadata for one requested part identity.</summary>
public sealed record NestPartFulfillment(string PartId, int Requested, int Placed, int Unplaced);
/// <summary>Copies of one requested part identity the proposal leaves unplaced, and why.</summary>
public sealed record NestUnplacedRequestPart(string PartId, int Count, NestUnplacedReason Reason);
/// <summary>Physical-sheet usage for one stock identity.</summary>
public sealed record NestStockUsage(string StockId, int Used, int? Remaining);
/// <summary>Maps each materialized layout (Nest.Plates index) to its stock identity.
/// Its Plate.Quantity gives the physical copies; schema 3 and older used one sheet per entry.</summary>
public sealed record NestPlateStockMapping(int PlateIndex, string StockId);
/// <summary>Independent geometry validation; null on old archives means not recorded.</summary>
public enum NestValidationStatus { Valid, Invalid, Unrepresentable }
public class NestResponse
{
public const int CurrentSchemaVersion = 5;
/// <summary>Zero identifies an archive written before response metadata was versioned.</summary>
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
/// <summary>Physical sheets: sum of Plate.Quantity, not materialized layout entries.</summary>
public int SheetCount { get; init; }
/// <summary>Placed-part area divided by total materialized physical-sheet area, as a 0.0–1.0 ratio.</summary>
public double Utilization { get; init; }
public TimeSpan CutTime { get; init; }
public TimeSpan Elapsed { get; init; }
/// <summary>Null means an older archive did not record whole-job fulfillment status.</summary>
public NestJobStatus? Status { get; init; }
public NestJobStopReason? StopReason { get; init; }
/// <summary>Fulfillment status is not geometry acceptance. Review this status before using Nest.</summary>
public NestValidationStatus? ValidationStatus { get; init; }
public IReadOnlyList<string> Violations { get; init; } = [];
public IReadOnlyList<NestPartFulfillment> Fulfillment { get; init; } = [];
/// <summary>Every unplaced copy by requested part and reason; empty when everything was
/// placed. Archives before schema 5 did not record it and load empty.</summary>
public IReadOnlyList<NestUnplacedRequestPart> Unplaced { get; init; } = [];
public IReadOnlyList<NestStockUsage> StockUsage { get; init; } = [];
public IReadOnlyList<NestPlateStockMapping> PlateStockMappings { get; init; } = [];
public NestCostSummary Costs { 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 and Spacing public fields.
Converters = { new JsonStringEnumConverter() },
};
public async Task SaveAsync(string path)
{
using var fs = new FileStream(path, FileMode.Create);
using var zip = new ZipArchive(fs, ZipArchiveMode.Create);
var requestEntry = zip.CreateEntry("request.json");
await using (var stream = requestEntry.Open())
{
await JsonSerializer.SerializeAsync(stream, Request, JsonOptions);
}
// Keep persisted data versioned and detached from the live mutable Nest graph.
var responseEntry = zip.CreateEntry("response.json");
await using (var stream = responseEntry.Open())
{
await JsonSerializer.SerializeAsync(
stream,
new NestResponseArchiveDto
{
SchemaVersion = CurrentSchemaVersion,
SheetCount = SheetCount,
Utilization = Utilization,
CutTimeTicks = CutTime.Ticks,
ElapsedTicks = Elapsed.Ticks,
Status = Status,
StopReason = StopReason,
ValidationStatus = ValidationStatus,
Violations = Violations is null ? [] : new List<string>(Violations),
Fulfillment = Fulfillment is null
? []
: new List<NestPartFulfillment>(Fulfillment),
Unplaced = Unplaced is null ? [] : new List<NestUnplacedRequestPart>(Unplaced),
Costs = Costs,
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage),
PlateStockMappings = PlateStockMappings is null
? []
: new List<NestPlateStockMapping>(PlateStockMappings),
},
JsonOptions
);
}
var nestEntry = zip.CreateEntry("nest.nest");
using var nestMs = new MemoryStream();
new NestWriter(Nest) { BestFitStore = BestFitCacheStore.Shared }.Write(nestMs);
nestMs.Position = 0;
await using (var stream = nestEntry.Open())
{
await nestMs.CopyToAsync(stream);
}
}
public static async Task<NestResponse> LoadAsync(string path)
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
using var zip = new ZipArchive(fs, ZipArchiveMode.Read);
var requestEntry =
zip.GetEntry("request.json")
?? throw new InvalidOperationException("Missing request.json in .nestquote file");
NestRequest request;
await using (var stream = requestEntry.Open())
{
request =
await JsonSerializer.DeserializeAsync<NestRequest>(stream, JsonOptions)
?? throw new InvalidOperationException("Invalid request.json in .nestquote file");
}
var responseEntry =
zip.GetEntry("response.json")
?? throw new InvalidOperationException("Missing response.json in .nestquote file");
NestResponseArchiveDto archive;
var hasSchemaVersion = false;
var hasStatusMetadata = false;
await using (var stream = responseEntry.Open())
using (var document = await JsonDocument.ParseAsync(stream))
{
var root = document.RootElement;
hasSchemaVersion = root.TryGetProperty("schemaVersion", out _);
hasStatusMetadata =
root.TryGetProperty("status", out _)
|| root.TryGetProperty("stopReason", out _)
|| root.TryGetProperty("fulfillment", out _)
|| root.TryGetProperty("stockUsage", out _)
|| root.TryGetProperty("plateStockMappings", out _);
archive =
root.Deserialize<NestResponseArchiveDto>(JsonOptions)
?? throw new InvalidOperationException("Invalid response.json in .nestquote file");
}
var nestEntry =
zip.GetEntry("nest.nest")
?? throw new InvalidOperationException("Missing nest.nest in .nestquote file");
Nest nest;
using (var nestMs = new MemoryStream())
{
await using (var stream = nestEntry.Open())
{
await stream.CopyToAsync(nestMs);
}
nestMs.Position = 0;
nest = new NestReader(nestMs) { BestFitStore = BestFitCacheStore.Shared }.Read();
}
return new NestResponse
{
SchemaVersion = hasSchemaVersion ? archive.SchemaVersion : 0,
SheetCount = archive.SheetCount,
Utilization = archive.Utilization,
CutTime = TimeSpan.FromTicks(archive.CutTimeTicks),
Elapsed = TimeSpan.FromTicks(archive.ElapsedTicks),
Status = hasStatusMetadata ? archive.Status : null,
StopReason = hasStatusMetadata ? archive.StopReason : null,
ValidationStatus = archive.ValidationStatus,
Violations = archive.Violations ?? [],
Fulfillment = hasStatusMetadata ? archive.Fulfillment ?? [] : [],
Unplaced = hasSchemaVersion && archive.SchemaVersion >= 5 ? archive.Unplaced ?? [] : [],
StockUsage = hasStatusMetadata ? archive.StockUsage ?? [] : [],
PlateStockMappings = hasStatusMetadata ? archive.PlateStockMappings ?? [] : [],
Costs = archive.Costs,
Nest = nest,
Request = request,
};
}
private sealed class NestResponseArchiveDto
{
public NestCostSummary Costs { get; init; }
public int SchemaVersion { get; init; }
public int SheetCount { get; init; }
public double Utilization { get; init; }
public long CutTimeTicks { get; init; }
public long ElapsedTicks { get; init; }
public NestJobStatus? Status { get; init; }
public NestJobStopReason? StopReason { get; init; }
public NestValidationStatus? ValidationStatus { get; init; }
public List<string> Violations { get; init; } = [];
public List<NestPartFulfillment> Fulfillment { get; init; } = [];
public List<NestUnplacedRequestPart> Unplaced { get; init; } = [];
public List<NestStockUsage> StockUsage { get; init; } = [];
public List<NestPlateStockMapping> PlateStockMappings { get; init; } = [];
}
}