From fbe82d198adffe7bd3cd6296a2f22d6f825bf347 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 5 Nov 2026 12:49:02 -0500 Subject: [PATCH] fix(report): keep IDs on neighboring part groups one spacing apart Each dotted group outline was padded 1 pt beyond its parts. At overview scale a 0.25 in part spacing is only ~1.3 pt, so the outlines of two adjacent clusters (e.g. the six small bars and the two holed plates on N26-27Y plate 1) overlapped, and the later group - with its ID - was silently dropped. Pad outlines by under half the part spacing instead, so neighboring clusters both get their outline and ID. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Reporting/NestReportDiagram.cs | 5 +++- .../Reporting/NestPdfLayoutTests.cs | 24 +++++++++++++++++++ .../Reporting/NestReportTestData.cs | 2 +- docs/nest-reports.md | 4 +++- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/OpenNest.Reporting/NestReportDiagram.cs b/OpenNest.Reporting/NestReportDiagram.cs index 40a9143d..9884b862 100644 --- a/OpenNest.Reporting/NestReportDiagram.cs +++ b/OpenNest.Reporting/NestReportDiagram.cs @@ -63,6 +63,9 @@ internal static class NestReportDiagram var groups = new List(); // A small screen-space gap also accommodates tightly packed parts with unset spacing. var gap = System.Math.Max(4, plate.PartSpacing * fit.Scale * 2); + // Outlines stay under half the part spacing from their members, so the outlines of two + // neighboring clusters (different IDs one spacing apart) never meet or overlap. + var margin = plate.PartSpacing > 0 ? System.Math.Min(1, plate.PartSpacing * fit.Scale * 0.45) : 1; var remaining = failed.ToHashSet(); foreach (var seed in failed) { @@ -90,7 +93,7 @@ internal static class NestReportDiagram var boundary = boxes[seed]; foreach (var member in members.Skip(1)) boundary.Union(boxes[member]); - boundary.Inflate(1, 1); + boundary.Inflate(margin, margin); var memberSet = members.ToHashSet(); // A rectangular group must not imply that unrelated or ungrouped parts belong to it. if (!view.Contains(boundary) diff --git a/OpenNest.Tests/Reporting/NestPdfLayoutTests.cs b/OpenNest.Tests/Reporting/NestPdfLayoutTests.cs index ec1cfea6..fd0c079c 100644 --- a/OpenNest.Tests/Reporting/NestPdfLayoutTests.cs +++ b/OpenNest.Tests/Reporting/NestPdfLayoutTests.cs @@ -256,6 +256,30 @@ public sealed class NestPdfLayoutTests : IDisposable Assert.False(plan.Groups[0].Boundary.IntersectsWith(plan.Groups[1].Boundary)); } + [Fact] + public void Plan_NeighboringClustersOfDifferentPartsOneSpacingApartAreBothGrouped() + { + // A second washer drawing (another ID) packed one part spacing to the right of the first + // cluster: their outlines must not meet, or the second group and its ID were dropped. + var nest = NestReportTestData.CreateDenseNest(); + var plate = nest.Plates[0]; + var other = new Drawing("Other washer", NestReportTestData.HoledSquare(1, 0.25)); + nest.Drawings.Add(other); + var right = plate.Parts.Max(part => part.BoundingBox.Right); + for (var row = 0; row < 4; row++) + plate.Parts.Add(new Part(other, new Vector(right + plate.PartSpacing, 30 + row * 1.5))); + var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + + var plan = NestReportDiagram.Plan(snapshot.Plates[0], 720, 320); + + Assert.Equal(2, plan.Groups.Count); + Assert.Equal(["2", "3"], plan.Groups.Select(group => group.Label.Id).Order()); + Assert.False(plan.Groups[0].Boundary.IntersectsWith(plan.Groups[1].Boundary)); + foreach (var group in plan.Groups) + Assert.All(group.Parts, index => Assert.True(group.Boundary.Contains( + plan.Fit.Rect(snapshot.Plates[0].Parts[index].Geometry.Bounds)))); + } + [SkippableFact] public void Write_LabelOnHoledPartAvoidsTheHole() { diff --git a/OpenNest.Tests/Reporting/NestReportTestData.cs b/OpenNest.Tests/Reporting/NestReportTestData.cs index 60b0d6d0..2684b164 100644 --- a/OpenNest.Tests/Reporting/NestReportTestData.cs +++ b/OpenNest.Tests/Reporting/NestReportTestData.cs @@ -126,7 +126,7 @@ public static class NestReportTestData return drawing; } - private static Program HoledSquare(double size, double radius) + internal static Program HoledSquare(double size, double radius) { var program = RectangleProgram(size, size); var center = size / 2; diff --git a/docs/nest-reports.md b/docs/nest-reports.md index 4037f9bd..ff417188 100644 --- a/docs/nest-reports.md +++ b/docs/nest-reports.md @@ -93,7 +93,9 @@ and a part too small to label never prevents export. Every part remains in the quantity tables even when its diagram label cannot fit. Nearby small parts with the same report ID may share a dotted rectangular outline -and one ID beside it. Separate clusters get separate outlines. A group is omitted +and one ID beside it. Separate clusters get separate outlines; an outline stays +within half the part spacing of its parts, so clusters of different parts packed +one spacing apart each keep their own outline and ID. A group is omitted if its rectangle includes any other part or cutoff, or if there is no clear space for its label. Clearance is tested against real contours, not bounding boxes, so a cluster nested in a larger part's cutout or notch is still grouped and labeled. Dotted group outlines are distinct from dashed scrap cutoffs.