feat(engine): break equal-cost ties toward the larger free offcut

Without salvage credit (desktop Auto Nest on a single plate size) every
complete one-sheet layout costs the same, so engines kept whichever layout
came first. N26-23S nested 27.86" long when turning two parts fits it in
21.3".

NestJobCost.CompactnessOffcut measures the largest full-width/full-length
edge offcut as salvage does, whatever the salvage rate. Default and Circles
(CandidateScore), Rectangles and Circles sheet choice (SheetByStock) and
Irregular sheet and plan ranking use it only after cost, so no cost changes.
Sheet-level keys round to 1e-6 to keep ties platform-independent.

Rectangles layout digests are re-captured: each changed fixture keeps its
sheets, parts and cost and leaves a larger offcut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-24 20:18:57 -04:00
1 parent ad3443f60d
commit 33f60d3dcb
10 files changed
+201 -33

No files matched your search

@@ -25,6 +25,9 @@ public class DefaultNestingEngineTests
private static (double X, double Y)[] Sheet(params (double X, double Y)[] poses) => poses;
private static NestJobResult OneSheet(NestJob job) => Layout(job, Sheet((0, 0), (5, 0)));
// Same 10 x 6 free strip as OneSheet, below the parts instead of above them.
private static NestJobResult OneSheetAtTop(NestJob job) => Layout(job, Sheet((0, 6), (5, 6)));
// Only a 10 x 5 strip left below the parts.
private static NestJobResult OneSheetHigher(NestJob job) => Layout(job, Sheet((0, 5), (5, 5)));
private static NestJobResult TwoSheets(NestJob job) => Layout(job, Sheet((0, 0)), Sheet((0, 0)));
private static NestJobResult Overlapping(NestJob job) => Layout(job, Sheet((0, 0), (1, 0)));
@@ -82,12 +85,24 @@ public class DefaultNestingEngineTests
{
var job = SquaresJob();
var low = OneSheet(job);
var high = OneSheetHigher(job);
var high = OneSheetAtTop(job);
Assert.Same(low, Choose(job, Returns(_ => low), Returns(_ => high)));
Assert.Same(high, Choose(job, Returns(_ => high), Returns(_ => low)));
}
[Fact]
public void LargerOffcutWinsAtEqualCostInEitherOrder()
{
var job = SquaresJob();
var compact = OneSheet(job);
var loose = OneSheetHigher(job);
Assert.Equal(NestJobCost.Evaluate(job, compact), NestJobCost.Evaluate(job, loose));
Assert.Same(compact, Choose(job, Returns(_ => loose), Returns(_ => compact)));
Assert.Same(compact, Choose(job, Returns(_ => compact), Returns(_ => loose)));
}
[Fact]
public void ACrashingCandidateIsSkipped()
{