using OpenNest.CNC; using OpenNest.Geometry; using Xunit; namespace OpenNest.Engine.Tests.Jobs; /// /// Parity between the legacy adapter and the migrated built-in plate nesters (Default/Strip) on /// generated geometry. The runner's placement validator enforces geometric safety on every committed /// candidate, so these tests assert fulfillment, status, and — for the deterministic Default/rectangle /// case — identical layouts. /// public class PlateNesterParityTests { private const double Tolerance = 1e-6; private static readonly Size PlateSize = new(30, 50); private static readonly Spacing Edge = new(1, 1, 1, 1); private static NestJob Job(IReadOnlyList parts, int? stockQuantity = 3, string strategy = "Default") { var stock = new NestPlateStock("stock", PlateSize, stockQuantity, 1, Edge); return new NestJob(parts, new[] { stock }, new NestJobOptions(strategy)); } private static NestJobResult Solve(IPlateNester nester, NestJob job) => new NestJobRunner(_ => nester).Solve(job); private static Dictionary ByPart(NestJobResult result) => result.Fulfillment.ToDictionary(f => f.PartId, StringComparer.Ordinal); private static void AssertLayoutsIdentical(NestJobResult left, NestJobResult right) { Assert.Equal(left.Plates.Count, right.Plates.Count); for (var i = 0; i < left.Plates.Count; i++) { var lPlates = left.Plates[i].Placements; var rPlates = right.Plates[i].Placements; Assert.Equal(lPlates.Count, rPlates.Count); var lSorted = lPlates.OrderBy(p => p.PartId).ThenBy(p => p.X).ThenBy(p => p.Y).ToList(); var rSorted = rPlates.OrderBy(p => p.PartId).ThenBy(p => p.X).ThenBy(p => p.Y).ToList(); for (var j = 0; j < lSorted.Count; j++) { Assert.Equal(lSorted[j].PartId, rSorted[j].PartId); Assert.Equal(lSorted[j].X, rSorted[j].X, 6); Assert.Equal(lSorted[j].Y, rSorted[j].Y, 6); Assert.True(AnglesEqual(lSorted[j].Rotation, rSorted[j].Rotation), $"rotation differs: {lSorted[j].Rotation} vs {rSorted[j].Rotation}"); } } } private static bool AnglesEqual(double left, double right) { var delta = (left - right) % (System.Math.PI * 2); return System.Math.Abs(delta) <= Tolerance || System.Math.Abs(System.Math.Abs(delta) - System.Math.PI * 2) <= Tolerance; } [Fact] public void DefaultParity_Rectangles_SameFulfillmentAndLayout() { var parts = new[] { new NestJobPart("a", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 4), new NestJobPart("b", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 3) }; var legacy = Solve(new LegacyPlateNesterAdapter(plate => new DefaultNestEngine(plate)), Job(parts)); var migrated = Solve(new DefaultPlateNester(), Job(parts)); Assert.Equal(legacy.Status, migrated.Status); Assert.Equal(NestJobStatus.Complete, migrated.Status); Assert.Equal(ByPart(legacy), ByPart(migrated)); foreach (var usage in legacy.StockUsage) Assert.Equal(usage.Used, migrated.StockUsage.First(u => u.StockId == usage.StockId).Used); // Automatic-rotation rectangles on a single stock size are deterministic: identical layouts. AssertLayoutsIdentical(legacy, migrated); } [Fact] public void StripParity_Rectangles_SameFulfillmentAndTotalCount() { var parts = new[] { new NestJobPart("a", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 4), new NestJobPart("b", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 3) }; var legacy = Solve(new LegacyPlateNesterAdapter(plate => new StripNestEngine(plate)), Job(parts, strategy: "Strip")); var migrated = Solve(new StripPlateNester(), Job(parts, strategy: "Strip")); Assert.Equal(legacy.Status, migrated.Status); Assert.Equal(NestJobStatus.Complete, migrated.Status); Assert.Equal(ByPart(legacy), ByPart(migrated)); Assert.Equal(legacy.Plates.SelectMany(p => p.Placements).Count(), migrated.Plates.SelectMany(p => p.Placements).Count()); // Shrink-fill ordering can differ between engine instances; do not assert identical coordinates. } [Fact] public void MigratedBuiltins_AreResolvedByProductionFactory() { Assert.IsType(PlateNesterFactory.Create("Default")); Assert.IsType(PlateNesterFactory.Create("Strip")); Assert.IsType(PlateNesterFactory.Create("Vertical Remnant")); Assert.IsType(PlateNesterFactory.Create("Horizontal Remnant")); } [Fact] public void AsymmetricPart_ValidAndFulfilled() { // L-shape: 6x4 outer with a corner notch removed (single closed contour, asymmetric). var lshape = new Program(); lshape.MoveTo(0, 0); lshape.LineTo(6, 0); lshape.LineTo(6, 4); lshape.LineTo(3, 4); lshape.LineTo(3, 2); lshape.LineTo(0, 2); lshape.LineTo(0, 0); var parts = new[] { new NestJobPart("l", PartGeometrySnapshot.FromProgram(lshape), 3), new NestJobPart("sq", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 2) }; var result = Solve(new DefaultPlateNester(), Job(parts)); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(3, ByPart(result)["l"].Placed); Assert.Equal(2, ByPart(result)["sq"].Placed); Assert.Equal(5, result.Plates.SelectMany(p => p.Placements).Count()); } [Fact] public void HoleAndArcParts_ValidAndFulfilled() { // 6x6 rectangle with a 2x2 inner hole (rapid contour), plus a D-shape with a semicircular arc. var holed = new Program(); holed.MoveTo(0, 0); holed.LineTo(6, 0); holed.LineTo(6, 6); holed.LineTo(0, 6); holed.LineTo(0, 0); holed.MoveTo(2, 2); holed.LineTo(4, 2); holed.LineTo(4, 4); holed.LineTo(2, 4); holed.LineTo(2, 2); var arc = new Program(); arc.MoveTo(0, 0); arc.LineTo(3, 0); arc.ArcTo(3, 5, 3, 2.5, RotationType.CCW); arc.LineTo(0, 5); arc.LineTo(0, 0); var parts = new[] { new NestJobPart("holed", PartGeometrySnapshot.FromProgram(holed), 2), new NestJobPart("arc", PartGeometrySnapshot.FromProgram(arc), 2) }; var result = Solve(new DefaultPlateNester(), Job(parts)); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(2, ByPart(result)["holed"].Placed); Assert.Equal(2, ByPart(result)["arc"].Placed); } [Fact] public void FixedRotation_Respected() { var part = new NestJobPart("fixed", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 2, rotation: RotationPolicy.Fixed(0)); var result = Solve(new DefaultPlateNester(), Job(new[] { part })); Assert.Equal(NestJobStatus.Complete, result.Status); var placements = result.Plates.SelectMany(p => p.Placements).ToList(); Assert.Equal(2, placements.Count); foreach (var placement in placements) Assert.True(AnglesEqual(placement.Rotation, 0), $"fixed rotation violated: {placement.Rotation}"); } [Fact] public void RepeatedNames_KeepIndependentIdentity() { // Two distinct requirements sharing identical geometry (and, via the mapper, name) but different IDs. var program = TestDrawingFactory.Rectangle(6, 4); var parts = new[] { new NestJobPart("first", PartGeometrySnapshot.FromProgram(program), 2), new NestJobPart("second", PartGeometrySnapshot.FromProgram(program), 1) }; var result = Solve(new DefaultPlateNester(), Job(parts)); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(2, ByPart(result)["first"].Placed); Assert.Equal(1, ByPart(result)["second"].Placed); var ids = result.Plates.SelectMany(p => p.Placements).Select(p => p.PartId); Assert.Equal(2, ids.Count(id => id == "first")); Assert.Equal(1, ids.Count(id => id == "second")); } [Fact] public void OffsetGeometry_ValidAndFulfilled() { // Part contour starting at a nonzero origin (offset geometry). var program = new Program(); program.MoveTo(12, 7); program.LineTo(18, 7); program.LineTo(18, 11); program.LineTo(12, 11); program.LineTo(12, 7); var part = new NestJobPart("offset", PartGeometrySnapshot.FromProgram(program), 2); var result = Solve(new DefaultPlateNester(), Job(new[] { part })); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(2, ByPart(result)["offset"].Placed); // The runner's validator guarantees containment and non-overlap for every committed placement. } [Fact] public void RunScopedCache_DrawingReusedAcrossTrials() { // 14x9 parts on 30x20: one sheet holds fewer than five, so the runner runs multiple candidate // trials through the same nester instance. The run-scoped drawing cache must keep producing // valid, correctly-attributed placements across trials. var part = new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(14, 9)), 5); var stock = new NestPlateStock("stock", new Size(30, 20), 3); var nester = new DefaultPlateNester(); var result = new NestJobRunner(_ => nester).Solve(new NestJob(new[] { part }, new[] { stock })); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(5, result.Fulfillment.Single(f => f.PartId == "p").Placed); Assert.Equal(2, result.Plates.Count); var usage = result.StockUsage.Single(); Assert.Equal(2, usage.Used); Assert.Equal(1, usage.Remaining); } [Fact] public void LegacyRemnantStrategies_StillResolveThroughAdapter() { // Remnant strategies must keep working through the legacy adapter after the factory change. var part = new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 2); foreach (var strategy in new[] { "Vertical Remnant", "Horizontal Remnant" }) { var result = Solve(PlateNesterFactory.Create(strategy), Job(new[] { part }, strategy: strategy)); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(2, result.Fulfillment.Single(f => f.PartId == "p").Placed); } } }