#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Jobs;
namespace OpenNest.Engine.NestingEngines.Rectangles;
///
/// Squeezes the sheet that finishes the job. The fit rules pack into the whole sheet, so when the
/// remaining demand leaves room they spread it out and rarely find the shortest strip that holds
/// it: on N26-2A5 (14 door stops and 14 lock plates on a 26 x 26 clamped sheet) every rule
/// stopped at 8.0 high while a 7.5 strip holds all 28. When some packing places all the remaining
/// demand, this re-packs it into a bin cut shorter (then, separately, narrower), running every rule
/// and mode at each probe, and returns the tightest complete layout per axis when it beats the
/// unsqueezed ones; the stock loop then picks by cost and edge drop as usual.
///
/// Whether a greedy packing fits is far from monotonic in the bin size (that job packs at 7.5 but
/// not at 7.62 or 7.74), and a tight layout ends where a stack of spaced part sides ends. So the
/// probes are those stack heights: sums of the parts' spaced sides along the axis, starting from
/// the bin edge or the far edge of a clamp keep-out the layout reaches. They walk down from the
/// best extent so far, each step spread over what is left of the range for the probes left, and a
/// failed probe never raises the floor. When there are too many sums to list, the walk steps
/// through the size range instead. A probe that places everything moves the walk down to that
/// layout's own extent, so the answer never keeps a probe's slack.
/// Deterministic, and bounded by a work budget over demand size: large jobs skip it entirely.
///
internal static class SheetSqueeze
{
/// Most probes per axis.
internal const int MaxProbes = 12;
/// Box placements the squeeze may spend: probes x variants x demand, both axes.
internal const long PlacementBudget = 50_000;
/// Most stack heights listed before the walk falls back to stepping the size range.
private const int MaxStackHeights = 2048;
/// A size-range walk stops once less than this fraction of the bin is left to try,
/// and a squeezed layout must beat the whole-sheet ones by at least as much.
private const double Resolution = 1e-3;
///
/// Probes sit this far past a stack height. MaxRects already lets a box overrun by its Eps
/// (1e-9), which covers a stack's float rounding; a margin past that Eps would hide the bin's
/// top edge from the contact-point rule, which scores edge contact within Eps.
///
private const double ProbeMargin = 1e-10;
/// The unsqueezed layouts of on this stock.
/// Every rule and mode packed under a bin limit (width, height).
public static IEnumerable Run(
IReadOnlyList types, IReadOnlyList remaining, NestPlateStock stock,
IReadOnlyList plans, Func> packAll,
CancellationToken token)
{
var demand = remaining.Sum();
bool Complete(SheetPlan p) => p.Parts.Count == demand && p.Envelope != null;
var complete = plans.Where(Complete).ToList();
if (demand == 0 || complete.Count == 0)
yield break;
var probes = (int)System.Math.Min(MaxProbes, PlacementBudget / (2L * demand * plans.Count));
if (probes == 0)
yield break;
var work = stock.WorkArea;
var s = stock.PartSpacing;
var (binWidth, binHeight) = SheetPacker.BinSize(stock);
foreach (var vertical in new[] { true, false })
{
double Extent(SheetPlan p) => vertical ? p.Envelope!.Top - work.Bottom : p.Envelope!.Right - work.Left;
var bin = vertical ? binHeight : binWidth;
var across = vertical ? binWidth : binHeight;
var baseline = complete.Min(Extent);
var hi = baseline + s;
var lo = LowerBound(types, remaining, across, s, vertical);
// How far across the squeezed axis the tightest whole-sheet layout reaches: only
// clamps within it can hold a stack, so a zone the demand never reaches changes nothing.
var widest = complete.Where(p => Extent(p) == baseline).Max(p => vertical
? p.Envelope!.Right - work.Left
: p.Envelope!.Top - work.Bottom);
var heights = StackHeights(types, remaining, stock, across, vertical, hi, widest);
SheetPlan? tightest = null;
// Probes walk down from the best extent so far, each spread over what is left of the
// range for the probes left. A failed probe only moves the walk below it: a smaller bin
// may still fit, so it never raises the floor.
var cursor = hi;
for (var probe = 0; probe < probes; probe++)
{
token.ThrowIfCancellationRequested();
var target = cursor - (cursor - lo) / (probes - probe);
double limit;
if (heights != null)
{
// The highest stack height at or under the target, else the next one down.
var next = heights.LastOrDefault(h => h > lo && h <= target + ProbeMargin);
if (next == 0)
next = heights.LastOrDefault(h => h > lo && h < cursor - ProbeMargin);
if (next == 0)
break;
limit = next + ProbeMargin;
}
else if (cursor - lo > Resolution * bin)
limit = target;
else
break;
var fits = packAll(vertical ? null : limit, vertical ? limit : null).Where(Complete).ToList();
if (fits.Count == 0)
{
cursor = limit - ProbeMargin;
continue;
}
// First of the tightest keeps enumeration order as the tie-break.
var best = fits.Aggregate((a, b) => Extent(b) < Extent(a) ? b : a);
tightest = best;
cursor = System.Math.Min(limit, Extent(best) + s) - ProbeMargin;
}
if (tightest != null && Extent(tightest) < baseline - Resolution * bin)
yield return tightest;
}
}
///
/// A bin extent along the squeezed axis below which the demand cannot fit: every part needs its
/// shortest fitting side plus spacing, and the boxes' spaced area must fit across the bin.
/// Clamps only add to it, so ignoring them keeps the bound valid.
///
private static double LowerBound(
IReadOnlyList types, IReadOnlyList remaining, double across, double s, bool vertical)
{
double fit = 0, area = 0;
foreach (var t in types)
{
if (remaining[t.Index] == 0 || t.Orientations.Count == 0)
continue;
var sizes = Sizes(t, s, vertical).ToList();
var fitting = sizes.Where(z => z.Across <= across + SheetPacker.OverhangAllowance).ToList();
if (fitting.Count > 0)
fit = System.Math.Max(fit, fitting.Min(z => z.Along));
area += remaining[t.Index] * sizes.Min(z => z.Along * z.Across);
}
return System.Math.Max(fit, area / across);
}
///
/// Every bin extent below that a stack of spaced part sides reaches from
/// the bin edge or the far edge of a clamp keep-out that starts within
/// across the axis, sorted; null when there are more than
/// . Stacks ignore part counts, so some heights are unreachable;
/// they cost a probe each, never a missed layout.
///
private static List? StackHeights(
IReadOnlyList types, IReadOnlyList remaining, NestPlateStock stock,
double across, bool vertical, double below, double reach)
{
var s = stock.PartSpacing;
var sides = types
.Where(t => remaining[t.Index] > 0)
.SelectMany(t => Sizes(t, s, vertical))
.Where(z => z.Across <= across + SheetPacker.OverhangAllowance && z.Along > 0)
.Select(z => z.Along)
.Distinct()
.ToArray();
var work = stock.WorkArea;
var bases = new List { 0 };
foreach (var zone in stock.ClampZones)
{
var edge = (vertical ? zone.Bounds.Top - work.Bottom : zone.Bounds.Right - work.Left)
+ stock.ClampClearance;
var near = (vertical ? zone.Bounds.Left - work.Left : zone.Bounds.Bottom - work.Bottom)
- stock.ClampClearance;
if (edge > 0 && edge < below && near < reach)
bases.Add(edge);
}
// Keys on a 1e-6 grid merge sums that differ only by float rounding; each keeps its
// largest float value so a probe just past it holds every stack it stands for.
var seen = new Dictionary();
var queue = new Queue(bases);
while (queue.Count > 0)
{
var v = queue.Dequeue();
foreach (var side in sides)
{
var w = v + side;
if (w >= below - ProbeMargin)
continue;
var key = System.Math.Round(w, 6);
if (seen.TryGetValue(key, out var kept))
{
if (w > kept)
seen[key] = w;
continue;
}
if (seen.Count >= MaxStackHeights)
return null;
seen.Add(key, w);
queue.Enqueue(w);
}
}
return seen.Values.OrderBy(h => h).ToList();
}
private static IEnumerable<(double Along, double Across)> Sizes(BoxType type, double s, bool vertical) =>
type.Orientations.Select(o => vertical
? (o.Height + s, o.Width + s)
: (o.Width + s, o.Height + s));
}