Files
OpenNest/OpenNest.Engine.Tests/Jobs/NestJobCostTests.cs
T
896ed2026a fix(engine): leave scribe marks out of layout bounds and salvage
The job runner already checked sheet bounds on material contours only,
but the benchmark validator and salvage scoring used Part.BoundingBox,
which includes scribe/etch moves. A PEP bend tick that ends a hair past
the part's edge passed the runner yet failed the benchmark when placed
flush to the sheet edge, and it could shrink the credited offcut. Marks
only mark the surface, so bounds and salvage now use material only.

Benchmark before/after (all five built-in engines, local fixtures,
salvage 0.5): no job changed validity or cost. Regression tests pin the
new rule: a protruding tick flush to the sheet edge is valid in all four
quadrants, and a tick past the parts envelope no longer shrinks salvage
(targeted fixture cost 130 -> 120).

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:56:49 -04:00

124 lines
6.1 KiB
C#

using OpenNest.CNC;
using OpenNest.Engine.Jobs;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests.Jobs;
public class NestJobCostTests
{
public static IEnumerable<object[]> EdgeCases()
{
foreach (var rate in new[] { 0.0, 0.5, 1.0 })
foreach (var edge in new[] { "bottom", "top", "left", "right" })
foreach (var quadrant in new[] { 1, 2, 3, 4 })
yield return new object[] { rate, edge, quadrant };
}
[Theory]
[MemberData(nameof(EdgeCases))]
public void EveryEdgeMatchesFrozenScoringExactly(double rate, string edge, int quadrant)
{
var stock = new NestPlateStock("sheet", new Size(20, 20), partSpacing: 0.25,
edgeSpacing: new Spacing(1, 1, 1, 1), quadrant: quadrant);
var work = stock.WorkArea;
var width = edge is "left" or "right" ? 4 : 18;
var height = edge is "bottom" or "top" ? 4 : 18;
var x = work.Left + (edge == "left" ? 14 : 0);
var y = work.Bottom + (edge == "bottom" ? 14 : 0);
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(
TestDrawingFactory.Rectangle(width, height)), 2);
var job = new NestJob(new[] { part }, new[] { stock },
new NestJobOptions(salvageRate: rate, minimumSalvageDimension: 2));
var sheet = new NestJobPlateResult(0, stock, new[] { new NestJobPlacement("part", 0, x, y, 0) });
var expected = 400 - rate * (18 * 13.75);
Assert.Equal(expected, LegacyNestJobCost.EstimateNetArea(job, sheet));
Assert.Equal(expected, NestJobCost.NetSheetArea(job, sheet));
Assert.Equal(expected, NestJobCost.NetSheetArea(job.Options, stock, new Box(x, y, width, height)));
#pragma warning disable CS0618 // Compatibility API must retain the old result.
Assert.Equal(expected, StockLadderNestingEngine.EstimateNetArea(job, sheet));
#pragma warning restore CS0618
}
[Theory]
[InlineData(0, 5, 100)]
[InlineData(0.5, 0, 100)]
[InlineData(0.5, 7, 100)]
[InlineData(0.5, 6, 70)]
[InlineData(0.5, 5, 70)]
public void StockLadderFixtureRetainsThresholds(double rate, double minimum, double expected)
{
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 4)), 1);
var stock = new NestPlateStock("sheet", new Size(10, 10));
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, 0, 0, 0) });
Assert.Equal(expected, NestJobCost.NetSheetArea(job, sheet));
Assert.Equal(LegacyNestJobCost.EstimateNetArea(job, sheet), NestJobCost.NetSheetArea(job, sheet));
}
[Theory]
[InlineData(0, false)]
[InlineData(0.5, false)]
[InlineData(0, true)]
[InlineData(0.5, true)]
public void RotatedPartsAndMultipleSheetsUseMaterialCost(double rate, bool marked)
{
var program = TestDrawingFactory.Rectangle(4, 3);
if (marked)
{
program.MoveTo(2, 2);
program.Codes.Add(new LinearMove(9, 2) { Layer = LayerType.Scribe });
}
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(program), 5);
var stock = new NestPlateStock("sheet", new Size(30, 40));
var other = new NestPlateStock("large", new Size(50, 50));
var job = new NestJob(new[] { part }, new[] { stock, other },
new NestJobOptions(salvageRate: rate, minimumSalvageDimension: 2));
var builder = new NestJobResultBuilder(job);
builder.AddSheet(stock, new[] { ("part", 10.123, 8.456, 0.37), ("part", 25.789, 17.321, 1.12) });
builder.AddSheet(other, new[] { ("part", 12.345, 19.876, 2.13) });
var result = builder.Build(NestJobStopReason.NoPlacementFound);
var cleanPart = new NestJobPart("part", PartGeometrySnapshot.FromProgram(
TestDrawingFactory.Rectangle(4, 3)), 5);
var cleanJob = new NestJob(new[] { cleanPart }, job.Plates, job.Options);
foreach (var sheet in result.Plates)
Assert.Equal(LegacyNestJobCost.EstimateNetArea(cleanJob, sheet), NestJobCost.NetSheetArea(job, sheet));
Assert.Equal(2500, NestJobCost.UnplacedPartPenalty(job));
Assert.Equal(result.Plates.Sum(sheet => LegacyNestJobCost.EstimateNetArea(cleanJob, sheet)) + 5000,
NestJobCost.Evaluate(job, result));
}
[Fact]
public void EtchBeyondMaterialDoesNotShrinkSalvageOffcut()
{
var program = TestDrawingFactory.Rectangle(4, 3);
program.MoveTo(2, 2);
program.Codes.Add(new LinearMove(15, 2) { Layer = LayerType.Scribe });
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(program), 1);
var stock = new NestPlateStock("sheet", new Size(10, 20));
var job = new NestJob(new[] { part }, new[] { stock },
new NestJobOptions(salvageRate: 0.5, minimumSalvageDimension: 1));
var sheet = new NestJobPlateResult(0, stock, new[] { new NestJobPlacement("part", 0, 0, 0, 0) });
Assert.Equal(130, LegacyNestJobCost.EstimateNetArea(job, sheet));
Assert.Equal(120, NestJobCost.NetSheetArea(job, sheet));
Assert.Equal(120, NestJobCost.NetSheetArea(job.Options, stock, JobPartGeometry.Read(part.Geometry).Bounds));
}
[Fact]
public void EmptySheetsAndEmptyStockRetainBenchmarkSemantics()
{
var stock = new NestPlateStock("sheet", new Size(10, 10));
var job = new NestJob(Array.Empty<NestJobPart>(), new[] { stock },
new NestJobOptions(salvageRate: 1, minimumSalvageDimension: 1));
var sheet = new NestJobPlateResult(0, stock, Array.Empty<NestJobPlacement>());
Assert.Equal(100, NestJobCost.NetSheetArea(job, sheet));
Assert.Equal(LegacyNestJobCost.EstimateNetArea(job, sheet), NestJobCost.NetSheetArea(job, sheet));
var empty = new NestJob(Array.Empty<NestJobPart>(), Array.Empty<NestPlateStock>());
Assert.Equal(0, NestJobCost.UnplacedPartPenalty(empty));
Assert.Equal(0, NestJobCost.Evaluate(empty, new NestJobResultBuilder(empty).Build(NestJobStopReason.Completed)));
}
}