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

@@ -40,6 +40,28 @@ public class NestJobCostTests
#pragma warning restore CS0618
}
[Theory]
[InlineData(0, 0, 247.5)]
[InlineData(0.5, 0, 247.5)]
[InlineData(0, 13.75, 247.5)]
[InlineData(0, 14, 0)]
public void CompactnessOffcutIgnoresSalvageRateButKeepsTheMinimum(double rate, double minimum, double expected)
{
var stock = new NestPlateStock("sheet", new Size(20, 20), partSpacing: 0.25,
edgeSpacing: new Spacing(1, 1, 1, 1));
var work = stock.WorkArea;
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(18, 4)), 1);
var job = new NestJob(new[] { part }, new[] { stock },
new NestJobOptions(salvageRate: rate, minimumSalvageDimension: minimum));
var sheet = new NestJobPlateResult(0, stock,
new[] { new NestJobPlacement("part", 0, work.Left, work.Bottom, 0) });
// The strip above the part: full work width by 18 - 4 - 0.25 spacing.
Assert.Equal(expected, NestJobCost.CompactnessOffcut(job, sheet));
Assert.Equal(expected, NestJobCost.CompactnessOffcut(job.Options, stock, new Box(work.Left, work.Bottom, 18, 4)));
Assert.Equal(0, NestJobCost.CompactnessOffcut(job, new NestJobPlateResult(0, stock, Array.Empty<NestJobPlacement>())));
}
[Theory]
[InlineData(0, 5, 100)]
[InlineData(0.5, 0, 100)]
@@ -0,0 +1,42 @@
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Tests.Jobs;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests.NestingEngines;
/// <summary>
/// Without salvage credit (desktop Auto Nest on one plate size) every complete one-sheet layout
/// costs the same, so engines must break the tie toward the larger free offcut. Shaped after
/// N26-23S: four 7.083 x 22.577 and four 6.236 x 18.688 boxes on a clamped 60 x 120 sheet,
/// which once nested 27.86" long when turning two of the smaller boxes fits it in 21.3".
/// </summary>
public class CompactTieBreakTests
{
[Theory]
[InlineData("Rectangles")]
[InlineData("Irregular")]
[InlineData("Default")]
public void EqualCostOneSheetLayoutsPreferTheLargestOffcut(string engine)
{
var template = new Plate(60, 120)
{
PartSpacing = 0.3,
EdgeSpacing = new Spacing(0.3, 0.75, 0.3, 0.25),
};
template.SetClampRail(new ClampRail(1.5, 0.625, new double[] { 20, 40, 60, 80, 100, 120, 140 }));
var items = new[]
{
new NestItem { Drawing = new Drawing("leg-bottom", TestDrawingFactory.Rectangle(7.083, 22.577)), Quantity = 4 },
new NestItem { Drawing = new Drawing("leg-top", TestDrawingFactory.Rectangle(6.236, 18.688)), Quantity = 4 },
};
var result = NestPipeline.Run(new NestPipelineRequest(engine, items,
NestStockBuilder.FromTemplate(template, null),
new NestJobOptions(maxPlates: 100, salvageRate: 0, minimumSalvageDimension: 0)));
Assert.True(result.IsValid, string.Join("; ", result.Violations));
var parts = Assert.Single(result.Plates).Parts;
Assert.Equal(8, parts.Count);
Assert.True(parts.Max(p => p.BoundingBox.Right) < 21.5, $"used length {parts.Max(p => p.BoundingBox.Right):0.###}");
}
}
@@ -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()
{
@@ -35,17 +35,19 @@ public class RectanglesClampZoneTests
}
[Theory]
// Pre-clamp baseline hashes, captured from the unchanged packer at commit a3f1124.
[InlineData("1FA61E933D2D0D6F1E3336FCEBBB21CEB7E7D1533FDB253F9D48E947217575CE")]
// Pre-clamp baseline hashes, captured from the unchanged packer at commit a3f1124 and
// re-captured when equal-cost sheet layouts began preferring the larger free offcut.
[InlineData("3D389D6B993B69634EB45FC2CB0309273DE1BCF57A7C2BD980C63B535667B0C6")]
public void ZeroClampLayoutIsBitIdenticalToBaseline(string hash) =>
Assert.Equal(hash, LayoutHash(Engine().Solve(Job(
new[] { Part("rect", Rectangle(10, 5), 12) },
new[] { Stock("sheet", 48, 96, spacing: 0.25) }))));
[Theory]
// Two complete candidates tie here on cost and placed material; the tie must fall to
// enumeration order on every platform (hash identical on Windows and Linux).
[InlineData("BEEEDEC81A7F987720FB5F331873103AD2470C839D568CE587F230BC39D210CC")]
// Complete candidates tie here on cost and placed material; the tie must fall to the
// compactness offcut, then enumeration order, the same way on every platform (hash
// identical on Windows and Linux).
[InlineData("9B343B6C5CF2CE8753F2E04ACB0A9C4D5921F4ADC15AA2586AF466769C0F019B")]
public void ZeroClampMixedShapeLayoutIsBitIdenticalToBaseline(string hash) =>
Assert.Equal(hash, LayoutHash(Engine().Solve(Job(
new[]
@@ -57,7 +59,7 @@ public class RectanglesClampZoneTests
new[] { Stock("sheet", 40, 60, spacing: 0.5, edge: new Spacing(0.5, 0.5, 0.5, 0.5)) }))));
[Theory]
[InlineData("DBF93C1684D85B5EE7EB24C96A84904D2BA0F9A8183631EFC61638CB8B9B935A")]
[InlineData("F848E4C6213888302A9EBAC2B9839879FB1A4B6D919ACB8932FA1639FF3508A9")]
public void ZeroClampMultiSheetLayoutIsBitIdenticalToBaseline(string hash) =>
Assert.Equal(hash, LayoutHash(Engine().Solve(Job(
new[] { Part("square", Rectangle(10, 10), 30) },
@@ -280,6 +282,7 @@ public class RectanglesClampZoneTests
[Theory]
// Captured from the unchanged packer at ab50900 (see progress task-6.2 evidence):
// non-inverted clamp spans must keep today's blocks and layouts bit-for-bit.
// KEEP_LARGE_SPACING was re-captured when equal-cost layouts began preferring the larger offcut.
[InlineData("KEEP_LARGE_SPACING")]
[InlineData("KEEP_BETWEEN_CLAMPS")]
[InlineData("KEEP_BETWEEN_SPACED")]
@@ -288,7 +291,7 @@ public class RectanglesClampZoneTests
{
var hash = fixture switch
{
"KEEP_LARGE_SPACING" => "F7E4691C085FFB2164CEFFFB9B082B5F271E56E8B8B3D2AE23ECF5646FD40450",
"KEEP_LARGE_SPACING" => "3F390B18E99C6625E06A19295145F01EA85FEB7089DA99ED093E683AC71F7D02",
"KEEP_BETWEEN_CLAMPS" => "F09B878211EB7C49649647003410B20191F635E3CD8DED6E03BEA5BD63B17D61",
"KEEP_BETWEEN_SPACED" => "F09B878211EB7C49649647003410B20191F635E3CD8DED6E03BEA5BD63B17D61",
"KEEP_MID_RAIL_SMALL_SPACING" => "97868486E834CA0B1DB110FF717635E38537DB96DF5CDFBB2AFE37E4B1366B71",
+47 -4
View File
@@ -19,6 +19,45 @@ public static class NestJobCost
var minimum = job.Options.MinimumSalvageDimension;
if (job.Options.SalvageRate == 0 || minimum <= 0 || sheet.Placements.Count == 0)
return area;
var (left, bottom, right, top) = MaterialEnvelope(job, sheet);
return NetSheetArea(job.Options, sheet.Stock, left, bottom, right, top);
}
/// <summary>
/// Tie-break for layouts of equal cost: the largest full-width/full-length edge offcut, as
/// salvage measures it but whatever the salvage rate. Offcuts must meet a positive
/// MinimumSalvageDimension; with none, any offcut counts. Larger is more compact. An empty
/// sheet has none. Never part of the cost, so it cannot outrank a cheaper layout.
/// </summary>
public static double CompactnessOffcut(NestJob job, NestJobPlateResult sheet)
{
if (sheet.Placements.Count == 0)
return 0;
var (left, bottom, right, top) = MaterialEnvelope(job, sheet);
return LargestOffcut(sheet.Stock, left, bottom, right, top,
System.Math.Max(0, job.Options.MinimumSalvageDimension));
}
/// <summary><see cref="CompactnessOffcut(NestJob, NestJobPlateResult)"/> from an existing
/// material-only placed-parts envelope.</summary>
public static double CompactnessOffcut(NestJobOptions options, NestPlateStock stock, Box partsEnvelope) =>
LargestOffcut(stock, partsEnvelope.Left, partsEnvelope.Bottom, partsEnvelope.Right, partsEnvelope.Top,
System.Math.Max(0, options.MinimumSalvageDimension));
/// <summary>
/// An offcut on a 1e-6 grid, for exact tie-break comparisons: differences below it are
/// envelope rounding, which must not decide a tie differently across platform math libraries.
/// </summary>
public static double TieBreakKey(double offcut) => System.Math.Round(offcut, 6);
/// <summary>Whole-job <see cref="CompactnessOffcut(NestJob, NestJobPlateResult)"/>, summed over sheets.</summary>
public static double CompactnessOffcut(NestJob job, NestJobResult result) =>
RequireFinite(result.Plates.Sum(sheet => CompactnessOffcut(job, sheet)));
/// <summary>Bounds of the sheet's rotated material contours; scribe/etch marks are excluded.</summary>
private static (double Left, double Bottom, double Right, double Top) MaterialEnvelope(
NestJob job, NestJobPlateResult sheet)
{
var parts = job.Parts.ToDictionary(p => p.Id);
var boxes = sheet
.Placements.Select(p =>
@@ -30,8 +69,7 @@ public static class NestJobCost
return NestLayoutCheck.MaterialBounds(part);
})
.ToList();
return NetSheetArea(job.Options, sheet.Stock, boxes.Min(b => b.Left),
boxes.Min(b => b.Bottom), boxes.Max(b => b.Right), boxes.Max(b => b.Top));
return (boxes.Min(b => b.Left), boxes.Min(b => b.Bottom), boxes.Max(b => b.Right), boxes.Max(b => b.Top));
}
/// <summary>
@@ -50,6 +88,12 @@ public static class NestJobCost
var minimum = options.MinimumSalvageDimension;
if (options.SalvageRate == 0 || minimum <= 0)
return area;
return area - options.SalvageRate * LargestOffcut(stock, left, bottom, right, top, minimum);
}
private static double LargestOffcut(
NestPlateStock stock, double left, double bottom, double right, double top, double minimum)
{
var work = stock.WorkArea;
var gap = stock.PartSpacing;
var candidates = new[]
@@ -59,12 +103,11 @@ public static class NestJobCost
(left - work.Left - gap, work.Width),
(work.Right - right - gap, work.Width),
};
var salvage = candidates
return candidates
.Where(c => c.Item1 >= minimum && c.Item2 >= minimum)
.Select(c => c.Item1 * c.Item2)
.DefaultIfEmpty(0)
.Max();
return area - options.SalvageRate * salvage;
}
public static bool UsesExplicitCosts(NestJob job) =>
@@ -5,9 +5,11 @@ using OpenNest.Engine.Jobs;
namespace OpenNest.Engine.NestingEngines;
/// <summary>Ranks whole-job candidate results: a valid layout before an invalid one, then the
/// fewest unplaced parts, then the lowest <see cref="NestJobCost"/>; remaining ties keep
/// candidate order.</summary>
internal sealed record CandidateScore(NestJobResult Result, bool Valid, int Unplaced, double Cost, int Order, bool Priced)
/// fewest unplaced parts, then the lowest <see cref="NestJobCost"/>, then the largest
/// <see cref="NestJobCost.CompactnessOffcut(NestJob, NestJobResult)"/> (without salvage credit every
/// one-sheet layout costs the same); remaining ties keep candidate order.</summary>
internal sealed record CandidateScore(
NestJobResult Result, bool Valid, int Unplaced, double Cost, double Offcut, int Order, bool Priced)
{
public static CandidateScore Of(NestJob job, NestJobResult result, int order)
{
@@ -16,7 +18,8 @@ internal sealed record CandidateScore(NestJobResult Result, bool Valid, int Unpl
var unplaced = System.Math.Max(0, job.Parts.Sum(p => p.Quantity) - placed);
// Cost needs every placement to name a known part, which only a valid result guarantees.
var cost = valid ? NestJobCost.Evaluate(job, result) : double.PositiveInfinity;
return new CandidateScore(result, valid, unplaced, cost, order, NestJobCost.UsesExplicitCosts(job));
var offcut = valid ? NestJobCost.CompactnessOffcut(job, result) : 0;
return new CandidateScore(result, valid, unplaced, cost, offcut, order, NestJobCost.UsesExplicitCosts(job));
}
public bool IsBetterThan(CandidateScore other)
@@ -30,6 +33,9 @@ internal sealed record CandidateScore(NestJobResult Result, bool Valid, int Unpl
var scale = System.Math.Max(1, System.Math.Max(System.Math.Abs(Cost), System.Math.Abs(other.Cost)));
if (double.IsFinite(Cost) && double.IsFinite(other.Cost) && System.Math.Abs(Cost - other.Cost) > 1e-9 * scale)
return Cost < other.Cost;
var offcutScale = System.Math.Max(1, System.Math.Max(Offcut, other.Offcut));
if (System.Math.Abs(Offcut - other.Offcut) > 1e-9 * offcutScale)
return Offcut > other.Offcut;
return Order < other.Order;
}
}
@@ -121,7 +121,8 @@ public sealed class IrregularNestingEngine : INestingEngine
using var solver = new Solver(job, types, null, default, WorkBudget, DrawingFor, SheetPacked, BatchSelected, TailReplaced);
var unplaced = job.Parts.Sum(p => p.Quantity) - physicalPlan.Sum(s => s.Parts.Count);
var plan = new Plan(physicalPlan, physicalPlan.Sum(s => NetCost(job.Options, s)), unplaced,
unplaced == 0 ? NestJobStopReason.Completed : NestJobStopReason.NoPlacementFound, NestJobCost.UsesExplicitCosts(job));
unplaced == 0 ? NestJobStopReason.Completed : NestJobStopReason.NoPlacementFound, NestJobCost.UsesExplicitCosts(job),
job.Options);
return BuildResult(job, types, solver.ImproveTail(plan, WorkBudget * 2), null, default);
}
@@ -170,7 +171,8 @@ public sealed class IrregularNestingEngine : INestingEngine
var reason = run.Reason;
if (unplaced > 0 && reason == NestJobStopReason.Completed)
reason = NestJobStopReason.NoPlacementFound; // Demand no stock can hold.
return new Plan(run.Sheets, NestJobCost.RequireFinite(run.Net + unplaced * Penalty), unplaced, reason, NestJobCost.UsesExplicitCosts(job));
return new Plan(run.Sheets, NestJobCost.RequireFinite(run.Net + unplaced * Penalty), unplaced, reason,
NestJobCost.UsesExplicitCosts(job), job.Options);
}
/// <summary>
@@ -326,6 +328,7 @@ public sealed class IrregularNestingEngine : INestingEngine
.Select((t, order) => (t.Fill, t.Net, order, Estimate: NestJobCost.RequireFinite(t.Net + System.Math.Max(0, remainingArea - t.Fill.PartArea) * bestRatio)))
.OrderBy(t => t.Estimate)
.ThenByDescending(t => t.Fill.Parts.Count)
.ThenByDescending(t => NestJobCost.TieBreakKey(Offcut(job.Options, t.Fill)))
.ThenBy(t => t.order)
.First();
@@ -387,17 +390,29 @@ public sealed class IrregularNestingEngine : INestingEngine
return builder.Build(plan.Reason);
}
private static double NetCost(NestJobOptions options, SheetFill fill)
private static double NetCost(NestJobOptions options, SheetFill fill) =>
fill.Parts.Count == 0 ? NestJobCost.GrossSheetCost(fill.Stock)
: NestJobCost.NetSheetCost(options, fill.Stock, Envelope(fill));
/// <summary>Equal-cost tie-break: the fill's <see cref="NestJobCost.CompactnessOffcut(NestJobOptions, NestPlateStock, OpenNest.Geometry.Box)"/>.</summary>
private static double Offcut(NestJobOptions options, SheetFill fill) =>
fill.Parts.Count == 0 ? 0 : NestJobCost.CompactnessOffcut(options, fill.Stock, Envelope(fill));
private static OpenNest.Geometry.Box Envelope(SheetFill fill)
{
if (fill.Parts.Count == 0) return NestJobCost.GrossSheetCost(fill.Stock);
var left = fill.Parts.Min(p => p.Left);
var bottom = fill.Parts.Min(p => p.Bottom);
return NestJobCost.NetSheetCost(options, fill.Stock, new OpenNest.Geometry.Box(left, bottom,
fill.Parts.Max(p => p.Right) - left, fill.Parts.Max(p => p.Top) - bottom));
return new OpenNest.Geometry.Box(left, bottom,
fill.Parts.Max(p => p.Right) - left, fill.Parts.Max(p => p.Top) - bottom);
}
private sealed record Plan(IReadOnlyList<SheetFill> Sheets, double Cost, int Unplaced, NestJobStopReason Reason, bool Priced)
/// <summary>Ranked by unplaced parts, cost, sheet count, then compactness offcut (without salvage
/// credit every one-sheet plan costs the same); full ties keep the incumbent.</summary>
private sealed record Plan(IReadOnlyList<SheetFill> Sheets, double Cost, int Unplaced, NestJobStopReason Reason,
bool Priced, NestJobOptions Options)
{
private double Offcut => Sheets.Sum(s => IrregularNestingEngine.Offcut(Options, s));
public bool IsBetterThan(Plan other)
{
if (Unplaced != other.Unplaced)
@@ -407,7 +422,11 @@ public sealed class IrregularNestingEngine : INestingEngine
var scale = System.Math.Max(1, System.Math.Max(Cost, other.Cost));
if (System.Math.Abs(Cost - other.Cost) > 1e-9 * scale)
return Cost < other.Cost;
return Sheets.Count < other.Sheets.Count;
if (Sheets.Count != other.Sheets.Count)
return Sheets.Count < other.Sheets.Count;
var offcut = Offcut;
var otherOffcut = other.Offcut;
return offcut - otherOffcut > 1e-9 * System.Math.Max(1, System.Math.Max(offcut, otherOffcut));
}
}
}
+14 -6
View File
@@ -68,7 +68,7 @@ internal static class SheetByStock
break;
}
var trials = new List<(SheetLayout Layout, double Net)>();
var trials = new List<(SheetLayout Layout, double Net, double Offcut)>();
if (preferred != null)
Try(preferred, trials);
if (trials.Count == 0)
@@ -119,7 +119,7 @@ internal static class SheetByStock
return result.Build(reason);
void Try(NestPlateStock stock, List<(SheetLayout Layout, double Net)> trials)
void Try(NestPlateStock stock, List<(SheetLayout Layout, double Net, double Offcut)> trials)
{
token.ThrowIfCancellationRequested();
if (stock.Quantity is int available && used[stock.Id] >= available)
@@ -128,7 +128,7 @@ internal static class SheetByStock
NestJobStage.EvaluatingCandidate, stock.Id, SheetsUsed(result, job), SheetsUsed(result, job), 0));
foreach (var layout in pack(stock, remaining))
if (layout.Poses.Count > 0)
trials.Add((layout, NetCost(job, layout)));
trials.Add((layout, NetCost(job, layout), Offcut(job, layout)));
}
}
@@ -138,19 +138,22 @@ internal static class SheetByStock
/// <summary>
/// Picks the layout with the lowest estimated whole-job cost. Remaining demand is priced at the
/// best net-cost-per-material ratio any candidate achieved, so a sheet that finishes the job
/// competes fairly with a denser partial one. Ties: more material placed, then enumeration order.
/// competes fairly with a denser partial one. Ties: more material placed, then the larger
/// compactness offcut (without salvage credit every layout on one stock costs the same), then
/// enumeration order.
/// </summary>
private static SheetLayout Choose(IReadOnlyList<SheetDemand> types, int[] remaining,
IReadOnlyDictionary<string, int> priority, List<(SheetLayout Layout, double Net)> trials)
IReadOnlyDictionary<string, int> priority, List<(SheetLayout Layout, double Net, double Offcut)> trials)
{
var demandArea = types.Sum(t => remaining[t.Index] * t.MaterialArea);
var bestRatio = trials.Min(t => t.Net / System.Math.Max(t.Layout.MaterialArea, 1e-12));
return trials
.Select((t, order) => (t.Layout, order,
.Select((t, order) => (t.Layout, t.Offcut, order,
Estimate: NestJobCost.RequireFinite(t.Net + System.Math.Max(0, demandArea - t.Layout.MaterialArea) * bestRatio)))
.OrderBy(t => PriorityDebt(types, remaining, priority, t.Layout))
.ThenBy(t => t.Estimate)
.ThenByDescending(t => t.Layout.MaterialArea)
.ThenByDescending(t => t.Offcut)
.ThenBy(t => t.order)
.First()
.Layout;
@@ -176,4 +179,9 @@ internal static class SheetByStock
layout.Envelope is { } envelope
? NestJobCost.NetSheetCost(job.Options, layout.Stock, envelope)
: NestJobCost.GrossSheetCost(layout.Stock);
private static double Offcut(NestJob job, SheetLayout layout) =>
layout.Envelope is { } envelope
? NestJobCost.TieBreakKey(NestJobCost.CompactnessOffcut(job.Options, layout.Stock, envelope))
: 0;
}
+4 -2
View File
@@ -235,8 +235,10 @@ With prices, gross sheet cost is the supplied value. Salvage is still computed f
the largest qualifying material-edge offcut; net cost is
`Cost * (NetSheetArea / SheetArea)`. Set desktop salvage rate to zero for purchase-price
comparisons. Entering prices preserves the selected salvage settings. MCP and API
currently disable salvage. Without prices, scores retain their existing area units
and tie ordering; fixed strategies retain their legacy no-salvage area tie-break.
currently disable salvage. Without prices, scores retain their existing area units;
whole-job engines break exact cost ties by the compactness offcut (see
[cost basis](nesting-engines.md#cost-basis-and-heuristic-limits)), and fixed strategies retain
their legacy no-salvage area tie-break.
Desktop reports gross cost, estimated salvage credit and net score. MCP reports
`basis`, each stock's used physical count, supplied cost and gross subtotal, plus
+9 -1
View File
@@ -11,7 +11,7 @@ in `OpenNest.Engine/NestingEngines/<Name>/`, its tests in `OpenNest.Engine.Tests
| Engine | Best for | Method |
|---|---|---|
| Default | Any job; used when no engine is named | Runs Irregular, Rectangles and Circles, checks each layout with the layout check and keeps the best: valid first, then fewest unplaced parts, then lowest salvage-credited cost; ties keep the earlier engine |
| Default | Any job; used when no engine is named | Runs Irregular, Rectangles and Circles, checks each layout with the layout check and keeps the best: valid first, then fewest unplaced parts, then lowest salvage-credited cost, then the largest compactness offcut; remaining ties keep the earlier engine |
| Rectangles | Plain and near-rectangular plates | Each part packed as the box of its material at its minimum-area rotation, using a maximal-rectangles free list; stock chosen sheet by sheet by salvage-credited look-ahead cost |
| Circles | Round parts: blanks, washers, flanges, gears by their tip circle | Each part packed as its minimum enclosing circle at centers tangent to two of walls, placed discs and clamp keep-outs, with hexagonal and square lattice seeds; bids only when every part is round; stock chosen as Rectangles does, plus whole-job runs preferring each stock and a tail re-plan |
| Irregular | Irregular profiles | No-fit-polygon frontier packing with gap filling and best-fit pairs, six whole-job strategy variants and a tail re-plan |
@@ -414,3 +414,11 @@ consolidation. Default compares valid equally fulfilled results by generic job s
Fixed Fill/Strip/remnant strategies rank priority fulfillment first, then net cost
for priced stock (gross area for legacy unpriced stock), then existing stable ties.
None guarantees globally optimal purchasing or explores every combination of patterns.
Without salvage credit, which desktop Auto Nest applies only when offering several plate
sizes, every complete layout on one stock costs the same. Default, Rectangles, Circles and
Irregular then break exact cost ties by `NestJobCost.CompactnessOffcut`: the largest
full-width or full-length edge offcut, measured as salvage measures it but whatever the
salvage rate (it honors a positive minimum salvage dimension; with none, any offcut counts).
Larger wins, summed over a job's sheets. It is never part of the cost, so it cannot outrank a
cheaper layout or change a reported cost.