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

@@ -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))