refactor(engine): route console and MCP nesting through named job engines

Console --engine now names a jobs engine for --autonest (solved once
through NestingEngineRegistry.Create and committed onto the plate) or a
built-in fill strategy for single-plate fill through the public
PlateFillService; unknown names exit with the valid choices instead of
consulting the process-global legacy registry. MCP nesting tools take an
explicit engine argument per call with the session default, never read
process-global active-engine state, and reject whole-job engine names on
single-plate fill tools. NestingEngineRegistry gains an explicit Create
(name) resolution; PlateFillService gains a public ResolveStrategy and a
plate-number Nest overload used by interactive callers.
This commit is contained in:
aj
2026-09-22 00:30:03 -04:00
parent f36e124039
commit 88966d118c
8 changed files with 416 additions and 73 deletions
@@ -53,6 +53,22 @@ public static class NestingEngineRegistry
public static IReadOnlyList<NestingEngineInfo> AvailableEngines => engines;
/// <summary>
/// Creates the engine registered under <paramref name="name"/> (case-insensitive). The caller's
/// explicit choice is the whole selection mechanism: unlike the legacy registry there is no
/// process-global active name to consult or mutate. Unknown names throw.
/// </summary>
public static INestingEngine Create(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
var info = engines.FirstOrDefault(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (info == null)
throw new NotSupportedException(
$"Unknown nesting engine: {name}. Available: {string.Join(", ", engines.Select(e => e.Name))}."
);
return info.Factory();
}
public static void Register(string name, string description, Func<INestingEngine> factory)
{
if (engines.Any(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
@@ -66,9 +66,28 @@ public static class PlateFillService
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
) => Nest(strategy, plate, items, 0, progress, token);
/// <summary>
/// Whole-plate fill with the strategy's orchestration (fill-vs-pack, compaction). Returns
/// proposed parts only; committing them to <paramref name="plate"/> stays with the caller.
/// </summary>
/// <param name="plateNumber">Plate index reported with progress, as the legacy engine's
/// <c>PlateNumber</c> was by interactive multi-plate loops.</param>
public static List<Part> Nest(
string strategy,
Plate plate,
List<NestItem> items,
int plateNumber,
IProgress<NestProgress> progress,
CancellationToken token
)
{
return RequireFiller(strategy, plate).Nest(items, progress, token);
ArgumentNullException.ThrowIfNull(strategy);
ResolveStrategy(strategy, allowEmpty: false);
var filler = CreateFiller(strategy, plate);
filler.PlateNumber = plateNumber;
return filler.Nest(items, progress, token);
}
/// <summary>
@@ -77,7 +96,11 @@ public static class PlateFillService
/// 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)
public static string ResolveStrategy(string strategy) => ResolveStrategy(strategy, true);
/// <inheritdoc cref="ResolveStrategy(string)"/>
/// <param name="allowEmpty">False rejects null/empty instead of defaulting (explicit service calls).</param>
internal static string ResolveStrategy(string strategy, bool allowEmpty)
{
if (string.IsNullOrWhiteSpace(strategy))
{