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));