refactor(engine): thread explicit placement strategy through multi-plate orchestrators

This commit is contained in:
aj
2026-09-21 20:22:45 -04:00
parent 856dbfd8af
commit f36e124039
5 changed files with 242 additions and 29 deletions
@@ -33,8 +33,7 @@ public static class PlateFillService
CancellationToken token
)
{
var filler = CreateFiller(strategy, plate);
return filler.Fill(item, workArea, progress, token);
return RequireFiller(strategy, plate).Fill(item, workArea, progress, token);
}
public static List<Part> FillGroup(
@@ -46,8 +45,7 @@ public static class PlateFillService
CancellationToken token
)
{
var filler = CreateFiller(strategy, plate);
return filler.Fill(groupParts, workArea, progress, token);
return RequireFiller(strategy, plate).Fill(groupParts, workArea, progress, token);
}
public static List<Part> PackArea(
@@ -59,33 +57,71 @@ public static class PlateFillService
CancellationToken token
)
{
var filler = CreateFiller(strategy, plate);
return filler.PackArea(box, items, progress, token);
return RequireFiller(strategy, plate).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);
ArgumentNullException.ThrowIfNull(plate);
return RequireFiller(strategy, plate).Nest(items, progress, token);
}
/// <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)
{
if (candidate.Equals(strategy, StringComparison.OrdinalIgnoreCase))
{
return candidate switch
{
"Default" => new DefaultPlateFiller(plate),
"Strip" => new StripPlateFiller(plate),
"Vertical Remnant" => new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical),
_ => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal),
};
}
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)
{
ArgumentNullException.ThrowIfNull(plate);
return ResolveStrategy(strategy) switch
{
"Default" => new DefaultPlateFiller(plate),
"Strip" => new StripPlateFiller(plate),
"Vertical Remnant" => new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical),
_ => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal),
};
}
/// <summary>Public operations require an explicit strategy name (null is an argument error).</summary>
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);
}
}
+11 -4
View File
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
using OpenNest.Math;
@@ -26,6 +28,7 @@ namespace OpenNest.Engine
private readonly IProgress<NestProgress> _progress;
private readonly CancellationToken _token;
private readonly MultiPlateNestOptions _options;
private readonly string _strategy;
private bool HasPlateOptions => _plateOptions != null && _plateOptions.Count > 0;
@@ -45,8 +48,12 @@ namespace OpenNest.Engine
_platePool = InitializePlatePool(existingPlates);
_progress = progress;
_token = token;
_strategy = PlateFillService.ResolveStrategy(options.Strategy);
}
private PlateFillerBase CreateFiller(Plate plate) =>
PlateFillService.CreateFiller(_strategy, plate);
// --- Static Utility Methods ---
public static bool FitsBounds(Box container, Box part)
@@ -226,7 +233,7 @@ namespace OpenNest.Engine
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 parts = engine.Fill(clonedItem, zone, _progress, _token);
@@ -381,7 +388,7 @@ namespace OpenNest.Engine
if (remnants.Count == 0)
break;
var engine = NestEngineRegistry.Create(pr.Plate);
var engine = CreateFiller(pr.Plate);
foreach (var remnant in remnants)
{
@@ -430,7 +437,7 @@ namespace OpenNest.Engine
if (remnants.Count == 0)
break;
var engine = NestEngineRegistry.Create(plate);
var engine = CreateFiller(plate);
foreach (var remnant in remnants)
{
@@ -643,7 +650,7 @@ namespace OpenNest.Engine
upgradeOption,
remnants =>
{
var engine = NestEngineRegistry.Create(target.Plate);
var engine = CreateFiller(target.Plate);
var tempItems = donorParts
.GroupBy(p => p.BaseDrawing)
.Select(g => new NestItem
+8
View File
@@ -10,6 +10,14 @@ namespace OpenNest.Engine
public PartSortOrder SortOrder { get; set; } = PartSortOrder.BoundingBoxArea;
public double MinRemnantSize { get; set; } = 12.0;
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
+12 -5
View File
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using OpenNest.Engine;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Geometry;
using OpenNest.Math;
@@ -18,7 +19,8 @@ namespace OpenNest.Engine
double salvageRate,
Plate templatePlate,
IProgress<NestProgress> progress = null,
CancellationToken token = default
CancellationToken token = default,
string strategy = null
)
{
if (
@@ -29,6 +31,10 @@ namespace OpenNest.Engine
)
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,
// skipping items that are too large for every plate option.
var minPartWidth = 0.0;
@@ -94,7 +100,8 @@ namespace OpenNest.Engine
salvageRate,
templatePlate,
progress,
token
token,
resolvedStrategy
);
if (result == null)
continue;
@@ -149,7 +156,8 @@ namespace OpenNest.Engine
double salvageRate,
Plate templatePlate,
IProgress<NestProgress> progress,
CancellationToken token
CancellationToken token,
string strategy
)
{
// Create a temporary plate with candidate size + settings from template.
@@ -178,8 +186,7 @@ namespace OpenNest.Engine
})
.ToList();
var engine = NestEngineRegistry.Create(tempPlate);
var parts = engine.Nest(clonedItems, progress, token);
var parts = PlateFillService.Nest(strategy, tempPlate, clonedItems, progress, token);
if (parts == null || parts.Count == 0)
return null;