mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-14 10:15:07 -05:00
The shop needs each sheet's weight for handling, so the summary's plates table gains a Sheet weight column: one full sheet from its area, the nest thickness and the nest (or catalog) density. A drawing's own material is not used, since it doesn't say what the stock is; with no density the column is left out. Plates with copies now put the total cut time (and total weight) on a second line instead of after a comma, which wrapped mid-phrase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
299 lines
13 KiB
C#
299 lines
13 KiB
C#
using System.Text.Json;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.IO;
|
|
using OpenNest.Reporting;
|
|
|
|
namespace OpenNest.Tests.Reporting;
|
|
|
|
public class NestReportBuilderTests
|
|
{
|
|
[Fact]
|
|
public void Capture_WithoutCutTimeLeavesEstimatesOut()
|
|
{
|
|
var snapshot = NestReportBuilder.Capture(NestReportTestData.CreateNest(), NestReportTestData.GeneratedAt);
|
|
|
|
Assert.False(snapshot.HasCutTimes);
|
|
Assert.Null(snapshot.TotalCutTime);
|
|
Assert.All(snapshot.Plates, plate => Assert.Null(plate.CutTime));
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_WeighsEachPartFromNetAreaThicknessAndPoundsPerCubicInch()
|
|
{
|
|
var snapshot = NestReportBuilder.Capture(NestReportTestData.CreateNest(), NestReportTestData.GeneratedAt);
|
|
|
|
Assert.Equal("lb", snapshot.WeightUnits);
|
|
// The 10 x 10 bracket's 1 in radius hole is not material.
|
|
Assert.Equal((100 - System.Math.PI) * 0.125 * 0.2836, snapshot.Drawings[0].Weight!.Value, 9);
|
|
Assert.Equal(8 * 0.125 * 0.2836, snapshot.Drawings[1].Weight!.Value, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_WeighsEachFullSheetFromItsAreaThicknessAndTheNestDensity()
|
|
{
|
|
var snapshot = NestReportBuilder.Capture(NestReportTestData.CreateNest(), NestReportTestData.GeneratedAt);
|
|
|
|
// One copy of the whole 24 x 48 sheet, not just the parts on it.
|
|
Assert.Equal(24 * 48 * 0.125 * 0.2836, snapshot.Plates[0].Weight!.Value, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_SheetWeightIgnoresTheDrawingsOwnMaterial()
|
|
{
|
|
var nest = new Nest("Weights") { Units = Units.Inches, Thickness = 0.125, Material = new Material("Steel", "", 0) };
|
|
var drawing = NestReportTestData.Rectangle("Plate", 1, 1);
|
|
drawing.Material = new Material("Steel", "", 0.2836);
|
|
nest.Drawings.Add(drawing);
|
|
var plate = new Plate(24, 48) { Quantity = 1 };
|
|
plate.Parts.Add(new Part(drawing));
|
|
nest.Plates.Add(plate);
|
|
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
|
|
Assert.NotNull(snapshot.Drawings[0].Weight);
|
|
Assert.Null(snapshot.Plates[0].Weight);
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_MillimeterJobConvertsTheVolumeAndReportsKilograms()
|
|
{
|
|
var nest = new Nest("Metric") { Units = Units.Millimeters, Thickness = 10, Material = new Material("Steel", "", 0.2836) };
|
|
nest.Drawings.Add(NestReportTestData.Rectangle("Block", 100, 100));
|
|
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
|
|
Assert.Equal("kg", snapshot.WeightUnits);
|
|
// 100 cm^3 of steel at 0.2836 lb/in^3 (7.85 g/cm^3) is 0.785 kg.
|
|
Assert.Equal(0.785, snapshot.Drawings[0].Weight!.Value, 3);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0.125, 0, 0, null)]
|
|
[InlineData(0, 0.2836, 0, null)]
|
|
[InlineData(0.125, 0, 0.1, 0.0125)]
|
|
[InlineData(0.125, 0.2836, 0.1, 0.03545)]
|
|
public void Capture_WeightNeedsThicknessAndPrefersTheNestDensity(double thickness, double nestDensity,
|
|
double drawingDensity, double? expected)
|
|
{
|
|
var nest = new Nest("Weights") { Units = Units.Inches, Thickness = thickness, Material = new Material("Steel", "", nestDensity) };
|
|
var drawing = NestReportTestData.Rectangle("Plate", 1, 1);
|
|
drawing.Material = new Material("Steel", "", drawingDensity);
|
|
nest.Drawings.Add(drawing);
|
|
|
|
var weight = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt).Drawings[0].Weight;
|
|
|
|
if (expected == null)
|
|
Assert.Null(weight);
|
|
else
|
|
Assert.Equal(expected.Value, weight!.Value, 9);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, 0.287, 0.1, 0.0358750)]
|
|
[InlineData(0.2836, 0.287, 0.1, 0.03545)]
|
|
[InlineData(0, 0, 0.1, 0.0125)]
|
|
[InlineData(0, 0, 0, null)]
|
|
public void Capture_CatalogDensityStandsInWhenTheNestMaterialHasNone(double nestDensity, double catalogDensity,
|
|
double drawingDensity, double? expected)
|
|
{
|
|
var nest = new Nest("Catalog") { Units = Units.Inches, Thickness = 0.125, Material = new Material("Stainless Steel", "310", nestDensity) };
|
|
var drawing = NestReportTestData.Rectangle("Plate", 1, 1);
|
|
drawing.Material = new Material("Stainless Steel", "310", drawingDensity);
|
|
nest.Drawings.Add(drawing);
|
|
|
|
var weight = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt, catalogDensity: catalogDensity).Drawings[0].Weight;
|
|
|
|
if (expected == null)
|
|
Assert.Null(weight);
|
|
else
|
|
Assert.Equal(expected.Value, weight!.Value, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_WithCutTimeEstimatesEachPlateLikeCalculateCutTime()
|
|
{
|
|
var nest = NestReportTestData.CreateMultiPlateNest();
|
|
var parameters = new CutParameters
|
|
{
|
|
Feedrate = 100,
|
|
HoleFeedrate = 60,
|
|
RapidTravelRate = 600,
|
|
PierceTime = TimeSpan.FromSeconds(1),
|
|
Units = Units.Inches,
|
|
Motion = new MotionProfile { CutAcceleration = 100, RapidAcceleration = 200, CornerDeviation = 0.001 },
|
|
};
|
|
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt, parameters);
|
|
|
|
Assert.True(snapshot.HasCutTimes);
|
|
var expectedTotal = TimeSpan.Zero;
|
|
foreach (var plate in snapshot.Plates)
|
|
{
|
|
var source = nest.Plates[plate.Number - 1];
|
|
Assert.Equal(CutSimulation.Build(source, parameters).TotalTime, plate.CutTime);
|
|
Assert.True(plate.CutTime > TimeSpan.Zero);
|
|
expectedTotal += plate.CutTime!.Value * plate.Copies;
|
|
}
|
|
Assert.Equal(expectedTotal, snapshot.TotalCutTime);
|
|
Assert.Equal(Timing.CalculateTime(nest, parameters), snapshot.TotalCutTime);
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_ReconcilesOrderedReferenceUnionWithoutCountingCutoffs()
|
|
{
|
|
var nest = NestReportTestData.CreateNest();
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
|
|
Assert.Equal("Report test job", snapshot.Name);
|
|
Assert.Equal("Test customer", snapshot.Customer);
|
|
Assert.Equal("Synthetic report test data", snapshot.Notes);
|
|
Assert.Equal("Steel", snapshot.Material);
|
|
Assert.Equal("A36", snapshot.Grade);
|
|
Assert.Equal(0.125, snapshot.Thickness);
|
|
Assert.Equal("in", snapshot.Units);
|
|
Assert.Equal(NestReportTestData.GeneratedAt, snapshot.GeneratedAt);
|
|
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));
|
|
Assert.Equal(new long[] { 1, 0, 5, 3 }, snapshot.Drawings.Select(d => d.Shortage));
|
|
Assert.Equal(new long[] { 0, 1, 0, 0 }, snapshot.Drawings.Select(d => d.Extra));
|
|
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[] { "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);
|
|
Assert.Equal(0.125, plate.PartSpacing);
|
|
Assert.Equal(nest.Plates[0].Utilization(), plate.Utilization);
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_RecountsAcrossPlateCopiesWithWideIntegersAndUniquePlateNumbers()
|
|
{
|
|
var nest = NestReportTestData.CreateNest();
|
|
var drawing = nest.Plates[0].Parts[0].BaseDrawing;
|
|
// Change copies after adding: the legacy cached Nested count stays stale.
|
|
nest.Plates[0].Quantity = int.MaxValue;
|
|
var second = new Plate(24, 48) { Quantity = 3 };
|
|
second.Parts.Add(new Part(drawing));
|
|
second.Parts.Add(new Part(drawing));
|
|
nest.Plates.Add(second);
|
|
var cached = drawing.Quantity.Nested;
|
|
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
|
|
Assert.Equal(checked(2L * int.MaxValue + 6), snapshot.Drawings[0].Nested);
|
|
Assert.Equal(new[] { 1, 2 }, snapshot.Drawings[0].Plates);
|
|
Assert.Equal(checked((long)int.MaxValue + 3), snapshot.TotalSheets);
|
|
Assert.Equal(cached, drawing.Quantity.Nested);
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_IsRepeatableDoesNotMutateSourcesAndRetainsNoMutableReferences()
|
|
{
|
|
var nest = NestReportTestData.CreateNest();
|
|
var part = nest.Plates[0].Parts[0];
|
|
var drawing = part.BaseDrawing;
|
|
var source = Fingerprint(nest);
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
var serialized = JsonSerializer.Serialize(snapshot);
|
|
|
|
Assert.Equal(source, Fingerprint(nest));
|
|
Assert.Equal(serialized, JsonSerializer.Serialize(NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt)));
|
|
Assert.NotEmpty(snapshot.Plates);
|
|
nest.Name = "Edited";
|
|
nest.Material.Name = "Edited material";
|
|
nest.Plates[0].Size = new Size(100, 200);
|
|
nest.Plates[0].Quantity = 42;
|
|
part.Location = new Vector(500, 600);
|
|
((LinearMove)part.Program.Codes[1]).EndPoint = new Vector(99, 99);
|
|
((LinearMove)drawing.Program.Codes[1]).EndPoint = new Vector(88, 88);
|
|
drawing.Name = "Edited drawing";
|
|
drawing.Quantity.Required = 123;
|
|
nest.Drawings.Clear();
|
|
nest.Plates.Clear();
|
|
Assert.Equal(serialized, JsonSerializer.Serialize(snapshot));
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_EmptyAndDemandOnlyJobsKeepExplicitEmptyCollectionsAndOrdinalOrder()
|
|
{
|
|
var nest = new Nest("Empty test");
|
|
var empty = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
Assert.Equal("Empty test", empty.Name);
|
|
Assert.Empty(empty.Drawings);
|
|
Assert.Empty(empty.Plates);
|
|
Assert.Equal(0, empty.TotalSheets);
|
|
nest.Drawings.Add(NestReportTestData.Rectangle("zeta", 1, 1, 5));
|
|
nest.Drawings.Add(NestReportTestData.Rectangle("Alpha", 1, 1, 2));
|
|
var demand = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
Assert.Equal(new[] { "Alpha", "zeta" }, demand.Drawings.Select(d => d.Name));
|
|
Assert.All(demand.Drawings, d => { Assert.Equal(0, d.Nested); Assert.Equal(d.Required, d.Shortage); });
|
|
Assert.Empty(demand.Plates);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, 48, 1)]
|
|
[InlineData(24, -1, 1)]
|
|
[InlineData(double.NaN, 48, 1)]
|
|
[InlineData(24, double.PositiveInfinity, 1)]
|
|
[InlineData(24, 48, 0)]
|
|
[InlineData(24, 48, -1)]
|
|
public void Capture_RejectsInvalidPlateBeforeProducingSnapshot(double width, double length, int copies)
|
|
{
|
|
var nest = NestReportTestData.CreateNest();
|
|
nest.Plates[0].Size = new Size(width, length);
|
|
nest.Plates[0].Quantity = copies;
|
|
var before = Fingerprint(nest);
|
|
var error = Assert.Throws<InvalidOperationException>(() => NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt));
|
|
Assert.Contains("Plate 1", error.Message);
|
|
Assert.Equal(before, Fingerprint(nest));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1, 0, 0, 48, 24)]
|
|
[InlineData(2, -48, 0, 0, 24)]
|
|
[InlineData(3, -48, -24, 0, 0)]
|
|
[InlineData(4, 0, -24, 48, 0)]
|
|
public void Capture_UsesQuadrantAwareStockBounds(int quadrant, double left, double bottom, double right, double top)
|
|
{
|
|
var nest = NestReportTestData.CreateNest();
|
|
nest.Plates[0].Quadrant = quadrant;
|
|
var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt);
|
|
Assert.Equal(new ReportBounds(left, bottom, right, top), Assert.Single(snapshot.Plates).Bounds);
|
|
}
|
|
|
|
internal static string Fingerprint(Nest nest) => JsonSerializer.Serialize(new
|
|
{
|
|
nest.Name,
|
|
nest.Customer,
|
|
nest.Notes,
|
|
nest.Material,
|
|
nest.Thickness,
|
|
nest.Units,
|
|
nest.DateCreated,
|
|
nest.DateLastModified,
|
|
Drawings = nest.Drawings.Select(d => new { d.Id, d.Name, d.Quantity, Main = NestWriter.GetProgramText(d.Program), Subs = NestWriter.GetSubProgramsText(d.Program) }),
|
|
Plates = nest.Plates.Select(p => new
|
|
{
|
|
p.Size,
|
|
p.Quantity,
|
|
p.PartSpacing,
|
|
p.Quadrant,
|
|
Parts = p.Parts.Select(part => new
|
|
{
|
|
part.BaseDrawing.Id,
|
|
part.Location,
|
|
part.Rotation,
|
|
part.HasManualLeadIns,
|
|
part.LeadInsLocked,
|
|
Main = NestWriter.GetProgramText(part.Program),
|
|
Subs = NestWriter.GetSubProgramsText(part.Program),
|
|
}),
|
|
}),
|
|
}, new JsonSerializerOptions { IncludeFields = true, NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals });
|
|
}
|