using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; namespace OpenNest.Engine.Jobs.Remote; /// /// Every engine solve one /// made, in order: what it asked (part and stock ids with their quantities) and the raw result /// the engine returned. A remote solve sends this back instead of bound plates, so the client /// replays the pipeline against its own drawings and validates and binds the result itself /// (). Poses are kept bit for bit, nonfinite ones included, /// so the client's validation sees exactly what the engine returned. /// public sealed class NestSolveTranscript { public const int CurrentVersion = 1; private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, }; public int Version { get; set; } = CurrentVersion; public List Solves { get; set; } = new(); public string ToJson() => JsonSerializer.Serialize(this, Options); /// Reads a transcript; throws when it is not one this build understands. public static NestSolveTranscript FromJson(string json) { NestSolveTranscript transcript; try { transcript = JsonSerializer.Deserialize(json, Options); } catch (JsonException ex) { throw new FormatException("The server's nest result is not valid JSON.", ex); } if (transcript?.Solves == null || transcript.Solves.Any(s => s?.Result == null)) throw new FormatException("The server's nest result is incomplete."); if (transcript.Version != CurrentVersion) throw new FormatException($"The server's nest result is version {transcript.Version}; this build reads {CurrentVersion}."); return transcript; } internal void Add(NestJob job, NestJobResult result) => Solves.Add(new SolveJson { Request = Fingerprint(job), Result = ResultJson.From(result) }); /// What a solve was asked: the job's part and stock ids with their quantities, in order. internal static List Fingerprint(NestJob job) => job.Parts.Select(p => $"part:{p.Id}:{p.Quantity}") .Concat(job.Plates.Select(s => $"stock:{s.Id}:{s.Quantity?.ToString() ?? "*"}")) .ToList(); public sealed class SolveJson { public List Request { get; set; } = new(); public ResultJson Result { get; set; } } public sealed class ResultJson { public NestJobStatus Status { get; set; } public NestJobStopReason StopReason { get; set; } public List Plates { get; set; } = new(); public List Fulfillment { get; set; } = new(); public List StockUsage { get; set; } = new(); internal static ResultJson From(NestJobResult result) => new() { Status = result.Status, StopReason = result.StopReason, Plates = result.Plates.Select(p => new PlateJson { PlateIndex = p.PlateIndex, Stock = NestStockJson.From(p.Stock), Placements = p.Placements.ToList(), }).ToList(), Fulfillment = result.Fulfillment.ToList(), StockUsage = result.StockUsage.ToList(), }; /// The raw result, with each sheet that matches one of 's /// stock entries using that entry itself, as an engine solving the job would. internal NestJobResult ToResult(NestJob job) { var plates = new List(Plates.Count); foreach (var plate in Plates) { var stock = plate.Stock?.ToStock() ?? throw new FormatException("A result sheet has no stock."); var own = job.Plates.FirstOrDefault(s => RepeatedPlateGrouping.SameStock(s, stock)); plates.Add(new NestJobPlateResult(plate.PlateIndex, own ?? stock, plate.Placements ?? new())); } return new NestJobResult(Status, StopReason, plates, Fulfillment ?? new(), StockUsage ?? new()); } } public sealed class PlateJson { public int PlateIndex { get; set; } public NestStockJson Stock { get; set; } public List Placements { get; set; } = new(); } }