using System.Threading; using OpenNest.Engine.Fill; using OpenNest.Engine.Jobs.Placement.Fillers; using OpenNest.Geometry; namespace OpenNest.Engine.Tests.Jobs; public class PlateFillerContractTests { [Theory] [InlineData("Default")] [InlineData("Vertical Remnant")] [InlineData("Horizontal Remnant")] public void StandardPlateFiller_Fill_ReturnsPartsBoundToInputDrawing(string strategy) { var plate = new Plate(new Size(30, 50)); var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4)); var filler = CreateFiller(strategy, plate); var parts = filler.Fill( new NestItem { Drawing = drawing, Quantity = 4 }, plate.WorkArea(), null, CancellationToken.None ); Assert.NotEmpty(parts); Assert.All(parts, part => Assert.Same(drawing, part.BaseDrawing)); Assert.NotEmpty(filler.PhaseResults); } [Fact] public void DefaultPlateFiller_UsesOverriddenAngleSelection() { var plate = new Plate(new Size(30, 50)); var filler = new AngleProbeDefaultPlateFiller(plate); var parts = filler.Fill( new NestItem { Drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4)), Quantity = 4, }, plate.WorkArea(), null, CancellationToken.None ); Assert.NotEmpty(parts); Assert.True(filler.BuildAnglesCalled); } [Fact] public void DefaultPlateFiller_Nest_UsesOverriddenFillAndPackArea() { var plate = new Plate(new Size(100, 100)); var fillDrawing = new Drawing("fill", TestDrawingFactory.Rectangle(10, 10)); var packDrawing = new Drawing("pack", TestDrawingFactory.Rectangle(10, 10)); var filler = new FillAndPackProbeDefaultPlateFiller(plate, fillDrawing, packDrawing); var parts = filler.Nest( new List { new() { Drawing = fillDrawing, Quantity = 10 }, new() { Drawing = packDrawing, Quantity = 1 }, }, null, CancellationToken.None ); Assert.Equal(1, filler.FillCalls); Assert.Equal(1, filler.PackAreaCalls); Assert.Equal(11, parts.Count); } [Fact] public void StripPlateFiller_Nest_ReturnsPlacedPartsAndDeductsInput() { var plate = new Plate(new Size(30, 50)); var items = new List { new() { Drawing = new Drawing("rect-a", TestDrawingFactory.Rectangle(6, 4)), Quantity = 5, }, new() { Drawing = new Drawing("rect-b", TestDrawingFactory.Rectangle(4, 3)), Quantity = 4, }, new() { Drawing = new Drawing("rect-c", TestDrawingFactory.Rectangle(2, 7)), Quantity = 3, }, }; var filler = new StripPlateFiller(plate) { PlateNumber = 7 }; var parts = filler.Nest(items, null, CancellationToken.None); Assert.NotEmpty(parts); Assert.All(parts, part => Assert.Contains(items, item => ReferenceEquals(item.Drawing, part.BaseDrawing))); Assert.All(items, item => Assert.InRange(item.Quantity, 0, 5)); } [Fact] public void StripPlateFiller_Nest_UsesOverriddenPackArea() { var plate = new Plate(new Size(100, 100)); var drawing = new Drawing("pack", TestDrawingFactory.Rectangle(10, 10)); var filler = new PackProbeStripPlateFiller(plate, drawing); var parts = filler.Nest( new List { new() { Drawing = drawing, Quantity = 1 } }, null, CancellationToken.None ); Assert.Equal(1, filler.PackAreaCalls); Assert.Single(parts); } [Fact] public void NestProgressReporter_Report_ClonesPartsAndPreservesReportFields() { var source = new Part( new Drawing("part", TestDrawingFactory.Rectangle(10, 20)), new Vector(3, 5) ); source.Rotate(0.5); var progress = new CapturingProgress(); var workArea = new Box(1, 2, 30, 40); NestProgressReporter.Report( progress, new ProgressReport { Phase = NestPhase.Pairs, PlateNumber = 3, Parts = new List { source }, WorkArea = workArea, Description = "candidate preview", IsOverallBest = true, } ); var reported = Assert.Single(progress.Reports); Assert.Equal(NestPhase.Pairs, reported.Phase); Assert.Equal(3, reported.PlateNumber); Assert.Equal("candidate preview", reported.Description); Assert.True(reported.IsOverallBest); Assert.Equal(workArea.X, reported.ActiveWorkArea.X); Assert.Equal(workArea.Y, reported.ActiveWorkArea.Y); Assert.Equal(workArea.Width, reported.ActiveWorkArea.Width); Assert.Equal(workArea.Length, reported.ActiveWorkArea.Length); var preview = Assert.Single(reported.BestParts); Assert.NotSame(source, preview); Assert.Same(source.BaseDrawing, preview.BaseDrawing); Assert.Equal(source.Location.X, preview.Location.X); Assert.Equal(source.Location.Y, preview.Location.Y); Assert.Equal(source.Rotation, preview.Rotation); } [Fact] public void NestProgressReporter_Report_DoesNotForwardMissingProgressOrParts() { var progress = new CapturingProgress(); var part = new Part(new Drawing("part", TestDrawingFactory.Rectangle())); NestProgressReporter.Report( null, new ProgressReport { Parts = new List { part } } ); NestProgressReporter.Report(progress, new ProgressReport { Parts = null }); NestProgressReporter.Report(progress, new ProgressReport { Parts = new List() }); Assert.Empty(progress.Reports); } [Fact] public void PlateFillOrchestrator_Nest_UsesThresholdIdentityAndCallerDelegates() { var plate = new Plate(new Size(100, 100)); var fillDrawing = new Drawing("duplicate", TestDrawingFactory.Rectangle(10, 10)); var packDrawing = new Drawing("duplicate", TestDrawingFactory.Rectangle(10, 10)); var items = new List { new() { Drawing = fillDrawing, Quantity = 10 }, new() { Drawing = packDrawing, Quantity = 1 }, }; var calls = new List(); var progress = new CapturingProgress(); var token = CancellationToken.None; var parts = PlateFillOrchestrator.Nest( plate, items, new DefaultFillComparer(), (item, workArea, receivedProgress, receivedToken) => { calls.Add("fill"); Assert.Same(progress, receivedProgress); Assert.Equal(token, receivedToken); Assert.Same(fillDrawing, item.Drawing); var placed = new List(); for (var i = 0; i < item.Quantity; i++) { var part = new Part(item.Drawing); part.Offset(new Vector(i * 10, 0)); placed.Add(part); } return placed; }, (workArea, packItems, receivedProgress, receivedToken) => { calls.Add("pack"); Assert.Same(progress, receivedProgress); Assert.Equal(token, receivedToken); var packItem = Assert.Single(packItems); Assert.Same(packDrawing, packItem.Drawing); return new List { new(packItem.Drawing, new Vector(0, 20)) }; }, progress, token ); Assert.Equal(new[] { "fill", "pack" }, calls); Assert.Equal(11, parts.Count); Assert.Equal(10, parts.Count(part => ReferenceEquals(part.BaseDrawing, fillDrawing))); Assert.Single(parts.Where(part => ReferenceEquals(part.BaseDrawing, packDrawing))); Assert.Equal(0, items[0].Quantity); Assert.Equal(0, items[1].Quantity); } [Fact] public void PlateFillOrchestrator_Nest_DoesNotInvokeDelegatesAfterCancellation() { var plate = new Plate(new Size(100, 100)); var item = new NestItem { Drawing = new Drawing("part", TestDrawingFactory.Rectangle(10, 10)), Quantity = 10, }; using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); var parts = PlateFillOrchestrator.Nest( plate, new List { item }, new DefaultFillComparer(), (_, _, _, _) => throw new Xunit.Sdk.XunitException("Fill must not run after cancellation"), (_, _, _, _) => throw new Xunit.Sdk.XunitException("Pack must not run after cancellation"), null, cancellation.Token ); Assert.Empty(parts); Assert.Equal(10, item.Quantity); } private sealed class FillAndPackProbeDefaultPlateFiller : DefaultPlateFiller { private readonly Drawing fillDrawing; private readonly Drawing packDrawing; internal FillAndPackProbeDefaultPlateFiller( Plate plate, Drawing fillDrawing, Drawing packDrawing ) : base(plate) { this.fillDrawing = fillDrawing; this.packDrawing = packDrawing; } internal int FillCalls { get; private set; } internal int PackAreaCalls { get; private set; } public override List Fill( NestItem item, Box workArea, IProgress progress, CancellationToken token ) { FillCalls++; Assert.Same(fillDrawing, item.Drawing); var parts = new List(); for (var i = 0; i < item.Quantity; i++) parts.Add(new Part(fillDrawing, new Vector(i * 10, 0))); return parts; } public override List PackArea( Box box, List items, IProgress progress, CancellationToken token ) { PackAreaCalls++; Assert.Same(packDrawing, Assert.Single(items).Drawing); return new List { new(packDrawing, new Vector(0, 20)) }; } } private sealed class AngleProbeDefaultPlateFiller : DefaultPlateFiller { internal AngleProbeDefaultPlateFiller(Plate plate) : base(plate) { } internal bool BuildAnglesCalled { get; private set; } public override List BuildAngles( NestItem item, ClassificationResult classification, Box workArea ) { BuildAnglesCalled = true; return base.BuildAngles(item, classification, workArea); } } private sealed class PackProbeStripPlateFiller : StripPlateFiller { private readonly Drawing drawing; internal PackProbeStripPlateFiller(Plate plate, Drawing drawing) : base(plate) { this.drawing = drawing; } internal int PackAreaCalls { get; private set; } public override List PackArea( Box box, List items, IProgress progress, CancellationToken token ) { PackAreaCalls++; Assert.Same(drawing, Assert.Single(items).Drawing); return new List { new(drawing, new Vector(0, 0)) }; } } private static PlateFillerBase CreateFiller(string strategy, Plate plate) => strategy switch { "Default" => new DefaultPlateFiller(plate), "Vertical Remnant" => new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical), "Horizontal Remnant" => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal), _ => throw new ArgumentOutOfRangeException(nameof(strategy)), }; private sealed class CapturingProgress : IProgress { public List Reports { get; } = new(); public void Report(NestProgress value) { Reports.Add(value); } } }