diff --git a/OpenNest.WinForms.Tests/Forms/MainFormReleaseNestingTests.cs b/OpenNest.WinForms.Tests/Forms/MainFormReleaseNestingTests.cs index 095b79f9..560f3f06 100644 --- a/OpenNest.WinForms.Tests/Forms/MainFormReleaseNestingTests.cs +++ b/OpenNest.WinForms.Tests/Forms/MainFormReleaseNestingTests.cs @@ -1,6 +1,7 @@ using System.Windows.Forms; using OpenNest.Controls; using OpenNest.Data; +using OpenNest.Data.MaterialLedger; using OpenNest.Engine; using OpenNest.Engine.Jobs; using OpenNest.Forms; @@ -28,7 +29,7 @@ public class MainFormReleaseNestingTests var unsaved = NestOf("N26-0002", 2); host.Begin(); - var first = Run(host.RenestAsync(saved, CancellationToken.None)); + var first = Run(host.RenestAsync(saved, null, CancellationToken.None)); var tab = Assert.Single(Tabs(main)); Assert.Same(saved, tab.Nest); Assert.True(first.Nested, first.Note); @@ -39,7 +40,7 @@ public class MainFormReleaseNestingTests Assert.Equal(1, tab.History.UndoCount); Assert.Equal("Auto Nest", tab.History.UndoLabel); Assert.False(main.TryUndo(tab)); - Run(host.RenestAsync(unsaved, CancellationToken.None)); + Run(host.RenestAsync(unsaved, null, CancellationToken.None)); Assert.Equal(2, Tabs(main).Count); // Make Nests opens what it saved: the tab already showing it is bound instead. @@ -68,7 +69,7 @@ public class MainFormReleaseNestingTests var nest = NestOf("N26-0003", 4); host.Begin(); - var renest = host.RenestAsync(nest, CancellationToken.None); + var renest = host.RenestAsync(nest, null, CancellationToken.None); PumpUntil(() => renest.IsCompleted); Assert.True(renest.IsCanceled || renest.Exception?.InnerException is OperationCanceledException); @@ -78,11 +79,54 @@ public class MainFormReleaseNestingTests Assert.Empty(Tabs(main)); }, TestTimeout, "The live release stop test did not complete."); - private static Nest NestOf(string name, int quantity) + [Fact] + public void LedgerStockIsNestedFirst_InTheTab_ThenTheRestOnTheNestDefaultsSheet() => + StaTestThread.Run(() => + { + using var main = new Host(); + main.SolveReleasedNest = (_, request, cts) => Task.Run(() => NestPipeline.Run( + NestingEngineRegistry.Create("Rectangles"), "Rectangles", request, null, cts.Token)); + main.Show(); + IReleaseNestingHost host = main; + // One free 10x10 ledger sheet holds one 8x8 part; the second copy tops up on 48x96. + var nest = NestOf("N26-0004", 2, partSize: 8); + nest.Material = new Material("Synthetic Steel", "SYN-1"); + nest.Thickness = 0.25; + var ledger = new ReleaseLedgerStock( + (_, _, _, _) => Task.FromResult(new LedgerStockResult.Ok( + Array.Empty(), + [new LedgerStockItem("S-1", 1, LedgerItemKinds.FullSheet, 10, 10, 1, + LedgerItemShapes.Rectangular, "B3", "Bay 3", null, null, DateTime.UnixEpoch)])), + _ => Task.FromResult(LedgerCodesInUse.Known(Array.Empty()))); + + host.Begin(); + var renest = Run(host.RenestAsync(nest, ledger, CancellationToken.None)); + + var tab = Assert.Single(Tabs(main)); + Assert.True(renest.Nested, renest.Note); + Assert.EndsWith(" 1 sheet from MaterialLedger stock.", renest.Note); + var coded = Assert.Single(nest.Plates, p => p.StockItemCodes.Count > 0); + Assert.Equal((new Size(10, 10), "S-1"), (coded.Size, Assert.Single(coded.StockItemCodes))); + Assert.Contains(nest.Plates, p => p.Parts.Count > 0 && p.Size.Equals(new Size(48, 96)) + && p.StockItemCodes.Count == 0); + Assert.Equal(2, nest.Plates.Sum(p => p.Parts.Count * p.Quantity)); + Assert.Equal("B3", tab.LedgerLocations["S-1"]); + // Each stage is its own Auto Nest step in the tab. + Assert.Equal(2, tab.History.UndoCount); + host.End(null!); + Application.DoEvents(); + }, TestTimeout, "The live release ledger test did not complete."); + + private static Nest NestOf(string name, int quantity, double partSize = 0) { var nest = new Nest(name) { Units = Units.Inches }; nest.PlateDefaults.Size = new Size(48, 96); - var drawing = new RectangleShape { Name = name + "-A", Length = 4, Width = 2 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = name + "-A", + Length = partSize > 0 ? partSize : 4, + Width = partSize > 0 ? partSize : 2, + }.GetDrawing(); drawing.Quantity.Required = quantity; nest.Drawings.Add(drawing); return nest; diff --git a/OpenNest.WinForms.Tests/Forms/SetReleaseFormsTests.cs b/OpenNest.WinForms.Tests/Forms/SetReleaseFormsTests.cs index 8b7df061..387b0318 100644 --- a/OpenNest.WinForms.Tests/Forms/SetReleaseFormsTests.cs +++ b/OpenNest.WinForms.Tests/Forms/SetReleaseFormsTests.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Security.Cryptography; using System.Windows.Forms; using OpenNest.Data; +using OpenNest.Data.MaterialLedger; using OpenNest.Forms; using OpenNest.IO; using OpenNest.Shapes; @@ -421,7 +422,11 @@ public class SetReleaseFormsTests var (host, panel) = ShowPanel(sets, library, nests); using var _ = host; - panel.Renest = (_, _) => { renested++; return Task.FromResult(new SetReleaseRenest(true, "")); }; + panel.Renest = (_, _, _) => + { + renested++; + return Task.FromResult(new SetReleaseRenest(true, "")); + }; var nestAfter = Field(panel, "nestAfterReleaseCheckBox"); nestAfter.Checked = true; panel.AddSet(set); @@ -510,6 +515,10 @@ public class SetReleaseFormsTests Run(panel.ReleaseAsync()); Assert.Equal(new[] { "Begin", "Renest Synthetic Aluminum", "Renest Synthetic Steel", "End" }, host.Calls); + // Both nests share the run's ledger stock, so neither takes a sheet the other took. + Assert.Equal(2, host.Ledgers.Count); + Assert.NotNull(host.Ledgers[0]); + Assert.Same(host.Ledgers[0], host.Ledgers[1]); Assert.Equal(FormWindowState.Minimized, form.WindowState); Assert.Same(panel.Result, host.Ended); // The host's nests are the ones saved and opened (bound) afterwards. @@ -1136,14 +1145,17 @@ public class SetReleaseFormsTests { public List Calls { get; } = new(); public List Nests { get; } = new(); + public List Ledgers { get; } = new(); public SetReleaseResult? Ended { get; private set; } public int StopAt { get; init; } public void Begin() => Calls.Add("Begin"); - public Task RenestAsync(Nest nest, CancellationToken token) + public Task RenestAsync(Nest nest, ReleaseLedgerStock ledger, + CancellationToken token) { Nests.Add(nest); + Ledgers.Add(ledger); Calls.Add($"Renest {nest.Material.Name}"); if (Nests.Count == StopAt) throw new OperationCanceledException(); diff --git a/OpenNest/Controls/ReleasePanel.cs b/OpenNest/Controls/ReleasePanel.cs index 54966cd9..233e5ea8 100644 --- a/OpenNest/Controls/ReleasePanel.cs +++ b/OpenNest/Controls/ReleasePanel.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using OpenNest.Data; +using OpenNest.Data.MaterialLedger; using OpenNest.Engine.BestFit; using OpenNest.Engine.Jobs; using OpenNest.Forms; @@ -70,9 +71,20 @@ namespace OpenNest.Controls /// /// Nests a group's built nest when Nest after release is ticked: Auto Nest headless with - /// the selected engine (); replaced by tests. + /// the selected engine (), on the run's MaterialLedger stock first + /// (null when not offered); replaced by tests. /// - internal Func> Renest { get; set; } = RenestAsync; + internal Func> Renest + { get; set; } = RenestAsync; + + /// + /// The MaterialLedger stock one Make Nests run nests on; replaced by tests. + /// + internal Func CreateLedgerStock { get; set; } = + () => new ReleaseLedgerStock( + (material, grade, thicknessIn, token) => MaterialLedgerStore.CreateClient() + .GetStockAsync(material, grade, thicknessIn, token), + token => LedgerCodesOnOtherNests.ReadAsync(Guid.Empty, token)); /// Shows Edit Drawing and returns the saved record, or null; replaced by tests. internal Func EditDrawing { get; set; } @@ -733,11 +745,15 @@ namespace OpenNest.Controls var merges = ChosenMerges(); var nestAfter = nestAfterReleaseCheckBox.Checked; var live = nestAfter ? NestingHost : null; + // One run shares its ledger stock, so two of its nests never take the same sheet. + var ledger = nestAfter ? CreateLedgerStock() : null; running = true; cancellation = new CancellationTokenSource(); UpdateCommands(); session = new SetReleaseSession(sets, library, nests, serverUrl, ReadPackage, WriteNest, CreateNest, ReadNest, - !nestAfter ? null : live == null ? Renest : LiveRenest(live)); + !nestAfter ? null + : live == null ? (nest, token) => Renest(nest, ledger, token) + : LiveRenest(live, ledger)); var progress = new Progress(ShowProgress); if (live != null) { @@ -767,12 +783,13 @@ namespace OpenNest.Controls } // The host's Auto Nest stops the whole release, as Stop here does. - private Func> LiveRenest(IReleaseNestingHost live) => + private Func> LiveRenest( + IReleaseNestingHost live, ReleaseLedgerStock ledger) => async (nest, token) => { try { - return await live.RenestAsync(nest, token); + return await live.RenestAsync(nest, ledger, token); } catch (OperationCanceledException) when (!token.IsCancellationRequested) { @@ -1138,16 +1155,29 @@ namespace OpenNest.Controls }.Where(s => !string.IsNullOrWhiteSpace(s))); // The nest is not shown anywhere yet, so Auto Nest runs off the UI thread on it alone. - private static Task RenestAsync(Nest nest, CancellationToken token) + private static Task RenestAsync(Nest nest, ReleaseLedgerStock ledger, + CancellationToken token) { var engineName = EngineSelection.EngineName; - return Task.Run(() => + return Task.Run(async () => { - var renested = NestRenest.Run(nest, engineName, AutoNestForm.DefaultMinRemnantSize, token: token); - return new SetReleaseRenest(renested.Nested, renested.Note); + var offer = ledger == null ? null : await ledger.OfferAsync(nest, token); + var renested = NestRenest.Run(nest, NestingEngineRegistry.Create(engineName), + engineName, offer?.Options, AutoNestForm.DefaultMinRemnantSize, token: token); + var ledgerNote = offer == null + ? null + : ledger.Assign(nest, offer, renested.Committed); + return new SetReleaseRenest(renested.Nested, + ReleaseNote(renested.Note, ledgerNote)); }, token); } + /// + /// The note a released nest shows: Auto Nest's, then what MaterialLedger stock it used. + /// + internal static string ReleaseNote(string renestNote, string ledgerNote) => + string.Join(" ", new[] { renestNote, ledgerNote }.Where(n => !string.IsNullOrEmpty(n))); + private static LibraryPackage ReadPackage(byte[] bytes) { var contents = DrawingPackage.Read(bytes); @@ -1179,11 +1209,13 @@ namespace OpenNest.Controls /// /// Shows , built but not saved, and nests it from scratch: every - /// drawing's required quantity, keeping only a valid layout (else no sheets and a note). - /// Throws when the operator stops it, which - /// stops the release. + /// drawing's required quantity on 's stock first, then the plate + /// defaults (null: the plate defaults only), keeping only valid layouts (else no sheets + /// and a note). Throws when the operator stops + /// it, which stops the release. /// - Task RenestAsync(Nest nest, CancellationToken token); + Task RenestAsync(Nest nest, ReleaseLedgerStock ledger, + CancellationToken token); /// /// Make Nests has finished ( is null if it threw) and has diff --git a/OpenNest/Forms/MainForm.Release.cs b/OpenNest/Forms/MainForm.Release.cs index 2b170d32..1ac580f5 100644 --- a/OpenNest/Forms/MainForm.Release.cs +++ b/OpenNest/Forms/MainForm.Release.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using System.Windows.Forms; using OpenNest.Controls; using OpenNest.Data; +using OpenNest.Data.MaterialLedger; using OpenNest.Engine.Jobs; using OpenNest.Forms.Commands; @@ -39,7 +40,8 @@ namespace OpenNest.Forms BringForward(); } - async Task IReleaseNestingHost.RenestAsync(Nest nest, CancellationToken token) + async Task IReleaseNestingHost.RenestAsync(Nest nest, + ReleaseLedgerStock ledger, CancellationToken token) { ArgumentNullException.ThrowIfNull(nest); token.ThrowIfCancellationRequested(); @@ -52,36 +54,49 @@ namespace OpenNest.Forms if (tab.PlateManager.CanClearAll) tab.ClearNest(); nest.UpdateDrawingQuantities(); - var request = NestRenest.CreateRequest(nest, EngineSelection.EngineName, AutoNestForm.DefaultMinRemnantSize); - if (request == null) - return new SetReleaseRenest(false, NestRenest.NothingToNestNote); + var offer = ledger == null ? null : await ledger.OfferAsync(nest, token); using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); // As for Auto Nest, switching tabs while it runs stops it. nestingCts = cts; - NestPipelineResult result; + NestRenestResult renested; try { - result = await (SolveReleasedNest ?? ((t, r, c) => AutoNestCommand.SolveAsync(this, t, r, c)))(tab, request, cts); + // Ledger stock first, then the plate defaults: each stage is its own Auto Nest run. + var solve = SolveReleasedNest + ?? ((t, r, c) => AutoNestCommand.SolveAsync(this, t, r, c)); + renested = await NestRenest.RunStagedAsync(nest, EngineSelection.EngineName, + offer?.Options, AutoNestForm.DefaultMinRemnantSize, + request => solve(tab, request, cts), + r => + { + using var change = tab.BeginChange("Auto Nest"); + change.MarkChanged(); + var applied = NestPipelineCommit.ApplyToEmptyPlates(r, tab.PlateManager, + token: cts.Token); + change.Complete(); + return applied; + }, cts.Token); } finally { nestingCts = null; } - cts.Token.ThrowIfCancellationRequested(); - var kept = NestRenest.Keep(result, r => + var ledgerNote = offer == null ? null : ledger.Assign(nest, offer, renested.Committed); + foreach (var row in offer?.Rows ?? Array.Empty()) { - using var change = tab.BeginChange("Auto Nest"); - change.MarkChanged(); - var applied = NestPipelineCommit.ApplyToEmptyPlates(r, tab.PlateManager, token: cts.Token); - change.Complete(); - return applied; - }); - if (kept.Nested) + foreach (var (code, location) in row.ItemLocations) + tab.LedgerLocations[code] = location; + } + if (renested.Nested) + { + tab.UpdatePlateList(); tab.PlateManager.LoadFirst(); + } tab.PlateView.ZoomToFit(); - return new SetReleaseRenest(kept.Nested, kept.Note); + return new SetReleaseRenest(renested.Nested, + ReleasePanel.ReleaseNote(renested.Note, ledgerNote)); } void IReleaseNestingHost.End(SetReleaseResult result)