refactor(packing): share the maximal-rectangles packing loop
Move the Rectangles engine's global and ordered pick loops into RectanglePacking.MaxRectsPacker, which works on plain box sizes so other fill paths can reuse it. SheetPacker maps part orientations onto it. No behavior change: rectangle-lane benchmark results are identical job for job (91 strict and 77 boxable jobs).
This commit is contained in:
@@ -9,15 +9,6 @@ using OpenNest.Geometry;
|
|||||||
|
|
||||||
namespace OpenNest.Engine.NestingEngines.Rectangles;
|
namespace OpenNest.Engine.NestingEngines.Rectangles;
|
||||||
|
|
||||||
/// <summary>How the next box is chosen on a sheet.</summary>
|
|
||||||
internal enum PickMode
|
|
||||||
{
|
|
||||||
/// <summary>Every step places whichever remaining type/orientation scores best anywhere.</summary>
|
|
||||||
Global,
|
|
||||||
/// <summary>Types in (priority, largest box first) order; each fills until it no longer fits.</summary>
|
|
||||||
Ordered,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>One placed box: which part type, which orientation, and its material bounds' corner.</summary>
|
/// <summary>One placed box: which part type, which orientation, and its material bounds' corner.</summary>
|
||||||
internal readonly record struct Placed(BoxType Type, BoxOrientation Orientation, double Left, double Bottom);
|
internal readonly record struct Placed(BoxType Type, BoxOrientation Orientation, double Left, double Bottom);
|
||||||
|
|
||||||
@@ -50,13 +41,14 @@ internal static class SheetPacker
|
|||||||
var work = stock.WorkArea;
|
var work = stock.WorkArea;
|
||||||
var s = stock.PartSpacing;
|
var s = stock.PartSpacing;
|
||||||
var sheet = new MaxRectsSheet(work.Right - work.Left + s, work.Top - work.Bottom + s);
|
var sheet = new MaxRectsSheet(work.Right - work.Left + s, work.Top - work.Bottom + s);
|
||||||
var left = remaining.ToArray();
|
var packTypes = types
|
||||||
var placed = new List<Placed>();
|
.Select(t => new PackType(
|
||||||
|
t.Priority, t.Orientations.Select(o => (o.Width + s, o.Height + s)).ToList(), t.BoxArea))
|
||||||
if (mode == PickMode.Global)
|
.ToList();
|
||||||
PackGlobal(types, left, sheet, s, rule, placed, token);
|
var placed = MaxRectsPacker
|
||||||
else
|
.Pack(packTypes, remaining.ToArray(), sheet, rule, mode, token)
|
||||||
PackOrdered(types, left, sheet, s, rule, placed, token);
|
.Select(p => new Placed(types[p.Type], types[p.Type].Orientations[p.Size], p.X, p.Y))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var area = 0.0;
|
var area = 0.0;
|
||||||
Box? envelope = null;
|
Box? envelope = null;
|
||||||
@@ -73,93 +65,6 @@ internal static class SheetPacker
|
|||||||
return new SheetPlan(stock, world, area, envelope, rule, mode);
|
return new SheetPlan(stock, world, area, envelope, rule, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PackGlobal(
|
|
||||||
IReadOnlyList<BoxType> types, int[] left, MaxRectsSheet sheet, double s, FitRule rule,
|
|
||||||
List<Placed> placed, CancellationToken token)
|
|
||||||
{
|
|
||||||
// Free space only shrinks, so an orientation that fails once never fits again on this sheet.
|
|
||||||
var dead = types.Select(t => new bool[t.Orientations.Count]).ToArray();
|
|
||||||
var tiers = types.Select(t => t.Priority).Distinct().Order().ToArray();
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
(BoxType Type, int Orientation, Rect Place, double P, double S)? best = null;
|
|
||||||
foreach (var tier in tiers)
|
|
||||||
{
|
|
||||||
foreach (var type in types)
|
|
||||||
{
|
|
||||||
if (type.Priority != tier || left[type.Index] == 0)
|
|
||||||
continue;
|
|
||||||
for (var o = 0; o < type.Orientations.Count; o++)
|
|
||||||
{
|
|
||||||
if (dead[type.Index][o])
|
|
||||||
continue;
|
|
||||||
var orientation = type.Orientations[o];
|
|
||||||
var fit = sheet.FindBest(orientation.Width + s, orientation.Height + s, rule);
|
|
||||||
if (fit is not { } f)
|
|
||||||
{
|
|
||||||
dead[type.Index][o] = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (best is not { } b || Better(f.Primary, f.Secondary, type.BoxArea, b.P, b.S, b.Type.BoxArea))
|
|
||||||
best = (type, o, f.Place, f.Primary, f.Secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (best != null)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (best is not { } chosen)
|
|
||||||
return;
|
|
||||||
sheet.Place(chosen.Place);
|
|
||||||
left[chosen.Type.Index]--;
|
|
||||||
placed.Add(new Placed(chosen.Type, chosen.Type.Orientations[chosen.Orientation], chosen.Place.X, chosen.Place.Y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void PackOrdered(
|
|
||||||
IReadOnlyList<BoxType> types, int[] left, MaxRectsSheet sheet, double s, FitRule rule,
|
|
||||||
List<Placed> placed, CancellationToken token)
|
|
||||||
{
|
|
||||||
var order = types
|
|
||||||
.Where(t => t.Orientations.Count > 0)
|
|
||||||
.OrderBy(t => t.Priority)
|
|
||||||
.ThenByDescending(t => t.BoxArea)
|
|
||||||
.ThenBy(t => t.Index);
|
|
||||||
|
|
||||||
foreach (var type in order)
|
|
||||||
{
|
|
||||||
while (left[type.Index] > 0)
|
|
||||||
{
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
(int Orientation, Rect Place, double P, double S)? best = null;
|
|
||||||
for (var o = 0; o < type.Orientations.Count; o++)
|
|
||||||
{
|
|
||||||
var orientation = type.Orientations[o];
|
|
||||||
var fit = sheet.FindBest(orientation.Width + s, orientation.Height + s, rule);
|
|
||||||
if (fit is { } f && (best is not { } b || Better(f.Primary, f.Secondary, 0, b.P, b.S, 0)))
|
|
||||||
best = (o, f.Place, f.Primary, f.Secondary);
|
|
||||||
}
|
|
||||||
if (best is not { } chosen)
|
|
||||||
break;
|
|
||||||
sheet.Place(chosen.Place);
|
|
||||||
left[type.Index]--;
|
|
||||||
placed.Add(new Placed(type, type.Orientations[chosen.Orientation], chosen.Place.X, chosen.Place.Y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Lower score wins; on a tie the larger box goes first (strict, so input order breaks full ties).</summary>
|
|
||||||
private static bool Better(double p, double s, double area, double bp, double bs, double barea)
|
|
||||||
{
|
|
||||||
if (p < bp - MaxRectsSheet.Eps) return true;
|
|
||||||
if (p > bp + MaxRectsSheet.Eps) return false;
|
|
||||||
if (s < bs - MaxRectsSheet.Eps) return true;
|
|
||||||
if (s > bs + MaxRectsSheet.Eps) return false;
|
|
||||||
return area > barea + MaxRectsSheet.Eps;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Box Union(Box a, Box b)
|
private static Box Union(Box a, Box b)
|
||||||
{
|
{
|
||||||
var l = System.Math.Min(a.Left, b.Left);
|
var l = System.Math.Min(a.Left, b.Left);
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.RectanglePacking;
|
||||||
|
|
||||||
|
/// <summary>How the next box is chosen on a sheet.</summary>
|
||||||
|
internal enum PickMode
|
||||||
|
{
|
||||||
|
/// <summary>Every step places whichever remaining type/size scores best anywhere.</summary>
|
||||||
|
Global,
|
||||||
|
|
||||||
|
/// <summary>Types in (priority, largest box first) order; each fills until it no longer fits.</summary>
|
||||||
|
Ordered,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A box type to pack.</summary>
|
||||||
|
/// <param name="Priority">Demand tier; lower numbers are served first.</param>
|
||||||
|
/// <param name="Sizes">Allowed packing sizes (one per orientation), already grown by any spacing.</param>
|
||||||
|
/// <param name="Area">Ranking area: larger boxes win score ties and go first in ordered mode.</param>
|
||||||
|
internal sealed record PackType(int Priority, IReadOnlyList<(double Width, double Height)> Sizes, double Area);
|
||||||
|
|
||||||
|
/// <summary>One placed box: type and size index, and its corner in sheet-local coordinates.</summary>
|
||||||
|
internal readonly record struct PackPlacement(int Type, int Size, double X, double Y);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Packs box types onto one <see cref="MaxRectsSheet"/> under a fit rule and pick mode. Lower
|
||||||
|
/// priority numbers are always served first: a higher-number type is only placed when no
|
||||||
|
/// lower-number type still fits anywhere. Deterministic: ties resolve by input order.
|
||||||
|
/// </summary>
|
||||||
|
internal static class MaxRectsPacker
|
||||||
|
{
|
||||||
|
/// <param name="left">Remaining count per type, indexed like <paramref name="types"/>; decremented as boxes are placed.</param>
|
||||||
|
public static List<PackPlacement> Pack(
|
||||||
|
IReadOnlyList<PackType> types,
|
||||||
|
int[] left,
|
||||||
|
MaxRectsSheet sheet,
|
||||||
|
FitRule rule,
|
||||||
|
PickMode mode,
|
||||||
|
CancellationToken token
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var placed = new List<PackPlacement>();
|
||||||
|
if (mode == PickMode.Global)
|
||||||
|
PackGlobal(types, left, sheet, rule, placed, token);
|
||||||
|
else
|
||||||
|
PackOrdered(types, left, sheet, rule, placed, token);
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PackGlobal(
|
||||||
|
IReadOnlyList<PackType> types,
|
||||||
|
int[] left,
|
||||||
|
MaxRectsSheet sheet,
|
||||||
|
FitRule rule,
|
||||||
|
List<PackPlacement> placed,
|
||||||
|
CancellationToken token
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Free space only shrinks, so a size that fails once never fits again on this sheet.
|
||||||
|
var dead = types.Select(t => new bool[t.Sizes.Count]).ToArray();
|
||||||
|
var tiers = types.Select(t => t.Priority).Distinct().Order().ToArray();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
(int Type, int Size, Rect Place, double P, double S)? best = null;
|
||||||
|
foreach (var tier in tiers)
|
||||||
|
{
|
||||||
|
for (var t = 0; t < types.Count; t++)
|
||||||
|
{
|
||||||
|
var type = types[t];
|
||||||
|
if (type.Priority != tier || left[t] == 0)
|
||||||
|
continue;
|
||||||
|
for (var o = 0; o < type.Sizes.Count; o++)
|
||||||
|
{
|
||||||
|
if (dead[t][o])
|
||||||
|
continue;
|
||||||
|
var (w, h) = type.Sizes[o];
|
||||||
|
var fit = sheet.FindBest(w, h, rule);
|
||||||
|
if (fit is not { } f)
|
||||||
|
{
|
||||||
|
dead[t][o] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
best is not { } b
|
||||||
|
|| Better(f.Primary, f.Secondary, type.Area, b.P, b.S, types[b.Type].Area)
|
||||||
|
)
|
||||||
|
best = (t, o, f.Place, f.Primary, f.Secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best != null)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best is not { } chosen)
|
||||||
|
return;
|
||||||
|
sheet.Place(chosen.Place);
|
||||||
|
left[chosen.Type]--;
|
||||||
|
placed.Add(new PackPlacement(chosen.Type, chosen.Size, chosen.Place.X, chosen.Place.Y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PackOrdered(
|
||||||
|
IReadOnlyList<PackType> types,
|
||||||
|
int[] left,
|
||||||
|
MaxRectsSheet sheet,
|
||||||
|
FitRule rule,
|
||||||
|
List<PackPlacement> placed,
|
||||||
|
CancellationToken token
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var order = Enumerable
|
||||||
|
.Range(0, types.Count)
|
||||||
|
.Where(t => types[t].Sizes.Count > 0)
|
||||||
|
.OrderBy(t => types[t].Priority)
|
||||||
|
.ThenByDescending(t => types[t].Area)
|
||||||
|
.ThenBy(t => t);
|
||||||
|
|
||||||
|
foreach (var t in order)
|
||||||
|
{
|
||||||
|
var type = types[t];
|
||||||
|
while (left[t] > 0)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
(int Size, Rect Place, double P, double S)? best = null;
|
||||||
|
for (var o = 0; o < type.Sizes.Count; o++)
|
||||||
|
{
|
||||||
|
var (w, h) = type.Sizes[o];
|
||||||
|
var fit = sheet.FindBest(w, h, rule);
|
||||||
|
if (fit is { } f && (best is not { } b || Better(f.Primary, f.Secondary, 0, b.P, b.S, 0)))
|
||||||
|
best = (o, f.Place, f.Primary, f.Secondary);
|
||||||
|
}
|
||||||
|
if (best is not { } chosen)
|
||||||
|
break;
|
||||||
|
sheet.Place(chosen.Place);
|
||||||
|
left[t]--;
|
||||||
|
placed.Add(new PackPlacement(t, chosen.Size, chosen.Place.X, chosen.Place.Y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Lower score wins; on a tie the larger box goes first (strict, so input order breaks full ties).</summary>
|
||||||
|
private static bool Better(double p, double s, double area, double bp, double bs, double barea)
|
||||||
|
{
|
||||||
|
if (p < bp - MaxRectsSheet.Eps)
|
||||||
|
return true;
|
||||||
|
if (p > bp + MaxRectsSheet.Eps)
|
||||||
|
return false;
|
||||||
|
if (s < bs - MaxRectsSheet.Eps)
|
||||||
|
return true;
|
||||||
|
if (s > bs + MaxRectsSheet.Eps)
|
||||||
|
return false;
|
||||||
|
return area > barea + MaxRectsSheet.Eps;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user