refactor(engine): thread explicit placement strategy through multi-plate orchestrators
This commit is contained in:
@@ -33,8 +33,7 @@ public static class PlateFillService
|
|||||||
CancellationToken token
|
CancellationToken token
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var filler = CreateFiller(strategy, plate);
|
return RequireFiller(strategy, plate).Fill(item, workArea, progress, token);
|
||||||
return filler.Fill(item, workArea, progress, token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Part> FillGroup(
|
public static List<Part> FillGroup(
|
||||||
@@ -46,8 +45,7 @@ public static class PlateFillService
|
|||||||
CancellationToken token
|
CancellationToken token
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var filler = CreateFiller(strategy, plate);
|
return RequireFiller(strategy, plate).Fill(groupParts, workArea, progress, token);
|
||||||
return filler.Fill(groupParts, workArea, progress, token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Part> PackArea(
|
public static List<Part> PackArea(
|
||||||
@@ -59,22 +57,56 @@ public static class PlateFillService
|
|||||||
CancellationToken token
|
CancellationToken token
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var filler = CreateFiller(strategy, plate);
|
return RequireFiller(strategy, plate).PackArea(box, items, progress, token);
|
||||||
return filler.PackArea(box, items, progress, token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PlateFillerBase CreateFiller(string strategy, Plate plate)
|
public static List<Part> Nest(
|
||||||
|
string strategy,
|
||||||
|
Plate plate,
|
||||||
|
List<NestItem> items,
|
||||||
|
IProgress<NestProgress> progress,
|
||||||
|
CancellationToken token
|
||||||
|
)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(strategy);
|
return RequireFiller(strategy, plate).Nest(items, progress, token);
|
||||||
ArgumentNullException.ThrowIfNull(plate);
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves a caller-supplied strategy name: null or empty means "Default"; otherwise the name
|
||||||
|
/// must match a built-in strategy, matched case-insensitively like the legacy registry's
|
||||||
|
/// ActiveEngineName so tolerant interactive callers keep working. Returns the canonical name;
|
||||||
|
/// unknown names throw <see cref="NotSupportedException"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ResolveStrategy(string strategy, bool allowEmpty = true)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(strategy))
|
||||||
|
{
|
||||||
|
if (allowEmpty)
|
||||||
|
return "Default";
|
||||||
|
throw new NotSupportedException(
|
||||||
|
$"Unknown placement strategy: '{strategy}'. Known strategies: {string.Join(", ", BuiltInStrategies)}."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// OrdinalIgnoreCase mirrors the legacy registry's ActiveEngineName matching so the
|
|
||||||
// interactive callers keep their tolerant name handling while moving off global state.
|
|
||||||
foreach (var candidate in BuiltInStrategies)
|
foreach (var candidate in BuiltInStrategies)
|
||||||
{
|
{
|
||||||
if (candidate.Equals(strategy, StringComparison.OrdinalIgnoreCase))
|
if (candidate.Equals(strategy, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotSupportedException(
|
||||||
|
$"Unknown placement strategy: {strategy}. Known strategies: {string.Join(", ", BuiltInStrategies)}."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the filler for an optional strategy (null/empty = Default). Internal so the
|
||||||
|
/// engine-side multi-plate orchestrators share one resolution/rejection contract.
|
||||||
|
/// </summary>
|
||||||
|
internal static PlateFillerBase CreateFiller(string strategy, Plate plate)
|
||||||
{
|
{
|
||||||
return candidate switch
|
ArgumentNullException.ThrowIfNull(plate);
|
||||||
|
return ResolveStrategy(strategy) switch
|
||||||
{
|
{
|
||||||
"Default" => new DefaultPlateFiller(plate),
|
"Default" => new DefaultPlateFiller(plate),
|
||||||
"Strip" => new StripPlateFiller(plate),
|
"Strip" => new StripPlateFiller(plate),
|
||||||
@@ -82,10 +114,14 @@ public static class PlateFillService
|
|||||||
_ => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal),
|
_ => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
throw new NotSupportedException(
|
/// <summary>Public operations require an explicit strategy name (null is an argument error).</summary>
|
||||||
$"Unknown placement strategy: {strategy}. Known strategies: {string.Join(", ", BuiltInStrategies)}."
|
private static PlateFillerBase RequireFiller(string strategy, Plate plate)
|
||||||
);
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(strategy);
|
||||||
|
// An explicit empty string is an unknown strategy, not the orchestrator's
|
||||||
|
// null-means-Default defaulting; only the orchestrator boundary may default.
|
||||||
|
ResolveStrategy(strategy, allowEmpty: false);
|
||||||
|
return CreateFiller(strategy, plate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Engine.Jobs.Placement;
|
||||||
|
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ namespace OpenNest.Engine
|
|||||||
private readonly IProgress<NestProgress> _progress;
|
private readonly IProgress<NestProgress> _progress;
|
||||||
private readonly CancellationToken _token;
|
private readonly CancellationToken _token;
|
||||||
private readonly MultiPlateNestOptions _options;
|
private readonly MultiPlateNestOptions _options;
|
||||||
|
private readonly string _strategy;
|
||||||
|
|
||||||
private bool HasPlateOptions => _plateOptions != null && _plateOptions.Count > 0;
|
private bool HasPlateOptions => _plateOptions != null && _plateOptions.Count > 0;
|
||||||
|
|
||||||
@@ -45,8 +48,12 @@ namespace OpenNest.Engine
|
|||||||
_platePool = InitializePlatePool(existingPlates);
|
_platePool = InitializePlatePool(existingPlates);
|
||||||
_progress = progress;
|
_progress = progress;
|
||||||
_token = token;
|
_token = token;
|
||||||
|
_strategy = PlateFillService.ResolveStrategy(options.Strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PlateFillerBase CreateFiller(Plate plate) =>
|
||||||
|
PlateFillService.CreateFiller(_strategy, plate);
|
||||||
|
|
||||||
// --- Static Utility Methods ---
|
// --- Static Utility Methods ---
|
||||||
|
|
||||||
public static bool FitsBounds(Box container, Box part)
|
public static bool FitsBounds(Box container, Box part)
|
||||||
@@ -226,7 +233,7 @@ namespace OpenNest.Engine
|
|||||||
|
|
||||||
private int FillAndPlace(PlateResult pr, Box zone, NestItem item)
|
private int FillAndPlace(PlateResult pr, Box zone, NestItem item)
|
||||||
{
|
{
|
||||||
var engine = NestEngineRegistry.Create(pr.Plate);
|
var engine = CreateFiller(pr.Plate);
|
||||||
var clonedItem = CloneItem(item);
|
var clonedItem = CloneItem(item);
|
||||||
var parts = engine.Fill(clonedItem, zone, _progress, _token);
|
var parts = engine.Fill(clonedItem, zone, _progress, _token);
|
||||||
|
|
||||||
@@ -381,7 +388,7 @@ namespace OpenNest.Engine
|
|||||||
if (remnants.Count == 0)
|
if (remnants.Count == 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(pr.Plate);
|
var engine = CreateFiller(pr.Plate);
|
||||||
|
|
||||||
foreach (var remnant in remnants)
|
foreach (var remnant in remnants)
|
||||||
{
|
{
|
||||||
@@ -430,7 +437,7 @@ namespace OpenNest.Engine
|
|||||||
if (remnants.Count == 0)
|
if (remnants.Count == 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(plate);
|
var engine = CreateFiller(plate);
|
||||||
|
|
||||||
foreach (var remnant in remnants)
|
foreach (var remnant in remnants)
|
||||||
{
|
{
|
||||||
@@ -643,7 +650,7 @@ namespace OpenNest.Engine
|
|||||||
upgradeOption,
|
upgradeOption,
|
||||||
remnants =>
|
remnants =>
|
||||||
{
|
{
|
||||||
var engine = NestEngineRegistry.Create(target.Plate);
|
var engine = CreateFiller(target.Plate);
|
||||||
var tempItems = donorParts
|
var tempItems = donorParts
|
||||||
.GroupBy(p => p.BaseDrawing)
|
.GroupBy(p => p.BaseDrawing)
|
||||||
.Select(g => new NestItem
|
.Select(g => new NestItem
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ namespace OpenNest.Engine
|
|||||||
public PartSortOrder SortOrder { get; set; } = PartSortOrder.BoundingBoxArea;
|
public PartSortOrder SortOrder { get; set; } = PartSortOrder.BoundingBoxArea;
|
||||||
public double MinRemnantSize { get; set; } = 12.0;
|
public double MinRemnantSize { get; set; } = 12.0;
|
||||||
public bool AllowPlateCreation { get; set; } = true;
|
public bool AllowPlateCreation { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Explicit placement strategy for every single-plate fill/pack this run performs
|
||||||
|
/// ("Default", "Strip", "Vertical Remnant", "Horizontal Remnant"). Null or empty means
|
||||||
|
/// "Default"; unknown names are rejected up front. This replaces reading the
|
||||||
|
/// process-global engine registry.
|
||||||
|
/// </summary>
|
||||||
|
public string Strategy { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MultiPlateResult
|
public class MultiPlateResult
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
using OpenNest.Engine.BestFit;
|
using OpenNest.Engine.BestFit;
|
||||||
|
using OpenNest.Engine.Jobs.Placement;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
|
||||||
@@ -18,7 +19,8 @@ namespace OpenNest.Engine
|
|||||||
double salvageRate,
|
double salvageRate,
|
||||||
Plate templatePlate,
|
Plate templatePlate,
|
||||||
IProgress<NestProgress> progress = null,
|
IProgress<NestProgress> progress = null,
|
||||||
CancellationToken token = default
|
CancellationToken token = default,
|
||||||
|
string strategy = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
@@ -29,6 +31,10 @@ namespace OpenNest.Engine
|
|||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
// Explicit strategy at the top-level boundary (null/empty = Default, unknown = throw);
|
||||||
|
// the size search never consults the process-global engine registry.
|
||||||
|
var resolvedStrategy = PlateFillService.ResolveStrategy(strategy);
|
||||||
|
|
||||||
// Find the minimum dimension needed to fit the largest part,
|
// Find the minimum dimension needed to fit the largest part,
|
||||||
// skipping items that are too large for every plate option.
|
// skipping items that are too large for every plate option.
|
||||||
var minPartWidth = 0.0;
|
var minPartWidth = 0.0;
|
||||||
@@ -94,7 +100,8 @@ namespace OpenNest.Engine
|
|||||||
salvageRate,
|
salvageRate,
|
||||||
templatePlate,
|
templatePlate,
|
||||||
progress,
|
progress,
|
||||||
token
|
token,
|
||||||
|
resolvedStrategy
|
||||||
);
|
);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -149,7 +156,8 @@ namespace OpenNest.Engine
|
|||||||
double salvageRate,
|
double salvageRate,
|
||||||
Plate templatePlate,
|
Plate templatePlate,
|
||||||
IProgress<NestProgress> progress,
|
IProgress<NestProgress> progress,
|
||||||
CancellationToken token
|
CancellationToken token,
|
||||||
|
string strategy
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// Create a temporary plate with candidate size + settings from template.
|
// Create a temporary plate with candidate size + settings from template.
|
||||||
@@ -178,8 +186,7 @@ namespace OpenNest.Engine
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(tempPlate);
|
var parts = PlateFillService.Nest(strategy, tempPlate, clonedItems, progress, token);
|
||||||
var parts = engine.Nest(clonedItems, progress, token);
|
|
||||||
|
|
||||||
if (parts == null || parts.Count == 0)
|
if (parts == null || parts.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.Jobs.Placement;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Engine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 3.2: MultiPlateNester and PlateOptimizer take an explicit placement strategy at their
|
||||||
|
/// top-level call boundary instead of consulting the process-global NestEngineRegistry.
|
||||||
|
/// </summary>
|
||||||
|
public class ExplicitPlacementStrategyTests
|
||||||
|
{
|
||||||
|
private static Drawing MakeRectDrawing(double w, double h, string name = "rect")
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(w, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
return new Drawing(name, pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NestItem MakeItem(string name, double w, double h, int qty) =>
|
||||||
|
new() { Drawing = MakeRectDrawing(w, h, name), Quantity = qty };
|
||||||
|
|
||||||
|
private static MultiPlateNestOptions MakeOptions()
|
||||||
|
{
|
||||||
|
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
|
||||||
|
template.EdgeSpacing = new Spacing();
|
||||||
|
return new MultiPlateNestOptions { Template = template };
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultiPlateNester_UnsetStrategy_MatchesExplicitDefault()
|
||||||
|
{
|
||||||
|
var unsetOptions = MakeOptions();
|
||||||
|
var explicitOptions = MakeOptions();
|
||||||
|
explicitOptions.Strategy = "Default";
|
||||||
|
Assert.Null(unsetOptions.Strategy);
|
||||||
|
|
||||||
|
var unsetResult = MultiPlateNester.Nest(
|
||||||
|
new List<NestItem> { MakeItem("a", 20, 10, 3), MakeItem("b", 12, 8, 4) },
|
||||||
|
unsetOptions
|
||||||
|
);
|
||||||
|
var explicitResult = MultiPlateNester.Nest(
|
||||||
|
new List<NestItem> { MakeItem("a", 20, 10, 3), MakeItem("b", 12, 8, 4) },
|
||||||
|
explicitOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.NotEmpty(unsetResult.Plates);
|
||||||
|
Assert.Equal(
|
||||||
|
unsetResult.Plates.Sum(p => p.Parts.Count),
|
||||||
|
explicitResult.Plates.Sum(p => p.Parts.Count)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Mystery Engine")]
|
||||||
|
[InlineData("StockLadder")]
|
||||||
|
public void MultiPlateNester_RejectsUnknownStrategy(string strategy)
|
||||||
|
{
|
||||||
|
var options = MakeOptions();
|
||||||
|
options.Strategy = strategy;
|
||||||
|
|
||||||
|
Assert.Throws<NotSupportedException>(() =>
|
||||||
|
MultiPlateNester.Nest(new List<NestItem> { MakeItem("a", 20, 10, 1) }, options)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultiPlateNester_HonorsExplicitStripStrategy()
|
||||||
|
{
|
||||||
|
var options = MakeOptions();
|
||||||
|
options.Strategy = "Strip";
|
||||||
|
|
||||||
|
var result = MultiPlateNester.Nest(
|
||||||
|
new List<NestItem> { MakeItem("a", 20, 10, 3), MakeItem("b", 12, 8, 4) },
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.NotEmpty(result.Plates);
|
||||||
|
Assert.Equal(0, result.UnplacedItems.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlateOptimizer_UnsetStrategy_MatchesExplicitDefault()
|
||||||
|
{
|
||||||
|
var options = new List<PlateOption>
|
||||||
|
{
|
||||||
|
new() { Width = 20, Length = 20, Cost = 100 },
|
||||||
|
new() { Width = 40, Length = 40, Cost = 400 },
|
||||||
|
};
|
||||||
|
var templatePlate = new Plate(40, 40) { PartSpacing = 0 };
|
||||||
|
var items = new List<NestItem> { MakeItem("rect", 10, 10, 1) };
|
||||||
|
|
||||||
|
var defaulted = PlateOptimizer.Optimize(items, options, 0.0, templatePlate);
|
||||||
|
var explicitDefault = PlateOptimizer.Optimize(
|
||||||
|
items,
|
||||||
|
options,
|
||||||
|
0.0,
|
||||||
|
templatePlate,
|
||||||
|
strategy: "Default"
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.NotNull(defaulted);
|
||||||
|
Assert.NotNull(explicitDefault);
|
||||||
|
Assert.Equal(defaulted.ChosenSize.Width, explicitDefault.ChosenSize.Width);
|
||||||
|
Assert.Equal(defaulted.Parts.Count, explicitDefault.Parts.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Mystery Engine")]
|
||||||
|
[InlineData("StockLadder")]
|
||||||
|
public void PlateOptimizer_RejectsUnknownStrategy(string strategy)
|
||||||
|
{
|
||||||
|
var options = new List<PlateOption> { new() { Width = 20, Length = 20, Cost = 100 } };
|
||||||
|
var templatePlate = new Plate(40, 40) { PartSpacing = 0 };
|
||||||
|
var items = new List<NestItem> { MakeItem("rect", 10, 10, 1) };
|
||||||
|
|
||||||
|
Assert.Throws<NotSupportedException>(() =>
|
||||||
|
PlateOptimizer.Optimize(items, options, 0.0, templatePlate, strategy: strategy)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlateFillService_Nest_ResolvesNamedStrategyAndRejectsUnknown()
|
||||||
|
{
|
||||||
|
var plate = new Plate(new Size(60, 80));
|
||||||
|
var drawing = MakeRectDrawing(6, 4, "part");
|
||||||
|
|
||||||
|
var parts = PlateFillService.Nest(
|
||||||
|
"Vertical Remnant",
|
||||||
|
plate,
|
||||||
|
new List<NestItem> { new() { Drawing = drawing, Quantity = 4 } },
|
||||||
|
null,
|
||||||
|
CancellationToken.None
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.NotEmpty(parts);
|
||||||
|
Assert.All(parts, part => Assert.Same(drawing, part.BaseDrawing));
|
||||||
|
Assert.Empty(plate.Parts);
|
||||||
|
|
||||||
|
Assert.Throws<NotSupportedException>(() =>
|
||||||
|
PlateFillService.Nest(
|
||||||
|
"Mystery Engine",
|
||||||
|
plate,
|
||||||
|
new List<NestItem> { new() { Drawing = drawing, Quantity = 1 } },
|
||||||
|
null,
|
||||||
|
CancellationToken.None
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user