refactor: Redesign nesting engines with pipeline pattern and add exhaustive search

- Rename Result to PackResult to avoid confusion with Result<T>
- Add PackingRequest as immutable configuration replacing mutable engine state
- Add PackingStrategy enum (AdvancedFit, BestFit, Exhaustive)
- Implement pipeline pattern for composable packing steps
- Rewrite AdvancedFitEngine as stateless using pipeline
- Rewrite BestFitEngine as stateless
- Add ExhaustiveFitEngine with symmetry breaking for optimal solutions
  - Tries all bin assignments to find minimum bins
  - Falls back to AdvancedFit for >20 items
  - Configurable threshold via constructor
- Update IEngine/IEngineFactory interfaces for new pattern
- Add strategy parameter to MCP tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 15:16:40 -05:00
parent 6e8469be4b
commit b19ecf3610
22 changed files with 898 additions and 351 deletions

View File

@@ -1,92 +1,70 @@
using System.Data;
namespace CutList.Core.Nesting
{
/// <summary>
/// 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.
/// </summary>
public class BestFitEngine : IEngine
{
public double StockLength { get; set; }
public double Spacing { get; set; }
public int MaxBinCount { get; set; } = int.MaxValue;
private List<BinItem> Items { get; set; }
public Result Pack(List<BinItem> items)
/// <summary>
/// Packs items into bins using the Best-Fit Decreasing algorithm.
/// </summary>
public PackResult Pack(PackingRequest request)
{
if (StockLength <= 0)
throw new Exception("Stock length must be greater than 0");
var result = new PackResult();
var items = request.Items.OrderByDescending(i => i.Length).ToList();
var bins = new List<Bin>();
Items = items.OrderByDescending(i => i.Length).ToList();
var result = new Result();
var itemsTooLarge = Items.Where(i => i.Length > StockLength).ToList();
result.AddItemsNotUsed(itemsTooLarge);
foreach (var item in itemsTooLarge)
// Filter oversized items
var oversizedItems = items.Where(i => i.Length > request.StockLength).ToList();
foreach (var item in oversizedItems)
{
Items.Remove(item);
items.Remove(item);
result.AddItemNotUsed(item);
}
var bins = GetBins();
result.AddBins(bins);
foreach (var bin in bins)
// Pack remaining items using best-fit
foreach (var item in items)
{
foreach (var item in bin.Items)
if (!TryFindBestBin(bins, item.Length, out var bestBin))
{
Items.Remove(item);
if (bins.Count < request.MaxBinCount)
{
bestBin = CreateBin(request);
bins.Add(bestBin);
}
}
if (bestBin != null)
{
bestBin.AddItem(item);
}
else
{
result.AddItemNotUsed(item);
}
}
result.AddItemsNotUsed(Items);
// Sort bins by utilization
var sortedBins = bins
.OrderByDescending(b => b.Utilization)
.ThenBy(b => b.Items.Count);
result.AddBins(sortedBins);
return result;
}
private List<Bin> GetBins()
private static Bin CreateBin(PackingRequest request)
{
var bins = new List<Bin>();
foreach (var item in Items)
return new Bin(request.StockLength)
{
Bin best_bin;
if (!FindBin(bins.ToArray(), item.Length, out best_bin))
{
if (item.Length > StockLength)
continue;
if (bins.Count < MaxBinCount)
{
best_bin = CreateBin();
bins.Add(best_bin);
}
}
if (best_bin != null)
best_bin.AddItem(item);
}
return bins
.OrderByDescending(b => b.Utilization)
.ThenBy(b => b.Items.Count)
.ToList();
}
private Bin CreateBin()
{
var length = StockLength;
return new Bin(length)
{
Spacing = Spacing
Spacing = request.Spacing
};
}
private static bool FindBin(IEnumerable<Bin> bins, double length, out Bin found)
private static bool TryFindBestBin(IEnumerable<Bin> bins, double length, out Bin? found)
{
found = null;
@@ -95,14 +73,13 @@ namespace CutList.Core.Nesting
if (bin.RemainingLength < length)
continue;
if (found == null)
found = bin;
if (bin.RemainingLength < found.RemainingLength)
if (found == null || bin.RemainingLength < found.RemainingLength)
{
found = bin;
}
}
return (found != null);
return found != null;
}
}
}
}