feat(fill): pack leftover parts with maximal rectangles
DefaultPlateFiller.PackArea (Default, Strip and both Remnant strategies) now packs leftover parts with the shared maximal-rectangles packer instead of bottom-left corner points. Every fit rule and pick mode is tried; the layout placing the most parts, then box area, per priority tier wins, and the strategy's own fill comparer breaks ties so remnant strategies keep their clear side. PackBottomLeft and PackEngine are removed. Rectangle-lane benchmark (91 strict + 77 boxable jobs) against the old packer: no strategy lost a valid layout; Default, Vertical Remnant, Horizontal Remnant and Strip complete 2-3 more strict jobs, and Strip gains a valid one. Cost on jobs complete in both runs falls for every strategy (Default -0.6% strict, -2.7% boxable). Default is about 7-10% slower on the changed jobs. Golden layouts for Default and both Remnant strategies are re-captured; each is complete, passes NestLayoutCheck and repeats exactly.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
using System.Globalization;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
using Xunit;
|
||||
using OpenNest.Engine.Jobs;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace OpenNest.Engine.Tests.Jobs;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Captured on Linux/.NET 8 at commit 42bbde7 (post ShrinkFiller axis fix), verified
|
||||
/// identical across 30 repeat runs per strategy. The Strip strategy is only pinned on the
|
||||
/// identical across 30 repeat runs per strategy. The Default and remnant mixed-job layouts were
|
||||
/// re-captured when leftover packing moved to the maximal-rectangles packer (every pose passes
|
||||
/// NestLayoutCheck). The Strip strategy is only pinned on the
|
||||
/// rectangle-variety job: on dense mixed-shape jobs the iterative shrink path intermittently
|
||||
/// proposes overlapping candidates (pre-existing scheduling nondeterminism, not a regression),
|
||||
/// so its mixed-geometry layout is deliberately not pinned here.
|
||||
@@ -169,12 +171,12 @@ public class GoldenLayoutTests
|
||||
AssertGolden(
|
||||
result,
|
||||
[
|
||||
P("arc", 0, 15, 1, 0),
|
||||
P("arc", 1, 15, 7, 0),
|
||||
P("arc", 2, 15, 13, 0),
|
||||
P("lshape", 0, 15, 19, 0),
|
||||
P("lshape", 1, 15, 24, 0),
|
||||
P("lshape", 2, 21.5, 1, 0),
|
||||
P("arc", 0, 20, 1, 1.5707963267948966),
|
||||
P("arc", 1, 20, 7.5, 1.5707963267948966),
|
||||
P("arc", 2, 20, 14, 1.5707963267948966),
|
||||
P("lshape", 0, 19, 20.5, 1.5707963267948966),
|
||||
P("lshape", 1, 24, 20.5, 1.5707963267948966),
|
||||
P("lshape", 2, 25, 1, 1.5707963267948966),
|
||||
P("rect-a", 0, 5, 21, 1.5707963267948966),
|
||||
P("rect-a", 1, 1, 1, 0),
|
||||
P("rect-a", 2, 1, 6, 0),
|
||||
@@ -183,12 +185,12 @@ public class GoldenLayoutTests
|
||||
P("rect-a", 5, 10, 21, 1.5707963267948966),
|
||||
P("rect-a", 6, 8, 1, 0),
|
||||
P("rect-a", 7, 8, 6, 0),
|
||||
P("rect-b", 0, 21.5, 6, 0),
|
||||
P("rect-b", 1, 21.5, 10, 0),
|
||||
P("rect-b", 2, 21.5, 14, 0),
|
||||
P("rect-b", 3, 22, 19, 0),
|
||||
P("rect-b", 4, 22, 23, 0),
|
||||
P("rect-b", 5, 26.5, 6, 0),
|
||||
P("rect-b", 0, 24, 8, 1.5707963267948966),
|
||||
P("rect-b", 1, 24, 13, 1.5707963267948966),
|
||||
P("rect-b", 2, 28, 8, 1.5707963267948966),
|
||||
P("rect-b", 3, 28, 13, 1.5707963267948966),
|
||||
P("rect-b", 4, 28, 18, 1.5707963267948966),
|
||||
P("rect-b", 5, 28, 23, 1.5707963267948966),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -206,12 +208,12 @@ public class GoldenLayoutTests
|
||||
AssertGolden(
|
||||
result,
|
||||
[
|
||||
P("arc", 0, 15, 1, 0),
|
||||
P("arc", 1, 15, 7, 0),
|
||||
P("arc", 2, 15, 13, 0),
|
||||
P("lshape", 0, 15, 19, 0),
|
||||
P("lshape", 1, 15, 24, 0),
|
||||
P("lshape", 2, 21.5, 1, 0),
|
||||
P("arc", 0, 20, 1, 1.5707963267948966),
|
||||
P("arc", 1, 20, 7.5, 1.5707963267948966),
|
||||
P("arc", 2, 20, 14, 1.5707963267948966),
|
||||
P("lshape", 0, 19, 20.5, 1.5707963267948966),
|
||||
P("lshape", 1, 24, 20.5, 1.5707963267948966),
|
||||
P("lshape", 2, 25, 1, 1.5707963267948966),
|
||||
P("rect-a", 0, 5, 21, 1.5707963267948966),
|
||||
P("rect-a", 1, 1, 1, 0),
|
||||
P("rect-a", 2, 1, 6, 0),
|
||||
@@ -220,12 +222,12 @@ public class GoldenLayoutTests
|
||||
P("rect-a", 5, 10, 21, 1.5707963267948966),
|
||||
P("rect-a", 6, 8, 1, 0),
|
||||
P("rect-a", 7, 8, 6, 0),
|
||||
P("rect-b", 0, 21.5, 6, 0),
|
||||
P("rect-b", 1, 21.5, 10, 0),
|
||||
P("rect-b", 2, 21.5, 14, 0),
|
||||
P("rect-b", 3, 22, 19, 0),
|
||||
P("rect-b", 4, 22, 23, 0),
|
||||
P("rect-b", 5, 26.5, 6, 0),
|
||||
P("rect-b", 0, 24, 8, 1.5707963267948966),
|
||||
P("rect-b", 1, 24, 13, 1.5707963267948966),
|
||||
P("rect-b", 2, 28, 8, 1.5707963267948966),
|
||||
P("rect-b", 3, 28, 13, 1.5707963267948966),
|
||||
P("rect-b", 4, 28, 18, 1.5707963267948966),
|
||||
P("rect-b", 5, 28, 23, 1.5707963267948966),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -241,12 +243,12 @@ public class GoldenLayoutTests
|
||||
AssertGolden(
|
||||
result,
|
||||
[
|
||||
P("arc", 0, 1, 23, 0),
|
||||
P("arc", 1, 7.5, 23, 0),
|
||||
P("arc", 2, 8, 8, 0),
|
||||
P("lshape", 0, 1, 8, 0),
|
||||
P("lshape", 1, 1, 13, 0),
|
||||
P("lshape", 2, 1, 18, 0),
|
||||
P("arc", 0, 1, 8, 0),
|
||||
P("arc", 1, 7.5, 8, 0),
|
||||
P("arc", 2, 14, 8, 0),
|
||||
P("lshape", 0, 20.5, 8, 0),
|
||||
P("lshape", 1, 27.5, 8, 0),
|
||||
P("lshape", 2, 34.5, 8, 0),
|
||||
P("rect-a", 0, 1, 1, 0),
|
||||
P("rect-a", 1, 8, 1, 0),
|
||||
P("rect-a", 2, 15, 1, 0),
|
||||
@@ -255,12 +257,12 @@ public class GoldenLayoutTests
|
||||
P("rect-a", 5, 36, 1, 1.5707963267948966),
|
||||
P("rect-a", 6, 41, 1, 1.5707963267948966),
|
||||
P("rect-a", 7, 46, 1, 1.5707963267948966),
|
||||
P("rect-b", 0, 8, 14, 0),
|
||||
P("rect-b", 1, 8, 18, 0),
|
||||
P("rect-b", 2, 13, 14, 0),
|
||||
P("rect-b", 3, 13, 18, 0),
|
||||
P("rect-b", 4, 14, 23, 0),
|
||||
P("rect-b", 5, 14.5, 8, 0),
|
||||
P("rect-b", 0, 41.5, 8, 0),
|
||||
P("rect-b", 1, 41.5, 12, 0),
|
||||
P("rect-b", 2, 20.5, 13, 0),
|
||||
P("rect-b", 3, 25.5, 13, 0),
|
||||
P("rect-b", 4, 30.5, 13, 0),
|
||||
P("rect-b", 5, 35.5, 13, 0),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Jobs.Placement;
|
||||
using OpenNest.Engine.RectanglePacking;
|
||||
using OpenNest.Engine.Tests.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Tests.RectanglePacking;
|
||||
|
||||
public class AreaPackerTests
|
||||
{
|
||||
private const double Eps = 1e-9;
|
||||
|
||||
private static NestItem Item(string name, double width, double length, int quantity, int priority = 0) =>
|
||||
new()
|
||||
{
|
||||
Drawing = new Drawing(name, TestDrawingFactory.Rectangle(width, length)),
|
||||
Quantity = quantity,
|
||||
Priority = priority,
|
||||
};
|
||||
|
||||
private static void AssertInsideAndSpaced(IReadOnlyList<Part> parts, Box area, double spacing)
|
||||
{
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var b = part.BoundingBox;
|
||||
Assert.True(
|
||||
b.Left >= area.Left - Eps && b.Bottom >= area.Bottom - Eps
|
||||
&& b.Right <= area.Right + Eps && b.Top <= area.Top + Eps,
|
||||
$"{part.BaseDrawing.Name} at ({b.Left},{b.Bottom})-({b.Right},{b.Top}) is outside the area"
|
||||
);
|
||||
}
|
||||
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
for (var j = i + 1; j < parts.Count; j++)
|
||||
{
|
||||
var a = parts[i].BoundingBox;
|
||||
var b = parts[j].BoundingBox;
|
||||
var gapX = System.Math.Max(b.Left - a.Right, a.Left - b.Right);
|
||||
var gapY = System.Math.Max(b.Bottom - a.Top, a.Bottom - b.Top);
|
||||
Assert.True(
|
||||
System.Math.Max(gapX, gapY) >= spacing - Eps,
|
||||
$"parts {i} and {j} are {System.Math.Max(gapX, gapY)} apart, spacing is {spacing}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultPackArea_PlacesDemandThatCornerPointPackingMissed()
|
||||
{
|
||||
// Three 5x7 and two 4x3 boxes fit a 12x12 area exactly when packed by free space;
|
||||
// packing only at placed parts' corners left one 4x3 out.
|
||||
var plate = new Plate(new Size(12, 12)) { PartSpacing = 0 };
|
||||
var area = new Box(0, 0, 12, 12);
|
||||
var items = new List<NestItem> { Item("tall", 5, 7, 3), Item("small", 4, 3, 2) };
|
||||
|
||||
var parts = PlateFillService.PackArea("Default", plate, area, items, null, CancellationToken.None);
|
||||
|
||||
Assert.Equal(5, parts.Count);
|
||||
Assert.Equal(3, parts.Count(p => p.BaseDrawing.Name == "tall"));
|
||||
Assert.Equal(2, parts.Count(p => p.BaseDrawing.Name == "small"));
|
||||
AssertInsideAndSpaced(parts, area, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_KeepsSpacingInsideTranslatedArea()
|
||||
{
|
||||
var area = new Box(10, 20, 30, 20);
|
||||
var items = new List<NestItem> { Item("a", 6, 4, 5), Item("b", 4, 3, 4) };
|
||||
|
||||
var parts = AreaPacker.Pack(area, items, 1, new DefaultFillComparer(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(9, parts.Count);
|
||||
AssertInsideAndSpaced(parts, area, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_TurnsPartThatOnlyFitsRotated()
|
||||
{
|
||||
// 20 long in X, but the area is only 10 wide in X and 30 tall.
|
||||
var area = new Box(0, 0, 10, 30);
|
||||
var items = new List<NestItem> { Item("long", 20, 5, 1) };
|
||||
|
||||
var parts = AreaPacker.Pack(area, items, 0, new DefaultFillComparer(), CancellationToken.None);
|
||||
|
||||
var part = Assert.Single(parts);
|
||||
Assert.Equal(System.Math.PI / 2, part.Rotation, 9);
|
||||
AssertInsideAndSpaced(parts, area, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_ServesLowerPriorityNumberFirst()
|
||||
{
|
||||
var area = new Box(0, 0, 10, 10);
|
||||
var items = new List<NestItem> { Item("later", 10, 10, 1, priority: 1), Item("first", 6, 6, 1, priority: 0) };
|
||||
|
||||
var parts = AreaPacker.Pack(area, items, 0, new DefaultFillComparer(), CancellationToken.None);
|
||||
|
||||
Assert.Equal("first", Assert.Single(parts).BaseDrawing.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_NeverExceedsQuantityAndSkipsEmptyDrawings()
|
||||
{
|
||||
var area = new Box(0, 0, 100, 100);
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
Item("two", 3, 3, 2),
|
||||
new() { Drawing = new Drawing("empty", new OpenNest.CNC.Program()), Quantity = 4 },
|
||||
};
|
||||
|
||||
var parts = AreaPacker.Pack(area, items, 0.5, new DefaultFillComparer(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, parts.Count);
|
||||
Assert.All(parts, p => Assert.Equal("two", p.BaseDrawing.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_CancelledTokenStillReturnsFirstValidLayout()
|
||||
{
|
||||
var area = new Box(0, 0, 30, 20);
|
||||
var items = new List<NestItem> { Item("a", 6, 4, 5), Item("b", 4, 3, 4) };
|
||||
|
||||
var parts = AreaPacker.Pack(area, items, 1, new DefaultFillComparer(), new CancellationToken(canceled: true));
|
||||
|
||||
Assert.NotEmpty(parts);
|
||||
AssertInsideAndSpaced(parts, area, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pack_LetsStrategyComparerChooseBetweenEqualLayouts()
|
||||
{
|
||||
// Two 60x2 strips fit stacked or end to end. Both place everything, so the strategy decides:
|
||||
// Vertical Remnant keeps the right side clear, Horizontal Remnant keeps the top clear.
|
||||
var area = new Box(0, 0, 130, 30);
|
||||
var items = new List<NestItem> { Item("a", 60, 2, 1), Item("b", 60, 2, 1) };
|
||||
|
||||
var vertical = AreaPacker.Pack(area, items, 0, new VerticalRemnantComparer(), CancellationToken.None);
|
||||
var horizontal = AreaPacker.Pack(area, items, 0, new HorizontalRemnantComparer(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, vertical.Count);
|
||||
Assert.Equal(60, vertical.Max(p => p.BoundingBox.Right) - vertical.Min(p => p.BoundingBox.Left), 9);
|
||||
Assert.Equal(2, horizontal.Count);
|
||||
Assert.Equal(2, horizontal.Max(p => p.BoundingBox.Top) - horizontal.Min(p => p.BoundingBox.Bottom), 9);
|
||||
}
|
||||
}
|
||||
@@ -399,13 +399,7 @@ internal class DefaultPlateFiller : PlateFillerBase
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var binItems = BinConverter.ToItems(items, Plate.PartSpacing, Plate.Area());
|
||||
var bin = BinConverter.CreateBin(box, Plate.PartSpacing);
|
||||
|
||||
var engine = new PackBottomLeft(bin);
|
||||
engine.Pack(binItems);
|
||||
|
||||
return BinConverter.ToParts(bin, items);
|
||||
return AreaPacker.Pack(box, items, Plate.PartSpacing, Comparer, token);
|
||||
}
|
||||
|
||||
protected virtual void RunPipeline(FillContext context)
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.RectanglePacking;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="NestItem.Priority"/>; 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.
|
||||
/// </summary>
|
||||
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 };
|
||||
|
||||
/// <summary>
|
||||
/// Returns the packed parts. Items and the area grow by <paramref name="spacing"/> 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.
|
||||
/// </summary>
|
||||
public static List<Part> Pack(
|
||||
Box area, IReadOnlyList<NestItem> items, double spacing, IFillComparer comparer, CancellationToken token)
|
||||
{
|
||||
var boxes = items.Select(i => i.Drawing.Program.BoundingBox()).ToList();
|
||||
var types = new List<PackType>(items.Count);
|
||||
var demand = new int[items.Count];
|
||||
var packArea = (area.Length + spacing) * (area.Width + spacing);
|
||||
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<Part>? 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);
|
||||
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!;
|
||||
}
|
||||
|
||||
/// <summary>Positive when <paramref name="a"/> serves the most urgent tier better, negative when worse, 0 on a tie.</summary>
|
||||
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<Part> ToParts(
|
||||
List<PackPlacement> placed, IReadOnlyList<NestItem> items, IReadOnlyList<Box> boxes, Box area)
|
||||
{
|
||||
var parts = new List<Part>(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;
|
||||
}
|
||||
}
|
||||
@@ -31,25 +31,6 @@ namespace OpenNest.Engine.RectanglePacking
|
||||
};
|
||||
}
|
||||
|
||||
public static List<Item> ToItems(List<NestItem> items, double partSpacing, double plateArea)
|
||||
{
|
||||
var binItems = new List<Item>();
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
var binItem = ToItem(item, partSpacing, i);
|
||||
|
||||
var maxQty = (int)System.Math.Floor(plateArea / binItem.Area());
|
||||
var qty = item.Quantity < maxQty ? item.Quantity : maxQty;
|
||||
|
||||
for (var j = 0; j < qty; j++)
|
||||
binItems.Add(binItem.Clone() as Item);
|
||||
}
|
||||
|
||||
return binItems;
|
||||
}
|
||||
|
||||
public static List<Part> ToParts(Bin bin, List<NestItem> items)
|
||||
{
|
||||
var parts = new List<Part>();
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.RectanglePacking
|
||||
{
|
||||
internal class PackBottomLeft : PackEngine
|
||||
{
|
||||
public PackBottomLeft(Bin bin)
|
||||
: base(bin) { }
|
||||
|
||||
public override void Pack(List<Item> items)
|
||||
{
|
||||
var byArea = items
|
||||
.Select(i => i.Clone() as Item)
|
||||
.OrderByDescending(i => i.Area())
|
||||
.ToList();
|
||||
var byLength = items
|
||||
.Select(i => i.Clone() as Item)
|
||||
.OrderByDescending(i => System.Math.Max(i.Width, i.Length))
|
||||
.ToList();
|
||||
|
||||
var resultA = PackWithOrder(byArea);
|
||||
var resultB = PackWithOrder(byLength);
|
||||
|
||||
var winner = PickWinner(resultA, resultB);
|
||||
|
||||
Bin.Items.AddRange(winner);
|
||||
}
|
||||
|
||||
private List<Item> PackWithOrder(List<Item> items)
|
||||
{
|
||||
var points = new List<Vector> { Bin.Location };
|
||||
var placed = new List<Item>();
|
||||
var skip = new List<int>();
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
|
||||
if (skip.Contains(item.Id))
|
||||
continue;
|
||||
|
||||
var pt = FindPointVertical(item, points, placed);
|
||||
|
||||
// If it doesn't fit, try rotated.
|
||||
if (pt == null)
|
||||
{
|
||||
item.Rotate();
|
||||
pt = FindPointVertical(item, points, placed);
|
||||
}
|
||||
|
||||
if (pt == null)
|
||||
{
|
||||
if (item.IsRotated)
|
||||
item.Rotate();
|
||||
skip.Add(item.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
item.Location = pt.Value;
|
||||
|
||||
points.Remove(pt.Value);
|
||||
points.Add(new Vector(item.Left, item.Top));
|
||||
points.Add(new Vector(item.Right, item.Bottom));
|
||||
|
||||
placed.Add(item);
|
||||
}
|
||||
|
||||
return placed;
|
||||
}
|
||||
|
||||
private static List<Item> PickWinner(List<Item> a, List<Item> b)
|
||||
{
|
||||
if (a.Count != b.Count)
|
||||
return a.Count > b.Count ? a : b;
|
||||
|
||||
if (a.Count == 0)
|
||||
return a;
|
||||
|
||||
var areaA = a.GetBoundingBox().Area();
|
||||
var areaB = b.GetBoundingBox().Area();
|
||||
|
||||
return areaB < areaA ? b : a;
|
||||
}
|
||||
|
||||
private Vector? FindPointVertical(Item item, List<Vector> points, List<Item> placed)
|
||||
{
|
||||
var pt = new Vector(double.MaxValue, double.MaxValue);
|
||||
|
||||
for (var i = 0; i < points.Count; i++)
|
||||
{
|
||||
var point = points[i];
|
||||
|
||||
item.Location = point;
|
||||
|
||||
if (!IsValid(item, placed))
|
||||
continue;
|
||||
|
||||
if (point.X < pt.X)
|
||||
pt = point;
|
||||
else if (point.X.IsEqualTo(pt.X) && point.Y < pt.Y)
|
||||
pt = point;
|
||||
}
|
||||
|
||||
if (pt.X != double.MaxValue && pt.Y != double.MaxValue)
|
||||
return pt;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsValid(Item item, List<Item> placed)
|
||||
{
|
||||
if (!Bin.Contains(item))
|
||||
return false;
|
||||
|
||||
foreach (var it in placed)
|
||||
{
|
||||
if (item.Intersects(it))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.RectanglePacking
|
||||
{
|
||||
internal abstract class PackEngine
|
||||
{
|
||||
public PackEngine(Bin bin)
|
||||
{
|
||||
Bin = bin;
|
||||
}
|
||||
|
||||
public Bin Bin { get; set; }
|
||||
|
||||
public abstract void Pack(List<Item> items);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user