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. 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 still gets a fresh private .
///
public sealed class DefaultPlateNester : IPlateNester
{
private readonly Func fillerFactory;
private readonly OrderedPlateNester restrictedRotationNester = new();
private readonly CandidatePlacementContext context = new();
public DefaultPlateNester()
: this(static plate => new DefaultPlateFiller(plate)) { }
/// Injectable for tests; defaults to .
internal DefaultPlateNester(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();
// The Default fill pipeline cannot express a locked or bounded rotation (start == end == 0
// reads as "unconstrained") and its Pairs/RectBestFit strategies rotate freely, so it 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));
}
}