namespace CutList.Core.Nesting { /// /// Best-Fit Decreasing bin packing engine. /// Places each item in the bin with the least remaining space that can still fit it. /// This is a stateless engine - all state is passed via PackingRequest. /// public class BestFitEngine : IEngine { /// /// Packs items into bins using the Best-Fit Decreasing algorithm. /// public PackResult Pack(PackingRequest request) { var result = new PackResult(); var items = request.Items.OrderByDescending(i => i.Length).ToList(); var bins = new List(); // Filter oversized items var oversizedItems = items.Where(i => i.Length > request.StockLength).ToList(); foreach (var item in oversizedItems) { items.Remove(item); result.AddItemNotUsed(item); } // Pack remaining items using best-fit foreach (var item in items) { if (!TryFindBestBin(bins, item.Length, out var bestBin)) { if (bins.Count < request.MaxBinCount) { bestBin = CreateBin(request); bins.Add(bestBin); } } if (bestBin != null) { bestBin.AddItem(item); } else { result.AddItemNotUsed(item); } } // Sort bins by utilization var sortedBins = bins .OrderByDescending(b => b.Utilization) .ThenBy(b => b.Items.Count); result.AddBins(sortedBins); return result; } private static Bin CreateBin(PackingRequest request) { return new Bin(request.StockLength) { Spacing = request.Spacing }; } private static bool TryFindBestBin(IEnumerable bins, double length, out Bin? found) { found = null; foreach (var bin in bins) { if (bin.RemainingLength < length) continue; if (found == null || bin.RemainingLength < found.RemainingLength) { found = bin; } } return found != null; } } }