feat(desktop): add File -> Export Nest Report command
Capture the report target and reject whole-job nesting, open progress windows, interactive fill and busy plate actions across every view sharing the nest; revalidate after the save dialog, capture the snapshot synchronously on the UI thread and render it through OpenNest.Reporting. Windows adapter tests cover enablement, guards, cancel, success and a write failure against an existing destination (compile-only on Linux; Windows runtime acceptance still owed).
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
// Reads Application.OpenForms for the busy guards, so it must not overlap fill-lifetime tests
|
||||
// that transiently open NestProgressForm.
|
||||
[Collection("Fill operation lifetime")]
|
||||
public class NestReportExportTests
|
||||
{
|
||||
[Fact]
|
||||
public void MenuFollowsActiveDocument() => RunSta(() =>
|
||||
{
|
||||
using var host = new TestMainForm();
|
||||
host.Show();
|
||||
var item = Menu(host, "mnuFileExportNestReport");
|
||||
Assert.False(item.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);
|
||||
|
||||
form.Close();
|
||||
host.Activate();
|
||||
Assert.False(item.Enabled);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void JobBusyRejectsBeforeAnyDialog() => RunSta(() =>
|
||||
{
|
||||
using var host = new TestMainForm();
|
||||
host.Show();
|
||||
using var form = new EditNestForm(new Nest("busy job")) { MdiParent = host };
|
||||
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
|
||||
form.Show();
|
||||
SetField(host, "nestingInProgress", true);
|
||||
try
|
||||
{
|
||||
Menu(host, "mnuFileExportNestReport").PerformClick();
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetField(host, "nestingInProgress", false);
|
||||
}
|
||||
|
||||
Assert.Empty(host.DialogRequests);
|
||||
var failure = Assert.Single(host.Failures);
|
||||
Assert.Contains("busy job", failure);
|
||||
Assert.Contains("Finish or cancel", failure);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void InteractiveFillOnAnyViewOfTheNestRejects() => RunSta(() =>
|
||||
{
|
||||
using var host = new TestMainForm();
|
||||
host.Show();
|
||||
using var form = new EditNestForm(new Nest("filling job")) { MdiParent = host };
|
||||
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
|
||||
form.Show();
|
||||
SetBackingField(form.PlateView, "IsFillInProgress", true);
|
||||
try
|
||||
{
|
||||
Menu(host, "mnuFileExportNestReport").PerformClick();
|
||||
Assert.Empty(host.DialogRequests);
|
||||
Assert.Contains("Finish or cancel", Assert.Single(host.Failures));
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetBackingField(form.PlateView, "IsFillInProgress", false);
|
||||
}
|
||||
|
||||
// Cleared guard: the command now reaches the (test-intercepted) dialog.
|
||||
Menu(host, "mnuFileExportNestReport").PerformClick();
|
||||
Assert.Single(host.DialogRequests);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void BusyPlateActionOnAnyViewOfTheNestRejects() => RunSta(() =>
|
||||
{
|
||||
using var host = new TestMainForm();
|
||||
host.Show();
|
||||
using var form = new EditNestForm(CreateJob("action job")) { MdiParent = host };
|
||||
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
|
||||
form.Show();
|
||||
// The default ActionSelect reports busy while parts are selected.
|
||||
form.PlateView.SelectAll();
|
||||
Assert.True(form.PlateView.Actions.CurrentAction!.IsBusy());
|
||||
try
|
||||
{
|
||||
Menu(host, "mnuFileExportNestReport").PerformClick();
|
||||
Assert.Empty(host.DialogRequests);
|
||||
Assert.Contains("Finish or cancel", Assert.Single(host.Failures));
|
||||
}
|
||||
finally
|
||||
{
|
||||
form.PlateView.DeselectAll();
|
||||
}
|
||||
Assert.False(form.PlateView.Actions.CurrentAction!.IsBusy());
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void InvalidNestNameRejectsBeforeAnyDialog() => RunSta(() =>
|
||||
{
|
||||
using var host = new TestMainForm();
|
||||
host.Show();
|
||||
using var form = new EditNestForm(new Nest("bad/name")) { MdiParent = host };
|
||||
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
|
||||
form.Show();
|
||||
|
||||
Menu(host, "mnuFileExportNestReport").PerformClick();
|
||||
|
||||
Assert.Empty(host.DialogRequests);
|
||||
Assert.Contains("bad/name", Assert.Single(host.Failures));
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void CancelWritesNothingAndNotifiesNothing() => RunSta(() =>
|
||||
{
|
||||
using var host = new TestMainForm { NextDialogPath = null };
|
||||
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");
|
||||
using var form = new EditNestForm(nest) { MdiParent = host };
|
||||
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
|
||||
form.Show();
|
||||
form.PlateManager.LoadAt(1);
|
||||
var path = Path.Combine(Path.GetTempPath(), $"opennest-report-ok-{Guid.NewGuid():N}.pdf");
|
||||
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(2, nest.Plates.Count);
|
||||
Assert.Single(nest.Plates[0].Parts);
|
||||
Assert.Equal(2, nest.Drawings.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(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();
|
||||
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.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"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void BusyAfterDialogRejectsWriteWithoutTouchingDestination() => RunSta(() =>
|
||||
{
|
||||
using var form = new EditNestForm(CreateJob("late busy"));
|
||||
form.PlateView.SetOverlapAutoCheck(null);
|
||||
var targets = form.CaptureReportTargets(() => false);
|
||||
var path = Path.Combine(Path.GetTempPath(), $"opennest-report-late-{Guid.NewGuid():N}.pdf");
|
||||
try
|
||||
{
|
||||
var error = Assert.Throws<InvalidOperationException>(
|
||||
() => form.WriteNestReport(targets, path, () => true));
|
||||
Assert.Contains("started while saving", error.Message);
|
||||
Assert.False(File.Exists(path));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void WriteRejectsAStaleTargetFromAnotherDocument() => RunSta(() =>
|
||||
{
|
||||
using var form = new EditNestForm(CreateJob("current job"));
|
||||
form.PlateView.SetOverlapAutoCheck(null);
|
||||
var stale = new EditNestForm.NestReportTargets(new Nest("other job"), "other.report.pdf");
|
||||
|
||||
var error = Assert.Throws<InvalidOperationException>(
|
||||
() => form.WriteNestReport(stale, "ignored.pdf", () => false));
|
||||
|
||||
Assert.Contains("active document changed", error.Message);
|
||||
});
|
||||
|
||||
private static Nest CreateJob(string name)
|
||||
{
|
||||
var nest = new Nest(name) { Units = Units.Inches };
|
||||
var drawing = new Drawing("rect", Rectangle());
|
||||
drawing.Quantity.Required = 2;
|
||||
nest.Drawings.Add(drawing);
|
||||
nest.Drawings.Add(new Drawing("spare", Rectangle()));
|
||||
var first = new Plate(24, 48) { Quantity = 1 };
|
||||
first.Parts.Add(new Part(drawing, new Vector(2, 2)));
|
||||
var second = new Plate(24, 48) { Quantity = 2 };
|
||||
second.Parts.Add(new Part(drawing, new Vector(4, 4)));
|
||||
nest.Plates.Add(first);
|
||||
nest.Plates.Add(second);
|
||||
return nest;
|
||||
}
|
||||
|
||||
private static Program Rectangle()
|
||||
{
|
||||
var program = new Program();
|
||||
program.Codes.Add(new RapidMove(0, 0));
|
||||
program.Codes.Add(new LinearMove(4, 0));
|
||||
program.Codes.Add(new LinearMove(4, 2));
|
||||
program.Codes.Add(new LinearMove(0, 2));
|
||||
program.Codes.Add(new LinearMove(0, 0));
|
||||
return program;
|
||||
}
|
||||
|
||||
private static ToolStripMenuItem Menu(MainForm host, string name) =>
|
||||
(ToolStripMenuItem)host.MainMenuStrip!.Items.Find(name, true).Single();
|
||||
|
||||
private static void SetField(object target, string name, object value) =>
|
||||
typeof(MainForm).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(target, value);
|
||||
|
||||
// Same reflection precedent PlateViewFillLifetimeTests uses for fill-lifetime state.
|
||||
private static void SetBackingField(object target, string property, object value) =>
|
||||
target.GetType()
|
||||
.GetField($"<{property}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.SetValue(target, value);
|
||||
|
||||
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; } = [];
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
private static void RunSta(System.Action action)
|
||||
{
|
||||
Exception? failure = null;
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failure = ex;
|
||||
}
|
||||
})
|
||||
{ IsBackground = true };
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
Assert.True(thread.Join(TimeSpan.FromSeconds(90)), "The STA test did not complete.");
|
||||
if (failure != null)
|
||||
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using OpenNest.Engine.Sequencing;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.Properties;
|
||||
using OpenNest.Reporting;
|
||||
using OpenNest.Shapes;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
@@ -478,6 +479,79 @@ namespace OpenNest.Forms
|
||||
} while (PlateManager.LoadNext());
|
||||
}
|
||||
|
||||
/// <summary>The report target fixed before the save dialog opens.</summary>
|
||||
internal sealed record NestReportTargets(Nest Nest, string SuggestedFileName);
|
||||
|
||||
/// <summary>
|
||||
/// Capture the report target before the owned save dialog opens. The exact nest
|
||||
/// reference is fixed here; later failures are reported against it, not a newer
|
||||
/// active document. Whole-job nesting, open progress windows (via <paramref name="isJobBusy"/>),
|
||||
/// interactive fill and busy plate actions on every view sharing this nest are rejected.
|
||||
/// </summary>
|
||||
internal NestReportTargets CaptureReportTargets(Func<bool> isJobBusy)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(isJobBusy);
|
||||
var nest = Nest;
|
||||
if (nest == null)
|
||||
throw new InvalidOperationException("No nest is available to report.");
|
||||
|
||||
// The nest name becomes the suggested file name; reject unusable names before the dialog.
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(nest.Name)
|
||||
|| nest.Name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0
|
||||
)
|
||||
throw new InvalidOperationException(
|
||||
$"The nest name '{nest.Name}' cannot be used as a report file name. Rename the nest first."
|
||||
);
|
||||
|
||||
if (isJobBusy() || IsReportTargetBusy(nest))
|
||||
throw new InvalidOperationException(
|
||||
"Finish or cancel the current nesting or plate action before exporting the nest report."
|
||||
);
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
internal void WriteNestReport(
|
||||
NestReportTargets targets,
|
||||
string destination,
|
||||
Func<bool> isJobBusy
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(targets);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(destination);
|
||||
ArgumentNullException.ThrowIfNull(isJobBusy);
|
||||
|
||||
if (!ReferenceEquals(targets.Nest, Nest))
|
||||
throw new InvalidOperationException(
|
||||
"The active document changed while saving. No report was written."
|
||||
);
|
||||
if (isJobBusy() || IsReportTargetBusy(targets.Nest))
|
||||
throw new InvalidOperationException(
|
||||
"A nesting or plate operation started while saving. No report was written."
|
||||
);
|
||||
|
||||
// Reporting must have no accounting side effects: no quantity refresh, no
|
||||
// selection or dirty-state change. Only the detached snapshot reaches the writer.
|
||||
var snapshot = NestReportBuilder.Capture(targets.Nest, DateTimeOffset.Now);
|
||||
NestPdfWriter.Write(snapshot, destination);
|
||||
}
|
||||
|
||||
private bool IsReportTargetBusy(Nest nest) =>
|
||||
Application
|
||||
.OpenForms.OfType<EditNestForm>()
|
||||
.Where(form => ReferenceEquals(form.Nest, nest))
|
||||
.Select(form => form.PlateView)
|
||||
.Any(view =>
|
||||
!view.IsDisposed
|
||||
&& (view.IsFillInProgress || view.Actions.CurrentAction?.IsBusy() == true)
|
||||
);
|
||||
|
||||
public void RotateCw()
|
||||
{
|
||||
PlateView.Plate.Rotate90(RotationType.CW);
|
||||
|
||||
Generated
+11
-1
@@ -39,6 +39,7 @@
|
||||
toolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator();
|
||||
mnuFileExport = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileExportAll = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileExportNestReport = new System.Windows.Forms.ToolStripMenuItem();
|
||||
toolStripMenuItem3 = new System.Windows.Forms.ToolStripSeparator();
|
||||
mnuFileExit = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuEdit = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -193,7 +194,7 @@
|
||||
//
|
||||
// mnuFile
|
||||
//
|
||||
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, toolStripMenuItem3, mnuFileExit });
|
||||
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileExportNestReport, toolStripMenuItem3, mnuFileExit });
|
||||
mnuFile.Name = "mnuFile";
|
||||
mnuFile.Size = new System.Drawing.Size(37, 20);
|
||||
mnuFile.Text = "&File";
|
||||
@@ -266,6 +267,14 @@
|
||||
mnuFileExportAll.Text = "Export All";
|
||||
mnuFileExportAll.Click += ExportAll_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;
|
||||
//
|
||||
// toolStripMenuItem3
|
||||
//
|
||||
toolStripMenuItem3.Name = "toolStripMenuItem3";
|
||||
@@ -1360,6 +1369,7 @@
|
||||
private System.Windows.Forms.ToolStripStatusLabel plateQtyStatusLabel;
|
||||
private System.Windows.Forms.ToolStripStatusLabel plateUtilStatusLabel;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileExportAll;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileExportNestReport;
|
||||
private System.Windows.Forms.ToolStripMenuItem openNestToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem pEPToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuViewZoomTo;
|
||||
|
||||
@@ -216,6 +216,7 @@ namespace OpenNest.Forms
|
||||
exportNestMenu.Enabled = hasValue;
|
||||
mnuFileExport.Enabled = hasValue;
|
||||
mnuFileExportAll.Enabled = hasValue;
|
||||
mnuFileExportNestReport.Enabled = hasValue && !databaseSaveInProgress;
|
||||
btnZoomOut.Enabled = hasValue;
|
||||
btnZoomIn.Enabled = hasValue;
|
||||
mnuEdit.Visible = hasValue;
|
||||
@@ -742,6 +743,87 @@ namespace OpenNest.Forms
|
||||
activeForm.ExportAll();
|
||||
}
|
||||
|
||||
private void ExportNestReport_Click(object sender, EventArgs e)
|
||||
{
|
||||
var form = activeForm;
|
||||
if (form == null)
|
||||
return;
|
||||
|
||||
// The report never depends on a selected post or NC output; it only needs a
|
||||
// quiescent nest so the snapshot is coherent.
|
||||
Func<bool> isJobBusy = IsNestJobBusy;
|
||||
EditNestForm.NestReportTargets targets;
|
||||
try
|
||||
{
|
||||
targets = form.CaptureReportTargets(isJobBusy);
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException)
|
||||
{
|
||||
ReportFailure(null, ex);
|
||||
return;
|
||||
}
|
||||
|
||||
var destination = ShowReportSaveDialog(targets.SuggestedFileName);
|
||||
if (destination == null)
|
||||
return; // Cancel: nothing written, job untouched.
|
||||
|
||||
try
|
||||
{
|
||||
form.WriteNestReport(targets, destination, isJobBusy);
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is InvalidOperationException
|
||||
or NotSupportedException
|
||||
or InvalidDataException
|
||||
or IOException
|
||||
or UnauthorizedAccessException
|
||||
or ArgumentException)
|
||||
{
|
||||
// Failures describe the captured target; an active-document switch is irrelevant.
|
||||
ReportFailure(targets, ex);
|
||||
return;
|
||||
}
|
||||
statusLabel1.Text = $"Saved nest report to {destination}";
|
||||
ReportSuccess(destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True while any operation could mutate nests or hold their plate views: whole-job
|
||||
/// nesting, an open progress window (a closed one can still await its commit), or a
|
||||
/// background database save serializing the live nest.
|
||||
/// </summary>
|
||||
private bool IsNestJobBusy() =>
|
||||
nestingInProgress
|
||||
|| Application.OpenForms.OfType<NestProgressForm>().Any()
|
||||
|| databaseSaveInProgress;
|
||||
|
||||
/// <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);
|
||||
|
||||
private void Exit_Click(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Reporting\OpenNest.Reporting.csproj" />
|
||||
<ProjectReference Include="..\Posts\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="..\Posts\OpenNest.Posts.CincinnatiCIFiber\OpenNest.Posts.CincinnatiCIFiber.csproj" ReferenceOutputAssembly="false" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
||||
|
||||
+22
-7
@@ -1,8 +1,18 @@
|
||||
# Nest report PDF library
|
||||
# Nest report PDF export
|
||||
|
||||
The first delivery is a bounded, cross-platform library slice, not yet a desktop
|
||||
menu command or a general report layout engine. Capture a stable `Nest` on its
|
||||
owning thread, then render only the detached snapshot:
|
||||
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. Opening and printing use any normal PDF viewer; the report includes
|
||||
no printer controls and there are no persisted report settings or templates.
|
||||
|
||||
The same two calls back the command and any future integration:
|
||||
|
||||
```csharp
|
||||
var snapshot = NestReportBuilder.Capture(nest, DateTimeOffset.Now);
|
||||
@@ -129,6 +139,11 @@ vector curves, unfilled holes and visible tab gaps with no connecting stroke.
|
||||
Pixel inspection supplements geometry assertions; it is not a CNC-validation
|
||||
result.
|
||||
|
||||
Windows compilation is not runtime acceptance. The later desktop adapter must be
|
||||
tested on Windows for busy-operation guards, cancellation, overwrite handling,
|
||||
unchanged job state and PDF viewing/printing, including the packaged application.
|
||||
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
|
||||
(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.
|
||||
|
||||
Reference in New Issue
Block a user