Files
OpenNest/docs/nest-reports.md
T
aj b15c4cf6c6 feat(reporting): add multi-plate pagination and dense-label fallback
Slice 2 of the nest report plan: pagination, overflow and dense-label
coverage on top of the Slice 1 one-plate library.

- NestPdfWriter: general multi-plate pagination. Summary/plate tables
  continue across pages with repeated (HeadingFormat) header rows; notes
  flow as an ordinary paragraph instead of a bounded cell; page headers
  are wrapped and sized into the top margin; drawing areas are located
  per page via DocumentRenderer.GetRenderInfoFromPage.
- ReportText: lossless pre-wrapping (MigraDoc clips an over-tall row and
  lets an unbroken token overflow a narrow cell without warning), capped
  table-cell line counts, plate-range compression ("1-2, 4"), and
  A/B/.../AA map-grid row names.
- NestReportDiagram: label placement centers each part ID on its
  PolyLabel pole (matching PlateView's LayoutPart), computed on a
  placement-independent quantized copy so identical parts share one
  label position. Parts whose ID cannot fit legibly at overview scale
  get a lettered/numbered map grid and a zoomed, framed detail page per
  crowded cell; the writer fails with plate/part/ID when even that
  cannot place a label, or a plate would need more than 24 detail views.
- Tests: NestPdfLayoutTests (multi-plate totals/ranges/same-named
  references, table continuation with no lost rows, long name/notes
  wrapping without column overflow, mm units in all four quadrants,
  dense-label detail views with hole avoidance, save/reload of the
  tabbed/lead-in fixture with stale tab flags, invalid later-plate data,
  late write failures against an existing destination, source
  unchanged on success/failure), ReportPdf test helper (page/word/
  content-stream extraction).
- docs/nest-reports.md: replace the Slice 1 one-plate limits section
  with the general pagination/dense-label contract and the process-wide
  PDFsharp/MigraDoc render lock.

Verification: Reporting filter 70/70; full OpenNest.Tests 2524
passed/21 skipped/0 failed; Engine 351/351; IO 77/77;
EnableWindowsTargeting=true full-solution build 0 errors; scoped
dotnet format --verify-no-changes exit 0. Preview PDFs rendered and
visually inspected (evidence: /home/aj/extracted/2026-09-29/opennest-report-slice2/).
Windows runtime, the desktop adapter and packaged-app font deployment
remain unverified (Slice 3).
2026-09-29 15:43:00 -04:00

7.7 KiB

Nest report PDF library

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:

var snapshot = NestReportBuilder.Capture(nest, DateTimeOffset.Now);
NestPdfWriter.Write(snapshot, destinationPath);

Reporting does not select a post, generate CNC, refresh drawing quantities, change the selected plate, or certify that a layout passed geometry/pre-post checks.

Snapshot contract

  • IDs (R001, ...) are local to the report. Visit non-cutoff placements in plate and part order, then append unseen demanded drawings ordered by ordinal name and source path. Identity is by reference: distinct same-named drawings stay distinct, including placed drawings absent from the name-keyed drawing collection.
  • Recount nested quantities from placements times sheet copies exactly once using checked wide integers. Include unplaced demand; report shortage and extra separately. Cached Quantity.Nested is not authoritative.
  • Distinguish distinct layouts from physical sheets. Per-sheet quantities do not include copies; total quantities do. Utilization uses net part area divided by full sheet area, excluding cutoffs.
  • Actual placed programs already contain rotations. Capture their material paths with the location applied once. Preserve holes and intentional tab gaps; exclude rapid, scribe, lead-in and lead-out paths. Keep Cut and Display contours.
  • If any material contour of a part is open, show all its contours without fill. Do not close the gap or substitute the drawing's nominal outline. Cutoffs are separate strokes with no product ID or quantity.
  • Snapshots retain values, not live drawings, parts, CNC programs or mutable geometry. Invalid/missing/nonfinite geometry fails with drawing/plate context.

Layout, pagination and dense-label fallback

An empty/demand-only job, a single plate, or many plates and drawings all produce one document: a Letter portrait summary followed by one Letter landscape section per plate, every page carrying a repeated header and "Page X of Y". Long notes, long drawing names, many drawings and many parts per sheet paginate naturally: MigraDoc continues the Plates/Parts tables and each plate's part table across pages with the heading row repeated, and notes flow as an ordinary paragraph. No row, table or note text is ever dropped or truncated to fit a page.

Each part ID is centered on its part's pole of inaccessibility (PolyLabel, the same method PlateView's LayoutPart uses), computed on a placement-independent quantized copy so identical parts always get the identical label position and the label naturally clears a central hole. When an ID cannot sit legibly inside its own material at overview scale (for example, a cluster of tiny repeated parts), the plate gains a lettered (rows)/numbered (columns) map grid drawn beneath the sheet, and only the crowded cells get a zoomed, framed detail page listing that cell's real coordinates. Detail-cell outlines use a long dash-dot stroke, distinct from the shorter dashed scrap-cutoff stroke and the dotted grid lines. IDs are never shrunk below 7 pt or silently dropped.

The writer still rejects, with NotSupportedException and before touching the destination:

  • a page header (nest name plus plate/material line) needing more than 3 wrapped lines;
  • a table cell needing more than 20 wrapped lines;
  • a part ID that cannot be placed legibly even in the most zoomed supported detail view, or a plate that would need more than 24 detail views to label every part — named with the plate, part index and ID.

Summary thumbnails and the sheet diagram are vector paths, never raster images. Each thumbnail is a small PDFsharp page embedded by MigraDoc as a form XObject. The diagram is drawn into a fixed-height table row reserved in MigraDoc's flow, located after layout with DocumentRenderer.GetRenderInfoFromPage. Closed parts use one even-odd filled path, so holes stay unfilled. Parts with any open contour, and cutoffs (dashed), stroke each contour as its own path: PDFsharp's StartFigure does not start a new subpath after an open figure, and would otherwise draw a false segment across a tab gap.

A report is fully rendered to a unique temporary sibling before replacement of its destination. A failed render or write leaves an existing report untouched and removes temporary output. Applications should obtain overwrite consent before calling the library. PDFsharp/MigraDoc layout and font state is process-wide; NestPdfWriter.Write serializes every export behind one static lock so concurrent calls cannot lay out text differently from a sequential export.

Advanced timing, cutting distances, pierce counts, weights, costs, gas use, and machine/NC identity are intentionally omitted until their semantics are verified. The PDF can be opened or printed through a normal PDF viewer; its fitted diagram is not a dimensioned cutting drawing.

Backend, fonts and redistribution

  • Official PDFsharp-MigraDoc Core 6.2.4, with PDFsharp 6.2.4, targeting net8.0. No GDI/WPF dependency or printer driver is used. MigraDoc owns flowing document layout; PDFsharp draws vectors into its measured reserved areas.
  • DejaVu Sans 2.37, regular and bold, is bundled unmodified as assembly resources. Source: https://dejavu-fonts.github.io/ ; binary provenance: Ubuntu fonts-dejavu-core 2.37-8. Both required faces are embedded in PDFs; neither generation nor viewing needs a platform font installation.
  • A custom resolver is installed once before fonts are created. Repeated exports reuse it. An already-installed foreign resolver is an explicit integration error, never silently replaced. A host that already uses PDFsharp needs resolver composition before adding report support. MigraDoc's predefined error font is pointed at the bundled family; its default (Courier New) is a platform font the bundled resolver deliberately cannot supply.
  • The initial text contract is printable ASCII/Latin-1 and line/tab separators. Other codepoints, controls and soft hyphens fail with the field and codepoint; there is no silent missing-glyph substitution. Broader Unicode/shaping support is deferred rather than implied by the font's larger character inventory.
  • PDFsharp/MigraDoc are MIT-licensed. The actual v6.2.4 license and the Microsoft transitive dependency notices are in OpenNest.Reporting/THIRD-PARTY-NOTICES.txt. Font permission/redistribution terms are in OpenNest.Reporting/Fonts/LICENSE.txt (Bitstream Vera permission with DejaVu changes in the public domain). Preserve both files in ReportingNotices/ when publishing; embedded fonts do not replace the obligation to ship their notices. Package notices are deduplicated, not rewritten or shortened.

Verification

dotnet test OpenNest.Tests/OpenNest.Tests.csproj --filter FullyQualifiedName~Reporting

The writer tests read page sizes and content streams with PDFsharp. Text assertions extract with Poppler's pdftotext -layout and are skipped, not passed, when poppler-utils is absent. For manual inspection also use pdfinfo, pdffonts and pdftoppm -png (Linux verification baseline: poppler-utils 24.02.0). Confirm page sizes, quantity rows, page X of Y, embedded fonts, native 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.