diff --git a/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs b/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs
index 4fd3e11..79858c6 100644
--- a/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs
+++ b/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs
@@ -9,15 +9,6 @@ using OpenNest.Geometry;
namespace OpenNest.Engine.NestingEngines.Rectangles;
-/// How the next box is chosen on a sheet.
-internal enum PickMode
-{
- /// Every step places whichever remaining type/orientation scores best anywhere.
- Global,
- /// Types in (priority, largest box first) order; each fills until it no longer fits.
- Ordered,
-}
-
/// One placed box: which part type, which orientation, and its material bounds' corner.
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 s = stock.PartSpacing;
var sheet = new MaxRectsSheet(work.Right - work.Left + s, work.Top - work.Bottom + s);
- var left = remaining.ToArray();
- var placed = new List();
-
- if (mode == PickMode.Global)
- PackGlobal(types, left, sheet, s, rule, placed, token);
- else
- PackOrdered(types, left, sheet, s, rule, placed, token);
+ var packTypes = types
+ .Select(t => new PackType(
+ t.Priority, t.Orientations.Select(o => (o.Width + s, o.Height + s)).ToList(), t.BoxArea))
+ .ToList();
+ var placed = MaxRectsPacker
+ .Pack(packTypes, remaining.ToArray(), sheet, rule, mode, token)
+ .Select(p => new Placed(types[p.Type], types[p.Type].Orientations[p.Size], p.X, p.Y))
+ .ToList();
var area = 0.0;
Box? envelope = null;
@@ -73,93 +65,6 @@ internal static class SheetPacker
return new SheetPlan(stock, world, area, envelope, rule, mode);
}
- private static void PackGlobal(
- IReadOnlyList types, int[] left, MaxRectsSheet sheet, double s, FitRule rule,
- List 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 types, int[] left, MaxRectsSheet sheet, double s, FitRule rule,
- List 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));
- }
- }
- }
-
- /// Lower score wins; on a tie the larger box goes first (strict, so input order breaks full ties).
- 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)
{
var l = System.Math.Min(a.Left, b.Left);
diff --git a/OpenNest.Engine/RectanglePacking/MaxRectsPacker.cs b/OpenNest.Engine/RectanglePacking/MaxRectsPacker.cs
new file mode 100644
index 0000000..d3964be
--- /dev/null
+++ b/OpenNest.Engine/RectanglePacking/MaxRectsPacker.cs
@@ -0,0 +1,158 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+
+namespace OpenNest.Engine.RectanglePacking;
+
+/// How the next box is chosen on a sheet.
+internal enum PickMode
+{
+ /// Every step places whichever remaining type/size scores best anywhere.
+ Global,
+
+ /// Types in (priority, largest box first) order; each fills until it no longer fits.
+ Ordered,
+}
+
+/// A box type to pack.
+/// Demand tier; lower numbers are served first.
+/// Allowed packing sizes (one per orientation), already grown by any spacing.
+/// Ranking area: larger boxes win score ties and go first in ordered mode.
+internal sealed record PackType(int Priority, IReadOnlyList<(double Width, double Height)> Sizes, double Area);
+
+/// One placed box: type and size index, and its corner in sheet-local coordinates.
+internal readonly record struct PackPlacement(int Type, int Size, double X, double Y);
+
+///
+/// Packs box types onto one 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.
+///
+internal static class MaxRectsPacker
+{
+ /// Remaining count per type, indexed like ; decremented as boxes are placed.
+ public static List Pack(
+ IReadOnlyList types,
+ int[] left,
+ MaxRectsSheet sheet,
+ FitRule rule,
+ PickMode mode,
+ CancellationToken token
+ )
+ {
+ var placed = new List();
+ 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 types,
+ int[] left,
+ MaxRectsSheet sheet,
+ FitRule rule,
+ List 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 types,
+ int[] left,
+ MaxRectsSheet sheet,
+ FitRule rule,
+ List 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));
+ }
+ }
+ }
+
+ /// Lower score wins; on a tie the larger box goes first (strict, so input order breaks full ties).
+ 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;
+ }
+}