This repository has been archived on 2026-09-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
OpenNest-Engines/OpenNest.Engine.Rectangles/MaxRectsSheet.cs
T
aj 13478f5102 feat(rectangles): add rectangle-lane maximal-rectangles engine
Nests every part as the axis-aligned box of its material at its minimum-area
rotation and packs boxes with a maximal-rectangles free list (6 placement rules x
global/ordered pick). Sheet choice uses the salvage-credited look-ahead cost.

Curved extremes read the way the layout check sees them (circumscribed arcs on
the Clipper grid), so discs, rings and obrounds stay valid at box contact; an
80-job sweep failed 16-50 jobs before that rule.

Local 2026 production lanes (--min-salvage-dimension 12, --parallel 3):
- 91 all-rectangular jobs: 91/91 valid, cost 333081 vs best general 332153
  (Gpt6Astra, 89/91 valid), 5.9 s vs 13.7 s (Opus55) and 169.5 s (Gpt6Astra)
- 77 box-filling (>= 90%) jobs: 77/77 valid, lowest total cost 641637
2026-09-29 20:49:00 -04:00

166 lines
6.3 KiB
C#

namespace OpenNest.Engine.Rectangles;
/// <summary>Axis-aligned rectangle in sheet-local packing coordinates.</summary>
internal readonly record struct Rect(double X, double Y, double W, double H)
{
public double Right => X + W;
public double Top => Y + H;
public bool Contains(Rect other) =>
other.X >= X - MaxRectsSheet.Eps && other.Y >= Y - MaxRectsSheet.Eps
&& other.Right <= Right + MaxRectsSheet.Eps && other.Top <= Top + MaxRectsSheet.Eps;
public bool Overlaps(Rect other) =>
other.X < Right - MaxRectsSheet.Eps && other.Right > X + MaxRectsSheet.Eps
&& other.Y < Top - MaxRectsSheet.Eps && other.Top > Y + MaxRectsSheet.Eps;
}
/// <summary>How a free position is scored; lower (Primary, Secondary) wins.</summary>
internal enum FitRule
{
/// <summary>Smallest leftover on the tighter side of the free rectangle.</summary>
BestShortSide,
/// <summary>Smallest leftover on the looser side of the free rectangle.</summary>
BestLongSide,
/// <summary>Smallest free rectangle that holds the item.</summary>
BestArea,
/// <summary>Lowest top edge, then leftmost: packs rows upward and keeps a clean top offcut.</summary>
BottomLeft,
/// <summary>Leftmost right edge, then lowest: packs columns rightward and keeps a clean right offcut.</summary>
LeftBottom,
/// <summary>Most perimeter touching the sheet edge or already placed items.</summary>
ContactPoint,
}
/// <summary>
/// Maximal-rectangles free-space tracker for one sheet (Jylänki, "A Thousand Ways to Pack the
/// Bin", 2010). Keeps every maximal empty rectangle, so any position a box can legally occupy
/// is the bottom-left corner of some free rectangle. Items and the bin are inflated by the part
/// spacing on their right/top sides by the caller, so touching inflated boxes are exactly one
/// spacing apart and the last box may touch the sheet's work-area edge.
/// </summary>
internal sealed class MaxRectsSheet
{
public const double Eps = 1e-9;
private readonly List<Rect> free = new();
private readonly List<Rect> used = new();
public MaxRectsSheet(double width, double height)
{
Width = width;
Height = height;
free.Add(new Rect(0, 0, width, height));
}
public double Width { get; }
public double Height { get; }
public IReadOnlyList<Rect> Used => used;
/// <summary>Best position for a w-by-h item under the rule, or null when nothing holds it.</summary>
public (Rect Place, double Primary, double Secondary)? FindBest(double w, double h, FitRule rule)
{
(Rect Place, double Primary, double Secondary)? best = null;
foreach (var f in free)
{
if (w > f.W + Eps || h > f.H + Eps)
continue;
var place = new Rect(f.X, f.Y, w, h);
var (p, s) = Score(f, place, rule);
if (best is not { } b || p < b.Primary - Eps
|| (p <= b.Primary + Eps && s < b.Secondary - Eps))
best = (place, p, s);
}
return best;
}
/// <summary>Commits an item and splits every free rectangle it intersects.</summary>
public void Place(Rect item)
{
var next = new List<Rect>(free.Count + 8);
foreach (var f in free)
{
if (!f.Overlaps(item))
{
next.Add(f);
continue;
}
if (item.X > f.X + Eps)
next.Add(new Rect(f.X, f.Y, item.X - f.X, f.H));
if (item.Right < f.Right - Eps)
next.Add(new Rect(item.Right, f.Y, f.Right - item.Right, f.H));
if (item.Y > f.Y + Eps)
next.Add(new Rect(f.X, f.Y, f.W, item.Y - f.Y));
if (item.Top < f.Top - Eps)
next.Add(new Rect(f.X, item.Top, f.W, f.Top - item.Top));
}
free.Clear();
free.AddRange(Prune(next));
used.Add(item);
}
private static List<Rect> Prune(List<Rect> rects)
{
// Drop rectangles contained in another; of two equal ones keep the first (deterministic).
var keep = new bool[rects.Count];
for (var i = 0; i < rects.Count; i++)
keep[i] = rects[i].W > Eps && rects[i].H > Eps;
for (var i = 0; i < rects.Count; i++)
{
if (!keep[i])
continue;
for (var j = 0; j < rects.Count; j++)
{
if (i == j || !keep[j])
continue;
if (rects[j].Contains(rects[i]) && (!rects[i].Contains(rects[j]) || j < i))
{
keep[i] = false;
break;
}
}
}
var result = new List<Rect>(rects.Count);
for (var i = 0; i < rects.Count; i++)
if (keep[i])
result.Add(rects[i]);
return result;
}
private (double Primary, double Secondary) Score(Rect f, Rect place, FitRule rule)
{
var dx = f.W - place.W;
var dy = f.H - place.H;
return rule switch
{
FitRule.BestShortSide => (System.Math.Min(dx, dy), System.Math.Max(dx, dy)),
FitRule.BestLongSide => (System.Math.Max(dx, dy), System.Math.Min(dx, dy)),
FitRule.BestArea => (f.W * f.H - place.W * place.H, System.Math.Min(dx, dy)),
FitRule.BottomLeft => (place.Top, place.X),
FitRule.LeftBottom => (place.Right, place.Y),
FitRule.ContactPoint => (-Contact(place), place.Top + place.Right),
_ => throw new ArgumentOutOfRangeException(nameof(rule)),
};
}
private double Contact(Rect r)
{
var total = 0.0;
if (r.X <= Eps) total += r.H;
if (r.Right >= Width - Eps) total += r.H;
if (r.Y <= Eps) total += r.W;
if (r.Top >= Height - Eps) total += r.W;
foreach (var u in used)
{
if (System.Math.Abs(u.X - r.Right) <= Eps || System.Math.Abs(u.Right - r.X) <= Eps)
total += Overlap(u.Y, u.Top, r.Y, r.Top);
if (System.Math.Abs(u.Y - r.Top) <= Eps || System.Math.Abs(u.Top - r.Y) <= Eps)
total += Overlap(u.X, u.Right, r.X, r.Right);
}
return total;
}
private static double Overlap(double a0, double a1, double b0, double b1) =>
System.Math.Max(0, System.Math.Min(a1, b1) - System.Math.Max(a0, b0));
}