#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Threading; namespace OpenNest.Engine.Jobs; /// /// A pipeline pre-pass that offers the engine a transformed job (compound proxies standing in /// for several parts) and expands the engine's answer back to the original job's parts, which /// are then validated again. Cutout bundles and common-line pairs are the two kinds. /// internal interface IPipelinePrepass { /// The job the engine solves. NestJob EngineJob { get; } /// The violation reported when the engine's answer cannot be expanded. string ReconciliationFailure { get; } /// Expands an answer to into one for . bool TryExpand(NestJob original, NestJobResult result, CancellationToken token, out NestJobResult? expanded, out string failure); /// Copies per original requirement that an unplaced proxy left unplaced as a frame's insert. IReadOnlyDictionary ReleasedInserts(NestJobResult engineResult); } /// The accounting every pre-pass checks before trusting an engine's answer. internal static class PipelinePrepassAccounting { /// /// True when is self-consistent for : /// consecutive sheet indices, known stock and parts, unique instance indices within quantity, /// and fulfillment, stock usage and status rows that match the placements. /// internal static bool Matches(NestJob engineJob, NestJobResult result, CancellationToken token) { var parts = engineJob.Parts.ToDictionary(p => p.Id); var counts = parts.ToDictionary(p => p.Key, _ => 0); var indices = parts.ToDictionary(p => p.Key, _ => new HashSet()); var stockById = engineJob.Plates.ToDictionary(s => s.Id, StringComparer.Ordinal); var used = stockById.ToDictionary(s => s.Key, _ => 0, StringComparer.Ordinal); var expectedIndex = 0; foreach (var sheet in result.Plates) { token.ThrowIfCancellationRequested(); if (sheet.PlateIndex != expectedIndex++ || sheet.Placements.Count == 0 || !used.ContainsKey(sheet.StockId)) return false; used[sheet.StockId]++; foreach (var pose in sheet.Placements) { if (pose.PartId == null || !parts.ContainsKey(pose.PartId) || pose.InstanceIndex < 0 || !indices[pose.PartId].Add(pose.InstanceIndex) || ++counts[pose.PartId] > parts[pose.PartId].Quantity) return false; } } foreach (var row in indices) { token.ThrowIfCancellationRequested(); if (row.Value.Any(index => index >= counts[row.Key])) return false; } if (result.Fulfillment.Count != parts.Count || result.StockUsage.Count != used.Count) return false; var seenParts = new HashSet(StringComparer.Ordinal); foreach (var row in result.Fulfillment) { token.ThrowIfCancellationRequested(); if (row.PartId == null || !parts.TryGetValue(row.PartId, out var part) || !seenParts.Add(row.PartId) || row.Requested != part.Quantity || row.Placed != counts[row.PartId] || row.Unplaced != part.Quantity - counts[row.PartId]) return false; } var seenStock = new HashSet(StringComparer.Ordinal); foreach (var row in result.StockUsage) { token.ThrowIfCancellationRequested(); if (row.StockId == null || !stockById.TryGetValue(row.StockId, out var stock) || !seenStock.Add(row.StockId) || row.Used != used[row.StockId] || row.Remaining != stock.Quantity - row.Used) return false; } var complete = parts.Values.All(p => counts[p.Id] == p.Quantity); return result.Status == (complete ? NestJobStatus.Complete : NestJobStatus.Incomplete) && (!complete || result.StopReason == NestJobStopReason.Completed) && (complete || result.StopReason != NestJobStopReason.Completed); } }