Files
OpenNest/OpenNest.Engine/NestingEngines/Rectangles/RectanglesNestingEngine.cs
T
ajandClaude Opus 5.5 9273a44258 feat(engine): squeeze the Rectangles sheet that finishes the job
Auto Nest left N26-2A5 (14 door stops, 14 lock plates on a clamped 26x26
sheet) 8.0 high, while the shop's hand layout holds all 28 under 7.5 and
leaves a half-inch-deeper drop. Every MaxRects fit rule packs into the
whole sheet, so with room to spare it spreads the parts out and never
looks for the shortest strip.

When a rule places all the remaining demand, Rectangles now repacks it
into shorter and narrower bins, every rule and mode per probe, and offers
the tightest complete layout per axis to the stock loop, which still
picks by cost and offcut. Fit is not monotonic in bin size (7.5 packs,
7.62 and 7.74 do not), so probes walk down through stack heights (sums of
spaced part sides from the bin edge or a reached clamp) instead of
bisecting. A placement budget skips sheets finishing ~2000+ parts.

Benchmark, Rectangles on 80 recent production nests with salvage credit:
11 cheaper, 0 worse, all valid, total time 14.7 s -> 13.2 s.

The zero-clamp hash is re-captured (the squeezed layout is narrower);
the far-clamp test now compares poses, since the clamp can reorder an
identical layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 11:10:06 -05:00

101 lines
4.7 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.RectanglePacking;
namespace OpenNest.Engine.NestingEngines.Rectangles;
/// <summary>
/// Rectangle-lane nesting engine: every part is nested as the axis-aligned box of its material at
/// its minimum-area rotations, packed with a maximal-rectangles free list (Jylänki 2010).
///
/// Built for jobs of plain and near-rectangular parts, where a part's box wastes almost nothing
/// and exact box packing beats contour-sliding engines on both speed and density. Irregular parts
/// are still placed validly, only as their bounding boxes; they are not nested into each other.
///
/// Sheet by sheet (<see cref="SheetByStock"/>), each available stock is packed under several
/// free-space scoring rules and two pick modes (best-fitting box anywhere, or largest type first),
/// and the candidate with the lowest estimated whole-job cost wins. The finished job is reported as
/// its one whole-job plan (<see cref="NestJobStage.PlanImproved"/>) before its plate commits.
/// Deterministic: no clocks or randomness; the only stop besides completion is the host token.
/// </summary>
public sealed class RectanglesNestingEngine : INestingEngine
{
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 };
// Instance-owned diagnostics; no process-global counters or mutable solver state.
internal Action<PickMode>? SheetPacked { get; init; }
internal Action<RepeatBatch>? BatchSelected { get; init; }
public NestJobResult Solve(
NestJob job,
IProgress<NestJobProgress>? progress = null,
CancellationToken token = default
)
{
ArgumentNullException.ThrowIfNull(job);
token.ThrowIfCancellationRequested();
var types = BoxCatalog.Build(job);
var remaining = types.Select(t => t.Part.Quantity).ToArray();
// Parts with unreadable geometry or no box that fits any offered sheet can never be placed.
foreach (var t in types)
if (t.Orientations.Count == 0 || !job.Plates.Any(stock => t.Orientations.Any(o => FitsStock(stock, o))))
remaining[t.Index] = 0;
var demand = types.Select(t => new SheetDemand(t.Index, t.Id, t.Priority, t.MaterialArea)).ToArray();
// One run makes one whole-job plan: report it, then its commits, as the other engines do.
var forward = progress == null ? null : new CandidateProgress(progress);
var result = SheetByStock.Run(job, demand, remaining, Pack, forward, token, BatchSelected);
CandidateProgress.ReportPlan(result, progress);
CandidateProgress.ReportCommits(result, progress, token);
return result;
IEnumerable<SheetLayout> Pack(NestPlateStock stock, IReadOnlyList<int> left)
{
// Both private MaxRects modes return only after their normal demand/no-fit search;
// neither returns partial/budget-limited output. This is not a geometric impossibility proof.
var plans = PackAll(stock, left, null, null);
foreach (var plan in plans)
yield return Layout(plan);
// A sheet that finishes the job is also tried squeezed into a shorter or narrower bin.
foreach (var plan in SheetSqueeze.Run(types, left, stock, plans,
(width, height) => PackAll(stock, left, width, height), token))
yield return Layout(plan);
}
IReadOnlyList<SheetPlan> PackAll(NestPlateStock stock, IReadOnlyList<int> left, double? width, double? height)
{
var plans = new List<SheetPlan>(Modes.Length * Rules.Length);
foreach (var mode in Modes)
foreach (var rule in Rules)
{
plans.Add(SheetPacker.Pack(types, left, stock, rule, mode, token, width, height));
SheetPacked?.Invoke(mode);
}
return plans;
}
static SheetLayout Layout(SheetPlan plan) =>
new(plan.Stock, plan.Poses().ToArray(), plan.MaterialArea, plan.Envelope);
}
private static bool FitsStock(NestPlateStock stock, BoxOrientation o)
{
if (stock.Outline != null)
return false; // Boxes pack rectangles only.
var work = stock.WorkArea;
return o.Width <= work.Right - work.Left + SheetPacker.OverhangAllowance
&& o.Height <= work.Top - work.Bottom + SheetPacker.OverhangAllowance;
}
}