diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs
index 3f5a314e..9aa84382 100644
--- a/OpenNest.Console/Program.cs
+++ b/OpenNest.Console/Program.cs
@@ -91,14 +91,13 @@ static class NestConsole
}
else
{
- try
- {
- PlateFillService.ResolveStrategy(options.Engine);
- }
- catch (NotSupportedException)
+ var engine = EngineNameResolver.Classify(options.Engine);
+ if (!engine.IsFillStrategy)
{
Console.Error.WriteLine(
- $"Error: unknown engine '{options.Engine}'. Fill strategies: {string.Join(", ", PlateFillService.BuiltInStrategies)} (jobs engines such as StockLadder require --autonest)"
+ engine.IsJobEngine
+ ? $"Error: engine '{engine.Name}' is a whole-job engine; single-plate fill supports: {string.Join(", ", PlateFillService.BuiltInStrategies)}. Use --autonest for whole-job engines."
+ : $"Error: unknown engine '{engine.Name}'. Fill strategies: {string.Join(", ", PlateFillService.BuiltInStrategies)} (jobs engines such as StockLadder require --autonest)"
);
return 1;
}
@@ -486,7 +485,7 @@ static class NestConsole
{
// Single-plate fill: explicit placement strategy through the public service;
// the process-global engine registry is never consulted.
- var strategy = ResolveFillStrategy(options.Engine);
+ var strategy = EngineNameResolver.Classify(options.Engine).FillStrategy;
var item = new NestItem { Drawing = drawing, Quantity = options.Quantity };
var parts = PlateFillService.FillItem(
strategy,
@@ -545,27 +544,6 @@ static class NestConsole
return (true, committed);
}
- static string ResolveFillStrategy(string engineName)
- {
- try
- {
- return PlateFillService.ResolveStrategy(engineName);
- }
- catch (NotSupportedException)
- {
- var isJobEngine = NestingEngineRegistry.AvailableEngines.Any(e =>
- e.Name.Equals(engineName, StringComparison.OrdinalIgnoreCase)
- );
- Console.Error.WriteLine(
- isJobEngine
- ? $"Error: engine '{engineName}' is a whole-job engine; single-plate fill supports: {string.Join(", ", PlateFillService.BuiltInStrategies)}. Use --autonest for whole-job engines."
- : $"Error: unknown engine '{engineName}'. Engines: {string.Join(", ", NestingEngineRegistry.AvailableEngines.Select(e => e.Name))}"
- );
- Environment.Exit(1);
- throw; // unreachable
- }
- }
-
static int CheckOverlaps(Plate plate, Options options)
{
if (!options.CheckOverlaps || plate.Parts.Count == 0)
diff --git a/OpenNest.Engine.Tests/Jobs/EngineNameResolverTests.cs b/OpenNest.Engine.Tests/Jobs/EngineNameResolverTests.cs
new file mode 100644
index 00000000..6c57c7e9
--- /dev/null
+++ b/OpenNest.Engine.Tests/Jobs/EngineNameResolverTests.cs
@@ -0,0 +1,43 @@
+using OpenNest.Engine.Jobs;
+using Xunit;
+
+namespace OpenNest.Engine.Tests.Jobs;
+
+public class EngineNameResolverTests
+{
+ [Theory]
+ [InlineData("Fill", "Fill")]
+ [InlineData(" strip ", "Strip")]
+ [InlineData("Default", "Fill")]
+ [InlineData(null, "Fill")]
+ [InlineData(" ", "Fill")]
+ public void FillStrategiesWinAndAreCanonicalized(string name, string expected)
+ {
+ var result = EngineNameResolver.Classify(name);
+
+ Assert.Equal(EngineNameKind.FillStrategy, result.Kind);
+ Assert.Equal(expected, result.FillStrategy);
+ }
+
+ [Theory]
+ [InlineData("StockLadder")]
+ [InlineData(" irregular ")]
+ [InlineData("Opus55NestingEngine")]
+ public void WholeJobEnginesAreRecognized(string name)
+ {
+ var result = EngineNameResolver.Classify(name);
+
+ Assert.Equal(EngineNameKind.JobEngine, result.Kind);
+ Assert.Null(result.FillStrategy);
+ Assert.Equal(name.Trim(), result.Name);
+ }
+
+ [Fact]
+ public void UnknownNamesAreUnknown()
+ {
+ var result = EngineNameResolver.Classify("Mystery Engine");
+
+ Assert.Equal(EngineNameKind.Unknown, result.Kind);
+ Assert.Null(result.FillStrategy);
+ }
+}
diff --git a/OpenNest.Engine/Jobs/EngineNameResolver.cs b/OpenNest.Engine/Jobs/EngineNameResolver.cs
new file mode 100644
index 00000000..93191735
--- /dev/null
+++ b/OpenNest.Engine/Jobs/EngineNameResolver.cs
@@ -0,0 +1,45 @@
+#nullable enable
+using System;
+using OpenNest.Engine.Jobs.Placement;
+
+namespace OpenNest.Engine.Jobs;
+
+public enum EngineNameKind
+{
+ FillStrategy,
+ JobEngine,
+ Unknown,
+}
+
+/// The trimmed caller name.
+/// Canonical strategy name when is FillStrategy.
+public readonly record struct EngineNameClassification(EngineNameKind Kind, string Name, string? FillStrategy)
+{
+ public bool IsFillStrategy => Kind == EngineNameKind.FillStrategy;
+ public bool IsJobEngine => Kind == EngineNameKind.JobEngine;
+}
+
+///
+/// Classifies a name from a front end selector that accepts both single-plate fill strategies
+/// () and whole-job engines ().
+/// Fill strategies win, so "Fill" and "Strip" resolve as strategies; an omitted name means
+/// . Front ends keep their own message wording.
+///
+public static class EngineNameResolver
+{
+ public static EngineNameClassification Classify(string? name)
+ {
+ var trimmed = name?.Trim() ?? string.Empty;
+ try
+ {
+ return new(EngineNameKind.FillStrategy, trimmed, PlateFillService.ResolveStrategy(trimmed));
+ }
+ catch (NotSupportedException)
+ {
+ var kind = NestingEngineRegistry.ResolveName(trimmed) != null
+ ? EngineNameKind.JobEngine
+ : EngineNameKind.Unknown;
+ return new(kind, trimmed, null);
+ }
+ }
+}
diff --git a/OpenNest.Mcp/Tools/NestingTools.cs b/OpenNest.Mcp/Tools/NestingTools.cs
index 3eef33df..1620ce09 100644
--- a/OpenNest.Mcp/Tools/NestingTools.cs
+++ b/OpenNest.Mcp/Tools/NestingTools.cs
@@ -598,29 +598,18 @@ namespace OpenNest.Mcp.Tools
/// Resolves the requested fill strategy; an omitted name means Fill. Returns null when the
/// name is not a single-plate placement strategy.
///
- private static string ResolveStrategy(string engine)
+ private static string ResolveStrategy(string engine) => EngineNameResolver.Classify(engine).FillStrategy;
+
+ private static string EngineError(string engine)
{
- try
- {
- return PlateFillService.ResolveStrategy(engine?.Trim());
- }
- catch (NotSupportedException)
- {
- return null;
- }
+ var classified = EngineNameResolver.Classify(engine);
+ return classified.IsJobEngine
+ ? $"Error: engine '{classified.Name}' is a whole-job engine; this tool supports: {string.Join(", ", PlateFillService.BuiltInStrategies)}. Use autonest_plate for whole-job engines."
+ : UnknownEngineMessage(classified.Name);
}
- private static string EngineError(string engine) => UnknownEngineMessage(engine.Trim());
-
- private static string UnknownEngineMessage(string engineName)
- {
- var isJobEngine = NestingEngineRegistry.AvailableEngines.Any(e =>
- e.Name.Equals(engineName, StringComparison.OrdinalIgnoreCase)
- );
- return isJobEngine
- ? $"Error: engine '{engineName}' is a whole-job engine; this tool supports: {string.Join(", ", PlateFillService.BuiltInStrategies)}. Use autonest_plate for whole-job engines."
- : $"Error: unknown engine '{engineName}'. Fill strategies: {string.Join(", ", PlateFillService.BuiltInStrategies)}; jobs engines: {string.Join(", ", NestingEngineRegistry.AvailableEngines.Select(e => e.Name))}";
- }
+ private static string UnknownEngineMessage(string engineName) =>
+ $"Error: unknown engine '{engineName}'. Fill strategies: {string.Join(", ", PlateFillService.BuiltInStrategies)}; jobs engines: {string.Join(", ", NestingEngineRegistry.AvailableEngines.Select(e => e.Name))}";
private (List items, string error) ParseItems(string drawingNames, string quantities)
{