#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Threading; using OpenNest.Engine.Jobs; using OpenNest.Geometry; namespace OpenNest.Engine.NestingEngines; /// A requested part type as the stock loop sees it; addresses the /// remaining-demand array. internal sealed record SheetDemand(int Index, string Id, int Priority, double MaterialArea); /// One proposed single-sheet layout from an engine's packer. /// Material placed, summed per type in type order so equal layouts get /// bit-identical areas (placement-order rounding differs across platform math libraries). /// Bounds of the placed material, or null to price the gross sheet. internal sealed record SheetLayout( NestPlateStock Stock, IReadOnlyList<(string PartId, double X, double Y, double Rotation)> Poses, double MaterialArea, Box? Envelope); /// /// Sheet-by-sheet stock selection shared by engines that propose whole single-sheet layouts. /// Each round asks the engine for layouts of the remaining demand on every available stock and /// commits the one with the lowest estimated whole-job cost: its salvage-credited net cost /// () plus the remaining demand priced at the best net-cost-per-material /// ratio seen among the candidates. A completed layout repeats while demand and stock allow it. /// Deterministic: no clocks or randomness; the only stop besides completion is the host token. /// internal static class SheetByStock { /// Demand per type index; updated as sheets commit. Zero a type the /// engine can never place before calling. /// Layouts of the given remaining demand on one stock. Each must be the /// packer's completed search, not budget-limited output, so a repeat is safe. /// When set, only this stock is tried while it is available and /// takes a layout; the others are tried only once it cannot. public static NestJobResult Run( NestJob job, IReadOnlyList types, int[] remaining, Func, IEnumerable> pack, IProgress? progress, CancellationToken token, Action? batchSelected = null, NestPlateStock? preferred = null) { var used = job.Plates.ToDictionary(s => s.Id, _ => 0, StringComparer.Ordinal); var parts = job.Parts.ToDictionary(p => p.Id, StringComparer.Ordinal); var priority = types.ToDictionary(t => t.Id, t => t.Priority, StringComparer.Ordinal); var result = new NestJobResultBuilder(job, progress); NestJobStopReason reason; while (true) { token.ThrowIfCancellationRequested(); if (remaining.All(r => r == 0)) { reason = NestJobStopReason.NoPlacementFound; // Builder reports Completed when demand is met. break; } if (job.Options.MaxPlates is int cap && SheetsUsed(result, job) >= cap) { reason = NestJobStopReason.PlateLimitReached; break; } var trials = new List<(SheetLayout Layout, double Net)>(); if (preferred != null) Try(preferred, trials); if (trials.Count == 0) foreach (var stock in job.Plates) if (!ReferenceEquals(stock, preferred)) Try(stock, trials); if (trials.Count == 0) { var exhausted = job.Plates.Any(s => s.Quantity is int q && used[s.Id] >= q); reason = exhausted ? NestJobStopReason.StockExhausted : NestJobStopReason.NoPlacementFound; break; } var chosen = Choose(types, remaining, priority, trials); var poses = chosen.Poses; var demand = types.ToDictionary(t => t.Id, t => remaining[t.Index], StringComparer.Ordinal); var counts = poses.GroupBy(p => p.PartId, StringComparer.Ordinal) .ToDictionary(g => g.Key, g => g.Count(), StringComparer.Ordinal); var availableEntries = job.Plates.Count(s => s.Quantity == null || used[s.Id] < s.Quantity); var copies = CompletedLayoutRepeat.Limit(demand, counts, searchCompleted: true, availableEntries, chosen.Stock.Quantity - used[chosen.Stock.Id], job.Options.MaxPlates - SheetsUsed(result, job)); if (copies > 1) { try { NestJobValidator.ValidateCandidate(new PlateCandidate(poses.Select(p => new NestJobPlacement(p.PartId, 0, p.X, p.Y, p.Rotation))), chosen.Stock, demand, parts); } catch (InvalidOperationException) { // Keep the original one-sheet path if the candidate validator cannot certify // reuse (including its stricter work-area tolerance). Final job validation still applies. copies = 1; } } batchSelected?.Invoke(new RepeatBatch(chosen.Stock.Id, copies, availableEntries, SheetsUsed(result, job), false)); for (var copy = 0; copy < copies; copy++) { token.ThrowIfCancellationRequested(); result.AddSheet(chosen.Stock, poses); // Builder owns fresh placements/instance indices. } used[chosen.Stock.Id] = checked(used[chosen.Stock.Id] + copies); foreach (var type in types) if (counts.TryGetValue(type.Id, out var count)) remaining[type.Index] = checked(remaining[type.Index] - checked(count * copies)); } return result.Build(reason); void Try(NestPlateStock stock, List<(SheetLayout Layout, double Net)> trials) { token.ThrowIfCancellationRequested(); if (stock.Quantity is int available && used[stock.Id] >= available) return; progress?.Report(new NestJobProgress( NestJobStage.EvaluatingCandidate, stock.Id, SheetsUsed(result, job), SheetsUsed(result, job), 0)); foreach (var layout in pack(stock, remaining)) if (layout.Poses.Count > 0) trials.Add((layout, NetCost(job, layout))); } } public static int SheetsUsed(NestJobResultBuilder builder, NestJob job) => job.Plates.Sum(builder.SheetsUsed); /// /// Picks the layout with the lowest estimated whole-job cost. Remaining demand is priced at the /// best net-cost-per-material ratio any candidate achieved, so a sheet that finishes the job /// competes fairly with a denser partial one. Ties: more material placed, then enumeration order. /// private static SheetLayout Choose(IReadOnlyList types, int[] remaining, IReadOnlyDictionary priority, List<(SheetLayout Layout, double Net)> trials) { var demandArea = types.Sum(t => remaining[t.Index] * t.MaterialArea); var bestRatio = trials.Min(t => t.Net / System.Math.Max(t.Layout.MaterialArea, 1e-12)); return trials .Select((t, order) => (t.Layout, order, Estimate: NestJobCost.RequireFinite(t.Net + System.Math.Max(0, demandArea - t.Layout.MaterialArea) * bestRatio))) .OrderBy(t => PriorityDebt(types, remaining, priority, t.Layout)) .ThenBy(t => t.Estimate) .ThenByDescending(t => t.Layout.MaterialArea) .ThenBy(t => t.order) .First() .Layout; } /// /// Priority guard: how many instances of the most urgent (lowest-number) tier with remaining /// demand this layout leaves unplaced. Layouts are ranked on this before cost, so a cheaper sheet /// can never win by serving a later tier at the expense of an earlier one. /// private static int PriorityDebt(IReadOnlyList types, int[] remaining, IReadOnlyDictionary priority, SheetLayout layout) { var active = types.Where(t => remaining[t.Index] > 0).ToList(); if (active.Count == 0) return 0; var top = active.Min(t => t.Priority); var placed = layout.Poses.Count(p => priority[p.PartId] == top); return active.Where(t => t.Priority == top).Sum(t => remaining[t.Index]) - placed; } private static double NetCost(NestJob job, SheetLayout layout) => layout.Envelope is { } envelope ? NestJobCost.NetSheetCost(job.Options, layout.Stock, envelope) : NestJobCost.GrossSheetCost(layout.Stock); }