using System; using System.Linq; using System.Threading; using OpenNest.Engine.Jobs.Adapters; using OpenNest.Engine.Jobs.Placement.Fillers; namespace OpenNest.Engine.Jobs.Placement; /// /// Built-in remnant placement strategy for the whole-job runner. Fills one candidate per trial with /// a for the vertical or horizontal remnant policy, on a fresh /// private plate, using the same run-scoped identity, progress, and accounting boundaries as /// . /// /// /// The remnant fillers share the Default pipeline's automatic-rotation limitation, so non-automatic /// requirements route through exactly as they do for Default. /// public sealed class RemnantPlateNester : IPlateNester { private readonly Func fillerFactory; private readonly OrderedPlateNester restrictedRotationNester = new(); private readonly CandidatePlacementContext context = new(); internal RemnantPlateNester(Func fillerFactory) { this.fillerFactory = fillerFactory ?? throw new ArgumentNullException(nameof(fillerFactory)); } /// Vertical-remnant policy: minimize X-extent, prefer horizontal placement. internal static RemnantPlateNester Vertical() => new(plate => new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical)); /// Horizontal-remnant policy: minimize Y-extent, prefer vertical placement. internal static RemnantPlateNester Horizontal() => new(plate => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal)); public PlateCandidate Place( PlatePlacementRequest request, IProgress progress = null, CancellationToken token = default ) { ArgumentNullException.ThrowIfNull(request); token.ThrowIfCancellationRequested(); // Same safety rule as DefaultPlateNester: the remnant fillers inherit the Default pipeline's // automatic-rotation limitation, so any non-automatic requirement goes to the policy-aware // ordered nester. if (request.Parts.Any(part => part.Rotation.Kind != RotationPolicyKind.Automatic)) return restrictedRotationNester.Place(request, progress, token); var plate = DrawingJobMapper.CreatePlate(request.Stock); var items = context.CreateItems(request.Parts); var filler = fillerFactory(plate) ?? throw new InvalidOperationException("Filler factory returned null."); var candidateProgress = CandidateProgressBridge.Create(progress, request.Stock.Id); var parts = filler.Nest(items, candidateProgress, token); token.ThrowIfCancellationRequested(); if (parts == null) throw new InvalidOperationException("Filler returned null placements."); return new PlateCandidate(context.MapPlacements(parts)); } }