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