mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-10 17:51:09 -05:00
Ring-shaped parts (a plate with a large center hole) were left unlabeled: the material at their pole is too narrow for the ID, and the label was never tried anywhere else. Group IDs were likewise only ever placed beside the dotted outline, so a cramped cluster lost its ID or straddled the sheet edge. A part whose pole spot is too tight now takes the roomiest clear spot within its own extents, such as its empty hole. A group ID takes the roomiest spot inside its outline or beside it; with none clear it may cover the group's own parts near the center, since operators read part detail from the parts list. No ID sits across the sheet outline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
521 lines
26 KiB
C#
521 lines
26 KiB
C#
using System.Text.RegularExpressions;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.IO;
|
|
using OpenNest.Reporting;
|
|
using PdfSharp.Pdf.IO;
|
|
|
|
namespace OpenNest.Tests.Reporting;
|
|
|
|
/// <summary>Slice 2: pagination, overflow, dense labels and late failures on real PDFs.</summary>
|
|
public sealed class NestPdfLayoutTests : IDisposable
|
|
{
|
|
private readonly string directory = Path.Combine(Path.GetTempPath(), "opennest-pdf-layout-" + Guid.NewGuid().ToString("N"));
|
|
|
|
public NestPdfLayoutTests() => Directory.CreateDirectory(directory);
|
|
|
|
public void Dispose() => Directory.Delete(directory, true);
|
|
|
|
[SkippableFact]
|
|
public void Write_MultiPlateJobHasOnePagePerPlateWithTotalsAcrossPlatesAndSameNamedReferences()
|
|
{
|
|
var path = Write(NestReportTestData.CreateMultiPlateNest());
|
|
var pages = ReportPdf.Pages(path);
|
|
|
|
Assert.Equal(5, pages.Length);
|
|
using (var pdf = PdfReader.Open(path, PdfDocumentOpenMode.Import))
|
|
{
|
|
Assert.Equal((612, 792), ReportPdf.EffectiveSize(pdf.Pages[0]));
|
|
for (var page = 1; page < 5; page++)
|
|
Assert.Equal((792, 612), ReportPdf.EffectiveSize(pdf.Pages[page]));
|
|
}
|
|
var summary = pages[0];
|
|
Assert.Matches(@"Distinct layouts:\s+4", summary);
|
|
Assert.Matches(@"Total physical sheets:\s+7", summary);
|
|
// Bracket 1: 2/sheet x 2 + 1 x 3 + 1 x 1 = 8. Same-named 3: 1 x 2 + 1 x 1 = 3.
|
|
// Weights: net area x 0.125 in x 0.2836 lb/in^3; the 10 x 10 bracket has a 1 in radius hole.
|
|
Assert.Equal(new[]
|
|
{
|
|
"1 EXTRA 3\nBracket\nSize: 10 x 10 in\nWeight: 3.43 lb ea\nTotal weight: 27.5 lb\nRequired 5, nested 8\nPlates: 1-2, 4",
|
|
"2 EXTRA 2\nRotated\nSize: 2 x 4 in\nWeight: 0.28 lb ea\nTotal weight: 0.85 lb\nRequired 1, nested 3\nPlates: 1, 3",
|
|
"3 SHORT 4\nBracket\nSize: 2 x 3 in\nWeight: 0.21 lb ea\nTotal weight: 0.64 lb\nRequired 7, nested 3\nPlates: 1, 3",
|
|
"4 SHORT 3\nUnplaced\nSize: 1 x 2 in\nWeight: 0.07 lb ea\nRequired 3, nested 0\nPlates: -",
|
|
}, ReportPdf.PartCards(path));
|
|
Assert.DoesNotContain("Weights need", summary);
|
|
Assert.Matches(@"3\s+30 x 60 in\s+1\s+2", summary);
|
|
for (var plate = 1; plate <= 4; plate++)
|
|
{
|
|
var page = pages[plate];
|
|
Assert.Contains($"Plate {plate} of 4", page);
|
|
Assert.Contains($"Page {plate + 1} of 5", page);
|
|
Assert.Contains("Nest report: Report test job", page);
|
|
}
|
|
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]
|
|
public void Write_ManyDrawingsAndPartsContinueTablesWithRepeatedHeadersAndNoLostRows()
|
|
{
|
|
var nest = NestReportTestData.CreateMultiPlateNest();
|
|
var plate = nest.Plates[0];
|
|
for (var i = 0; i < 70; i++)
|
|
{
|
|
var drawing = NestReportTestData.Rectangle($"Demand {i:D2}", 1, 1, i + 1);
|
|
nest.Drawings.Add(drawing);
|
|
if (i < 40)
|
|
plate.Parts.Add(new Part(drawing, new Vector(1 + i % 20 * 2.2, 14 + i / 20 * 2.2)));
|
|
}
|
|
var path = Write(nest);
|
|
var pages = ReportPdf.Pages(path);
|
|
var total = pages.Length;
|
|
|
|
var summaryPages = pages.TakeWhile(page => !page.Contains("Plate 1 of 4")).ToList();
|
|
Assert.True(summaryPages.Count >= 2, $"summary pages: {summaryPages.Count}");
|
|
// Every card exactly once, in ID order, and no card is split across pages.
|
|
var cards = ReportPdf.PartCards(path);
|
|
Assert.Equal(Enumerable.Range(1, 74).Select(id => id.ToString()),
|
|
cards.Select(card => card.Split(' ', '\n')[0]));
|
|
// Placements first (4-43 on plate 1), then unplaced demand by ordinal name.
|
|
Assert.Matches(@"^43 SHORT 38\nDemand 39\nSize: 1 x 1 in\n(?:.*\n)+Required 40, nested 2\nPlates: 1$", cards[42]);
|
|
Assert.Matches(@"^73 SHORT 70\nDemand 69\n(?:.*\n)+Required 70, nested 0\nPlates: -$", cards[72]);
|
|
Assert.Matches(@"^74 SHORT 3\nUnplaced\n(?:.*\n)+Required 3, nested 0\nPlates: -$", cards[73]);
|
|
|
|
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)^\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($@"{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]);
|
|
}
|
|
|
|
[SkippableFact]
|
|
public void Write_LongNamesAndNotesWrapWithoutLosingTextOrOverflowingColumns()
|
|
{
|
|
var nest = NestReportTestData.CreateMultiPlateNest();
|
|
var longName = "PN-" + string.Concat(Enumerable.Repeat("ABCDEFGHIJ", 7)) + " revision C with long description";
|
|
nest.Plates[0].Parts[2].BaseDrawing.Name = longName;
|
|
nest.Notes = string.Join(" ", Enumerable.Range(0, 900).Select(i => $"note{i}"));
|
|
nest.Customer = "Customer " + string.Concat(Enumerable.Repeat("X", 120));
|
|
var path = Write(nest);
|
|
var pages = ReportPdf.Pages(path);
|
|
|
|
var all = string.Join("\n", pages);
|
|
var notes = Regex.Matches(all, @"note(\d+)").Select(match => int.Parse(match.Groups[1].Value)).ToList();
|
|
Assert.Equal(Enumerable.Range(0, 900), notes);
|
|
Assert.Contains(string.Concat(Enumerable.Repeat("X", 120)), Regex.Replace(all, @"\s+", ""));
|
|
// The long drawing name wraps inside its card (words past the card edge are not
|
|
// collected) and inside its plate-table column.
|
|
var card = ReportPdf.PartCards(path)[1].Split('\n');
|
|
Assert.Equal("2 EXTRA 2", card[0]);
|
|
var size = Array.FindIndex(card, line => line.StartsWith("Size:", StringComparison.Ordinal));
|
|
Assert.True(size > 2, "the long name should need several card lines");
|
|
Assert.Equal(longName.Replace(" ", ""), string.Concat(card[1..size]).Replace(" ", ""));
|
|
var platePage = Array.FindIndex(pages, page => page.Contains("Plate 1 of 4"));
|
|
Assert.Equal(longName.Replace(" ", ""), ColumnText(path, platePage + 1, "Drawing", "Parts"));
|
|
}
|
|
|
|
[SkippableTheory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void Write_MillimeterJobInEveryQuadrantDrawsInsideTheSheetOutline(int quadrant)
|
|
{
|
|
var nest = NestReportTestData.CreateMultiPlateNest();
|
|
nest.Units = Units.Millimeters;
|
|
nest.Thickness = 3;
|
|
foreach (var plate in nest.Plates)
|
|
{
|
|
plate.Quadrant = quadrant;
|
|
// Move every placement (and cutoff) into the sheet's own quadrant.
|
|
var bounds = plate.BoundingBox(false);
|
|
foreach (var part in plate.Parts)
|
|
part.Offset(bounds.Left, bounds.Bottom);
|
|
}
|
|
var path = Write(nest);
|
|
var pages = ReportPdf.Pages(path);
|
|
|
|
Assert.Equal(5, pages.Length);
|
|
Assert.Contains("millimeters (mm)", pages[0]);
|
|
Assert.Contains("3 mm", pages[0]);
|
|
Assert.Matches(@"24 x 48 mm", pages[2]);
|
|
using var pdf = PdfReader.Open(path, PdfDocumentOpenMode.Import);
|
|
var stream = ReportPdf.ContentStreams(pdf.Pages[1]).Last();
|
|
stream = stream[(stream.IndexOf("W* n", StringComparison.Ordinal) + 4)..];
|
|
var sheet = Regex.Match(stream, @"([-\d.]+) ([-\d.]+) ([-\d.]+) ([-\d.]+) re\s+S");
|
|
Assert.True(sheet.Success);
|
|
double Value(int group) => double.Parse(sheet.Groups[group].Value, System.Globalization.CultureInfo.InvariantCulture);
|
|
var (left, top, width, height) = (Value(1), Value(2), Value(3), Value(4));
|
|
var points = Regex.Matches(stream, @"(?m)^([-\d.]+) ([-\d.]+) [ml]$")
|
|
.Select(match => (X: double.Parse(match.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture),
|
|
Y: double.Parse(match.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture)))
|
|
.ToList();
|
|
Assert.NotEmpty(points);
|
|
Assert.All(points, point =>
|
|
{
|
|
Assert.InRange(point.X, left - 1e-3, left + width + 1e-3);
|
|
Assert.InRange(point.Y, top - 1e-3, top + height + 1e-3);
|
|
});
|
|
}
|
|
|
|
[SkippableFact]
|
|
public void Write_TinyRepeatedPartsShareOneDottedGroupOnThePlatePage()
|
|
{
|
|
var nest = NestReportTestData.CreateDenseNest();
|
|
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(Enumerable.Range(2, 24), group.Parts.Order());
|
|
Assert.Equal(2, plan.Labels.Count);
|
|
Assert.DoesNotContain(plan.Labels, label => label.Id == "2");
|
|
Assert.All(group.Parts, index => Assert.True(group.Boundary.Contains(plan.Fit.Rect(snapshot.Plates[0].Parts[index].Geometry.Bounds))));
|
|
Assert.False(group.Boundary.IntersectsWith(group.Label.Box));
|
|
var labels = plan.Labels.Append(group.Label).ToList();
|
|
var font = NestReportDiagram.LabelFont();
|
|
foreach (var label in labels)
|
|
{
|
|
Assert.True(label.Box.Width >= ReportText.Size(label.Id, font).Width);
|
|
Assert.True(new PdfSharp.Drawing.XRect(0, 0, 720, 320).Contains(label.Box));
|
|
Assert.DoesNotContain(labels, other => other != label && other.Box.IntersectsWith(label.Box));
|
|
}
|
|
var path = Write(nest);
|
|
var pages = ReportPdf.Pages(path);
|
|
Assert.Equal(2, pages.Length);
|
|
Assert.Contains("A dotted outline groups like parts under one ID", pages[1]);
|
|
Assert.DoesNotContain("detail", pages[1]);
|
|
using var pdf = PdfReader.Open(path, PdfDocumentOpenMode.Import);
|
|
var diagram = ReportPdf.ContentStreams(pdf.Pages[1]).Last();
|
|
Assert.Matches(@"\[0\.6 1\.8\]\s*0\s+d", diagram);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void Plan_GroupDoesNotEncloseUnrelatedPartsOrCutoffs(bool cutoff)
|
|
{
|
|
var nest = NestReportTestData.CreateDenseNest();
|
|
var drawing = NestReportTestData.Rectangle("Obstacle", 0.1, 0.1);
|
|
drawing.IsCutOff = cutoff;
|
|
nest.Plates[0].Parts.Add(new Part(drawing, new Vector(63.2, 31.2)));
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
var plan = NestReportDiagram.Plan(snapshot.Plates[0], 720, 320);
|
|
Assert.Empty(plan.Groups);
|
|
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()
|
|
{
|
|
var nest = NestReportTestData.CreateDenseNest();
|
|
var plate = nest.Plates[0];
|
|
for (var i = 14; i < 26; i++)
|
|
plate.Parts[i].Offset(25, 0);
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
var plan = NestReportDiagram.Plan(snapshot.Plates[0], 720, 320);
|
|
Assert.Equal(2, plan.Groups.Count);
|
|
Assert.All(plan.Groups, group => Assert.Equal(12, group.Parts.Count));
|
|
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))));
|
|
}
|
|
|
|
[Fact]
|
|
public void Plan_RingTooNarrowForItsIdIsLabeledInItsHole()
|
|
{
|
|
// A 6.5 in plate with a 4.56 in hole on a 60 x 96 sheet: the ring of material is ~5 pt wide
|
|
// at overview scale, too narrow for the ID, but the empty hole has plenty of room.
|
|
var nest = new Nest("Ring test") { Units = Units.Inches };
|
|
var ring = new Drawing("Ring", NestReportTestData.HoledSquare(6.5, 2.28));
|
|
nest.Drawings.Add(ring);
|
|
var plate = new Plate(60, 96) { Quantity = 1, PartSpacing = 0.25 };
|
|
plate.Parts.Add(new Part(ring, new Vector(80, 50)));
|
|
nest.Plates.Add(plate);
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
|
|
var plan = NestReportDiagram.Plan(snapshot.Plates[0], 720, 320);
|
|
|
|
var label = Assert.Single(plan.Labels);
|
|
Assert.Empty(plan.Groups);
|
|
var center = plan.Fit.Model(new PdfSharp.Drawing.XPoint(label.Box.X + label.Box.Width / 2, label.Box.Y + label.Box.Height / 2));
|
|
var bounds = snapshot.Plates[0].Parts[0].Geometry.Bounds;
|
|
Assert.InRange(center.X - (bounds.Left + bounds.Right) / 2, -0.5, 0.5);
|
|
Assert.InRange(center.Y - (bounds.Bottom + bounds.Top) / 2, -0.5, 0.5);
|
|
}
|
|
|
|
[SkippableFact]
|
|
public void Write_LabelOnHoledPartAvoidsTheHole()
|
|
{
|
|
var snapshot = NestReportBuilder.Capture(NestReportTestData.CreateNest(), NestReportTestData.GeneratedAt);
|
|
var plan = NestReportDiagram.Plan(snapshot.Plates[0], 720, 320);
|
|
var label = plan.Labels.First(label => label.Part == 0);
|
|
var hole = snapshot.Plates[0].Parts[0].Geometry.Contours.SelectMany(c => c.Segments).Single(s => s.Center != null);
|
|
var holeBox = plan.Fit.Rect(new ReportBounds(hole.Center!.X - hole.Radius, hole.Center.Y - hole.Radius,
|
|
hole.Center.X + hole.Radius, hole.Center.Y + hole.Radius));
|
|
Assert.False(label.Box.IntersectsWith(holeBox));
|
|
}
|
|
|
|
[Fact]
|
|
public void Plan_LabelsSitAtThePolyLabelPoleLikePlateView()
|
|
{
|
|
// L-shape: the bounding-box center lies in the notch, outside the material.
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
foreach (var (x, y) in new[] { (10.0, 0.0), (10, 2), (2, 2), (2, 10), (0, 10), (0, 0) })
|
|
program.Codes.Add(new LinearMove(x, y));
|
|
var nest = new Nest("L test") { Units = Units.Inches };
|
|
var drawing = new Drawing("L bracket", program);
|
|
nest.Drawings.Add(drawing);
|
|
var plate = new Plate(24, 48) { Quantity = 1 };
|
|
plate.Parts.Add(new Part(drawing, new Vector(4, 4)));
|
|
plate.Parts.Add(new Part(NestReportTestData.CreateHoledDrawing("Holed", 1), new Vector(20, 4)));
|
|
nest.Plates.Add(plate);
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
|
|
var plan = NestReportDiagram.Plan(snapshot.Plates[0], 720, 320);
|
|
|
|
foreach (var label in plan.Labels)
|
|
{
|
|
var geometry = snapshot.Plates[0].Parts[label.Part].Geometry;
|
|
var origin = new Vector(geometry.Bounds.Left, geometry.Bounds.Bottom);
|
|
var rings = geometry.Contours.Select(contour => ClipperBridge.Flatten(ToShape(contour),
|
|
NestReportDiagram.ChordTolerance(geometry.Bounds), circumscribe: false))
|
|
.OrderByDescending(ring => ring.Area()).ToList();
|
|
foreach (var ring in rings)
|
|
ring.Offset(-origin.X, -origin.Y);
|
|
var pole = PolyLabel.Find(rings[0], rings.Skip(1).ToList(), NestReportDiagram.PolePrecision(geometry.Bounds));
|
|
var expected = plan.Fit.Point(pole.X + origin.X, pole.Y + origin.Y);
|
|
// Pole of the placement-independent copy, to within the search precision.
|
|
var tolerance = NestReportDiagram.PolePrecision(geometry.Bounds) * plan.Fit.Scale;
|
|
Assert.InRange(label.Box.X + label.Box.Width / 2 - expected.X, -tolerance, tolerance);
|
|
Assert.InRange(label.Box.Y + label.Box.Height / 2 - expected.Y, -tolerance, tolerance);
|
|
}
|
|
var l = plan.Labels.Single(label => label.Part == 0);
|
|
var center = plan.Fit.Model(new PdfSharp.Drawing.XPoint(l.Box.X + l.Box.Width / 2, l.Box.Y + l.Box.Height / 2));
|
|
// The pole of this L is in its corner square, about 1.17 in from both outer edges.
|
|
Assert.InRange(center.X - 4, 0.9, 1.4);
|
|
Assert.InRange(center.Y - 4, 0.9, 1.4);
|
|
}
|
|
|
|
[Fact]
|
|
public void Plan_IdenticalRepeatedPartsGetTheSameLabelPositionOnEveryCopy()
|
|
{
|
|
// A square with a central hole has four equally deep poles; every copy must pick the same one.
|
|
var snapshot = NestReportBuilder.Capture(NestReportTestData.CreateDenseNest(), NestReportTestData.GeneratedAt);
|
|
var plan = NestReportDiagram.Plan(snapshot.Plates[0], 720, 320);
|
|
var offsets = plan.Labels.GroupBy(label => label.Id).SelectMany(group =>
|
|
{
|
|
var relative = group.Select(label =>
|
|
{
|
|
var bounds = plan.Fit.Rect(snapshot.Plates[0].Parts[label.Part].Geometry.Bounds);
|
|
return (X: label.Box.X - bounds.X, Y: label.Box.Y - bounds.Y);
|
|
}).ToList();
|
|
return relative.Select(offset => (offset.X - relative[0].X, offset.Y - relative[0].Y));
|
|
});
|
|
Assert.All(offsets, delta =>
|
|
{
|
|
Assert.InRange(delta.Item1, -0.01, 0.01);
|
|
Assert.InRange(delta.Item2, -0.01, 0.01);
|
|
});
|
|
Assert.Equal(2, plan.Labels.Count(label => label.Id == "1"));
|
|
}
|
|
|
|
private static OpenNest.Geometry.Shape ToShape(ReportContour contour)
|
|
{
|
|
var shape = new OpenNest.Geometry.Shape();
|
|
foreach (var segment in contour.Segments)
|
|
{
|
|
var start = new Vector(segment.Start.X, segment.Start.Y);
|
|
if (segment.Center == null)
|
|
shape.Entities.Add(new Line(start, new Vector(segment.End.X, segment.End.Y)));
|
|
else
|
|
shape.Entities.Add(new Circle(new Vector(segment.Center.X, segment.Center.Y), segment.Radius));
|
|
}
|
|
return shape;
|
|
}
|
|
|
|
[Fact]
|
|
public void Write_TabbedLeadInFixtureSurvivesSaveReloadWithStaleTabFlags()
|
|
{
|
|
var nest = NestReportTestData.CreateTabbedNest();
|
|
var original = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
using var stream = new MemoryStream();
|
|
Assert.True(new NestWriter(nest).Write(stream));
|
|
var loaded = new NestReader(new MemoryStream(stream.ToArray())).Read();
|
|
foreach (var part in loaded.Plates[0].Parts)
|
|
{
|
|
if (part.CuttingParameters != null)
|
|
part.CuttingParameters.TabsEnabled = false;
|
|
}
|
|
if (loaded.Plates[0].CuttingParameters != null)
|
|
loaded.Plates[0].CuttingParameters.TabsEnabled = false;
|
|
|
|
var reloaded = NestReportBuilder.Capture(loaded, NestReportTestData.GeneratedAt);
|
|
|
|
var before = Assert.Single(original.Plates[0].Parts).Geometry;
|
|
var after = Assert.Single(reloaded.Plates[0].Parts).Geometry;
|
|
Assert.True(after.StrokeOnly);
|
|
Assert.Equal(before.Contours.Select(c => c.Closed), after.Contours.Select(c => c.Closed));
|
|
Assert.Equal(before.Contours.Select(c => c.Segments.Length), after.Contours.Select(c => c.Segments.Length));
|
|
Assert.Equal(before.Bounds.Left, after.Bounds.Left, 6);
|
|
Assert.Equal(before.Bounds.Bottom, after.Bounds.Bottom, 6);
|
|
Assert.Equal(before.Bounds.Right, after.Bounds.Right, 6);
|
|
Assert.Equal(before.Bounds.Top, after.Bounds.Top, 6);
|
|
Write(loaded, "reloaded.pdf");
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_InvalidLaterPlateFailsWithItsNumberAndNoOutput()
|
|
{
|
|
var nest = NestReportTestData.CreateMultiPlateNest();
|
|
((LinearMove)nest.Plates[3].Parts[0].Program.Codes[1]).EndPoint = new Vector(double.NaN, 0);
|
|
var path = Path.Combine(directory, "keep.report.pdf");
|
|
File.WriteAllText(path, "keep");
|
|
var before = NestReportBuilderTests.Fingerprint(nest);
|
|
|
|
var error = Assert.Throws<InvalidOperationException>(() =>
|
|
NestPdfWriter.Write(NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt), path));
|
|
|
|
Assert.Contains("Plate 4, part 1", error.Message);
|
|
Assert.Equal("keep", File.ReadAllText(path));
|
|
Assert.Equal(new[] { path }, Directory.GetFiles(directory));
|
|
Assert.Equal(before, NestReportBuilderTests.Fingerprint(nest));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(512)]
|
|
[InlineData(40_000)]
|
|
public void Write_LateMultiPageWriteFailureLeavesExistingReportAndSourceUnchanged(long failAfter)
|
|
{
|
|
var nest = NestReportTestData.CreateDenseNest();
|
|
var path = Path.Combine(directory, "existing.report.pdf");
|
|
var original = "%PDF previous report"u8.ToArray();
|
|
File.WriteAllBytes(path, original);
|
|
var before = NestReportBuilderTests.Fingerprint(nest);
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
|
|
Assert.Throws<IOException>(() => NestPdfWriter.Write(snapshot, path,
|
|
stream => new NestPdfWriterTests.FailingStream(stream, failAfter)));
|
|
|
|
Assert.Equal(original, File.ReadAllBytes(path));
|
|
Assert.Equal(new[] { path }, Directory.GetFiles(directory));
|
|
Assert.Equal(before, NestReportBuilderTests.Fingerprint(nest));
|
|
}
|
|
|
|
[Fact]
|
|
public void Write_SuccessLeavesSourceQuantitiesProgramsAndTimestampsUnchanged()
|
|
{
|
|
var nest = NestReportTestData.CreateDenseNest();
|
|
var before = NestReportBuilderTests.Fingerprint(nest);
|
|
Write(nest);
|
|
Assert.Equal(before, NestReportBuilderTests.Fingerprint(nest));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ranges_CompressesConsecutivePlateNumbersLosslessly()
|
|
{
|
|
Assert.Equal("-", ReportText.Ranges([]));
|
|
Assert.Equal("1", ReportText.Ranges([1]));
|
|
Assert.Equal("1-3, 5, 7-8", ReportText.Ranges([1, 2, 3, 5, 7, 8]));
|
|
Assert.Equal("A", ReportText.RowName(0));
|
|
Assert.Equal("Z", ReportText.RowName(25));
|
|
Assert.Equal("AA", ReportText.RowName(26));
|
|
}
|
|
|
|
[Fact]
|
|
public void Wrap_BreaksUnbrokenTokensWithoutDroppingCharacters()
|
|
{
|
|
ReportFonts.Initialize();
|
|
var font = new PdfSharp.Drawing.XFont(ReportFonts.Family, 9);
|
|
var text = "short " + string.Concat(Enumerable.Repeat("0123456789", 8)) + "\nsecond line";
|
|
var lines = ReportText.Wrap(text, font, 60);
|
|
Assert.All(lines, line => Assert.True(ReportText.Size(line, font).Width <= 59));
|
|
Assert.Equal(text.Replace("\n", "").Replace(" ", ""), string.Concat(lines).Replace(" ", ""));
|
|
Assert.Contains("second line", lines);
|
|
}
|
|
|
|
/// <summary>Text of the 2 row inside one table column, read from word boxes in reading order.</summary>
|
|
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 == "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);
|
|
// Nothing from this cell may extend into the next column.
|
|
Assert.DoesNotContain(words, word => word.Left < limit.Left && word.Right > limit.Left
|
|
&& word.Top > above.Bottom && word.Bottom < below.Top);
|
|
return string.Concat(cell.Select(word => word.Text));
|
|
}
|
|
|
|
private string Write(Nest nest, string name = "layout.report.pdf")
|
|
{
|
|
var path = Path.Combine(directory, name);
|
|
NestPdfWriter.Write(NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt), path);
|
|
return path;
|
|
}
|
|
}
|