namespace CutList.Core.Nesting
{
///
/// Exhaustive bin packing engine that tries all possible combinations
/// to find the optimal solution. Falls back to AdvancedFitEngine for
/// item counts exceeding the threshold due to exponential complexity.
///
public class ExhaustiveFitEngine : IEngine
{
///
/// Default maximum number of items before falling back to AdvancedFitEngine.
/// Testing showed 20 items is safe (~100ms worst case), while 21+ can take seconds.
///
public const int DefaultMaxItems = 20;
private readonly IEngine _fallbackEngine;
private readonly int _maxItems;
public ExhaustiveFitEngine() : this(DefaultMaxItems)
{
}
///
/// Creates an exhaustive engine with a custom item threshold for testing.
///
/// Maximum items before falling back. Use int.MaxValue to disable fallback.
public ExhaustiveFitEngine(int maxItems)
{
_maxItems = maxItems;
_fallbackEngine = new AdvancedFitEngine();
}
public PackResult Pack(PackingRequest request)
{
// Filter oversized items first
var validItems = new List();
var oversizedItems = new List();
foreach (var item in request.Items)
{
if (item.Length > request.StockLength)
oversizedItems.Add(item);
else
validItems.Add(item);
}
// Fall back to AdvancedFit for large item counts
if (validItems.Count > _maxItems)
{
var fallbackResult = _fallbackEngine.Pack(request);
return fallbackResult;
}
// Sort items descending for better pruning
var sortedItems = validItems.OrderByDescending(i => i.Length).ToList();
// Find optimal solution using exhaustive search
var bestSolution = new SearchState
{
Bins = new List>(),
BinCount = int.MaxValue
};
var currentState = new SearchState
{
Bins = new List>(),
BinCount = 0
};
Search(sortedItems, 0, currentState, bestSolution, request);
// Build result from best solution
var result = new PackResult();
result.AddItemsNotUsed(oversizedItems);
foreach (var binItems in bestSolution.Bins)
{
var bin = new Bin(request.StockLength) { Spacing = request.Spacing };
foreach (var item in binItems.OrderByDescending(i => i.Length))
{
bin.AddItem(item);
}
result.AddBin(bin);
}
// Sort bins by utilization
var sortedBins = result.Bins
.OrderByDescending(b => b.Utilization)
.ThenBy(b => b.Items.Count)
.ToList();
var finalResult = new PackResult();
finalResult.AddItemsNotUsed(oversizedItems);
finalResult.AddBins(sortedBins);
return finalResult;
}
private void Search(
List items,
int itemIndex,
SearchState current,
SearchState best,
PackingRequest request)
{
// All items placed - check if this is better
if (itemIndex >= items.Count)
{
if (current.BinCount < best.BinCount ||
(current.BinCount == best.BinCount && GetTotalWaste(current, request) < GetTotalWaste(best, request)))
{
best.BinCount = current.BinCount;
best.Bins = current.Bins.Select(b => b.ToList()).ToList();
}
return;
}
// Pruning: if we already have more bins than best, stop
if (current.BinCount >= best.BinCount)
return;
// Respect max bin count
if (current.BinCount >= request.MaxBinCount)
return;
var item = items[itemIndex];
// Symmetry breaking: if this item has the same length as the previous item,
// only place it in bins with index >= where previous item went.
// This avoids redundant exploration of equivalent permutations.
int minBinIndex = 0;
if (itemIndex > 0 && items[itemIndex - 1].Length == item.Length)
{
minBinIndex = current.LastBinIndexUsed;
}
// Try placing in each existing bin (respecting symmetry constraint)
for (int i = minBinIndex; i < current.Bins.Count; i++)
{
var binUsed = GetBinUsedLength(current.Bins[i], request.Spacing);
var remaining = request.StockLength - binUsed;
// Item fits if adding it (with spacing) stays within tolerance
// Bin class allows going over by up to spacing amount
if (item.Length <= remaining)
{
// Place item in this bin
current.Bins[i].Add(item);
var prevBinIndex = current.LastBinIndexUsed;
current.LastBinIndexUsed = i;
Search(items, itemIndex + 1, current, best, request);
current.LastBinIndexUsed = prevBinIndex;
current.Bins[i].RemoveAt(current.Bins[i].Count - 1);
}
}
// Try placing in a new bin (if allowed)
if (current.BinCount < request.MaxBinCount && current.BinCount < best.BinCount)
{
int newBinIndex = current.Bins.Count;
current.Bins.Add(new List { item });
current.BinCount++;
var prevBinIndex = current.LastBinIndexUsed;
current.LastBinIndexUsed = newBinIndex;
Search(items, itemIndex + 1, current, best, request);
current.LastBinIndexUsed = prevBinIndex;
current.Bins.RemoveAt(current.Bins.Count - 1);
current.BinCount--;
}
}
private double GetBinUsedLength(List binItems, double spacing)
{
if (binItems.Count == 0)
return 0;
return binItems.Sum(i => i.Length) + binItems.Count * spacing;
}
private double GetTotalWaste(SearchState state, PackingRequest request)
{
double totalWaste = 0;
foreach (var bin in state.Bins)
{
var used = GetBinUsedLength(bin, request.Spacing);
totalWaste += request.StockLength - used;
}
return totalWaste;
}
private class SearchState
{
public List> Bins { get; set; } = new();
public int BinCount { get; set; }
public int LastBinIndexUsed { get; set; }
}
}
}