feat(engine): execute inventory-bounded multi-plate jobs

This commit is contained in:
aj
2026-09-17 13:55:11 -04:00
parent 71dffce72c
commit 0963b051be
9 changed files with 659 additions and 9 deletions
@@ -0,0 +1,77 @@
using System;
using OpenNest.CNC;
namespace OpenNest;
/// <summary>Explicit-ID input mapping and exact supported-geometry reconstruction. Never retains caller objects.</summary>
public static class DrawingJobMapper
{
public static NestJobPart FromDrawing(string partId, Drawing drawing, int quantity)
{
ArgumentNullException.ThrowIfNull(drawing);
var constraints = drawing.Constraints;
return new NestJobPart(partId, PartGeometrySnapshot.FromProgram(drawing.Program), quantity, drawing.Priority,
constraints == null ? RotationPolicy.Automatic :
RotationPolicy.FromLegacy(constraints.StepAngle, constraints.StartAngle, constraints.EndAngle));
}
public static NestJobPart FromItem(string partId, NestItem item)
{
ArgumentNullException.ThrowIfNull(item);
ArgumentNullException.ThrowIfNull(item.Drawing);
return new NestJobPart(partId, PartGeometrySnapshot.FromProgram(item.Drawing.Program), item.Quantity,
item.Priority, RotationPolicy.FromLegacy(item.StepAngle, item.RotationStart, item.RotationEnd));
}
/// <summary>Available stock is explicit; the legacy plate repeat count is not inventory.</summary>
public static NestPlateStock FromPlate(string stockId, Plate plate, int? quantity)
{
ArgumentNullException.ThrowIfNull(plate);
return new NestPlateStock(stockId, plate.Size, quantity, plate.PartSpacing, plate.EdgeSpacing, plate.Quadrant);
}
public static Program ToProgram(PartGeometrySnapshot geometry)
{
ArgumentNullException.ThrowIfNull(geometry);
var program = new Program(geometry.Mode);
foreach (var motion in geometry.Motions)
{
var code = motion.Type switch
{
CodeType.RapidMove => (Motion)new RapidMove(motion.X, motion.Y),
CodeType.LinearMove => new LinearMove(motion.X, motion.Y) { Layer = motion.Layer },
CodeType.ArcMove => new ArcMove(motion.X, motion.Y, motion.CenterX, motion.CenterY, motion.Rotation)
{ Layer = motion.Layer },
_ => throw new NotSupportedException("Unsupported snapshot motion.")
};
code.Suppressed = motion.Suppressed;
program.Codes.Add(code);
}
return program;
}
internal static Drawing CreateDrawing(NestJobPart part)
{
var drawing = new Drawing(part.Id, ToProgram(part.Geometry)) { Priority = part.Priority };
drawing.Quantity.Required = part.Quantity;
drawing.Constraints = new NestConstraints
{
StepAngle = LegacyStep(part.Rotation),
StartAngle = part.Rotation.Start,
EndAngle = part.Rotation.End
};
return drawing;
}
// A fixed angle needs a nonzero legacy step so it is not misread as automatic.
internal static double LegacyStep(RotationPolicy policy) => policy.Kind == RotationPolicyKind.Fixed
? OpenNest.Math.Angle.TwoPI : policy.Step;
internal static Plate CreatePlate(NestPlateStock stock) => new(stock.Size)
{
Quantity = 1,
PartSpacing = stock.PartSpacing,
EdgeSpacing = stock.EdgeSpacing,
Quadrant = stock.Quadrant
};
}
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Threading;
namespace OpenNest;
/// <summary>
/// A fresh private legacy plate/drawing/item graph for each call. Only returned poses cross the boundary;
/// legacy quantity mutations are deliberately ignored. Does not certify geometric safety or rotation compliance.
/// </summary>
public sealed class LegacyPlateNesterAdapter : IPlateNester
{
private readonly Func<Plate, NestEngineBase> engineFactory;
public LegacyPlateNesterAdapter(Func<Plate, NestEngineBase> engineFactory)
{
ArgumentNullException.ThrowIfNull(engineFactory);
this.engineFactory = engineFactory;
}
/// <summary>Minimal built-in selection; never reads or changes NestEngineRegistry.</summary>
public static IPlateNester Create(string strategy) => strategy == "Default"
? new LegacyPlateNesterAdapter(plate => new DefaultNestEngine(plate))
: throw new NotSupportedException($"Unknown placement strategy: {strategy}.");
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>();
var identities = new Dictionary<Drawing, string>(ReferenceEqualityComparer.Instance);
foreach (var requirement in request.Parts)
{
var drawing = DrawingJobMapper.CreateDrawing(requirement);
identities.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("Legacy engine factory returned null.");
var parts = engine.Nest(items, null, token);
token.ThrowIfCancellationRequested();
if (parts == null) throw new InvalidOperationException("Legacy engine returned null placements.");
var placements = new List<NestJobPlacement>();
foreach (var part in parts)
{
if (part?.BaseDrawing == null || !identities.TryGetValue(part.BaseDrawing, out var id))
throw new InvalidOperationException("Legacy placement does not reference a private requirement drawing.");
placements.Add(new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation));
}
return new PlateCandidate(placements);
}
}
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest;
/// <summary>A detached mutable domain nest plus explicit requirement identity (never inferred from names).</summary>
public sealed class MaterializedNestResult
{
internal MaterializedNestResult(Nest nest, Dictionary<string, Drawing> drawings)
{
Nest = nest;
DrawingsByPartId = new ReadOnlyDictionary<string, Drawing>(drawings);
}
public Nest Nest { get; }
public IReadOnlyDictionary<string, Drawing> DrawingsByPartId { get; }
}
/// <summary>Materializes a result from the same job. Geometry safety remains the solver's future validation boundary.</summary>
public static class NestResultMaterializer
{
public static MaterializedNestResult Materialize(NestJob job, NestJobResult result)
{
ArgumentNullException.ThrowIfNull(job);
ArgumentNullException.ThrowIfNull(result);
var nest = new Nest();
var drawings = job.Parts.ToDictionary(p => p.Id, DrawingJobMapper.CreateDrawing, StringComparer.Ordinal);
foreach (var drawing in drawings.Values) nest.Drawings.Add(drawing);
foreach (var sheet in result.Plates)
{
var plate = DrawingJobMapper.CreatePlate(sheet.Stock);
foreach (var pose in sheet.Placements)
{
if (!drawings.TryGetValue(pose.PartId, out var drawing))
throw new ArgumentException("Result contains a requirement not present in the job.", nameof(result));
// Do not use CreateAtOrigin: it normalizes bounds and would change the snapshot frame.
var part = new Part(drawing);
part.Rotate(pose.Rotation);
part.Location = new Vector(pose.X, pose.Y);
part.UpdateBounds();
// Quantity=1 is set before the only attachment; Plate's event owns Nested accounting.
plate.Parts.Add(part);
}
nest.Plates.Add(plate);
}
return new MaterializedNestResult(nest, drawings);
}
}