Merge branch 'fix/report-group-ids'

This commit is contained in:
aj committed 2026-11-04 09:38:39 -05:00
commit 54cea0c325
3 files changed
+51 -11

No files matched your search

+21 -10
View File
@@ -46,15 +46,20 @@ internal static class NestReportDiagram
var view = new XRect(0, 0, width, height);
var fit = Fit.Create(union, new XRect(Inset, Inset, width - 2 * Inset, height - 2 * Inset));
var labels = PlaceLabels(plate, shapes, obstacles, all, fit, view, font, out var failed);
var groups = PlaceGroups(plate, failed, labels, fit, view, font);
var groups = PlaceGroups(plate, obstacles, failed, labels, fit, view, font);
return new DiagramView(canvas, fit, labels, groups);
}
private static List<DiagramGroup> PlaceGroups(ReportPlate plate, List<int> failed,
/// <summary>
/// Groups unlabeled same-ID neighbors. Clearance against other parts and cutoffs is tested on
/// their real contours, not their bounding boxes: a cluster nested in a large part's hole or
/// concave notch lies inside that part's bounding box but touches none of its material.
/// </summary>
private static List<DiagramGroup> PlaceGroups(ReportPlate plate, List<Shape> obstacles, List<int> failed,
List<DiagramLabel> labels, Fit fit, XRect view, XFont font)
{
// Obstacles are the parts, in plate order, followed by the cutoffs.
var boxes = plate.Parts.Select(part => fit.Rect(part.Geometry.Bounds)).ToList();
var cutoffs = plate.Cutoffs.Select(cutoff => fit.Rect(cutoff.Bounds)).ToList();
var groups = new List<DiagramGroup>();
// A small screen-space gap also accommodates tightly packed parts with unset spacing.
var gap = System.Math.Max(4, plate.PartSpacing * fit.Scale * 2);
@@ -89,8 +94,7 @@ internal static class NestReportDiagram
var memberSet = members.ToHashSet();
// A rectangular group must not imply that unrelated or ungrouped parts belong to it.
if (!view.Contains(boundary)
|| boxes.Where((_, index) => !memberSet.Contains(index)).Any(box => box.IntersectsWith(boundary))
|| cutoffs.Any(box => box.IntersectsWith(boundary))
|| obstacles.Where((_, index) => !memberSet.Contains(index)).Any(shape => shape.Crosses(ModelBox(fit, boundary)))
|| labels.Any(label => label.Box.IntersectsWith(boundary))
|| groups.Any(group => group.Boundary.IntersectsWith(boundary) || group.Label.Box.IntersectsWith(boundary)))
continue;
@@ -112,8 +116,7 @@ internal static class NestReportDiagram
{
var padded = box;
padded.Inflate(Clearance, Clearance);
if (!view.Contains(padded) || boxes.Any(other => other.IntersectsWith(padded))
|| cutoffs.Any(other => other.IntersectsWith(padded))
if (!view.Contains(padded) || obstacles.Any(shape => shape.Crosses(ModelBox(fit, padded)))
|| labels.Any(label => label.Box.IntersectsWith(padded))
|| groups.Any(group => group.Boundary.IntersectsWith(padded) || group.Label.Box.IntersectsWith(padded)))
continue;
@@ -143,9 +146,9 @@ internal static class NestReportDiagram
var height = size.Height + 2 * LabelPadding;
var pole = fit.Point(shapes[index].Pole.X, shapes[index].Pole.Y);
var box = new XRect(pole.X - width / 2, pole.Y - height / 2, width, height);
var topLeft = fit.Model(new XPoint(box.X - Clearance, box.Y - Clearance));
var bottomRight = fit.Model(new XPoint(box.Right + Clearance, box.Bottom + Clearance));
var test = new Box(topLeft.X, bottomRight.Y, bottomRight.X - topLeft.X, topLeft.Y - bottomRight.Y);
var padded = box;
padded.Inflate(Clearance, Clearance);
var test = ModelBox(fit, padded);
if (view.Contains(box) && !placed.Any(label => label.Box.IntersectsWith(box))
&& !obstacles.Any(shape => shape.Crosses(test)))
placed.Add(new DiagramLabel(index, id, box));
@@ -155,6 +158,14 @@ internal static class NestReportDiagram
return placed;
}
/// <summary>The model-space rectangle under a canvas rectangle.</summary>
private static Box ModelBox(Fit fit, XRect rect)
{
var topLeft = fit.Model(new XPoint(rect.Left, rect.Top));
var bottomRight = fit.Model(new XPoint(rect.Right, rect.Bottom));
return new Box(topLeft.X, bottomRight.Y, bottomRight.X - topLeft.X, topLeft.Y - bottomRight.Y);
}
private static ReportBounds Union(ReportPlate plate, IEnumerable<Shape> shapes)
{
var left = plate.Bounds.Left;
@@ -209,6 +209,34 @@ public sealed class NestPdfLayoutTests : IDisposable
Assert.Equal(2, plan.Labels.Count);
}
[Fact]
public void Plan_GroupNestedInsideALargerPartsCutoutIsStillLabeled()
{
// A frame whose cutout holds the washer cluster: its bounding box covers the group, but
// its material does not, so the group outline and ID are still clear.
var nest = NestReportTestData.CreateDenseNest();
var frame = new Program();
frame.Codes.Add(new RapidMove(0, 0));
frame.Codes.Add(new LinearMove(0, 30));
frame.Codes.Add(new LinearMove(40, 30));
frame.Codes.Add(new LinearMove(40, 0));
frame.Codes.Add(new LinearMove(0, 0));
frame.Codes.Add(new RapidMove(5, 5));
frame.Codes.Add(new LinearMove(35, 5));
frame.Codes.Add(new LinearMove(35, 25));
frame.Codes.Add(new LinearMove(5, 25));
frame.Codes.Add(new LinearMove(5, 5));
nest.Plates[0].Parts.Add(new Part(new Drawing("Frame", frame), new Vector(50, 20)));
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
var plan = NestReportDiagram.Plan(snapshot.Plates[0], 720, 320);
var group = Assert.Single(plan.Groups);
Assert.Equal("2", group.Label.Id);
Assert.Equal(24, group.Parts.Count);
var frameBox = plan.Fit.Rect(snapshot.Plates[0].Parts[^1].Geometry.Bounds);
Assert.True(frameBox.Contains(group.Boundary));
Assert.True(frameBox.Contains(group.Label.Box));
}
[Fact]
public void Plan_SeparatedClustersOfTheSamePartGetSeparateGroups()
{
+2 -1
View File
@@ -70,7 +70,8 @@ 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
if its rectangle includes any other part or cutoff, or if there is no clear space
for its label. Dotted group outlines are distinct from dashed scrap cutoffs.
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.
The writer still rejects, with `NotSupportedException` and before touching
the destination: