Files
OpenNest/OpenNest.Tests/Reporting/PartPrintoutTests.cs
T
ajandClaude Opus 5.5 696e0bdac6 feat(reporting): print one-page part printouts from the nest
Shops used CutSend to get a part printout (the part drawn to fit a Letter
page over a details table) because OpenNest had no way to make one. File >
View Part Printouts prints every part drawing of the nest, and the drawing
list's View Part Printout prints one; both open the report viewer to save
or print.

Cut length and pierce count walk the program as it is cut, so etch marks,
tab spans and display-only moves add neither, a tabbed cutout counts one
pierce per piece, and breakoff and common-line cuts count. The drawing list
item reaches the main window directly so nests made by BOM import get it
too. Rendering shares the nest report's lock, fonts and Latin-1 limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 07:10:56 -05:00

148 lines
5.9 KiB
C#

using OpenNest.CNC;
using OpenNest.Reporting;
using PdfSharp.Pdf.IO;
namespace OpenNest.Tests.Reporting;
public sealed class PartPrintoutTests : IDisposable
{
private readonly string directory = Path.Combine(Path.GetTempPath(), "opennest-printout-" + Guid.NewGuid().ToString("N"));
public PartPrintoutTests() => Directory.CreateDirectory(directory);
public void Dispose()
{
try { Directory.Delete(directory, true); } catch (IOException) { }
}
[Fact]
public void Capture_DefaultsToEveryPartDrawingInNestOrder()
{
var snapshot = PartPrintoutBuilder.Capture(NestReportTestData.CreateNest(), null, NestReportTestData.GeneratedAt);
// The cutoff drawing is never a part page.
Assert.Equal(["Unplaced", "Rotated", "Bracket"], snapshot.Parts.Select(part => part.Name).ToArray());
Assert.Equal("Steel A36", snapshot.Material);
Assert.Equal(0.125, snapshot.Thickness);
Assert.Equal("in", snapshot.Units);
}
[Fact]
public void Capture_MeasuresTheDrawing()
{
var nest = NestReportTestData.CreateNest();
var bracket = nest.Drawings.Single(drawing => drawing.Name == "Bracket");
var part = Assert.Single(PartPrintoutBuilder.Capture(nest, [bracket], NestReportTestData.GeneratedAt).Parts);
Assert.Equal(10, part.Width, 6);
Assert.Equal(10, part.Length, 6);
Assert.Equal(100 - System.Math.PI, part.Area, 6);
Assert.Equal(40 + 2 * System.Math.PI, part.CutLength, 6);
Assert.Equal(2, part.PierceCount);
Assert.Equal(5, part.Quantity);
Assert.Equal((100 - System.Math.PI) * 0.125 * 0.2836, part.Weight!.Value, 6);
Assert.Equal("Test customer", part.Customer);
}
[Fact]
public void Capture_ReportsYExtentAsWidthAndXExtentAsLength()
{
var nest = NestReportTestData.CreateNest();
var rotated = nest.Drawings.Single(drawing => drawing.Name == "Rotated"); // 4 along X, 2 along Y
var part = Assert.Single(PartPrintoutBuilder.Capture(nest, [rotated], NestReportTestData.GeneratedAt).Parts);
Assert.Equal(2, part.Width, 6);
Assert.Equal(4, part.Length, 6);
}
[Fact]
public void CutMetrics_CountOnlyWhatTheHeadCuts()
{
var program = new Program();
// 10 x 10 outside: one pierce, 40 cut.
program.Codes.Add(new RapidMove(0, 0));
program.Codes.Add(new LinearMove(0, 10));
program.Codes.Add(new LinearMove(10, 10));
program.Codes.Add(new LinearMove(10, 0));
program.Codes.Add(new LinearMove(0, 0));
// An etch mark is neither cut length nor a pierce.
program.Codes.Add(new RapidMove(1, 1));
program.Codes.Add(new LinearMove(3, 1) { Layer = LayerType.Scribe });
// A tabbed 4 x 2 cutout: two cut pieces either side of 1-long tabs, so two pierces, 10 cut.
program.Codes.Add(new RapidMove(3, 4));
program.Codes.Add(new LinearMove(7, 4));
program.Codes.Add(new LinearMove(7, 5));
program.Codes.Add(new LinearMove(7, 6) { Layer = LayerType.Tab });
program.Codes.Add(new LinearMove(3, 6));
program.Codes.Add(new LinearMove(3, 5));
program.Codes.Add(new LinearMove(3, 4) { Layer = LayerType.Tab });
// A breakoff's break line is cut: one pierce, 2 cut.
program.Codes.Add(new RapidMove(8, 2));
program.Codes.Add(new LinearMove(8, 4) { Layer = LayerType.Break });
var (length, pierces) = PartPrintoutBuilder.CutMetrics(program);
Assert.Equal(40 + 10 + 2, length, 6);
Assert.Equal(4, pierces);
}
[Fact]
public void Write_MakesOneLetterPortraitPageAndBookmarkPerPart()
{
var nest = NestReportTestData.CreateNest();
var path = Path.Combine(directory, "parts.pdf");
PartPrintoutWriter.Write(PartPrintoutBuilder.Capture(nest, null, NestReportTestData.GeneratedAt), path);
using var pdf = PdfReader.Open(path, PdfDocumentOpenMode.Modify);
Assert.Equal(3, pdf.PageCount);
foreach (var page in pdf.Pages)
Assert.Equal((612, 792), ReportPdf.EffectiveSize(page));
Assert.Equal(["Unplaced", "Rotated", "Bracket"], pdf.Outlines.Select(outline => outline.Title).ToArray());
}
[SkippableFact]
public void Write_PageCarriesTheDetailsTable()
{
var nest = NestReportTestData.CreateNest();
var bracket = nest.Drawings.Single(drawing => drawing.Name == "Bracket");
var path = Path.Combine(directory, "bracket.pdf");
PartPrintoutWriter.Write(PartPrintoutBuilder.Capture(nest, [bracket], NestReportTestData.GeneratedAt), path);
var text = ReportPdf.Text(path, 1);
Assert.Contains("Bracket", text);
Assert.Contains("Test customer", text);
Assert.Matches(@"DIMENSIONS\s+10\.00"" x 10\.00""\s+MATERIAL\s+Steel A36", text);
Assert.Matches(@"THICKNESS\s+0\.125""\s+CUT LENGTH\s+46\.28""", text);
Assert.Matches(@"PIERCE COUNT\s+2\s+WEIGHT\s+3\.43 lb", text);
Assert.Matches(@"AREA\s+96\.86 sq in\s+QUANTITY\s+5", text);
Assert.Contains("Generated 9/29/2026 12:00 PM", text);
}
[Fact]
public void Write_RejectsTextTheBundledFontCannotShow()
{
var nest = NestReportTestData.CreateNest();
var drawing = nest.Drawings.Single(item => item.Name == "Rotated");
drawing.Customer = "em—dash";
var path = Path.Combine(directory, "bad.pdf");
var error = Assert.Throws<InvalidOperationException>(() =>
PartPrintoutWriter.Write(PartPrintoutBuilder.Capture(nest, [drawing], NestReportTestData.GeneratedAt), path));
Assert.Contains("U+2014", error.Message);
Assert.False(File.Exists(path));
}
[Fact]
public void Capture_RejectsAnEmptySelection()
{
var error = Assert.Throws<InvalidOperationException>(() =>
PartPrintoutBuilder.Capture(new Nest("empty"), null, NestReportTestData.GeneratedAt));
Assert.Contains("no drawings", error.Message);
}
}