From 3cae257fb17c79741c732e2150cb6257d9f1c9c2 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 17 Oct 2026 21:07:13 -0400 Subject: [PATCH] refactor(ui): extract MainForm Auto Nest, post and storage workflows Moves the long workflow handlers into OpenNest/Forms/Commands: AutoNestCommand (dialog, owned modal progress, solve, validation review, commit and result notices), PostCommand (prepare, configure, verify, save dialog, multi-file overwrite confirmation) and NestStorageController (database open/save/save-as with reconciliation and storage errors). MainForm keeps the busy guards, the nesting lockout and nestingCts, and reacts to the controller's StateChanged to lock storage settings and saving. The characterization test now flips the controller's IsSaving instead of the removed databaseSaveInProgress field. Co-Authored-By: Claude Opus 5.5 --- .../Forms/MainFormCharacterizationTests.cs | 8 +- OpenNest/Forms/Commands/AutoNestCommand.cs | 196 +++++++++++++++ .../Forms/Commands/NestStorageController.cs | 156 ++++++++++++ OpenNest/Forms/Commands/PostCommand.cs | 122 ++++++++++ OpenNest/Forms/MainForm.File.cs | 120 +--------- OpenNest/Forms/MainForm.Guards.cs | 2 +- OpenNest/Forms/MainForm.Nest.cs | 224 +----------------- OpenNest/Forms/MainForm.Status.cs | 10 +- OpenNest/Forms/MainForm.cs | 6 +- 9 files changed, 510 insertions(+), 334 deletions(-) create mode 100644 OpenNest/Forms/Commands/AutoNestCommand.cs create mode 100644 OpenNest/Forms/Commands/NestStorageController.cs create mode 100644 OpenNest/Forms/Commands/PostCommand.cs diff --git a/OpenNest.WinForms.Tests/Forms/MainFormCharacterizationTests.cs b/OpenNest.WinForms.Tests/Forms/MainFormCharacterizationTests.cs index 919958b5..58008339 100644 --- a/OpenNest.WinForms.Tests/Forms/MainFormCharacterizationTests.cs +++ b/OpenNest.WinForms.Tests/Forms/MainFormCharacterizationTests.cs @@ -59,9 +59,10 @@ public class MainFormCharacterizationTests SetField(host, "nestingInProgress", false); // A database save only blocks the report snapshot. - SetField(host, "databaseSaveInProgress", true); + var storage = GetField(host, "storage"); + SetBackingField(storage, "IsSaving", true); Assert.Equal(new Guards(false, false, false, true), Read(host, editor, views)); - SetField(host, "databaseSaveInProgress", false); + SetBackingField(storage, "IsSaving", false); // A fill on a view of the nest blocks the plate guards but not the report. SetBackingField(editor.PlateView, "IsFillInProgress", true); @@ -102,6 +103,9 @@ public class MainFormCharacterizationTests private static object Invoke(MainForm host, string name, params object[] args) => typeof(MainForm).GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(host, args)!; + private static object GetField(MainForm host, string name) => + typeof(MainForm).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(host)!; + private static void SetField(MainForm host, string name, object value) => typeof(MainForm).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(host, value); diff --git a/OpenNest/Forms/Commands/AutoNestCommand.cs b/OpenNest/Forms/Commands/AutoNestCommand.cs new file mode 100644 index 00000000..36c3afa6 --- /dev/null +++ b/OpenNest/Forms/Commands/AutoNestCommand.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using OpenNest.Diagnostics; +using OpenNest.Engine; +using OpenNest.Engine.Jobs; + +namespace OpenNest.Forms.Commands +{ + /// + /// Auto Nest from the confirmed dialog to the committed plates: owned modal progress, + /// whole-job solve, validation review, commit to empty plates and the result notices. + /// The caller owns the busy guard, the nesting lockout and the cancellation source. + /// + internal sealed class AutoNestCommand + { + private const string Caption = "Auto Nest"; + + private readonly IWin32Window owner; + private readonly EditNestForm target; + private readonly List items; + private readonly List plateOptions; + private readonly double salvageRate; + private readonly double minRemnantSize; + + private AutoNestCommand(IWin32Window owner, EditNestForm target, List items, + List plateOptions, string engineName, double salvageRate, double minRemnantSize) + { + this.owner = owner; + this.target = target; + this.items = items; + this.plateOptions = plateOptions; + EngineName = engineName; + this.salvageRate = salvageRate; + this.minRemnantSize = minRemnantSize; + } + + /// The engine chosen in the dialog, or the current selection when it named none. + public string EngineName { get; } + + /// + /// Shows the Auto Nest dialog for . Returns null when it is + /// cancelled, nothing has a quantity, or the plate-size options are invalid. + /// + public static AutoNestCommand Prompt(IWin32Window owner, EditNestForm target) + { + using var form = new AutoNestForm(target.Nest); + if (target.Nest.PlateOptions.Count > 0) + form.LoadPlateOptions(target.Nest.PlateOptions, target.Nest.SalvageRate); + if (form.ShowDialog(owner) != DialogResult.OK) + return null; + + var items = form.GetNestItems(); + if (!items.Any(it => it.Quantity > 0)) + return null; + var plateOptions = default(List); + if (form.OptimizePlateSize && !form.TryGetPlateOptions(out plateOptions, out var stockError)) + { + MessageBox.Show(owner, stockError, Caption, MessageBoxButtons.OK, MessageBoxIcon.Warning); + return null; + } + + return new AutoNestCommand(owner, target, items, plateOptions, + form.EngineName ?? EngineSelection.EngineName, form.SalvageRate, form.MinRemnantSize); + } + + /// + /// Solves and commits the job. Cancelling discards the whole + /// proposal; failures are reported to the user rather than thrown. + /// + public async Task RunAsync(CancellationTokenSource cts) + { + using var progressForm = new NestProgressForm(cts, showPlateRow: true) + { + AllowAccept = false, + HoldOpenUntilCompleted = true, + }; + var receivingProgress = true; + var progress = new Progress(p => + { + if (!receivingProgress || target.IsDisposed || cts.IsCancellationRequested) + return; + progressForm.UpdateProgress(p); + if (p.IsOverallBest) + target.PlateView.SetActiveParts(p.BestParts); + target.PlateView.ActiveWorkArea = p.ActiveWorkArea; + }); + var jobProgress = JobEngineNest.CreateProgress(EngineName, progress); + try + { + var request = new NestPipelineRequest(EngineName, items, + NestStockBuilder.FromTemplate(target.PlateView.Plate, plateOptions), + new NestJobOptions(maxPlates: 100, + salvageRate: plateOptions?.Count > 0 ? salvageRate : 0, + minimumSalvageDimension: minRemnantSize)); + + async Task 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; + cts.Token.ThrowIfCancellationRequested(); + target.PlateView.ClearPreviewParts(); + target.PlateView.ActiveWorkArea = null; + + var allowInvalid = false; + if (!result.IsValid) + { + using var review = new NestValidationForm(result.Violations, result.CanKeep); + if (review.ShowDialog(owner) != DialogResult.OK) + return; + allowInvalid = true; + } + + var costs = result.IsValid && result.CanKeep ? NestCostSummary.FromAccepted(result) : null; + var applied = NestPipelineCommit.ApplyToEmptyPlates(result, target.PlateManager, + allowInvalid, cts.Token); + if (applied.Count > 0) + target.PlateManager.LoadAt(target.Nest.Plates.IndexOf(applied[0])); + if (plateOptions?.Count > 0) + { + target.Nest.PlateOptions = plateOptions; + target.Nest.SalvageRate = salvageRate; + } + target.PlateView.Invalidate(); + if (allowInvalid) + { + target.OverlapDisplay = OverlapDisplayMode.Both; + await target.CheckOverlapsAsync(); + } + ShowResultNotices(result, applied, costs); + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + // Stop discards the entire proposal, including engines that ignore cancellation. + } + catch (Exception ex) + { + MessageBox.Show(owner, $"Nesting error: {ex.Message}", Caption, + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + receivingProgress = false; + if (!target.IsDisposed) + { + target.PlateView.ClearPreviewParts(); + target.PlateView.ActiveWorkArea = null; + } + } + } + + private void ShowResultNotices(NestPipelineResult result, IReadOnlyList applied, + NestCostSummary costs) + { + // D8/D9: name every clamp Auto Nest pulled as a last resort and its added time. + var removalNotices = applied + .Where(plate => plate.RemovedClamps.Count > 0) + .Select(plate => $"Sheet {target.Nest.Plates.IndexOf(plate) + 1}: remove " + + $"{string.Join(", ", plate.RemovedClamps.OrderBy(id => id, StringComparer.Ordinal))} " + + $"(+{plate.ClampRemovalMinutes:0.#} min)") + .ToList(); + if (removalNotices.Count > 0) + MessageBox.Show(owner, + "Auto Nest removed clamps as a last resort so a part that would not " + + "otherwise fit could be nested:\n\n" + string.Join("\n", removalNotices), + Caption, MessageBoxButtons.OK, MessageBoxIcon.Information); + if (costs?.Basis == "supplied-cost") + MessageBox.Show(owner, $"Gross sheet cost: {costs.GrossTotal:G}\nEstimated salvage credit: {costs.SalvageCredit:G}\nNet score: {costs.NetScore:G}", + "Auto Nest Cost", MessageBoxButtons.OK, MessageBoxIcon.Information); + // Never drop demand silently: name every unplaced part and why (the drawing + // list keeps showing the remaining quantities after this closes). + if (result.Unplaced.Count > 0) + MessageBox.Show(owner, $"{EngineName} could not place every part:\n\n" + + string.Join("\n", result.Unplaced.Select(u => u.Describe())), + Caption, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } +} diff --git a/OpenNest/Forms/Commands/NestStorageController.cs b/OpenNest/Forms/Commands/NestStorageController.cs new file mode 100644 index 00000000..3113e6bd --- /dev/null +++ b/OpenNest/Forms/Commands/NestStorageController.cs @@ -0,0 +1,156 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using System.Windows.Forms; +using OpenNest.Data; +using OpenNest.Engine.BestFit; +using OpenNest.IO; + +namespace OpenNest.Forms.Commands +{ + /// + /// Nest-database open and save for the main window. Tracks the in-progress open and save + /// so the owner can lock storage settings and saving; fires + /// whenever either changes. + /// + internal sealed class NestStorageController + { + private readonly Form owner; + private readonly Action showStatus; + + public NestStorageController(Form owner, Action showStatus) + { + this.owner = owner; + this.showStatus = showStatus; + } + + public event EventHandler StateChanged; + + /// True while a background save serializes a live nest. + public bool IsSaving { get; private set; } + + public bool IsOpening { get; private set; } + + public bool IsBusy => IsSaving || IsOpening; + + /// + /// Lets the user pick a saved nest and reads its current revision; + /// receives the nest with its current metadata applied, the reader for its warnings, + /// and the snapshot that identifies the remote copy. + /// + public async Task OpenAsync(Action load) + { + try + { + var repository = NestStorage.CreateRepository(); + var serverUrl = NestStorage.Settings.ServerUrl; + using var dlg = new SavedNestsForm(repository, serverUrl); + if (dlg.ShowDialog(owner) != DialogResult.OK) + return; + + SetOpening(true); + var snapshot = await NestHistorySession.ReadCurrentAsync(repository, dlg.SelectedId, serverUrl); + using var stream = new MemoryStream(snapshot.Archive); + var reader = new NestReader(stream) { BestFitStore = BestFitCacheStore.Shared }; + var nest = reader.Read(); + // Metadata edits create revisions without rewriting the archive. Use their + // current editable fields rather than silently reverting them on the next save. + nest.Name = snapshot.Metadata.Name; + nest.Customer = snapshot.Metadata.Customer; + nest.Notes = snapshot.Metadata.Comments; + nest.Status = snapshot.Metadata.Status; + nest.MadeBy = snapshot.Metadata.MadeBy; + nest.DateLastModified = snapshot.Metadata.DateModified; + load(nest, reader, snapshot); + } + catch (Exception ex) + { + ShowError("open", ex); + } + finally + { + SetOpening(false); + } + } + + public Task SaveAsync(EditNestForm form) => SaveAsync(form, saveCopy: false); + + public async Task SaveAsAsync(EditNestForm form) + { + // A pending upload is reconciled without writing, so only ask for a name + // when this click will actually create the copy. + string copyName = null; + if (!form.Document.PendingCreateRequiresReconciliation) + { + using var dlg = new NestNameForm(form.Nest.Name); + if (dlg.ShowDialog(owner) != DialogResult.OK) + return; + copyName = dlg.NestName; + } + await SaveAsync(form, saveCopy: true, copyName); + } + + private async Task SaveAsync(EditNestForm form, bool saveCopy, string copyName = null) + { + var originalName = form.Nest.Name; + SetSaving(true); + try + { + var serverUrl = NestStorage.Settings.ServerUrl; + var repository = NestStorage.CreateRepository(); + if (form.Document.PendingCreateRequiresReconciliation) + { + var reconciled = await form.Document.ReconcilePendingCreateAsync(repository, serverUrl); + if (!owner.IsDisposed) + showStatus(reconciled is null + ? $"No pending nest was found. Review your work, then {(form.Document.PendingCreateWasCopy ? "Save As" : "Save")} again to retry the same ID." + : $"Found saved version {reconciled.Revision}. Review the current nest before saving again."); + return; // Never combine reconciliation with a write in the same click. + } + if (copyName != null) + form.Nest.Name = copyName; + var saved = await form.Document.SaveToDatabaseAsync( + repository, serverUrl, saveCopy); + if (!form.IsDisposed) + form.Text = form.Document.Name; + if (!owner.IsDisposed) + showStatus($"Saved {saved.Name} to nest database"); + } + catch (Exception ex) + { + // Keep the new name only while its copy may still be pending on the server; + // otherwise a later Save would rename the original nest. + if (copyName != null && !form.Document.PendingCreateWasCopy) + form.Nest.Name = originalName; + if (!owner.IsDisposed) + ShowError("save", ex); + } + finally + { + SetSaving(false); + } + } + + public void ShowError(string action, Exception ex) => + MessageBox.Show(owner, $"Could not {action} nest: {ex.Message}", + "Nest Storage", MessageBoxButtons.OK, MessageBoxIcon.Error); + + private void SetOpening(bool value) + { + IsOpening = value; + OnStateChanged(); + } + + private void SetSaving(bool value) + { + IsSaving = value; + OnStateChanged(); + } + + private void OnStateChanged() + { + if (!owner.IsDisposed) + StateChanged?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/OpenNest/Forms/Commands/PostCommand.cs b/OpenNest/Forms/Commands/PostCommand.cs new file mode 100644 index 00000000..320a2e03 --- /dev/null +++ b/OpenNest/Forms/Commands/PostCommand.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows.Forms; + +namespace OpenNest.Forms.Commands +{ + /// + /// Posts a nest: prepares and configures the post, runs pre-post verification, asks for + /// the output path and confirms multi-file overwrites before writing. The caller keeps the + /// nest stable (busy and cutting-inspection guards) for the command's whole lifetime. + /// + internal sealed class PostCommand + { + private readonly IWin32Window owner; + private readonly Nest nest; + private readonly IPostProcessor postProcessor; + + public PostCommand(IWin32Window owner, Nest nest, IPostProcessor postProcessor) + { + this.owner = owner; + this.nest = nest; + this.postProcessor = postProcessor; + } + + public void Run() + { + if (postProcessor is IPostProcessorNestAware nestAware) + nestAware.PrepareForNest(nest); + + if (postProcessor is IConfigurablePostProcessor configurable) + { + using var configForm = new PostProcessorConfigForm(configurable); + if (configForm.ShowDialog(owner) != DialogResult.OK) + return; + } + + // Verification runs fresh on every attempt; its consent never outlives this call. + using (var verification = new PostVerificationForm(nest, postProcessor)) + { + if (verification.ShowDialog(owner) != DialogResult.OK) + return; + } + + using var dialog = new SaveFileDialog(); + dialog.Filter = "CNC File (*.cnc) | *.cnc"; + dialog.FileName = nest.Name; + + if (dialog.ShowDialog(owner) != DialogResult.OK) + return; + + var path = dialog.FileName; + if (postProcessor is IMultiFilePostProcessor multiFile) + { + PostMultiFile(multiFile, path); + return; + } + + postProcessor.Post(nest, path); + } + + private void PostMultiFile(IMultiFilePostProcessor multiFile, string path) + { + var files = multiFile.GetOutputFiles(nest, path); + if (!ConfirmOverwrite(files, path)) + return; + + try + { + postProcessor.Post(nest, path); + } + catch (Exception ex) + when (ex is InvalidOperationException or IOException or UnauthorizedAccessException) + { + MessageBox.Show( + owner, + ex.Message, + postProcessor.Name, + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + return; + } + + if (files.Count > 1) + MessageBox.Show( + owner, + $"Saved {files.Count} programs, one per sheet:\n\n" + + string.Join("\n", files.Select(Path.GetFileName)) + + $"\n\nin {Path.GetDirectoryName(path)}", + postProcessor.Name, + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); + } + + /// + /// The save dialog only checks the chosen name; ask before replacing any + /// other existing file a multi-file post will write. + /// + private bool ConfirmOverwrite(IReadOnlyList files, string chosenPath) + { + var existing = files + .Where(f => File.Exists(f) && !string.Equals(f, chosenPath, StringComparison.OrdinalIgnoreCase)) + .ToList(); + if (existing.Count == 0) + return true; + + var answer = MessageBox.Show( + owner, + "These files already exist and will be replaced:\n\n" + + string.Join("\n", existing.Select(Path.GetFileName)) + + "\n\nReplace them?", + "Confirm Save", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning + ); + return answer == DialogResult.Yes; + } + } +} diff --git a/OpenNest/Forms/MainForm.File.cs b/OpenNest/Forms/MainForm.File.cs index 1f19f2b5..96a877da 100644 --- a/OpenNest/Forms/MainForm.File.cs +++ b/OpenNest/Forms/MainForm.File.cs @@ -1,10 +1,10 @@ using System; using System.IO; using System.Linq; -using System.Threading.Tasks; using System.Windows.Forms; using OpenNest.Data; using OpenNest.Engine.BestFit; +using OpenNest.Forms.Commands; using OpenNest.IO; using OpenNest.Properties; @@ -172,46 +172,12 @@ namespace OpenNest.Forms return; } - try + await storage.OpenAsync((nest, reader, snapshot) => { - var repository = NestStorage.CreateRepository(); - var serverUrl = NestStorage.Settings.ServerUrl; - using var dlg = new SavedNestsForm(repository, serverUrl); - if (dlg.ShowDialog(this) != DialogResult.OK) - return; - - databaseOpenInProgress = true; - storageModeMenu.Enabled = false; - mnuToolsOptions.Enabled = false; - var snapshot = await NestHistorySession.ReadCurrentAsync(repository, dlg.SelectedId, serverUrl); - using var stream = new MemoryStream(snapshot.Archive); - var reader = new NestReader(stream) { BestFitStore = BestFitCacheStore.Shared }; - var nest = reader.Read(); - // Metadata edits create revisions without rewriting the archive. Use their - // current editable fields rather than silently reverting them on the next save. - nest.Name = snapshot.Metadata.Name; - nest.Customer = snapshot.Metadata.Customer; - nest.Notes = snapshot.Metadata.Comments; - nest.Status = snapshot.Metadata.Status; - nest.MadeBy = snapshot.Metadata.MadeBy; - nest.DateLastModified = snapshot.Metadata.DateModified; LoadNest(nest, remoteId: snapshot.Metadata.Id, serverUrl: snapshot.ServerUrl, remoteRevision: snapshot.Metadata.Revision); ShowNestWarnings(reader); - } - catch (Exception ex) - { - ShowStorageError("open", ex); - } - finally - { - databaseOpenInProgress = false; - if (!IsDisposed) - { - storageModeMenu.Enabled = !databaseSaveInProgress; - mnuToolsOptions.Enabled = !databaseSaveInProgress; - } - } + }); } private void ShowNestWarnings(NestReader reader) @@ -231,86 +197,30 @@ namespace OpenNest.Forms private async void Save_Click(object sender, EventArgs e) { var form = activeForm; - if (form == null || databaseSaveInProgress) + if (form == null || storage.IsSaving) return; if (NestStorage.Settings.Mode == NestStorageMode.File) form.Save(); else - await SaveDatabaseAsync(form, saveCopy: false); + await storage.SaveAsync(form); } private async void SaveAs_Click(object sender, EventArgs e) { var form = activeForm; - if (form == null || databaseSaveInProgress) + if (form == null || storage.IsSaving) return; if (NestStorage.Settings.Mode == NestStorageMode.File) - { form.SaveAs(); - return; - } - - // A pending upload is reconciled without writing, so only ask for a name - // when this click will actually create the copy. - string copyName = null; - if (!form.Document.PendingCreateRequiresReconciliation) - { - using var dlg = new NestNameForm(form.Nest.Name); - if (dlg.ShowDialog(this) != DialogResult.OK) - return; - copyName = dlg.NestName; - } - await SaveDatabaseAsync(form, saveCopy: true, copyName); + else + await storage.SaveAsAsync(form); } - private async Task SaveDatabaseAsync(EditNestForm form, bool saveCopy, string copyName = null) + private void Storage_StateChanged(object sender, EventArgs e) { - var originalName = form.Nest.Name; - databaseSaveInProgress = true; - storageModeMenu.Enabled = false; - mnuToolsOptions.Enabled = false; + storageModeMenu.Enabled = !storage.IsBusy; + mnuToolsOptions.Enabled = !storage.IsBusy; EnableCheck(); - try - { - var serverUrl = NestStorage.Settings.ServerUrl; - var repository = NestStorage.CreateRepository(); - if (form.Document.PendingCreateRequiresReconciliation) - { - var reconciled = await form.Document.ReconcilePendingCreateAsync(repository, serverUrl); - if (!IsDisposed) - statusLabel1.Text = reconciled is null - ? $"No pending nest was found. Review your work, then {(form.Document.PendingCreateWasCopy ? "Save As" : "Save")} again to retry the same ID." - : $"Found saved version {reconciled.Revision}. Review the current nest before saving again."; - return; // Never combine reconciliation with a write in the same click. - } - if (copyName != null) - form.Nest.Name = copyName; - var saved = await form.Document.SaveToDatabaseAsync( - repository, serverUrl, saveCopy); - if (!form.IsDisposed) - form.Text = form.Document.Name; - if (!IsDisposed) - statusLabel1.Text = $"Saved {saved.Name} to nest database"; - } - catch (Exception ex) - { - // Keep the new name only while its copy may still be pending on the server; - // otherwise a later Save would rename the original nest. - if (copyName != null && !form.Document.PendingCreateWasCopy) - form.Nest.Name = originalName; - if (!IsDisposed) - ShowStorageError("save", ex); - } - finally - { - databaseSaveInProgress = false; - if (!IsDisposed) - { - storageModeMenu.Enabled = !databaseOpenInProgress; - mnuToolsOptions.Enabled = !databaseOpenInProgress; - EnableCheck(); - } - } } private void StorageMode_Click(object sender, EventArgs e) @@ -327,7 +237,7 @@ namespace OpenNest.Forms } catch (Exception ex) { - ShowStorageError("change storage mode", ex); + storage.ShowError("change storage mode", ex); } } @@ -347,14 +257,10 @@ namespace OpenNest.Forms } catch (Exception ex) { - ShowStorageError("export", ex); + storage.ShowError("export", ex); } } - private void ShowStorageError(string action, Exception ex) => - MessageBox.Show(this, $"Could not {action} nest: {ex.Message}", - "Nest Storage", MessageBoxButtons.OK, MessageBoxIcon.Error); - private void Export_Click(object sender, EventArgs e) { if (activeForm == null) diff --git a/OpenNest/Forms/MainForm.Guards.cs b/OpenNest/Forms/MainForm.Guards.cs index 184dfb46..3baa7fb5 100644 --- a/OpenNest/Forms/MainForm.Guards.cs +++ b/OpenNest/Forms/MainForm.Guards.cs @@ -57,7 +57,7 @@ namespace OpenNest.Forms /// nesting, an open progress window (a closed one can still await its commit), or a /// background database save serializing the live nest. /// - private bool IsNestJobBusy() => IsJobRunning || databaseSaveInProgress; + private bool IsNestJobBusy() => IsJobRunning || storage.IsSaving; /// /// Refuses with while plate work on diff --git a/OpenNest/Forms/MainForm.Nest.cs b/OpenNest/Forms/MainForm.Nest.cs index ae9f7cb7..f723d2a5 100644 --- a/OpenNest/Forms/MainForm.Nest.cs +++ b/OpenNest/Forms/MainForm.Nest.cs @@ -1,16 +1,11 @@ using System; -using System.Collections.Generic; using System.Drawing; -using System.IO; using System.Linq; using System.Threading; -using System.Threading.Tasks; using System.Windows.Forms; using OpenNest.Collections; -using OpenNest.Diagnostics; -using OpenNest.Engine; using OpenNest.Engine.Fill; -using OpenNest.Engine.Jobs; +using OpenNest.Forms.Commands; namespace OpenNest.Forms { @@ -179,138 +174,20 @@ namespace OpenNest.Forms "Finish or cancel the current plate operation before Auto Nest.", "Auto Nest")) return; - using var form = new AutoNestForm(target.Nest); - if (target.Nest.PlateOptions.Count > 0) - form.LoadPlateOptions(target.Nest.PlateOptions, target.Nest.SalvageRate); - if (form.ShowDialog(this) != DialogResult.OK) + var command = AutoNestCommand.Prompt(this, target); + if (command == null) return; - - var items = form.GetNestItems(); - if (!items.Any(it => it.Quantity > 0)) - return; - var plateOptions = default(List); - if (form.OptimizePlateSize && !form.TryGetPlateOptions(out plateOptions, out var stockError)) - { - MessageBox.Show(this, stockError, "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } - var engineName = form.EngineName ?? EngineSelection.EngineName; - EngineSelection.EngineName = engineName; - engineComboBox.SelectedItem = engineName; + EngineSelection.EngineName = command.EngineName; + engineComboBox.SelectedItem = command.EngineName; using var cts = new CancellationTokenSource(); nestingCts = cts; - using var progressForm = new NestProgressForm(cts, showPlateRow: true) - { - AllowAccept = false, - HoldOpenUntilCompleted = true, - }; - var receivingProgress = true; - var progress = new Progress(p => - { - if (!receivingProgress || target.IsDisposed || cts.IsCancellationRequested) - return; - progressForm.UpdateProgress(p); - if (p.IsOverallBest) - target.PlateView.SetActiveParts(p.BestParts); - target.PlateView.ActiveWorkArea = p.ActiveWorkArea; - }); - var jobProgress = JobEngineNest.CreateProgress(engineName, progress); SetNestingLockout(true); try { - var request = new NestPipelineRequest(engineName, items, - NestStockBuilder.FromTemplate(target.PlateView.Plate, plateOptions), - new NestJobOptions(maxPlates: 100, - salvageRate: plateOptions?.Count > 0 ? form.SalvageRate : 0, - minimumSalvageDimension: form.MinRemnantSize)); - - async Task 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(this); - var result = await solve; - cts.Token.ThrowIfCancellationRequested(); - target.PlateView.ClearPreviewParts(); - target.PlateView.ActiveWorkArea = null; - - var allowInvalid = false; - if (!result.IsValid) - { - using var review = new NestValidationForm(result.Violations, result.CanKeep); - if (review.ShowDialog(this) != DialogResult.OK) - return; - allowInvalid = true; - } - - var costs = result.IsValid && result.CanKeep ? NestCostSummary.FromAccepted(result) : null; - var applied = NestPipelineCommit.ApplyToEmptyPlates(result, target.PlateManager, - allowInvalid, cts.Token); - if (applied.Count > 0) - target.PlateManager.LoadAt(target.Nest.Plates.IndexOf(applied[0])); - if (plateOptions?.Count > 0) - { - target.Nest.PlateOptions = plateOptions; - target.Nest.SalvageRate = form.SalvageRate; - } - target.PlateView.Invalidate(); - if (allowInvalid) - { - target.OverlapDisplay = OverlapDisplayMode.Both; - await target.CheckOverlapsAsync(); - } - // D8/D9: name every clamp Auto Nest pulled as a last resort and its added time. - var removalNotices = applied - .Where(plate => plate.RemovedClamps.Count > 0) - .Select(plate => $"Sheet {target.Nest.Plates.IndexOf(plate) + 1}: remove " - + $"{string.Join(", ", plate.RemovedClamps.OrderBy(id => id, StringComparer.Ordinal))} " - + $"(+{plate.ClampRemovalMinutes:0.#} min)") - .ToList(); - if (removalNotices.Count > 0) - MessageBox.Show(this, - "Auto Nest removed clamps as a last resort so a part that would not " - + "otherwise fit could be nested:\n\n" + string.Join("\n", removalNotices), - "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information); - if (costs?.Basis == "supplied-cost") - MessageBox.Show(this, $"Gross sheet cost: {costs.GrossTotal:G}\nEstimated salvage credit: {costs.SalvageCredit:G}\nNet score: {costs.NetScore:G}", - "Auto Nest Cost", MessageBoxButtons.OK, MessageBoxIcon.Information); - // Never drop demand silently: name every unplaced part and why (the drawing - // list keeps showing the remaining quantities after this closes). - if (result.Unplaced.Count > 0) - MessageBox.Show(this, $"{engineName} could not place every part:\n\n" - + string.Join("\n", result.Unplaced.Select(u => u.Describe())), - "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - // Stop discards the entire proposal, including engines that ignore cancellation. - } - catch (Exception ex) - { - MessageBox.Show(this, $"Nesting error: {ex.Message}", "Auto Nest", - MessageBoxButtons.OK, MessageBoxIcon.Error); + await command.RunAsync(cts); } finally { - receivingProgress = false; - if (!target.IsDisposed) - { - target.PlateView.ClearPreviewParts(); - target.PlateView.ActiveWorkArea = null; - } SetNestingLockout(false); nestingCts = null; } @@ -359,94 +236,7 @@ namespace OpenNest.Forms "Verify Nest Before Posting")) return; - var nest = editForm.Nest; - - if (postProcessor is IPostProcessorNestAware nestAware) - nestAware.PrepareForNest(nest); - - if (postProcessor is IConfigurablePostProcessor configurable) - { - using var configForm = new PostProcessorConfigForm(configurable); - if (configForm.ShowDialog(this) != DialogResult.OK) - return; - } - - using (var verification = new PostVerificationForm(nest, postProcessor)) - { - if (verification.ShowDialog(this) != DialogResult.OK) - return; - } - - using var dialog = new SaveFileDialog(); - dialog.Filter = "CNC File (*.cnc) | *.cnc"; - dialog.FileName = nest.Name; - - if (dialog.ShowDialog(this) == DialogResult.OK) - { - var path = dialog.FileName; - - if (postProcessor is IMultiFilePostProcessor multiFile) - { - var files = multiFile.GetOutputFiles(nest, path); - if (!ConfirmOverwrite(files, path)) - return; - - try - { - postProcessor.Post(nest, path); - } - catch (Exception ex) - when (ex is InvalidOperationException or IOException or UnauthorizedAccessException) - { - MessageBox.Show( - this, - ex.Message, - postProcessor.Name, - MessageBoxButtons.OK, - MessageBoxIcon.Warning - ); - return; - } - - if (files.Count > 1) - MessageBox.Show( - this, - $"Saved {files.Count} programs, one per sheet:\n\n" - + string.Join("\n", files.Select(Path.GetFileName)) - + $"\n\nin {Path.GetDirectoryName(path)}", - postProcessor.Name, - MessageBoxButtons.OK, - MessageBoxIcon.Information - ); - return; - } - - postProcessor.Post(nest, path); - } - } - - /// - /// The save dialog only checks the chosen name; ask before replacing any - /// other existing file a multi-file post will write. - /// - private bool ConfirmOverwrite(IReadOnlyList files, string chosenPath) - { - var existing = files - .Where(f => File.Exists(f) && !string.Equals(f, chosenPath, StringComparison.OrdinalIgnoreCase)) - .ToList(); - if (existing.Count == 0) - return true; - - var answer = MessageBox.Show( - this, - "These files already exist and will be replaced:\n\n" - + string.Join("\n", existing.Select(Path.GetFileName)) - + "\n\nReplace them?", - "Confirm Save", - MessageBoxButtons.YesNo, - MessageBoxIcon.Warning - ); - return answer == DialogResult.Yes; + new PostCommand(this, editForm.Nest, postProcessor).Run(); } private void CalculateNestCutTime_Click(object sender, EventArgs e) diff --git a/OpenNest/Forms/MainForm.Status.cs b/OpenNest/Forms/MainForm.Status.cs index 1864fd57..e17b455a 100644 --- a/OpenNest/Forms/MainForm.Status.cs +++ b/OpenNest/Forms/MainForm.Status.cs @@ -15,14 +15,14 @@ namespace OpenNest.Forms var hasValue = activeForm != null; - mnuFileSave.Enabled = hasValue && !databaseSaveInProgress; - btnSave.Enabled = hasValue && !databaseSaveInProgress; - btnSaveAs.Enabled = hasValue && !databaseSaveInProgress; - mnuFileSaveAs.Enabled = hasValue && !databaseSaveInProgress; + mnuFileSave.Enabled = hasValue && !storage.IsSaving; + btnSave.Enabled = hasValue && !storage.IsSaving; + btnSaveAs.Enabled = hasValue && !storage.IsSaving; + mnuFileSaveAs.Enabled = hasValue && !storage.IsSaving; exportNestMenu.Enabled = hasValue; mnuFileExport.Enabled = hasValue; mnuFileExportAll.Enabled = hasValue; - mnuFileExportNestReport.Enabled = hasValue && !databaseSaveInProgress; + mnuFileExportNestReport.Enabled = hasValue && !storage.IsSaving; mnuEdit.Visible = hasValue; mnuView.Visible = hasValue; mnuNest.Visible = hasValue; diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 6aecadca..c56457f8 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -9,6 +9,7 @@ using System.Windows.Forms; using OpenNest.Data; using OpenNest.Engine.BestFit; using OpenNest.Engine.Jobs; +using OpenNest.Forms.Commands; using OpenNest.IO; using OpenNest.Properties; @@ -21,8 +22,7 @@ namespace OpenNest.Forms private bool clickUpdateLocation; private bool nestingInProgress; private CancellationTokenSource nestingCts; - private bool databaseSaveInProgress; - private bool databaseOpenInProgress; + private readonly NestStorageController storage; private readonly ToolStripMenuItem storageModeMenu = new("Storage Mode..."); private readonly ToolStripMenuItem exportNestMenu = new("Export .nest..."); private readonly ToolStripDropDownButton postButton = new() @@ -97,6 +97,8 @@ namespace OpenNest.Forms public MainForm() { + storage = new NestStorageController(this, text => statusLabel1.Text = text); + storage.StateChanged += Storage_StateChanged; InitializeComponent(); toolStrip1.Items.Add(CreateActionButton("btnCutPlanner", "Plan Cutting", Resources.cut_planner, PlatePlanCutting_Click));