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