using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Jobs;
namespace OpenNest.Engine.Qwen38FlashNext.Engine;
using Math = System.Math;
internal sealed record SheetAttempt(SheetPacker Packer, int StockIndex);
///
/// Whole-job decision layer: which stock the next sheet uses, the order parts are
/// demanded in, when a sheet is finished, and when the job stops. Every placement
/// inside a sheet comes from ; nothing here delegates to a
/// built-in engine, nester, filler, or runner.
///
internal sealed class JobSolver
{
private readonly NestJob _job;
private readonly PartPreparation _prep;
private readonly Dictionary _remaining;
private readonly Dictionary _placed;
private readonly Dictionary _used;
private readonly List _sheets = new();
private sealed record CommittedSheet(int StockIndex, List Placements);
public JobSolver(NestJob job, PartPreparation prep)
{
_job = job;
_prep = prep;
_remaining = job.Parts.ToDictionary(p => p.Id, p => p.Quantity, StringComparer.Ordinal);
_placed = job.Parts.ToDictionary(p => p.Id, _ => 0, StringComparer.Ordinal);
_used = job.Plates.ToDictionary(s => s.Id, _ => 0, StringComparer.Ordinal);
}
private static readonly bool _diag =
Environment.GetEnvironmentVariable("QWEN_NEST_DIAG") == "1";
private void Diag(string message)
{
if (_diag)
Console.Error.WriteLine(
$"[qwen] sheets={_sheets.Count} placed={_placed.Values.Sum()} " +
$"mem={GC.GetTotalMemory(false) / 1048576}MB gc0={GC.CollectionCount(0)} " +
$"gc2={GC.CollectionCount(2)} {message}"
);
}
public NestJobResult Solve(IProgress? progress, CancellationToken token)
{
var reason = NestJobStopReason.Completed;
while (true)
{
token.ThrowIfCancellationRequested();
var outstanding = OutstandingDemands();
if (outstanding.Count == 0)
break;
if (_job.Options.MaxPlates is int cap && _sheets.Count >= cap)
{
reason = NestJobStopReason.PlateLimitReached;
break;
}
var attempt = BestNextSheet(outstanding, progress, token);
Diag($"nextSheet -> {(attempt == null ? "none" : $"stock {_job.Plates[attempt.StockIndex].Id} placed {attempt.Packer.Placed.Count}")}");
if (attempt == null)
{
reason = AnyStockAvailable()
? NestJobStopReason.NoPlacementFound
: NestJobStopReason.StockExhausted;
break;
}
CommitSheet(attempt.Packer);
progress?.Report(
new NestJobProgress(
NestJobStage.PlateCommitted,
_job.Plates[attempt.StockIndex].Id,
_sheets.Count - 1,
_sheets.Count,
_placed.Values.Sum()
)
);
}
return BuildResult(reason);
}
private List OutstandingDemands()
{
var demands = new List();
foreach (var model in _prep.Models)
if (_remaining[model.Id] > 0)
demands.Add(model);
// This engine's own ordering: priority first, then the tallest-then-largest
// part first (a part's thinnest orientation extent), then id for determinism.
demands.Sort(
(a, b) =>
{
var byPriority = a.Priority.CompareTo(b.Priority);
if (byPriority != 0)
return byPriority;
if (DemandOrderMode == 1)
{
var byArea = b.Area.CompareTo(a.Area);
if (byArea != 0)
return byArea;
}
else if (DemandOrderMode == 2)
{
// Biggest footprint first (worst-case largest extent, descending).
var byMaxSpan = MaximumMaxSpan(b).CompareTo(MaximumMaxSpan(a));
if (byMaxSpan != 0)
return -byMaxSpan;
var byArea2 = b.Area.CompareTo(a.Area);
if (byArea2 != 0)
return byArea2;
}
else
{
var bySpan = MinimumMaxSpan(b).CompareTo(MinimumMaxSpan(a));
if (bySpan != 0)
return bySpan;
var byArea = b.Area.CompareTo(a.Area);
if (byArea != 0)
return byArea;
}
return string.CompareOrdinal(a.Id, b.Id);
}
);
return demands;
}
private double MinimumMaxSpan(PartModel model)
{
if (!_minimumSpan.TryGetValue(model.Id, out var span))
{
span = double.MaxValue;
foreach (var angle in PartPreparation.CandidateAngles(model))
{
var orientation = _prep.Oriented(model, angle, 0);
var worst = Math.Max(orientation.Width, orientation.Height);
if (worst < span)
span = worst;
}
_minimumSpan[model.Id] = span;
}
return span;
}
private readonly Dictionary _minimumSpan = new(StringComparer.Ordinal);
private double MaximumMaxSpan(PartModel model)
{
if (!_maximumSpan.TryGetValue(model.Id, out var span))
{
span = 0;
foreach (var angle in PartPreparation.CandidateAngles(model))
{
var orientation = _prep.Oriented(model, angle, 0);
var worst = Math.Max(orientation.Width, orientation.Height);
if (worst > span)
span = worst;
}
_maximumSpan[model.Id] = span;
}
return span;
}
private readonly Dictionary _maximumSpan = new(StringComparer.Ordinal);
///
/// Packs every available stock size independently and commits the best trial:
/// most instances first, then highest priority coverage, then the smallest sheet
/// area (the cost function the benchmark scores), then input order.
///
private SheetAttempt? BestNextSheet(
List outstanding,
IProgress? progress,
CancellationToken token
)
{
SheetAttempt? best = null;
TrialScore bestScore = default;
for (var index = 0; index < _job.Plates.Count; index++)
{
var stock = _job.Plates[index];
if (stock.Quantity is int quantity && _used[stock.Id] >= quantity)
continue;
token.ThrowIfCancellationRequested();
progress?.Report(
new NestJobProgress(
NestJobStage.EvaluatingCandidate,
stock.Id,
_sheets.Count,
_sheets.Count,
_placed.Values.Sum()
)
);
var packer = SheetPacker.Create(stock, _prep, index);
var fillWatch = System.Diagnostics.Stopwatch.StartNew();
FillSheet(packer, outstanding, token);
fillWatch.Stop();
if (_diag)
Diag(
$"trial stock {stock.Id}: placed={packer.Placed.Count} " +
$"{fillWatch.ElapsedMilliseconds}ms {packer.DiagStats()}"
);
if (packer.Placed.Count == 0)
continue;
var score = ScoreTrial(packer);
bool Better(TrialScore s)
{
if (CostFirstScoring)
{
// Benchmark cost is total plate AREA, so prefer the trial that
// delivers the cheapest material per unit of part area placed;
// priority coverage still outranks, and count breaks cost ties.
if (best == null)
return true;
if (s.priorityHits != bestScore.priorityHits)
return s.priorityHits > bestScore.priorityHits;
if (Math.Abs(s.costPerArea - bestScore.costPerArea) > 1e-9)
return s.costPerArea < bestScore.costPerArea;
if (s.count != bestScore.count)
return s.count > bestScore.count;
return s.area < bestScore.area;
}
return best == null
|| s.count > bestScore.count
|| (s.count == bestScore.count && s.priorityHits > bestScore.priorityHits)
|| (
s.count == bestScore.count
&& s.priorityHits == bestScore.priorityHits
&& s.area < bestScore.area
);
}
if (Better(score))
{
best = new SheetAttempt(packer, index);
bestScore = score;
}
}
return best;
}
///
/// This engine's fill policy for one sheet: walk the demand order and drain each
/// requirement greedily; a requirement that cannot place any more instances is
/// skipped (never aborts the sheet) and retried on the next sheet. Consumes a
/// local copy of demand - losing this trial must not change job state.
///
private void FillSheet(SheetPacker packer, List outstanding, CancellationToken token)
{
var available = new Dictionary(StringComparer.Ordinal);
foreach (var model in outstanding)
available[model.Id] = _remaining[model.Id];
// One drain pass per requirement, in demand order. Gap-filling retries are
// deliberately NOT an unbounded loop: a sheet's failed-insert scans get more
// expensive as it fills, so an unbounded retry loop blows the benchmark's
// 5-minute wall (observed 2-3x on a 69-drawing job). Pass two runs only with
// the explicit retry budget below.
foreach (var model in outstanding)
{
if (available[model.Id] <= 0)
continue;
if (!packer.CanEverFit(model))
continue;
var modelWatch = System.Diagnostics.Stopwatch.StartNew();
while (available[model.Id] > 0 && !packer.IsFull)
{
token.ThrowIfCancellationRequested();
if (!packer.TryInsert(model, out _))
break;
available[model.Id]--;
}
modelWatch.Stop();
if (_diag && modelWatch.ElapsedMilliseconds > 200)
Diag($" fill model {model.Id}: placed={packer.Placed.Count} {modelWatch.ElapsedMilliseconds}ms {packer.DiagStats()}");
}
// Gap-fill pass: a part that could not fit between two early placements may
// fit the gaps a later model leaves behind. Failed-insert scans cost about a
// full candidate sweep each, so the pass is hard time-boxed - on a crowded
// sheet the untried budget was measured at minutes per job, well over the
// benchmark's wall; the box keeps worst case near the first pass's cost.
var retryWatch = System.Diagnostics.Stopwatch.StartNew();
var retryAgain = true;
while (retryAgain && retryWatch.ElapsedMilliseconds < GapFillMilliseconds)
{
retryAgain = false;
foreach (var model in outstanding)
{
if (available[model.Id] <= 0 || packer.IsFull)
continue;
if (retryWatch.ElapsedMilliseconds >= GapFillMilliseconds)
break;
while (available[model.Id] > 0)
{
token.ThrowIfCancellationRequested();
if (!packer.TryInsert(model, out _))
break;
available[model.Id]--;
retryAgain = true;
}
}
}
}
///
/// Demand ordering within the priority sort: 1 = largest material area first
/// (measured best: big parts establish the sheet skeleton, small ones then fill
/// the seams; 12% lower job cost than span-first on a real production job), 2 = largest footprint
/// first, 0 = smallest worst-case extent first (original).
///
private static readonly int DemandOrderMode =
int.TryParse(Environment.GetEnvironmentVariable("QWEN_DEMAND_ORDER"), out var m)
? m
: 1;
/// Wall-clock budget for one sheet's gap-fill pass.
private static readonly int GapFillMilliseconds =
int.TryParse(Environment.GetEnvironmentVariable("QWEN_GAPFILL_MS"), out var ms)
? ms
: 120;
/// Trial-sheet metrics; costPerArea = plate area / part area placed.
private readonly record struct TrialScore(
int count,
int priorityHits,
double area,
double costPerArea
);
///
/// Greedy trial-comparison mode. Cost-first optimizes the benchmark's cost
/// function (total plate area); count-first is the conservative fill policy.
/// Env override exists for A/B measurement.
///
private static readonly bool CostFirstScoring =
Environment.GetEnvironmentVariable("QWEN_COST_FIRST") != "0";
private TrialScore ScoreTrial(SheetPacker packer)
{
var count = packer.Placed.Count;
var bestPriority = int.MaxValue;
foreach (var placed in packer.Placed)
if (placed.Model.Priority < bestPriority)
bestPriority = placed.Model.Priority;
var priorityHits = packer.Placed.Count(p => p.Model.Priority == bestPriority);
var area = packer.Stock.Size.Width * packer.Stock.Size.Length;
var placedArea = 0.0;
foreach (var placed in packer.Placed)
placedArea += placed.Model.Area;
var costPerArea = placedArea > 1e-9 ? area / placedArea : double.MaxValue;
return new TrialScore(count, priorityHits, area, costPerArea);
}
private void CommitSheet(SheetPacker packer)
{
_sheets.Add(new CommittedSheet(packer.StockIndex, packer.Placed));
_used[packer.Stock.Id]++;
foreach (var placed in packer.Placed)
{
_placed[placed.Model.Id]++;
_remaining[placed.Model.Id]--;
}
}
private bool AnyStockAvailable()
{
foreach (var stock in _job.Plates)
if (stock.Quantity is null || _used[stock.Id] < stock.Quantity.Value)
return true;
return false;
}
private NestJobResult BuildResult(NestJobStopReason reason)
{
var instanceIndex = new Dictionary(StringComparer.Ordinal);
var plates = new List();
foreach (var sheet in _sheets)
{
var placements = new List(sheet.Placements.Count);
foreach (var placed in sheet.Placements)
{
instanceIndex.TryGetValue(placed.Model.Id, out var next);
instanceIndex[placed.Model.Id] = next + 1;
placements.Add(
new NestJobPlacement(
placed.Model.Id,
next,
placed.X,
placed.Y,
placed.Orientation.Angle
)
);
}
plates.Add(
new NestJobPlateResult(sheet.StockIndex, _job.Plates[sheet.StockIndex], placements)
);
}
var fulfillment = _job.Parts
.Select(part => new PartFulfillment(
part.Id,
part.Quantity,
_placed[part.Id],
_remaining[part.Id]
))
.ToList();
var stockUsage = _job.Plates
.Select(stock => new StockUsage(
stock.Id,
_used[stock.Id],
stock.Quantity is int quantity ? quantity - _used[stock.Id] : null
))
.ToList();
return new NestJobResult(
reason == NestJobStopReason.Completed
? NestJobStatus.Complete
: NestJobStatus.Incomplete,
reason,
plates,
fulfillment,
stockUsage
);
}
}