diff --git a/OpenNest.Engine.Tests/Jobs/NestLayoutCheckTests.cs b/OpenNest.Engine.Tests/Jobs/NestLayoutCheckTests.cs index 18c61d8..6107690 100644 --- a/OpenNest.Engine.Tests/Jobs/NestLayoutCheckTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestLayoutCheckTests.cs @@ -6,32 +6,78 @@ namespace OpenNest.Engine.Tests.Jobs; public class NestLayoutCheckTests { + [Theory] + [InlineData(null, false)] + [InlineData(1, true)] + [InlineData(2, false)] + [InlineData(3, false)] + public void MaxPlatesCountsPhysicalSheetsIncludingEmptyOnes(int? maxPlates, bool exceedsLimit) + { + var stock = new NestPlateStock("sheet", new Size(48, 96)); + var job = new NestJob(Array.Empty(), new[] { stock }, new NestJobOptions(maxPlates: maxPlates)); + var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed, + new[] + { + new NestJobPlateResult(7, stock, Array.Empty()), + new NestJobPlateResult(42, stock, Array.Empty()), + }, Array.Empty(), Array.Empty()); + + var violations = NestLayoutCheck.Violations(job, result); + + if (exceedsLimit) + Assert.Contains(violations, v => v.Contains("MaxPlates") && v.Contains("2") && v.Contains("1")); + else + Assert.Empty(violations); + } + + [Theory] + [InlineData("ghost", 1, 1, 0)] + [InlineData(null, 1, 1, 0)] + [InlineData("p", double.NaN, 1, 0)] + [InlineData("p", 1, double.PositiveInfinity, 0)] + [InlineData("p", 1, 1, double.NaN)] + public void MalformedPlacementsProduceDiagnosticsRatherThanThrowing(string? partId, double x, double y, double rotation) + { + var stock = new NestPlateStock("sheet", new Size(48, 96)); + var job = new NestJob(new[] + { + new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()), 1), + }, new[] { stock }); + var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed, + new[] { new NestJobPlateResult(0, stock, new[] { new NestJobPlacement(partId!, 0, x, y, rotation) }) }, + Array.Empty(), Array.Empty()); + + var violations = NestLayoutCheck.Violations(job, result); + + Assert.NotEmpty(violations); + } + [Fact] public void TangentDiscsClearAtSafeMarginAcrossRadiiAnglesAndTolerances() { var count = 0; foreach (var radius in new[] { 0.1, 1.0, 10.0 }) - foreach (var tolerance in new[] { 0.0, 0.0005, 0.01 }) - foreach (var spacing in new[] { 0.0, 0.25 }) - { - var program = new Program(); - program.MoveTo(radius, 0); - program.Codes.Add(new ArcMove(radius, 0, 0, 0, RotationType.CW)); - var geometry = JobPartGeometry.Read(PartGeometrySnapshot.FromProgram(program)); - // Two inscribed engine outlines can underestimate true extent by t each. - var distance = 2 * radius + spacing - + NestTolerances.SafeClearanceMargin(tolerance) - 2 * tolerance; - for (var degrees = 0; degrees < 360; degrees += 15) - { - var angle = degrees * System.Math.PI / 180; - var a = new NestJobPlacement("disc", 0, 0.12345, -0.54321, angle / 3); - var b = new NestJobPlacement("disc", 1, a.X + distance * System.Math.Cos(angle), - a.Y + distance * System.Math.Sin(angle), -angle / 7); - Assert.True(NestLayoutCheck.Clears(geometry, a, geometry, b, spacing)); - Assert.True(NestLayoutCheck.Clears(geometry, b, geometry, a, spacing)); - count++; - } - } + foreach (var tolerance in new[] { 0.0, 0.0005, 0.01 }) + foreach (var spacing in new[] { 0.0, 0.25 }) + { + var program = new Program(); + program.MoveTo(radius, 0); + program.Codes.Add(new ArcMove(radius, 0, 0, 0, RotationType.CW)); + var geometry = JobPartGeometry.Read(PartGeometrySnapshot.FromProgram(program)); + // Two inscribed engine outlines can underestimate true extent by t each. + var distance = 2 * radius + spacing + + NestTolerances.SafeClearanceMargin(tolerance) - 2 * tolerance; + for (var degrees = 0; degrees < 360; degrees += 15) + { + var angle = degrees * System.Math.PI / 180; + var a = new NestJobPlacement("disc", 0, 0.12345, -0.54321, angle / 3); + var b = new NestJobPlacement("disc", 1, a.X + distance * System.Math.Cos(angle), + a.Y + distance * System.Math.Sin(angle), -angle / 7); + Assert.True(NestLayoutCheck.Clears(geometry, a, geometry, b, spacing)); + Assert.True(NestLayoutCheck.Clears(geometry, b, geometry, a, spacing)); + count++; + } + } Assert.Equal(432, count); } diff --git a/OpenNest.Engine.Tests/Jobs/NestPipelineTests.cs b/OpenNest.Engine.Tests/Jobs/NestPipelineTests.cs index 9519a60..fbbee52 100644 --- a/OpenNest.Engine.Tests/Jobs/NestPipelineTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestPipelineTests.cs @@ -1,3 +1,4 @@ +using OpenNest.CNC; using OpenNest.Engine.Jobs; using OpenNest.Geometry; @@ -45,6 +46,7 @@ public class NestPipelineTests var result = NestPipeline.Run(Request("Default", item)); Assert.True(result.IsValid, string.Join("; ", result.Violations)); + Assert.True(result.CanKeep); Assert.Equal(NestJobStatus.Complete, result.Status); var parts = result.Plates.SelectMany(p => p.Parts).ToList(); Assert.Equal(10, parts.Count); @@ -58,6 +60,8 @@ public class NestPipelineTests [Fact] public void OverlappingEngineOutputIsReportedByDrawingNameWithoutThrowing() { + var item = Item("bracket", 2); + var before = PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions; var engine = new StubEngine(job => OnePlate( job, @@ -66,29 +70,167 @@ public class NestPipelineTests ) ); - var result = NestPipeline.Run(engine, "Overlapper", Request("Overlapper", Item("bracket", 2))); + var result = NestPipeline.Run(engine, "Overlapper", Request("Overlapper", item)); Assert.False(result.IsValid); + Assert.True(result.CanKeep); Assert.Contains(result.Violations, v => v.Contains("bracket") && v.Contains("spacing")); Assert.Equal(2, result.Plates.Single().Parts.Count); + Assert.All(result.Plates.Single().Parts, part => Assert.Same(item.Drawing, part.BaseDrawing)); + Assert.Equal(before, PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions); + Assert.Equal(2, item.Quantity); + Assert.Equal(0, item.Drawing.Quantity.Nested); + } + + [Theory] + [InlineData("ghost")] + [InlineData(null)] + public void UnknownOrNullRequirementMakesTheEntireProposalNonKeepable(string? partId) + { + var item = Item("bracket", 1); + var before = PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions; + var engine = new StubEngine(job => new NestJobResult( + NestJobStatus.Complete, + NestJobStopReason.Completed, + new[] + { + new NestJobPlateResult(0, job.Plates[0], new[] { new NestJobPlacement(job.Parts[0].Id, 0, 1, 1, 0) }), + new NestJobPlateResult(1, job.Plates[0], new[] { new NestJobPlacement(partId!, 0, 30, 1, 0) }), + }, + Array.Empty(), + Array.Empty() + )); + + var result = NestPipeline.Run(engine, "Ghost", Request("Ghost", item)); + + Assert.False(result.IsValid); + Assert.False(result.CanKeep); + Assert.Contains(result.Violations, v => v.Contains(partId ?? "null") && v.Contains("Plate 1")); + Assert.Empty(result.Plates); + Assert.Equal(2, result.Raw.Plates.Count); + Assert.Equal(before, PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions); + Assert.Equal(1, item.Quantity); + Assert.Equal(0, item.Drawing.Quantity.Nested); + } + + [Theory] + [InlineData(double.NaN, 1, 0, "X")] + [InlineData(double.PositiveInfinity, 1, 0, "X")] + [InlineData(double.NegativeInfinity, 1, 0, "X")] + [InlineData(1, double.NaN, 0, "Y")] + [InlineData(1, double.PositiveInfinity, 0, "Y")] + [InlineData(1, double.NegativeInfinity, 0, "Y")] + [InlineData(1, 1, double.NaN, "Rotation")] + [InlineData(1, 1, double.PositiveInfinity, "Rotation")] + [InlineData(1, 1, double.NegativeInfinity, "Rotation")] + public void NonfinitePoseIsReportedAndNeverProposed(double x, double y, double rotation, string field) + { + var item = Item("bracket", 2); + var before = PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions; + var engine = new StubEngine(job => OnePlate(job, + new NestJobPlacement(job.Parts[0].Id, 0, 1, 1, 0), + new NestJobPlacement(job.Parts[0].Id, 1, x, y, rotation))); + + var result = NestPipeline.Run(engine, "Nonfinite", Request("Nonfinite", item)); + + Assert.False(result.IsValid); + Assert.False(result.CanKeep); + Assert.Empty(result.Plates); + Assert.Contains(result.Violations, v => v.Contains(field) && v.Contains("finite") && v.Contains("bracket")); + Assert.Equal(before, PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions); + Assert.Equal(2, item.Quantity); + Assert.Equal(0, item.Drawing.Quantity.Nested); } [Fact] - public void PlacementForUnknownRequirementIsAViolationNotSilentlyDropped() + public void ReportsEveryStructuralViolationWithoutMaterializingAnyGeometry() { - var engine = new StubEngine(job => - OnePlate( - job, - new NestJobPlacement(job.Parts[0].Id, 0, 1, 1, 0), - new NestJobPlacement("ghost", 0, 30, 1, 0) - ) - ); + var engine = new StubEngine(job => OnePlate(job, + new NestJobPlacement("ghost", 0, double.NaN, double.PositiveInfinity, double.NegativeInfinity), + new NestJobPlacement(null!, 0, 1, 1, 0), + new NestJobPlacement(job.Parts[0].Id, 0, double.NegativeInfinity, 1, 0))); - var result = NestPipeline.Run(engine, "Ghost", Request("Ghost", Item("bracket", 1))); + var result = NestPipeline.Run(engine, "Malformed", Request("Malformed", Item("bracket", 1))); + + Assert.False(result.CanKeep); + Assert.Empty(result.Plates); + Assert.Equal(6, result.Violations.Count); + Assert.Contains(result.Violations, v => v.Contains("ghost") && v.Contains("not part of this job")); + Assert.Contains(result.Violations, v => v.Contains("null") && v.Contains("not part of this job")); + Assert.Equal(2, result.Violations.Count(v => v.Contains("nonfinite X"))); + Assert.Contains(result.Violations, v => v.Contains("nonfinite Y")); + Assert.Contains(result.Violations, v => v.Contains("nonfinite Rotation")); + } + + [Fact] + public void ExceedingMaxPlatesIsInvalidButRemainsFullyKeepable() + { + var item = Item("bracket", 2); + var request = Request("TooManySheets", item) with { Options = new NestJobOptions(maxPlates: 1) }; + var engine = new StubEngine(job => new NestJobResult( + NestJobStatus.Complete, + NestJobStopReason.Completed, + Enumerable.Range(0, 2).Select(i => new NestJobPlateResult(i, job.Plates[0], + new[] { new NestJobPlacement(job.Parts[0].Id, i, 1, 1, 0) })), + new[] { new PartFulfillment(job.Parts[0].Id, 2, 2, 0) }, + new[] { new StockUsage(job.Plates[0].Id, 2, null) } + )); + + var result = NestPipeline.Run(engine, "TooManySheets", request); Assert.False(result.IsValid); - Assert.Contains(result.Violations, v => v.Contains("ghost")); - Assert.Single(result.Plates.Single().Parts); + Assert.True(result.CanKeep); + Assert.Contains(result.Violations, v => v.Contains("MaxPlates") && v.Contains("2") && v.Contains("1")); + Assert.Equal(2, result.Plates.Count); + Assert.All(result.Plates, plate => Assert.Same(item.Drawing, Assert.Single(plate.Parts).BaseDrawing)); + Assert.Equal(2, item.Quantity); + Assert.Equal(0, item.Drawing.Quantity.Nested); + } + + [Fact] + public void InvalidStockIsRejectedBeforeCallingAnArbitraryEngine() + { + var called = false; + var engine = new StubEngine(job => + { + called = true; + return OnePlate(job); + }); + var request = Request("Unchecked", Item("bracket", 1)) with + { + Stock = new[] { new NestPlateStock("bad", new Size(48, 96), partSpacing: double.NaN) }, + }; + + var error = Assert.Throws(() => NestPipeline.Run(engine, "Unchecked", request)); + + Assert.Contains("Invalid stock", error.Message); + Assert.False(called); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void InvalidGeometryIsRejectedBeforeCallingAnArbitraryEngine(bool nonfinite) + { + var item = Item("bracket", 1); + if (nonfinite) + ((Motion)item.Drawing.Program.Codes[1]).EndPoint = new Vector(double.NaN, 0); + else + item.Drawing.Program.Codes.Clear(); + var called = false; + var engine = new StubEngine(job => + { + called = true; + return OnePlate(job); + }); + + var error = Assert.Throws(() => + NestPipeline.Run(engine, "Unchecked", Request("Unchecked", item))); + + Assert.Contains("Geometry must contain finite motions", error.Message); + Assert.False(called); + Assert.Equal(1, item.Quantity); + Assert.Equal(0, item.Drawing.Quantity.Nested); } [Fact] diff --git a/OpenNest.Engine.Tests/Jobs/NestResultBinderTests.cs b/OpenNest.Engine.Tests/Jobs/NestResultBinderTests.cs new file mode 100644 index 0000000..0ec1a77 --- /dev/null +++ b/OpenNest.Engine.Tests/Jobs/NestResultBinderTests.cs @@ -0,0 +1,32 @@ +using OpenNest.Engine.Jobs; +using OpenNest.Engine.Jobs.Adapters; +using OpenNest.Geometry; + +namespace OpenNest.Engine.Tests.Jobs; + +public class NestResultBinderTests +{ + [Theory] + [InlineData("ghost", 1, 1, 0)] + [InlineData(null, 1, 1, 0)] + [InlineData("p", double.NaN, 1, 0)] + [InlineData("p", 1, double.PositiveInfinity, 0)] + [InlineData("p", 1, 1, double.NaN)] + public void MalformedSheetCannotBePartiallyBound(string? partId, double x, double y, double rotation) + { + var drawing = new Drawing("bracket", TestDrawingFactory.Rectangle()); + var before = PartGeometrySnapshot.FromProgram(drawing.Program).Motions; + var sheet = new NestJobPlateResult(0, new NestPlateStock("sheet", new Size(48, 96)), + new[] + { + new NestJobPlacement("p", 0, 1, 1, 0), + new NestJobPlacement(partId!, 1, x, y, rotation), + }); + var drawings = new Dictionary { ["p"] = drawing }; + + Assert.Throws(() => NestResultBinder.Bind(sheet, drawings)); + + Assert.Equal(before, PartGeometrySnapshot.FromProgram(drawing.Program).Motions); + Assert.Equal(0, drawing.Quantity.Nested); + } +} diff --git a/OpenNest.Engine/Jobs/Adapters/NestResultBinder.cs b/OpenNest.Engine/Jobs/Adapters/NestResultBinder.cs index 0187d73..559383c 100644 --- a/OpenNest.Engine/Jobs/Adapters/NestResultBinder.cs +++ b/OpenNest.Engine/Jobs/Adapters/NestResultBinder.cs @@ -7,8 +7,8 @@ namespace OpenNest.Engine.Jobs.Adapters; /// /// Binds one result sheet's poses to the caller's own drawings. The pose semantics match /// : rotate about the snapshot origin, then translate. -/// Placements whose requirement ID is not in the map are skipped here; the pipeline reports -/// them as violations instead of dropping them silently. +/// Every pose must be finite and refer to a mapped drawing. Malformed sheets are rejected +/// before binding any parts; the pipeline reports these violations without invoking binding. /// public static class NestResultBinder { @@ -19,12 +19,17 @@ public static class NestResultBinder { ArgumentNullException.ThrowIfNull(sheet); ArgumentNullException.ThrowIfNull(drawingsByPartId); + foreach (var pose in sheet.Placements) + { + if (pose.PartId == null || !drawingsByPartId.TryGetValue(pose.PartId, out var drawing) || drawing == null) + throw new ArgumentException("Result contains a requirement not present in the drawing map.", nameof(sheet)); + if (!double.IsFinite(pose.X) || !double.IsFinite(pose.Y) || !double.IsFinite(pose.Rotation)) + throw new ArgumentException("Result contains a nonfinite placement pose.", nameof(sheet)); + } var parts = new List(sheet.Placements.Count); foreach (var pose in sheet.Placements) { - if (!drawingsByPartId.TryGetValue(pose.PartId, out var drawing)) - continue; - var part = new Part(drawing); + var part = new Part(drawingsByPartId[pose.PartId]); part.Rotate(pose.Rotation); part.Location = new Vector(pose.X, pose.Y); part.UpdateBounds(); diff --git a/OpenNest.Engine/Jobs/NestLayoutCheck.cs b/OpenNest.Engine/Jobs/NestLayoutCheck.cs index b0ca987..8481c36 100644 --- a/OpenNest.Engine/Jobs/NestLayoutCheck.cs +++ b/OpenNest.Engine/Jobs/NestLayoutCheck.cs @@ -21,7 +21,8 @@ namespace OpenNest.Engine.Jobs; /// public static class NestLayoutCheck { - /// Checks bounds, spacing, quantities, offered stock and rotation policies. + /// Checks placement structure, bounds, spacing, quantities, offered stock, + /// MaxPlates and rotation policies. Unrepresentable placements skip geometry checks. /// Requirement IDs are used in messages. Instance indices and fulfillment metadata are /// not checked, matching the benchmark contract. public static IReadOnlyList Violations(NestJob job, NestJobResult result) @@ -36,21 +37,61 @@ public static class NestLayoutCheck NestJob job, NestJobResult result, IReadOnlyDictionary displayNames + ) => Violations(job, result, displayNames, out _); + + internal static IReadOnlyList Violations( + NestJob job, + NestJobResult result, + IReadOnlyDictionary displayNames, + out bool canKeep ) { ArgumentNullException.ThrowIfNull(job); ArgumentNullException.ThrowIfNull(result); ArgumentNullException.ThrowIfNull(displayNames); - var materialized = NestResultMaterializer.Materialize(job, result); - var requirements = job.Parts.ToDictionary(p => materialized.DrawingsByPartId[p.Id], - p => (Name: displayNames.GetValueOrDefault(p.Id, p.Id), p.Quantity)); - var runs = materialized.Nest.Plates.Select(p => (p, p.Parts.ToList())).ToList(); - var violations = Validate(runs, requirements); + var violations = new List(); + ValidateStructure(job, result, displayNames, violations); + canKeep = violations.Count == 0; + if (canKeep) + { + var materialized = NestResultMaterializer.Materialize(job, result); + var requirements = job.Parts.ToDictionary(p => materialized.DrawingsByPartId[p.Id], + p => (Name: displayNames.GetValueOrDefault(p.Id, p.Id), p.Quantity)); + var runs = materialized.Nest.Plates.Select(p => (p, p.Parts.ToList())).ToList(); + violations.AddRange(Validate(runs, requirements)); + } ValidateAgainstJob(job, result, job.Parts.ToDictionary(p => p.Id, p => displayNames.GetValueOrDefault(p.Id, p.Id)), violations); return violations; } + private static void ValidateStructure( + NestJob job, + NestJobResult result, + IReadOnlyDictionary displayNames, + List violations + ) + { + var partIds = job.Parts.Select(p => p.Id).ToHashSet(StringComparer.Ordinal); + foreach (var sheet in result.Plates) + { + for (var i = 0; i < sheet.Placements.Count; i++) + { + var pose = sheet.Placements[i]; + var name = pose.PartId == null ? "" : displayNames.GetValueOrDefault(pose.PartId, pose.PartId); + var description = $"Plate {sheet.PlateIndex} placement {i} for '{name}'"; + if (pose.PartId == null || !partIds.Contains(pose.PartId)) + violations.Add($"{description}, which is not part of this job"); + if (!double.IsFinite(pose.X)) + violations.Add($"{description} has nonfinite X ({pose.X})"); + if (!double.IsFinite(pose.Y)) + violations.Add($"{description} has nonfinite Y ({pose.Y})"); + if (!double.IsFinite(pose.Rotation)) + violations.Add($"{description} has nonfinite Rotation ({pose.Rotation})"); + } + } + } + /// Tests material clearance using the benchmark's conservative outlines. /// The leftmost raw outline is inflated, matching the full-layout sweep; ties retain /// argument order. Geometry is cloned before transformation. @@ -121,7 +162,7 @@ public static class NestLayoutCheck /// Checks what the materialized layout cannot show: every sheet must be /// one of the job's own stock entries (an engine may not invent a sheet /// size or loosen its spacing/edge settings, which the layout checks - /// would otherwise trust), finite stock may not be overdrawn, and every + /// would otherwise trust), finite stock and MaxPlates may not be overdrawn, and every /// placement's rotation must satisfy its part's RotationPolicy. /// internal static void ValidateAgainstJob( @@ -135,6 +176,9 @@ public static class NestLayoutCheck var partsById = job.Parts.ToDictionary(p => p.Id); var sheetsUsed = new Dictionary(); + if (job.Options.MaxPlates is int maxPlates && jobResult.Plates.Count > maxPlates) + result.Add($"Used {jobResult.Plates.Count} sheet(s) but the job MaxPlates limit is {maxPlates}"); + foreach (var sheet in jobResult.Plates) { if ( @@ -167,8 +211,10 @@ public static class NestLayoutCheck { foreach (var placement in sheet.Placements) { - if (!partsById.TryGetValue(placement.PartId, out var part)) - continue; // reported by ValidateQuantities + if (placement.PartId == null + || !partsById.TryGetValue(placement.PartId, out var part) + || !double.IsFinite(placement.Rotation)) + continue; // reported by ValidateStructure if (!part.Rotation.Allows(placement.Rotation)) { diff --git a/OpenNest.Engine/Jobs/NestPipeline.cs b/OpenNest.Engine/Jobs/NestPipeline.cs index 4cf3692..858a39b 100644 --- a/OpenNest.Engine/Jobs/NestPipeline.cs +++ b/OpenNest.Engine/Jobs/NestPipeline.cs @@ -34,6 +34,7 @@ public sealed class NestPipelineResult NestJobResult raw, IReadOnlyList plates, IReadOnlyList violations, + bool canKeep, TimeSpan solveTime, TimeSpan validationTime ) @@ -43,6 +44,7 @@ public sealed class NestPipelineResult Raw = raw; Plates = plates; Violations = violations; + CanKeep = canKeep; SolveTime = solveTime; ValidationTime = validationTime; } @@ -53,6 +55,9 @@ public sealed class NestPipelineResult public IReadOnlyList Plates { get; } public IReadOnlyList Violations { get; } public bool IsValid => Violations.Count == 0; + /// True when every placement can be represented, even if layout rules fail. + /// False for unknown/null requirement IDs or nonfinite poses; Plates is then empty. + public bool CanKeep { get; } public NestJobStatus Status => Raw.Status; public NestJobStopReason StopReason => Raw.StopReason; public TimeSpan SolveTime { get; } @@ -109,6 +114,7 @@ public static class NestPipeline } var job = new NestJob(parts, request.Stock, request.Options); + NestJobValidator.Validate(job); var clock = Stopwatch.StartNew(); var raw = @@ -118,16 +124,22 @@ public static class NestPipeline token.ThrowIfCancellationRequested(); clock.Restart(); - var violations = Validate(job, raw, drawingsByPartId); + var names = drawingsByPartId.ToDictionary( + kv => kv.Key, + kv => kv.Value.Name ?? kv.Key, + StringComparer.Ordinal + ); + var violations = NestLayoutCheck.Violations(job, raw, names, out var canKeep); var validationTime = clock.Elapsed; - var plates = raw - .Plates.Select(sheet => new ProposedPlate( + var plates = canKeep + ? raw.Plates.Select(sheet => new ProposedPlate( sheet.PlateIndex, sheet.Stock, NestResultBinder.Bind(sheet, drawingsByPartId) )) - .ToList(); + .ToList() + : new List(); token.ThrowIfCancellationRequested(); return new NestPipelineResult( @@ -136,55 +148,9 @@ public static class NestPipeline raw, plates, violations, + canKeep, solveTime, validationTime ); } - - /// Benchmark validation, with messages naming the caller's drawings rather than - /// internal requirement IDs. Placements for unknown requirements are reported and then - /// excluded so the remaining layout is still checked. - private static IReadOnlyList Validate( - NestJob job, - NestJobResult raw, - IReadOnlyDictionary drawingsByPartId - ) - { - var violations = new List(); - var known = raw; - var unknown = raw - .Plates.SelectMany(sheet => - sheet.Placements.Where(p => !drawingsByPartId.ContainsKey(p.PartId)) - .Select(p => (sheet.PlateIndex, p.PartId)) - ) - .ToList(); - - if (unknown.Count > 0) - { - foreach (var group in unknown.GroupBy(u => (u.PlateIndex, u.PartId))) - violations.Add( - $"Plate {group.Key.PlateIndex} has {group.Count()} placement(s) for '{group.Key.PartId}', which is not part of this job" - ); - - known = new NestJobResult( - raw.Status, - raw.StopReason, - raw.Plates.Select(sheet => new NestJobPlateResult( - sheet.PlateIndex, - sheet.Stock, - sheet.Placements.Where(p => drawingsByPartId.ContainsKey(p.PartId)) - )), - raw.Fulfillment, - raw.StockUsage - ); - } - - var names = drawingsByPartId.ToDictionary( - kv => kv.Key, - kv => kv.Value.Name ?? kv.Key, - StringComparer.Ordinal - ); - violations.AddRange(NestLayoutCheck.Violations(job, known, names)); - return violations; - } }