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:
@@ -1,248 +1,33 @@
|
||||
using System.Data;
|
||||
using CutList.Core.Nesting.Pipeline;
|
||||
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
/// <summary>
|
||||
/// Advanced bin packing engine using First-Fit Decreasing with optimization.
|
||||
/// This is a stateless engine that uses a composable pipeline of steps.
|
||||
/// </summary>
|
||||
public class AdvancedFitEngine : IEngine
|
||||
{
|
||||
private readonly PackingPipeline _pipeline;
|
||||
|
||||
public AdvancedFitEngine()
|
||||
{
|
||||
Bins = new List<Bin>();
|
||||
_pipeline = new PackingPipeline()
|
||||
.AddStep(new FilterOversizedItemsStep())
|
||||
.AddStep(new SortItemsDescendingStep())
|
||||
.AddStep(new FirstFitDecreasingStep())
|
||||
.AddStep(new OptimizationStep())
|
||||
.AddStep(new SortBinItemsStep())
|
||||
.AddStep(new DuplicateBinsStep())
|
||||
.AddStep(new SortBinsByUtilizationStep());
|
||||
}
|
||||
|
||||
public double StockLength { get; set; }
|
||||
|
||||
public double Spacing { get; set; }
|
||||
|
||||
public int MaxBinCount { get; set; } = int.MaxValue;
|
||||
|
||||
private List<BinItem> Items { get; set; }
|
||||
|
||||
private List<Bin> Bins { get; set; }
|
||||
|
||||
public Result Pack(List<BinItem> items)
|
||||
/// <summary>
|
||||
/// Packs items into bins using the FFD algorithm with optimization passes.
|
||||
/// </summary>
|
||||
public PackResult Pack(PackingRequest request)
|
||||
{
|
||||
if (StockLength <= 0)
|
||||
throw new Exception("Stock length must be greater than 0");
|
||||
|
||||
Items = items.OrderByDescending(i => i.Length).ToList();
|
||||
|
||||
var result = new Result();
|
||||
var itemsTooLarge = Items.Where(i => i.Length > StockLength).ToList();
|
||||
result.AddItemsNotUsed(itemsTooLarge);
|
||||
|
||||
Items.RemoveAll(item => itemsTooLarge.Contains(item));
|
||||
|
||||
CreateBins();
|
||||
|
||||
var finalItemsTooLarge = Items.Where(i => i.Length > StockLength).ToList();
|
||||
result.AddItemsNotUsed(finalItemsTooLarge);
|
||||
result.AddBins(Bins);
|
||||
|
||||
foreach (var bin in result.Bins)
|
||||
{
|
||||
foreach (var item in bin.Items)
|
||||
{
|
||||
Items.Remove(item);
|
||||
return _pipeline.Execute(request);
|
||||
}
|
||||
}
|
||||
|
||||
result.AddItemsNotUsed(Items);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CreateBins()
|
||||
{
|
||||
while (Items.Count > 0 && CanAddMoreBins())
|
||||
{
|
||||
var bin = new Bin(StockLength)
|
||||
{
|
||||
Spacing = Spacing
|
||||
};
|
||||
|
||||
FillBin(bin);
|
||||
|
||||
while (TryImprovePacking(bin))
|
||||
{
|
||||
}
|
||||
|
||||
bin.SortItems((a, b) =>
|
||||
{
|
||||
int comparison = b.Length.CompareTo(a.Length);
|
||||
return comparison != 0 ? comparison : a.Length.CompareTo(b.Length);
|
||||
});
|
||||
|
||||
Bins.Add(bin);
|
||||
|
||||
CreateDuplicateBins(bin);
|
||||
}
|
||||
|
||||
Bins = Bins
|
||||
.OrderByDescending(b => b.Utilization)
|
||||
.ThenBy(b => b.Items.Count)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private bool CanAddMoreBins()
|
||||
{
|
||||
if (MaxBinCount == -1)
|
||||
return true;
|
||||
|
||||
if (Bins.Count < MaxBinCount)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void FillBin(Bin bin)
|
||||
{
|
||||
for (int i = 0; i < Items.Count; i++)
|
||||
{
|
||||
if (bin.RemainingLength >= Items[i].Length)
|
||||
{
|
||||
bin.AddItem(Items[i]);
|
||||
Items.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateDuplicateBins(Bin originalBin)
|
||||
{
|
||||
// Count how many times the bin can be duplicated
|
||||
int duplicateCount = GetDuplicateCount(originalBin);
|
||||
|
||||
for (int i = 0; i < duplicateCount; i++)
|
||||
{
|
||||
if (!CanAddMoreBins())
|
||||
break;
|
||||
|
||||
var newBin = new Bin(originalBin.Length)
|
||||
{
|
||||
Spacing = Spacing
|
||||
};
|
||||
|
||||
foreach (var item in originalBin.Items)
|
||||
{
|
||||
var newItem = Items.FirstOrDefault(a => a.Length == item.Length);
|
||||
newBin.AddItem(newItem);
|
||||
Items.Remove(newItem);
|
||||
}
|
||||
|
||||
Bins.Add(newBin);
|
||||
}
|
||||
}
|
||||
|
||||
private int GetDuplicateCount(Bin bin)
|
||||
{
|
||||
int count = int.MaxValue;
|
||||
|
||||
foreach (var item in bin.Items.GroupBy(i => i.Length))
|
||||
{
|
||||
int availableCount = Items.Count(i => i.Length == item.Key);
|
||||
count = Math.Min(count, availableCount / item.Count());
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private bool TryImprovePacking(Bin bin)
|
||||
{
|
||||
if (bin.Items.Count == 0)
|
||||
return false;
|
||||
|
||||
if (Items.Count < 2)
|
||||
return false;
|
||||
|
||||
var lengthGroups = GroupItemsByLength(bin.Items);
|
||||
|
||||
var shortestLengthItemAvailable = Items.Min(i => i.Length);
|
||||
|
||||
foreach (var group in lengthGroups)
|
||||
{
|
||||
var minRemainingLength = bin.RemainingLength;
|
||||
var firstItem = group.Items.FirstOrDefault();
|
||||
bin.RemoveItem(firstItem);
|
||||
|
||||
for (int i = 0; i < Items.Count; i++)
|
||||
{
|
||||
var item1 = Items[i];
|
||||
|
||||
if (Items[i].Length > bin.RemainingLength)
|
||||
continue;
|
||||
|
||||
var bin2 = new Bin(bin.RemainingLength);
|
||||
bin2.Spacing = bin.Spacing;
|
||||
bin2.AddItem(item1);
|
||||
|
||||
for (int j = i + 1; j < Items.Count; j++)
|
||||
{
|
||||
if (bin2.RemainingLength < shortestLengthItemAvailable)
|
||||
break;
|
||||
|
||||
var item2 = Items[j];
|
||||
|
||||
if (item2.Length > bin2.RemainingLength)
|
||||
continue;
|
||||
|
||||
bin2.AddItem(item2);
|
||||
}
|
||||
|
||||
if (bin2.RemainingLength < minRemainingLength)
|
||||
{
|
||||
Items.Add(firstItem);
|
||||
bin.AddItems(bin2.Items);
|
||||
|
||||
foreach (var item in bin2.Items)
|
||||
{
|
||||
Items.Remove(item);
|
||||
}
|
||||
|
||||
// improvement made
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bin.AddItem(firstItem);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<LengthGroup> GroupItemsByLength(IEnumerable<BinItem> items)
|
||||
{
|
||||
var groups = new List<LengthGroup>();
|
||||
var groupMap = new Dictionary<double, LengthGroup>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!groupMap.TryGetValue(item.Length, out var group))
|
||||
{
|
||||
group = new LengthGroup
|
||||
{
|
||||
Length = item.Length,
|
||||
Items = new List<BinItem>()
|
||||
};
|
||||
groupMap[item.Length] = group;
|
||||
groups.Add(group);
|
||||
}
|
||||
group.Items.Add(item);
|
||||
}
|
||||
|
||||
groups.Sort((a, b) => b.Length.CompareTo(a.Length));
|
||||
if (groups.Count > 0)
|
||||
{
|
||||
groups.RemoveAt(0); // Remove the largest length group
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
internal class LengthGroup
|
||||
{
|
||||
public double Length { get; set; }
|
||||
public List<BinItem> Items { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
result.AddItemsNotUsed(Items);
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of IEngineFactory that creates AdvancedFitEngine instances.
|
||||
/// Can be extended to support different engine types based on configuration.
|
||||
/// Default implementation of IEngineFactory that creates packing engines
|
||||
/// based on the specified strategy.
|
||||
/// </summary>
|
||||
public class EngineFactory : IEngineFactory
|
||||
{
|
||||
public IEngine CreateEngine(double stockLength, double spacing, int maxBinCount)
|
||||
public IEngine CreateEngine(PackingStrategy strategy = PackingStrategy.AdvancedFit)
|
||||
{
|
||||
return new AdvancedFitEngine
|
||||
return strategy switch
|
||||
{
|
||||
StockLength = stockLength,
|
||||
Spacing = spacing,
|
||||
MaxBinCount = maxBinCount
|
||||
PackingStrategy.AdvancedFit => new AdvancedFitEngine(),
|
||||
PackingStrategy.BestFit => new BestFitEngine(),
|
||||
PackingStrategy.Exhaustive => new ExhaustiveFitEngine(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, "Unknown packing strategy")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
198
CutList.Core/Nesting/ExhaustiveFitEngine.cs
Normal file
198
CutList.Core/Nesting/ExhaustiveFitEngine.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ExhaustiveFitEngine : IEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Default maximum number of items before falling back to AdvancedFitEngine.
|
||||
/// Testing showed 20 items is safe (~100ms worst case), while 21+ can take seconds.
|
||||
/// </summary>
|
||||
public const int DefaultMaxItems = 20;
|
||||
|
||||
private readonly IEngine _fallbackEngine;
|
||||
private readonly int _maxItems;
|
||||
|
||||
public ExhaustiveFitEngine() : this(DefaultMaxItems)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an exhaustive engine with a custom item threshold for testing.
|
||||
/// </summary>
|
||||
/// <param name="maxItems">Maximum items before falling back. Use int.MaxValue to disable fallback.</param>
|
||||
public ExhaustiveFitEngine(int maxItems)
|
||||
{
|
||||
_maxItems = maxItems;
|
||||
_fallbackEngine = new AdvancedFitEngine();
|
||||
}
|
||||
|
||||
public PackResult Pack(PackingRequest request)
|
||||
{
|
||||
// Filter oversized items first
|
||||
var validItems = new List<BinItem>();
|
||||
var oversizedItems = new List<BinItem>();
|
||||
|
||||
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<List<BinItem>>(),
|
||||
BinCount = int.MaxValue
|
||||
};
|
||||
|
||||
var currentState = new SearchState
|
||||
{
|
||||
Bins = new List<List<BinItem>>(),
|
||||
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<BinItem> 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<BinItem> { 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<BinItem> 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<List<BinItem>> Bins { get; set; } = new();
|
||||
public int BinCount { get; set; }
|
||||
public int LastBinIndexUsed { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
namespace CutList.Core.Nesting
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for bin packing engines.
|
||||
/// Engines are stateless - all configuration is passed via PackingRequest.
|
||||
/// </summary>
|
||||
public interface IEngine
|
||||
{
|
||||
Result Pack(List<BinItem> items);
|
||||
/// <summary>
|
||||
/// Packs items into bins according to the request configuration.
|
||||
/// </summary>
|
||||
/// <param name="request">The packing configuration and items.</param>
|
||||
/// <returns>The packing result with bins and unused items.</returns>
|
||||
PackResult Pack(PackingRequest request);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,10 @@ namespace CutList.Core.Nesting
|
||||
public interface IEngineFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a configured engine instance for bin packing.
|
||||
/// Creates an engine instance for the specified packing strategy.
|
||||
/// </summary>
|
||||
/// <param name="stockLength">The length of stock bins</param>
|
||||
/// <param name="spacing">The spacing/kerf between items</param>
|
||||
/// <param name="maxBinCount">Maximum number of bins to create</param>
|
||||
/// <returns>A configured IEngine instance</returns>
|
||||
IEngine CreateEngine(double stockLength, double spacing, int maxBinCount);
|
||||
/// <param name="strategy">The packing strategy to use.</param>
|
||||
/// <returns>A configured IEngine instance.</returns>
|
||||
IEngine CreateEngine(PackingStrategy strategy = PackingStrategy.AdvancedFit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
namespace CutList.Core.Nesting
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
public class MultiBinEngine : IEngine
|
||||
/// <summary>
|
||||
/// Engine that coordinates packing across multiple bin types with different sizes.
|
||||
/// Uses priority ordering to determine which bin types to fill first.
|
||||
/// </summary>
|
||||
public class MultiBinEngine
|
||||
{
|
||||
private readonly IEngineFactory _engineFactory;
|
||||
private readonly List<MultiBin> _bins;
|
||||
|
||||
public MultiBinEngine() : this(new EngineFactory())
|
||||
{
|
||||
@@ -14,16 +19,14 @@
|
||||
_bins = new List<MultiBin>();
|
||||
}
|
||||
|
||||
private readonly List<MultiBin> _bins;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the read-only collection of bins.
|
||||
/// Gets the read-only collection of bin types.
|
||||
/// Use SetBins() to configure bins for packing.
|
||||
/// </summary>
|
||||
public IReadOnlyList<MultiBin> Bins => _bins.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the bins to use for packing.
|
||||
/// Sets the bin types to use for packing.
|
||||
/// </summary>
|
||||
public void SetBins(IEnumerable<MultiBin> bins)
|
||||
{
|
||||
@@ -34,26 +37,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The spacing/kerf between items.
|
||||
/// </summary>
|
||||
public double Spacing { get; set; }
|
||||
|
||||
public Result Pack(List<BinItem> items)
|
||||
/// <summary>
|
||||
/// The packing strategy to use for each bin type.
|
||||
/// </summary>
|
||||
public PackingStrategy Strategy { get; set; } = PackingStrategy.AdvancedFit;
|
||||
|
||||
/// <summary>
|
||||
/// Packs items across all configured bin types.
|
||||
/// </summary>
|
||||
public PackResult Pack(List<BinItem> items)
|
||||
{
|
||||
var bins = _bins
|
||||
var sortedBinTypes = _bins
|
||||
.Where(b => b.Length > 0)
|
||||
.OrderBy(b => b.Priority)
|
||||
.ThenBy(b => b.Length)
|
||||
.ToList();
|
||||
|
||||
var result = new Result();
|
||||
var result = new PackResult();
|
||||
var remainingItems = new List<BinItem>(items);
|
||||
|
||||
foreach (var bin in bins)
|
||||
{
|
||||
var engine = _engineFactory.CreateEngine(bin.Length, Spacing, bin.Quantity);
|
||||
var r = engine.Pack(remainingItems);
|
||||
var engine = _engineFactory.CreateEngine(Strategy);
|
||||
|
||||
result.AddBins(r.Bins);
|
||||
remainingItems = r.ItemsNotUsed.ToList();
|
||||
foreach (var binType in sortedBinTypes)
|
||||
{
|
||||
if (remainingItems.Count == 0)
|
||||
break;
|
||||
|
||||
var request = new PackingRequest(
|
||||
items: remainingItems,
|
||||
stockLength: binType.Length,
|
||||
spacing: Spacing,
|
||||
maxBinCount: binType.Quantity
|
||||
);
|
||||
|
||||
var packResult = engine.Pack(request);
|
||||
|
||||
result.AddBins(packResult.Bins);
|
||||
remainingItems = packResult.ItemsNotUsed.ToList();
|
||||
}
|
||||
|
||||
result.AddItemsNotUsed(remainingItems);
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
namespace CutList.Core.Nesting
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
public class Result
|
||||
/// <summary>
|
||||
/// Represents the result of a bin packing operation.
|
||||
/// Contains the packed bins and any items that could not be placed.
|
||||
/// </summary>
|
||||
public class PackResult
|
||||
{
|
||||
private readonly List<BinItem> _itemsNotUsed;
|
||||
private readonly List<Bin> _bins;
|
||||
|
||||
public Result()
|
||||
public PackResult()
|
||||
{
|
||||
_itemsNotUsed = new List<BinItem>();
|
||||
_bins = new List<Bin>();
|
||||
}
|
||||
|
||||
public PackResult(IEnumerable<Bin> bins, IEnumerable<BinItem> itemsNotUsed)
|
||||
{
|
||||
_bins = bins?.ToList() ?? new List<Bin>();
|
||||
_itemsNotUsed = itemsNotUsed?.ToList() ?? new List<BinItem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Items that could not be placed in any bin (e.g., too large for stock).
|
||||
/// </summary>
|
||||
public IReadOnlyList<BinItem> ItemsNotUsed => _itemsNotUsed;
|
||||
|
||||
/// <summary>
|
||||
/// The bins containing packed items.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Bin> Bins => _bins;
|
||||
|
||||
public void AddItemNotUsed(BinItem item)
|
||||
51
CutList.Core/Nesting/PackingRequest.cs
Normal file
51
CutList.Core/Nesting/PackingRequest.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable configuration object for a bin packing operation.
|
||||
/// Replaces mutable engine state with a clean request/response pattern.
|
||||
/// </summary>
|
||||
public sealed class PackingRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new packing request.
|
||||
/// </summary>
|
||||
/// <param name="items">The items to be packed.</param>
|
||||
/// <param name="stockLength">The length of stock bins.</param>
|
||||
/// <param name="spacing">The spacing/kerf between items (default 0).</param>
|
||||
/// <param name="maxBinCount">Maximum number of bins to create (default unlimited).</param>
|
||||
public PackingRequest(
|
||||
IReadOnlyList<BinItem> items,
|
||||
double stockLength,
|
||||
double spacing = 0,
|
||||
int maxBinCount = int.MaxValue)
|
||||
{
|
||||
if (stockLength <= 0)
|
||||
throw new ArgumentException("Stock length must be greater than 0", nameof(stockLength));
|
||||
|
||||
Items = items ?? throw new ArgumentNullException(nameof(items));
|
||||
StockLength = stockLength;
|
||||
Spacing = spacing;
|
||||
MaxBinCount = maxBinCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The items to be packed into bins.
|
||||
/// </summary>
|
||||
public IReadOnlyList<BinItem> Items { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of each stock bin.
|
||||
/// </summary>
|
||||
public double StockLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The spacing/kerf between items (blade width).
|
||||
/// </summary>
|
||||
public double Spacing { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of bins to create. Use int.MaxValue for unlimited.
|
||||
/// </summary>
|
||||
public int MaxBinCount { get; }
|
||||
}
|
||||
}
|
||||
31
CutList.Core/Nesting/PackingStrategy.cs
Normal file
31
CutList.Core/Nesting/PackingStrategy.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the bin packing strategy/algorithm to use.
|
||||
/// </summary>
|
||||
public enum PackingStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// First-Fit Decreasing with optimization passes.
|
||||
/// Sorts items by length descending, then packs using first-fit.
|
||||
/// Includes optimization to improve packing by swapping items.
|
||||
/// Best for general-purpose use cases.
|
||||
/// </summary>
|
||||
AdvancedFit,
|
||||
|
||||
/// <summary>
|
||||
/// Best-Fit Decreasing algorithm.
|
||||
/// Sorts items by length descending, then places each item in the bin
|
||||
/// with the least remaining space that can still fit it.
|
||||
/// Simpler but can be effective for certain distributions.
|
||||
/// </summary>
|
||||
BestFit,
|
||||
|
||||
/// <summary>
|
||||
/// Exhaustive search that tries all possible combinations.
|
||||
/// Guarantees optimal solution but has exponential time complexity.
|
||||
/// Automatically falls back to AdvancedFit for more than 15 items.
|
||||
/// </summary>
|
||||
Exhaustive
|
||||
}
|
||||
}
|
||||
60
CutList.Core/Nesting/Pipeline/DuplicateBinsStep.cs
Normal file
60
CutList.Core/Nesting/Pipeline/DuplicateBinsStep.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
namespace CutList.Core.Nesting.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates duplicate bins when the same packing pattern can be repeated.
|
||||
/// If there are enough remaining items to fill another bin identically,
|
||||
/// creates copies to reduce iteration overhead.
|
||||
/// </summary>
|
||||
public class DuplicateBinsStep : IPackingStep
|
||||
{
|
||||
public void Execute(PackingContext context)
|
||||
{
|
||||
// Process bins that were created before this step
|
||||
// We need a copy since we'll be adding new bins
|
||||
var originalBins = context.Bins.ToList();
|
||||
|
||||
foreach (var originalBin in originalBins)
|
||||
{
|
||||
CreateDuplicateBins(originalBin, context);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateDuplicateBins(Bin originalBin, PackingContext context)
|
||||
{
|
||||
int duplicateCount = GetDuplicateCount(originalBin, context.RemainingItems);
|
||||
|
||||
for (int i = 0; i < duplicateCount; i++)
|
||||
{
|
||||
if (!context.CanAddMoreBins())
|
||||
break;
|
||||
|
||||
var newBin = context.CreateBin();
|
||||
|
||||
foreach (var item in originalBin.Items)
|
||||
{
|
||||
var matchingItem = context.RemainingItems.FirstOrDefault(a => a.Length == item.Length);
|
||||
if (matchingItem != null)
|
||||
{
|
||||
newBin.AddItem(matchingItem);
|
||||
context.RemainingItems.Remove(matchingItem);
|
||||
}
|
||||
}
|
||||
|
||||
context.Bins.Add(newBin);
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetDuplicateCount(Bin bin, List<BinItem> remainingItems)
|
||||
{
|
||||
int count = int.MaxValue;
|
||||
|
||||
foreach (var lengthGroup in bin.Items.GroupBy(i => i.Length))
|
||||
{
|
||||
int availableCount = remainingItems.Count(i => i.Length == lengthGroup.Key);
|
||||
count = Math.Min(count, availableCount / lengthGroup.Count());
|
||||
}
|
||||
|
||||
return count == int.MaxValue ? 0 : count;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
CutList.Core/Nesting/Pipeline/FilterOversizedItemsStep.cs
Normal file
22
CutList.Core/Nesting/Pipeline/FilterOversizedItemsStep.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace CutList.Core.Nesting.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Removes items that are too large to fit in the stock length.
|
||||
/// These items are moved to the OversizedItems collection.
|
||||
/// </summary>
|
||||
public class FilterOversizedItemsStep : IPackingStep
|
||||
{
|
||||
public void Execute(PackingContext context)
|
||||
{
|
||||
var oversized = context.RemainingItems
|
||||
.Where(item => item.Length > context.StockLength)
|
||||
.ToList();
|
||||
|
||||
foreach (var item in oversized)
|
||||
{
|
||||
context.RemainingItems.Remove(item);
|
||||
context.OversizedItems.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
CutList.Core/Nesting/Pipeline/FirstFitDecreasingStep.cs
Normal file
34
CutList.Core/Nesting/Pipeline/FirstFitDecreasingStep.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
namespace CutList.Core.Nesting.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the First-Fit Decreasing (FFD) bin packing algorithm.
|
||||
/// Assumes items are already sorted by length descending.
|
||||
/// Places each item in the first bin that has enough space.
|
||||
/// </summary>
|
||||
public class FirstFitDecreasingStep : IPackingStep
|
||||
{
|
||||
public void Execute(PackingContext context)
|
||||
{
|
||||
while (context.RemainingItems.Count > 0 && context.CanAddMoreBins())
|
||||
{
|
||||
var bin = context.CreateBin();
|
||||
FillBin(bin, context.RemainingItems);
|
||||
context.Bins.Add(bin);
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillBin(Bin bin, List<BinItem> remainingItems)
|
||||
{
|
||||
for (int i = 0; i < remainingItems.Count; i++)
|
||||
{
|
||||
var item = remainingItems[i];
|
||||
if (bin.RemainingLength >= item.Length)
|
||||
{
|
||||
bin.AddItem(item);
|
||||
remainingItems.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
CutList.Core/Nesting/Pipeline/IPackingStep.cs
Normal file
15
CutList.Core/Nesting/Pipeline/IPackingStep.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace CutList.Core.Nesting.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single step in the packing pipeline.
|
||||
/// Each step modifies the PackingContext to progress toward a final result.
|
||||
/// </summary>
|
||||
public interface IPackingStep
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes this step, modifying the context as needed.
|
||||
/// </summary>
|
||||
/// <param name="context">The mutable packing context.</param>
|
||||
void Execute(PackingContext context);
|
||||
}
|
||||
}
|
||||
123
CutList.Core/Nesting/Pipeline/OptimizationStep.cs
Normal file
123
CutList.Core/Nesting/Pipeline/OptimizationStep.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
namespace CutList.Core.Nesting.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to improve bin utilization by swapping items.
|
||||
/// For each bin, tries replacing a packed item with unpacked items
|
||||
/// to achieve better space utilization.
|
||||
/// </summary>
|
||||
public class OptimizationStep : IPackingStep
|
||||
{
|
||||
public void Execute(PackingContext context)
|
||||
{
|
||||
foreach (var bin in context.Bins)
|
||||
{
|
||||
while (TryImprovePacking(bin, context.RemainingItems, context.Spacing))
|
||||
{
|
||||
// Keep optimizing until no improvement can be made
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryImprovePacking(Bin bin, List<BinItem> remainingItems, double spacing)
|
||||
{
|
||||
if (bin.Items.Count == 0)
|
||||
return false;
|
||||
|
||||
if (remainingItems.Count < 2)
|
||||
return false;
|
||||
|
||||
var lengthGroups = GroupItemsByLength(bin.Items);
|
||||
var shortestLengthItemAvailable = remainingItems.Min(i => i.Length);
|
||||
|
||||
foreach (var group in lengthGroups)
|
||||
{
|
||||
var minRemainingLength = bin.RemainingLength;
|
||||
var firstItem = group.Items.FirstOrDefault();
|
||||
if (firstItem == null)
|
||||
continue;
|
||||
|
||||
bin.RemoveItem(firstItem);
|
||||
|
||||
for (int i = 0; i < remainingItems.Count; i++)
|
||||
{
|
||||
var item1 = remainingItems[i];
|
||||
|
||||
if (item1.Length > bin.RemainingLength)
|
||||
continue;
|
||||
|
||||
var testBin = new Bin(bin.RemainingLength)
|
||||
{
|
||||
Spacing = spacing
|
||||
};
|
||||
testBin.AddItem(item1);
|
||||
|
||||
for (int j = i + 1; j < remainingItems.Count; j++)
|
||||
{
|
||||
if (testBin.RemainingLength < shortestLengthItemAvailable)
|
||||
break;
|
||||
|
||||
var item2 = remainingItems[j];
|
||||
|
||||
if (item2.Length > testBin.RemainingLength)
|
||||
continue;
|
||||
|
||||
testBin.AddItem(item2);
|
||||
}
|
||||
|
||||
if (testBin.RemainingLength < minRemainingLength)
|
||||
{
|
||||
// Found improvement: swap the items
|
||||
remainingItems.Add(firstItem);
|
||||
bin.AddItems(testBin.Items);
|
||||
|
||||
foreach (var item in testBin.Items)
|
||||
{
|
||||
remainingItems.Remove(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bin.AddItem(firstItem);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<LengthGroup> GroupItemsByLength(IReadOnlyList<BinItem> items)
|
||||
{
|
||||
var groups = new List<LengthGroup>();
|
||||
var groupMap = new Dictionary<double, LengthGroup>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!groupMap.TryGetValue(item.Length, out var group))
|
||||
{
|
||||
group = new LengthGroup
|
||||
{
|
||||
Length = item.Length,
|
||||
Items = new List<BinItem>()
|
||||
};
|
||||
groupMap[item.Length] = group;
|
||||
groups.Add(group);
|
||||
}
|
||||
group.Items.Add(item);
|
||||
}
|
||||
|
||||
groups.Sort((a, b) => b.Length.CompareTo(a.Length));
|
||||
if (groups.Count > 0)
|
||||
{
|
||||
groups.RemoveAt(0); // Remove the largest length group
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private class LengthGroup
|
||||
{
|
||||
public double Length { get; set; }
|
||||
public List<BinItem> Items { get; set; } = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
89
CutList.Core/Nesting/Pipeline/PackingContext.cs
Normal file
89
CutList.Core/Nesting/Pipeline/PackingContext.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
namespace CutList.Core.Nesting.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Mutable context passed through the packing pipeline.
|
||||
/// Contains the working state that pipeline steps modify.
|
||||
/// </summary>
|
||||
public class PackingContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new packing context from a request.
|
||||
/// </summary>
|
||||
public PackingContext(PackingRequest request)
|
||||
{
|
||||
Request = request ?? throw new ArgumentNullException(nameof(request));
|
||||
RemainingItems = new List<BinItem>(request.Items);
|
||||
Bins = new List<Bin>();
|
||||
OversizedItems = new List<BinItem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The original immutable request.
|
||||
/// </summary>
|
||||
public PackingRequest Request { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Items that have not yet been placed in a bin.
|
||||
/// Pipeline steps remove items from this list as they are packed.
|
||||
/// </summary>
|
||||
public List<BinItem> RemainingItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Items that are too large to fit in any stock bin.
|
||||
/// </summary>
|
||||
public List<BinItem> OversizedItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The bins that have been created and filled.
|
||||
/// </summary>
|
||||
public List<Bin> Bins { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property for stock length from the request.
|
||||
/// </summary>
|
||||
public double StockLength => Request.StockLength;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property for spacing from the request.
|
||||
/// </summary>
|
||||
public double Spacing => Request.Spacing;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property for max bin count from the request.
|
||||
/// </summary>
|
||||
public int MaxBinCount => Request.MaxBinCount;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if more bins can be added without exceeding the limit.
|
||||
/// </summary>
|
||||
public bool CanAddMoreBins()
|
||||
{
|
||||
if (MaxBinCount == -1)
|
||||
return true;
|
||||
|
||||
return Bins.Count < MaxBinCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new bin with the stock length and spacing from the request.
|
||||
/// </summary>
|
||||
public Bin CreateBin()
|
||||
{
|
||||
return new Bin(StockLength)
|
||||
{
|
||||
Spacing = Spacing
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the final PackResult from the current context state.
|
||||
/// </summary>
|
||||
public PackResult ToResult()
|
||||
{
|
||||
var allUnusedItems = new List<BinItem>();
|
||||
allUnusedItems.AddRange(OversizedItems);
|
||||
allUnusedItems.AddRange(RemainingItems);
|
||||
return new PackResult(Bins, allUnusedItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
CutList.Core/Nesting/Pipeline/PackingPipeline.cs
Normal file
39
CutList.Core/Nesting/Pipeline/PackingPipeline.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
namespace CutList.Core.Nesting.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a sequence of packing steps to produce a final result.
|
||||
/// Provides a composable, testable approach to bin packing algorithms.
|
||||
/// </summary>
|
||||
public class PackingPipeline
|
||||
{
|
||||
private readonly List<IPackingStep> _steps = new();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a step to the pipeline.
|
||||
/// </summary>
|
||||
/// <param name="step">The step to add.</param>
|
||||
/// <returns>This pipeline for fluent chaining.</returns>
|
||||
public PackingPipeline AddStep(IPackingStep step)
|
||||
{
|
||||
_steps.Add(step ?? throw new ArgumentNullException(nameof(step)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes all steps in sequence and returns the result.
|
||||
/// </summary>
|
||||
/// <param name="request">The packing request to process.</param>
|
||||
/// <returns>The packing result.</returns>
|
||||
public PackResult Execute(PackingRequest request)
|
||||
{
|
||||
var context = new PackingContext(request);
|
||||
|
||||
foreach (var step in _steps)
|
||||
{
|
||||
step.Execute(context);
|
||||
}
|
||||
|
||||
return context.ToResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
CutList.Core/Nesting/Pipeline/SortBinItemsStep.cs
Normal file
20
CutList.Core/Nesting/Pipeline/SortBinItemsStep.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace CutList.Core.Nesting.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Sorts items within each bin by length descending for consistent output.
|
||||
/// </summary>
|
||||
public class SortBinItemsStep : IPackingStep
|
||||
{
|
||||
public void Execute(PackingContext context)
|
||||
{
|
||||
foreach (var bin in context.Bins)
|
||||
{
|
||||
bin.SortItems((a, b) =>
|
||||
{
|
||||
int comparison = b.Length.CompareTo(a.Length);
|
||||
return comparison != 0 ? comparison : a.Name.CompareTo(b.Name);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
CutList.Core/Nesting/Pipeline/SortBinsByUtilizationStep.cs
Normal file
20
CutList.Core/Nesting/Pipeline/SortBinsByUtilizationStep.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace CutList.Core.Nesting.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Sorts bins by utilization (highest first) for optimal presentation.
|
||||
/// Secondary sort by item count (fewer items first for ties).
|
||||
/// </summary>
|
||||
public class SortBinsByUtilizationStep : IPackingStep
|
||||
{
|
||||
public void Execute(PackingContext context)
|
||||
{
|
||||
var sorted = context.Bins
|
||||
.OrderByDescending(b => b.Utilization)
|
||||
.ThenBy(b => b.Items.Count)
|
||||
.ToList();
|
||||
|
||||
context.Bins.Clear();
|
||||
context.Bins.AddRange(sorted);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
CutList.Core/Nesting/Pipeline/SortItemsDescendingStep.cs
Normal file
19
CutList.Core/Nesting/Pipeline/SortItemsDescendingStep.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace CutList.Core.Nesting.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Sorts remaining items by length in descending order.
|
||||
/// This is the "Decreasing" part of First-Fit Decreasing (FFD) algorithm.
|
||||
/// </summary>
|
||||
public class SortItemsDescendingStep : IPackingStep
|
||||
{
|
||||
public void Execute(PackingContext context)
|
||||
{
|
||||
var sorted = context.RemainingItems
|
||||
.OrderByDescending(item => item.Length)
|
||||
.ToList();
|
||||
|
||||
context.RemainingItems.Clear();
|
||||
context.RemainingItems.AddRange(sorted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,9 @@ public static class CutListTools
|
||||
[Description("Stock bins available. Each needs: length (string), quantity (int, use -1 for unlimited), priority (int, lower = used first, default 25)")]
|
||||
StockBinInput[] stockBins,
|
||||
[Description("Blade kerf/width in inches (default 0.125)")]
|
||||
double kerf = 0.125)
|
||||
double kerf = 0.125,
|
||||
[Description("Packing strategy: 'advanced' (default), 'bestfit', or 'exhaustive' (optimal but slow, max 15 items)")]
|
||||
string strategy = "advanced")
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -39,7 +41,7 @@ public static class CutListTools
|
||||
if (binsError != null)
|
||||
return new CutListResult { Success = false, Error = binsError };
|
||||
|
||||
var packResult = RunPackingAlgorithm(binItems!, multiBins!, kerf);
|
||||
var packResult = RunPackingAlgorithm(binItems!, multiBins!, kerf, ParseStrategy(strategy));
|
||||
|
||||
// Convert results
|
||||
var resultBins = new List<ResultBin>();
|
||||
@@ -176,7 +178,9 @@ public static class CutListTools
|
||||
[Description("Blade kerf/width in inches (default 0.125)")]
|
||||
double kerf = 0.125,
|
||||
[Description("File path to save the report. If not provided, saves to a temp file.")]
|
||||
string? filePath = null)
|
||||
string? filePath = null,
|
||||
[Description("Packing strategy: 'advanced' (default), 'bestfit', or 'exhaustive' (optimal but slow, max 15 items)")]
|
||||
string strategy = "advanced")
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -188,7 +192,7 @@ public static class CutListTools
|
||||
if (binsError != null)
|
||||
return new CutListReportResult { Success = false, Error = binsError };
|
||||
|
||||
var packResult = RunPackingAlgorithm(binItems!, multiBins!, kerf);
|
||||
var packResult = RunPackingAlgorithm(binItems!, multiBins!, kerf, ParseStrategy(strategy));
|
||||
|
||||
// Determine file path
|
||||
var outputPath = string.IsNullOrWhiteSpace(filePath)
|
||||
@@ -247,14 +251,25 @@ public static class CutListTools
|
||||
return (multiBins, null);
|
||||
}
|
||||
|
||||
private static Core.Nesting.Result RunPackingAlgorithm(List<BinItem> items, List<MultiBin> bins, double kerf)
|
||||
private static PackResult RunPackingAlgorithm(List<BinItem> items, List<MultiBin> bins, double kerf, PackingStrategy packingStrategy = PackingStrategy.AdvancedFit)
|
||||
{
|
||||
var engine = new MultiBinEngine();
|
||||
engine.SetBins(bins);
|
||||
engine.Spacing = kerf;
|
||||
engine.Strategy = packingStrategy;
|
||||
return engine.Pack(items);
|
||||
}
|
||||
|
||||
private static PackingStrategy ParseStrategy(string strategy)
|
||||
{
|
||||
return strategy?.ToLowerInvariant() switch
|
||||
{
|
||||
"bestfit" or "best" => PackingStrategy.BestFit,
|
||||
"exhaustive" or "optimal" => PackingStrategy.Exhaustive,
|
||||
_ => PackingStrategy.AdvancedFit
|
||||
};
|
||||
}
|
||||
|
||||
private static double ParseLength(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace CutList.Services
|
||||
/// <param name="stockBins">The available stock bins</param>
|
||||
/// <param name="cuttingTool">The cutting tool to use (determines kerf/spacing)</param>
|
||||
/// <returns>Result containing the packing result with optimized bins and unused items, or error message</returns>
|
||||
public Result<CutList.Core.Nesting.Result> Pack(List<PartInputItem> parts, List<BinInputItem> stockBins, Tool cuttingTool)
|
||||
public Result<PackResult> Pack(List<PartInputItem> parts, List<BinInputItem> stockBins, Tool cuttingTool)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -30,11 +30,11 @@ namespace CutList.Services
|
||||
engine.Spacing = cuttingTool.Kerf;
|
||||
|
||||
var packResult = engine.Pack(binItems);
|
||||
return Result<CutList.Core.Nesting.Result>.Success(packResult);
|
||||
return Result<PackResult>.Success(packResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<CutList.Core.Nesting.Result>.Failure($"Packing failed: {ex.Message}");
|
||||
return Result<PackResult>.Failure($"Packing failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user