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;
|
namespace OpenNest;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Instance-scoped strategy resolution for the whole-job runner. The built-in strategies map
|
/// Instance-scoped strategy resolution for the whole-job runner. Default and Strip resolve to the
|
||||||
/// to private engine factories; the process-global NestEngineRegistry (including plugin
|
/// migrated built-in plate nesters; the remnant strategies still use the legacy adapter during
|
||||||
/// registrations and ActiveEngineName) is neither read nor modified. Unknown keys reject.
|
/// rollout. The process-global NestEngineRegistry (including plugin registrations and
|
||||||
|
/// ActiveEngineName) is neither read nor modified. Unknown keys reject.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class PlateNesterFactory
|
public static class PlateNesterFactory
|
||||||
{
|
{
|
||||||
@@ -13,8 +14,8 @@ public static class PlateNesterFactory
|
|||||||
ArgumentNullException.ThrowIfNull(strategy);
|
ArgumentNullException.ThrowIfNull(strategy);
|
||||||
return strategy switch
|
return strategy switch
|
||||||
{
|
{
|
||||||
"Default" => new LegacyPlateNesterAdapter(plate => new DefaultNestEngine(plate)),
|
"Default" => new DefaultPlateNester(),
|
||||||
"Strip" => new LegacyPlateNesterAdapter(plate => new StripNestEngine(plate)),
|
"Strip" => new StripPlateNester(),
|
||||||
"Vertical Remnant" => new LegacyPlateNesterAdapter(plate => new VerticalRemnantEngine(plate)),
|
"Vertical Remnant" => new LegacyPlateNesterAdapter(plate => new VerticalRemnantEngine(plate)),
|
||||||
"Horizontal Remnant" => new LegacyPlateNesterAdapter(plate => new HorizontalRemnantEngine(plate)),
|
"Horizontal Remnant" => new LegacyPlateNesterAdapter(plate => new HorizontalRemnantEngine(plate)),
|
||||||
_ => throw new NotSupportedException($"Unknown placement strategy: {strategy}.")
|
_ => throw new NotSupportedException($"Unknown placement strategy: {strategy}.")
|
||||||
|
|||||||
Reference in New Issue
Block a user