diff --git a/OpenNest.Benchmark/BenchmarkRunner.cs b/OpenNest.Benchmark/BenchmarkRunner.cs
index 65d8488..1ad3b84 100644
--- a/OpenNest.Benchmark/BenchmarkRunner.cs
+++ b/OpenNest.Benchmark/BenchmarkRunner.cs
@@ -179,7 +179,7 @@ namespace OpenNest.Benchmark
var plateArea = plateRuns.Sum(run => run.Plate.Area());
var netSheetArea = validation.Valid
? plateResults.Sum(result =>
- StockLadderNestingEngine.EstimateNetArea(baselineJob, result)
+ NestJobCost.NetSheetArea(baselineJob, result)
)
: 0;
var sizeBreakdown = plateRuns
@@ -266,7 +266,7 @@ namespace OpenNest.Benchmark
// Salvage credit is recomputed from the job's own geometry, never taken from the engine.
var netSheetArea = validation.Valid
? jobResult.Plates.Sum(p =>
- StockLadderNestingEngine.EstimateNetArea(nestJob, p)
+ NestJobCost.NetSheetArea(nestJob, p)
)
: 0;
diff --git a/OpenNest.Benchmark/JobResult.cs b/OpenNest.Benchmark/JobResult.cs
index 85d5f6f..75287c0 100644
--- a/OpenNest.Benchmark/JobResult.cs
+++ b/OpenNest.Benchmark/JobResult.cs
@@ -23,7 +23,7 @@ namespace OpenNest.Benchmark
public double PlateArea { get; init; }
/// Sheet area consumed after crediting salvageable offcuts
- /// (StockLadderNestingEngine.EstimateNetArea summed over every plate).
+ /// (NestJobCost.NetSheetArea summed over every plate).
/// Equals PlateArea when salvage credit is disabled.
public double NetSheetArea { get; init; }
diff --git a/OpenNest.Engine.Tests/Jobs/LegacyNestJobCost.cs b/OpenNest.Engine.Tests/Jobs/LegacyNestJobCost.cs
new file mode 100644
index 0000000..0b4a60b
--- /dev/null
+++ b/OpenNest.Engine.Tests/Jobs/LegacyNestJobCost.cs
@@ -0,0 +1,42 @@
+using OpenNest.Engine.Jobs;
+using OpenNest.Engine.Jobs.Adapters;
+
+namespace OpenNest.Engine.Tests.Jobs;
+
+// Frozen pre-PR2 computation: keep independent of NestJobCost to detect scoring drift.
+internal static class LegacyNestJobCost
+{
+ public static double EstimateNetArea(NestJob job, NestJobPlateResult sheet)
+ {
+ var area = sheet.Stock.Size.Width * sheet.Stock.Size.Length;
+ var minimum = job.Options.MinimumSalvageDimension;
+ if (job.Options.SalvageRate == 0 || minimum <= 0 || sheet.Placements.Count == 0)
+ return area;
+ var work = DrawingJobMapper.CreatePlate(sheet.Stock).WorkArea();
+ var parts = job.Parts.ToDictionary(p => p.Id);
+ var boxes = sheet
+ .Placements.Select(p =>
+ {
+ var part = new Part(DrawingJobMapper.CreateDrawing(parts[p.PartId]));
+ part.Rotate(p.Rotation);
+ part.Location = new OpenNest.Geometry.Vector(p.X, p.Y);
+ part.UpdateBounds();
+ return part.BoundingBox;
+ })
+ .ToList();
+ var gap = sheet.Stock.PartSpacing;
+ var candidates = new[]
+ {
+ (work.Length, boxes.Min(b => b.Bottom) - work.Bottom - gap),
+ (work.Length, work.Top - boxes.Max(b => b.Top) - gap),
+ (boxes.Min(b => b.Left) - work.Left - gap, work.Width),
+ (work.Right - boxes.Max(b => b.Right) - gap, work.Width),
+ };
+ var salvage = candidates
+ .Where(c => c.Item1 >= minimum && c.Item2 >= minimum)
+ .Select(c => c.Item1 * c.Item2)
+ .DefaultIfEmpty(0)
+ .Max();
+ return area - job.Options.SalvageRate * salvage;
+ }
+}
diff --git a/OpenNest.Engine.Tests/Jobs/NestJobCostTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobCostTests.cs
new file mode 100644
index 0000000..4aedddf
--- /dev/null
+++ b/OpenNest.Engine.Tests/Jobs/NestJobCostTests.cs
@@ -0,0 +1,115 @@
+using OpenNest.CNC;
+using OpenNest.Engine.Jobs;
+using OpenNest.Geometry;
+
+namespace OpenNest.Engine.Tests.Jobs;
+
+public class NestJobCostTests
+{
+ public static IEnumerable