feat(engine): introduce whole-job nesting contracts
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Synchronous whole-job solver. Cancellation throws, rather than returning partial success.</summary>
|
||||
public interface INestingEngine
|
||||
{
|
||||
NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null, CancellationToken token = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Places on one sheet only. Must not change stock, demand, or caller-owned domain objects.</summary>
|
||||
public interface IPlateNester
|
||||
{
|
||||
PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>One material/unit system's requirements. Collections are copied; all nested values are immutable.</summary>
|
||||
public sealed class NestJob
|
||||
{
|
||||
public NestJob(IEnumerable<NestJobPart> parts, IEnumerable<NestPlateStock> plates, NestJobOptions options = null)
|
||||
{
|
||||
Parts = Own(parts);
|
||||
Plates = Own(plates);
|
||||
Options = options ?? new NestJobOptions();
|
||||
if (Parts.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Parts.Count ||
|
||||
Plates.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Plates.Count)
|
||||
throw new ArgumentException("Part and stock IDs must each be unique.");
|
||||
}
|
||||
|
||||
public IReadOnlyList<NestJobPart> Parts { get; }
|
||||
public IReadOnlyList<NestPlateStock> Plates { get; }
|
||||
public NestJobOptions Options { get; }
|
||||
|
||||
internal static IReadOnlyList<T> Own<T>(IEnumerable<T> source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
var values = source.ToArray();
|
||||
if (values.Any(value => value is null))
|
||||
throw new ArgumentException("Null entries are not allowed.", nameof(source));
|
||||
return Array.AsReadOnly(values);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Immutable per-job options; selection never changes the legacy global registry.</summary>
|
||||
public sealed class NestJobOptions
|
||||
{
|
||||
public NestJobOptions(string placementStrategy = "Default", int? maxPlates = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(placementStrategy);
|
||||
if (maxPlates <= 0) throw new ArgumentOutOfRangeException(nameof(maxPlates));
|
||||
PlacementStrategy = placementStrategy;
|
||||
MaxPlates = maxPlates;
|
||||
}
|
||||
|
||||
public string PlacementStrategy { get; }
|
||||
/// <summary>Maximum physical sheets to commit, or null for no explicit cap.</summary>
|
||||
public int? MaxPlates { get; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>An immutable requirement, independent of drawing names, UI state, and drawing quantity counters.</summary>
|
||||
public sealed class NestJobPart
|
||||
{
|
||||
public NestJobPart(string id, PartGeometrySnapshot geometry, int quantity, int priority = 0,
|
||||
RotationPolicy rotation = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
ArgumentNullException.ThrowIfNull(geometry);
|
||||
if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
|
||||
Id = id;
|
||||
Geometry = geometry;
|
||||
Quantity = quantity;
|
||||
Priority = priority;
|
||||
Rotation = rotation ?? RotationPolicy.Automatic;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public PartGeometrySnapshot Geometry { get; }
|
||||
/// <summary>Positive number requested; never decremented by placement code.</summary>
|
||||
public int Quantity { get; }
|
||||
public int Priority { get; }
|
||||
public RotationPolicy Rotation { get; }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public enum NestJobStatus { Complete, Incomplete }
|
||||
public enum NestJobStopReason { Completed, StockExhausted, NoPlacementFound, PlateLimitReached }
|
||||
public enum NestJobStage { EvaluatingCandidate, PlateCommitted }
|
||||
|
||||
/// <summary>Committed counts only; candidate evaluation does not imply committed production.</summary>
|
||||
public sealed record NestJobProgress(NestJobStage Stage, string StockId, int PlateIndex,
|
||||
int CommittedPlates, int CommittedParts);
|
||||
|
||||
/// <summary>
|
||||
/// Rotate about the snapshot origin, then translate by X/Y into the selected plate quadrant frame.
|
||||
/// Rotation is in radians. InstanceIndex is zero-based and unique within a part requirement across the job.
|
||||
/// The runner assigns final instance indices when committing a candidate.
|
||||
/// </summary>
|
||||
public sealed record NestJobPlacement(string PartId, int InstanceIndex, double X, double Y, double Rotation);
|
||||
|
||||
/// <summary>Requested = Placed + Unplaced for a requirement ID.</summary>
|
||||
public sealed record PartFulfillment(string PartId, int Requested, int Placed, int Unplaced);
|
||||
|
||||
/// <summary>Used counts physical sheets; Remaining is null only for unlimited stock.</summary>
|
||||
public sealed record StockUsage(string StockId, int Used, int? Remaining);
|
||||
|
||||
/// <summary>One physical sheet, with owned ordered placements and immutable stock/settings snapshot.</summary>
|
||||
public sealed class NestJobPlateResult
|
||||
{
|
||||
public NestJobPlateResult(int plateIndex, NestPlateStock stock, IEnumerable<NestJobPlacement> placements)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stock);
|
||||
PlateIndex = plateIndex;
|
||||
Stock = stock;
|
||||
Placements = NestJob.Own(placements);
|
||||
}
|
||||
|
||||
public int PlateIndex { get; }
|
||||
public string StockId => Stock.Id;
|
||||
public NestPlateStock Stock { get; }
|
||||
public IReadOnlyList<NestJobPlacement> Placements { get; }
|
||||
}
|
||||
|
||||
/// <summary>Detached result values in commit/input order; no mutable Drawing, Plate, or NestItem escapes.</summary>
|
||||
public sealed class NestJobResult
|
||||
{
|
||||
public NestJobResult(NestJobStatus status, NestJobStopReason stopReason,
|
||||
IEnumerable<NestJobPlateResult> plates, IEnumerable<PartFulfillment> fulfillment,
|
||||
IEnumerable<StockUsage> stockUsage)
|
||||
{
|
||||
Status = status;
|
||||
StopReason = stopReason;
|
||||
Plates = NestJob.Own(plates);
|
||||
Fulfillment = NestJob.Own(fulfillment);
|
||||
StockUsage = NestJob.Own(stockUsage);
|
||||
}
|
||||
|
||||
public NestJobStatus Status { get; }
|
||||
public NestJobStopReason StopReason { get; }
|
||||
public IReadOnlyList<NestJobPlateResult> Plates { get; }
|
||||
public IReadOnlyList<PartFulfillment> Fulfillment { get; }
|
||||
public IReadOnlyList<StockUsage> StockUsage { get; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Contract-stage runner: empty jobs only. Nonempty allocation is not implemented yet.</summary>
|
||||
public sealed class NestJobRunner : INestingEngine
|
||||
{
|
||||
private readonly Func<string, IPlateNester> plateNesterFactory;
|
||||
|
||||
/// <summary>Stores a runner-local strategy factory; never consults the global engine registry.</summary>
|
||||
public NestJobRunner(Func<string, IPlateNester> plateNesterFactory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plateNesterFactory);
|
||||
this.plateNesterFactory = plateNesterFactory;
|
||||
}
|
||||
|
||||
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (job.Parts.Count != 0)
|
||||
throw new NotSupportedException("Whole-job allocation is not implemented yet; only empty jobs are supported.");
|
||||
return new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
|
||||
Array.Empty<NestJobPlateResult>(), Array.Empty<PartFulfillment>(),
|
||||
job.Plates.Select(stock => new StockUsage(stock.Id, 0, stock.Quantity)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Immutable stock settings. Size and spacing are copied value types, not caller-owned settings.</summary>
|
||||
public sealed class NestPlateStock
|
||||
{
|
||||
public NestPlateStock(string id, Size size, int? quantity = null, double partSpacing = 0,
|
||||
Spacing edgeSpacing = default, int quadrant = 1)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
if (quantity < 0) throw new ArgumentOutOfRangeException(nameof(quantity));
|
||||
Id = id;
|
||||
Size = size;
|
||||
Quantity = quantity;
|
||||
PartSpacing = partSpacing;
|
||||
EdgeSpacing = edgeSpacing;
|
||||
Quadrant = quadrant;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public Size Size { get; }
|
||||
/// <summary>Available physical sheets: null is unlimited, zero is legal but unavailable.</summary>
|
||||
public int? Quantity { get; }
|
||||
public double PartSpacing { get; }
|
||||
public Spacing EdgeSpacing { get; }
|
||||
public int Quadrant { get; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Exact immutable CNC motion values. Rapid moves retain contour/hole boundaries; arcs are not tessellated.</summary>
|
||||
public sealed record PartGeometryMotion(CodeType Type, double X, double Y, double CenterX,
|
||||
double CenterY, RotationType Rotation, LayerType Layer, bool Suppressed);
|
||||
|
||||
/// <summary>
|
||||
/// Owned geometry only: no Drawing, quantity, events, or mutable CNC references are retained.
|
||||
/// This initial boundary supports flat rapid/linear/arc programs and rejects other instructions explicitly.
|
||||
/// Coordinates and mode are preserved without normalization, rounding, or polygon approximation.
|
||||
/// </summary>
|
||||
public sealed class PartGeometrySnapshot
|
||||
{
|
||||
private PartGeometrySnapshot(Mode mode, IEnumerable<PartGeometryMotion> motions)
|
||||
{
|
||||
Mode = mode;
|
||||
Motions = NestJob.Own(motions);
|
||||
}
|
||||
|
||||
public Mode Mode { get; }
|
||||
public IReadOnlyList<PartGeometryMotion> Motions { get; }
|
||||
|
||||
/// <summary>Copies supported motion geometry immediately; later program edits cannot affect this snapshot.</summary>
|
||||
public static PartGeometrySnapshot FromProgram(Program program)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
var motions = program.Codes.Select(code => code switch
|
||||
{
|
||||
ArcMove arc => new PartGeometryMotion(arc.Type, arc.EndPoint.X, arc.EndPoint.Y,
|
||||
arc.CenterPoint.X, arc.CenterPoint.Y, arc.Rotation, arc.Layer, arc.Suppressed),
|
||||
LinearMove line => new PartGeometryMotion(line.Type, line.EndPoint.X, line.EndPoint.Y,
|
||||
0, 0, default, line.Layer, line.Suppressed),
|
||||
RapidMove rapid => new PartGeometryMotion(rapid.Type, rapid.EndPoint.X, rapid.EndPoint.Y,
|
||||
0, 0, default, default, rapid.Suppressed),
|
||||
_ => throw new NotSupportedException("Geometry snapshots currently support only flat rapid/linear/arc programs.")
|
||||
});
|
||||
return new PartGeometrySnapshot(program.Mode, motions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Owned candidate poses only; not committed fulfillment or inventory accounting.</summary>
|
||||
public sealed class PlateCandidate
|
||||
{
|
||||
public PlateCandidate(IEnumerable<NestJobPlacement> placements) => Placements = NestJob.Own(placements);
|
||||
public IReadOnlyList<NestJobPlacement> Placements { get; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Read-only stock settings and remaining requirements for a single candidate trial.</summary>
|
||||
public sealed class PlatePlacementRequest
|
||||
{
|
||||
public PlatePlacementRequest(NestPlateStock stock, IEnumerable<NestJobPart> parts)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stock);
|
||||
Stock = stock;
|
||||
Parts = NestJob.Own(parts);
|
||||
}
|
||||
|
||||
public NestPlateStock Stock { get; }
|
||||
public IReadOnlyList<NestJobPart> Parts { get; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public enum RotationPolicyKind { Fixed, BoundedSweep, Automatic }
|
||||
|
||||
/// <summary>Immutable rotation constraints, in radians about the geometry origin.</summary>
|
||||
public sealed class RotationPolicy
|
||||
{
|
||||
private RotationPolicy(RotationPolicyKind kind, double start, double end, double step)
|
||||
{
|
||||
if (!double.IsFinite(start) || !double.IsFinite(end) || !double.IsFinite(step))
|
||||
throw new ArgumentException("Angles must be finite.");
|
||||
Kind = kind;
|
||||
Start = start;
|
||||
End = end;
|
||||
Step = step;
|
||||
}
|
||||
|
||||
public RotationPolicyKind Kind { get; }
|
||||
public double Start { get; }
|
||||
public double End { get; }
|
||||
public double Step { get; }
|
||||
public static RotationPolicy Automatic { get; } = new(RotationPolicyKind.Automatic, 0, 0, 0);
|
||||
public static RotationPolicy Fixed(double angle) => new(RotationPolicyKind.Fixed, angle, angle, 0);
|
||||
public static RotationPolicy BoundedSweep(double start, double end, double step)
|
||||
{
|
||||
if (step <= 0 || end < start) throw new ArgumentException("Sweep needs a positive step and ordered bounds.");
|
||||
return new RotationPolicy(RotationPolicyKind.BoundedSweep, start, end, step);
|
||||
}
|
||||
|
||||
/// <summary>Preserves the legacy zero-step automatic sentinel; zero never means locked rotation.</summary>
|
||||
public static RotationPolicy FromLegacy(double stepAngle, double rotationStart, double rotationEnd) =>
|
||||
stepAngle == 0 ? Automatic : BoundedSweep(rotationStart, rotationEnd, stepAngle);
|
||||
}
|
||||
Reference in New Issue
Block a user