diff --git a/OpenNest.Avalonia/MainWindow.axaml b/OpenNest.Avalonia/MainWindow.axaml
index de1e2faf..84af0654 100644
--- a/OpenNest.Avalonia/MainWindow.axaml
+++ b/OpenNest.Avalonia/MainWindow.axaml
@@ -10,7 +10,7 @@
-
+
diff --git a/OpenNest.Avalonia/MainWindow.axaml.cs b/OpenNest.Avalonia/MainWindow.axaml.cs
index 0ed9e87f..ab79f4ad 100644
--- a/OpenNest.Avalonia/MainWindow.axaml.cs
+++ b/OpenNest.Avalonia/MainWindow.axaml.cs
@@ -426,25 +426,25 @@ namespace OpenNest.Avalonia
writingFile = true;
try
{
- var path = await PickNestFileAsync(tab, "Export .nest");
+ var path = await PickNestFileAsync(tab, "Save Copy as .nest File");
if (path == null || IsNesting(tab, "exporting"))
return;
try
{
// Export never changes the nest's file, name or unsaved mark.
tabs.ExportCopy(tab, path);
- ShowNotice($"Exported .nest to {path}");
+ ShowNotice($"Saved a copy to {path}");
}
catch (SaveTargetRefusedException ex)
{
await ConfirmDialog.ShowAsync(
- this, "Export .nest", $"The nest was not exported. {ex.Message}");
+ this, "Save Copy", $"The copy was not saved. {ex.Message}");
}
catch (Exception ex)
{
- await ConfirmDialog.ShowAsync(this, "Export .nest",
- $"The nest was not exported.\n\n{path}\n\n{ex.Message}\n\nChoose "
- + "File > Export .nest again to pick another folder or name.");
+ await ConfirmDialog.ShowAsync(this, "Save Copy",
+ $"The copy was not saved.\n\n{path}\n\n{ex.Message}\n\nChoose "
+ + "File > Save Copy as .nest File again to pick another folder or name.");
}
}
finally
diff --git a/OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs b/OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs
index 708cd253..20065b29 100644
--- a/OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs
+++ b/OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs
@@ -47,25 +47,21 @@ public class NestReportExportTests
{
using var host = new TestMainForm();
host.Show();
- var item = Menu(host, "mnuFileExportNestReport");
var view = Menu(host, "mnuFileViewNestReport");
- Assert.False(item.Enabled);
Assert.False(view.Enabled);
using var form = new EditNestForm(new Nest("enabled job")) { MdiParent = host };
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
form.Show();
- Assert.True(item.Enabled);
Assert.True(view.Enabled);
form.Close();
host.Activate();
- Assert.False(item.Enabled);
Assert.False(view.Enabled);
});
[Fact]
- public void JobBusyRejectsBeforeAnyDialog() => RunSta(() =>
+ public void JobBusyRejectsBeforeRendering() => RunSta(() =>
{
using var host = new TestMainForm();
host.Show();
@@ -75,14 +71,14 @@ public class NestReportExportTests
SetField(host, "nestingInProgress", true);
try
{
- Menu(host, "mnuFileExportNestReport").PerformClick();
+ Menu(host, "mnuFileViewNestReport").PerformClick();
}
finally
{
SetField(host, "nestingInProgress", false);
}
- Assert.Empty(host.DialogRequests);
+ Assert.Empty(host.Viewed);
var failure = Assert.Single(host.Failures);
Assert.Contains("busy job", failure);
Assert.Contains("Finish or cancel", failure);
@@ -99,8 +95,8 @@ public class NestReportExportTests
SetBackingField(form.PlateView, "IsFillInProgress", true);
try
{
- Menu(host, "mnuFileExportNestReport").PerformClick();
- Assert.Empty(host.DialogRequests);
+ Menu(host, "mnuFileViewNestReport").PerformClick();
+ Assert.Empty(host.Viewed);
Assert.Contains("Finish or cancel", Assert.Single(host.Failures));
}
finally
@@ -108,9 +104,9 @@ public class NestReportExportTests
SetBackingField(form.PlateView, "IsFillInProgress", false);
}
- // Cleared guard: the command now reaches the (test-intercepted) dialog.
- Menu(host, "mnuFileExportNestReport").PerformClick();
- Assert.Single(host.DialogRequests);
+ // Cleared guard: the command now reaches the (test-intercepted) viewer.
+ Menu(host, "mnuFileViewNestReport").PerformClick();
+ NestReportViewerForm.DeleteReportFolder(Assert.Single(host.Viewed));
});
[Fact]
@@ -126,8 +122,8 @@ public class NestReportExportTests
Assert.True(form.PlateView.Actions.CurrentAction!.IsBusy());
try
{
- Menu(host, "mnuFileExportNestReport").PerformClick();
- Assert.Empty(host.DialogRequests);
+ Menu(host, "mnuFileViewNestReport").PerformClick();
+ Assert.Empty(host.Viewed);
Assert.Contains("Finish or cancel", Assert.Single(host.Failures));
}
finally
@@ -138,7 +134,7 @@ public class NestReportExportTests
});
[Fact]
- public void InvalidNestNameRejectsBeforeAnyDialog() => RunSta(() =>
+ public void InvalidNestNameRejectsBeforeRendering() => RunSta(() =>
{
using var host = new TestMainForm();
host.Show();
@@ -146,87 +142,58 @@ public class NestReportExportTests
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
form.Show();
- Menu(host, "mnuFileExportNestReport").PerformClick();
+ Menu(host, "mnuFileViewNestReport").PerformClick();
- Assert.Empty(host.DialogRequests);
+ Assert.Empty(host.Viewed);
Assert.Contains("bad/name", Assert.Single(host.Failures));
});
[Fact]
- public void CancelWritesNothingAndNotifiesNothing() => RunSta(() =>
+ public void ViewLeavesJobStateUntouched() => RunSta(() =>
{
- using var host = new TestMainForm { NextDialogPath = null };
+ using var host = new TestMainForm();
host.Show();
- using var form = new EditNestForm(CreateJob("cancel job")) { MdiParent = host };
- form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
- form.Show();
- var path = Path.Combine(Path.GetTempPath(), $"opennest-report-cancel-{Guid.NewGuid():N}.pdf");
-
- host.NextDialogPath = path;
- host.NextDialogResult = DialogResult.Cancel;
- Menu(host, "mnuFileExportNestReport").PerformClick();
-
- Assert.False(File.Exists(path));
- Assert.Empty(host.Failures);
- Assert.Empty(host.Successes);
- });
-
- [Fact]
- public void SuccessfulExportWritesPdfAndLeavesJobStateUntouched() => RunSta(() =>
- {
- using var host = new TestMainForm { NextDialogResult = DialogResult.OK };
- host.Show();
- var nest = CreateJob("export job");
+ var nest = CreateJob("state job");
using var form = new EditNestForm(nest) { MdiParent = host };
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
form.Show();
form.PlateManager.LoadAt(1);
// EnsureSentinel already appended a trailing empty plate behind the two loaded here.
- var plateCountBeforeExport = nest.Plates.Count;
- var path = Path.Combine(Path.GetTempPath(), $"opennest-report-ok-{Guid.NewGuid():N}.pdf");
+ var plateCountBeforeView = nest.Plates.Count;
+
+ Menu(host, "mnuFileViewNestReport").PerformClick();
+
+ var path = Assert.Single(host.Viewed);
try
{
- host.NextDialogPath = path;
- Menu(host, "mnuFileExportNestReport").PerformClick();
-
- Assert.Equal(path, Assert.Single(host.Successes));
- var bytes = File.ReadAllBytes(path);
- Assert.Equal("%PDF-", System.Text.Encoding.ASCII.GetString(bytes, 0, 5));
- Assert.True(bytes.Length > 1000);
// Selected plate and job contents are untouched by reporting.
Assert.Equal(1, form.PlateManager.CurrentIndex);
- Assert.Equal(plateCountBeforeExport, nest.Plates.Count);
+ Assert.Equal(plateCountBeforeView, nest.Plates.Count);
Assert.Single(nest.Plates[0].Parts);
Assert.Equal(2, nest.Drawings.Count);
}
finally
{
- File.Delete(path);
+ NestReportViewerForm.DeleteReportFolder(path);
}
});
[Fact]
public void RenderFailureLeavesExistingDestinationUnchanged() => RunSta(() =>
{
- using var host = new TestMainForm { NextDialogResult = DialogResult.OK };
- host.Show();
var nest = CreateJob("failure job");
// Outside the report's supported text contract: fails font validation before layout.
nest.Notes = "em\u2014dash";
- using var form = new EditNestForm(nest) { MdiParent = host };
- form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
- form.Show();
+ using var form = new EditNestForm(nest);
+ form.PlateView.SetOverlapAutoCheck(null);
+ var targets = form.CaptureReportTargets(() => false);
var path = Path.Combine(Path.GetTempPath(), $"opennest-report-fail-{Guid.NewGuid():N}.pdf");
var original = new byte[] { 1, 2, 3, 4 };
File.WriteAllBytes(path, original);
try
{
- host.NextDialogPath = path;
- Menu(host, "mnuFileExportNestReport").PerformClick();
+ Assert.ThrowsAny(() => form.WriteNestReport(targets, path, () => false));
- Assert.Empty(host.Successes);
- var failure = Assert.Single(host.Failures);
- Assert.Contains("failure job", failure);
Assert.Equal(original, File.ReadAllBytes(path));
Assert.Empty(Directory.GetFiles(Path.GetDirectoryName(path)!, ".*report-fail*.tmp"));
}
@@ -248,7 +215,6 @@ public class NestReportExportTests
Menu(host, "mnuFileViewNestReport").PerformClick();
- Assert.Empty(host.DialogRequests);
Assert.Empty(host.Failures);
var path = Assert.Single(host.Viewed);
try
@@ -421,28 +387,16 @@ public class NestReportExportTests
// Same reflection precedent PlateViewFillLifetimeTests uses for fill-lifetime state.
private sealed class TestMainForm : MainForm
{
- public List DialogRequests { get; } = [];
- public string? NextDialogPath { get; set; }
- public DialogResult NextDialogResult { get; set; } = DialogResult.OK;
public List Failures { get; } = [];
- public List Successes { get; } = [];
public List Viewed { get; } = [];
// No startup migrations/automatic new document, and no settings save at close.
protected override void OnLoad(EventArgs e) { }
protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { }
- internal override string? ShowReportSaveDialog(string suggestedFileName)
- {
- DialogRequests.Add(suggestedFileName);
- return NextDialogResult == DialogResult.OK ? NextDialogPath : null;
- }
-
internal override void ReportFailure(EditNestForm.NestReportTargets? targets, Exception error) =>
Failures.Add($"{targets?.Nest.Name}|{error.Message}");
- internal override void ReportSuccess(string destination) => Successes.Add(destination);
-
internal override void ShowReportViewer(string pdfPath, EditNestForm.NestReportTargets targets) =>
Viewed.Add(pdfPath);
diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs
index 754bbae4..853f1385 100644
--- a/OpenNest/Forms/EditNestForm.cs
+++ b/OpenNest/Forms/EditNestForm.cs
@@ -815,16 +815,16 @@ namespace OpenNest.Forms
if (isJobBusy() || IsReportTargetBusy(nest))
throw new InvalidOperationException(
- $"Finish or cancel the current nesting or plate action before exporting the report for '{nest.Name}'."
+ $"Finish or cancel the current nesting or plate action before creating the report for '{nest.Name}'."
);
return new NestReportTargets(nest, $"{nest.Name}.report.pdf");
}
///
- /// Revalidate the fixed target after the dialog closed, then synchronously capture the
- /// snapshot on the UI thread and write it. The dialog pumps messages, so an async fill
- /// could have committed or started meanwhile; once capture begins nothing yields.
+ /// Revalidate the fixed target, then synchronously capture the snapshot on the UI thread
+ /// and write it. A caller that pumps messages between capture and write (a dialog) lets an
+ /// async fill commit or start meanwhile; once capture begins nothing yields.
///
internal void WriteNestReport(
NestReportTargets targets,
diff --git a/OpenNest/Forms/MainForm.Designer.cs b/OpenNest/Forms/MainForm.Designer.cs
index 80cf6547..5d8970bc 100644
--- a/OpenNest/Forms/MainForm.Designer.cs
+++ b/OpenNest/Forms/MainForm.Designer.cs
@@ -51,7 +51,7 @@ namespace OpenNest.Forms
mnuFileExport = new System.Windows.Forms.ToolStripMenuItem();
mnuFileExportAll = new System.Windows.Forms.ToolStripMenuItem();
mnuFileViewNestReport = new System.Windows.Forms.ToolStripMenuItem();
- mnuFileExportNestReport = new System.Windows.Forms.ToolStripMenuItem();
+ mnuFileReportsSeparator = new System.Windows.Forms.ToolStripSeparator();
mnuFileViewPartPrintouts = new System.Windows.Forms.ToolStripMenuItem();
toolStripMenuItem3 = new System.Windows.Forms.ToolStripSeparator();
mnuFileExit = new System.Windows.Forms.ToolStripMenuItem();
@@ -222,7 +222,7 @@ namespace OpenNest.Forms
//
// mnuFile
//
- mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, mnuFileDrawingLibrary, mnuFileDrawingSets, mnuFileReleases, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileViewNestReport, mnuFileExportNestReport, mnuFileViewPartPrintouts, toolStripMenuItem3, mnuFileExit });
+ mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, mnuFileDrawingLibrary, mnuFileDrawingSets, mnuFileReleases, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileReportsSeparator, mnuFileViewNestReport, mnuFileViewPartPrintouts, toolStripMenuItem3, mnuFileExit });
mnuFile.Name = "mnuFile";
mnuFile.DropDownOpening += FileMenu_Opening;
mnuFile.Size = new System.Drawing.Size(37, 20);
@@ -307,38 +307,35 @@ namespace OpenNest.Forms
//
mnuFileExport.Name = "mnuFileExport";
mnuFileExport.Size = new System.Drawing.Size(146, 22);
- mnuFileExport.Text = "Export";
+ mnuFileExport.Text = "Export Plate...";
mnuFileExport.Click += Export_Click;
//
// mnuFileExportAll
//
mnuFileExportAll.Name = "mnuFileExportAll";
mnuFileExportAll.Size = new System.Drawing.Size(146, 22);
- mnuFileExportAll.Text = "Export All";
+ mnuFileExportAll.Text = "Export All Plates...";
mnuFileExportAll.Click += ExportAll_Click;
//
+ // mnuFileReportsSeparator
+ //
+ mnuFileReportsSeparator.Name = "mnuFileReportsSeparator";
+ mnuFileReportsSeparator.Size = new System.Drawing.Size(143, 6);
+ //
// mnuFileViewNestReport
//
mnuFileViewNestReport.Enabled = false;
mnuFileViewNestReport.Name = "mnuFileViewNestReport";
mnuFileViewNestReport.Size = new System.Drawing.Size(146, 22);
- mnuFileViewNestReport.Text = "View Nest Report...";
+ mnuFileViewNestReport.Text = "Nest Report...";
mnuFileViewNestReport.Click += ViewNestReport_Click;
//
- // mnuFileExportNestReport
- //
- mnuFileExportNestReport.Enabled = false;
- mnuFileExportNestReport.Name = "mnuFileExportNestReport";
- mnuFileExportNestReport.Size = new System.Drawing.Size(146, 22);
- mnuFileExportNestReport.Text = "Export Nest Report...";
- mnuFileExportNestReport.Click += ExportNestReport_Click;
- //
// mnuFileViewPartPrintouts
//
mnuFileViewPartPrintouts.Enabled = false;
mnuFileViewPartPrintouts.Name = "mnuFileViewPartPrintouts";
mnuFileViewPartPrintouts.Size = new System.Drawing.Size(146, 22);
- mnuFileViewPartPrintouts.Text = "View Part Printouts...";
+ mnuFileViewPartPrintouts.Text = "Part Printouts...";
mnuFileViewPartPrintouts.Click += ViewPartPrintouts_Click;
//
// toolStripMenuItem3
@@ -1538,7 +1535,7 @@ namespace OpenNest.Forms
private System.Windows.Forms.ToolStripStatusLabel plateUtilStatusLabel;
private System.Windows.Forms.ToolStripMenuItem mnuFileExportAll;
private System.Windows.Forms.ToolStripMenuItem mnuFileViewNestReport;
- private System.Windows.Forms.ToolStripMenuItem mnuFileExportNestReport;
+ private System.Windows.Forms.ToolStripSeparator mnuFileReportsSeparator;
private System.Windows.Forms.ToolStripMenuItem mnuFileViewPartPrintouts;
private System.Windows.Forms.ToolStripMenuItem openNestToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem pEPToolStripMenuItem;
diff --git a/OpenNest/Forms/MainForm.File.cs b/OpenNest/Forms/MainForm.File.cs
index 6367dee4..297f58b0 100644
--- a/OpenNest/Forms/MainForm.File.cs
+++ b/OpenNest/Forms/MainForm.File.cs
@@ -304,7 +304,7 @@ namespace OpenNest.Forms
{
// Export never changes the document's save path, remote id, or nest name.
new NestWriter(form.Nest) { BestFitStore = BestFitCacheStore.Shared }.Write(dlg.FileName);
- statusLabel1.Text = $"Exported .nest to {dlg.FileName}";
+ statusLabel1.Text = $"Saved a copy to {dlg.FileName}";
}
catch (Exception ex)
{
@@ -316,23 +316,10 @@ namespace OpenNest.Forms
private void ExportAll_Click(object sender, EventArgs e) => WithActive(f => f.ExportAll());
- private void ExportNestReport_Click(object sender, EventArgs e)
- {
- var form = activeForm;
- if (form == null || !TryCaptureReportTargets(form, out var targets))
- return;
-
- var destination = ShowReportSaveDialog(targets.SuggestedFileName);
- if (destination == null)
- return; // Cancel: nothing written, job untouched.
-
- if (!TryWriteNestReport(form, targets, destination))
- return;
- statusLabel1.Text = $"Saved nest report to {destination}";
- ReportSuccess(destination);
- }
-
- /// Renders the report to a private temporary file and opens it in a viewer to save or print.
+ ///
+ /// Renders the report to a private temporary file and opens it in a viewer to save or
+ /// print. The viewer's Save As is the only way to keep the PDF, with or without WebView2.
+ ///
private void ViewNestReport_Click(object sender, EventArgs e)
{
var form = activeForm;
@@ -467,32 +454,13 @@ namespace OpenNest.Forms
viewer.Show(this);
}
- /// Returns the chosen path, or null on cancel. Overwrite consent is the dialog prompt.
- internal virtual string ShowReportSaveDialog(string suggestedFileName)
- {
- using var dlg = new SaveFileDialog
- {
- Filter = "PDF report (*.pdf)|*.pdf",
- FileName = suggestedFileName,
- AddExtension = true,
- DefaultExt = ".pdf",
- OverwritePrompt = true,
- };
- return dlg.ShowDialog(this) == DialogResult.OK ? dlg.FileName : null;
- }
-
/// Failure notice naming the fixed target; internal so tests observe it without dialogs.
internal virtual void ReportFailure(
EditNestForm.NestReportTargets targets,
Exception error) =>
MessageBox.Show(this,
- $"Could not export the nest report{(targets == null ? "" : $" for '{targets.Nest.Name}'")}: {error.Message}",
- "Export Nest Report", MessageBoxButtons.OK, MessageBoxIcon.Warning);
-
- /// Success notice; internal so tests observe completion without dialogs.
- internal virtual void ReportSuccess(string destination) =>
- MessageBox.Show(this, $"Nest report saved to:\n\n{destination}", "Export Nest Report",
- MessageBoxButtons.OK, MessageBoxIcon.Information);
+ $"Could not create the nest report{(targets == null ? "" : $" for '{targets.Nest.Name}'")}: {error.Message}",
+ "Nest Report", MessageBoxButtons.OK, MessageBoxIcon.Warning);
private void Exit_Click(object sender, EventArgs e)
{
diff --git a/OpenNest/Forms/MainForm.Status.cs b/OpenNest/Forms/MainForm.Status.cs
index 6813c941..aea10692 100644
--- a/OpenNest/Forms/MainForm.Status.cs
+++ b/OpenNest/Forms/MainForm.Status.cs
@@ -22,8 +22,7 @@ namespace OpenNest.Forms
exportNestMenu.Enabled = hasValue;
mnuFileExport.Enabled = hasValue;
mnuFileExportAll.Enabled = hasValue;
- mnuFileExportNestReport.Enabled = hasValue && !storage.IsSaving;
- mnuFileViewNestReport.Enabled = mnuFileExportNestReport.Enabled;
+ mnuFileViewNestReport.Enabled = hasValue && !storage.IsSaving;
mnuFileViewPartPrintouts.Enabled = hasValue;
mnuEdit.Visible = hasValue;
mnuView.Visible = hasValue;
diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs
index da08dbce..daa79316 100644
--- a/OpenNest/Forms/MainForm.cs
+++ b/OpenNest/Forms/MainForm.cs
@@ -25,7 +25,7 @@ namespace OpenNest.Forms
private CancellationTokenSource nestingCts;
private readonly NestStorageController storage;
private readonly ToolStripMenuItem storageModeMenu = new("Storage Mode...");
- private readonly ToolStripMenuItem exportNestMenu = new("Export .nest...");
+ private readonly ToolStripMenuItem exportNestMenu = new("Save Copy as .nest File...");
private readonly ToolStripButton btnCutPlanner;
private readonly ToolStripButton btnSimulateCutting;
private readonly ToolStripButton btnSequenceByPath;
@@ -131,8 +131,11 @@ namespace OpenNest.Forms
toolStrip1.Items.Add(postButton);
storageModeMenu.Click += StorageMode_Click;
exportNestMenu.Click += ExportNest_Click;
- mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileExport), exportNestMenu);
- mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileExit), storageModeMenu);
+ // A local .nest copy is a save, not a plate export, so it sits with Save and Save As.
+ mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileSaveAs) + 1, exportNestMenu);
+ var exitIndex = mnuFile.DropDownItems.IndexOf(mnuFileExit);
+ mnuFile.DropDownItems.Insert(exitIndex, new ToolStripSeparator());
+ mnuFile.DropDownItems.Insert(exitIndex, storageModeMenu);
InitializeAlignMenu();
InitializeHelpMenu();
UpdateOverlapMenu();
diff --git a/docs/nest-reports.md b/docs/nest-reports.md
index d781b5cb..20b83e55 100644
--- a/docs/nest-reports.md
+++ b/docs/nest-reports.md
@@ -1,23 +1,23 @@
# Nest report PDF export
-Desktop: **File -> Export Nest Report...** writes `.report.pdf` for the
-active nest's whole job through an overwrite-confirming save dialog. The command
-is disabled without an open document and during a background database save, and
-it refuses to run while whole-job nesting, an open progress window, interactive
-fill or a busy plate action holds the nest or any second window sharing it. After
-the dialog closes, the target and those conditions are revalidated; the snapshot
-is then captured synchronously on the UI thread and only that detached snapshot
-reaches the renderer. Export never changes the selected plate, dirty state,
-timestamps or quantities, and it never touches post selection, verification or
-CNC output. There are no persisted report settings or templates.
+Desktop: **File -> Nest Report...** renders `.report.pdf` for the
+active nest's whole job. The command is disabled without an open document and
+during a background database save, and it refuses to run while whole-job
+nesting, an open progress window, interactive fill or a busy plate action holds
+the nest or any second window sharing it. The target and those conditions are
+revalidated before the write; the snapshot is then captured synchronously on the
+UI thread and only that detached snapshot reaches the renderer. Reporting never
+changes the selected plate, dirty state, timestamps or quantities, and it never
+touches post selection, verification or CNC output. There are no persisted report
+settings or templates.
-**File -> View Nest Report...** runs the same guards and capture without a dialog,
-renders to a private folder under `%TEMP%\OpenNest\Reports\` and opens a modeless
-viewer window (WebView2's PDF viewer, sharing Help's WebView2 data folder) with
-**Save As...**, which copies the rendered file to a chosen path, and **Print...**,
-the WebView2 print dialog. Closing the window deletes its temporary folder; a
-render failure opens no viewer and leaves no folder. Without the WebView2 Runtime
-the window still offers Save As.
+The report renders to a private folder under `%TEMP%\OpenNest\Reports\` and opens
+a modeless viewer window (WebView2's PDF viewer, sharing Help's WebView2 data
+folder) with **Save As...**, which copies the rendered file to a chosen path, and
+**Print...**, the WebView2 print dialog. Save As is the only way to keep the PDF;
+there is no separate direct-save command. Closing the window deletes its temporary
+folder; a render failure opens no viewer and leaves no folder. Without the WebView2
+Runtime the window still offers Save As.
The same two calls back the command and any future integration:
@@ -31,7 +31,7 @@ the selected plate, or certify that a layout passed geometry/pre-post checks.
## Part printouts
-**File -> View Part Printouts...** prints every non-cutoff drawing of the active
+**File -> Part Printouts...** prints every non-cutoff drawing of the active
nest; **View Part Printout...** on the drawing list's right-click menu prints the
selected one. Both render to `%TEMP%\OpenNest\Reports\` and open the report viewer,
suggesting `.printout.pdf` or `.printout.pdf`. Each part
@@ -191,7 +191,7 @@ concurrent calls cannot lay out text differently from a sequential export.
## Estimated cut time
-When the caller passes `CutParameters` (the desktop export always does, from the
+When the caller passes `CutParameters` (the desktop always does, from the
settings last used in Calculate Cut Time or the simulator), each plate gets an
estimated time per copy from `CutSimulation`, plus clamp-removal minutes, the same
as Calculate Cut Time (see [cut-time estimates](cut-time-estimates.md)). The summary
@@ -253,10 +253,9 @@ Pixel inspection supplements geometry assertions; it is not a CNC-validation
result.
Windows compilation is not runtime acceptance. `OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs`
-covers enablement, busy/cancel/failure/success adapter behavior, but it only
-compiles on Linux. Windows runtime acceptance still owes: exporting a real nest
+covers enablement, busy/failure/viewer adapter behavior, but it only
+compiles on Linux. Windows runtime acceptance still owes: reporting a real nest
(and the packaged application, so bundled fonts/notices are verified), checking
page sizes/labels/copies and embedded fonts with `pdffonts`, a tabbed part with a
-hole, opening and printing the PDF, and exercising fill/nesting rejection, cancel,
-overwrite and invalid-path behavior with job state and any existing destination
-unchanged.
+hole, saving and printing the PDF from the viewer, and exercising fill/nesting
+rejection with job state unchanged.
diff --git a/docs/nest-storage.md b/docs/nest-storage.md
index d9d189f8..29a04abd 100644
--- a/docs/nest-storage.md
+++ b/docs/nest-storage.md
@@ -44,9 +44,9 @@ The desktop app can save nests two ways:
the record from ordinary browsing but retains every archive; **Deleted nests...**
opens an explicit recovery view. Neither action purges data. An old desktop
without a revision precondition cannot update or delete on an upgraded server.
- A separate **File > Export .nest...** command is always available (in both
- modes) for producing a local file to share or back up. Export does not change
- the document's file save path or database record association.
+ A separate **File > Save Copy as .nest File...** command is always available
+ (in both modes) for producing a local file to share or back up. The copy does
+ not change the document's file save path or database record association.
Dragging one or more `.nest` files onto the main window (or onto an open
plate) opens them from disk directly, in either mode — this is the only way