From c6f8a06dc7676b76888b74f137f74e1562514dbb Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 30 Sep 2026 22:07:46 -0400 Subject: [PATCH] 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). --- .../NestingEngines/Rectangles/SheetPacker.cs | 111 +----------- .../RectanglePacking/MaxRectsPacker.cs | 158 ++++++++++++++++++ 2 files changed, 166 insertions(+), 103 deletions(-) create mode 100644 OpenNest.Engine/RectanglePacking/MaxRectsPacker.cs 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; + } +}