From ecca71e185c4dee881c66fb0e232ffffb2358333 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 19 Sep 2026 08:12:10 -0400 Subject: [PATCH] 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 --- .../Jobs/FixedStrategyNestingEngineTests.cs | 44 +++++++++++++++++++ .../Jobs/FixedStrategyNestingEngine.cs | 29 ++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs create mode 100644 OpenNest.Engine/Jobs/FixedStrategyNestingEngine.cs diff --git a/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs b/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs new file mode 100644 index 0000000..b753099 --- /dev/null +++ b/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs @@ -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(() => new FixedStrategyNestingEngine(null)); + Assert.Throws(() => new FixedStrategyNestingEngine(" ")); + } +} diff --git a/OpenNest.Engine/Jobs/FixedStrategyNestingEngine.cs b/OpenNest.Engine/Jobs/FixedStrategyNestingEngine.cs new file mode 100644 index 0000000..adcaca4 --- /dev/null +++ b/OpenNest.Engine/Jobs/FixedStrategyNestingEngine.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; + +namespace OpenNest; + +/// +/// 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. +/// +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 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); + } +}