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.IO;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using OpenNest.Properties;
|
using OpenNest.Properties;
|
||||||
|
using OpenNest.Reporting;
|
||||||
using OpenNest.Shapes;
|
using OpenNest.Shapes;
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
@@ -478,6 +479,79 @@ namespace OpenNest.Forms
|
|||||||
} while (PlateManager.LoadNext());
|
} 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()
|
public void RotateCw()
|
||||||
{
|
{
|
||||||
PlateView.Plate.Rotate90(RotationType.CW);
|
PlateView.Plate.Rotate90(RotationType.CW);
|
||||||
|
|||||||
Generated
+11
-1
@@ -39,6 +39,7 @@
|
|||||||
toolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator();
|
toolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
mnuFileExport = new System.Windows.Forms.ToolStripMenuItem();
|
mnuFileExport = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuFileExportAll = new System.Windows.Forms.ToolStripMenuItem();
|
mnuFileExportAll = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
mnuFileExportNestReport = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
toolStripMenuItem3 = new System.Windows.Forms.ToolStripSeparator();
|
toolStripMenuItem3 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
mnuFileExit = new System.Windows.Forms.ToolStripMenuItem();
|
mnuFileExit = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuEdit = new System.Windows.Forms.ToolStripMenuItem();
|
mnuEdit = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
@@ -193,7 +194,7 @@
|
|||||||
//
|
//
|
||||||
// mnuFile
|
// 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.Name = "mnuFile";
|
||||||
mnuFile.Size = new System.Drawing.Size(37, 20);
|
mnuFile.Size = new System.Drawing.Size(37, 20);
|
||||||
mnuFile.Text = "&File";
|
mnuFile.Text = "&File";
|
||||||
@@ -266,6 +267,14 @@
|
|||||||
mnuFileExportAll.Text = "Export All";
|
mnuFileExportAll.Text = "Export All";
|
||||||
mnuFileExportAll.Click += ExportAll_Click;
|
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
|
||||||
//
|
//
|
||||||
toolStripMenuItem3.Name = "toolStripMenuItem3";
|
toolStripMenuItem3.Name = "toolStripMenuItem3";
|
||||||
@@ -1360,6 +1369,7 @@
|
|||||||
private System.Windows.Forms.ToolStripStatusLabel plateQtyStatusLabel;
|
private System.Windows.Forms.ToolStripStatusLabel plateQtyStatusLabel;
|
||||||
private System.Windows.Forms.ToolStripStatusLabel plateUtilStatusLabel;
|
private System.Windows.Forms.ToolStripStatusLabel plateUtilStatusLabel;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuFileExportAll;
|
private System.Windows.Forms.ToolStripMenuItem mnuFileExportAll;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuFileExportNestReport;
|
||||||
private System.Windows.Forms.ToolStripMenuItem openNestToolStripMenuItem;
|
private System.Windows.Forms.ToolStripMenuItem openNestToolStripMenuItem;
|
||||||
private System.Windows.Forms.ToolStripMenuItem pEPToolStripMenuItem;
|
private System.Windows.Forms.ToolStripMenuItem pEPToolStripMenuItem;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuViewZoomTo;
|
private System.Windows.Forms.ToolStripMenuItem mnuViewZoomTo;
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ namespace OpenNest.Forms
|
|||||||
exportNestMenu.Enabled = hasValue;
|
exportNestMenu.Enabled = hasValue;
|
||||||
mnuFileExport.Enabled = hasValue;
|
mnuFileExport.Enabled = hasValue;
|
||||||
mnuFileExportAll.Enabled = hasValue;
|
mnuFileExportAll.Enabled = hasValue;
|
||||||
|
mnuFileExportNestReport.Enabled = hasValue && !databaseSaveInProgress;
|
||||||
btnZoomOut.Enabled = hasValue;
|
btnZoomOut.Enabled = hasValue;
|
||||||
btnZoomIn.Enabled = hasValue;
|
btnZoomIn.Enabled = hasValue;
|
||||||
mnuEdit.Visible = hasValue;
|
mnuEdit.Visible = hasValue;
|
||||||
@@ -742,6 +743,87 @@ namespace OpenNest.Forms
|
|||||||
activeForm.ExportAll();
|
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)
|
private void Exit_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.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.Cincinnati\OpenNest.Posts.Cincinnati.csproj" ReferenceOutputAssembly="false" />
|
||||||
<ProjectReference Include="..\Posts\OpenNest.Posts.CincinnatiCIFiber\OpenNest.Posts.CincinnatiCIFiber.csproj" ReferenceOutputAssembly="false" />
|
<ProjectReference Include="..\Posts\OpenNest.Posts.CincinnatiCIFiber\OpenNest.Posts.CincinnatiCIFiber.csproj" ReferenceOutputAssembly="false" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
<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
|
Desktop: **File -> Export Nest Report...** writes `<nest-name>.report.pdf` for the
|
||||||
menu command or a general report layout engine. Capture a stable `Nest` on its
|
active nest's whole job through an overwrite-confirming save dialog. The command
|
||||||
owning thread, then render only the detached snapshot:
|
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
|
```csharp
|
||||||
var snapshot = NestReportBuilder.Capture(nest, DateTimeOffset.Now);
|
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
|
Pixel inspection supplements geometry assertions; it is not a CNC-validation
|
||||||
result.
|
result.
|
||||||
|
|
||||||
Windows compilation is not runtime acceptance. The later desktop adapter must be
|
Windows compilation is not runtime acceptance. `OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs`
|
||||||
tested on Windows for busy-operation guards, cancellation, overwrite handling,
|
covers enablement, busy/cancel/failure/success adapter behavior, but it only
|
||||||
unchanged job state and PDF viewing/printing, including the packaged application.
|
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