mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 00:21:06 -04:00
feat(desktop): nest released nests live in the main window
With Nest after release ticked, Make Nests now minimizes the Drawing Sets window and nests each group's built nest in its own main-window tab, before it is saved, with Auto Nest's progress window and live plan strip, one nest at a time in plan order. Stop there (or switching tabs) stops the whole release, as the panel's Stop does; the layout is kept by the same rules as the headless renest, without the Keep review. After upload each tab is bound to its saved record instead of a second tab opening; a tab whose nest was not saved is closed. The main window stays in front, and the Drawing Sets window flashes its taskbar button when a group failed or was refused or the release was not recorded. NestRenest splits into CreateRequest and Keep so the solve runs off the UI thread and the commit on it; AutoNestCommand's solve-with-progress step is shared rather than copied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
27796851f3
commit
f600c5e58b
11 files changed
+622
-64
No files matched your search
@@ -25,12 +25,17 @@ public sealed record NestRenestResult(
|
||||
/// <see cref="NestPipeline"/>. A layout is committed only when it is valid and keepable;
|
||||
/// otherwise the nest is left without plates for the operator to run Auto Nest. Copies the
|
||||
/// layout leaves unplaced stay as remaining quantity. The caller owns the nest throughout.
|
||||
/// A caller showing the nest uses <see cref="CreateRequest"/> and <see cref="Keep"/> to solve
|
||||
/// off its thread and commit on it.
|
||||
/// </summary>
|
||||
public static class NestRenest
|
||||
{
|
||||
/// <summary>The note left when no layout is kept.</summary>
|
||||
public const string NotKeptNote = "Auto Nest could not keep a layout: open it and run Auto Nest.";
|
||||
|
||||
/// <summary>The note left when no drawing has a quantity to nest.</summary>
|
||||
public const string NothingToNestNote = "Nothing has a quantity to nest.";
|
||||
|
||||
/// <summary>Most physical sheets one renest may use, as the Auto Nest dialog allows.</summary>
|
||||
public const int MaxPlates = 100;
|
||||
|
||||
@@ -51,6 +56,24 @@ public static class NestRenest
|
||||
|
||||
nest.Plates.Clear();
|
||||
nest.UpdateDrawingQuantities();
|
||||
var request = CreateRequest(nest, engineName, minimumSalvageDimension);
|
||||
if (request == null)
|
||||
return new NestRenestResult(false, 0, 0, Array.Empty<NestUnplacedPart>(), NothingToNestNote);
|
||||
|
||||
var result = NestPipeline.Run(engine, engineName, request, progress, token);
|
||||
// Engines that ignore cancellation still have their proposal discarded.
|
||||
token.ThrowIfCancellationRequested();
|
||||
return Keep(result, kept => NestPipelineCommit.ApplyToPlates(kept, nest.CreatePlate, token: token));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Auto Nest job for every drawing's required quantity of <paramref name="nest"/>, on
|
||||
/// sheets from its plate defaults and plate-size options; null when nothing has a quantity.
|
||||
/// The nest is not changed, so a caller showing it can solve this off its thread.
|
||||
/// </summary>
|
||||
public static NestPipelineRequest CreateRequest(Nest nest, string engineName, double minimumSalvageDimension = 0)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
var items = nest.Drawings
|
||||
.Where(d => !d.IsCutOff && d.Quantity.Required > 0)
|
||||
.Select(d => new NestItem
|
||||
@@ -64,21 +87,29 @@ public static class NestRenest
|
||||
})
|
||||
.ToList();
|
||||
if (items.Count == 0)
|
||||
return new NestRenestResult(false, 0, 0, Array.Empty<NestUnplacedPart>(), "Nothing has a quantity to nest.");
|
||||
return null;
|
||||
|
||||
var options = nest.PlateOptions;
|
||||
var request = new NestPipelineRequest(engineName, items,
|
||||
return new NestPipelineRequest(engineName, items,
|
||||
NestStockBuilder.FromTemplate(nest.PlateDefaults.CreateNew(), options),
|
||||
new NestJobOptions(maxPlates: MaxPlates,
|
||||
salvageRate: options.Count > 0 ? nest.SalvageRate : 0,
|
||||
minimumSalvageDimension: options.Count > 0 ? minimumSalvageDimension : 0));
|
||||
var result = NestPipeline.Run(engine, engineName, request, progress, token);
|
||||
// Engines that ignore cancellation still have their proposal discarded.
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps <paramref name="result"/> only when it is valid, keepable and places something:
|
||||
/// <paramref name="commit"/> then attaches its sheets to the nest (on the nest's own
|
||||
/// thread) and returns them. Otherwise nothing is committed and the note says so.
|
||||
/// </summary>
|
||||
public static NestRenestResult Keep(NestPipelineResult result, Func<NestPipelineResult, IReadOnlyList<Plate>> commit)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
ArgumentNullException.ThrowIfNull(commit);
|
||||
if (!result.IsValid || !result.CanKeep || result.Plates.All(p => p.Parts.Count == 0))
|
||||
return new NestRenestResult(false, 0, 0, result.Unplaced, NotKeptNote);
|
||||
|
||||
var applied = NestPipelineCommit.ApplyToPlates(result, nest.CreatePlate, token: token);
|
||||
var applied = commit(result);
|
||||
var sheets = applied.Sum(p => p.Quantity);
|
||||
var area = applied.Sum(p => p.Area() * p.Quantity);
|
||||
var utilization = area > 0 ? applied.Sum(p => p.Utilization() * p.Area() * p.Quantity) / area : 0;
|
||||
|
||||
@@ -84,6 +84,30 @@ public class NestRenestTests
|
||||
Assert.Empty(nest.Plates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRequest_LeavesTheNestAlone_AndKeepCommitsTheSolvedLayoutThroughTheCaller()
|
||||
{
|
||||
var nest = NestOf(("SYN-A", 4, 2, 6), ("SYN-B", 3, 3, 0));
|
||||
Assert.Null(NestRenest.CreateRequest(NestOf(("SYN-Z", 4, 2, 0)), "Rectangles"));
|
||||
|
||||
var request = NestRenest.CreateRequest(nest, "Rectangles");
|
||||
|
||||
Assert.NotNull(request);
|
||||
Assert.Empty(nest.Plates);
|
||||
Assert.Equal(new[] { ("SYN-A", 6) }, request.Items.Select(i => (i.Drawing.Name, i.Quantity)));
|
||||
var solved = NestPipeline.Run(request);
|
||||
var committed = 0;
|
||||
var kept = NestRenest.Keep(solved, r =>
|
||||
{
|
||||
committed++;
|
||||
return NestPipelineCommit.ApplyToPlates(r, nest.CreatePlate);
|
||||
});
|
||||
Assert.True(kept.Nested, kept.Note);
|
||||
Assert.Equal(1, committed);
|
||||
Assert.Equal(6, nest.Plates.Sum(p => p.Parts.Count * p.Quantity));
|
||||
Assert.StartsWith("Nested on 1 sheet", kept.Note);
|
||||
}
|
||||
|
||||
/// <summary>Puts every copy at the same spot, so the layout never validates.</summary>
|
||||
private sealed class OverlappingEngine : INestingEngine
|
||||
{
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
// Make Nests nesting live in the main window. Reads Application.OpenForms through MainForm,
|
||||
// so it must not overlap fill-lifetime tests.
|
||||
[Collection("Fill operation lifetime")]
|
||||
public class MainFormReleaseNestingTests
|
||||
{
|
||||
private static readonly TimeSpan TestTimeout = TimeSpan.FromMinutes(1);
|
||||
|
||||
[Fact]
|
||||
public void EachNestNestsInItsOwnTab_ASavedOneIsBoundNotReopened_AndAnUnsavedOneCloses() => 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;
|
||||
var saved = NestOf("N26-0001", 6);
|
||||
var unsaved = NestOf("N26-0002", 2);
|
||||
|
||||
host.Begin();
|
||||
var first = Run(host.RenestAsync(saved, CancellationToken.None));
|
||||
var tab = Assert.Single(Tabs(main));
|
||||
Assert.Same(saved, tab.Nest);
|
||||
Assert.True(first.Nested, first.Note);
|
||||
Assert.StartsWith("Nested on 1 sheet", first.Note);
|
||||
Assert.Equal(6, saved.Plates.Sum(p => p.Parts.Count * p.Quantity));
|
||||
Assert.Equal(Guid.Empty, tab.Document.RemoteId);
|
||||
Run(host.RenestAsync(unsaved, CancellationToken.None));
|
||||
Assert.Equal(2, Tabs(main).Count);
|
||||
|
||||
// Make Nests opens what it saved: the tab already showing it is bound instead.
|
||||
var record = new NestRecord { Id = Guid.NewGuid(), Revision = 1, Name = saved.Name };
|
||||
main.OpenServerNest(saved, record);
|
||||
Assert.Equal(2, Tabs(main).Count);
|
||||
Assert.Equal((record.Id, 1), (tab.Document.RemoteId, tab.Document.RemoteRevision));
|
||||
|
||||
host.End(null!);
|
||||
Application.DoEvents();
|
||||
Assert.Same(tab, Assert.Single(Tabs(main)));
|
||||
}, TestTimeout, "The live release nesting test did not complete.");
|
||||
|
||||
[Fact]
|
||||
public void StoppingTheAutoNestStopsTheRelease_AndLeavesNoLayout() => StaTestThread.Run(() =>
|
||||
{
|
||||
using var main = new Host();
|
||||
main.SolveReleasedNest = (_, _, cts) =>
|
||||
{
|
||||
// The progress window's Stop.
|
||||
cts.Cancel();
|
||||
return Task.FromException<NestPipelineResult>(new OperationCanceledException(cts.Token));
|
||||
};
|
||||
main.Show();
|
||||
IReleaseNestingHost host = main;
|
||||
var nest = NestOf("N26-0003", 4);
|
||||
|
||||
host.Begin();
|
||||
var renest = host.RenestAsync(nest, CancellationToken.None);
|
||||
PumpUntil(() => renest.IsCompleted);
|
||||
|
||||
Assert.True(renest.IsCanceled || renest.Exception?.InnerException is OperationCanceledException);
|
||||
Assert.DoesNotContain(nest.Plates, p => p.Parts.Count > 0);
|
||||
host.End(null!);
|
||||
Application.DoEvents();
|
||||
Assert.Empty(Tabs(main));
|
||||
}, TestTimeout, "The live release stop test did not complete.");
|
||||
|
||||
private static Nest NestOf(string name, int quantity)
|
||||
{
|
||||
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();
|
||||
drawing.Quantity.Required = quantity;
|
||||
nest.Drawings.Add(drawing);
|
||||
return nest;
|
||||
}
|
||||
|
||||
private static List<EditNestForm> Tabs(MainForm main) =>
|
||||
main.MdiChildren.OfType<EditNestForm>().Where(f => !f.IsDisposed).ToList();
|
||||
|
||||
private static T Run<T>(Task<T> task)
|
||||
{
|
||||
PumpUntil(() => task.IsCompleted);
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static void PumpUntil(Func<bool> condition)
|
||||
{
|
||||
var until = DateTime.UtcNow.AddSeconds(30);
|
||||
while (!condition() && DateTime.UtcNow < until)
|
||||
{
|
||||
Application.DoEvents();
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
Assert.True(condition(), "Timed out.");
|
||||
}
|
||||
|
||||
private sealed class Host : MainForm
|
||||
{
|
||||
// No startup migrations/automatic new document, and no settings save at close.
|
||||
protected override void OnLoad(EventArgs e) { }
|
||||
protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using OpenNest.Forms;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Shapes;
|
||||
using OpenNest.Tests.Data;
|
||||
using IReleaseNestingHost = OpenNest.Controls.IReleaseNestingHost;
|
||||
using ReleasePanel = OpenNest.Controls.ReleasePanel;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
@@ -479,6 +480,109 @@ public class SetReleaseFormsTests
|
||||
"The remembered nest-after test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestAfterRelease_WithANestingHost_NestsEachNestThereInPlanOrder_AndTheWindowStepsAside()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new PackageLibrary();
|
||||
var a = library.Add("SYN-A", "Synthetic Steel");
|
||||
var c = library.Add("SYN-C", "Synthetic Aluminum");
|
||||
var sets = new InMemoryDrawingSets();
|
||||
var set = CreateSet(sets, (a, 1), (c, 2));
|
||||
var nests = new MemoryNests();
|
||||
var host = new FakeNestingHost();
|
||||
var opened = new List<Nest>();
|
||||
|
||||
using var form = new DrawingSetsForm(sets, library, Server);
|
||||
form.EnableRelease(nests, (nest, _) => opened.Add(nest), null, host);
|
||||
var panel = form.ReleasePanel;
|
||||
panel.CreateNest = (group, _) => Task.FromResult(new Nest("N") { Units = group.Units });
|
||||
var finished = 0;
|
||||
panel.MakeNestsFinished += (_, _) => finished++;
|
||||
form.Show();
|
||||
Field<CheckBox>(panel, "nestAfterReleaseCheckBox").Checked = true;
|
||||
panel.AddSet(set);
|
||||
|
||||
Run(panel.ReleaseAsync());
|
||||
|
||||
Assert.Equal(new[] { "Begin", "Renest Synthetic Aluminum", "Renest Synthetic Steel", "End" }, host.Calls);
|
||||
Assert.Equal(FormWindowState.Minimized, form.WindowState);
|
||||
Assert.Same(panel.Result, host.Ended);
|
||||
// The host's nests are the ones saved and opened (bound) afterwards.
|
||||
Assert.Equal(host.Nests, opened);
|
||||
Assert.Equal(2, nests.Uploaded.Count);
|
||||
Assert.All(Field<DataGridView>(panel, "groupGrid").Rows.Cast<DataGridViewRow>(),
|
||||
r => Assert.EndsWith("Nested live.", Note(r)));
|
||||
Assert.False(panel.NeedsAttention);
|
||||
Assert.Equal(1, finished);
|
||||
},
|
||||
TestTimeout,
|
||||
"The live nesting test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestingHostStop_StopsTheRelease_SavesNothing_AndEndsTheHost()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new PackageLibrary();
|
||||
var a = library.Add("SYN-A", "Synthetic Steel");
|
||||
var c = library.Add("SYN-C", "Synthetic Aluminum");
|
||||
var sets = new InMemoryDrawingSets();
|
||||
var set = CreateSet(sets, (a, 1), (c, 2));
|
||||
var nests = new MemoryNests();
|
||||
var host = new FakeNestingHost { StopAt = 2 };
|
||||
|
||||
var (window, panel) = ShowPanel(sets, library, nests);
|
||||
using var _ = window;
|
||||
panel.NestingHost = host;
|
||||
panel.AddSet(set);
|
||||
|
||||
Run(panel.ReleaseAsync());
|
||||
|
||||
Assert.Equal(new[] { "Begin", "Renest Synthetic Aluminum", "Renest Synthetic Steel", "End" }, host.Calls);
|
||||
Assert.True(panel.Result!.Canceled);
|
||||
Assert.Empty(nests.Uploaded);
|
||||
Assert.Empty(sets.Releases);
|
||||
Assert.Same(panel.Result, host.Ended);
|
||||
Assert.Empty(panel.Result.Created);
|
||||
Assert.True(panel.NeedsAttention);
|
||||
Assert.StartsWith("Canceled: 0 nests saved", Field<Label>(panel, "statusLabel").Text);
|
||||
},
|
||||
TestTimeout,
|
||||
"The live nesting stop test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestAfterRelease_Unticked_DoesNotUseTheNestingHost()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new PackageLibrary();
|
||||
var a = library.Add("SYN-A", "Synthetic Steel");
|
||||
var sets = new InMemoryDrawingSets();
|
||||
var set = CreateSet(sets, (a, 1));
|
||||
var host = new FakeNestingHost();
|
||||
|
||||
var (window, panel) = ShowPanel(sets, library, new MemoryNests());
|
||||
using var _ = window;
|
||||
panel.NestingHost = host;
|
||||
Field<CheckBox>(panel, "nestAfterReleaseCheckBox").Checked = false;
|
||||
panel.AddSet(set);
|
||||
|
||||
Run(panel.ReleaseAsync());
|
||||
|
||||
Assert.Empty(host.Calls);
|
||||
Assert.Single(sets.Releases);
|
||||
},
|
||||
TestTimeout,
|
||||
"The unticked live nesting test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetsTab_DoubleClickAddsASetAtOneKit_AgainAddsAKit_AndAddToReleaseTakesEverySelectedSet()
|
||||
{
|
||||
@@ -1040,6 +1144,32 @@ public class SetReleaseFormsTests
|
||||
private static T Field<T>(object owner, string name) =>
|
||||
(T)owner.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(owner)!;
|
||||
|
||||
/// <summary>Records what Make Nests asks of the main window, and can stop at a given nest.</summary>
|
||||
private sealed class FakeNestingHost : IReleaseNestingHost
|
||||
{
|
||||
public List<string> Calls { get; } = new();
|
||||
public List<Nest> Nests { get; } = new();
|
||||
public SetReleaseResult? Ended { get; private set; }
|
||||
public int StopAt { get; init; }
|
||||
|
||||
public void Begin() => Calls.Add("Begin");
|
||||
|
||||
public Task<SetReleaseRenest> RenestAsync(Nest nest, CancellationToken token)
|
||||
{
|
||||
Nests.Add(nest);
|
||||
Calls.Add($"Renest {nest.Material.Name}");
|
||||
if (Nests.Count == StopAt)
|
||||
throw new OperationCanceledException();
|
||||
return Task.FromResult(new SetReleaseRenest(true, "Nested live."));
|
||||
}
|
||||
|
||||
public void End(SetReleaseResult result)
|
||||
{
|
||||
Calls.Add("End");
|
||||
Ended = result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Real drawing packages by revision, so the release reads them as the desktop does.</summary>
|
||||
private sealed class PackageLibrary : IDrawingLibraryStore
|
||||
{
|
||||
|
||||
@@ -30,8 +30,9 @@ namespace OpenNest.Controls
|
||||
/// name) shows why at once; Fix Drawings, a double-click or the group's right-click menu
|
||||
/// opens Edit Drawing and the plan is rebuilt with what was saved. Make Nests is the only
|
||||
/// step that writes: <see cref="SetReleaseSession"/> builds the nests, nests each with Auto
|
||||
/// Nest when Nest after release is ticked (remembered per user), saves them, opens each in
|
||||
/// its own tab and records the release; each group's outcome is then shown.
|
||||
/// Nest when Nest after release is ticked (remembered per user; live in the main window
|
||||
/// through <see cref="NestingHost"/>, else headless), saves them, opens each in its own
|
||||
/// tab and records the release; each group's outcome is then shown.
|
||||
/// </summary>
|
||||
public partial class ReleasePanel : UserControl
|
||||
{
|
||||
@@ -85,6 +86,18 @@ namespace OpenNest.Controls
|
||||
/// <summary>Raised once Make Nests has opened the nests it made, so the host can show them.</summary>
|
||||
public event EventHandler NestsOpened;
|
||||
|
||||
/// <summary>Raised when Make Nests hands its nesting to <see cref="NestingHost"/>.</summary>
|
||||
public event EventHandler NestingStarted;
|
||||
|
||||
/// <summary>Raised when Make Nests has finished, whatever its outcome (see <see cref="NeedsAttention"/>).</summary>
|
||||
public event EventHandler MakeNestsFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Nests each released nest where the operator can watch it (the main window, live, in
|
||||
/// the nest's own tab) when Nest after release is ticked; null nests headless here.
|
||||
/// </summary>
|
||||
public IReleaseNestingHost NestingHost { get; set; }
|
||||
|
||||
/// <summary>The finished release, once Make Nests has run; otherwise null.</summary>
|
||||
public SetReleaseResult Result { get; private set; }
|
||||
|
||||
@@ -719,30 +732,63 @@ namespace OpenNest.Controls
|
||||
|
||||
var merges = ChosenMerges();
|
||||
var nestAfter = nestAfterReleaseCheckBox.Checked;
|
||||
var live = nestAfter ? NestingHost : null;
|
||||
running = true;
|
||||
cancellation = new CancellationTokenSource();
|
||||
UpdateCommands();
|
||||
session = new SetReleaseSession(sets, library, nests, serverUrl, ReadPackage, WriteNest, CreateNest, ReadNest,
|
||||
nestAfter ? Renest : null);
|
||||
!nestAfter ? null : live == null ? Renest : LiveRenest(live));
|
||||
var progress = new Progress<SetReleaseProgress>(ShowProgress);
|
||||
if (live != null)
|
||||
{
|
||||
live.Begin();
|
||||
NestingStarted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
try
|
||||
{
|
||||
Result = await session.RunAsync(plan, chosen, Environment.UserName, notesBox.Text, progress, cancellation.Token, merges);
|
||||
// A nest nested live is already in its tab, which this binds to its saved record.
|
||||
foreach (var done in Result.Created)
|
||||
openNest(done.Nest, done.Record);
|
||||
}
|
||||
finally
|
||||
{
|
||||
running = false;
|
||||
live?.End(Result);
|
||||
}
|
||||
|
||||
foreach (var done in Result.Created)
|
||||
openNest(done.Nest, done.Record);
|
||||
if (IsDisposed)
|
||||
return;
|
||||
ShowOutcome();
|
||||
if (Result.Created.Count > 0)
|
||||
NestsOpened?.Invoke(this, EventArgs.Empty);
|
||||
if (!IsDisposed)
|
||||
{
|
||||
ShowOutcome();
|
||||
if (Result.Created.Count > 0)
|
||||
NestsOpened?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
MakeNestsFinished?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
// The host's Auto Nest stops the whole release, as Stop here does.
|
||||
private Func<Nest, CancellationToken, Task<SetReleaseRenest>> LiveRenest(IReleaseNestingHost live) =>
|
||||
async (nest, token) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await live.RenestAsync(nest, token);
|
||||
}
|
||||
catch (OperationCanceledException) when (!token.IsCancellationRequested)
|
||||
{
|
||||
cancellation.Cancel();
|
||||
throw;
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// True when the last Make Nests needs the operator here: a group failed or was
|
||||
/// refused, or the release was not recorded.
|
||||
/// </summary>
|
||||
public bool NeedsAttention =>
|
||||
Result != null && (Result.Release == null
|
||||
|| Result.Groups.Any(g => g.Outcome is SetReleaseGroupOutcome.Failed or SetReleaseGroupOutcome.Refused));
|
||||
|
||||
/// <summary>Posts the release again after every nest was saved but recording it failed.</summary>
|
||||
internal async Task RetryRecordAsync()
|
||||
{
|
||||
@@ -1122,6 +1168,30 @@ namespace OpenNest.Controls
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where Make Nests nests what it releases when Nest after release is ticked: the main
|
||||
/// window, which shows each nest in its tab while Auto Nest runs on it.
|
||||
/// </summary>
|
||||
public interface IReleaseNestingHost
|
||||
{
|
||||
/// <summary>Make Nests is starting; nesting follows, one nest at a time in plan order.</summary>
|
||||
void Begin();
|
||||
|
||||
/// <summary>
|
||||
/// Shows <paramref name="nest"/>, 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 <see cref="OperationCanceledException"/> when the operator stops it, which
|
||||
/// stops the release.
|
||||
/// </summary>
|
||||
Task<SetReleaseRenest> RenestAsync(Nest nest, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Make Nests has finished (<paramref name="result"/> is null if it threw) and has
|
||||
/// already opened what it saved: whatever was shown but not saved goes.
|
||||
/// </summary>
|
||||
void End(SetReleaseResult result);
|
||||
}
|
||||
|
||||
/// <summary>A release being built, kept while the Drawing Sets window is closed.</summary>
|
||||
public sealed record ReleaseDraft(IReadOnlyList<SetReleasePlanLine> Sets, IReadOnlyList<SetReleaseDrawingLine> Drawings,
|
||||
string Notes, bool IncludeArchived);
|
||||
|
||||
@@ -76,25 +76,6 @@ namespace OpenNest.Forms.Commands
|
||||
/// </summary>
|
||||
public async Task RunAsync(CancellationTokenSource cts)
|
||||
{
|
||||
using var progressForm = new NestProgressForm(cts, showPlateRow: true)
|
||||
{
|
||||
AllowAccept = false,
|
||||
HoldOpenUntilCompleted = true,
|
||||
};
|
||||
progressForm.ShowPlanStrip(target.PlateView.ColorScheme);
|
||||
var receivingProgress = true;
|
||||
bool Receiving() => receivingProgress && !target.IsDisposed && !cts.IsCancellationRequested;
|
||||
var progress = new Progress<NestProgress>(p =>
|
||||
{
|
||||
if (Receiving())
|
||||
progressForm.UpdateProgress(p);
|
||||
});
|
||||
var plans = new Progress<IReadOnlyList<ProposedPlate>>(sheets =>
|
||||
{
|
||||
if (Receiving())
|
||||
progressForm.UpdatePlan(sheets);
|
||||
});
|
||||
var jobProgress = JobEngineNest.CreateProgress(EngineName, progress, plans);
|
||||
try
|
||||
{
|
||||
var request = new NestPipelineRequest(EngineName, items,
|
||||
@@ -102,26 +83,7 @@ namespace OpenNest.Forms.Commands
|
||||
new NestJobOptions(maxPlates: 100,
|
||||
salvageRate: plateOptions?.Count > 0 ? salvageRate : 0,
|
||||
minimumSalvageDimension: minRemnantSize));
|
||||
|
||||
async Task<NestPipelineResult> SolveAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() => NestPipeline.Run(request, jobProgress, cts.Token));
|
||||
}
|
||||
finally
|
||||
{
|
||||
receivingProgress = false;
|
||||
progressForm.ShowCompleted();
|
||||
progressForm.Close();
|
||||
}
|
||||
}
|
||||
|
||||
var solve = SolveAsync();
|
||||
// Owned modal progress holds drawings/plates stable through solve and cancellation.
|
||||
if (!solve.IsCompleted)
|
||||
progressForm.ShowDialog(owner);
|
||||
var result = await solve;
|
||||
var result = await SolveAsync(owner, target, request, cts);
|
||||
cts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
var allowInvalid = false;
|
||||
@@ -169,10 +131,56 @@ namespace OpenNest.Forms.Commands
|
||||
MessageBox.Show(owner, $"Nesting error: {ex.Message}", Caption,
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Solves <paramref name="request"/> off the UI thread behind the owned modal progress
|
||||
/// window, with the best plan so far as a sheet strip; its Stop cancels
|
||||
/// <paramref name="cts"/>. Nothing is committed: the caller keeps or discards the result.
|
||||
/// Make Nests uses it too, to nest each released nest in its tab as Auto Nest would.
|
||||
/// </summary>
|
||||
internal static async Task<NestPipelineResult> SolveAsync(IWin32Window owner, EditNestForm target,
|
||||
NestPipelineRequest request, CancellationTokenSource cts)
|
||||
{
|
||||
using var progressForm = new NestProgressForm(cts, showPlateRow: true)
|
||||
{
|
||||
receivingProgress = false;
|
||||
AllowAccept = false,
|
||||
HoldOpenUntilCompleted = true,
|
||||
};
|
||||
progressForm.ShowPlanStrip(target.PlateView.ColorScheme);
|
||||
var receivingProgress = true;
|
||||
bool Receiving() => receivingProgress && !target.IsDisposed && !cts.IsCancellationRequested;
|
||||
var progress = new Progress<NestProgress>(p =>
|
||||
{
|
||||
if (Receiving())
|
||||
progressForm.UpdateProgress(p);
|
||||
});
|
||||
var plans = new Progress<IReadOnlyList<ProposedPlate>>(sheets =>
|
||||
{
|
||||
if (Receiving())
|
||||
progressForm.UpdatePlan(sheets);
|
||||
});
|
||||
var jobProgress = JobEngineNest.CreateProgress(request.EngineName, progress, plans);
|
||||
|
||||
async Task<NestPipelineResult> RunSolveAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() => NestPipeline.Run(request, jobProgress, cts.Token));
|
||||
}
|
||||
finally
|
||||
{
|
||||
receivingProgress = false;
|
||||
progressForm.ShowCompleted();
|
||||
progressForm.Close();
|
||||
}
|
||||
}
|
||||
|
||||
var solve = RunSolveAsync();
|
||||
// Owned modal progress holds drawings/plates stable through solve and cancellation.
|
||||
if (!solve.IsCompleted)
|
||||
progressForm.ShowDialog(owner);
|
||||
return await solve;
|
||||
}
|
||||
|
||||
// Costs and unplaced parts were shown in the review; clamp removals exist only once kept.
|
||||
|
||||
@@ -55,18 +55,23 @@ namespace OpenNest.Forms
|
||||
// Shown modeless, where a DialogResult button does not close the window.
|
||||
closeButton.Click += (_, _) => Close();
|
||||
Load += (_, _) => FitReleasePanel();
|
||||
releasePanel.NestingStarted += ReleasePanel_NestingStarted;
|
||||
releasePanel.MakeNestsFinished += ReleasePanel_MakeNestsFinished;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the release panel beside the Library and Sets tabs, and New Release on the
|
||||
/// Releases tab: nests are saved through <paramref name="nests"/> and each is opened with
|
||||
/// <paramref name="openNest"/>; a nest <paramref name="isNestOpen"/> reports as open in a
|
||||
/// tab is never merged into.
|
||||
/// tab is never merged into. With <paramref name="nestingHost"/>, Nest after release nests
|
||||
/// each nest there, live, and this window steps aside (minimized) meanwhile.
|
||||
/// </summary>
|
||||
public void EnableRelease(INestRepository nests, Action<Nest, NestRecord> openNest, Func<Guid, bool> isNestOpen = null)
|
||||
public void EnableRelease(INestRepository nests, Action<Nest, NestRecord> openNest, Func<Guid, bool> isNestOpen = null,
|
||||
IReleaseNestingHost nestingHost = null)
|
||||
{
|
||||
releasePanel.Initialize(sets, library, nests, serverUrl, openNest);
|
||||
releasePanel.IsNestOpen = isNestOpen ?? (_ => false);
|
||||
releasePanel.NestingHost = nestingHost;
|
||||
browser.EnableRelease(set => releasePanel.AddSet(set));
|
||||
libraryView.EnableRelease((drawing, quantity) => releasePanel.AddDrawings(new[] { drawing }, quantity));
|
||||
releasesView.EnableRelease(StartRelease);
|
||||
@@ -118,6 +123,16 @@ namespace OpenNest.Forms
|
||||
splitContainer.Panel1MinSize, available - splitContainer.Panel2MinSize);
|
||||
}
|
||||
|
||||
// The nesting shows in the main window, so this one gets out of its way.
|
||||
private void ReleasePanel_NestingStarted(object sender, EventArgs e) => WindowState = FormWindowState.Minimized;
|
||||
|
||||
// The outcomes stay here; a release that needs a look flashes the taskbar button.
|
||||
private void ReleasePanel_MakeNestsFinished(object sender, EventArgs e)
|
||||
{
|
||||
if (!IsDisposed && IsHandleCreated && releasePanel.NeedsAttention && ActiveForm != this)
|
||||
Win32.FlashTaskbarUntilActive(Handle);
|
||||
}
|
||||
|
||||
// A draft closed in a tab meanwhile can be merged into now.
|
||||
private void DrawingSetsForm_Activated(object sender, EventArgs e) => releasePanel.RefreshOpenNests();
|
||||
|
||||
|
||||
@@ -45,6 +45,13 @@ namespace OpenNest.Forms
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
// Make Nests nested it in a tab already: that tab becomes the saved nest.
|
||||
if (MdiChildren.OfType<EditNestForm>().FirstOrDefault(f => ReferenceEquals(f.Nest, nest)) is { } shown)
|
||||
{
|
||||
shown.Document.BindRemote(record.Id, NestStorage.Settings.ServerUrl, record.Revision);
|
||||
releaseTabs.Remove(shown);
|
||||
return;
|
||||
}
|
||||
var windowState = ActiveMdiChild?.WindowState ?? FormWindowState.Maximized;
|
||||
LoadNest(nest, windowState, record.Id, NestStorage.Settings.ServerUrl, record.Revision);
|
||||
}
|
||||
@@ -188,7 +195,7 @@ namespace OpenNest.Forms
|
||||
ShowInTaskbar = true,
|
||||
MinimizeBox = true,
|
||||
};
|
||||
form.EnableRelease(nests, OpenServerNest, IsServerNestOpen);
|
||||
form.EnableRelease(nests, OpenServerNest, IsServerNestOpen, this);
|
||||
form.RestoreRelease(releaseDraft);
|
||||
releaseDraft = null;
|
||||
// The nests a release made open here, so this window comes forward to show them.
|
||||
@@ -214,7 +221,7 @@ namespace OpenNest.Forms
|
||||
private async void Save_Click(object sender, EventArgs e)
|
||||
{
|
||||
var form = activeForm;
|
||||
if (form == null || storage.IsSaving)
|
||||
if (form == null || storage.IsSaving || RefuseSaveDuringRelease(form))
|
||||
return;
|
||||
if (NestStorage.Settings.Mode == NestStorageMode.File)
|
||||
form.Save();
|
||||
@@ -225,7 +232,7 @@ namespace OpenNest.Forms
|
||||
private async void SaveAs_Click(object sender, EventArgs e)
|
||||
{
|
||||
var form = activeForm;
|
||||
if (form == null || storage.IsSaving)
|
||||
if (form == null || storage.IsSaving || RefuseSaveDuringRelease(form))
|
||||
return;
|
||||
if (NestStorage.Settings.Mode == NestStorageMode.File)
|
||||
form.SaveAs();
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Forms.Commands;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// Make Nests with Nest after release ticked nests here, where the operator can watch: each
|
||||
/// built nest opens in its own tab before it is saved and Auto Nest runs on it with the
|
||||
/// usual progress window and plan strip, one nest at a time in plan order. The release
|
||||
/// keeps only a valid layout, as headless (<see cref="NestRenest"/>), without the Keep or
|
||||
/// validation review. Stop in the progress window, or switching tabs, stops the release.
|
||||
/// Once saved, a tab is bound to its server record; a tab whose nest was not saved
|
||||
/// (refused, failed or stopped before upload) is closed.
|
||||
/// </summary>
|
||||
public partial class MainForm : IReleaseNestingHost
|
||||
{
|
||||
// Tabs Make Nests opened for nests it has not saved yet.
|
||||
private readonly List<EditNestForm> releaseTabs = new();
|
||||
|
||||
/// <summary>
|
||||
/// Solves a released nest's Auto Nest behind the progress window; replaced by tests,
|
||||
/// which cannot wait on its modal loop.
|
||||
/// </summary>
|
||||
internal Func<EditNestForm, NestPipelineRequest, CancellationTokenSource, Task<NestPipelineResult>> SolveReleasedNest { get; set; }
|
||||
|
||||
void IReleaseNestingHost.Begin()
|
||||
{
|
||||
releaseTabs.Clear();
|
||||
// The nests being released are not edited until they are saved.
|
||||
SetNestingLockout(true);
|
||||
BringForward();
|
||||
}
|
||||
|
||||
async Task<SetReleaseRenest> IReleaseNestingHost.RenestAsync(Nest nest, CancellationToken token)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
token.ThrowIfCancellationRequested();
|
||||
LoadNest(nest, ActiveMdiChild?.WindowState ?? FormWindowState.Maximized);
|
||||
var tab = MdiChildren.OfType<EditNestForm>().Last(f => ReferenceEquals(f.Nest, nest));
|
||||
releaseTabs.Add(tab);
|
||||
tab.Activate();
|
||||
|
||||
// A merged draft's sheets are already gone; whatever the new nest started with goes too.
|
||||
if (tab.PlateManager.CanClearAll)
|
||||
tab.PlateManager.ClearAll();
|
||||
nest.UpdateDrawingQuantities();
|
||||
var request = NestRenest.CreateRequest(nest, EngineSelection.EngineName, AutoNestForm.DefaultMinRemnantSize);
|
||||
if (request == null)
|
||||
return new SetReleaseRenest(false, NestRenest.NothingToNestNote);
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
// As for Auto Nest, switching tabs while it runs stops it.
|
||||
nestingCts = cts;
|
||||
NestPipelineResult result;
|
||||
try
|
||||
{
|
||||
result = await (SolveReleasedNest ?? ((t, r, c) => AutoNestCommand.SolveAsync(this, t, r, c)))(tab, request, cts);
|
||||
}
|
||||
finally
|
||||
{
|
||||
nestingCts = null;
|
||||
}
|
||||
cts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
var kept = NestRenest.Keep(result,
|
||||
r => NestPipelineCommit.ApplyToEmptyPlates(r, tab.PlateManager, token: cts.Token));
|
||||
if (kept.Nested)
|
||||
tab.PlateManager.LoadFirst();
|
||||
tab.PlateView.ZoomToFit();
|
||||
return new SetReleaseRenest(kept.Nested, kept.Note);
|
||||
}
|
||||
|
||||
void IReleaseNestingHost.End(SetReleaseResult result)
|
||||
{
|
||||
// Saved nests were bound to their records and left the list; the rest were never saved.
|
||||
foreach (var tab in releaseTabs.ToList())
|
||||
{
|
||||
if (!tab.IsDisposed)
|
||||
tab.Close();
|
||||
}
|
||||
releaseTabs.Clear();
|
||||
SetNestingLockout(false);
|
||||
|
||||
var first = result?.Created.FirstOrDefault()?.Nest;
|
||||
MdiChildren.OfType<EditNestForm>().FirstOrDefault(f => first != null && ReferenceEquals(f.Nest, first))?.Activate();
|
||||
BringForward();
|
||||
}
|
||||
|
||||
// A nest Make Nests is releasing is saved by the release; a Save now would make a second nest.
|
||||
private bool RefuseSaveDuringRelease(EditNestForm form)
|
||||
{
|
||||
if (!releaseTabs.Contains(form))
|
||||
return false;
|
||||
MessageBox.Show(this, $"'{form.Nest.Name}' is being released. Make Nests saves it when it finishes.",
|
||||
"Save", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void BringForward()
|
||||
{
|
||||
if (WindowState == FormWindowState.Minimized)
|
||||
WindowState = FormWindowState.Normal;
|
||||
Activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-1
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@@ -41,6 +41,34 @@ namespace OpenNest
|
||||
uint uFlags
|
||||
);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool FlashWindowEx(ref FLASHWINFO info);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct FLASHWINFO
|
||||
{
|
||||
public uint cbSize;
|
||||
public IntPtr hwnd;
|
||||
public uint dwFlags;
|
||||
public uint uCount;
|
||||
public uint dwTimeout;
|
||||
}
|
||||
|
||||
/// <summary>Flashes the window's taskbar button until the window comes to the foreground.</summary>
|
||||
public static void FlashTaskbarUntilActive(IntPtr hWnd)
|
||||
{
|
||||
const uint FLASHW_TRAY = 0x2;
|
||||
const uint FLASHW_TIMERNOFG = 0xC;
|
||||
var info = new FLASHWINFO
|
||||
{
|
||||
cbSize = (uint)Marshal.SizeOf<FLASHWINFO>(),
|
||||
hwnd = hWnd,
|
||||
dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG,
|
||||
};
|
||||
FlashWindowEx(ref info);
|
||||
}
|
||||
|
||||
#region Constants
|
||||
|
||||
public const int GWL_EXSTYLE = -20;
|
||||
|
||||
+19
-1
@@ -603,6 +603,24 @@ Auto Nest". The row lists each group's sheets and utilization, and any
|
||||
copies left unplaced, which stay as remaining quantity. Unticked, new nests are
|
||||
saved without sheets and merged drafts are left cleared for Auto Nest.
|
||||
|
||||
Ticked, the nesting runs where the operator can watch it. When Make Nests
|
||||
starts, the drawing sets window minimizes and the main window comes forward.
|
||||
Each group's nest opens in its own main-window tab as soon as it is built, before
|
||||
it is saved, and Auto Nest runs on it there with the usual progress window and
|
||||
live plan strip (`AutoNestCommand.SolveAsync`), one nest at a time in plan order,
|
||||
switching to each tab in turn. The release keeps the layout as headless
|
||||
`NestRenest` would (only a valid one, with no Keep or validation review). Stop in
|
||||
the progress window (or switching tabs, as for Auto Nest) stops the whole release,
|
||||
as the panel's Stop does: nothing is saved. Until Make Nests finishes the main
|
||||
window's Nest and Plate menus are locked and Save is refused in those tabs, since
|
||||
the release saves them. After saving, each tab is bound to its server record
|
||||
(no second tab opens). A tab whose nest was not saved (a later group refused or
|
||||
failed, or the release stopped before upload) is **closed**, since it is not part
|
||||
of any release. The main window stays in front with the saved nests, and the
|
||||
drawing sets window keeps the outcomes; its taskbar button flashes when a group
|
||||
failed or was refused or the release was not recorded. A renest that throws
|
||||
still leaves the nest without sheets and a note, and the release goes on.
|
||||
|
||||
**Make Nests (n)**, in bold, with n the groups ticked, is the only step that
|
||||
writes; **Clear** empties the panel. It runs `OpenNest.Data.SetReleaseSession`:
|
||||
every ticked group's new nest is made as File > New would (the server's next nest
|
||||
@@ -611,7 +629,7 @@ plate size and spacings converted (25.4 mm per inch) when those differ; the nest
|
||||
takes the group's material, grade and thickness, the release's customer when
|
||||
there is one, and the **Draft** status. A merged draft is saved only while nobody
|
||||
else has saved it since it was listed. Each created or merged nest opens in its
|
||||
own tab of the main window, bound to its server record, the main window comes
|
||||
own tab of the main window (or keeps the tab it was nested in), bound to its server record, the main window comes
|
||||
forward, and the release (with its notes) is recorded. A release with drawings
|
||||
on their own first checks that the server can record them (see below) and, on an
|
||||
older server, saves nothing and says so. Each row then shows its group's outcome:
|
||||
|
||||
Reference in new issue
Block a user