using OpenNest.Data.MaterialLedger; using OpenNest.Engine; using OpenNest.Engine.Jobs; using OpenNest.Geometry; using OpenNest.Shapes; namespace OpenNest.Tests.Data; /// /// After Auto Nest commits, each plate on ledger stock takes the next codes of its row in nest /// order; nothing else touches codes, so a rejected proposal leaves them as they were. /// public class LedgerStockAssignmentTests { private static LedgerStockRow Row(string id, double width, double length, params string[] codes) => new(id, new Size(width, length), codes.Length, IsDrop: codes.Distinct().Count() > 1, IsIrregular: false, codes, Array.Empty()); [Fact] public void Plates_DequeueTheirRowsCodes_InNestOrder_AndBundlesRepeatTheirCode() { var drops = Row("ledger:1", 30, 52.5, "DROP-1182", "DROP-1190", "DROP-1201"); var bundle = Row("ledger:2", 60, 120, "SHEET-456", "SHEET-456", "SHEET-456"); var nest = new Nest(); var first = Add(nest, 1); var second = Add(nest, 2); var third = Add(nest, 2); var hand = Add(nest, 1); hand.StockItemCodes = ["STALE"]; // Committed out of nest order: codes still follow Nest.Plates. LedgerStockAssignment.Assign(nest.Plates, [(third, "ledger:2"), (first, "ledger:1"), (second, "ledger:1"), (hand, "option-0")], [drops, bundle]); Assert.Equal(["DROP-1182"], first.StockItemCodes); Assert.Equal(["DROP-1190", "DROP-1201"], second.StockItemCodes); Assert.Equal(["SHEET-456", "SHEET-456"], third.StockItemCodes); Assert.Empty(hand.StockItemCodes); } [Fact] public void ARowThatRunsOut_LeavesTheRemainingSheetsUncoded() { var nest = new Nest(); var first = Add(nest, 2); var second = Add(nest, 1); LedgerStockAssignment.Assign(nest.Plates, [(first, "ledger:1"), (second, "ledger:1")], [Row("ledger:1", 30, 52.5, "DROP-1")]); Assert.Equal(["DROP-1"], first.StockItemCodes); Assert.Empty(second.StockItemCodes); } [Fact] public void ASheetSolvedWithClampsRemoved_TakesItsBaseRowsCodes() { var nest = new Nest(); var plate = Add(nest, 1); LedgerStockAssignment.Assign(nest.Plates, [(plate, "ledger:3::removed:clamp-2")], [Row("ledger:3", 48, 96, "SHEET-9")]); Assert.Equal(["SHEET-9"], plate.StockItemCodes); Assert.Equal("ledger:3", LedgerStockAssignment.BaseStockId("ledger:3::removed:clamp-2,clamp-3")); } [Fact] public void CommittedProposal_CodesThePlatesItWasSolvedOn() { var (nest, drawing, manager) = Workspace(); using (manager) { var rows = new[] { Row("ledger:1", 30, 52.5, "DROP-1", "DROP-2") }; var result = Solve(drawing, overlap: false); Assert.True(result.IsValid, string.Join("; ", result.Violations)); var applied = NestPipelineCommit.ApplyToEmptyPlates(result, manager); LedgerStockAssignment.Assign(nest.Plates, NestPipelineCommit.PairWithStock(result, applied).Select(p => (p.Plate, p.Stock.Id)), rows); Assert.Equal(["DROP-1"], Assert.Single(applied).StockItemCodes); } } [Fact] public void RejectedProposal_LeavesCodesUntouched() { var (nest, drawing, manager) = Workspace(); using (manager) { var empty = nest.Plates.Single(); empty.StockItemCodes = ["SHEET-OLD"]; var result = Solve(drawing, overlap: true); Assert.False(result.IsValid); // Auto Nest assigns only after a commit; without consent there is none. Assert.Throws(() => NestPipelineCommit.ApplyToEmptyPlates(result, manager)); Assert.Equal(["SHEET-OLD"], empty.StockItemCodes); Assert.Empty(empty.Parts); } } [Fact] public void Describe_NamesEachCodeWithItsLocation_CountsRuns_AndNotesUncodedSheets() { var plate = new Plate(60, 120) { Quantity = 4 }; Assert.Null(LedgerStockAssignment.Describe(plate, null)); plate.StockItemCodes = ["SHEET-456", "SHEET-456", "DROP-1182"]; var locations = new Dictionary { ["SHEET-456"] = "A1" }; Assert.Equal("Stock: SHEET-456 (A1) x2, DROP-1182, 1 uncoded", LedgerStockAssignment.Describe(plate, locations)); } [Fact] public void Mapper_KeepsEachItemsLocationForDisplay() { var mapping = LedgerStockMapper.Map( [ new LedgerStockItem("D-1", 1, LedgerItemKinds.Drop, 24, 36, 1, LedgerItemShapes.Rectangular, "R1", "Rack 1", null, null, DateTime.UtcNow), new LedgerStockItem("D-2", 1, LedgerItemKinds.Drop, 24, 36, 1, LedgerItemShapes.Rectangular, "R2", "Rack 2", null, null, DateTime.UtcNow), ], Units.Inches); var row = Assert.Single(mapping.Rows); Assert.Equal("R1", row.ItemLocations["D-1"]); Assert.Equal("R2", row.ItemLocations["D-2"]); } private static Plate Add(Nest nest, int quantity) { var plate = new Plate(30, 52.5) { Quantity = quantity }; nest.Plates.Add(plate); return plate; } private static (Nest Nest, Drawing Drawing, PlateManager Manager) Workspace() { var nest = new Nest(); var drawing = new RectangleShape { Name = "SYN", Length = 4, Width = 2 }.GetDrawing(); nest.Drawings.Add(drawing); nest.CreatePlate(); return (nest, drawing, new PlateManager(nest)); } private static NestPipelineResult Solve(Drawing drawing, bool overlap) => NestPipeline.Run(new OnFirstStock(overlap), "stub", new NestPipelineRequest("stub", [new NestItem { Drawing = drawing, Quantity = 2 }], [new NestPlateStock("ledger:1", new Size(30, 52.5), 2, 0.25, new Spacing(1, 1), 1)])); // Two copies on one sheet of the first stock row; overlapping when asked, so validation fails. private sealed class OnFirstStock(bool overlap) : INestingEngine { public NestJobResult Solve(NestJob job, IProgress progress = null, CancellationToken token = default) => new( NestJobStatus.Complete, NestJobStopReason.Completed, [new NestJobPlateResult(0, job.Plates[0], [ new NestJobPlacement(job.Parts[0].Id, 0, 2, 2, 0), new NestJobPlacement(job.Parts[0].Id, 1, overlap ? 2 : 20, 2, 0), ])], [new PartFulfillment(job.Parts[0].Id, 2, 2, 0)], [new StockUsage(job.Plates[0].Id, 1, 1)]); } }