feat(engine): add FixedStrategyNestingEngine adapting IPlateNester strategies to INestingEngine

Implements a sealed adapter class that forces a fixed IPlateNester strategy onto
any NestJob, overriding the job's own PlacementStrategy while preserving MaxPlates.
Delegates all multi-plate allocation and stock selection to NestJobRunner.

This allows single-plate nesting strategies to compete as full whole-job
INestingEngine solvers in benchmarks, enabling comparative performance testing
of placement algorithms across various job configurations.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-19 08:12:10 -04:00
co-authored by Claude Haiku 4.5
parent f0fe79f0f1
commit ecca71e185
2 changed files with 73 additions and 0 deletions
@@ -0,0 +1,44 @@
using OpenNest.CNC;
using OpenNest.Geometry;
using Xunit;
namespace OpenNest.Engine.Tests.Jobs;
public class FixedStrategyNestingEngineTests
{
[Fact]
public void ForcesConfiguredStrategyRegardlessOfJobOptions()
{
var engine = new FixedStrategyNestingEngine("Strip");
// The job itself declares an unknown strategy; if FixedStrategyNestingEngine
// didn't override it, PlateNesterFactory would reject it with NotSupportedException.
var job = FiniteStockJobTests.Job(1, new NestJobOptions("Not A Real Strategy"));
var result = engine.Solve(job);
Assert.Equal(NestJobStatus.Complete, result.Status);
}
[Fact]
public void PreservesJobMaxPlates()
{
var engine = new FixedStrategyNestingEngine("Default");
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(100, 100)), 6);
var stock = new NestPlateStock("sheet", new Size(220, 220), quantity: null, partSpacing: 2.0,
edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1);
var job = new NestJob(new[] { part }, new[] { stock }, new NestJobOptions("Default", maxPlates: 1));
var result = engine.Solve(job);
Assert.Equal(NestJobStatus.Incomplete, result.Status);
Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason);
Assert.Single(result.Plates);
}
[Fact]
public void RejectsNullOrWhitespaceStrategyAtConstruction()
{
Assert.Throws<ArgumentException>(() => new FixedStrategyNestingEngine(null));
Assert.Throws<ArgumentException>(() => new FixedStrategyNestingEngine(" "));
}
}
@@ -0,0 +1,29 @@
using System;
using System.Threading;
namespace OpenNest;
/// <summary>
/// Adapts one fixed IPlateNester strategy to the whole-job INestingEngine contract, so it can compete
/// as a full job solver alongside model-submitted engines. Delegates all multi-plate/size selection to
/// NestJobRunner; only the placement strategy key is forced, overriding whatever the job itself declared.
/// </summary>
public sealed class FixedStrategyNestingEngine : INestingEngine
{
private readonly string strategy;
private readonly NestJobRunner runner = new(PlateNesterFactory.Create);
public FixedStrategyNestingEngine(string strategy)
{
if (string.IsNullOrWhiteSpace(strategy))
throw new ArgumentException("Strategy cannot be null or whitespace.", nameof(strategy));
this.strategy = strategy;
}
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(job);
var forced = new NestJob(job.Parts, job.Plates, new NestJobOptions(strategy, job.Options.MaxPlates));
return runner.Solve(forced, progress, token);
}
}