refactor(engine): remove legacy nesting engine surface
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<NotSupportedException>(() => 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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class NestJobExampleTests
|
||||
{
|
||||
@@ -160,36 +161,27 @@ public class NestJobExampleTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<NestItem> { item }, null, CancellationToken.None);
|
||||
var parts = PlateFillService.Nest(
|
||||
"Default",
|
||||
plate,
|
||||
new List<NestItem> { 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(
|
||||
|
||||
@@ -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<NestItem>
|
||||
{
|
||||
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<NestItem>
|
||||
var plate = new Plate(new Size(30, 50));
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
@@ -120,48 +95,29 @@ public class PlateFillerContractTests
|
||||
Quantity = 3,
|
||||
},
|
||||
};
|
||||
var facadeItems = new List<NestItem>
|
||||
{
|
||||
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<NestItem> { 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<Part> expected, List<Part> 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<NestProgress>
|
||||
{
|
||||
public List<NestProgress> Reports { get; } = new();
|
||||
|
||||
Reference in New Issue
Block a user