diff --git a/CLAUDE.md b/CLAUDE.md
index 6eca997..077270e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -41,13 +41,13 @@ Domain model, geometry, and CNC primitives organized into namespaces:
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
### OpenNest.Engine (class library, depends on Core)
-Nesting algorithms provide both a legacy single-plate API and a whole-job API. The legacy path centers on `NestEngineBase`, `DefaultNestEngine` (formerly `NestEngine`), and the global `NestEngineRegistry`. New job callers use immutable, ID-based contracts in `Jobs/`: `INestingEngine.Solve(NestJob)` returns `NestJobResult`; `NestJobRunner` alone commits demand and finite/unlimited stock accounting; `IPlateNester` only proposes a one-sheet candidate; and `PlateNesterFactory` resolves a named strategy without reading or changing the process-global registry.
+Nesting algorithms use the jobs-only API. `INestingEngine.Solve(NestJob)` returns `NestJobResult`; `NestJobRunner` alone commits demand and finite/unlimited stock accounting; `IPlateNester` only proposes a one-sheet candidate; and `PlateNesterFactory` resolves a named built-in placement strategy.
- **Whole-job API (`Jobs/`)**: `NestJob` owns part requirements, physical stock, and options for one material/thickness/unit system. `PartGeometrySnapshot` contains owned flat rapid/line/arc geometry; results contain stock IDs and placement poses (radians), not mutable desktop models. `NestJobPlacementValidator` validates contours, rotation, usable work area, overlap, and spacing before accounting commits. The runner selects valid trial candidates greedily by priority vector, sheet area, envelope, and input order; an incomplete result reports why but does not prove geometric impossibility. `DrawingJobMapper` and `NestResultMaterializer` are the domain-boundary adapters.
-- **Placement boundary (`Jobs/Placement/`, `Jobs/Adapters/`)**: `DefaultPlateNester` and `StripPlateNester` are migrated built-ins with run-scoped private geometry; `LegacyPlateNesterAdapter` remains for remnant strategies and legacy plugins/callers during rollout. Job-path identity is reference-based rather than drawing name; `PlateOptimizer` retains legacy name-based helpers and is deliberately outside the runner path.
-- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`. Existing desktop, CLI, and MCP callers remain on this compatibility path until separate migrations preserve their existing-plate, preview, and accept/cancel semantics.
-- **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`.
-- **NestEngineRegistry**: Static registry — `Create(Plate)` factory, `ActiveEngineName` global selection, `LoadPlugins(directory)` for DLL discovery. All callsites use `NestEngineRegistry.Create(plate)` except `BruteForceRunner` which uses `new DefaultNestEngine(plate)` directly for training consistency.
+- **Placement boundary (`Jobs/Placement/`, `Jobs/Adapters/`)**: `DefaultPlateNester`, `StripPlateNester`, and `RemnantPlateNester` are built-ins with run-scoped private geometry. `PlateFillService` is the public single-plate proposal service for interactive fill/group/pack flows; it returns parts without mutating caller-owned plates. Job-path identity is reference-based rather than drawing name; `PlateOptimizer` retains name-based helpers and remains outside the runner path.
+- **Filler pipeline (`Jobs/Placement/Fillers/`)**: internal `DefaultPlateFiller`, `StripPlateFiller`, and policy-backed `RemnantPlateFiller` implement the standard single-plate geometry pipeline. `Default` runs the Linear, Pairs, RectBestFit, and Extents phases; remnant variants preserve their distinct comparer, direction, trim-axis, and angle-ordering policies.
+- **Engine registration**: `NestingEngineRegistry` holds whole-job `INestingEngine` implementations including the four fixed strategies and `StockLadder`. It loads plug-ins that implement `INestingEngine` and have a public parameterless constructor. Plug-ins for the removed single-plate inheritance API are not binary compatible.
+- **IFillComparer**: Interface enabling filler-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), and `HorizontalRemnantComparer` (minimize Y-extent) are grouped into `FillPolicy` on `FillContext`.
- **Fill/** (`namespace OpenNest.Engine.Fill`): Fill algorithms — `FillLinear` (grid-based), `FillExtents` (extents-based pair tiling), `PairFiller` (interlocking pairs), `ShrinkFiller`, `RemnantFiller`/`RemnantFinder`, `Compactor` (post-fill gravity compaction), `FillScore` (lexicographic comparison: count > utilization > compactness), `Pattern`/`PatternTiler`, `PartBoundary`, `RotationAnalysis`, `AngleCandidateBuilder`, `BestCombination`, `AccumulatingProgress`.
- **Strategies/** (`namespace OpenNest.Engine.Strategies`): Pluggable fill strategy layer — `IFillStrategy` interface, `FillContext`, `FillStrategyRegistry` (auto-discovers strategies via reflection, supports plugin DLLs), `FillHelpers`. Built-in strategies: `LinearFillStrategy`, `PairsFillStrategy`, `RectBestFitStrategy`, `ExtentsFillStrategy`.
- **BestFit/** (`namespace OpenNest.Engine.BestFit`): NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups.
diff --git a/OpenNest.Engine.Tests/Jobs/NestJobEngineSelectionTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobEngineSelectionTests.cs
index 655769a..2dbeac5 100644
--- a/OpenNest.Engine.Tests/Jobs/NestJobEngineSelectionTests.cs
+++ b/OpenNest.Engine.Tests/Jobs/NestJobEngineSelectionTests.cs
@@ -1,4 +1,3 @@
-using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
@@ -6,15 +5,14 @@ using OpenNest.Engine.Jobs.Adapters;
namespace OpenNest.Engine.Tests.Jobs;
///
-/// Strategy selection must be instance-scoped: explicit engine choices work without touching the
-/// process-global NestEngineRegistry.ActiveEngineName, and unknown strategies are rejected.
+/// Strategy selection is instance-scoped: explicit engine choices complete independently,
+/// and unknown strategies are rejected.
///
public class NestJobEngineSelectionTests
{
[Fact]
- public void ExplicitDefaultAndStripSelectionsDoNotTouchGlobalRegistry()
+ public void ExplicitDefaultAndStripSelectionsCompleteIndependently()
{
- var original = NestEngineRegistry.ActiveEngineName;
var job = FiniteStockJobTests.Job(1);
var defaultResult = new NestJobRunner(PlateNesterFactory.Create).Solve(job);
@@ -24,14 +22,11 @@ public class NestJobEngineSelectionTests
new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip"))
);
Assert.Equal(NestJobStatus.Complete, stripResult.Status);
-
- Assert.Equal(original, NestEngineRegistry.ActiveEngineName);
}
[Fact]
- public void FactoryResolvesNamedEnginesWithoutGlobalState()
+ public void FactoryResolvesEachNamedBuiltInStrategy()
{
- var original = NestEngineRegistry.ActiveEngineName;
var defaultNester = PlateNesterFactory.Create("Default");
var stripNester = PlateNesterFactory.Create("Strip");
var verticalNester = PlateNesterFactory.Create("Vertical Remnant");
@@ -42,7 +37,6 @@ public class NestJobEngineSelectionTests
Assert.NotNull(verticalNester);
Assert.NotNull(horizontalNester);
Assert.NotSame(defaultNester, stripNester);
- Assert.Equal(original, NestEngineRegistry.ActiveEngineName);
}
[Fact]
@@ -57,22 +51,6 @@ public class NestJobEngineSelectionTests
);
}
- [Fact]
- public void LegacyRegistryPluginsDoNotLeakIntoJobSelection()
- {
- // A plugin engine registered through the legacy registry must not become selectable
- // through the job factory; the new boundary is independent of registry state.
- NestEngineRegistry.Register(
- "ProbePlugin",
- "test plugin",
- plate => new PluginShapeEngine(plate)
- );
- Assert.Contains(NestEngineRegistry.AvailableEngines, e => e.Name == "ProbePlugin");
-
- Assert.Throws(() => PlateNesterFactory.Create("ProbePlugin"));
- Assert.NotNull(PlateNesterFactory.Create("Default"));
- }
-
[Fact]
public void StripEngineEndToEndPlacesAndAccounts()
{
@@ -89,10 +67,4 @@ public class NestJobEngineSelectionTests
foreach (var f in result.Fulfillment)
Assert.Equal(f.Requested, f.Placed + f.Unplaced);
}
-
- private sealed class PluginShapeEngine(Plate plate) : NestEngineBase(plate)
- {
- public override string Name => "ProbePlugin";
- public override string Description => "registered via legacy registry only";
- }
}
diff --git a/OpenNest.Engine.Tests/Jobs/NestJobExampleTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobExampleTests.cs
index fec83f4..c513fe7 100644
--- a/OpenNest.Engine.Tests/Jobs/NestJobExampleTests.cs
+++ b/OpenNest.Engine.Tests/Jobs/NestJobExampleTests.cs
@@ -2,13 +2,14 @@ using OpenNest.CNC;
using OpenNest.Geometry;
using Xunit;
using OpenNest.Engine.Jobs;
+using OpenNest.Engine.Jobs.Placement;
namespace OpenNest.Engine.Tests.Jobs;
///
/// Runnable end-to-end example of the whole-job engine API: multiple part requirements, multiple plate
/// sizes, and an enumeration of every returned plate, placement, leftover, and stock line. Also the
-/// documentation checkpoint for the legacy caller boundaries that have not been migrated (task 8).
+/// documentation checkpoint for the public single-plate placement service used by interactive flows.
///
public class NestJobExampleTests
{
@@ -160,36 +161,27 @@ public class NestJobExampleTests
}
///
- /// Legacy caller boundaries documented for task 8 — these paths still use the old single-plate
- /// engine entry points and are deliberately NOT migrated in this slice. Verified against source at
- /// the time of writing:
- /// - Desktop UI: OpenNest/Forms/MainForm.cs RunAutoNestAsync (~line 1004) and NestSinglePlateAsync
- /// (~line 1087) orchestrate plate-first and part-first fills directly against NestEngineRegistry
- /// engines. Migration requires preserving populated-plate editing, preview routing, and
- /// Accept-versus-Cancel semantics — a separate adapter design (documented follow-on).
- /// - CLI: OpenNest.Console/Program.cs calls engine.Nest(...) (~line 316) on one plate. Migration
- /// point: build a NestJob from imported drawings plus CLI plate options and call Solve once.
- /// - MCP: OpenNest.Mcp/Tools/NestingTools.cs calls engine.Nest(...) (~line 239) on the session
- /// plate. Migration point: same single job call, materialized through NestResultMaterializer.
- /// The public API (OpenNest.Api NestRunner) already delegates to NestJobRunner.Solve (task 6).
- /// This test exercises the legacy compatibility signature so an accidental removal of that entry
- /// point breaks the documented contract.
+ /// The public placement service is the single-plate contract for preview-driven flows. It proposes
+ /// parts without mutating the caller's plate, so the UI can accept or discard them explicitly.
///
[Fact]
- public void LegacyCompatibilityEntryPointsStillExist()
+ public void PlateFillService_ProposesSinglePlatePartsWithoutMutatingPlate()
{
var plate = new Plate { Size = new Size(300.0, 200.0), Quadrant = 1 };
- var drawing = new Drawing("legacy", TestDrawingFactory.Rectangle(50.0, 50.0));
+ var drawing = new Drawing("preview", TestDrawingFactory.Rectangle(50.0, 50.0));
var item = new NestItem { Drawing = drawing, Quantity = 1 };
- // MainForm/Console/MCP still reach the legacy single-plate signature unchanged; the engine
- // returns placed Parts for the caller to attach (legacy paths do not attach on their own).
- var engine = NestEngineRegistry.Create(plate);
- var parts = engine.Nest(new List { item }, null, CancellationToken.None);
+ var parts = PlateFillService.Nest(
+ "Default",
+ plate,
+ new List { item },
+ progress: null,
+ token: CancellationToken.None
+ );
- Assert.NotNull(engine);
var placed = Assert.Single(parts);
Assert.Same(drawing, placed.BaseDrawing);
+ Assert.Empty(plate.Parts);
}
private static NestJobPart Part(
diff --git a/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs b/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs
index 00ba923..ac69144 100644
--- a/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs
+++ b/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs
@@ -11,55 +11,31 @@ public class PlateFillerContractTests
[InlineData("Default")]
[InlineData("Vertical Remnant")]
[InlineData("Horizontal Remnant")]
- public void StandardPlateFiller_Fill_MatchesCompatibilityFacade(string strategy)
+ public void StandardPlateFiller_Fill_ReturnsPartsBoundToInputDrawing(string strategy)
{
- var directPlate = new Plate(new Size(30, 50));
- var facadePlate = new Plate(new Size(30, 50));
- var directDrawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
- var facadeDrawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
- var directFiller = CreateFiller(strategy, directPlate);
- var facade = CreateFacade(strategy, facadePlate);
+ var plate = new Plate(new Size(30, 50));
+ var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
+ var filler = CreateFiller(strategy, plate);
- var directParts = directFiller.Fill(
- new NestItem { Drawing = directDrawing, Quantity = 4 },
- directPlate.WorkArea(),
- null,
- CancellationToken.None
- );
- var facadeParts = facade.Fill(
- new NestItem { Drawing = facadeDrawing, Quantity = 4 },
- facadePlate.WorkArea(),
+ var parts = filler.Fill(
+ new NestItem { Drawing = drawing, Quantity = 4 },
+ plate.WorkArea(),
null,
CancellationToken.None
);
- Assert.Equal(facade.WinnerPhase, directFiller.WinnerPhase);
- Assert.Equal(
- facade.PhaseResults.Select(result => (result.Phase, result.PartCount)),
- directFiller.PhaseResults.Select(result => (result.Phase, result.PartCount))
- );
- Assert.Equal(
- facade.AngleResults.Select(result => (result.AngleDeg, result.Direction, result.PartCount)),
- directFiller.AngleResults.Select(result => (result.AngleDeg, result.Direction, result.PartCount))
- );
- Assert.Equal(facadeParts.Count, directParts.Count);
- for (var i = 0; i < directParts.Count; i++)
- {
- Assert.Same(directDrawing, directParts[i].BaseDrawing);
- Assert.Same(facadeDrawing, facadeParts[i].BaseDrawing);
- Assert.Equal(facadeParts[i].Location.X, directParts[i].Location.X, 9);
- Assert.Equal(facadeParts[i].Location.Y, directParts[i].Location.Y, 9);
- Assert.Equal(facadeParts[i].Rotation, directParts[i].Rotation, 9);
- }
+ Assert.NotEmpty(parts);
+ Assert.All(parts, part => Assert.Same(drawing, part.BaseDrawing));
+ Assert.NotEmpty(filler.PhaseResults);
}
[Fact]
- public void CompatibilityDefaultFacade_UsesOverriddenAngleSelection()
+ public void DefaultPlateFiller_UsesOverriddenAngleSelection()
{
var plate = new Plate(new Size(30, 50));
- var engine = new AngleProbeDefaultNestEngine(plate);
+ var filler = new AngleProbeDefaultPlateFiller(plate);
- var parts = engine.Fill(
+ var parts = filler.Fill(
new NestItem
{
Drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4)),
@@ -71,18 +47,18 @@ public class PlateFillerContractTests
);
Assert.NotEmpty(parts);
- Assert.True(engine.BuildAnglesCalled);
+ Assert.True(filler.BuildAnglesCalled);
}
[Fact]
- public void CompatibilityDefaultFacade_Nest_UsesOverriddenFillAndPackArea()
+ public void DefaultPlateFiller_Nest_UsesOverriddenFillAndPackArea()
{
var plate = new Plate(new Size(100, 100));
var fillDrawing = new Drawing("fill", TestDrawingFactory.Rectangle(10, 10));
var packDrawing = new Drawing("pack", TestDrawingFactory.Rectangle(10, 10));
- var engine = new FillAndPackProbeDefaultNestEngine(plate, fillDrawing, packDrawing);
+ var filler = new FillAndPackProbeDefaultPlateFiller(plate, fillDrawing, packDrawing);
- var parts = engine.Nest(
+ var parts = filler.Nest(
new List
{
new() { Drawing = fillDrawing, Quantity = 10 },
@@ -92,17 +68,16 @@ public class PlateFillerContractTests
CancellationToken.None
);
- Assert.Equal(1, engine.FillCalls);
- Assert.Equal(1, engine.PackAreaCalls);
+ Assert.Equal(1, filler.FillCalls);
+ Assert.Equal(1, filler.PackAreaCalls);
Assert.Equal(11, parts.Count);
}
[Fact]
- public void StripPlateFiller_Nest_MatchesCompatibilityFacade()
+ public void StripPlateFiller_Nest_ReturnsPlacedPartsAndDeductsInput()
{
- var directPlate = new Plate(new Size(30, 50));
- var facadePlate = new Plate(new Size(30, 50));
- var directItems = new List
+ var plate = new Plate(new Size(30, 50));
+ var items = new List
{
new()
{
@@ -120,48 +95,29 @@ public class PlateFillerContractTests
Quantity = 3,
},
};
- var facadeItems = new List
- {
- new()
- {
- Drawing = new Drawing("rect-a", TestDrawingFactory.Rectangle(6, 4)),
- Quantity = 5,
- },
- new()
- {
- Drawing = new Drawing("rect-b", TestDrawingFactory.Rectangle(4, 3)),
- Quantity = 4,
- },
- new()
- {
- Drawing = new Drawing("rect-c", TestDrawingFactory.Rectangle(2, 7)),
- Quantity = 3,
- },
- };
- var directFiller = new StripPlateFiller(directPlate) { PlateNumber = 7 };
- var facade = new StripNestEngine(facadePlate) { PlateNumber = 7 };
+ var filler = new StripPlateFiller(plate) { PlateNumber = 7 };
- var directParts = directFiller.Nest(directItems, null, CancellationToken.None);
- var facadeParts = facade.Nest(facadeItems, null, CancellationToken.None);
+ var parts = filler.Nest(items, null, CancellationToken.None);
- AssertEquivalentLayouts(facadeParts, directParts);
- Assert.Equal(facadeItems.Select(item => item.Quantity), directItems.Select(item => item.Quantity));
+ Assert.NotEmpty(parts);
+ Assert.All(parts, part => Assert.Contains(items, item => ReferenceEquals(item.Drawing, part.BaseDrawing)));
+ Assert.All(items, item => Assert.InRange(item.Quantity, 0, 5));
}
[Fact]
- public void CompatibilityStripFacade_Nest_UsesOverriddenPackArea()
+ public void StripPlateFiller_Nest_UsesOverriddenPackArea()
{
var plate = new Plate(new Size(100, 100));
var drawing = new Drawing("pack", TestDrawingFactory.Rectangle(10, 10));
- var engine = new PackProbeStripNestEngine(plate, drawing);
+ var filler = new PackProbeStripPlateFiller(plate, drawing);
- var parts = engine.Nest(
+ var parts = filler.Nest(
new List { new() { Drawing = drawing, Quantity = 1 } },
null,
CancellationToken.None
);
- Assert.Equal(1, engine.PackAreaCalls);
+ Assert.Equal(1, filler.PackAreaCalls);
Assert.Single(parts);
}
@@ -304,12 +260,12 @@ public class PlateFillerContractTests
Assert.Equal(10, item.Quantity);
}
- private sealed class FillAndPackProbeDefaultNestEngine : DefaultNestEngine
+ private sealed class FillAndPackProbeDefaultPlateFiller : DefaultPlateFiller
{
private readonly Drawing fillDrawing;
private readonly Drawing packDrawing;
- internal FillAndPackProbeDefaultNestEngine(
+ internal FillAndPackProbeDefaultPlateFiller(
Plate plate,
Drawing fillDrawing,
Drawing packDrawing
@@ -352,9 +308,9 @@ public class PlateFillerContractTests
}
}
- private sealed class AngleProbeDefaultNestEngine : DefaultNestEngine
+ private sealed class AngleProbeDefaultPlateFiller : DefaultPlateFiller
{
- internal AngleProbeDefaultNestEngine(Plate plate)
+ internal AngleProbeDefaultPlateFiller(Plate plate)
: base(plate) { }
internal bool BuildAnglesCalled { get; private set; }
@@ -370,11 +326,11 @@ public class PlateFillerContractTests
}
}
- private sealed class PackProbeStripNestEngine : StripNestEngine
+ private sealed class PackProbeStripPlateFiller : StripPlateFiller
{
private readonly Drawing drawing;
- internal PackProbeStripNestEngine(Plate plate, Drawing drawing)
+ internal PackProbeStripPlateFiller(Plate plate, Drawing drawing)
: base(plate)
{
this.drawing = drawing;
@@ -395,31 +351,6 @@ public class PlateFillerContractTests
}
}
- private static void AssertEquivalentLayouts(List expected, List actual)
- {
- var expectedParts = expected
- .OrderBy(part => part.BaseDrawing.Name)
- .ThenBy(part => part.Location.X)
- .ThenBy(part => part.Location.Y)
- .ThenBy(part => part.Rotation)
- .ToList();
- var actualParts = actual
- .OrderBy(part => part.BaseDrawing.Name)
- .ThenBy(part => part.Location.X)
- .ThenBy(part => part.Location.Y)
- .ThenBy(part => part.Rotation)
- .ToList();
-
- Assert.Equal(expectedParts.Count, actualParts.Count);
- for (var i = 0; i < expectedParts.Count; i++)
- {
- Assert.Equal(expectedParts[i].BaseDrawing.Name, actualParts[i].BaseDrawing.Name);
- Assert.Equal(expectedParts[i].Location.X, actualParts[i].Location.X, 9);
- Assert.Equal(expectedParts[i].Location.Y, actualParts[i].Location.Y, 9);
- Assert.Equal(expectedParts[i].Rotation, actualParts[i].Rotation, 9);
- }
- }
-
private static PlateFillerBase CreateFiller(string strategy, Plate plate) => strategy switch
{
"Default" => new DefaultPlateFiller(plate),
@@ -428,14 +359,6 @@ public class PlateFillerContractTests
_ => throw new ArgumentOutOfRangeException(nameof(strategy)),
};
- private static NestEngineBase CreateFacade(string strategy, Plate plate) => strategy switch
- {
- "Default" => new DefaultNestEngine(plate),
- "Vertical Remnant" => new VerticalRemnantEngine(plate),
- "Horizontal Remnant" => new HorizontalRemnantEngine(plate),
- _ => throw new ArgumentOutOfRangeException(nameof(strategy)),
- };
-
private sealed class CapturingProgress : IProgress
{
public List Reports { get; } = new();
diff --git a/OpenNest.Engine/DefaultNestEngine.cs b/OpenNest.Engine/DefaultNestEngine.cs
deleted file mode 100644
index eca7f35..0000000
--- a/OpenNest.Engine/DefaultNestEngine.cs
+++ /dev/null
@@ -1,191 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using OpenNest.Engine.Fill;
-using OpenNest.Engine.Jobs.Placement.Fillers;
-using OpenNest.Engine.Strategies;
-using OpenNest.Geometry;
-
-namespace OpenNest.Engine
-{
- public class DefaultNestEngine : NestEngineBase
- {
- private DefaultPlateFiller filler;
- private bool forceFullAngleSweep;
-
- public DefaultNestEngine(Plate plate)
- : base(plate) { }
-
- public override string Name => "Default";
-
- public override string Description =>
- "Multi-phase nesting (Linear, Pairs, RectBestFit, Extents)";
-
- public override NestPhase WinnerPhase
- {
- get => Filler.WinnerPhase;
- protected set => Filler.SetWinnerPhase(value);
- }
-
- public override List PhaseResults => Filler.PhaseResults;
-
- public override List AngleResults => Filler.AngleResults;
-
- public bool ForceFullAngleSweep
- {
- get => forceFullAngleSweep;
- set
- {
- forceFullAngleSweep = value;
- if (filler != null)
- filler.ForceFullAngleSweep = value;
- }
- }
-
- private DefaultPlateFiller Filler
- {
- get
- {
- if (filler == null || !ReferenceEquals(filler.Plate, Plate))
- {
- filler = CreateFiller(Plate);
- filler.ForceFullAngleSweep = forceFullAngleSweep;
- }
- return filler;
- }
- }
-
- private DefaultPlateFiller PrepareFiller()
- {
- var current = Filler;
- current.PlateNumber = PlateNumber;
- current.NestDirection = NestDirection;
- return current;
- }
-
- internal virtual DefaultPlateFiller CreateFiller(Plate plate) =>
- new LegacyDefaultPlateFiller(this, plate);
-
- internal DefaultPlateFiller CreateRemnantFiller(Plate plate, RemnantFillPolicy policy) =>
- new LegacyRemnantPlateFiller(this, plate, policy);
-
- protected override IFillComparer CreateComparer() => Filler.CreateComparerCore();
-
- public override NestDirection? PreferredDirection => null;
-
- public override ShrinkAxis TrimAxis => ShrinkAxis.Width;
-
- public override List BuildAngles(
- NestItem item,
- ClassificationResult classification,
- Box workArea
- ) => Filler.BuildAnglesCore(item, classification, workArea);
-
- protected override void RecordProductiveAngles(List angleResults) =>
- Filler.RecordProductiveAnglesCore(angleResults);
-
- protected virtual void RunPipeline(FillContext context) => Filler.RunPipelineCore(context);
-
- public override List Fill(
- NestItem item,
- Box workArea,
- IProgress progress,
- CancellationToken token
- )
- {
- var current = PrepareFiller();
- return current.Fill(item, workArea, progress, token);
- }
-
- public override List Fill(
- List groupParts,
- Box workArea,
- IProgress progress,
- CancellationToken token
- )
- {
- var current = PrepareFiller();
- return current.Fill(groupParts, workArea, progress, token);
- }
-
- public override List PackArea(
- Box box,
- List items,
- IProgress progress,
- CancellationToken token
- )
- {
- var current = PrepareFiller();
- return current.PackArea(box, items, progress, token);
- }
-
- public override List Nest(
- List items,
- IProgress progress,
- CancellationToken token
- )
- {
- return base.Nest(items, progress, token);
- }
-
- private sealed class LegacyDefaultPlateFiller : DefaultPlateFiller
- {
- private readonly DefaultNestEngine engine;
-
- internal LegacyDefaultPlateFiller(DefaultNestEngine engine, Plate plate)
- : base(plate)
- {
- this.engine = engine;
- }
-
- protected override IFillComparer CreateComparer() => engine.CreateComparer();
-
- public override NestDirection? PreferredDirection => engine.PreferredDirection;
-
- public override ShrinkAxis TrimAxis => engine.TrimAxis;
-
- public override List BuildAngles(
- NestItem item,
- ClassificationResult classification,
- Box workArea
- ) => engine.BuildAngles(item, classification, workArea);
-
- protected override void RecordProductiveAngles(List angleResults) =>
- engine.RecordProductiveAngles(angleResults);
-
- protected override void RunPipeline(FillContext context) => engine.RunPipeline(context);
- }
-
- private sealed class LegacyRemnantPlateFiller : RemnantPlateFiller
- {
- private readonly DefaultNestEngine engine;
-
- internal LegacyRemnantPlateFiller(
- DefaultNestEngine engine,
- Plate plate,
- RemnantFillPolicy policy
- )
- : base(plate, policy)
- {
- this.engine = engine;
- }
-
- protected override IFillComparer CreateComparer() => engine.CreateComparer();
-
- public override NestDirection? PreferredDirection => engine.PreferredDirection;
-
- public override ShrinkAxis TrimAxis => engine.TrimAxis;
-
- public override List BuildAngles(
- NestItem item,
- ClassificationResult classification,
- Box workArea
- ) => engine.BuildAngles(item, classification, workArea);
-
- protected override void RecordProductiveAngles(List angleResults) =>
- engine.RecordProductiveAngles(angleResults);
-
- protected override void RunPipeline(FillContext context) => engine.RunPipeline(context);
- }
- }
-}
diff --git a/OpenNest.Engine/Fill/DefaultFillComparer.cs b/OpenNest.Engine/Fill/DefaultFillComparer.cs
index 656511e..b09fe57 100644
--- a/OpenNest.Engine/Fill/DefaultFillComparer.cs
+++ b/OpenNest.Engine/Fill/DefaultFillComparer.cs
@@ -5,7 +5,7 @@ namespace OpenNest.Engine.Fill
{
///
/// Ranks fill results by count first, then density.
- /// This is the original scoring logic used by DefaultNestEngine.
+ /// This is the original scoring logic used by the Default plate filler.
///
public class DefaultFillComparer : IFillComparer
{
diff --git a/OpenNest.Engine/HorizontalRemnantEngine.cs b/OpenNest.Engine/HorizontalRemnantEngine.cs
deleted file mode 100644
index 0f0fd9a..0000000
--- a/OpenNest.Engine/HorizontalRemnantEngine.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Collections.Generic;
-using OpenNest.Engine.Fill;
-using OpenNest.Engine.Jobs.Placement.Fillers;
-using OpenNest.Engine.Strategies;
-using OpenNest.Geometry;
-
-namespace OpenNest.Engine
-{
- ///
- /// Optimizes for the largest top-side horizontal drop.
- ///
- public class HorizontalRemnantEngine : DefaultNestEngine
- {
- public HorizontalRemnantEngine(Plate plate)
- : base(plate) { }
-
- public override string Name => "Horizontal Remnant";
-
- public override string Description => "Optimizes for largest top-side horizontal drop";
-
- protected override IFillComparer CreateComparer() =>
- RemnantFillPolicy.Horizontal.CreateComparer();
-
- public override NestDirection? PreferredDirection =>
- RemnantFillPolicy.Horizontal.PreferredDirection;
-
- public override ShrinkAxis TrimAxis => RemnantFillPolicy.Horizontal.TrimAxis;
-
- public override List BuildAngles(
- NestItem item,
- ClassificationResult classification,
- Box workArea
- ) => RemnantFillPolicy.Horizontal.BuildAngles(item, classification);
-
- internal override DefaultPlateFiller CreateFiller(Plate plate) =>
- CreateRemnantFiller(plate, RemnantFillPolicy.Horizontal);
- }
-}
diff --git a/OpenNest.Engine/Jobs/NestingEngineRegistry.cs b/OpenNest.Engine/Jobs/NestingEngineRegistry.cs
index b44eca2..87fca0d 100644
--- a/OpenNest.Engine/Jobs/NestingEngineRegistry.cs
+++ b/OpenNest.Engine/Jobs/NestingEngineRegistry.cs
@@ -8,11 +8,10 @@ using System.Reflection;
namespace OpenNest.Engine.Jobs;
///
-/// Registry of whole-job INestingEngine implementations, parallel to NestEngineRegistry (which is for
-/// the legacy single-plate NestEngineBase). The four production strategies are exposed here through
-/// FixedStrategyNestingEngine so they compete on equal footing with model-submitted engines. Unlike
-/// NestEngineRegistry, this has no ActiveEngineName/global-selection concept — callers choose an engine
-/// explicitly from AvailableEngines.
+/// Registry of whole-job implementations. The four production
+/// strategies are exposed through so they compete on
+/// equal footing with model-submitted engines. Callers choose an engine explicitly from
+/// ; there is no process-global active selection.
///
public static class NestingEngineRegistry
{
@@ -55,8 +54,7 @@ public static class NestingEngineRegistry
///
/// Creates the engine registered under (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.
+ /// explicit choice is the whole selection mechanism; unknown names throw.
///
public static INestingEngine Create(string name)
{
@@ -81,9 +79,8 @@ public static class NestingEngineRegistry
}
/// Scans *.dll in directory for non-abstract INestingEngine types with a public
- /// parameterless constructor, registering each under its CLR type name. Mirrors
- /// NestEngineRegistry.LoadPlugins's per-assembly/per-type isolation: one bad plugin never
- /// prevents the rest from loading.
+ /// parameterless constructor, registering each under its CLR type name. Per-assembly and per-type
+ /// isolation ensures one bad plugin never prevents the rest from loading.
public static void LoadPlugins(string directory)
{
if (!Directory.Exists(directory))
diff --git a/OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs b/OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs
index 43f81bc..d7c8fad 100644
--- a/OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs
+++ b/OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs
@@ -20,9 +20,7 @@ internal class DefaultPlateFiller : PlateFillerBase
internal DefaultPlateFiller(Plate plate)
: base(plate) { }
- protected override IFillComparer CreateComparer() => CreateComparerCore();
-
- internal IFillComparer CreateComparerCore() => new DefaultFillComparer();
+ protected override IFillComparer CreateComparer() => new DefaultFillComparer();
internal bool ForceFullAngleSweep
{
@@ -34,18 +32,9 @@ internal class DefaultPlateFiller : PlateFillerBase
NestItem item,
ClassificationResult classification,
Box workArea
- ) => BuildAnglesCore(item, classification, workArea);
-
- internal List BuildAnglesCore(
- NestItem item,
- ClassificationResult classification,
- Box workArea
) => angleBuilder.Build(item, classification, workArea);
- protected override void RecordProductiveAngles(List angleResults) =>
- RecordProductiveAnglesCore(angleResults);
-
- internal void RecordProductiveAnglesCore(List angleResults)
+ protected override void RecordProductiveAngles(List angleResults)
{
angleBuilder.RecordProductive(angleResults);
}
@@ -419,9 +408,7 @@ internal class DefaultPlateFiller : PlateFillerBase
return BinConverter.ToParts(bin, items);
}
- protected virtual void RunPipeline(FillContext context) => RunPipelineCore(context);
-
- internal void RunPipelineCore(FillContext context)
+ protected virtual void RunPipeline(FillContext context)
{
var classification = PartClassifier.Classify(context.Item.Drawing);
context.PartType = classification.Type;
diff --git a/OpenNest.Engine/Jobs/Placement/Fillers/PlateFillerBase.cs b/OpenNest.Engine/Jobs/Placement/Fillers/PlateFillerBase.cs
index 97604e5..51136dd 100644
--- a/OpenNest.Engine/Jobs/Placement/Fillers/PlateFillerBase.cs
+++ b/OpenNest.Engine/Jobs/Placement/Fillers/PlateFillerBase.cs
@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Threading;
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
-using OpenNest.Math;
namespace OpenNest.Engine.Jobs.Placement.Fillers;
@@ -27,11 +25,6 @@ internal abstract class PlateFillerBase
public NestPhase WinnerPhase { get; protected set; }
- internal void SetWinnerPhase(NestPhase winnerPhase)
- {
- WinnerPhase = winnerPhase;
- }
-
public List PhaseResults { get; } = new();
public List AngleResults { get; } = new();
@@ -61,11 +54,9 @@ internal abstract class PlateFillerBase
protected FillPolicy BuildPolicy() => new(Comparer, PreferredDirection);
- internal IFillComparer FillComparer => Comparer;
-
protected string BuildProgressSummary() => BuildProgressSummary(PhaseResults);
- internal static string BuildProgressSummary(IReadOnlyList phaseResults)
+ private static string BuildProgressSummary(IReadOnlyList phaseResults)
{
if (phaseResults.Count == 0)
return null;
@@ -78,70 +69,7 @@ internal abstract class PlateFillerBase
}
protected bool IsBetterFill(List candidate, List current, Box workArea) =>
- IsBetterFill(Comparer, candidate, current, workArea);
-
- internal static bool IsBetterFill(
- IFillComparer comparer,
- List candidate,
- List current,
- Box workArea
- ) => comparer.IsBetter(candidate, current, workArea);
-
- protected bool IsBetterValidFill(List candidate, List current, Box workArea)
- {
- if (
- candidate != null
- && candidate.Count > 0
- && HasOverlaps(candidate, Plate.PartSpacing)
- )
- {
- Debug.WriteLine(
- $"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})"
- );
- return false;
- }
-
- return IsBetterFill(candidate, current, workArea);
- }
-
- internal static bool HasOverlaps(List parts, double spacing)
- {
- if (parts == null || parts.Count <= 1)
- return false;
-
- for (var i = 0; i < parts.Count; i++)
- {
- var box1 = parts[i].BoundingBox;
-
- for (var j = i + 1; j < parts.Count; j++)
- {
- var box2 = parts[j].BoundingBox;
-
- var overlapX = System.Math.Min(box1.Right, box2.Right)
- - System.Math.Max(box1.Left, box2.Left);
- var overlapY = System.Math.Min(box1.Top, box2.Top)
- - System.Math.Max(box1.Bottom, box2.Bottom);
-
- if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
- continue;
-
- List points;
- if (parts[i].Intersects(parts[j], out points))
- {
- var first = parts[i].BoundingBox;
- var second = parts[j].BoundingBox;
- Debug.WriteLine(
- $"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({first.Left:F2},{first.Bottom:F2})-({first.Right:F2},{first.Top:F2}) rot={parts[i].Rotation:F2}"
- + $" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({second.Left:F2},{second.Bottom:F2})-({second.Right:F2},{second.Top:F2}) rot={parts[j].Rotation:F2}"
- + $" intersections={points?.Count ?? 0}"
- );
- return true;
- }
- }
- }
-
- return false;
- }
+ Comparer.IsBetter(candidate, current, workArea);
public virtual List Fill(
NestItem item,
diff --git a/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs b/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs
index 2ea1aac..4a589dd 100644
--- a/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs
+++ b/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs
@@ -40,13 +40,6 @@ internal class StripPlateFiller : PlateFillerBase
List items,
IProgress progress,
CancellationToken token
- ) => PackAreaCore(box, items, progress, token);
-
- internal List PackAreaCore(
- Box box,
- List items,
- IProgress progress,
- CancellationToken token
)
{
var inner = new DefaultPlateFiller(Plate);
diff --git a/OpenNest.Engine/Jobs/PlateNesterFactory.cs b/OpenNest.Engine/Jobs/PlateNesterFactory.cs
index 5c3f702..4699846 100644
--- a/OpenNest.Engine/Jobs/PlateNesterFactory.cs
+++ b/OpenNest.Engine/Jobs/PlateNesterFactory.cs
@@ -5,9 +5,8 @@ namespace OpenNest.Engine.Jobs;
///
/// Instance-scoped strategy resolution for the whole-job runner. All four built-in strategies
-/// resolve directly to filler-backed plate nesters. The process-global NestEngineRegistry
-/// (including plugin registrations and ActiveEngineName) is neither read nor modified.
-/// Unknown keys reject.
+/// resolve directly to filler-backed plate nesters. Selection is explicit per job; no shared
+/// mutable strategy setting is read or modified. Unknown keys reject.
///
public static class PlateNesterFactory
{
diff --git a/OpenNest.Engine/NestEngineBase.cs b/OpenNest.Engine/NestEngineBase.cs
deleted file mode 100644
index 1ca33d5..0000000
--- a/OpenNest.Engine/NestEngineBase.cs
+++ /dev/null
@@ -1,216 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Threading;
-using OpenNest.Engine;
-using OpenNest.Engine.BestFit;
-using OpenNest.Engine.Fill;
-using OpenNest.Engine.Jobs.Placement.Fillers;
-using OpenNest.Engine.Strategies;
-using OpenNest.Geometry;
-using OpenNest.Math;
-
-namespace OpenNest.Engine
-{
- public abstract class NestEngineBase
- {
- protected NestEngineBase(Plate plate)
- {
- Plate = plate;
- }
-
- public Plate Plate { get; set; }
-
- public int PlateNumber { get; set; }
-
- public NestDirection NestDirection { get; set; }
-
- private readonly List phaseResults = new();
- private readonly List angleResults = new();
-
- public virtual NestPhase WinnerPhase { get; protected set; }
-
- public virtual List PhaseResults => phaseResults;
-
- public virtual List AngleResults => angleResults;
-
- public abstract string Name { get; }
-
- public abstract string Description { get; }
-
- // --- Engine policy ---
-
- private IFillComparer _comparer;
-
- protected IFillComparer Comparer => _comparer ??= CreateComparer();
-
- protected virtual IFillComparer CreateComparer() => new DefaultFillComparer();
-
- public virtual NestDirection? PreferredDirection => null;
-
- public virtual ShrinkAxis TrimAxis => ShrinkAxis.Width;
-
- public virtual List BuildAngles(
- NestItem item,
- ClassificationResult classification,
- Box workArea
- )
- {
- return new List
- {
- classification.PrimaryAngle,
- classification.PrimaryAngle + OpenNest.Math.Angle.HalfPI,
- };
- }
-
- protected virtual void RecordProductiveAngles(List angleResults) { }
-
- protected FillPolicy BuildPolicy() => new FillPolicy(Comparer, PreferredDirection);
-
- // --- Virtual methods (side-effect-free, return parts) ---
-
- public virtual List Fill(
- NestItem item,
- Box workArea,
- IProgress progress,
- CancellationToken token
- )
- {
- return new List();
- }
-
- public virtual List Fill(
- List groupParts,
- Box workArea,
- IProgress progress,
- CancellationToken token
- )
- {
- return new List();
- }
-
- public virtual List PackArea(
- Box box,
- List items,
- IProgress progress,
- CancellationToken token
- )
- {
- return new List();
- }
-
- // --- Nest: compatibility façade over shared single-plate orchestration ---
-
- public virtual List Nest(
- List items,
- IProgress progress,
- CancellationToken token
- )
- {
- return PlateFillOrchestrator.Nest(
- Plate,
- items,
- Comparer,
- (item, workArea, sink, cancellation) =>
- FillExact(item, workArea, sink, cancellation),
- (workArea, packItems, sink, cancellation) =>
- PackArea(workArea, packItems, sink, cancellation),
- progress,
- token
- );
- }
-
- // --- FillExact (non-virtual, delegates to virtual Fill) ---
-
- public List FillExact(
- NestItem item,
- Box workArea,
- IProgress progress,
- CancellationToken token
- )
- {
- return Fill(item, workArea, progress, token);
- }
-
- // --- Convenience overloads (mutate plate, return bool) ---
-
- public bool Fill(NestItem item)
- {
- return Fill(item, Plate.WorkArea());
- }
-
- public bool Fill(NestItem item, Box workArea)
- {
- var parts = Fill(item, workArea, null, CancellationToken.None);
-
- if (parts == null || parts.Count == 0)
- return false;
-
- Plate.Parts.AddRange(parts);
- return true;
- }
-
- public bool Fill(List groupParts)
- {
- return Fill(groupParts, Plate.WorkArea());
- }
-
- public bool Fill(List groupParts, Box workArea)
- {
- var parts = Fill(groupParts, workArea, null, CancellationToken.None);
-
- if (parts == null || parts.Count == 0)
- return false;
-
- Plate.Parts.AddRange(parts);
- return true;
- }
-
- public bool Pack(List items)
- {
- var workArea = Plate.WorkArea();
- var parts = PackArea(workArea, items, null, CancellationToken.None);
-
- if (parts == null || parts.Count == 0)
- return false;
-
- Plate.Parts.AddRange(parts);
- return true;
- }
-
- // --- Protected utilities ---
-
- internal static void ReportProgress(IProgress progress, ProgressReport report)
- {
- NestProgressReporter.Report(progress, report);
- }
-
- protected string BuildProgressSummary() =>
- PlateFillerBase.BuildProgressSummary(PhaseResults);
-
- protected bool IsBetterFill(List candidate, List current, Box workArea) =>
- PlateFillerBase.IsBetterFill(Comparer, candidate, current, workArea);
-
- protected bool IsBetterValidFill(List candidate, List current, Box workArea)
- {
- if (
- candidate != null
- && candidate.Count > 0
- && HasOverlaps(candidate, Plate.PartSpacing)
- )
- {
- Debug.WriteLine(
- $"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})"
- );
- return false;
- }
-
- return IsBetterFill(candidate, current, workArea);
- }
-
- protected static bool HasOverlaps(List parts, double spacing) =>
- PlateFillerBase.HasOverlaps(parts, spacing);
-
- }
-}
diff --git a/OpenNest.Engine/NestEngineInfo.cs b/OpenNest.Engine/NestEngineInfo.cs
deleted file mode 100644
index 209e7a0..0000000
--- a/OpenNest.Engine/NestEngineInfo.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-
-namespace OpenNest.Engine
-{
- public class NestEngineInfo
- {
- public NestEngineInfo(string name, string description, Func factory)
- {
- Name = name;
- Description = description;
- Factory = factory;
- }
-
- public string Name { get; }
- public string Description { get; }
- public Func Factory { get; }
- }
-}
diff --git a/OpenNest.Engine/NestEngineRegistry.cs b/OpenNest.Engine/NestEngineRegistry.cs
deleted file mode 100644
index 212afd4..0000000
--- a/OpenNest.Engine/NestEngineRegistry.cs
+++ /dev/null
@@ -1,134 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-
-namespace OpenNest.Engine
-{
- public static class NestEngineRegistry
- {
- private static readonly List engines = new();
-
- static NestEngineRegistry()
- {
- Register(
- "Default",
- "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)",
- plate => new DefaultNestEngine(plate)
- );
-
- Register(
- "Strip",
- "Strip-based nesting for mixed-drawing layouts",
- plate => new StripNestEngine(plate)
- );
-
- Register(
- "Vertical Remnant",
- "Optimizes for largest right-side vertical drop",
- plate => new VerticalRemnantEngine(plate)
- );
-
- Register(
- "Horizontal Remnant",
- "Optimizes for largest top-side horizontal drop",
- plate => new HorizontalRemnantEngine(plate)
- );
- }
-
- public static IReadOnlyList AvailableEngines => engines;
-
- public static string ActiveEngineName { get; set; } = "Default";
-
- public static NestEngineBase Create(Plate plate)
- {
- var info = engines.FirstOrDefault(e =>
- e.Name.Equals(ActiveEngineName, StringComparison.OrdinalIgnoreCase)
- );
-
- if (info == null)
- {
- Debug.WriteLine(
- $"[NestEngineRegistry] Engine '{ActiveEngineName}' not found, falling back to Default"
- );
- info = engines[0];
- }
-
- return info.Factory(plate);
- }
-
- public static void Register(
- string name,
- string description,
- Func factory
- )
- {
- if (engines.Any(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
- {
- Debug.WriteLine($"[NestEngineRegistry] Duplicate engine '{name}' skipped");
- return;
- }
-
- engines.Add(new NestEngineInfo(name, description, factory));
- }
-
- public static void LoadPlugins(string directory)
- {
- if (!Directory.Exists(directory))
- return;
-
- foreach (var dll in Directory.GetFiles(directory, "*.dll"))
- {
- try
- {
- var assembly = Assembly.LoadFrom(dll);
-
- foreach (var type in assembly.GetTypes())
- {
- if (type.IsAbstract || !typeof(NestEngineBase).IsAssignableFrom(type))
- continue;
-
- var ctor = type.GetConstructor(new[] { typeof(Plate) });
-
- if (ctor == null)
- {
- Debug.WriteLine(
- $"[NestEngineRegistry] Skipping {type.Name}: no Plate constructor"
- );
- continue;
- }
-
- // Create a temporary instance to read Name and Description.
- try
- {
- var tempPlate = new Plate();
- var instance = (NestEngineBase)ctor.Invoke(new object[] { tempPlate });
- Register(
- instance.Name,
- instance.Description,
- plate => (NestEngineBase)ctor.Invoke(new object[] { plate })
- );
- Debug.WriteLine(
- $"[NestEngineRegistry] Loaded plugin engine: {instance.Name}"
- );
- }
- catch (Exception ex)
- {
- Debug.WriteLine(
- $"[NestEngineRegistry] Failed to instantiate {type.Name}: {ex.Message}"
- );
- }
- }
- }
- catch (Exception ex)
- {
- Debug.WriteLine(
- $"[NestEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}"
- );
- }
- }
- }
- }
-}
diff --git a/OpenNest.Engine/Strategies/FillContext.cs b/OpenNest.Engine/Strategies/FillContext.cs
index c7df18f..8f18351 100644
--- a/OpenNest.Engine/Strategies/FillContext.cs
+++ b/OpenNest.Engine/Strategies/FillContext.cs
@@ -84,7 +84,7 @@ namespace OpenNest.Engine.Strategies
/// the canonical drawing's baked source angle whenever it's non-zero. No-op when
/// isn't set. Internal so callers that build their own
/// outside (e.g. the fallback
- /// report in DefaultPlateFiller.RunPipelineCore for strategies that don't self-report)
+ /// report in DefaultPlateFiller.RunPipeline for strategies that don't self-report)
/// can apply the same rebind before reaching the UI.
///
internal List ToOriginalFrame(List parts)
diff --git a/OpenNest.Engine/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs
deleted file mode 100644
index 158bf27..0000000
--- a/OpenNest.Engine/StripNestEngine.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using OpenNest.Engine.Jobs.Placement.Fillers;
-using OpenNest.Geometry;
-
-namespace OpenNest.Engine
-{
- public class StripNestEngine : NestEngineBase
- {
- private StripPlateFiller filler;
-
- public StripNestEngine(Plate plate)
- : base(plate) { }
-
- public override string Name => "Strip";
-
- public override string Description =>
- "Iterative shrink-fill nesting for mixed-drawing layouts";
-
- private StripPlateFiller Filler
- {
- get
- {
- if (filler == null || !ReferenceEquals(filler.Plate, Plate))
- filler = new LegacyStripPlateFiller(this, Plate);
- return filler;
- }
- }
-
- private StripPlateFiller PrepareFiller()
- {
- var current = Filler;
- current.PlateNumber = PlateNumber;
- current.NestDirection = NestDirection;
- return current;
- }
-
- public override List Fill(
- NestItem item,
- Box workArea,
- IProgress progress,
- CancellationToken token
- )
- {
- var current = PrepareFiller();
- return current.Fill(item, workArea, progress, token);
- }
-
- public override List Fill(
- List groupParts,
- Box workArea,
- IProgress progress,
- CancellationToken token
- )
- {
- var current = PrepareFiller();
- return current.Fill(groupParts, workArea, progress, token);
- }
-
- public override List PackArea(
- Box box,
- List items,
- IProgress progress,
- CancellationToken token
- )
- {
- var current = PrepareFiller();
- return current.PackAreaCore(box, items, progress, token);
- }
-
- public override List Nest(
- List items,
- IProgress progress,
- CancellationToken token
- )
- {
- var current = PrepareFiller();
- return current.Nest(items, progress, token);
- }
-
- private sealed class LegacyStripPlateFiller : StripPlateFiller
- {
- private readonly StripNestEngine engine;
-
- internal LegacyStripPlateFiller(StripNestEngine engine, Plate plate)
- : base(plate)
- {
- this.engine = engine;
- }
-
- public override List PackArea(
- Box box,
- List items,
- IProgress progress,
- CancellationToken token
- ) => engine.PackArea(box, items, progress, token);
- }
- }
-}
diff --git a/OpenNest.Engine/VerticalRemnantEngine.cs b/OpenNest.Engine/VerticalRemnantEngine.cs
deleted file mode 100644
index a8869d6..0000000
--- a/OpenNest.Engine/VerticalRemnantEngine.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System.Collections.Generic;
-using OpenNest.Engine.Fill;
-using OpenNest.Engine.Jobs.Placement.Fillers;
-using OpenNest.Engine.Strategies;
-using OpenNest.Geometry;
-
-namespace OpenNest.Engine
-{
- ///
- /// Optimizes for the largest right-side vertical drop.
- ///
- public class VerticalRemnantEngine : DefaultNestEngine
- {
- public VerticalRemnantEngine(Plate plate)
- : base(plate) { }
-
- public override string Name => "Vertical Remnant";
-
- public override string Description => "Optimizes for largest right-side vertical drop";
-
- protected override IFillComparer CreateComparer() => RemnantFillPolicy.Vertical.CreateComparer();
-
- public override NestDirection? PreferredDirection => RemnantFillPolicy.Vertical.PreferredDirection;
-
- public override List BuildAngles(
- NestItem item,
- ClassificationResult classification,
- Box workArea
- ) => RemnantFillPolicy.Vertical.BuildAngles(item, classification);
-
- internal override DefaultPlateFiller CreateFiller(Plate plate) =>
- CreateRemnantFiller(plate, RemnantFillPolicy.Vertical);
- }
-}
diff --git a/OpenNest.Tests/Engine/EngineOverlapTests.cs b/OpenNest.Tests/Engine/EngineOverlapTests.cs
index a6d77c3..30f50cb 100644
--- a/OpenNest.Tests/Engine/EngineOverlapTests.cs
+++ b/OpenNest.Tests/Engine/EngineOverlapTests.cs
@@ -3,6 +3,7 @@ using OpenNest.Geometry;
using OpenNest.IO;
using Xunit.Abstractions;
using OpenNest.Engine;
+using OpenNest.Engine.Jobs.Placement;
namespace OpenNest.Tests.Engine;
@@ -31,7 +32,7 @@ public class EngineOverlapTests
[InlineData("Strip")]
[InlineData("Vertical Remnant")]
[InlineData("Horizontal Remnant")]
- public void FillPlate_NoOverlaps(string engineName)
+ public void FillPlate_NoOverlaps(string strategy)
{
var drawing = ImportDxf();
if (drawing is null)
@@ -39,23 +40,21 @@ public class EngineOverlapTests
var plate = new Plate(60, 120);
- NestEngineRegistry.ActiveEngineName = engineName;
- var engine = NestEngineRegistry.Create(plate);
-
var item = new NestItem { Drawing = drawing };
- var success = engine.Fill(item);
+ var parts = PlateFillService.FillItem(
+ strategy,
+ plate,
+ item,
+ plate.WorkArea(),
+ progress: null,
+ token: CancellationToken.None
+ );
+ plate.Parts.AddRange(parts);
_output.WriteLine(
- $"Engine: {engine.Name}, Parts: {plate.Parts.Count}, Utilization: {plate.Utilization():P1}"
+ $"Strategy: {strategy}, Parts: {plate.Parts.Count}, Utilization: {plate.Utilization():P1}"
);
- if (engine is DefaultNestEngine defaultEngine)
- {
- _output.WriteLine($"Winner phase: {defaultEngine.WinnerPhase}");
- foreach (var pr in defaultEngine.PhaseResults)
- _output.WriteLine($" Phase {pr.Phase}: {pr.PartCount} parts in {pr.TimeMs}ms");
- }
-
// Show rotation distribution
var rotGroups = plate
.Parts.GroupBy(p => System.Math.Round(OpenNest.Math.Angle.ToDegrees(p.Rotation), 1))
@@ -74,7 +73,7 @@ public class EngineOverlapTests
Assert.False(
hasOverlaps,
- $"Engine '{engineName}' produced {collisionPoints.Count} collision point(s) with {plate.Parts.Count} parts"
+ $"Strategy '{strategy}' produced {collisionPoints.Count} collision point(s) with {plate.Parts.Count} parts"
);
}
diff --git a/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs b/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs
index 92bdbbe..0fde7c5 100644
--- a/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs
+++ b/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs
@@ -1,5 +1,6 @@
using OpenNest.Geometry;
using OpenNest.Engine;
+using OpenNest.Engine.Jobs.Placement.Fillers;
namespace OpenNest.Tests.Engine;
@@ -17,49 +18,49 @@ public class EngineRefactorSmokeTests
}
[Fact]
- public void DefaultEngine_FillNestItem_ProducesResults()
+ public void DefaultPlateFiller_FillNestItem_ProducesResults()
{
var plate = new Plate(60, 120);
- var engine = new DefaultNestEngine(plate);
+ var filler = new DefaultPlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
- var parts = engine.Fill(
+ var parts = filler.Fill(
item,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
- Assert.True(parts.Count > 0, "DefaultNestEngine should fill parts");
+ Assert.True(parts.Count > 0, "DefaultPlateFiller should fill parts");
}
[Fact]
- public void DefaultEngine_FillGroupParts_ProducesResults()
+ public void DefaultPlateFiller_FillGroupParts_ProducesResults()
{
var plate = new Plate(60, 120);
- var engine = new DefaultNestEngine(plate);
+ var filler = new DefaultPlateFiller(plate);
var drawing = MakeRectDrawing(20, 10);
var groupParts = new List { new Part(drawing) };
- var parts = engine.Fill(
+ var parts = filler.Fill(
groupParts,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
- Assert.True(parts.Count > 0, "DefaultNestEngine group fill should produce parts");
+ Assert.True(parts.Count > 0, "DefaultPlateFiller group fill should produce parts");
}
[Fact]
- public void DefaultEngine_ForceFullAngleSweep_StillWorks()
+ public void DefaultPlateFiller_ForceFullAngleSweep_StillWorks()
{
var plate = new Plate(60, 120);
- var engine = new DefaultNestEngine(plate);
- engine.ForceFullAngleSweep = true;
+ var filler = new DefaultPlateFiller(plate);
+ filler.ForceFullAngleSweep = true;
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
- var parts = engine.Fill(
+ var parts = filler.Fill(
item,
plate.WorkArea(),
null,
@@ -70,33 +71,33 @@ public class EngineRefactorSmokeTests
}
[Fact]
- public void StripEngine_Nest_ProducesResults()
+ public void StripPlateFiller_Nest_ProducesResults()
{
var plate = new Plate(60, 120);
- var engine = new StripNestEngine(plate);
+ var filler = new StripPlateFiller(plate);
var items = new List
{
new NestItem { Drawing = MakeRectDrawing(20, 10, "large"), Quantity = 10 },
new NestItem { Drawing = MakeRectDrawing(8, 5, "small"), Quantity = 5 },
};
- var parts = engine.Nest(items, null, System.Threading.CancellationToken.None);
+ var parts = filler.Nest(items, null, System.Threading.CancellationToken.None);
- Assert.True(parts.Count > 0, "StripNestEngine should nest parts");
+ Assert.True(parts.Count > 0, "StripPlateFiller should nest parts");
}
[Fact]
- public void DefaultEngine_Nest_ProducesResults()
+ public void DefaultPlateFiller_Nest_ProducesResults()
{
var plate = new Plate(60, 120);
- var engine = new DefaultNestEngine(plate);
+ var filler = new DefaultPlateFiller(plate);
var items = new List
{
new NestItem { Drawing = MakeRectDrawing(20, 10, "a"), Quantity = 5 },
new NestItem { Drawing = MakeRectDrawing(15, 8, "b"), Quantity = 3 },
};
- var parts = engine.Nest(items, null, System.Threading.CancellationToken.None);
+ var parts = filler.Nest(items, null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "Base Nest method should place parts");
}
diff --git a/OpenNest.Tests/Engine/ExplicitPlacementStrategyTests.cs b/OpenNest.Tests/Engine/ExplicitPlacementStrategyTests.cs
index 42c13a4..7f8959d 100644
--- a/OpenNest.Tests/Engine/ExplicitPlacementStrategyTests.cs
+++ b/OpenNest.Tests/Engine/ExplicitPlacementStrategyTests.cs
@@ -7,7 +7,7 @@ namespace OpenNest.Tests.Engine;
///
/// Phase 3.2: MultiPlateNester and PlateOptimizer take an explicit placement strategy at their
-/// top-level call boundary instead of consulting the process-global NestEngineRegistry.
+/// top-level call boundary instead of consulting process-global selection state.
///
public class ExplicitPlacementStrategyTests
{
diff --git a/OpenNest.Tests/Engine/NestInvarianceTests.cs b/OpenNest.Tests/Engine/NestInvarianceTests.cs
index 7ccd22b..8d79009 100644
--- a/OpenNest.Tests/Engine/NestInvarianceTests.cs
+++ b/OpenNest.Tests/Engine/NestInvarianceTests.cs
@@ -2,6 +2,7 @@ using System.Threading;
using OpenNest.CNC;
using OpenNest.Engine;
using OpenNest.Engine.BestFit;
+using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
using OpenNest.Math;
@@ -37,9 +38,9 @@ public class NestInvarianceTests
private static int RunFillCount(Drawing drawing, Plate plate)
{
BestFitCache.Clear();
- var engine = new DefaultNestEngine(plate);
+ var filler = new DefaultPlateFiller(plate);
var item = new NestItem { Drawing = drawing };
- var parts = engine.Fill(
+ var parts = filler.Fill(
item,
plate.WorkArea(),
progress: null,
@@ -71,9 +72,9 @@ public class NestInvarianceTests
foreach (var theta in new[] { 0.0, 0.3, 0.8, 1.2 })
{
BestFitCache.Clear();
- var engine = new DefaultNestEngine(plate);
+ var filler = new DefaultPlateFiller(plate);
var item = new NestItem { Drawing = MakeImportedAt(theta) };
- var parts = engine.Fill(item, workArea, progress: null, token: CancellationToken.None);
+ var parts = filler.Fill(item, workArea, progress: null, token: CancellationToken.None);
Assert.NotNull(parts);
foreach (var p in parts)
diff --git a/OpenNest.Tests/Engine/RemnantEngineTests.cs b/OpenNest.Tests/Engine/RemnantEngineTests.cs
index 4b3dd42..dd0167e 100644
--- a/OpenNest.Tests/Engine/RemnantEngineTests.cs
+++ b/OpenNest.Tests/Engine/RemnantEngineTests.cs
@@ -1,5 +1,7 @@
using OpenNest.Engine;
using OpenNest.Engine.Fill;
+using OpenNest.Engine.Jobs.Placement;
+using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
namespace OpenNest.Tests.Engine;
@@ -18,82 +20,79 @@ public class RemnantEngineTests
}
[Fact]
- public void VerticalRemnantEngine_UsesVerticalRemnantComparer()
+ public void VerticalRemnantPlateFiller_UsesHorizontalPreferredDirection()
{
var plate = new Plate(60, 120);
- var engine = new VerticalRemnantEngine(plate);
- Assert.Equal("Vertical Remnant", engine.Name);
- Assert.Equal(NestDirection.Horizontal, engine.PreferredDirection);
+ var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical);
+ Assert.Equal(NestDirection.Horizontal, filler.PreferredDirection);
}
[Fact]
- public void HorizontalRemnantEngine_UsesHorizontalRemnantComparer()
+ public void HorizontalRemnantPlateFiller_UsesVerticalPreferredDirection()
{
var plate = new Plate(60, 120);
- var engine = new HorizontalRemnantEngine(plate);
- Assert.Equal("Horizontal Remnant", engine.Name);
- Assert.Equal(NestDirection.Vertical, engine.PreferredDirection);
+ var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal);
+ Assert.Equal(NestDirection.Vertical, filler.PreferredDirection);
}
[Fact]
- public void VerticalRemnantEngine_Fill_ProducesResults()
+ public void VerticalRemnantPlateFiller_Fill_ProducesResults()
{
var plate = new Plate(60, 120);
- var engine = new VerticalRemnantEngine(plate);
+ var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
- var parts = engine.Fill(
+ var parts = filler.Fill(
item,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
- Assert.True(parts.Count > 0, "VerticalRemnantEngine should fill parts");
+ Assert.True(parts.Count > 0, "VerticalRemnantPlateFiller should fill parts");
}
[Fact]
- public void HorizontalRemnantEngine_Fill_ProducesResults()
+ public void HorizontalRemnantPlateFiller_Fill_ProducesResults()
{
var plate = new Plate(60, 120);
- var engine = new HorizontalRemnantEngine(plate);
+ var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
- var parts = engine.Fill(
+ var parts = filler.Fill(
item,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
- Assert.True(parts.Count > 0, "HorizontalRemnantEngine should fill parts");
+ Assert.True(parts.Count > 0, "HorizontalRemnantPlateFiller should fill parts");
}
[Fact]
- public void Registry_ContainsBothRemnantEngines()
+ public void PlateFillService_ListsBothRemnantStrategies()
{
- var names = NestEngineRegistry.AvailableEngines.Select(e => e.Name).ToList();
- Assert.Contains("Vertical Remnant", names);
- Assert.Contains("Horizontal Remnant", names);
+ Assert.Contains("Vertical Remnant", PlateFillService.BuiltInStrategies);
+ Assert.Contains("Horizontal Remnant", PlateFillService.BuiltInStrategies);
}
[Fact]
- public void VerticalRemnantEngine_ProducesTighterXExtent_ThanDefault()
+ public void VerticalRemnantPlateFiller_ProducesTighterXExtent_ThanDefault()
{
var plate = new Plate(60, 120);
var drawing = MakeRectDrawing(20, 10);
var item = new NestItem { Drawing = drawing };
- var defaultEngine = new DefaultNestEngine(plate);
- var remnantEngine = new VerticalRemnantEngine(plate);
+ var defaultFiller = new DefaultPlateFiller(plate);
+ var remnantFiller = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical);
- var defaultParts = defaultEngine.Fill(
+ var defaultParts = defaultFiller.Fill(
item,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
- var remnantParts = remnantEngine.Fill(
+ var remnantParts = remnantFiller.Fill(
item,
plate.WorkArea(),
null,
@@ -110,7 +109,7 @@ public class RemnantEngineTests
Assert.True(
remnantXExtent <= defaultXExtent + 0.01,
- $"Remnant X-extent ({remnantXExtent:F1}) should be <= default ({defaultXExtent:F1})"
+ $"Remnant X-extent ({remnantXExtent:F1}) should be <= default filler ({defaultXExtent:F1})"
);
}
}
diff --git a/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs b/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs
index 3a581a4..e35a04b 100644
--- a/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs
+++ b/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs
@@ -1,4 +1,5 @@
using OpenNest.Engine.Fill;
+using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
using OpenNest.Engine;
@@ -54,8 +55,8 @@ public class IterativeShrinkFillerTests
Func> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
- var engine = new DefaultNestEngine(plate);
- return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
+ var filler = new DefaultPlateFiller(plate);
+ return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
@@ -75,8 +76,8 @@ public class IterativeShrinkFillerTests
Func> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
- var engine = new DefaultNestEngine(plate);
- return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
+ var filler = new DefaultPlateFiller(plate);
+ return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
@@ -100,8 +101,8 @@ public class IterativeShrinkFillerTests
Func> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
- var engine = new DefaultNestEngine(plate);
- return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
+ var filler = new DefaultPlateFiller(plate);
+ return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 60, 30), fillFunc, 1.0);
@@ -122,8 +123,8 @@ public class IterativeShrinkFillerTests
Func> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
- var engine = new DefaultNestEngine(plate);
- return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
+ var filler = new DefaultPlateFiller(plate);
+ return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
diff --git a/OpenNest.Tests/Fill/RemnantFillerTests2.cs b/OpenNest.Tests/Fill/RemnantFillerTests2.cs
index 52b08dc..fc7c00c 100644
--- a/OpenNest.Tests/Fill/RemnantFillerTests2.cs
+++ b/OpenNest.Tests/Fill/RemnantFillerTests2.cs
@@ -1,4 +1,5 @@
using OpenNest.Engine.Fill;
+using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
using OpenNest.Engine;
@@ -35,8 +36,8 @@ public class RemnantFillerTests2
Func> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
- var engine = new DefaultNestEngine(plate);
- return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
+ var filler = new DefaultPlateFiller(plate);
+ return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var placed = filler.FillItems(items, fillFunc);
@@ -59,8 +60,8 @@ public class RemnantFillerTests2
Func> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
- var engine = new DefaultNestEngine(plate);
- return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
+ var filler = new DefaultPlateFiller(plate);
+ return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
filler.FillItems(items, fillFunc);
diff --git a/OpenNest.Tests/Fill/ShrinkFillerTests.cs b/OpenNest.Tests/Fill/ShrinkFillerTests.cs
index 6998708..cbdba86 100644
--- a/OpenNest.Tests/Fill/ShrinkFillerTests.cs
+++ b/OpenNest.Tests/Fill/ShrinkFillerTests.cs
@@ -1,4 +1,5 @@
using OpenNest.Engine.Fill;
+using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
using OpenNest.Engine;
@@ -27,8 +28,8 @@ public class ShrinkFillerTests
Func> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
- var engine = new DefaultNestEngine(plate);
- return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
+ var filler = new DefaultPlateFiller(plate);
+ return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Length);
@@ -49,8 +50,8 @@ public class ShrinkFillerTests
Func> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
- var engine = new DefaultNestEngine(plate);
- return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
+ var filler = new DefaultPlateFiller(plate);
+ return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Width);
diff --git a/OpenNest.Tests/Strategies/FillPipelineTests.cs b/OpenNest.Tests/Strategies/FillPipelineTests.cs
index feadf77..437ff60 100644
--- a/OpenNest.Tests/Strategies/FillPipelineTests.cs
+++ b/OpenNest.Tests/Strategies/FillPipelineTests.cs
@@ -1,4 +1,5 @@
using OpenNest.Engine.Strategies;
+using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
using OpenNest.Engine;
@@ -21,14 +22,14 @@ public class FillPipelineTests
public void Pipeline_PopulatesPhaseResults()
{
var plate = new Plate(60, 120);
- var engine = new DefaultNestEngine(plate);
+ var filler = new DefaultPlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
- engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
+ filler.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
Assert.True(
- engine.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count,
- $"Expected phase results from all active strategies, got {engine.PhaseResults.Count}"
+ filler.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count,
+ $"Expected phase results from all active strategies, got {filler.PhaseResults.Count}"
);
}
@@ -36,10 +37,10 @@ public class FillPipelineTests
public void Pipeline_SetsWinnerPhase()
{
var plate = new Plate(60, 120);
- var engine = new DefaultNestEngine(plate);
+ var filler = new DefaultPlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
- var parts = engine.Fill(
+ var parts = filler.Fill(
item,
plate.WorkArea(),
null,
@@ -48,11 +49,11 @@ public class FillPipelineTests
Assert.True(parts.Count > 0);
Assert.True(
- engine.WinnerPhase == NestPhase.Pairs
- || engine.WinnerPhase == NestPhase.Linear
- || engine.WinnerPhase == NestPhase.RectBestFit
- || engine.WinnerPhase == NestPhase.Extents
- || engine.WinnerPhase == NestPhase.Custom
+ filler.WinnerPhase == NestPhase.Pairs
+ || filler.WinnerPhase == NestPhase.Linear
+ || filler.WinnerPhase == NestPhase.RectBestFit
+ || filler.WinnerPhase == NestPhase.Extents
+ || filler.WinnerPhase == NestPhase.Custom
);
}
@@ -60,13 +61,13 @@ public class FillPipelineTests
public void Pipeline_RespectsCancellation()
{
var plate = new Plate(60, 120);
- var engine = new DefaultNestEngine(plate);
+ var filler = new DefaultPlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var cts = new System.Threading.CancellationTokenSource();
cts.Cancel();
// Pre-cancelled token should return empty or partial results without throwing
- var parts = engine.Fill(item, plate.WorkArea(), null, cts.Token);
+ var parts = filler.Fill(item, plate.WorkArea(), null, cts.Token);
// Should not throw — graceful degradation
Assert.NotNull(parts);
diff --git a/OpenNest/Forms/EngineSelection.cs b/OpenNest/Forms/EngineSelection.cs
index 5ed8ac7..04a1c50 100644
--- a/OpenNest/Forms/EngineSelection.cs
+++ b/OpenNest/Forms/EngineSelection.cs
@@ -7,8 +7,7 @@ using OpenNest.Engine.Jobs.Placement;
namespace OpenNest.Forms
{
///
- /// App-scoped nesting-engine selection — the desktop replacement for the process-global
- /// NestEngineRegistry.ActiveEngineName. The selected name addresses a whole-job
+ /// App-scoped nesting-engine selection. The selected name addresses a whole-job
/// INestingEngine resolved through NestingEngineRegistry at call time; single-plate
/// interactive fill uses FillStrategy, which maps a built-in engine to its placement
/// strategy and falls back to Default for jobs-only engines (StockLadder, plug-ins).
diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs
index 5da13e2..a63eaf0 100644
--- a/OpenNest/Forms/MainForm.cs
+++ b/OpenNest/Forms/MainForm.cs
@@ -72,8 +72,8 @@ namespace OpenNest.Forms
// BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
// Jobs-side plug-in discovery: INestingEngine implementations are registered per
- // assembly/type with the same per-DLL isolation as before. Binary plug-ins derived
- // from the legacy NestEngineBase no longer load here after the Phase-4 removal.
+ // assembly/type with per-DLL isolation. Existing plug-ins must implement the jobs
+ // contract and expose a public parameterless constructor.
var enginesDir = Path.Combine(Application.StartupPath, "Engines");
NestingEngineRegistry.LoadPlugins(enginesDir);
diff --git a/README.md b/README.md
index 4d2c645..9e6b1fc 100644
--- a/README.md
+++ b/README.md
@@ -102,7 +102,7 @@ The new whole-job contracts in `OpenNest.Engine/Jobs` (`namespace OpenNest`) use
`NestJobRunner.Solve` allocates a job across physical sheets from the full stock inventory: every available stock entry is trialled independently each iteration, and only the winning candidate consumes a sheet or reduces demand. Selection is a documented deterministic greedy policy — lexicographic placed-count vector by ascending part priority, then lower consumed sheet area, then smaller placement envelope, then original stock input order (see `NestJobCandidateComparer`). It is a tie policy, not a guarantee of global-minimum material or plate count. Finite stock is never exceeded; `MaxPlates` caps sheet count; empty parts complete without consuming stock; empty or fully exhausted stock returns `Incomplete/StockExhausted`; a zero-placement candidate stops with `NoPlacementFound` and consumes no sheet.
-`DrawingJobMapper` snapshots caller drawings/items under explicit requirement IDs. The built-in plate nesters create fresh private drawings, items, and plates for each trial through `CandidatePlacementContext` and map returned drawings **by reference**, never by name. Mutable legacy quantities never drive the fulfillment ledger. `PlateNesterFactory` resolves the built-in strategy names (`Default`, `Strip`, `Vertical Remnant`, `Horizontal Remnant`) to instance-scoped placement strategies; it neither reads nor changes the process-global `NestEngineRegistry`, and unknown keys reject. Quantity deduction in the engine paths the runner reaches (base-class fill/pack, strip deduction, remnant-fill ledger, shrink-leftover counting) is keyed by drawing reference, not display name, so same-name drawings and repeated requirements stay independent. `NestResultMaterializer` returns a detached domain nest and `DrawingsByPartId` identity map. Each output plate represents one physical sheet (`Quantity = 1`), and each placement is attached exactly once so domain quantity events do not double count.
+`DrawingJobMapper` snapshots caller drawings/items under explicit requirement IDs. The built-in plate nesters create fresh private drawings, items, and plates for each trial through `CandidatePlacementContext` and map returned drawings **by reference**, never by name. Per-trial `NestItem` quantities never drive the fulfillment ledger. `PlateNesterFactory` resolves the built-in strategy names (`Default`, `Strip`, `Vertical Remnant`, `Horizontal Remnant`) to instance-scoped placement strategies and rejects unknown keys. Quantity deduction inside a filler run (fill/pack, strip deduction, remnant-fill ledger, shrink-leftover counting) is keyed by drawing reference, not display name, so same-name drawings and repeated requirements stay independent. `NestResultMaterializer` returns a detached domain nest and `DrawingsByPartId` identity map. Each output plate represents one physical sheet (`Quantity = 1`), and each placement is attached exactly once so domain quantity events do not double count.
```csharp
var job = new NestJob(
@@ -118,7 +118,7 @@ var domainResult = NestResultMaterializer.Materialize(job, result);
**Placement strategies:** `Default`, `Strip`, `Vertical Remnant`, and `Horizontal Remnant` are filler-backed built-ins (`OpenNest.Engine/Jobs/Placement/DefaultPlateNester.cs`, `StripPlateNester.cs`, `RemnantPlateNester.cs`) that run the internal `Jobs/Placement/Fillers` geometry while keeping demand read-only. A runnable end-to-end example — multiple requirements, mixed finite/unlimited stock, full plate/leftover enumeration — lives in `OpenNest.Engine.Tests/Jobs/NestJobExampleTests.cs`.
-**Legacy caller boundaries (not yet migrated):** the desktop UI (`MainForm.RunAutoNestAsync` / `NestSinglePlateAsync`), the CLI (`OpenNest.Console`), and MCP (`NestingTools`) still call the old single-plate `engine.Nest(...)` entry points unchanged. UI adoption needs a separate adapter preserving populated-plate editing, preview routing, and Accept-versus-Cancel semantics. The public API (`OpenNest.Api`, `NestRunner.RunAsync`) already delegates to one `NestJobRunner.Solve` call and reports status, stop reason, part fulfillment, stock usage, and plate-to-stock mapping; `.nestquote` archives carry a schema version and round-trip incomplete jobs.
+**Single-plate placement:** interactive fills use `PlateFillService`, which explicitly selects one of `Default`, `Strip`, `Vertical Remnant`, or `Horizontal Remnant` and returns proposed parts without mutating the caller's plate. The caller remains responsible for preview accept/cancel and attachment. Whole-job work in the desktop app, console, MCP server, and public API resolves a named `INestingEngine`, solves one `NestJob`, and materializes committed results. `NestRunner.RunAsync` reports status, stop reason, part fulfillment, stock usage, and plate-to-stock mapping; `.nestquote` archives carry a schema version and round-trip incomplete jobs.
### Fresh DXF job verification (headless)
@@ -244,7 +244,7 @@ Each engine-on-job solve is independent, so `--parallel ` (default 3) runs up
An engine's layout is rejected (scoring zero for that job) if any part falls outside the work area, any two parts are closer than the required spacing, or a drawing gets more parts placed than requested. A run that doesn't finish within its time budget also scores zero, as a timeout.
-Custom competitor engines can be added by dropping a DLL implementing `INestingEngine` with a public parameterless constructor into the `Engines/` directory next to the benchmark executable; each one is registered under its own CLR type name. This is a separate plugin contract from the desktop app's `NestEngineRegistry`/`NestEngineBase` (which requires a `(Plate)` constructor) — a `NestEngineBase` plugin dropped into the benchmark's `Engines/` folder is silently skipped, since the benchmark only ever solves whole jobs.
+Custom competitor engines can be added by dropping a DLL that implements `INestingEngine` with a public parameterless constructor into the `Engines/` directory next to the benchmark executable; each one is registered under its own CLR type name. This jobs plug-in contract is also the supported extension point for new nesting engines. Plugins built for the removed single-plate inheritance API are not binary compatible and must be migrated to `INestingEngine`.
### Conservative bend endpoint repair (opt-in)
@@ -392,15 +392,17 @@ source geometry changes were made. Source SHA-256:
## Nesting Engines
-OpenNest uses a pluggable engine architecture. The active engine can be selected at runtime.
+OpenNest uses jobs-only nesting engines. `NestingEngineRegistry` resolves each engine by explicit name; `NestJobRunner` is the only component that commits demand and stock. Interactive single-plate operations use `PlateFillService` and return proposed parts for the caller to accept or discard.
| Engine | Description |
|--------|-------------|
-| **Default** | Multi-phase strategy: linear fill, pair fill, rect best-fit, then remainder. Balances density and speed. |
+| **Default** | Multi-phase strategy: linear fill, pair fill, rect best-fit, then extents. Balances density and speed. |
+| **Strip** | Iterative shrink-fill strategy for mixed-drawing layouts. |
| **Vertical Remnant** | Optimizes for a clean vertical drop on the right side of the plate. |
| **Horizontal Remnant** | Optimizes for a clean horizontal drop on the top of the plate. |
+| **StockLadder** | Whole-job stock-constrained strategy; available to named job callers but not the desktop's four-choice combo. |
-Custom engines can be built by subclassing `NestEngineBase` and registering via `NestEngineRegistry` or dropping a plugin DLL in the `Engines/` directory.
+Custom nesting plugins must implement `INestingEngine` with a public parameterless constructor and can be loaded from the `Engines/` directory. Plugins built for the removed single-plate inheritance API are not binary compatible.
### Fill Strategies