using OpenNest.CNC; using OpenNest.Geometry; using Xunit; using OpenNest.Engine.Jobs; using OpenNest.Engine.Jobs.Placement; namespace OpenNest.Engine.Tests.Jobs; /// /// Runnable end-to-end example of the whole-job engine API: multiple part requirements, multiple plate /// sizes, and an enumeration of every returned plate, placement, leftover, and stock line. Also the /// documentation checkpoint for the public single-plate placement service used by interactive flows. /// public class NestJobExampleTests { [Fact] public void MultiRequirementMultiStockJobEnumeratesEveryPlateAndLeftover() { // Two requirements with independent IDs, quantities, and priorities. var job = new NestJob( new[] { Part("bracket", 100.0, 60.0, 5, priority: 0), Part("plate-clip", 40.0, 40.0, 8, priority: 1), }, // Mixed inventory: five large sheets and unlimited small sheets. new[] { new NestPlateStock( "large", new Size(600.0, 400.0), quantity: 5, partSpacing: 2.0, edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1 ), new NestPlateStock( "small", new Size(300.0, 300.0), quantity: null, partSpacing: 2.0, edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1 ), } ); var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job); // -- Every physical plate is enumerated with its stock identity and placements. -- Console.WriteLine($"Status: {result.Status}, stop reason: {result.StopReason}."); foreach (var plate in result.Plates) { Console.WriteLine( $"Plate {plate.PlateIndex} from stock '{plate.StockId}' " + $"({plate.Stock.Size.Width} x {plate.Stock.Size.Length}):" ); foreach (var placement in plate.Placements) Console.WriteLine( $" {placement.PartId} #{placement.InstanceIndex} at " + $"({placement.X:F1}, {placement.Y:F1}) rotated {placement.Rotation:F3} rad." ); } // -- Every requirement reports exact fulfillment, including leftovers. -- foreach (var fulfillment in result.Fulfillment) Console.WriteLine( $"Requirement '{fulfillment.PartId}': requested {fulfillment.Requested}, " + $"placed {fulfillment.Placed}, unplaced {fulfillment.Unplaced}." ); // -- Every stock line reports physical sheets used and remaining availability. -- foreach (var usage in result.StockUsage) Console.WriteLine( $"Stock '{usage.StockId}': used {usage.Used}, " + $"remaining {(usage.Remaining.HasValue ? usage.Remaining.Value.ToString() : "unlimited")}." ); // Invariants the enumeration relies on: conservation per requirement and per stock line, no // empty plates, every plate bound to supplied stock, and per-placement instance accounting. foreach (var fulfillment in result.Fulfillment) { Assert.Equal(fulfillment.Requested, fulfillment.Placed + fulfillment.Unplaced); Assert.True(fulfillment.Unplaced >= 0); } foreach (var usage in result.StockUsage) { var stock = job.Plates.First(candidate => candidate.Id == usage.StockId); Assert.True(usage.Used >= 0); Assert.Equal( stock.Quantity is int capacity ? capacity - usage.Used : (int?)null, usage.Remaining ); } Assert.All(result.Plates, plate => Assert.NotEmpty(plate.Placements)); var plateCountByStock = result .Plates.GroupBy(plate => plate.StockId) .ToDictionary(group => group.Key, group => group.Count()); foreach (var usage in result.StockUsage) Assert.Equal(usage.Used, plateCountByStock.GetValueOrDefault(usage.StockId)); var instanceIndicesByPart = result .Plates.SelectMany(plate => plate.Placements) .GroupBy(placement => placement.PartId) .ToDictionary( group => group.Key, group => group.Select(placement => placement.InstanceIndex) ); foreach (var fulfillment in result.Fulfillment) Assert.Equal( Enumerable.Range(0, fulfillment.Placed), instanceIndicesByPart .GetValueOrDefault(fulfillment.PartId, new List()) .OrderBy(index => index) ); // The default heuristic completes this synthetic job from the mixed inventory. Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(NestJobStopReason.Completed, result.StopReason); Assert.Equal( 5, result.Fulfillment.Single(fulfillment => fulfillment.PartId == "bracket").Placed ); Assert.Equal( 8, result.Fulfillment.Single(fulfillment => fulfillment.PartId == "plate-clip").Placed ); } [Fact] public void MaxPlatesExampleShowsExplicitLeftovers() { // Same shape of job, but a plate budget forces an explicit partial result. var job = new NestJob( new[] { Part("part", 100.0, 100.0, 6, priority: 0) }, new[] { new NestPlateStock( "sheet", new Size(220.0, 220.0), quantity: null, partSpacing: 2.0, edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1 ), }, new NestJobOptions("Default", maxPlates: 1) ); var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job); Assert.Equal(NestJobStatus.Incomplete, result.Status); Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason); var single = Assert.Single(result.Plates); Assert.Equal("sheet", single.StockId); var fulfillment = Assert.Single(result.Fulfillment); Assert.Equal(6, fulfillment.Requested); Assert.Equal(single.Placements.Count, fulfillment.Placed); Assert.Equal(fulfillment.Requested - fulfillment.Placed, fulfillment.Unplaced); } /// /// The public placement service is the single-plate contract for preview-driven flows. It proposes /// parts without mutating the caller's plate, so the UI can accept or discard them explicitly. /// [Fact] public void PlateFillService_ProposesSinglePlatePartsWithoutMutatingPlate() { var plate = new Plate { Size = new Size(300.0, 200.0), Quadrant = 1 }; var drawing = new Drawing("preview", TestDrawingFactory.Rectangle(50.0, 50.0)); var item = new NestItem { Drawing = drawing, Quantity = 1 }; var parts = PlateFillService.Nest( "Default", plate, new List { item }, progress: null, token: CancellationToken.None ); var placed = Assert.Single(parts); Assert.Same(drawing, placed.BaseDrawing); Assert.Empty(plate.Parts); } private static NestJobPart Part( string id, double width, double length, int quantity, int priority ) => new( id, PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(width, length)), quantity, priority ); }