From 4feada094c09e9a37f239bf15411e9328a3e58c3 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 10 Oct 2026 13:14:39 -0400 Subject: [PATCH] fix(report): use plain numeric part IDs --- OpenNest.Reporting/NestReportBuilder.cs | 2 +- .../Reporting/NestPdfLayoutTests.cs | 46 +++++++++---------- .../Reporting/NestPdfWriterTests.cs | 25 +++++----- .../Reporting/NestReportBuilderTests.cs | 4 +- .../Reporting/NestReportTestData.cs | 4 +- docs/nest-reports.md | 2 +- 6 files changed, 42 insertions(+), 41 deletions(-) diff --git a/OpenNest.Reporting/NestReportBuilder.cs b/OpenNest.Reporting/NestReportBuilder.cs index 423206f..03c4c3c 100644 --- a/OpenNest.Reporting/NestReportBuilder.cs +++ b/OpenNest.Reporting/NestReportBuilder.cs @@ -21,7 +21,7 @@ public static class NestReportBuilder { if (byReference.TryGetValue(drawing, out var existing)) return existing; - var item = new DrawingCount(drawing, "R" + (ordered.Count + 1).ToString("D3", CultureInfo.InvariantCulture)); + var item = new DrawingCount(drawing, (ordered.Count + 1).ToString(CultureInfo.InvariantCulture)); byReference.Add(drawing, item); ordered.Add(item); return item; diff --git a/OpenNest.Tests/Reporting/NestPdfLayoutTests.cs b/OpenNest.Tests/Reporting/NestPdfLayoutTests.cs index 9a7e37a..6560366 100644 --- a/OpenNest.Tests/Reporting/NestPdfLayoutTests.cs +++ b/OpenNest.Tests/Reporting/NestPdfLayoutTests.cs @@ -32,11 +32,11 @@ public sealed class NestPdfLayoutTests : IDisposable var summary = pages[0]; Assert.Matches(@"Distinct layouts:\s+4", summary); Assert.Matches(@"Total physical sheets:\s+7", summary); - // Bracket R001: 2/sheet x 2 + 1 x 3 + 1 x 1 = 8. Same-named R003: 1 x 2 + 1 x 1 = 3. - Assert.Matches(@"R001\s+Bracket\s+5\s+8\s+0\s+3\s+1-2, 4", summary); - Assert.Matches(@"R002\s+Rotated\s+1\s+3\s+0\s+2\s+1, 3", summary); - Assert.Matches(@"R003\s+Bracket\s+7\s+3\s+4\s+0\s+1, 3", summary); - Assert.Matches(@"R004\s+Unplaced\s+3\s+0\s+3\s+0\s+-", summary); + // Bracket 1: 2/sheet x 2 + 1 x 3 + 1 x 1 = 8. Same-named 3: 1 x 2 + 1 x 1 = 3. + Assert.Matches(@"1\s+Bracket\s+5\s+8\s+0\s+3\s+1-2, 4", summary); + Assert.Matches(@"2\s+Rotated\s+1\s+3\s+0\s+2\s+1, 3", summary); + Assert.Matches(@"3\s+Bracket\s+7\s+3\s+4\s+0\s+1, 3", summary); + Assert.Matches(@"4\s+Unplaced\s+3\s+0\s+3\s+0\s+-", summary); Assert.Matches(@"3\s+30 x 60 in\s+1\s+2", summary); for (var plate = 1; plate <= 4; plate++) { @@ -45,10 +45,10 @@ public sealed class NestPdfLayoutTests : IDisposable Assert.Contains($"Page {plate + 1} of 5", page); Assert.Contains("Nest report: Report test job", page); } - Assert.Matches(@"R001\s+Bracket\s+1\s+3", pages[2]); - Assert.Matches(@"R002\s+Rotated\s+1\s+1", pages[3]); - Assert.Matches(@"R003\s+Bracket\s+1\s+1", pages[3]); - Assert.DoesNotContain("R001", pages[3]); + Assert.Matches(@"1\s+Bracket\s+1\s+3", pages[2]); + Assert.Matches(@"2\s+Rotated\s+1\s+1", pages[3]); + Assert.Matches(@"3\s+Bracket\s+1\s+1", pages[3]); + Assert.DoesNotMatch(@"(?m)^1\s+Bracket", pages[3]); } [SkippableFact] @@ -72,23 +72,23 @@ public sealed class NestPdfLayoutTests : IDisposable var summaryText = string.Join("\n", summaryPages); // Every row exactly once; the Parts heading row repeats on every continuation page. for (var id = 1; id <= 74; id++) - Assert.Single(Regex.Matches(summaryText, $@"(?m)^\s*R{id:D3}\s")); + Assert.Single(Regex.Matches(summaryText, $@"(?m)^\s*{id}\s+(?:Bracket|Rotated|Unplaced|Demand)\b")); Assert.All(summaryPages.Skip(1), page => Assert.Matches(@"ID\s+Part\s+Drawing\s+Required\s+Nested\s+Shortage", page)); - // Placements first (R004-R043 on plate 1), then unplaced demand by ordinal name. - Assert.Matches(@"R043\s+Demand 39\s+40\s+2\s+38\s+0\s+1", summaryText); - Assert.Matches(@"R073\s+Demand 69\s+70\s+0\s+70\s+0\s+-", summaryText); - Assert.Matches(@"R074\s+Unplaced\s+3\s+0\s+3\s+0\s+-", summaryText); + // Placements first (4-43 on plate 1), then unplaced demand by ordinal name. + Assert.Matches(@"43\s+Demand 39\s+40\s+2\s+38\s+0\s+1", summaryText); + Assert.Matches(@"73\s+Demand 69\s+70\s+0\s+70\s+0\s+-", summaryText); + Assert.Matches(@"74\s+Unplaced\s+3\s+0\s+3\s+0\s+-", summaryText); var platePages = pages.Where(page => page.Contains("Plate 1 of 4")).ToList(); Assert.True(platePages.Count >= 2, $"plate 1 pages: {platePages.Count}"); Assert.All(platePages, page => Assert.Contains("Nest report: Report test job", page)); // Every page carrying plate-table rows repeats the table heading. - var tablePages = platePages.Where(page => Regex.IsMatch(page, @"(?m)^R\d{3}\s+Demand \d{2}\s+1\s+2$")).ToList(); + var tablePages = platePages.Where(page => Regex.IsMatch(page, @"(?m)^\d+\s+Demand \d{2}\s+1\s+2$")).ToList(); Assert.True(tablePages.Count >= 2, $"plate table pages: {tablePages.Count}"); Assert.All(tablePages, page => Assert.Matches(@"ID\s+Drawing\s+Parts per plate\s+Total \(2 plates\)", page)); var plateText = string.Join("\n", platePages); for (var i = 0; i < 40; i++) - Assert.Matches($@"R{i + 4:D3}\s+Demand {i:D2}\s+1\s+2", plateText); + Assert.Matches($@"{i + 4}\s+Demand {i:D2}\s+1\s+2", plateText); Assert.DoesNotContain("Demand 40", plateText); for (var page = 1; page <= total; page++) Assert.Contains($"Page {page} of {total}", pages[page - 1]); @@ -110,7 +110,7 @@ public sealed class NestPdfLayoutTests : IDisposable Assert.Equal(Enumerable.Range(0, 900), notes); Assert.Contains(string.Concat(Enumerable.Repeat("X", 120)), Regex.Replace(all, @"\s+", "")); // The long drawing name is wrapped inside its column on the summary and plate tables. - var summaryPage = Array.FindIndex(pages, page => page.Contains("Required") && page.Contains("R002")); + var summaryPage = Array.FindIndex(pages, page => page.Contains("Required") && page.Contains("2")); var platePage = Array.FindIndex(pages, page => page.Contains("Plate 1 of 4")); Assert.True(summaryPage >= 0 && platePage > summaryPage); Assert.Equal(longName.Replace(" ", ""), ColumnText(path, summaryPage + 1, "Drawing", "Required")); @@ -200,9 +200,9 @@ public sealed class NestPdfLayoutTests : IDisposable Assert.Contains("cells outlined dash-dot have detail views", plateText); foreach (var detail in plan.Details) Assert.Contains($"Plate 1 detail {detail.Cell}:", plateText); - var washerLabels = pages.Skip(1).Sum(page => Regex.Matches(page, @"\bR002\b").Count); + var washerLabels = pages.Skip(1).Sum(page => Regex.Matches(page, @"\b2\b").Count); // 24 diagram labels plus one table row. - Assert.True(washerLabels >= 25, $"R002 occurrences: {washerLabels}"); + Assert.True(washerLabels >= 25, $"2 occurrences: {washerLabels}"); } [SkippableFact] @@ -284,7 +284,7 @@ public sealed class NestPdfLayoutTests : IDisposable Assert.InRange(delta.Item2, -0.01, 0.01); }); } - Assert.Equal(24, plan.Details.Sum(detail => detail.Labels.Count(label => label.Id == "R002"))); + Assert.Equal(24, plan.Details.Sum(detail => detail.Labels.Count(label => label.Id == "2"))); } private static OpenNest.Geometry.Shape ToShape(ReportContour contour) @@ -401,14 +401,14 @@ public sealed class NestPdfLayoutTests : IDisposable Assert.Contains("second line", lines); } - /// Text of the R002 row inside one table column, read from word boxes in reading order. + /// Text of the 2 row inside one table column, read from word boxes in reading order. private static string ColumnText(string path, int page, string column, string next) { var words = ReportPdf.Words(path, page); var heading = words.First(word => word.Text == column); var limit = words.First(word => word.Text == next && System.Math.Abs(word.Top - heading.Top) < 1); - var above = words.First(word => word.Text == "R001" && word.Top > heading.Top); - var below = words.First(word => word.Text == "R003" && word.Top > above.Top); + var above = words.First(word => word.Text == "1" && word.Left < heading.Left && word.Top > heading.Top); + var below = words.First(word => word.Text == "3" && word.Left < heading.Left && word.Top > above.Top); var cell = words.Where(word => word.Left >= heading.Left - 1 && word.Right <= limit.Left && word.Top > above.Bottom && word.Bottom < below.Top) .OrderBy(word => word.Top).ThenBy(word => word.Left); diff --git a/OpenNest.Tests/Reporting/NestPdfWriterTests.cs b/OpenNest.Tests/Reporting/NestPdfWriterTests.cs index b19971d..583901c 100644 --- a/OpenNest.Tests/Reporting/NestPdfWriterTests.cs +++ b/OpenNest.Tests/Reporting/NestPdfWriterTests.cs @@ -48,11 +48,12 @@ public sealed class NestPdfWriterTests : IDisposable Assert.Matches(@"Distinct layouts:\s+1", summary); Assert.Matches(@"Total physical sheets:\s+2", summary); Assert.Matches(@"1\s+24 x 48 in\s+2\s+4\s+\d+\.\d%", summary); - Assert.Matches(@"R001\s+Bracket\s+5\s+4\s+1\s+0\s+1", summary); - Assert.Matches(@"R002\s+Rotated\s+1\s+2\s+0\s+1\s+1", summary); - Assert.Matches(@"R003\s+Bracket\s+7\s+2\s+5\s+0\s+1", summary); - Assert.Matches(@"R004\s+Unplaced\s+3\s+0\s+3\s+0\s+-", summary); + Assert.Matches(@"1\s+Bracket\s+5\s+4\s+1\s+0\s+1", summary); + Assert.Matches(@"2\s+Rotated\s+1\s+2\s+0\s+1\s+1", summary); + Assert.Matches(@"3\s+Bracket\s+7\s+2\s+5\s+0\s+1", summary); + Assert.Matches(@"4\s+Unplaced\s+3\s+0\s+3\s+0\s+-", summary); Assert.Contains("Page 1 of 2", summary); + Assert.DoesNotMatch(@"\bR\d+\b", summary + plate); Assert.Matches(@"Stock size\s+Quantity\s+Parts per plate", summary); Assert.DoesNotContain("Parts/sheet", summary + plate); Assert.DoesNotContain("Copies", summary + plate); @@ -60,10 +61,10 @@ public sealed class NestPdfWriterTests : IDisposable Assert.Contains("Plate 1", plate); Assert.Contains("Report test job", plate); Assert.Matches(@"Quantity:\s+2", plate); - Assert.Matches(@"R001\s+Bracket\s+2\s+4", plate); - Assert.Matches(@"R002\s+Rotated\s+1\s+2", plate); - Assert.Matches(@"R003\s+Bracket\s+1\s+2", plate); - Assert.DoesNotContain("R004", plate); + Assert.Matches(@"1\s+Bracket\s+2\s+4", plate); + Assert.Matches(@"2\s+Rotated\s+1\s+2", plate); + Assert.Matches(@"3\s+Bracket\s+1\s+2", plate); + Assert.DoesNotMatch(@"(?m)^4\s+Unplaced", plate); Assert.DoesNotContain("Cutoff test", plate + summary); Assert.Contains("not a dimensioned cutting drawing", plate); Assert.Contains("not a geometry or CNC approval", plate); @@ -86,7 +87,7 @@ public sealed class NestPdfWriterTests : IDisposable nest.Drawings.Add(NestReportTestData.Rectangle("Alpha", 20, 10, 2)); var demand = ReportPdf.Text(WriteReport(nest, "demand.pdf"), 1); Assert.Contains("No plates in this job.", demand); - Assert.Matches(@"R001\s+Alpha\s+2\s+0\s+2\s+0\s+-", demand); + Assert.Matches(@"1\s+Alpha\s+2\s+0\s+2\s+0\s+-", demand); Assert.Contains("Page 1 of 1", demand); } @@ -132,8 +133,8 @@ public sealed class NestPdfWriterTests : IDisposable } [Theory] - [InlineData("illegible-label", "Plate 4, part 2 (R004)")] - [InlineData("cell-overflow", "Drawing R002")] + [InlineData("illegible-label", "Plate 4, part 2 (4)")] + [InlineData("cell-overflow", "Drawing 2")] [InlineData("header-overflow", "Page header")] public void Write_UnsupportedLayoutFailsBeforeReplacingDestination(string scenario, string identified) { @@ -177,7 +178,7 @@ public sealed class NestPdfWriterTests : IDisposable var error = Assert.Throws(() => NestPdfWriter.Write(snapshot, path)); - Assert.Contains("R002", error.Message); + Assert.Contains("2", error.Message); Assert.Contains("U+4E2D", error.Message); Assert.Equal("keep", File.ReadAllText(path)); Assert.Equal(new[] { path }, Directory.GetFiles(directory)); diff --git a/OpenNest.Tests/Reporting/NestReportBuilderTests.cs b/OpenNest.Tests/Reporting/NestReportBuilderTests.cs index 481ace8..dec3a4f 100644 --- a/OpenNest.Tests/Reporting/NestReportBuilderTests.cs +++ b/OpenNest.Tests/Reporting/NestReportBuilderTests.cs @@ -22,7 +22,7 @@ public class NestReportBuilderTests Assert.Equal(0.125, snapshot.Thickness); Assert.Equal("in", snapshot.Units); Assert.Equal(NestReportTestData.GeneratedAt, snapshot.GeneratedAt); - Assert.Equal(new[] { "R001", "R002", "R003", "R004" }, snapshot.Drawings.Select(d => d.Id)); + Assert.Equal(new[] { "1", "2", "3", "4" }, snapshot.Drawings.Select(d => d.Id)); Assert.Equal(new[] { "Bracket", "Rotated", "Bracket", "Unplaced" }, snapshot.Drawings.Select(d => d.Name)); Assert.Equal(new long[] { 5, 1, 7, 3 }, snapshot.Drawings.Select(d => d.Required)); Assert.Equal(new long[] { 4, 2, 2, 0 }, snapshot.Drawings.Select(d => d.Nested)); @@ -31,7 +31,7 @@ public class NestReportBuilderTests Assert.All(snapshot.Drawings.Take(3), d => Assert.Equal(new[] { 1 }, d.Plates)); Assert.Empty(snapshot.Drawings[3].Plates); var plate = Assert.Single(snapshot.Plates); - Assert.Equal(new[] { "R001", "R001", "R002", "R003" }, plate.Parts.Select(p => p.ReportId)); + Assert.Equal(new[] { "1", "1", "2", "3" }, plate.Parts.Select(p => p.ReportId)); Assert.Single(plate.Cutoffs); Assert.Equal(2, snapshot.TotalSheets); Assert.Equal(new ReportBounds(0, 0, 48, 24), plate.Bounds); diff --git a/OpenNest.Tests/Reporting/NestReportTestData.cs b/OpenNest.Tests/Reporting/NestReportTestData.cs index 3e3806b..321cf07 100644 --- a/OpenNest.Tests/Reporting/NestReportTestData.cs +++ b/OpenNest.Tests/Reporting/NestReportTestData.cs @@ -49,8 +49,8 @@ public static class NestReportTestData } /// - /// Four layouts: "Bracket" R001 on plates 1, 2 and 4; R002 on plates 1 and 3; the distinct - /// same-named "Bracket" R003 on plates 1 and 3; R004 demanded but unplaced. + /// Four layouts: "Bracket" 1 on plates 1, 2 and 4; 2 on plates 1 and 3; the distinct + /// same-named "Bracket" 3 on plates 1 and 3; 4 demanded but unplaced. /// public static Nest CreateMultiPlateNest() { diff --git a/docs/nest-reports.md b/docs/nest-reports.md index f666264..1568112 100644 --- a/docs/nest-reports.md +++ b/docs/nest-reports.md @@ -24,7 +24,7 @@ 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 +- IDs (`1`, `2`, ...) 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.