using System; using System.Collections.Generic; using System.Threading; namespace OpenNest; /// /// Migrated built-in placement strategy for the whole-job runner. Reuses /// fill/pack geometry but owns its own run-scoped bookkeeping: remaining demand is read from the /// request and placement counts are derived from returned placements, so the engine's private /// mutations never feed back into job accounting. /// /// /// A 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 engines mutate (per-trial) and canonical-frame copies, /// never the shared or its Quantity. Identity is by Drawing reference, /// never by name. Each trial still gets a fresh private . /// public sealed class DefaultPlateNester : IPlateNester { private readonly Func engineFactory; private readonly Dictionary drawingsById = new(StringComparer.Ordinal); private readonly Dictionary idByDrawing = new(ReferenceEqualityComparer.Instance); public DefaultPlateNester() : this(static plate => new DefaultNestEngine(plate)) { } /// Injectable for tests; defaults to . public DefaultPlateNester(Func engineFactory) { this.engineFactory = engineFactory ?? throw new ArgumentNullException(nameof(engineFactory)); } public PlateCandidate Place(PlatePlacementRequest request, IProgress progress = null, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(request); token.ThrowIfCancellationRequested(); var plate = DrawingJobMapper.CreatePlate(request.Stock); var items = new List(request.Parts.Count); 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); } // 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 = engineFactory(plate) ?? throw new InvalidOperationException("Engine factory returned null."); var legacyProgress = CandidateProgressBridge.Create(progress, request.Stock.Id); var parts = engine.Nest(items, legacyProgress, token); token.ThrowIfCancellationRequested(); if (parts == null) throw new InvalidOperationException("Engine returned null placements."); var placements = new List(parts.Count); 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); } }