refactor(engine): separate plate placement from job allocation
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using Xunit;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Parity between the legacy adapter and the migrated built-in plate nesters (Default/Strip) on
|
||||
/// generated geometry. The runner's placement validator enforces geometric safety on every committed
|
||||
/// candidate, so these tests assert fulfillment, status, and — for the deterministic Default/rectangle
|
||||
/// case — identical layouts.
|
||||
/// </summary>
|
||||
public class PlateNesterParityTests
|
||||
{
|
||||
private const double Tolerance = 1e-6;
|
||||
|
||||
private static readonly Size PlateSize = new(30, 50);
|
||||
private static readonly Spacing Edge = new(1, 1, 1, 1);
|
||||
|
||||
private static NestJob Job(IReadOnlyList<NestJobPart> parts, int? stockQuantity = 3,
|
||||
string strategy = "Default")
|
||||
{
|
||||
var stock = new NestPlateStock("stock", PlateSize, stockQuantity, 1, Edge);
|
||||
return new NestJob(parts, new[] { stock }, new NestJobOptions(strategy));
|
||||
}
|
||||
|
||||
private static NestJobResult Solve(IPlateNester nester, NestJob job) =>
|
||||
new NestJobRunner(_ => nester).Solve(job);
|
||||
|
||||
private static Dictionary<string, PartFulfillment> ByPart(NestJobResult result) =>
|
||||
result.Fulfillment.ToDictionary(f => f.PartId, StringComparer.Ordinal);
|
||||
|
||||
private static void AssertLayoutsIdentical(NestJobResult left, NestJobResult right)
|
||||
{
|
||||
Assert.Equal(left.Plates.Count, right.Plates.Count);
|
||||
for (var i = 0; i < left.Plates.Count; i++)
|
||||
{
|
||||
var lPlates = left.Plates[i].Placements;
|
||||
var rPlates = right.Plates[i].Placements;
|
||||
Assert.Equal(lPlates.Count, rPlates.Count);
|
||||
var lSorted = lPlates.OrderBy(p => p.PartId).ThenBy(p => p.X).ThenBy(p => p.Y).ToList();
|
||||
var rSorted = rPlates.OrderBy(p => p.PartId).ThenBy(p => p.X).ThenBy(p => p.Y).ToList();
|
||||
for (var j = 0; j < lSorted.Count; j++)
|
||||
{
|
||||
Assert.Equal(lSorted[j].PartId, rSorted[j].PartId);
|
||||
Assert.Equal(lSorted[j].X, rSorted[j].X, 6);
|
||||
Assert.Equal(lSorted[j].Y, rSorted[j].Y, 6);
|
||||
Assert.True(AnglesEqual(lSorted[j].Rotation, rSorted[j].Rotation),
|
||||
$"rotation differs: {lSorted[j].Rotation} vs {rSorted[j].Rotation}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AnglesEqual(double left, double right)
|
||||
{
|
||||
var delta = (left - right) % (System.Math.PI * 2);
|
||||
return System.Math.Abs(delta) <= Tolerance ||
|
||||
System.Math.Abs(System.Math.Abs(delta) - System.Math.PI * 2) <= Tolerance;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultParity_Rectangles_SameFulfillmentAndLayout()
|
||||
{
|
||||
var parts = new[]
|
||||
{
|
||||
new NestJobPart("a", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 4),
|
||||
new NestJobPart("b", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 3)
|
||||
};
|
||||
|
||||
var legacy = Solve(new LegacyPlateNesterAdapter(plate => new DefaultNestEngine(plate)), Job(parts));
|
||||
var migrated = Solve(new DefaultPlateNester(), Job(parts));
|
||||
|
||||
Assert.Equal(legacy.Status, migrated.Status);
|
||||
Assert.Equal(NestJobStatus.Complete, migrated.Status);
|
||||
Assert.Equal(ByPart(legacy), ByPart(migrated));
|
||||
foreach (var usage in legacy.StockUsage)
|
||||
Assert.Equal(usage.Used, migrated.StockUsage.First(u => u.StockId == usage.StockId).Used);
|
||||
// Automatic-rotation rectangles on a single stock size are deterministic: identical layouts.
|
||||
AssertLayoutsIdentical(legacy, migrated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripParity_Rectangles_SameFulfillmentAndTotalCount()
|
||||
{
|
||||
var parts = new[]
|
||||
{
|
||||
new NestJobPart("a", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 4),
|
||||
new NestJobPart("b", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 3)
|
||||
};
|
||||
|
||||
var legacy = Solve(new LegacyPlateNesterAdapter(plate => new StripNestEngine(plate)),
|
||||
Job(parts, strategy: "Strip"));
|
||||
var migrated = Solve(new StripPlateNester(), Job(parts, strategy: "Strip"));
|
||||
|
||||
Assert.Equal(legacy.Status, migrated.Status);
|
||||
Assert.Equal(NestJobStatus.Complete, migrated.Status);
|
||||
Assert.Equal(ByPart(legacy), ByPart(migrated));
|
||||
Assert.Equal(legacy.Plates.SelectMany(p => p.Placements).Count(),
|
||||
migrated.Plates.SelectMany(p => p.Placements).Count());
|
||||
// Shrink-fill ordering can differ between engine instances; do not assert identical coordinates.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MigratedBuiltins_AreResolvedByProductionFactory()
|
||||
{
|
||||
Assert.IsType<DefaultPlateNester>(PlateNesterFactory.Create("Default"));
|
||||
Assert.IsType<StripPlateNester>(PlateNesterFactory.Create("Strip"));
|
||||
Assert.IsType<LegacyPlateNesterAdapter>(PlateNesterFactory.Create("Vertical Remnant"));
|
||||
Assert.IsType<LegacyPlateNesterAdapter>(PlateNesterFactory.Create("Horizontal Remnant"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsymmetricPart_ValidAndFulfilled()
|
||||
{
|
||||
// L-shape: 6x4 outer with a corner notch removed (single closed contour, asymmetric).
|
||||
var lshape = new Program();
|
||||
lshape.MoveTo(0, 0);
|
||||
lshape.LineTo(6, 0);
|
||||
lshape.LineTo(6, 4);
|
||||
lshape.LineTo(3, 4);
|
||||
lshape.LineTo(3, 2);
|
||||
lshape.LineTo(0, 2);
|
||||
lshape.LineTo(0, 0);
|
||||
|
||||
var parts = new[]
|
||||
{
|
||||
new NestJobPart("l", PartGeometrySnapshot.FromProgram(lshape), 3),
|
||||
new NestJobPart("sq", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 2)
|
||||
};
|
||||
var result = Solve(new DefaultPlateNester(), Job(parts));
|
||||
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(3, ByPart(result)["l"].Placed);
|
||||
Assert.Equal(2, ByPart(result)["sq"].Placed);
|
||||
Assert.Equal(5, result.Plates.SelectMany(p => p.Placements).Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HoleAndArcParts_ValidAndFulfilled()
|
||||
{
|
||||
// 6x6 rectangle with a 2x2 inner hole (rapid contour), plus a D-shape with a semicircular arc.
|
||||
var holed = new Program();
|
||||
holed.MoveTo(0, 0);
|
||||
holed.LineTo(6, 0);
|
||||
holed.LineTo(6, 6);
|
||||
holed.LineTo(0, 6);
|
||||
holed.LineTo(0, 0);
|
||||
holed.MoveTo(2, 2);
|
||||
holed.LineTo(4, 2);
|
||||
holed.LineTo(4, 4);
|
||||
holed.LineTo(2, 4);
|
||||
holed.LineTo(2, 2);
|
||||
|
||||
var arc = new Program();
|
||||
arc.MoveTo(0, 0);
|
||||
arc.LineTo(3, 0);
|
||||
arc.ArcTo(3, 5, 3, 2.5, RotationType.CCW);
|
||||
arc.LineTo(0, 5);
|
||||
arc.LineTo(0, 0);
|
||||
|
||||
var parts = new[]
|
||||
{
|
||||
new NestJobPart("holed", PartGeometrySnapshot.FromProgram(holed), 2),
|
||||
new NestJobPart("arc", PartGeometrySnapshot.FromProgram(arc), 2)
|
||||
};
|
||||
var result = Solve(new DefaultPlateNester(), Job(parts));
|
||||
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(2, ByPart(result)["holed"].Placed);
|
||||
Assert.Equal(2, ByPart(result)["arc"].Placed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FixedRotation_Respected()
|
||||
{
|
||||
var part = new NestJobPart("fixed", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)),
|
||||
2, rotation: RotationPolicy.Fixed(0));
|
||||
var result = Solve(new DefaultPlateNester(), Job(new[] { part }));
|
||||
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
var placements = result.Plates.SelectMany(p => p.Placements).ToList();
|
||||
Assert.Equal(2, placements.Count);
|
||||
foreach (var placement in placements)
|
||||
Assert.True(AnglesEqual(placement.Rotation, 0), $"fixed rotation violated: {placement.Rotation}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepeatedNames_KeepIndependentIdentity()
|
||||
{
|
||||
// Two distinct requirements sharing identical geometry (and, via the mapper, name) but different IDs.
|
||||
var program = TestDrawingFactory.Rectangle(6, 4);
|
||||
var parts = new[]
|
||||
{
|
||||
new NestJobPart("first", PartGeometrySnapshot.FromProgram(program), 2),
|
||||
new NestJobPart("second", PartGeometrySnapshot.FromProgram(program), 1)
|
||||
};
|
||||
var result = Solve(new DefaultPlateNester(), Job(parts));
|
||||
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(2, ByPart(result)["first"].Placed);
|
||||
Assert.Equal(1, ByPart(result)["second"].Placed);
|
||||
var ids = result.Plates.SelectMany(p => p.Placements).Select(p => p.PartId);
|
||||
Assert.Equal(2, ids.Count(id => id == "first"));
|
||||
Assert.Equal(1, ids.Count(id => id == "second"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OffsetGeometry_ValidAndFulfilled()
|
||||
{
|
||||
// Part contour starting at a nonzero origin (offset geometry).
|
||||
var program = new Program();
|
||||
program.MoveTo(12, 7);
|
||||
program.LineTo(18, 7);
|
||||
program.LineTo(18, 11);
|
||||
program.LineTo(12, 11);
|
||||
program.LineTo(12, 7);
|
||||
|
||||
var part = new NestJobPart("offset", PartGeometrySnapshot.FromProgram(program), 2);
|
||||
var result = Solve(new DefaultPlateNester(), Job(new[] { part }));
|
||||
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(2, ByPart(result)["offset"].Placed);
|
||||
// The runner's validator guarantees containment and non-overlap for every committed placement.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunScopedCache_DrawingReusedAcrossTrials()
|
||||
{
|
||||
// 14x9 parts on 30x20: one sheet holds fewer than five, so the runner runs multiple candidate
|
||||
// trials through the same nester instance. The run-scoped drawing cache must keep producing
|
||||
// valid, correctly-attributed placements across trials.
|
||||
var part = new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(14, 9)), 5);
|
||||
var stock = new NestPlateStock("stock", new Size(30, 20), 3);
|
||||
var nester = new DefaultPlateNester();
|
||||
var result = new NestJobRunner(_ => nester).Solve(new NestJob(new[] { part }, new[] { stock }));
|
||||
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(5, result.Fulfillment.Single(f => f.PartId == "p").Placed);
|
||||
Assert.Equal(2, result.Plates.Count);
|
||||
var usage = result.StockUsage.Single();
|
||||
Assert.Equal(2, usage.Used);
|
||||
Assert.Equal(1, usage.Remaining);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyRemnantStrategies_StillResolveThroughAdapter()
|
||||
{
|
||||
// Remnant strategies must keep working through the legacy adapter after the factory change.
|
||||
var part = new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 2);
|
||||
foreach (var strategy in new[] { "Vertical Remnant", "Horizontal Remnant" })
|
||||
{
|
||||
var result = Solve(PlateNesterFactory.Create(strategy), Job(new[] { part }, strategy: strategy));
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(2, result.Fulfillment.Single(f => f.PartId == "p").Placed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Bridges legacy <see cref="IProgress{NestProgress}"/> reporting into job progress while a candidate
|
||||
/// trial is being evaluated. Used by both the legacy adapter and the migrated built-in nesters so the
|
||||
/// stage/context mapping has one implementation.
|
||||
/// </summary>
|
||||
internal static class CandidateProgressBridge
|
||||
{
|
||||
internal static IProgress<NestProgress> Create(IProgress<NestJobProgress> progress, string stockId)
|
||||
{
|
||||
if (progress == null) return null;
|
||||
return new LegacyToJob(progress, stockId);
|
||||
}
|
||||
|
||||
private sealed class LegacyToJob(IProgress<NestJobProgress> progress, string stockId) : IProgress<NestProgress>
|
||||
{
|
||||
public void Report(NestProgress value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
progress.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, -1, 0, 0, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Migrated built-in placement strategy for the whole-job runner. Reuses <see cref="DefaultNestEngine"/>
|
||||
/// fill/pack geometry but owns its own run-scoped bookkeeping: remaining demand is read from the
|
||||
/// request and placement counts are derived from returned placements, so the engine's private
|
||||
/// <see cref="NestItem.Quantity"/> mutations never feed back into job accounting.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A private <see cref="Drawing"/> per requirement is created once per solve and reused across every
|
||||
/// candidate trial (the runner reuses one <see cref="IPlateNester"/> instance per job). This is safe
|
||||
/// because the engines mutate <see cref="NestItem.Quantity"/> (per-trial) and canonical-frame copies,
|
||||
/// never the shared <see cref="Drawing"/> or its <c>Quantity</c>. Identity is by Drawing reference,
|
||||
/// never by name. Each trial still gets a fresh private <see cref="Plate"/>.
|
||||
/// </remarks>
|
||||
public sealed class DefaultPlateNester : IPlateNester
|
||||
{
|
||||
private readonly Func<Plate, DefaultNestEngine> engineFactory;
|
||||
private readonly Dictionary<string, Drawing> drawingsById = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<Drawing, string> idByDrawing = new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
public DefaultPlateNester() : this(static plate => new DefaultNestEngine(plate))
|
||||
{
|
||||
}
|
||||
|
||||
/// <param name="engineFactory">Injectable for tests; defaults to <see cref="DefaultNestEngine"/>.</param>
|
||||
public DefaultPlateNester(Func<Plate, DefaultNestEngine> engineFactory)
|
||||
{
|
||||
this.engineFactory = engineFactory ?? throw new ArgumentNullException(nameof(engineFactory));
|
||||
}
|
||||
|
||||
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var plate = DrawingJobMapper.CreatePlate(request.Stock);
|
||||
var items = new List<NestItem>(request.Parts.Count);
|
||||
foreach (var requirement in request.Parts)
|
||||
{
|
||||
if (!drawingsById.TryGetValue(requirement.Id, out var drawing))
|
||||
{
|
||||
drawing = DrawingJobMapper.CreateDrawing(requirement);
|
||||
drawingsById.Add(requirement.Id, drawing);
|
||||
idByDrawing.Add(drawing, requirement.Id);
|
||||
}
|
||||
|
||||
// Quantity is the request's remaining demand; the engine may mutate this per-trial item,
|
||||
// and that mutation is deliberately discarded — placement counts come from the result.
|
||||
items.Add(new NestItem
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = requirement.Quantity,
|
||||
Priority = requirement.Priority,
|
||||
StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation),
|
||||
RotationStart = requirement.Rotation.Start,
|
||||
RotationEnd = requirement.Rotation.End
|
||||
});
|
||||
}
|
||||
|
||||
var engine = engineFactory(plate) ?? throw new InvalidOperationException("Engine factory returned null.");
|
||||
var legacyProgress = CandidateProgressBridge.Create(progress, request.Stock.Id);
|
||||
var parts = engine.Nest(items, legacyProgress, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (parts == null) throw new InvalidOperationException("Engine returned null placements.");
|
||||
|
||||
var placements = new List<NestJobPlacement>(parts.Count);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part?.BaseDrawing == null || !idByDrawing.TryGetValue(part.BaseDrawing, out var id))
|
||||
throw new InvalidOperationException("Placement does not reference a known requirement drawing.");
|
||||
placements.Add(new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation));
|
||||
}
|
||||
|
||||
return new PlateCandidate(placements);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Migrated built-in placement strategy for the whole-job runner. Reuses <see cref="StripNestEngine"/>
|
||||
/// iterative shrink-fill/pack geometry with the same run-scoped bookkeeping as <see cref="DefaultPlateNester"/>:
|
||||
/// remaining demand is read from the request and placement counts are derived from returned placements.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A private <see cref="Drawing"/> per requirement is created once per solve and reused across trials
|
||||
/// (safe: the engine mutates per-trial <see cref="NestItem.Quantity"/> and canonical copies, never the
|
||||
/// shared Drawing). Identity is by Drawing reference. Each trial gets a fresh private <see cref="Plate"/>.
|
||||
/// </remarks>
|
||||
public sealed class StripPlateNester : IPlateNester
|
||||
{
|
||||
private readonly Func<Plate, StripNestEngine> engineFactory;
|
||||
private readonly Dictionary<string, Drawing> drawingsById = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<Drawing, string> idByDrawing = new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
public StripPlateNester() : this(static plate => new StripNestEngine(plate))
|
||||
{
|
||||
}
|
||||
|
||||
/// <param name="engineFactory">Injectable for tests; defaults to <see cref="StripNestEngine"/>.</param>
|
||||
public StripPlateNester(Func<Plate, StripNestEngine> engineFactory)
|
||||
{
|
||||
this.engineFactory = engineFactory ?? throw new ArgumentNullException(nameof(engineFactory));
|
||||
}
|
||||
|
||||
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var plate = DrawingJobMapper.CreatePlate(request.Stock);
|
||||
var items = new List<NestItem>(request.Parts.Count);
|
||||
foreach (var requirement in request.Parts)
|
||||
{
|
||||
if (!drawingsById.TryGetValue(requirement.Id, out var drawing))
|
||||
{
|
||||
drawing = DrawingJobMapper.CreateDrawing(requirement);
|
||||
drawingsById.Add(requirement.Id, drawing);
|
||||
idByDrawing.Add(drawing, requirement.Id);
|
||||
}
|
||||
|
||||
items.Add(new NestItem
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = requirement.Quantity,
|
||||
Priority = requirement.Priority,
|
||||
StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation),
|
||||
RotationStart = requirement.Rotation.Start,
|
||||
RotationEnd = requirement.Rotation.End
|
||||
});
|
||||
}
|
||||
|
||||
var engine = engineFactory(plate) ?? throw new InvalidOperationException("Engine factory returned null.");
|
||||
var legacyProgress = CandidateProgressBridge.Create(progress, request.Stock.Id);
|
||||
var parts = engine.Nest(items, legacyProgress, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (parts == null) throw new InvalidOperationException("Engine returned null placements.");
|
||||
|
||||
var placements = new List<NestJobPlacement>(parts.Count);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part?.BaseDrawing == null || !idByDrawing.TryGetValue(part.BaseDrawing, out var id))
|
||||
throw new InvalidOperationException("Placement does not reference a known requirement drawing.");
|
||||
placements.Add(new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation));
|
||||
}
|
||||
|
||||
return new PlateCandidate(placements);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@ using System;
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Instance-scoped strategy resolution for the whole-job runner. The built-in strategies map
|
||||
/// to private engine factories; the process-global NestEngineRegistry (including plugin
|
||||
/// registrations and ActiveEngineName) is neither read nor modified. Unknown keys reject.
|
||||
/// Instance-scoped strategy resolution for the whole-job runner. Default and Strip resolve to the
|
||||
/// migrated built-in plate nesters; the remnant strategies still use the legacy adapter during
|
||||
/// rollout. The process-global NestEngineRegistry (including plugin registrations and
|
||||
/// ActiveEngineName) is neither read nor modified. Unknown keys reject.
|
||||
/// </summary>
|
||||
public static class PlateNesterFactory
|
||||
{
|
||||
@@ -13,8 +14,8 @@ public static class PlateNesterFactory
|
||||
ArgumentNullException.ThrowIfNull(strategy);
|
||||
return strategy switch
|
||||
{
|
||||
"Default" => new LegacyPlateNesterAdapter(plate => new DefaultNestEngine(plate)),
|
||||
"Strip" => new LegacyPlateNesterAdapter(plate => new StripNestEngine(plate)),
|
||||
"Default" => new DefaultPlateNester(),
|
||||
"Strip" => new StripPlateNester(),
|
||||
"Vertical Remnant" => new LegacyPlateNesterAdapter(plate => new VerticalRemnantEngine(plate)),
|
||||
"Horizontal Remnant" => new LegacyPlateNesterAdapter(plate => new HorizontalRemnantEngine(plate)),
|
||||
_ => throw new NotSupportedException($"Unknown placement strategy: {strategy}.")
|
||||
|
||||
Reference in New Issue
Block a user