refactor(file-menu): regroup the export and report commands

The File menu ran six output commands together with no separators, and
"Export" / "Export All" didn't say what they exported. Group them by what
they produce:

- Save Copy as .nest File... (was Export .nest...) moves next to Save As,
  since it writes the nest file rather than exporting a plate.
- Export Plate... / Export All Plates... (were Export / Export All) get their
  own group and the "..." their save dialogs call for.
- Nest Report... / Part Printouts... (were View ...) form a reports group.
- Storage Mode... gets its own separator above Exit.

Export Nest Report... is removed: the report viewer already has Save As
(available even without WebView2), so it was a second path to the same PDF.
Its guard and failure tests now drive the viewer path, and the atomic-write
test calls WriteNestReport directly. The Avalonia prototype's matching
command is renamed too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-12 08:41:12 -05:00
1 parent 755f021092
commit ecd0bba4eb
10 files changed
+90 -170

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@
<Separator />
<MenuItem x:Name="SaveMenuItem" Header="_Save" InputGesture="Ctrl+S" Click="OnSave" />
<MenuItem x:Name="SaveAsMenuItem" Header="Save _As..." Click="OnSaveAs" />
<MenuItem x:Name="ExportNestMenuItem" Header="_Export .nest..." Click="OnExportNest" />
<MenuItem x:Name="ExportNestMenuItem" Header="Save _Copy as .nest File..." Click="OnExportNest" />
<Separator />
<MenuItem x:Name="CloseMenuItem" Header="_Close" Click="OnCloseNest" />
<MenuItem x:Name="CloseAllMenuItem" Header="Close A_ll" Click="OnCloseAll" />
+6 -6
View File
@@ -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
@@ -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<Exception>(() => 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<string> DialogRequests { get; } = [];
public string? NextDialogPath { get; set; }
public DialogResult NextDialogResult { get; set; } = DialogResult.OK;
public List<string> Failures { get; } = [];
public List<string> Successes { get; } = [];
public List<string> 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);
+4 -4
View File
@@ -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");
}
/// <summary>
/// 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.
/// </summary>
internal void WriteNestReport(
NestReportTargets targets,
+12 -15
View File
@@ -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;
+7 -39
View File
@@ -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);
}
/// <summary>Renders the report to a private temporary file and opens it in a viewer to save or print.</summary>
/// <summary>
/// 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.
/// </summary>
private void ViewNestReport_Click(object sender, EventArgs e)
{
var form = activeForm;
@@ -467,32 +454,13 @@ namespace OpenNest.Forms
viewer.Show(this);
}
/// <summary>Returns the chosen path, or null on cancel. Overwrite consent is the dialog prompt.</summary>
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;
}
/// <summary>Failure notice naming the fixed target; internal so tests observe it without dialogs.</summary>
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);
/// <summary>Success notice; internal so tests observe completion without dialogs.</summary>
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)
{
+1 -2
View File
@@ -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;
+6 -3
View File
@@ -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();
+23 -24
View File
@@ -1,23 +1,23 @@
# Nest report PDF export
Desktop: **File -> Export Nest Report...** writes `<nest-name>.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 `<nest-name>.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 `<nest-name>.printout.pdf` or `<drawing-name>.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.
+3 -3
View File
@@ -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