#nullable enable using System.Collections.Generic; using System.Linq; using System.Threading; namespace OpenNest.Engine.RectanglePacking; /// How the next box is chosen on a sheet. internal enum PickMode { /// Every step places whichever remaining type/size scores best anywhere. Global, /// Types in (priority, largest box first) order; each fills until it no longer fits. Ordered, } /// A box type to pack. /// Demand tier; lower numbers are served first. /// Allowed packing sizes (one per orientation), already grown by any spacing. /// Ranking area: larger boxes win score ties and go first in ordered mode. internal sealed record PackType(int Priority, IReadOnlyList<(double Width, double Height)> Sizes, double Area); /// One placed box: type and size index, and its corner in sheet-local coordinates. internal readonly record struct PackPlacement(int Type, int Size, double X, double Y); /// /// Packs box types onto one under a fit rule and pick mode. Lower /// priority numbers are always served first: a higher-number type is only placed when no /// lower-number type still fits anywhere. Deterministic: ties resolve by input order. /// internal static class MaxRectsPacker { /// Remaining count per type, indexed like ; decremented as boxes are placed. public static List Pack( IReadOnlyList types, int[] left, MaxRectsSheet sheet, FitRule rule, PickMode mode, CancellationToken token ) { var placed = new List(); if (mode == PickMode.Global) PackGlobal(types, left, sheet, rule, placed, token); else PackOrdered(types, left, sheet, rule, placed, token); return placed; } private static void PackGlobal( IReadOnlyList types, int[] left, MaxRectsSheet sheet, FitRule rule, List placed, CancellationToken token ) { // Free space only shrinks, so a size that fails once never fits again on this sheet. var dead = types.Select(t => new bool[t.Sizes.Count]).ToArray(); var tiers = types.Select(t => t.Priority).Distinct().Order().ToArray(); while (true) { token.ThrowIfCancellationRequested(); (int Type, int Size, Rect Place, double P, double S)? best = null; foreach (var tier in tiers) { for (var t = 0; t < types.Count; t++) { var type = types[t]; if (type.Priority != tier || left[t] == 0) continue; for (var o = 0; o < type.Sizes.Count; o++) { if (dead[t][o]) continue; var (w, h) = type.Sizes[o]; var fit = sheet.FindBest(w, h, rule); if (fit is not { } f) { dead[t][o] = true; continue; } if ( best is not { } b || Better(f.Primary, f.Secondary, type.Area, b.P, b.S, types[b.Type].Area) ) best = (t, o, f.Place, f.Primary, f.Secondary); } } if (best != null) break; } if (best is not { } chosen) return; sheet.Place(chosen.Place); left[chosen.Type]--; placed.Add(new PackPlacement(chosen.Type, chosen.Size, chosen.Place.X, chosen.Place.Y)); } } private static void PackOrdered( IReadOnlyList types, int[] left, MaxRectsSheet sheet, FitRule rule, List placed, CancellationToken token ) { var order = Enumerable .Range(0, types.Count) .Where(t => types[t].Sizes.Count > 0) .OrderBy(t => types[t].Priority) .ThenByDescending(t => types[t].Area) .ThenBy(t => t); foreach (var t in order) { var type = types[t]; while (left[t] > 0) { token.ThrowIfCancellationRequested(); (int Size, Rect Place, double P, double S)? best = null; for (var o = 0; o < type.Sizes.Count; o++) { var (w, h) = type.Sizes[o]; var fit = sheet.FindBest(w, h, rule); if (fit is { } f && (best is not { } b || Better(f.Primary, f.Secondary, 0, b.P, b.S, 0))) best = (o, f.Place, f.Primary, f.Secondary); } if (best is not { } chosen) break; sheet.Place(chosen.Place); left[t]--; placed.Add(new PackPlacement(t, chosen.Size, chosen.Place.X, chosen.Place.Y)); } } } /// Lower score wins; on a tie the larger box goes first (strict, so input order breaks full ties). private static bool Better(double p, double s, double area, double bp, double bs, double barea) { if (p < bp - MaxRectsSheet.Eps) return true; if (p > bp + MaxRectsSheet.Eps) return false; if (s < bs - MaxRectsSheet.Eps) return true; if (s > bs + MaxRectsSheet.Eps) return false; return area > barea + MaxRectsSheet.Eps; } }