fix(packing): apply work-area slack only at plate boundaries

This commit is contained in:
aj
2026-10-01 08:15:57 -04:00
parent a26e663f01
commit 6b6928213d
5 changed files with 159 additions and 7 deletions
+12 -3
View File
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.NestingEngines.Rectangles;
using OpenNest.Geometry;
using OpenNest.Math;
@@ -33,13 +34,21 @@ internal static class AreaPacker
/// 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>
/// <param name="workArea">Plate work area, when known. Only matching right/top boundaries
/// receive the existing work-area overhang allowance; internal areas remain strict.</param>
public static List<Part> Pack(
Box area, IReadOnlyList<NestItem> items, double spacing, IFillComparer comparer, CancellationToken token)
Box area, IReadOnlyList<NestItem> items, double spacing, IFillComparer comparer, CancellationToken token, Box? workArea = null)
{
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);
// Only the positive edges can overhang: placements anchor at a free box's lower-left.
var rightSlack = workArea != null && System.Math.Abs(area.Right - workArea.Right) <= MaxRectsSheet.Eps
? SheetPacker.OverhangAllowance : 0;
var topSlack = workArea != null && System.Math.Abs(area.Top - workArea.Top) <= MaxRectsSheet.Eps
? SheetPacker.OverhangAllowance : 0;
// Include boundary slack in this upper bound so a near-full-area item is not capped at zero.
var packArea = (area.Length + spacing + rightSlack) * (area.Width + spacing + topSlack);
for (var i = 0; i < items.Count; i++)
{
var w = boxes[i].Length + spacing;
@@ -67,7 +76,7 @@ internal static class AreaPacker
if (best != null && token.IsCancellationRequested)
return best;
var sheet = new MaxRectsSheet(area.Length + spacing, area.Width + spacing);
var sheet = new MaxRectsSheet(area.Length + spacing, area.Width + spacing, rightSlack, topSlack);
var placed = MaxRectsPacker.Pack(
types, (int[])demand.Clone(), sheet, rule, mode, CancellationToken.None);
var tierScores = tiers
@@ -50,9 +50,13 @@ internal sealed class MaxRectsSheet
private readonly List<Rect> free = new();
private readonly List<Rect> used = new();
private readonly double rightSlack;
private readonly double topSlack;
public MaxRectsSheet(double width, double height)
public MaxRectsSheet(double width, double height, double rightSlack = 0, double topSlack = 0)
{
this.rightSlack = rightSlack;
this.topSlack = topSlack;
Width = width;
Height = height;
free.Add(new Rect(0, 0, width, height));
@@ -68,9 +72,15 @@ internal sealed class MaxRectsSheet
(Rect Place, double Primary, double Secondary)? best = null;
foreach (var f in free)
{
if (w > f.W + Eps || h > f.H + Eps)
// Clamp only at the bin's allowed plate boundaries. Internal free edges still
// use Eps, and real sizes are retained there to preserve neighbour spacing.
var packW = w > f.W && w <= f.W + rightSlack && System.Math.Abs(f.Right - Width) <= Eps
? f.W : w;
var packH = h > f.H && h <= f.H + topSlack && System.Math.Abs(f.Top - Height) <= Eps
? f.H : h;
if (packW > f.W + Eps || packH > f.H + Eps)
continue;
var place = new Rect(f.X, f.Y, w, h);
var place = new Rect(f.X, f.Y, packW, packH);
var (p, s) = Score(f, place, rule);
if (best is not { } b || p < b.Primary - Eps
|| (p <= b.Primary + Eps && s < b.Secondary - Eps))