refactor(engine): separate plate placement from job allocation
This commit is contained in:
@@ -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