feat(engine): fill cutouts first in every whole-job run

NestPipeline.Run now bundles parts that fit other parts' enclosed cutouts
into their frames before the selected engine, for desktop, Console, MCP and
API alike. A valid complete bundled trial is kept; a refused trial falls back
to the ordinary solve (reason on RefusedCutoutTrial), and an incomplete trial
is compared with it, keeping whichever valid proposal places more. An
incomplete but valid trial is now keepable like any incomplete result.
Expanded sheets list each frame's inserts before the frame, so the committed
order cuts every insert before the cutout that releases its slug.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-17 12:51:18 -04:00
1 parent 6ac56c4a3f
commit 4a1587d6bd
10 files changed
+363 -137

No files matched your search

@@ -28,14 +28,15 @@ public class CutoutPipelinePreviewTests
public void RegisteredEngineExpandsOnlyPhysicalOriginalDrawings(string engine)
{
var request = Request(engine);
var result = NestPipeline.RunCutoutPreview(request);
var result = NestPipeline.RunCutoutTrial(request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.True(result.CanKeep);
var plate = Assert.Single(result.Plates);
Assert.Equal(2, plate.Parts.Count);
Assert.Same(request.Items[0].Drawing, plate.Parts[0].BaseDrawing);
Assert.Same(request.Items[1].Drawing, plate.Parts[1].BaseDrawing);
Assert.Equal(new[] { "part-0", "part-1" }, result.Raw.Plates[0].Placements.Select(p => p.PartId));
// The insert precedes its frame so placed order cuts it before the frame's cutout.
Assert.Same(request.Items[1].Drawing, plate.Parts[0].BaseDrawing);
Assert.Same(request.Items[0].Drawing, plate.Parts[1].BaseDrawing);
Assert.Equal(new[] { "part-1", "part-0" }, result.Raw.Plates[0].Placements.Select(p => p.PartId));
Assert.All(result.Raw.Fulfillment, f => Assert.Equal((1, 1, 0), (f.Requested, f.Placed, f.Unplaced)));
Assert.Equal(0, request.Items[0].Drawing.Quantity.Nested);
Assert.Equal(0, request.Items[1].Drawing.Quantity.Nested);
@@ -66,7 +67,7 @@ public class CutoutPipelinePreviewTests
{
var called = 0;
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
called++;
var proxy = Assert.Single(job.Parts);
@@ -81,24 +82,23 @@ public class CutoutPipelinePreviewTests
}
[Fact]
public void IncompleteEngineDoesNotBindReservedInsertEvenWithInvalidConsent()
public void UnplacedProxyLeavesItsReservedInsertAsUnplacedDemand()
{
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(new Stub(job => Result(job)), "stub", request);
Assert.False(result.CanKeep);
var result = NestPipeline.RunCutoutTrial(new Stub(job => Result(job)), "stub", request);
Assert.True(result.CanKeep);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Empty(result.Plates);
Assert.Contains(result.Violations, v => v.Contains("did not complete"));
Assert.All(result.Raw.Fulfillment, f => Assert.Equal((1, 0, 1),
(f.Requested, f.Placed, f.Unplaced)));
Assert.Equal(0, request.Items[1].Drawing.Quantity.Nested);
AssertCannotCommit(result, request);
}
[Fact]
public void ForgedFulfillmentOrDuplicateIndexDoesNotBindComposite()
{
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var id = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-")).Id;
var pose = new NestJobPlacement(id, 1, 11, 11, 0);
@@ -115,7 +115,7 @@ public class CutoutPipelinePreviewTests
var request = Request("stub");
request.Items[1].Quantity = 2;
request = request with { Stock = new[] { new NestPlateStock("sheet", new Size(26, 26), 1, 0.25) } };
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var proxy = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-"));
var residual = job.Parts.Single(p => p.Id == "part-1");
@@ -124,7 +124,7 @@ public class CutoutPipelinePreviewTests
new NestJobPlacement(residual.Id, 0, 22, 1, 0));
}), "stub", request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(new[] { "part-0", "part-1", "part-1" },
Assert.Equal(new[] { "part-1", "part-0", "part-1" },
result.Raw.Plates.Single().Placements.Select(p => p.PartId));
Assert.Equal(new[] { 0, 1 }, result.Raw.Plates.Single().Placements
.Where(p => p.PartId == "part-1").Select(p => p.InstanceIndex));
@@ -136,7 +136,7 @@ public class CutoutPipelinePreviewTests
public void UntrustedStockOverdrawOrRotationCannotBindAComposite()
{
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var proxy = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-"));
return Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11,
@@ -146,7 +146,7 @@ public class CutoutPipelinePreviewTests
Assert.Empty(result.Plates);
Assert.Contains(result.Violations, v => v.Contains("rotation constraint"));
var overdraw = NestPipeline.RunCutoutPreview(new Stub(job => new NestJobResult(
var overdraw = NestPipeline.RunCutoutTrial(new Stub(job => new NestJobResult(
NestJobStatus.Complete, NestJobStopReason.Completed,
new[] { new NestJobPlateResult(0, job.Plates[0], new[] {
new NestJobPlacement(job.Parts[0].Id, 0, 11, 11, 0) }),
@@ -165,7 +165,7 @@ public class CutoutPipelinePreviewTests
cts.Cancel();
var engine = new Stub(_ => throw new Exception("Must not call Solve"));
Assert.Throws<OperationCanceledException>(() =>
NestPipeline.RunCutoutPreview(engine, "stub", Request("stub"), token: cts.Token));
NestPipeline.RunCutoutTrial(engine, "stub", Request("stub"), token: cts.Token));
}
[Fact]
@@ -173,7 +173,7 @@ public class CutoutPipelinePreviewTests
{
var request = Request("stub");
request.Items[1].Quantity = 2;
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var proxy = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-"));
return Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11, 0),
@@ -210,8 +210,8 @@ public class CutoutPipelinePreviewTests
{
var request = Request("stub");
var progress = new CollectProgress();
var result = NestPipeline.RunCutoutPreview(new ProgressStub(), "stub", request, progress);
Assert.False(result.CanKeep);
var result = NestPipeline.RunCutoutTrial(new ProgressStub(), "stub", request, progress);
Assert.Empty(result.Plates);
Assert.Single(progress.Events);
Assert.Equal(NestJobStage.EvaluatingCandidate, progress.Events[0].Stage);
Assert.Equal(0, progress.Events[0].CommittedPlates);
@@ -238,7 +238,7 @@ public class CutoutPipelinePreviewTests
request.Items[1].Quantity = 3;
request.Items[1].Drawing.Program = TestShapes.Rectangle(6, 6);
request = request with { Stock = new[] { new NestPlateStock("sheet", new Size(30, 30), 1, 0.25) } };
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var proxy = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-"));
Assert.Equal(2, job.Parts.Single(p => p.Id == "part-1").Quantity);
@@ -257,7 +257,7 @@ public class CutoutPipelinePreviewTests
public void ForgedFulfillmentOrStockMetadataCannotBind(bool fulfillment)
{
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var proxy = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-"));
var good = Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11, 0));
@@ -280,7 +280,7 @@ public class CutoutPipelinePreviewTests
new NestItem { Drawing = initial.Items[1].Drawing, Quantity = 1 } },
Stock = new[] { new NestPlateStock("sheet", new Size(30, 30), 1, 0.25) }
};
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var proxy = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-"));
var residual = job.Parts.Where(p => p.Id != proxy.Id)
@@ -289,9 +289,9 @@ public class CutoutPipelinePreviewTests
.Concat(residual).ToArray());
}), "stub", request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(new[] { "part-0", "part-1", "part-2" },
Assert.Equal(new[] { "part-1", "part-0", "part-2" },
result.Raw.Plates.Single().Placements.Select(p => p.PartId));
Assert.Same(result.Plates.Single().Parts[1].BaseDrawing,
Assert.Same(result.Plates.Single().Parts[0].BaseDrawing,
result.Plates.Single().Parts[2].BaseDrawing);
}
@@ -308,7 +308,7 @@ public class CutoutPipelinePreviewTests
Options = new NestJobOptions(maxPlates: 2)
};
var calls = 0;
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
calls++;
var proxies = job.Parts.Where(p => p.Id.StartsWith("__cutout-proxy-")).ToArray();
@@ -322,7 +322,7 @@ public class CutoutPipelinePreviewTests
Assert.Equal(1, calls);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(2, result.Raw.Plates.Count);
Assert.All(result.Raw.Plates, plate => Assert.Equal(new[] { "part-0", "part-1" },
Assert.All(result.Raw.Plates, plate => Assert.Equal(new[] { "part-1", "part-0" },
plate.Placements.Select(p => p.PartId)));
Assert.All(result.Raw.Fulfillment, f => Assert.Equal((2, 2, 0),
(f.Requested, f.Placed, f.Unplaced)));
@@ -331,7 +331,7 @@ public class CutoutPipelinePreviewTests
}
[Fact]
public void UnplacedFrameReleasesInsertDemandWithoutABindablePartialProposal()
public void UnplacedFrameReleasesInsertDemandAndKeepsTheValidPartialProposal()
{
var request = Request("stub");
request.Items[0].Quantity = 2;
@@ -339,7 +339,7 @@ public class CutoutPipelinePreviewTests
request.Items[1].Drawing.Program = TestShapes.Rectangle(6, 6);
request = request with { Stock = new[] { new NestPlateStock("sheet", new Size(22, 22), 2, 0.25) } };
var calls = 0;
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
calls++;
var first = job.Parts.Single(p => p.Id.EndsWith("-0"));
@@ -348,13 +348,13 @@ public class CutoutPipelinePreviewTests
return builder.Build(NestJobStopReason.NoPlacementFound);
}), "stub", request);
Assert.Equal(1, calls);
Assert.False(result.CanKeep);
Assert.Empty(result.Plates);
Assert.True(result.CanKeep);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(2, Assert.Single(result.Plates).Parts.Count);
Assert.Equal(NestJobStatus.Incomplete, result.Raw.Status);
Assert.All(result.Raw.Fulfillment, f => Assert.Equal((2, 1, 1),
(f.Requested, f.Placed, f.Unplaced)));
Assert.Equal(new[] { "part-0", "part-1" }, result.Raw.Plates.Single().Placements.Select(p => p.PartId));
AssertCannotCommit(result, request);
Assert.Equal(new[] { "part-1", "part-0" }, result.Raw.Plates.Single().Placements.Select(p => p.PartId));
}
[Fact]
@@ -370,7 +370,7 @@ public class CutoutPipelinePreviewTests
new NestItem { Drawing = frame2, Quantity = 1 }, initial.Items[1] },
Stock = new[] { new NestPlateStock("sheet", new Size(22, 22), 2, 0.25) }
};
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var proxies = job.Parts.Where(p => p.Id.StartsWith("__cutout-proxy-")).ToArray();
Assert.Equal(2, proxies.Length);
@@ -381,7 +381,7 @@ public class CutoutPipelinePreviewTests
return builder.Build(NestJobStopReason.Completed);
}), "stub", request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(new[] { "part-0", "part-2", "part-1", "part-2" },
Assert.Equal(new[] { "part-2", "part-0", "part-2", "part-1" },
result.Raw.Plates.SelectMany(p => p.Placements).Select(p => p.PartId));
Assert.Equal(new[] { 0, 1 }, result.Raw.Plates.SelectMany(p => p.Placements)
.Where(p => p.PartId == "part-2").Select(p => p.InstanceIndex));
@@ -403,7 +403,7 @@ public class CutoutPipelinePreviewTests
new NestItem { Drawing = new Drawing("insert", TestShapes.Rectangle(3, 3)), Quantity = 1,
StepAngle = quarter, RotationStart = quarter, RotationEnd = quarter } },
new[] { new NestPlateStock("sheet", new Size(22, 22), 1, 0.25) });
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var proxy = Assert.Single(job.Parts);
Assert.True(proxy.Rotation.Allows(quarter));
@@ -412,11 +412,11 @@ public class CutoutPipelinePreviewTests
}), "stub", request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
var poses = result.Raw.Plates.Single().Placements;
Assert.Equal(quarter, poses[0].Rotation, 8);
Assert.Equal(quarter, poses[1].Rotation, 8);
Assert.Equal(quarter, poses[0].Rotation, 8);
// Independent square-hole oracle, not the production clearance predicate.
var localX = poses[1].Y - poses[0].Y;
var localY = -(poses[1].X - poses[0].X);
var localX = poses[0].Y - poses[1].Y;
var localY = -(poses[0].X - poses[1].X);
foreach (var dx in new[] { 0.0, 3.0 })
foreach (var dy in new[] { 0.0, 3.0 })
{
@@ -434,7 +434,7 @@ public class CutoutPipelinePreviewTests
new NestPlateStock("tight", new Size(22, 22), 1, 0.25),
new NestPlateStock("wide", new Size(22, 22), 1, 0.6, quadrant: 3) }
};
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var proxy = Assert.Single(job.Parts);
var builder = new NestJobResultBuilder(job);
@@ -461,10 +461,10 @@ public class CutoutPipelinePreviewTests
}));
try
{
var result = NestPipeline.RunCutoutPreview(Request(name));
var result = NestPipeline.RunCutoutTrial(Request(name));
Assert.Equal(1, calls);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(new[] { "part-0", "part-1" },
Assert.Equal(new[] { "part-1", "part-0" },
result.Raw.Plates.Single().Placements.Select(p => p.PartId));
}
finally
@@ -492,13 +492,13 @@ public class CutoutPipelinePreviewTests
new NestItem { Drawing = frame, Quantity = 1 },
new NestItem { Drawing = insert, Quantity = 2 } },
new[] { new NestPlateStock("sheet", new Size(22, 22), 1, 0.25) });
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
var proxy = Assert.Single(job.Parts);
return Result(job, new NestJobPlacement(proxy.Id, 0, 1, 1, 0));
}), "stub", request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
var inserts = result.Raw.Plates.Single().Placements.Skip(1).ToArray();
var inserts = result.Raw.Plates.Single().Placements.Take(2).ToArray();
Assert.Equal(2, inserts.Length);
Assert.All(inserts, p =>
{
@@ -515,7 +515,7 @@ public class CutoutPipelinePreviewTests
{
var request = Request("stub");
request.Items[1].Priority = 3;
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
Assert.DoesNotContain(job.Parts, p => p.Id.StartsWith("__cutout-proxy-"));
return Result(job);
@@ -538,13 +538,13 @@ public class CutoutPipelinePreviewTests
new Spacing(0.5, 0.5), quadrant) },
Options = new NestJobOptions(maxPlates: 1)
};
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
Result(job, new NestJobPlacement(Assert.Single(job.Parts).Id, 0, x, y, 0))),
"stub", request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(quadrant, result.Plates.Single().Stock.Quadrant);
Assert.Equal((x, y), (result.Raw.Plates.Single().Placements[0].X,
result.Raw.Plates.Single().Placements[0].Y));
Assert.Equal((x, y), (result.Raw.Plates.Single().Placements.Last().X,
result.Raw.Plates.Single().Placements.Last().Y));
}
[Fact]
@@ -552,7 +552,7 @@ public class CutoutPipelinePreviewTests
{
var request = Request("stub");
using var cts = new CancellationTokenSource();
Assert.Throws<OperationCanceledException>(() => NestPipeline.RunCutoutPreview(
Assert.Throws<OperationCanceledException>(() => NestPipeline.RunCutoutTrial(
new Stub(job =>
{
var proxy = Assert.Single(job.Parts);
@@ -566,7 +566,7 @@ public class CutoutPipelinePreviewTests
public void CompleteStatusWithMissingProxyIsUntrustedAndNotBindable()
{
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(new Stub(job => new NestJobResult(
var result = NestPipeline.RunCutoutTrial(new Stub(job => new NestJobResult(
NestJobStatus.Complete, NestJobStopReason.Completed,
Array.Empty<NestJobPlateResult>(),
job.Parts.Select(p => new PartFulfillment(p.Id, p.Quantity, 0, p.Quantity)),
@@ -582,7 +582,7 @@ public class CutoutPipelinePreviewTests
{
var request = Request("stub");
var progress = new CollectProgress();
var result = NestPipeline.RunCutoutPreview(new ReportingStub(), "stub", request, progress);
var result = NestPipeline.RunCutoutTrial(new ReportingStub(), "stub", request, progress);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(new[] { NestJobStage.EvaluatingCandidate, NestJobStage.PlateCommitted },
progress.Events.Select(e => e.Stage));
@@ -617,11 +617,11 @@ public class CutoutPipelinePreviewTests
new NestItem { Drawing = new Drawing("long-frame", program), Quantity = 1 },
new NestItem { Drawing = new Drawing("insert", TestShapes.Rectangle(3, 3)), Quantity = 1 } },
new[] { new NestPlateStock("narrow", new Size(22, 12), 1, 0.25) });
var result = NestPipeline.RunCutoutPreview(request);
var result = NestPipeline.RunCutoutTrial(request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
var frame = result.Raw.Plates.Single().Placements[0];
var frame = result.Raw.Plates.Single().Placements.Last();
Assert.Equal(System.Math.PI / 2, frame.Rotation, 7);
Assert.Equal(new[] { "part-0", "part-1" },
Assert.Equal(new[] { "part-1", "part-0" },
result.Raw.Plates.Single().Placements.Select(p => p.PartId));
}
@@ -629,7 +629,7 @@ public class CutoutPipelinePreviewTests
public void ChangedLiveInsertDuringSolveNeverBindsUncheckedComposite()
{
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
request.Items[1].Drawing.Program = TestShapes.Rectangle(40, 40);
var proxy = Assert.Single(job.Parts);
@@ -655,7 +655,7 @@ public class CutoutPipelinePreviewTests
Stock = new[] { new NestPlateStock("sheet", new Size(22, 22), 2, 0.25) },
Options = options
};
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
Assert.Same(options, job.Options);
Assert.Equal("Strip", job.Options.PlacementStrategy);
@@ -687,14 +687,14 @@ public class CutoutPipelinePreviewTests
new NestItem { Drawing = new Drawing("insert", TestShapes.Rectangle(3, 3)),
Quantity = 1, StepAngle = 1, RotationStart = angle, RotationEnd = angle } },
new[] { new NestPlateStock("sheet", new Size(40, 40), 1, 0.25) });
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
Result(job, new NestJobPlacement(Assert.Single(job.Parts).Id, 0, 15, 5, angle))),
"stub", request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
var poses = result.Raw.Plates.Single().Placements;
Assert.Equal(angle, poses[1].Rotation, 8);
var dx = poses[1].X - poses[0].X;
var dy = poses[1].Y - poses[0].Y;
Assert.Equal(angle, poses[0].Rotation, 8);
var dx = poses[0].X - poses[1].X;
var dy = poses[0].Y - poses[1].Y;
var cosine = System.Math.Cos(angle);
var sine = System.Math.Sin(angle);
var localX = dx * cosine + dy * sine;
@@ -722,28 +722,15 @@ public class CutoutPipelinePreviewTests
new NestItem { Drawing = new Drawing("insert", TestShapes.Rectangle(6, 6)),
Quantity = 40, StepAngle = 1, RotationStart = 0, RotationEnd = 0 } },
new[] { new NestPlateStock("sheet", new Size(22, 22), 40, 0.25) });
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
{
Assert.Equal(32, job.Parts.Count(p => p.Id.StartsWith("__cutout-proxy-")));
Assert.Equal(8, job.Parts.Single(p => p.Id == "part-0").Quantity);
Assert.Equal(8, job.Parts.Single(p => p.Id == "part-1").Quantity);
return Result(job);
}), "stub", request);
Assert.False(result.CanKeep);
Assert.Empty(result.Plates);
Assert.All(result.Raw.Fulfillment, f => Assert.Equal((40, 0, 40),
(f.Requested, f.Placed, f.Unplaced)));
}
[Fact]
public void OrdinaryPipelineIsStillUnchanged()
{
var request = Request("stub");
var result = NestPipeline.Run(new Stub(job =>
{
Assert.DoesNotContain(job.Parts, p => p.Id.StartsWith("__cutout-proxy-"));
return Result(job);
}), "stub", request);
Assert.True(result.CanKeep);
Assert.Empty(result.Plates);
}
}
@@ -0,0 +1,135 @@
using OpenNest.Engine.Jobs;
using OpenNest.Geometry;
using TestShapes = OpenNest.Engine.Tests.NestingEngines.Shapes;
namespace OpenNest.Engine.Tests.Jobs.Cutouts;
/// <summary>Ordinary whole-job runs bundle parts into other parts' cutouts first, and fall back
/// to the ordinary solve whenever that bundled trial is refused or places less.</summary>
[Collection("Cutout pipeline registry")]
public class CutoutPipelineRunTests
{
private static NestPipelineRequest Request(string engine, int sheets = 1) =>
new(engine,
new[] { new NestItem { Drawing = new Drawing("frame", TestShapes.Ring(20, 10)), Quantity = 1 },
new NestItem { Drawing = new Drawing("insert", TestShapes.Rectangle(6, 6)), Quantity = 1 } },
new[] { new NestPlateStock("sheet", new Size(22, 22), sheets, 0.25) },
new NestJobOptions(maxPlates: sheets));
private sealed class Stub(Func<NestJob, NestJobResult> solve) : INestingEngine
{
public List<NestJob> Jobs { get; } = new();
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default)
{
Jobs.Add(job);
return solve(job);
}
}
private static bool IsTrial(NestJob job) => job.Parts.Any(p => p.Id.StartsWith("__cutout-proxy-"));
private static NestJobResult Place(NestJob job, params (string Id, double X, double Y)[] poses)
{
var builder = new NestJobResultBuilder(job);
if (poses.Length > 0)
builder.AddSheet(job.Plates[0], poses.Select(p => (p.Id, p.X, p.Y, 0.0)));
var complete = job.Parts.All(p => poses.Count(x => x.Id == p.Id) == p.Quantity);
return builder.Build(complete ? NestJobStopReason.Completed : NestJobStopReason.NoPlacementFound);
}
[Theory]
[InlineData("Irregular")]
[InlineData("Default")]
public void OrdinaryRunPlacesAnInsertInsideItsFrameThatTheOrdinarySolveCannotFit(string engine)
{
var request = Request(engine);
var baseline = NestPipeline.RunWithoutCutouts(request);
Assert.Equal(NestJobStatus.Incomplete, baseline.Status);
var result = NestPipeline.Run(request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Empty(result.RefusedCutoutTrial);
var plate = Assert.Single(result.Plates);
Assert.Equal(new[] { request.Items[1].Drawing, request.Items[0].Drawing },
plate.Parts.Select(p => p.BaseDrawing));
}
[Fact]
public void RefusedTrialFallsBackToTheOrdinarySolveAndSaysWhy()
{
var request = Request("stub");
var engine = new Stub(job => IsTrial(job)
// Forged: the proxy is placed with a quarter turn its fixed rotation forbids.
? new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
new[] { new NestJobPlateResult(0, job.Plates[0], new[] {
new NestJobPlacement(job.Parts[0].Id, 0, 11, 11, System.Math.PI / 2) }) },
job.Parts.Select(p => new PartFulfillment(p.Id, 1, 1, 0)),
new[] { new StockUsage("sheet", 1, 0) })
: Place(job, ("part-0", 11, 11)));
var result = NestPipeline.Run(engine, "stub", request);
Assert.Equal(2, engine.Jobs.Count);
Assert.True(IsTrial(engine.Jobs[0]));
Assert.False(IsTrial(engine.Jobs[1]));
Assert.True(result.CanKeep);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Contains(result.RefusedCutoutTrial, v => v.Contains("rotation constraint"));
Assert.Equal(new[] { "part-0" }, Assert.Single(result.Raw.Plates).Placements.Select(p => p.PartId));
}
[Fact]
public void IncompleteTrialYieldsToAnOrdinarySolveThatPlacesMore()
{
var request = Request("stub");
request.Items[1].Quantity = 2;
request = request with { Stock = new[] { new NestPlateStock("sheet", new Size(22, 40), 1, 0.25) } };
// The trial leaves its proxy (and the reserved insert) unplaced; the ordinary solve does not.
var engine = new Stub(job => IsTrial(job)
? Place(job, ("part-1", 30, 1))
: Place(job, ("part-0", 11, 11), ("part-1", 23, 1), ("part-1", 30, 1)));
var result = NestPipeline.Run(engine, "stub", request);
Assert.Equal(2, engine.Jobs.Count);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Empty(result.RefusedCutoutTrial);
Assert.Equal(3, Assert.Single(result.Plates).Parts.Count);
}
[Fact]
public void IncompleteTrialThatPlacesAtLeastAsMuchIsKept()
{
var request = Request("stub");
request.Items[0].Quantity = 2;
request.Items[1].Quantity = 2;
request = request with { Stock = new[] { new NestPlateStock("sheet", new Size(22, 22), 1, 0.25) } };
var engine = new Stub(job => IsTrial(job)
? Place(job, (job.Parts.First(IsProxy).Id, 11, 11))
: Place(job, ("part-0", 11, 11)));
var result = NestPipeline.Run(engine, "stub", request);
Assert.Equal(2, engine.Jobs.Count);
Assert.True(result.CanKeep);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(NestJobStatus.Incomplete, result.Status);
Assert.Equal(new[] { "part-1", "part-0" },
Assert.Single(result.Raw.Plates).Placements.Select(p => p.PartId));
Assert.All(result.Raw.Fulfillment, f => Assert.Equal((2, 1, 1), (f.Requested, f.Placed, f.Unplaced)));
static bool IsProxy(NestJobPart part) => part.Id.StartsWith("__cutout-proxy-");
}
[Fact]
public void StopDuringTheTrialNeverStartsTheOrdinarySolve()
{
using var cts = new CancellationTokenSource();
var engine = new Stub(job =>
{
cts.Cancel();
return Place(job);
});
Assert.Throws<OperationCanceledException>(() =>
NestPipeline.Run(engine, "stub", Request("stub"), token: cts.Token));
Assert.True(IsTrial(Assert.Single(engine.Jobs)));
}
}
@@ -7,7 +7,9 @@ using OpenNest.Engine.Jobs.Adapters;
namespace OpenNest.Engine.Jobs.Cutouts;
/// <summary>Internal-only frame bundles. No normal pipeline caller enables this.</summary>
/// <summary>Frame bundles prepared by <see cref="NestPipeline"/> before every whole-job solve:
/// engines place a frame proxy and never see its reserved inserts. Only actually placed
/// proxies expand, and the expanded physical layout is validated again.</summary>
internal sealed class CutoutPipelinePrepass
{
// A job with thousands of repeats still offers the residual as ordinary demand;
@@ -141,7 +143,8 @@ internal sealed class CutoutPipelinePrepass
}
/// <summary>Reject untrusted accounting before expansion. A failed trial is never bindable,
/// including with allowInvalid. Only actually placed proxies consume reserved inserts.</summary>
/// including with allowInvalid. Only actually placed proxies consume reserved inserts;
/// an unplaced proxy's inserts stay unplaced demand of their own requirements.</summary>
internal bool TryExpand(NestJob original, NestJobResult result, CancellationToken token,
out NestJobResult? expanded,
out string failure)
@@ -215,7 +218,8 @@ internal sealed class CutoutPipelinePrepass
poses.Add((pose.PartId, pose.X, pose.Y, pose.Rotation));
continue;
}
poses.Add((bundle.FrameId, pose.X, pose.Y, pose.Rotation));
// Inserts precede their frame: cutting the frame's cutout releases the slug
// each insert is cut from, so placed order is the safe cutting order.
var sine = System.Math.Sin(pose.Rotation);
var cosine = System.Math.Cos(pose.Rotation);
foreach (var local in bundle.LocalInserts)
@@ -223,6 +227,7 @@ internal sealed class CutoutPipelinePrepass
pose.X + local.X * cosine - local.Y * sine,
pose.Y + local.X * sine + local.Y * cosine,
pose.Rotation + local.Rotation));
poses.Add((bundle.FrameId, pose.X, pose.Y, pose.Rotation));
}
// Canonical stock reference, not an engine-supplied object with altered settings.
var stock = original.Plates.Single(s => s.Id == sheet.StockId);
+99 -26
View File
@@ -75,6 +75,16 @@ public sealed class NestPipelineResult
public TimeSpan SolveTime { get; }
public TimeSpan ValidationTime { get; }
internal IReadOnlyDictionary<string, Drawing> DrawingsByPartId { get; }
/// <summary>Why a bundled cutout trial was refused before this ordinary solve replaced it;
/// empty when no trial ran or the trial was not refused. Never part of <see cref="Violations"/>:
/// the refused trial has no bindable plates.</summary>
public IReadOnlyList<string> RefusedCutoutTrial { get; private init; } = Array.Empty<string>();
internal NestPipelineResult WithRefusedCutoutTrial(IReadOnlyList<string> refusals) =>
new(EngineName, Job, Raw, Plates, Violations, CanKeep, SolveTime, ValidationTime,
DrawingsByPartId)
{ RefusedCutoutTrial = refusals };
}
/// <summary>
@@ -108,39 +118,85 @@ public static class NestPipeline
return Run(engine, request.EngineName, request, progress, token);
}
/// <summary>Runs a caller-supplied engine instance through the same validation and binding.</summary>
/// <summary>Runs a caller-supplied engine instance through the same validation and binding.
/// Parts that fit other parts' enclosed cutouts are first bundled into their frames
/// (engine-agnostic); see <see cref="CutoutPipelinePrepass"/>. The engine then solves once.
/// A bundled trial that fails physical validation, or leaves demand unplaced, is compared
/// with an ordinary solve of the same job and the better valid proposal is returned.</summary>
public static NestPipelineResult Run(
INestingEngine engine,
string engineName,
NestPipelineRequest request,
IProgress<NestJobProgress> progress = null,
CancellationToken token = default
) => RunCore(engine, engineName, request, progress, token, cutoutPreview: false);
) => Run(engine, engineName, request, progress, token, cutouts: true);
/// <summary>Internal test-only integration trial. Normal Run never prepares cutouts;
/// this entry point must not be called by production front ends before cutting/post gates.</summary>
internal static NestPipelineResult RunCutoutPreview(
/// <summary>The ordinary solve alone, with no cutout pre-pass: a characterization baseline
/// for tests and benchmarks, not a front-end setting.</summary>
internal static NestPipelineResult RunWithoutCutouts(
NestPipelineRequest request,
IProgress<NestJobProgress> progress = null,
CancellationToken token = default
) => Run(NestingEngineRegistry.Create(request.EngineName), request.EngineName, request,
progress, token, cutouts: false);
/// <summary>The bundled cutout trial alone, without the ordinary-solve comparison, so tests
/// can exercise its validation boundary. Jobs without bundles run the ordinary solve.</summary>
internal static NestPipelineResult RunCutoutTrial(
INestingEngine engine,
string engineName,
NestPipelineRequest request,
IProgress<NestJobProgress> progress = null,
CancellationToken token = default
) => RunCore(engine, engineName, request, progress, token, cutoutPreview: true);
)
{
var (job, drawingsByPartId) = CreateJob(engine, request, token);
var prepass = CutoutPipelinePrepass.Prepare(job, token);
var result = Evaluate(engine, engineName, job, prepass, drawingsByPartId, progress, token);
return prepass != null && result.CanKeep && result.IsValid
? Committed(result, progress, token) : result;
}
internal static NestPipelineResult RunCutoutPreview(
internal static NestPipelineResult RunCutoutTrial(
NestPipelineRequest request,
IProgress<NestJobProgress> progress = null,
CancellationToken token = default
) => RunCutoutPreview(NestingEngineRegistry.Create(request.EngineName),
) => RunCutoutTrial(NestingEngineRegistry.Create(request.EngineName),
request.EngineName, request, progress, token);
private static NestPipelineResult RunCore(
private static NestPipelineResult Run(
INestingEngine engine,
string engineName,
NestPipelineRequest request,
IProgress<NestJobProgress> progress,
CancellationToken token,
bool cutoutPreview
bool cutouts
)
{
var (job, drawingsByPartId) = CreateJob(engine, request, token);
var prepass = cutouts ? CutoutPipelinePrepass.Prepare(job, token) : null;
if (prepass == null)
return Evaluate(engine, engineName, job, null, drawingsByPartId, progress, token);
var trial = Evaluate(engine, engineName, job, prepass, drawingsByPartId, progress, token);
var trialKeepable = trial.CanKeep && trial.IsValid;
if (trialKeepable && trial.Status == NestJobStatus.Complete)
return Committed(trial, progress, token);
// Unplaced frames return their reserved inserts to unplaced demand, and an untrusted
// composite is never bindable; the ordinary solve is the established baseline.
var plain = Evaluate(engine, engineName, job, null, drawingsByPartId, progress, token);
if (!trialKeepable)
return plain.WithRefusedCutoutTrial(trial.Violations);
if (plain.CanKeep && plain.IsValid && Placed(plain) > Placed(trial))
return plain;
return Committed(trial, progress, token);
}
private static (NestJob Job, Dictionary<string, Drawing> Drawings) CreateJob(
INestingEngine engine,
NestPipelineRequest request,
CancellationToken token
)
{
ArgumentNullException.ThrowIfNull(engine);
@@ -163,7 +219,37 @@ public static class NestPipeline
var job = new NestJob(parts, request.Stock, request.Options);
NestJobValidator.Validate(job);
var prepass = cutoutPreview ? CutoutPipelinePrepass.Prepare(job, token) : null;
return (job, drawingsByPartId);
}
private static int Placed(NestPipelineResult result) =>
result.Raw.Plates.Sum(p => p.Placements.Count);
// Trial progress hid the engine's proxy commit counts; report validated physical ones.
private static NestPipelineResult Committed(NestPipelineResult result,
IProgress<NestJobProgress> progress, CancellationToken token)
{
var count = 0;
for (var i = 0; i < result.Raw.Plates.Count; i++)
{
count += result.Raw.Plates[i].Placements.Count;
progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted,
result.Raw.Plates[i].StockId, i, i + 1, count));
}
token.ThrowIfCancellationRequested();
return result;
}
private static NestPipelineResult Evaluate(
INestingEngine engine,
string engineName,
NestJob job,
CutoutPipelinePrepass prepass,
IReadOnlyDictionary<string, Drawing> drawingsByPartId,
IProgress<NestJobProgress> progress,
CancellationToken token
)
{
var engineJob = prepass?.EngineJob ?? job;
if (prepass != null)
NestJobValidator.Validate(engineJob);
@@ -176,8 +262,8 @@ public static class NestPipeline
var solveTime = clock.Elapsed;
token.ThrowIfCancellationRequested();
// D8/C10: only the main (non-cutout-preview) path offers last-resort clamp removal.
// No-op whenever nothing is blocked, so an ordinary solve is unaffected (C10).
// D8/C10: only the ordinary solve offers last-resort clamp removal; an incomplete
// cutout trial is compared with that solve instead. No-op whenever nothing is blocked.
if (prepass == null)
{
var recovered = ClampRemovalRecovery.TryRecover(job, raw, engine, token);
@@ -208,8 +294,6 @@ public static class NestPipeline
{
raw = expanded;
violations.AddRange(NestLayoutCheck.Violations(job, raw, names, out var physicalKeep));
if (raw.Status != NestJobStatus.Complete)
violations.Add("Cutout engine did not complete the transformed job; unplaced frames' reserved inserts remain unplaced in the original job");
canKeep = physicalKeep && violations.Count == 0;
}
else
@@ -253,17 +337,6 @@ public static class NestPipeline
}
}
token.ThrowIfCancellationRequested();
if (prepass != null && canKeep)
{
var count = 0;
for (var i = 0; i < raw.Plates.Count; i++)
{
count += raw.Plates[i].Placements.Count;
progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted,
raw.Plates[i].StockId, i, i + 1, count));
}
}
token.ThrowIfCancellationRequested();
return new NestPipelineResult(
engineName,
@@ -10,14 +10,14 @@ using OpenNest.Tests.CuttingPlanning;
namespace OpenNest.Tests.Engine;
/// <summary>
/// Cutouts-first placements are only cut-safe once the insert is cut before its host and the
/// host's hole leads clear it. Posting review must say so on the committed parts, and a fresh
/// Plan Cutting run on those actual parts must produce that order.
/// Cutouts-first placements are only cut-safe while the insert is cut before its host and the
/// host's hole leads clear it. Auto Nest commits inserts first; posting review must warn once
/// that order is lost, and a fresh Plan Cutting run on those actual parts must restore it.
/// </summary>
public class CutoutCuttingSafetyTests
{
[Fact]
public void CommittedCutoutPreview_WarnsUntilPlanCuttingCutsTheInsertFirst()
public void CommittedCutoutNest_CutsTheInsertFirstAndWarnsWhenReordered()
{
var frame = new OpenNest.Shapes.RingShape { OuterDiameter = 20, InnerDiameter = 10 }.GetDrawing();
frame.Name = "neutral-frame";
@@ -33,7 +33,8 @@ public class CutoutCuttingSafetyTests
new NestItem { Drawing = insert, Quantity = 1 } },
new[] { new NestPlateStock("sheet", new Size(22, 22), 1, 0.25) },
new NestJobOptions(maxPlates: 1));
var preview = NestPipeline.RunCutoutPreview(request);
var preview = NestPipeline.Run(request);
Assert.Empty(preview.RefusedCutoutTrial);
Assert.True(preview.IsValid, string.Join("; ", preview.Violations));
var nest = new Nest();
nest.Drawings.Add(frame);
@@ -41,11 +42,15 @@ public class CutoutCuttingSafetyTests
using (var manager = new PlateManager(nest))
NestPipelineCommit.ApplyToEmptyPlates(preview, manager);
var plate = Assert.Single(nest.Plates, p => p.Parts.Count != 0);
Assert.Equal(new[] { frame, insert }, plate.Parts.Select(p => p.BaseDrawing));
var host = plate.Parts[0];
var inner = plate.Parts[1];
Assert.Equal(new[] { insert, frame }, plate.Parts.Select(p => p.BaseDrawing));
var inner = plate.Parts[0];
var host = plate.Parts[1];
var committed = PostVerificationAnalyzer.Analyze(nest);
Assert.DoesNotContain(committed.Findings, f => f.Kind is PostVerificationKind.NestedCutOrder
or PostVerificationKind.NestedLead or PostVerificationKind.Incomplete);
// As committed, the frame (and so its cutout) is cut before the insert inside it.
// Reordered (for example by hand), the frame's cutout is cut before the insert inside it.
plate.Parts.Reorder(new[] { host, inner });
var before = PostVerificationAnalyzer.Analyze(nest);
var order = Assert.Single(before.Findings, f => f.Kind == PostVerificationKind.NestedCutOrder);
Assert.Equal((2, 1), (order.PartNumber, order.OtherPartNumber));
@@ -26,12 +26,13 @@ public class CutoutPipelinePersistenceTests
new NestItem { Drawing = insert, Quantity = 2 } },
new[] { new NestPlateStock("sheet", new Size(22, 22), 2, 0.25) },
new NestJobOptions(maxPlates: 2));
var ordinary = NestPipeline.Run(request);
var ordinary = NestPipeline.RunWithoutCutouts(request);
Assert.Contains(ordinary.Raw.Fulfillment, f => f.Unplaced > 0);
var result = NestPipeline.RunCutoutPreview(request);
var result = NestPipeline.Run(request);
Assert.Empty(result.RefusedCutoutTrial);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(2, result.Raw.Plates.Count);
Assert.All(result.Raw.Plates, p => Assert.Equal(new[] { "part-0", "part-1" },
Assert.All(result.Raw.Plates, p => Assert.Equal(new[] { "part-1", "part-0" },
p.Placements.Select(x => x.PartId)));
var nest = new Nest();
@@ -41,7 +42,7 @@ public class CutoutPipelinePersistenceTests
{
var committed = NestPipelineCommit.ApplyToEmptyPlates(result, manager);
Assert.Equal(2, committed.Sum(p => p.Quantity));
Assert.All(committed, plate => Assert.Equal(new[] { frame, insert },
Assert.All(committed, plate => Assert.Equal(new[] { insert, frame },
plate.Parts.Select(p => p.BaseDrawing)));
}
Assert.Equal(2, frame.Quantity.Nested);
@@ -59,7 +60,7 @@ public class CutoutPipelinePersistenceTests
Assert.Equal(expected.PartSpacing, actual.PartSpacing);
Assert.Equal(expected.Quadrant, actual.Quadrant);
Assert.Equal(expected.EdgeSpacing, actual.EdgeSpacing);
Assert.Equal(new[] { "neutral-frame", "neutral-insert" },
Assert.Equal(new[] { "neutral-insert", "neutral-frame" },
actual.Parts.Select(p => p.BaseDrawing.Name));
for (var j = 0; j < actual.Parts.Count; j++)
{
+4 -3
View File
@@ -4,9 +4,10 @@
1. Snapshot caller requirements and stock into a `NestJob`.
2. Resolve the selected name through `NestingEngineRegistry` (built-ins and plug-ins use the same path).
3. Run the engine without mutating caller drawings or plates.
4. Check returned placements with the independent `NestLayoutCheck` used by the benchmark.
5. Consolidate exact ordered nonempty layouts only when the entire result is valid, then bind each layout to the caller's drawing references; the caller decides whether to commit it.
3. Bundle parts that fit other parts' enclosed cutouts into their frames ([filling cutouts](nesting-engines.md#filling-cutouts)); a refused or less complete bundled trial is replaced by the ordinary solve.
4. Run the engine without mutating caller drawings or plates.
5. Check returned placements with the independent `NestLayoutCheck` used by the benchmark (for bundles, again after expanding them to physical frames and inserts).
6. Consolidate exact ordered nonempty layouts only when the entire result is valid, then bind each layout to the caller's drawing references; the caller decides whether to commit it.
`NestStockBuilder.FromTemplate` snapshots sheet options and spacing settings for multiple-sheet jobs. `SinglePlate` offers exactly one physical sheet, regardless of the legacy plate repeat quantity. `NestPipelineCommit.ApplyToEmptyPlates` applies accepted desktop proposals to empty/new plates with the checked size, quadrant, spacing and total-copy quantity semantics. Quantity is set before parts are attached, so drawing counts multiply exactly once. It never fills occupied plates.
+3
View File
@@ -109,6 +109,9 @@ the final replay enforce them:
pairs; containment is proven on native clean material. Touching or crossing boundaries, or material that cannot be
captured, refuse as `UnsupportedGeometry` naming both parts. A part in a concave
pocket outside the host's material has no dependency.
Auto Nest's cutout pre-pass commits each insert before its frame, so an unedited
cutouts-first plate already satisfies this order; its leads are still checked
([nesting engines](nesting-engines.md#filling-cutouts)).
- A preserved manual order that violates a prerequisite, or a cycle, is a
`ConstraintConflict`. Replay rechecks the captured prerequisites and refuses a
violating order rather than trusting the search.
+29 -15
View File
@@ -133,12 +133,12 @@ Default inherits these fast paths by running the two engines; other engines and
keep their existing search behavior. Quantity-bearing presentation is a separate validated-pipeline
representation; repeating does not bypass whole-job validation or change cutting programs.
## Filling cutouts (not yet in production)
## Filling cutouts
Placing parts inside another part's enclosed cutout is being built as a step that runs before any
engine, so every engine benefits. Only an internal test-only pipeline preview calls it: a part
inside a cutout must be cut before the cutout's contour. Ordinary whole-job calls do not enable
this path.
Every whole-job `NestPipeline.Run` (desktop Auto Nest, Console, MCP and API) first places parts
inside other parts' enclosed cutouts, before the selected engine, so every engine benefits and
none needs to know about holes. The steps below prepare frame-plus-insert bundles; the engine
places each bundle's frame as an ordinary part and never sees its reserved inserts.
`CutoutLatticeFill` (`OpenNest.Engine/Jobs/Cutouts/`) fills one closed cutout with copies of one
part. It runs Fill over the cutout's bounds plus one part step on every side, then shifts
@@ -163,7 +163,7 @@ NFP first, then Fill if demand remains, taking the higher-count valid proposal.
An 0.20 NFP-only rule would lose a second 4-inch square in a 10-inch round hole.
Six geometry-only, anonymized real-job probes and neutral ring fixtures informed
the search-order hints; these limited cases are not a global density guarantee.
The internal `NestPipeline.RunCutoutPreview` builds quantity-one proxy instances for eligible
`CutoutPipelinePrepass` builds quantity-one proxy instances for eligible
closed-cutout frames. It considers legal frame rotations without outer-perimeter symmetry
reduction, shifts insert rotation policies into the local frame, and reserves certified router
poses by original requirement ID. It can fill multiple holes and repeated/different frame
@@ -174,21 +174,35 @@ offered to the selected engine once. Only actually placed proxies expand to orig
insert identities. An unplaced proxy's reservations return to the original unplaced demand;
they cannot be packed retroactively in that same solve. Engine indices and metadata are checked,
then every physical pose, stock, rotation, quantity and fulfillment is independently validated
before binding caller drawings. Incomplete, forged or geometrically invalid composites return
no bindable plates even with invalid-result consent. Candidate evaluation progress is forwarded
before binding caller drawings. Forged or geometrically invalid composites return no bindable
plates even with invalid-result consent. Candidate evaluation progress is forwarded
with unverified proxy commitment counts zeroed; transformed proxy commits are hidden. Physical
commits are reported only after physical validation. Preparation is bounded to 32 bundles
and 16 base sweep-angle samples per frame (automatic quarter-turns are all retained);
remaining copies stay ordinary requirements. Fixed legal proxy orientations are a
conservative search choice, not full engine choice among all legal orientations; unsupported
bundles remain ordinary independent requirements. This is an internal pipeline path, not a
production setting or cut-ready nest.
bundles remain ordinary independent requirements.
Committed preview parts keep the frame before its inserts, so they are not cut-ready as placed.
Pre-post verification warns on a nested part cut after its host and on host/insert leads that
meet the other part (see [pre-post verification](post-verification.md)); Plan Cutting on those
parts cuts each insert before its frame and refuses a kept order that does not. Enabling the
pre-pass for ordinary whole-job calls is still pending (Windows acceptance and front-end wiring).
`NestPipeline.Run` keeps a bundled trial that is valid and complete. Otherwise it also runs the
ordinary solve of the same job (with last-resort clamp removal, which the bundled trial does not
offer) and returns:
- the ordinary solve when the trial was refused, with the refusal on
`NestPipelineResult.RefusedCutoutTrial` (never in `Violations`, since the refused trial has no
bindable plates);
- the ordinary solve when the trial was incomplete and the valid ordinary solve places more
physical parts;
- otherwise the incomplete trial, which is a keepable layout like any other incomplete result.
So a job with cutouts may take up to two engine solves; a job without eligible frames takes
one, exactly as before. Expanded sheets list each frame's inserts immediately before the frame,
so the committed part order cuts every insert before the cutout that would release its slug.
Leads are not changed: pre-post verification still warns when a host's hole lead or an insert's
lead meets the other part, or when a later reorder (by hand or Auto Sequence) puts the host
first (see [pre-post verification](post-verification.md)); Plan Cutting cuts each insert before
its frame and refuses a kept order that does not. Engine benchmarks call engines directly and do
not run this pre-pass; internal `NestPipeline.RunWithoutCutouts` is the ordinary-solve baseline
for tests.
## Renamed engines
+4 -2
View File
@@ -102,8 +102,10 @@ shows the first three check categories, even when there are no findings:
material. Leads already reported as fixed leads are not reported again. A pair whose
containment cannot be established (touching or crossing boundaries, uncapturable material)
is incomplete, never clear. Each line appears in the summary only when there is such a
finding. Plan Cutting's Ready result for the plate cuts nested parts first with leads that
clear them; this review checks the live programs again before posting.
finding. Auto Nest commits cutout inserts before their frame, so an unedited plate has no
order finding; leads keep their own checks. Plan Cutting's Ready result for the plate cuts
nested parts first with leads that clear them; this review checks the live programs again
before posting.
Findings identify the plate and part's position in the cutting sequence. Read the
scrollable report, cancel to fix the nest, then post again to rerun verification.