#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; } }