refactor(engine): share jobs placement identity and progress mechanics
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
using OpenNest.Engine.Jobs.Placement;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Tests.Jobs;
|
||||||
|
|
||||||
|
public class CandidatePlacementContextTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void FreshItemsReusePrivateDrawingsAndReflectCurrentRequirement()
|
||||||
|
{
|
||||||
|
var context = new CandidatePlacementContext();
|
||||||
|
var geometry = PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4));
|
||||||
|
|
||||||
|
var first = Assert.Single(
|
||||||
|
context.CreateItems(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new NestJobPart(
|
||||||
|
"part",
|
||||||
|
geometry,
|
||||||
|
3,
|
||||||
|
priority: 7,
|
||||||
|
rotation: RotationPolicy.BoundedSweep(0.1, 0.7, 0.2)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
var current = Assert.Single(
|
||||||
|
context.CreateItems(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new NestJobPart(
|
||||||
|
"part",
|
||||||
|
geometry,
|
||||||
|
1,
|
||||||
|
priority: 4,
|
||||||
|
rotation: RotationPolicy.Fixed(0.5)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.NotSame(first, current);
|
||||||
|
Assert.Same(first.Drawing, current.Drawing);
|
||||||
|
Assert.Equal(1, current.Quantity);
|
||||||
|
Assert.Equal(4, current.Priority);
|
||||||
|
Assert.Equal(OpenNest.Math.Angle.TwoPI, current.StepAngle);
|
||||||
|
Assert.Equal(0.5, current.RotationStart);
|
||||||
|
Assert.Equal(0.5, current.RotationEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlacementMappingUsesPrivateDrawingIdentityAndRejectsInvalidParts()
|
||||||
|
{
|
||||||
|
var context = new CandidatePlacementContext();
|
||||||
|
var items = context.CreateItems(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new NestJobPart(
|
||||||
|
"part",
|
||||||
|
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)),
|
||||||
|
1
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
var placed = new Part(items[0].Drawing, new Vector(7, 11));
|
||||||
|
placed.Rotate(0.3);
|
||||||
|
|
||||||
|
var placement = Assert.Single(context.MapPlacements(new[] { placed }));
|
||||||
|
|
||||||
|
Assert.Equal("part", placement.PartId);
|
||||||
|
Assert.Equal(0, placement.InstanceIndex);
|
||||||
|
// The mapping carries the part's committed pose; Rotate moves both program and location.
|
||||||
|
Assert.Equal(placed.Location.X, placement.X, 9);
|
||||||
|
Assert.Equal(placed.Location.Y, placement.Y, 9);
|
||||||
|
Assert.Equal(placed.Rotation, placement.Rotation, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullPartIsRejected()
|
||||||
|
{
|
||||||
|
var context = new CandidatePlacementContext();
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_ = context.MapPlacements(new List<Part> { null! });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ForeignDrawingWithKnownNameIsRejected()
|
||||||
|
{
|
||||||
|
var context = new CandidatePlacementContext();
|
||||||
|
_ = context.CreateItems(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new NestJobPart(
|
||||||
|
"part",
|
||||||
|
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)),
|
||||||
|
1
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Same requirement name, but a foreign Drawing instance: identity is by reference.
|
||||||
|
var foreign = new Part(new Drawing("part", TestDrawingFactory.Rectangle(6, 4)));
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_ = context.MapPlacements(new[] { foreign });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,8 +53,7 @@ public sealed class LegacyPlateNesterAdapter : IPlateNester
|
|||||||
var engine =
|
var engine =
|
||||||
engineFactory(plate)
|
engineFactory(plate)
|
||||||
?? throw new InvalidOperationException("Legacy engine factory returned null.");
|
?? throw new InvalidOperationException("Legacy engine factory returned null.");
|
||||||
var legacyProgress =
|
var legacyProgress = CandidateProgressBridge.Create(progress, request.Stock.Id);
|
||||||
progress == null ? null : new LegacyProgress(progress, request.Stock.Id);
|
|
||||||
var parts = engine.Nest(items, legacyProgress, token);
|
var parts = engine.Nest(items, legacyProgress, token);
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
if (parts == null)
|
if (parts == null)
|
||||||
@@ -72,16 +71,4 @@ public sealed class LegacyPlateNesterAdapter : IPlateNester
|
|||||||
}
|
}
|
||||||
return new PlateCandidate(placements);
|
return new PlateCandidate(placements);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class LegacyProgress(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,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
|
namespace OpenNest.Engine.Jobs.Placement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the private Drawing-to-requirement identity map for one plate-nester run.
|
||||||
|
/// It creates fresh mutable legacy items for each candidate while only returned poses cross back
|
||||||
|
/// into the immutable jobs boundary.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CandidatePlacementContext
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, Drawing> drawingsById = new(StringComparer.Ordinal);
|
||||||
|
private readonly Dictionary<Drawing, string> idByDrawing = new(
|
||||||
|
ReferenceEqualityComparer.Instance
|
||||||
|
);
|
||||||
|
|
||||||
|
internal List<NestItem> CreateItems(IEnumerable<NestJobPart> requirements)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(requirements);
|
||||||
|
|
||||||
|
var items = new List<NestItem>();
|
||||||
|
foreach (var requirement in requirements)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(requirement);
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal List<NestJobPlacement> MapPlacements(IEnumerable<Part> parts)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(parts);
|
||||||
|
|
||||||
|
var placements = new List<NestJobPlacement>();
|
||||||
|
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 placements;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
@@ -13,7 +12,8 @@ namespace OpenNest.Engine.Jobs.Placement;
|
|||||||
/// <see cref="NestItem.Quantity"/> mutations never feed back into job accounting.
|
/// <see cref="NestItem.Quantity"/> mutations never feed back into job accounting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// A private <see cref="Drawing"/> per requirement is created once per solve and reused across every
|
/// The identity/progress boundary mechanics live in <see cref="CandidatePlacementContext"/>: one
|
||||||
|
/// 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
|
/// 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,
|
/// 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 the shared <see cref="Drawing"/> or its <c>Quantity</c>. Identity is by Drawing reference,
|
||||||
@@ -23,10 +23,7 @@ public sealed class DefaultPlateNester : IPlateNester
|
|||||||
{
|
{
|
||||||
private readonly Func<Plate, DefaultNestEngine> engineFactory;
|
private readonly Func<Plate, DefaultNestEngine> engineFactory;
|
||||||
private readonly OrderedPlateNester restrictedRotationNester = new();
|
private readonly OrderedPlateNester restrictedRotationNester = new();
|
||||||
private readonly Dictionary<string, Drawing> drawingsById = new(StringComparer.Ordinal);
|
private readonly CandidatePlacementContext context = new();
|
||||||
private readonly Dictionary<Drawing, string> idByDrawing = new(
|
|
||||||
ReferenceEqualityComparer.Instance
|
|
||||||
);
|
|
||||||
|
|
||||||
public DefaultPlateNester()
|
public DefaultPlateNester()
|
||||||
: this(static plate => new DefaultNestEngine(plate)) { }
|
: this(static plate => new DefaultNestEngine(plate)) { }
|
||||||
@@ -55,30 +52,9 @@ public sealed class DefaultPlateNester : IPlateNester
|
|||||||
return restrictedRotationNester.Place(request, progress, token);
|
return restrictedRotationNester.Place(request, progress, token);
|
||||||
|
|
||||||
var plate = DrawingJobMapper.CreatePlate(request.Stock);
|
var plate = DrawingJobMapper.CreatePlate(request.Stock);
|
||||||
var items = new List<NestItem>(request.Parts.Count);
|
// Quantity is the request's remaining demand; the engine may mutate these per-trial items,
|
||||||
foreach (var requirement in request.Parts)
|
// and that mutation is deliberately discarded — placement counts come from the result.
|
||||||
{
|
var items = context.CreateItems(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 =
|
var engine =
|
||||||
engineFactory(plate)
|
engineFactory(plate)
|
||||||
@@ -89,18 +65,6 @@ public sealed class DefaultPlateNester : IPlateNester
|
|||||||
if (parts == null)
|
if (parts == null)
|
||||||
throw new InvalidOperationException("Engine returned null placements.");
|
throw new InvalidOperationException("Engine returned null placements.");
|
||||||
|
|
||||||
var placements = new List<NestJobPlacement>(parts.Count);
|
return new PlateCandidate(context.MapPlacements(parts));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
using OpenNest.Engine.Jobs.Adapters;
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
@@ -11,17 +10,15 @@ namespace OpenNest.Engine.Jobs.Placement;
|
|||||||
/// remaining demand is read from the request and placement counts are derived from returned placements.
|
/// remaining demand is read from the request and placement counts are derived from returned placements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// A private <see cref="Drawing"/> per requirement is created once per solve and reused across trials
|
/// The identity/progress boundary mechanics live in <see cref="CandidatePlacementContext"/>: 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
|
/// (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"/>.
|
/// shared Drawing). Identity is by Drawing reference. Each trial gets a fresh private <see cref="Plate"/>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class StripPlateNester : IPlateNester
|
public sealed class StripPlateNester : IPlateNester
|
||||||
{
|
{
|
||||||
private readonly Func<Plate, StripNestEngine> engineFactory;
|
private readonly Func<Plate, StripNestEngine> engineFactory;
|
||||||
private readonly Dictionary<string, Drawing> drawingsById = new(StringComparer.Ordinal);
|
private readonly CandidatePlacementContext context = new();
|
||||||
private readonly Dictionary<Drawing, string> idByDrawing = new(
|
|
||||||
ReferenceEqualityComparer.Instance
|
|
||||||
);
|
|
||||||
|
|
||||||
public StripPlateNester()
|
public StripPlateNester()
|
||||||
: this(static plate => new StripNestEngine(plate)) { }
|
: this(static plate => new StripNestEngine(plate)) { }
|
||||||
@@ -43,28 +40,7 @@ public sealed class StripPlateNester : IPlateNester
|
|||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var plate = DrawingJobMapper.CreatePlate(request.Stock);
|
var plate = DrawingJobMapper.CreatePlate(request.Stock);
|
||||||
var items = new List<NestItem>(request.Parts.Count);
|
var items = context.CreateItems(request.Parts);
|
||||||
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 =
|
var engine =
|
||||||
engineFactory(plate)
|
engineFactory(plate)
|
||||||
@@ -75,18 +51,6 @@ public sealed class StripPlateNester : IPlateNester
|
|||||||
if (parts == null)
|
if (parts == null)
|
||||||
throw new InvalidOperationException("Engine returned null placements.");
|
throw new InvalidOperationException("Engine returned null placements.");
|
||||||
|
|
||||||
var placements = new List<NestJobPlacement>(parts.Count);
|
return new PlateCandidate(context.MapPlacements(parts));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user