diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs
index adc8923..62090ec 100644
--- a/OpenNest.Console/Program.cs
+++ b/OpenNest.Console/Program.cs
@@ -79,7 +79,7 @@ static class NestConsole
// single-plate placement strategy. Unknown names exit with the valid choices.
if (options.AutoNest)
{
- // ResolveName also accepts renamed engines' old names (for example Default).
+ // ResolveName also accepts renamed engines' old names (for example Opus55NestingEngine).
if (NestingEngineRegistry.ResolveName(options.Engine) == null)
{
Console.Error.WriteLine(
diff --git a/OpenNest.Engine.Tests/FillStrategyNameTests.cs b/OpenNest.Engine.Tests/FillStrategyNameTests.cs
index d74ad07..bba1c1f 100644
--- a/OpenNest.Engine.Tests/FillStrategyNameTests.cs
+++ b/OpenNest.Engine.Tests/FillStrategyNameTests.cs
@@ -1,23 +1,25 @@
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Placement;
+using OpenNest.Engine.NestingEngines.Default;
using OpenNest.Engine.Tests.Jobs;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests;
///
-/// The fill engine and placement strategy are named "Fill"; the old name "Default" remains a
-/// hidden alias so saved selections, scripts and API requests keep their behavior.
+/// The multi-phase fill engine and placement strategy are named "Fill". "Default" names the
+/// router engine; fill-strategy callers still read "Default" as Fill.
///
public class FillStrategyNameTests
{
[Fact]
- public void FillIsListedAndDefaultIsNot()
+ public void FillIsListedBesideTheDefaultRouter()
{
var engines = NestingEngineRegistry.AvailableEngines.Select(e => e.Name).ToList();
+ Assert.Equal("Default", engines[0]);
Assert.Contains("Fill", engines);
- Assert.DoesNotContain("Default", engines, StringComparer.OrdinalIgnoreCase);
+ Assert.IsType(NestingEngineRegistry.Create("Fill"));
Assert.Equal(new[] { "Fill", "Strip", "Vertical Remnant", "Horizontal Remnant" },
PlateFillService.BuiltInStrategies);
}
@@ -25,22 +27,21 @@ public class FillStrategyNameTests
[Theory]
[InlineData("Default")]
[InlineData(" default ")]
- public void LegacyDefaultEngineNameResolvesToFill(string name)
+ public void DefaultNamesTheRouter(string name)
{
- Assert.Equal("Fill", NestingEngineRegistry.ResolveName(name));
- Assert.IsType(NestingEngineRegistry.Create(name));
- Assert.Equal(NestJobStatus.Complete, NestingEngineRegistry.Create(name).Solve(FiniteStockJobTests.Job(1)).Status);
+ Assert.Equal("Default", NestingEngineRegistry.ResolveName(name));
+ Assert.IsType(NestingEngineRegistry.Create(name));
}
[Fact]
- public void PlugInNamedDefaultCannotShadowFill()
+ public void PlugInNamedDefaultCannotReplaceTheRouter()
{
var before = NestingEngineRegistry.AvailableEngines.Count;
NestingEngineRegistry.Register("Default", "stale plug-in", () => new FixedStrategyNestingEngine("Strip"));
Assert.Equal(before, NestingEngineRegistry.AvailableEngines.Count);
- Assert.Equal("Fill", NestingEngineRegistry.ResolveName("Default"));
+ Assert.IsType(NestingEngineRegistry.Create("Default"));
}
[Theory]
diff --git a/OpenNest.Engine.Tests/NestingEngines/DefaultNestingEngineTests.cs b/OpenNest.Engine.Tests/NestingEngines/DefaultNestingEngineTests.cs
new file mode 100644
index 0000000..86f7d5e
--- /dev/null
+++ b/OpenNest.Engine.Tests/NestingEngines/DefaultNestingEngineTests.cs
@@ -0,0 +1,185 @@
+using OpenNest.Engine.Jobs;
+using OpenNest.Engine.NestingEngines.Default;
+using OpenNest.Engine.NestingEngines.Irregular;
+using OpenNest.Engine.NestingEngines.Rectangles;
+using static OpenNest.Engine.Tests.NestingEngines.JobBuilder;
+using static OpenNest.Engine.Tests.NestingEngines.Shapes;
+
+namespace OpenNest.Engine.Tests.NestingEngines;
+
+public class DefaultNestingEngineTests
+{
+ // Two 4x4 squares on 10x10 sheets with no spacing: either both on one sheet (cost 100),
+ // or one per sheet (cost 200), or one placed and one unplaced (100 + a 100 penalty).
+ private static NestJob SquaresJob() =>
+ Job([Part("p", Rectangle(4, 4), 2, RotationPolicy.Fixed(0))], [Stock("s", 10, 10)]);
+
+ private static NestJobResult Layout(NestJob job, params (double X, double Y)[][] sheets)
+ {
+ var builder = new NestJobResultBuilder(job);
+ foreach (var sheet in sheets)
+ builder.AddSheet(job.Plates[0], sheet.Select(p => ("p", p.X, p.Y, 0.0)));
+ return builder.Build(builder.IsComplete ? NestJobStopReason.Completed : NestJobStopReason.NoPlacementFound);
+ }
+
+ private static (double X, double Y)[] Sheet(params (double X, double Y)[] poses) => poses;
+
+ private static NestJobResult OneSheet(NestJob job) => Layout(job, Sheet((0, 0), (5, 0)));
+ private static NestJobResult OneSheetHigher(NestJob job) => Layout(job, Sheet((0, 5), (5, 5)));
+ private static NestJobResult TwoSheets(NestJob job) => Layout(job, Sheet((0, 0)), Sheet((0, 0)));
+ private static NestJobResult Overlapping(NestJob job) => Layout(job, Sheet((0, 0), (1, 0)));
+ private static NestJobResult OnePlaced(NestJob job) => Layout(job, Sheet((0, 0)));
+
+ private sealed class Stub(Func?, CancellationToken, NestJobResult> solve) : INestingEngine
+ {
+ public NestJobResult Solve(NestJob job, IProgress? progress = null, CancellationToken token = default) =>
+ solve(job, progress, token);
+ }
+
+ private static Func Returns(Func layout) =>
+ () => new Stub((job, _, _) => layout(job));
+
+ private static NestJobResult Choose(NestJob job, params Func[] candidates) =>
+ new DefaultNestingEngine(candidates).Solve(job);
+
+ [Fact]
+ public void CheaperValidLayoutWinsInEitherOrder()
+ {
+ var job = SquaresJob();
+ var cheap = OneSheet(job);
+ var dear = TwoSheets(job);
+
+ Assert.Same(cheap, Choose(job, Returns(_ => dear), Returns(_ => cheap)));
+ Assert.Same(cheap, Choose(job, Returns(_ => cheap), Returns(_ => dear)));
+ }
+
+ [Fact]
+ public void InvalidLayoutLosesToAValidOneEvenWhenSmaller()
+ {
+ var job = SquaresJob();
+ var invalid = Overlapping(job);
+ var valid = TwoSheets(job);
+ Assert.NotEmpty(NestLayoutCheck.Violations(job, invalid));
+
+ Assert.Same(valid, Choose(job, Returns(_ => invalid), Returns(_ => valid)));
+ Assert.Same(valid, Choose(job, Returns(_ => valid), Returns(_ => invalid)));
+ }
+
+ [Fact]
+ public void PlacingEveryPartWinsAtEqualCost()
+ {
+ var job = SquaresJob();
+ var complete = TwoSheets(job);
+ var partial = OnePlaced(job);
+ Assert.Equal(NestJobCost.Evaluate(job, complete), NestJobCost.Evaluate(job, partial), 9);
+
+ Assert.Same(complete, Choose(job, Returns(_ => partial), Returns(_ => complete)));
+ Assert.Same(complete, Choose(job, Returns(_ => complete), Returns(_ => partial)));
+ }
+
+ [Fact]
+ public void EqualLayoutsKeepCandidateOrder()
+ {
+ var job = SquaresJob();
+ var low = OneSheet(job);
+ var high = OneSheetHigher(job);
+
+ Assert.Same(low, Choose(job, Returns(_ => low), Returns(_ => high)));
+ Assert.Same(high, Choose(job, Returns(_ => high), Returns(_ => low)));
+ }
+
+ [Fact]
+ public void ACrashingCandidateIsSkipped()
+ {
+ var job = SquaresJob();
+ var valid = TwoSheets(job);
+
+ var result = Choose(job, () => new Stub((_, _, _) => throw new InvalidOperationException("boom")), Returns(_ => valid));
+
+ Assert.Same(valid, result);
+ }
+
+ [Fact]
+ public void ACandidateWithoutAResultIsSkipped()
+ {
+ var job = SquaresJob();
+ var valid = TwoSheets(job);
+
+ Assert.Same(valid, Choose(job, () => new Stub((_, _, _) => null!), Returns(_ => valid)));
+ }
+
+ [Fact]
+ public void WhenEveryCandidateCrashesTheFirstFailureIsRethrown()
+ {
+ var job = SquaresJob();
+
+ var error = Assert.Throws(() => Choose(job,
+ () => new Stub((_, _, _) => throw new InvalidOperationException("first")),
+ () => new Stub((_, _, _) => throw new InvalidOperationException("second"))));
+
+ Assert.Equal("first", error.Message);
+ }
+
+ [Fact]
+ public void CancellationStopsTheSearch()
+ {
+ var job = SquaresJob();
+ using var cancellation = new CancellationTokenSource();
+ var laterRan = false;
+
+ Assert.ThrowsAny(() => new DefaultNestingEngine(new Func[]
+ {
+ () => new Stub((_, _, token) => { cancellation.Cancel(); token.ThrowIfCancellationRequested(); return OneSheet(job); }),
+ () => { laterRan = true; return new Stub((j, _, _) => OneSheet(j)); },
+ }).Solve(job, token: cancellation.Token));
+ Assert.False(laterRan);
+ }
+
+ [Fact]
+ public void OnlyTheChosenLayoutCommitsAreReported()
+ {
+ var job = SquaresJob();
+ Func Reporting(Func layout) => () => new Stub((j, progress, _) =>
+ {
+ progress?.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, "s", 0, 0, 0));
+ var result = layout(j);
+ foreach (var plate in result.Plates)
+ progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted, "s", plate.PlateIndex, 99, 99));
+ return result;
+ });
+ var reports = new List();
+
+ var result = new DefaultNestingEngine(new[] { Reporting(TwoSheets), Reporting(OneSheet) })
+ .Solve(job, new Capture(reports.Add));
+
+ Assert.Single(result.Plates);
+ Assert.Equal(2, reports.Count(r => r.Stage == NestJobStage.EvaluatingCandidate));
+ var commit = Assert.Single(reports, r => r.Stage == NestJobStage.PlateCommitted);
+ Assert.Equal((0, 1, 2), (commit.PlateIndex, commit.CommittedPlates, commit.CommittedParts));
+ Assert.Equal(NestJobStage.PlateCommitted, reports[^1].Stage);
+ }
+
+ [Fact]
+ public void BuiltInCandidatesReturnTheCheaperOfIrregularAndRectangles()
+ {
+ var job = Job([Part("disc", Disc(2), 6), Part("ell", LShape(9, 7, 3), 4), Part("plate", Rectangle(12, 5), 3)],
+ [Stock("a", 24, 30, 0.25), Stock("b", 30, 40, 0.25)]);
+
+ var result = new DefaultNestingEngine().Solve(job);
+
+ LayoutAssert.Valid(job, result);
+ Assert.Equal(NestJobStatus.Complete, result.Status);
+ var best = new INestingEngine[] { new IrregularNestingEngine(), new RectanglesNestingEngine() }
+ .Select(engine => engine.Solve(job))
+ .Where(r => NestLayoutCheck.Violations(job, r).Count == 0 && r.Status == NestJobStatus.Complete)
+ .Min(r => NestJobCost.Evaluate(job, r));
+ Assert.Equal(best, NestJobCost.Evaluate(job, result), 9);
+ }
+
+ private sealed class Capture(Action action) : IProgress
+ {
+ public void Report(NestJobProgress value) => action(value);
+ }
+}
+
+public sealed class DefaultContractTests : EngineContractTests { }
diff --git a/OpenNest.Engine/Jobs/NestingEngineRegistry.cs b/OpenNest.Engine/Jobs/NestingEngineRegistry.cs
index 6f8b2d5..3c8389e 100644
--- a/OpenNest.Engine/Jobs/NestingEngineRegistry.cs
+++ b/OpenNest.Engine/Jobs/NestingEngineRegistry.cs
@@ -5,14 +5,16 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
+using OpenNest.Engine.NestingEngines.Default;
using OpenNest.Engine.NestingEngines.Irregular;
using OpenNest.Engine.NestingEngines.Rectangles;
namespace OpenNest.Engine.Jobs;
///
-/// Registry of whole-job implementations. The four production
-/// strategies are exposed through . Callers choose an
+/// Registry of whole-job implementations. "Default" chooses among the
+/// built-in engines per job; the four fill strategies are exposed through
+/// . Callers choose an
/// engine explicitly from ; there is no process-global active
/// selection. Plug-ins loaded from an Engines/ folder register under their CLR type name.
///
@@ -28,11 +30,17 @@ public static class NestingEngineRegistry
{
["Opus55NestingEngine"] = "Irregular",
["RectanglesNestingEngine"] = "Rectangles",
- ["Default"] = "Fill",
};
static NestingEngineRegistry()
{
+ // Listed first: the engine front ends use when the caller names none.
+ Register(
+ "Default",
+ "Any job: runs Irregular and Rectangles and keeps the cheapest valid layout",
+ () => new DefaultNestingEngine()
+ );
+
Register(
"Rectangles",
"Plain and near-rectangular parts: maximal-rectangles box packing",
diff --git a/OpenNest.Engine/NestingEngines/Default/DefaultNestingEngine.cs b/OpenNest.Engine/NestingEngines/Default/DefaultNestingEngine.cs
new file mode 100644
index 0000000..bb6ee1f
--- /dev/null
+++ b/OpenNest.Engine/NestingEngines/Default/DefaultNestingEngine.cs
@@ -0,0 +1,137 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using OpenNest.Engine.Jobs;
+using OpenNest.Engine.NestingEngines.Irregular;
+using OpenNest.Engine.NestingEngines.Rectangles;
+
+namespace OpenNest.Engine.NestingEngines.Default;
+
+///
+/// The engine used when the caller names none. It runs each candidate engine on the whole job,
+/// checks every result with and returns the best one: a valid
+/// layout before an invalid one, then the fewest unplaced parts, then the lowest
+/// ; remaining ties keep candidate order.
+///
+/// Candidates are Irregular, then Rectangles. Neither wins every job: Rectangles packs each part
+/// as its box, which is often best for plain plates and is a cheap, valid fallback for the rest.
+/// A candidate that throws or returns no result is skipped; when every candidate throws, the
+/// first exception is rethrown. Cancellation stops the search. Candidates' plate commits are not forwarded; the chosen
+/// result's commits are reported once it is selected.
+///
+/// Deterministic: candidates run one after another and the choice uses no clock.
+///
+public sealed class DefaultNestingEngine : INestingEngine
+{
+ private readonly IReadOnlyList> candidates;
+
+ public DefaultNestingEngine()
+ : this(new Func[] { () => new IrregularNestingEngine(), () => new RectanglesNestingEngine() })
+ {
+ }
+
+ /// Test seam: candidate engines in tie-break order.
+ internal DefaultNestingEngine(IReadOnlyList> candidates)
+ {
+ ArgumentNullException.ThrowIfNull(candidates);
+ if (candidates.Count == 0)
+ throw new ArgumentException("At least one candidate engine is required.", nameof(candidates));
+ this.candidates = candidates;
+ }
+
+ public NestJobResult Solve(
+ NestJob job,
+ IProgress? progress = null,
+ CancellationToken token = default
+ )
+ {
+ ArgumentNullException.ThrowIfNull(job);
+ token.ThrowIfCancellationRequested();
+ var forward = progress == null ? null : new CandidateProgress(progress);
+ Scored? best = null;
+ ExceptionDispatchInfo? firstFailure = null;
+
+ for (var order = 0; order < candidates.Count; order++)
+ {
+ token.ThrowIfCancellationRequested();
+ NestJobResult? result;
+ try
+ {
+ result = candidates[order]().Solve(job, forward, token);
+ }
+ catch (Exception ex)
+ {
+ // A cancelled solve is rethrown by the token checks around each candidate.
+ firstFailure ??= ExceptionDispatchInfo.Capture(ex);
+ continue;
+ }
+ if (result == null)
+ continue;
+
+ var scored = Scored.Of(job, result, order);
+ if (best == null || scored.IsBetterThan(best))
+ best = scored;
+ }
+
+ token.ThrowIfCancellationRequested();
+ if (best == null)
+ {
+ firstFailure?.Throw();
+ throw new InvalidOperationException("No candidate engine returned a result.");
+ }
+
+ ReportCommits(best.Result, progress);
+ return best.Result;
+ }
+
+ private static void ReportCommits(NestJobResult result, IProgress? progress)
+ {
+ if (progress == null)
+ return;
+ var parts = 0;
+ for (var i = 0; i < result.Plates.Count; i++)
+ {
+ var plate = result.Plates[i];
+ parts += plate.Placements.Count;
+ progress.Report(new NestJobProgress(NestJobStage.PlateCommitted, plate.StockId, plate.PlateIndex, i + 1, parts));
+ }
+ }
+
+ private sealed record Scored(NestJobResult Result, bool Valid, int Unplaced, double Cost, int Order)
+ {
+ public static Scored Of(NestJob job, NestJobResult result, int order)
+ {
+ var valid = NestLayoutCheck.Violations(job, result).Count == 0;
+ var placed = result.Plates.Sum(p => p.Placements.Count);
+ var unplaced = System.Math.Max(0, job.Parts.Sum(p => p.Quantity) - placed);
+ // Cost needs every placement to name a known part, which only a valid result guarantees.
+ var cost = valid ? NestJobCost.Evaluate(job, result) : double.PositiveInfinity;
+ return new Scored(result, valid, unplaced, cost, order);
+ }
+
+ public bool IsBetterThan(Scored other)
+ {
+ if (Valid != other.Valid)
+ return Valid;
+ if (Unplaced != other.Unplaced)
+ return Unplaced < other.Unplaced;
+ var scale = System.Math.Max(1, System.Math.Max(System.Math.Abs(Cost), System.Math.Abs(other.Cost)));
+ if (double.IsFinite(Cost) && double.IsFinite(other.Cost) && System.Math.Abs(Cost - other.Cost) > 1e-9 * scale)
+ return Cost < other.Cost;
+ return Order < other.Order;
+ }
+ }
+
+ /// Forwards a candidate's progress except plate commits, which belong to the chosen result.
+ private sealed class CandidateProgress(IProgress inner) : IProgress
+ {
+ public void Report(NestJobProgress value)
+ {
+ if (value != null && value.Stage != NestJobStage.PlateCommitted)
+ inner.Report(value);
+ }
+ }
+}
diff --git a/README.md b/README.md
index f0afd69..8c39da6 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,7 @@ Engines implement `INestingEngine.Solve(NestJob)`. Desktop Auto Nest, console au
| Engine | Description |
|--------|-------------|
+| **Default** | Any job: runs Irregular and Rectangles and keeps the cheapest valid layout |
| **Rectangles** | Plain and near-rectangular plates: maximal-rectangles box packing |
| **Irregular** | Irregular profiles: no-fit-polygon frontier packing |
| **Fill** | Multi-phase: linear fill → pairs → rect best-fit → extents (named Default in earlier releases) |
diff --git a/docs/automatic-nesting.md b/docs/automatic-nesting.md
index 8cc8014..69df1d7 100644
--- a/docs/automatic-nesting.md
+++ b/docs/automatic-nesting.md
@@ -42,7 +42,7 @@ The pre-pipeline multi-plate orchestrator and plate-size optimizer have been rem
## Console and MCP
-Console `--autonest` uses the selected jobs engine against one physical sheet. The selected plate's old parts are replaced only after acceptance; other plates are unchanged. Default demand is still one of each drawing unless `--quantity` is supplied. A partially fulfilled, valid result may be saved; a successful placement is not a claim that all demand was met.
+Console `--autonest` uses the jobs engine named by `--engine` (Default when omitted) against one physical sheet. The selected plate's old parts are replaced only after acceptance; other plates are unchanged. Default demand is still one of each drawing unless `--quantity` is supplied. A partially fulfilled, valid result may be saved; a successful placement is not a claim that all demand was met.
Invalid output is printed and rejected with exit code 2 without saving or posting. `--allow-invalid` explicitly accepts representable layout violations. Malformed output, multiple returned sheets, and zero placements are never saved by this path. Unknown engines exit 1. `--autonest --keep-parts` rejects an occupied target: use the plain interactive fill path for existing obstacles instead.
diff --git a/docs/nesting-engines.md b/docs/nesting-engines.md
index 3b42ce3..05ae758 100644
--- a/docs/nesting-engines.md
+++ b/docs/nesting-engines.md
@@ -11,11 +11,18 @@ in `OpenNest.Engine/NestingEngines//`, its tests in `OpenNest.Engine.Tests
| Engine | Best for | Method |
|---|---|---|
+| Default | Any job; used when no engine is named | Runs Irregular, then Rectangles, checks both layouts with the layout check and keeps the best: valid first, then fewest unplaced parts, then lowest salvage-credited cost; ties keep Irregular |
| Rectangles | Plain and near-rectangular plates | Each part packed as the box of its material at its minimum-area rotation, using a maximal-rectangles free list; stock chosen sheet by sheet by salvage-credited look-ahead cost |
| Irregular | Irregular profiles | No-fit-polygon frontier packing with gap filling and best-fit pairs, six whole-job strategy variants and a tail re-plan |
| StockLadder | Caller-supplied stock ladders | Constrained-first fill with equivalent-demand area repacking |
| Fill, Strip, Vertical Remnant, Horizontal Remnant | Single-strategy fills | The fixed placement strategies behind interactive fill; Fill is the multi-phase lattice fill (linear, pairs, rectangle best-fit, remainder) |
+Neither Irregular nor Rectangles wins every job, even within its own lane, so Default runs both
+rather than choosing by part shape; Rectangles adds little time and is also the fallback when an
+Irregular layout fails the check. A candidate that throws is skipped. Default routes the whole job:
+engines cannot share a sheet, so a job mixing plain and irregular parts goes to both engines whole.
+A future circle/ring engine joins Default as another candidate.
+
Rectangles places irregular parts validly, but only as their bounding boxes; it never nests into a
notch or hole. Box sides account for how the layout check flattens arcs, so round-edged parts stay
valid at box contact.
@@ -85,10 +92,11 @@ desktop selections, scripts and API requests keep working:
|---|---|
| `Opus55NestingEngine` | Irregular |
| `RectanglesNestingEngine` | Rectangles |
-| `Default` | Fill |
-Fill strategy calls (interactive fill, `PlateFillService`, console fill without `--autonest`, MCP fill
-tools) also accept `Default` as Fill.
+In v0.3.0 and earlier, `Default` was the multi-phase fill engine now named Fill. `Default` now names the
+choosing engine above, so saved selections, scripts and API requests that name it get that
+engine. Fill-strategy calls (interactive fill, `PlateFillService`, console fill without
+`--autonest`, MCP fill tools) still read `Default` as Fill.
Gpt6Astra and Qwen38FlashNext are no longer shipped and have no alias. A saved selection of either
falls back to the default engine with the usual status-bar warning.
diff --git a/scripts/ReleaseSmoke/Program.cs b/scripts/ReleaseSmoke/Program.cs
index 2d59729..d386ac4 100644
--- a/scripts/ReleaseSmoke/Program.cs
+++ b/scripts/ReleaseSmoke/Program.cs
@@ -20,7 +20,7 @@ try
var create = registry.GetMethod("Create")!;
// Every built-in engine the desktop offers must instantiate from the packaged assembly.
- string[] expected = ["Rectangles", "Irregular", "StockLadder", "Fill", "Strip", "Vertical Remnant", "Horizontal Remnant"];
+ string[] expected = ["Default", "Rectangles", "Irregular", "StockLadder", "Fill", "Strip", "Vertical Remnant", "Horizontal Remnant"];
foreach (var name in expected)
{
var engine = create.Invoke(null, [name])!;