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 placement strategy for the whole-job runner, for every entry in /// . Fills each candidate trial with a /// on a fresh private plate and owns its own run-scoped bookkeeping: remaining demand is read /// from the request and placement counts are derived from returned placements, so the filler's /// private mutations never feed back into job accounting. /// /// /// The identity/progress boundary mechanics live in : one /// private per requirement is created once per solve and reused across every /// candidate trial (the runner reuses one instance per job). This is safe /// because the fillers mutate (per-trial) and canonical-frame copies, /// never the shared or its Quantity. Identity is by Drawing reference, /// never by name. Each trial gets a fresh private and filler, so learned angles /// never cross trials. /// internal sealed class PlateNester : IPlateNester { private readonly Func fillerFactory; private readonly OrderedPlateNester restrictedRotationNester = new(); private readonly CandidatePlacementContext context = new(); /// Builds the trial's filler; injectable for tests. internal PlateNester(Func fillerFactory) { this.fillerFactory = fillerFactory ?? throw new ArgumentNullException(nameof(fillerFactory)); } public PlateCandidate Place( PlatePlacementRequest request, IProgress progress = null, CancellationToken token = default ) { ArgumentNullException.ThrowIfNull(request); token.ThrowIfCancellationRequested(); // No filler can express a locked or bounded rotation (start == end == 0 reads as // "unconstrained"): the Pairs/RectBestFit strategies rotate freely and the strip fills // try two classification angles, so they can return poses the requirement's // RotationPolicy forbids. Restricted requirements go to the policy-aware ordered nester, // which only proposes allowed angles and validates each pose. if (request.Parts.Any(part => part.Rotation.Kind != RotationPolicyKind.Automatic)) return restrictedRotationNester.Place(request, progress, token); var plate = DrawingJobMapper.CreatePlate(request.Stock); // Quantity is the request's remaining demand; the filler may mutate these per-trial items, // and that mutation is deliberately discarded — placement counts come from the result. var items = context.CreateItems(request.Parts); var filler = fillerFactory(plate) ?? throw new InvalidOperationException("Filler factory returned null."); var legacyProgress = CandidateProgressBridge.Create(progress, request.Stock.Id); var parts = filler.Nest(items, legacyProgress, token); token.ThrowIfCancellationRequested(); if (parts == null) throw new InvalidOperationException("Filler returned null placements."); return new PlateCandidate(context.MapPlacements(parts)); } }