#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.NestingEngines.Rectangles;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.RectanglePacking;
///
/// Packs mixed parts into one rectangular area as their program bounding boxes, at 0 or 90
/// degrees, with the maximal-rectangles packer. Every fit rule and pick mode is tried. The layout
/// that places the most parts, then the most box area, wins, compared tier by tier from the lowest
/// ; the caller's fill comparer breaks ties, so leftover packing
/// keeps the same goal (density, or a clear right or top remnant) as the rest of the fill.
///
internal static class AreaPacker
{
private static readonly FitRule[] Rules =
{
FitRule.BestShortSide,
FitRule.BestLongSide,
FitRule.BestArea,
FitRule.BottomLeft,
FitRule.LeftBottom,
FitRule.ContactPoint,
};
private static readonly PickMode[] Modes = { PickMode.Global, PickMode.Ordered };
///
/// Returns the packed parts. Items and the area grow by on their
/// right/top sides, so neighbouring parts are one spacing apart and the last may touch the
/// area's edge. A cancelled token stops between candidates and keeps the best layout so far.
///
/// Plate work area, when known. Only matching right/top boundaries
/// receive the existing work-area overhang allowance; internal areas remain strict.
public static List Pack(
Box area, IReadOnlyList items, double spacing, IFillComparer comparer, CancellationToken token, Box? workArea = null)
{
var boxes = items.Select(i => i.Drawing.Program.BoundingBox()).ToList();
var types = new List(items.Count);
var demand = new int[items.Count];
// Only the positive edges can overhang: placements anchor at a free box's lower-left.
var rightSlack = workArea != null && System.Math.Abs(area.Right - workArea.Right) <= MaxRectsSheet.Eps
? SheetPacker.OverhangAllowance : 0;
var topSlack = workArea != null && System.Math.Abs(area.Top - workArea.Top) <= MaxRectsSheet.Eps
? SheetPacker.OverhangAllowance : 0;
// Include boundary slack in this upper bound so a near-full-area item is not capped at zero.
var packArea = (area.Length + spacing + rightSlack) * (area.Width + spacing + topSlack);
for (var i = 0; i < items.Count; i++)
{
var w = boxes[i].Length + spacing;
var h = boxes[i].Width + spacing;
var sizes = new List<(double, double)>();
if (w > 0 && h > 0 && boxes[i].Length > 0 && boxes[i].Width > 0)
{
sizes.Add((w, h));
if (!w.IsEqualTo(h))
sizes.Add((h, w));
}
types.Add(new PackType(items[i].Priority, sizes, boxes[i].Length * boxes[i].Width));
// More copies than the area could hold by box area alone can never be placed.
var capacity = sizes.Count > 0 ? (int)System.Math.Min(int.MaxValue, packArea / (w * h)) : 0;
demand[i] = System.Math.Clamp(items[i].Quantity, 0, capacity);
}
var tiers = types.Select(t => t.Priority).Distinct().Order().ToArray();
List? best = null;
(int Count, double Area)[]? bestTiers = null;
foreach (var mode in Modes)
foreach (var rule in Rules)
{
if (best != null && token.IsCancellationRequested)
return best;
var sheet = new MaxRectsSheet(area.Length + spacing, area.Width + spacing, rightSlack, topSlack);
var placed = MaxRectsPacker.Pack(
types, (int[])demand.Clone(), sheet, rule, mode, CancellationToken.None);
var tierScores = tiers
.Select(tier =>
{
var inTier = placed.Where(p => types[p.Type].Priority == tier).ToList();
return (inTier.Count, inTier.Sum(p => types[p.Type].Area));
})
.ToArray();
var compare = best == null ? 1 : CompareTiers(tierScores, bestTiers!);
if (compare < 0)
continue;
var parts = ToParts(placed, items, boxes, area);
if (best == null || compare > 0 || comparer.IsBetter(parts, best, area))
{
best = parts;
bestTiers = tierScores;
}
}
return best!;
}
/// Positive when serves the most urgent tier better, negative when worse, 0 on a tie.
private static int CompareTiers((int Count, double Area)[] a, (int Count, double Area)[] b)
{
for (var i = 0; i < a.Length; i++)
{
if (a[i].Count != b[i].Count)
return a[i].Count > b[i].Count ? 1 : -1;
if (a[i].Area > b[i].Area + Tolerance.Epsilon)
return 1;
if (a[i].Area < b[i].Area - Tolerance.Epsilon)
return -1;
}
return 0;
}
private static List ToParts(
List placed, IReadOnlyList items, IReadOnlyList boxes, Box area)
{
var parts = new List(placed.Count);
foreach (var p in placed)
{
var part = new Part(items[p.Type].Drawing);
if (p.Size == 1)
part.Rotate(Angle.HalfPI);
var bounds = p.Size == 1 ? part.Program.BoundingBox() : boxes[p.Type];
part.Offset(new Vector(area.X + p.X, area.Y + p.Y) - bounds.Location);
parts.Add(part);
}
return parts;
}
}