mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 07:34:14 -04:00
feat(cutouts): reconcile repeated frames and inserts through one solve
This commit is contained in:
1 parent
1b9d8a36d8
commit
e897258a85
6 files changed
+679
-77
No files matched your search
@@ -6,6 +6,10 @@ using TestShapes = OpenNest.Engine.Tests.NestingEngines.Shapes;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs.Cutouts;
|
||||
|
||||
[CollectionDefinition("Cutout pipeline registry", DisableParallelization = true)]
|
||||
public sealed class CutoutPipelineRegistryCollection { }
|
||||
|
||||
[Collection("Cutout pipeline registry")]
|
||||
public class CutoutPipelinePreviewTests
|
||||
{
|
||||
private static NestPipelineRequest Request(string engine = "Irregular")
|
||||
@@ -84,6 +88,8 @@ public class CutoutPipelinePreviewTests
|
||||
Assert.False(result.CanKeep);
|
||||
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);
|
||||
}
|
||||
@@ -289,6 +295,445 @@ public class CutoutPipelinePreviewTests
|
||||
result.Plates.Single().Parts[2].BaseDrawing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepeatedFrameReservationsExpandOnTwoPhysicalSheets()
|
||||
{
|
||||
var request = Request("stub");
|
||||
request.Items[0].Quantity = 2;
|
||||
request.Items[1].Quantity = 2;
|
||||
request.Items[1].Drawing.Program = TestShapes.Rectangle(6, 6);
|
||||
request = request with
|
||||
{
|
||||
Stock = new[] { new NestPlateStock("sheet", new Size(22, 22), 2, 0.25) },
|
||||
Options = new NestJobOptions(maxPlates: 2)
|
||||
};
|
||||
var calls = 0;
|
||||
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
|
||||
{
|
||||
calls++;
|
||||
var proxies = job.Parts.Where(p => p.Id.StartsWith("__cutout-proxy-")).ToArray();
|
||||
Assert.Equal(2, proxies.Length);
|
||||
Assert.Equal(2, job.Parts.Count);
|
||||
var builder = new NestJobResultBuilder(job);
|
||||
foreach (var proxy in proxies)
|
||||
builder.AddSheet(job.Plates[0], new[] { (proxy.Id, 11.0, 11.0, 0.0) });
|
||||
return builder.Build(NestJobStopReason.Completed);
|
||||
}), "stub", request);
|
||||
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" },
|
||||
plate.Placements.Select(p => p.PartId)));
|
||||
Assert.All(result.Raw.Fulfillment, f => Assert.Equal((2, 2, 0),
|
||||
(f.Requested, f.Placed, f.Unplaced)));
|
||||
Assert.Equal(new[] { 0, 1 }, result.Raw.Plates.SelectMany(p => p.Placements)
|
||||
.Where(p => p.PartId == "part-1").Select(p => p.InstanceIndex));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnplacedFrameReleasesInsertDemandWithoutABindablePartialProposal()
|
||||
{
|
||||
var request = Request("stub");
|
||||
request.Items[0].Quantity = 2;
|
||||
request.Items[1].Quantity = 2;
|
||||
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 =>
|
||||
{
|
||||
calls++;
|
||||
var first = job.Parts.Single(p => p.Id.EndsWith("-0"));
|
||||
var builder = new NestJobResultBuilder(job);
|
||||
builder.AddSheet(job.Plates[0], new[] { (first.Id, 11.0, 11.0, 0.0) });
|
||||
return builder.Build(NestJobStopReason.NoPlacementFound);
|
||||
}), "stub", request);
|
||||
Assert.Equal(1, calls);
|
||||
Assert.False(result.CanKeep);
|
||||
Assert.Empty(result.Plates);
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentFrameRequirementsShareScarceInsertsByIdentity()
|
||||
{
|
||||
var initial = Request("stub");
|
||||
var frame2 = new Drawing("second-frame", TestShapes.Ring(18, 10));
|
||||
initial.Items[1].Quantity = 2;
|
||||
initial.Items[1].Drawing.Program = TestShapes.Rectangle(6, 6);
|
||||
var request = initial with
|
||||
{
|
||||
Items = new[] { initial.Items[0],
|
||||
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 proxies = job.Parts.Where(p => p.Id.StartsWith("__cutout-proxy-")).ToArray();
|
||||
Assert.Equal(2, proxies.Length);
|
||||
Assert.Equal(2, job.Parts.Count);
|
||||
var builder = new NestJobResultBuilder(job);
|
||||
foreach (var proxy in proxies)
|
||||
builder.AddSheet(job.Plates[0], new[] { (proxy.Id, 11.0, 11.0, 0.0) });
|
||||
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" },
|
||||
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));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FixedQuarterTurnComposesLocalInsertRotationAndTranslation()
|
||||
{
|
||||
var frame = TestShapes.Rectangle(20, 20);
|
||||
frame.MoveTo(5, 5);
|
||||
frame.LineTo(5, 15);
|
||||
frame.LineTo(15, 15);
|
||||
frame.LineTo(15, 5);
|
||||
frame.LineTo(5, 5);
|
||||
var quarter = System.Math.PI / 2;
|
||||
var request = new NestPipelineRequest("stub", new[] {
|
||||
new NestItem { Drawing = new Drawing("frame", frame), Quantity = 1,
|
||||
StepAngle = quarter, RotationStart = quarter, RotationEnd = quarter },
|
||||
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 proxy = Assert.Single(job.Parts);
|
||||
Assert.True(proxy.Rotation.Allows(quarter));
|
||||
Assert.False(proxy.Rotation.Allows(0));
|
||||
return Result(job, new NestJobPlacement(proxy.Id, 0, 21, 1, quarter));
|
||||
}), "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);
|
||||
// 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);
|
||||
foreach (var dx in new[] { 0.0, 3.0 })
|
||||
foreach (var dy in new[] { 0.0, 3.0 })
|
||||
{
|
||||
Assert.InRange(localX + dx, 5.25, 14.75);
|
||||
Assert.InRange(localY + dy, 5.25, 14.75);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeterogeneousStockUsesConservativeSpacingAndOriginalQuadrant()
|
||||
{
|
||||
var request = Request("stub") with
|
||||
{
|
||||
Stock = new[] {
|
||||
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 proxy = Assert.Single(job.Parts);
|
||||
var builder = new NestJobResultBuilder(job);
|
||||
builder.AddSheet(job.Plates[1], new[] { (proxy.Id, -11.0, -11.0, 0.0) });
|
||||
return builder.Build(NestJobStopReason.Completed);
|
||||
}), "stub", request);
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
Assert.Equal("wide", result.Raw.Plates.Single().StockId);
|
||||
Assert.Equal(3, result.Plates.Single().Stock.Quadrant);
|
||||
Assert.Equal(0.6, result.Plates.Single().Stock.PartSpacing);
|
||||
Assert.Equal(2, result.Plates.Single().Parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisteredPluginRunsTheSamePhysicalExpansionBoundary()
|
||||
{
|
||||
var name = "CutoutPlugin-" + Guid.NewGuid().ToString("N");
|
||||
var calls = 0;
|
||||
NestingEngineRegistry.Register(name, "test plugin", () => new Stub(job =>
|
||||
{
|
||||
calls++;
|
||||
var proxy = Assert.Single(job.Parts);
|
||||
return Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11, 0));
|
||||
}));
|
||||
try
|
||||
{
|
||||
var result = NestPipeline.RunCutoutPreview(Request(name));
|
||||
Assert.Equal(1, calls);
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
Assert.Equal(new[] { "part-0", "part-1" },
|
||||
result.Raw.Plates.Single().Placements.Select(p => p.PartId));
|
||||
}
|
||||
finally
|
||||
{
|
||||
var registry = Assert.IsType<List<NestingEngineInfo>>(NestingEngineRegistry.AvailableEngines);
|
||||
Assert.Equal(1, registry.RemoveAll(info => info.Name == name));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TwoCutoutsReserveDifferentCopiesWithinOneFrame()
|
||||
{
|
||||
var program = TestShapes.Rectangle(20, 20);
|
||||
foreach (var origin in new[] { 2.0, 12.0 })
|
||||
{
|
||||
program.MoveTo(origin, origin);
|
||||
program.LineTo(origin, origin + 6);
|
||||
program.LineTo(origin + 6, origin + 6);
|
||||
program.LineTo(origin + 6, origin);
|
||||
program.LineTo(origin, origin);
|
||||
}
|
||||
var frame = new Drawing("two-hole-frame", program);
|
||||
var insert = new Drawing("insert", TestShapes.Rectangle(4, 4));
|
||||
var request = new NestPipelineRequest("stub", new[] {
|
||||
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 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();
|
||||
Assert.Equal(2, inserts.Length);
|
||||
Assert.All(inserts, p =>
|
||||
{
|
||||
var localX = p.X - 1;
|
||||
var localY = p.Y - 1;
|
||||
Assert.Contains(new[] { 2.0, 12.0 }, origin =>
|
||||
localX >= origin + 0.25 && localX + 4 <= origin + 5.75
|
||||
&& localY >= origin + 0.25 && localY + 4 <= origin + 5.75);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPrioritiesRemainIndependentInsteadOfElevatingAnInsert()
|
||||
{
|
||||
var request = Request("stub");
|
||||
request.Items[1].Priority = 3;
|
||||
var result = NestPipeline.RunCutoutPreview(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);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 11.0, 11.0)]
|
||||
[InlineData(2, -11.0, 11.0)]
|
||||
[InlineData(3, -11.0, -11.0)]
|
||||
[InlineData(4, 11.0, -11.0)]
|
||||
public void AllQuadrantsKeepPhysicalStockAndExpansion(int quadrant, double x, double y)
|
||||
{
|
||||
var request = Request("stub") with
|
||||
{
|
||||
Stock = new[] {
|
||||
new NestPlateStock("sheet", new Size(22, 22), 1, 0.25,
|
||||
new Spacing(0.5, 0.5), quadrant) },
|
||||
Options = new NestJobOptions(maxPlates: 1)
|
||||
};
|
||||
var result = NestPipeline.RunCutoutPreview(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));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancellationAfterAnEngineIgnoresStopDiscardsEveryBundle()
|
||||
{
|
||||
var request = Request("stub");
|
||||
using var cts = new CancellationTokenSource();
|
||||
Assert.Throws<OperationCanceledException>(() => NestPipeline.RunCutoutPreview(
|
||||
new Stub(job =>
|
||||
{
|
||||
var proxy = Assert.Single(job.Parts);
|
||||
cts.Cancel();
|
||||
return Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11, 0));
|
||||
}), "stub", request, token: cts.Token));
|
||||
Assert.All(request.Items, item => Assert.Equal(0, item.Drawing.Quantity.Nested));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteStatusWithMissingProxyIsUntrustedAndNotBindable()
|
||||
{
|
||||
var request = Request("stub");
|
||||
var result = NestPipeline.RunCutoutPreview(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)),
|
||||
job.Plates.Select(s => new StockUsage(s.Id, 0, s.Quantity)))), "stub", request);
|
||||
Assert.False(result.CanKeep);
|
||||
Assert.Empty(result.Plates);
|
||||
Assert.Contains(result.Violations, v => v.Contains("accounting"));
|
||||
AssertCannotCommit(result, request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuccessfulCompositeReportsOnlyValidatedPhysicalCommitCounts()
|
||||
{
|
||||
var request = Request("stub");
|
||||
var progress = new CollectProgress();
|
||||
var result = NestPipeline.RunCutoutPreview(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));
|
||||
Assert.Equal(2, progress.Events.Last().CommittedParts);
|
||||
Assert.Equal(1, progress.Events.Last().CommittedPlates);
|
||||
}
|
||||
|
||||
private sealed class ReportingStub : INestingEngine
|
||||
{
|
||||
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress>? progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var proxy = Assert.Single(job.Parts);
|
||||
progress?.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate,
|
||||
job.Plates[0].Id, 0, 0, 0));
|
||||
progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted,
|
||||
job.Plates[0].Id, 0, 1, 1));
|
||||
return Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11, 0));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AutomaticBundleRetainsStockFeasibleQuarterTurn()
|
||||
{
|
||||
var program = TestShapes.Rectangle(20, 10);
|
||||
program.MoveTo(2, 2);
|
||||
program.LineTo(2, 8);
|
||||
program.LineTo(18, 8);
|
||||
program.LineTo(18, 2);
|
||||
program.LineTo(2, 2);
|
||||
var request = new NestPipelineRequest("Irregular", new[] {
|
||||
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);
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
var frame = result.Raw.Plates.Single().Placements[0];
|
||||
Assert.Equal(System.Math.PI / 2, frame.Rotation, 7);
|
||||
Assert.Equal(new[] { "part-0", "part-1" },
|
||||
result.Raw.Plates.Single().Placements.Select(p => p.PartId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangedLiveInsertDuringSolveNeverBindsUncheckedComposite()
|
||||
{
|
||||
var request = Request("stub");
|
||||
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
|
||||
{
|
||||
request.Items[1].Drawing.Program = TestShapes.Rectangle(40, 40);
|
||||
var proxy = Assert.Single(job.Parts);
|
||||
return Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11, 0));
|
||||
}), "stub", request);
|
||||
Assert.False(result.CanKeep);
|
||||
Assert.Empty(result.Plates);
|
||||
Assert.Contains(result.Violations, v => v.Contains("changed") && v.Contains("insert"));
|
||||
AssertCannotCommit(result, request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NondefaultOptionsPassUnchangedAndPlateCapRejectsOtherwiseValidOutput()
|
||||
{
|
||||
var request = Request("stub");
|
||||
request.Items[0].Quantity = 2;
|
||||
request.Items[1].Quantity = 2;
|
||||
request.Items[1].Drawing.Program = TestShapes.Rectangle(6, 6);
|
||||
var options = new NestJobOptions("Strip", maxPlates: 1,
|
||||
salvageRate: 0.4, minimumSalvageDimension: 2);
|
||||
request = request with
|
||||
{
|
||||
Stock = new[] { new NestPlateStock("sheet", new Size(22, 22), 2, 0.25) },
|
||||
Options = options
|
||||
};
|
||||
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
|
||||
{
|
||||
Assert.Same(options, job.Options);
|
||||
Assert.Equal("Strip", job.Options.PlacementStrategy);
|
||||
Assert.Equal(0.4, job.Options.SalvageRate);
|
||||
var builder = new NestJobResultBuilder(job);
|
||||
foreach (var proxy in job.Parts)
|
||||
builder.AddSheet(job.Plates[0], new[] { (proxy.Id, 11.0, 11.0, 0.0) });
|
||||
return builder.Build(NestJobStopReason.Completed);
|
||||
}), "stub", request);
|
||||
Assert.False(result.CanKeep);
|
||||
Assert.Empty(result.Plates);
|
||||
Assert.Contains(result.Violations, v => v.Contains("MaxPlates"));
|
||||
AssertCannotCommit(result, request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObliqueFramePreservesLegalGlobalInsertRotationAndWallClearance()
|
||||
{
|
||||
var program = TestShapes.Rectangle(20, 20);
|
||||
program.MoveTo(5, 5);
|
||||
program.LineTo(5, 15);
|
||||
program.LineTo(15, 15);
|
||||
program.LineTo(15, 5);
|
||||
program.LineTo(5, 5);
|
||||
const double angle = 0.3;
|
||||
var request = new NestPipelineRequest("stub", new[] {
|
||||
new NestItem { Drawing = new Drawing("frame", program), Quantity = 1,
|
||||
StepAngle = 1, RotationStart = angle, RotationEnd = angle },
|
||||
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 =>
|
||||
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;
|
||||
var cosine = System.Math.Cos(angle);
|
||||
var sine = System.Math.Sin(angle);
|
||||
var localX = dx * cosine + dy * sine;
|
||||
var localY = -dx * sine + dy * cosine;
|
||||
foreach (var x in new[] { 0.0, 3.0 })
|
||||
foreach (var y in new[] { 0.0, 3.0 })
|
||||
{
|
||||
Assert.InRange(localX + x, 5.25, 14.75);
|
||||
Assert.InRange(localY + y, 5.25, 14.75);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeDemandLeavesExcessFramesAndInsertsIndependentAfterBoundedPreparation()
|
||||
{
|
||||
var frame = TestShapes.Rectangle(20, 20);
|
||||
frame.MoveTo(5, 5);
|
||||
frame.LineTo(5, 15);
|
||||
frame.LineTo(15, 15);
|
||||
frame.LineTo(15, 5);
|
||||
frame.LineTo(5, 5);
|
||||
var request = new NestPipelineRequest("stub", new[] {
|
||||
new NestItem { Drawing = new Drawing("frame", frame), Quantity = 40,
|
||||
StepAngle = 1, RotationStart = 0, RotationEnd = 0 },
|
||||
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 =>
|
||||
{
|
||||
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.All(result.Raw.Fulfillment, f => Assert.Equal((40, 0, 40),
|
||||
(f.Requested, f.Placed, f.Unplaced)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrdinaryPipelineIsStillUnchanged()
|
||||
{
|
||||
|
||||
@@ -22,8 +22,9 @@ namespace OpenNest.Engine.Jobs.Cutouts;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Suited to many small copies in a large cutout; a few large inserts belong to NFP placement.
|
||||
/// Poses are in the frame's own coordinates: frame at the origin, unrotated. Not wired into
|
||||
/// any engine or pipeline yet: placing parts in cutouts waits on containment-aware cutting order.
|
||||
/// Poses are in the frame's own coordinates: frame at the origin, unrotated. Used by the
|
||||
/// internal pipeline cutout preview, but not enabled for normal whole-job calls;
|
||||
/// production placement waits on cutting/post safety.
|
||||
/// </remarks>
|
||||
internal static class CutoutLatticeFill
|
||||
{
|
||||
|
||||
@@ -3,23 +3,23 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
|
||||
namespace OpenNest.Engine.Jobs.Cutouts;
|
||||
|
||||
/// <summary>Internal-only single-frame composite trial. No normal pipeline caller enables this.</summary>
|
||||
/// <summary>Internal-only frame bundles. No normal pipeline caller enables this.</summary>
|
||||
internal sealed class CutoutPipelinePrepass
|
||||
{
|
||||
private readonly string proxyId;
|
||||
private readonly string frameId;
|
||||
private readonly IReadOnlyList<NestJobPlacement> localInserts;
|
||||
// A job with thousands of repeats still offers the residual as ordinary demand;
|
||||
// do not multiply expensive Clipper/Fill preparation by every physical instance.
|
||||
private const int MaxBundlesPerJob = 32;
|
||||
private sealed record Bundle(string FrameId, IReadOnlyList<NestJobPlacement> LocalInserts);
|
||||
private readonly IReadOnlyDictionary<string, Bundle> bundles;
|
||||
|
||||
private CutoutPipelinePrepass(NestJob engineJob, string proxyId, string frameId,
|
||||
IReadOnlyList<NestJobPlacement> localInserts)
|
||||
private CutoutPipelinePrepass(NestJob engineJob, IReadOnlyDictionary<string, Bundle> bundles)
|
||||
{
|
||||
EngineJob = engineJob;
|
||||
this.proxyId = proxyId;
|
||||
this.frameId = frameId;
|
||||
this.localInserts = localInserts;
|
||||
this.bundles = bundles;
|
||||
}
|
||||
|
||||
internal NestJob EngineJob { get; }
|
||||
@@ -29,51 +29,121 @@ internal sealed class CutoutPipelinePrepass
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (original.Plates.Count == 0)
|
||||
return null;
|
||||
// A single fixed-zero proxy is safe even when a symmetric outer perimeter causes
|
||||
// an engine to deduplicate rotations. General orientation intersections are deferred.
|
||||
// One reservation vector works for every offered stock. Current jobs cannot
|
||||
// represent stock-conditional insert quantities; use the largest spacing.
|
||||
var spacing = original.Plates.Max(p => p.PartSpacing);
|
||||
foreach (var frame in original.Parts.Where(p => p.Quantity == 1 && p.Rotation.Allows(0)))
|
||||
var remaining = original.Parts.ToDictionary(p => p.Id, p => p.Quantity);
|
||||
var geometry = original.Parts.ToDictionary(p => p.Id, p => JobPartGeometry.TryRead(p.Geometry));
|
||||
var bundles = new Dictionary<string, Bundle>(StringComparer.Ordinal);
|
||||
var byFrame = new Dictionary<string, List<NestJobPart>>(StringComparer.Ordinal);
|
||||
foreach (var frame in original.Parts)
|
||||
{
|
||||
var geometry = JobPartGeometry.TryRead(frame.Geometry);
|
||||
if (geometry == null || geometry.Cutouts.Count == 0)
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (bundles.Count >= MaxBundlesPerJob)
|
||||
break;
|
||||
var profile = geometry[frame.Id];
|
||||
if (profile == null || profile.Cutouts.Count == 0)
|
||||
continue;
|
||||
var candidates = original.Parts.Where(p => p.Id != frame.Id).ToArray();
|
||||
if (candidates.Length == 0)
|
||||
continue;
|
||||
for (var hole = 0; hole < geometry.Cutouts.Count; hole++)
|
||||
var proxies = new List<NestJobPart>();
|
||||
byFrame[frame.Id] = proxies;
|
||||
for (var instance = 0; instance < frame.Quantity; instance++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var poses = CutoutRouter.Fill(frame, hole, candidates, spacing, token);
|
||||
if (poses.Count == 0)
|
||||
continue;
|
||||
var reserved = poses.GroupBy(p => p.PartId).ToDictionary(g => g.Key, g => g.Count());
|
||||
var proxyId = "__cutout-proxy-" + frame.Id;
|
||||
if (original.Parts.Any(p => p.Id == proxyId))
|
||||
throw new InvalidOperationException("Cutout proxy ID collides with a requirement.");
|
||||
var transformed = new List<NestJobPart>();
|
||||
foreach (var part in original.Parts)
|
||||
if (bundles.Count >= MaxBundlesPerJob)
|
||||
break;
|
||||
IReadOnlyList<NestJobPlacement>? chosen = null;
|
||||
double frameAngle = 0;
|
||||
// Outer-perimeter symmetry cannot deduplicate asymmetric cutouts.
|
||||
foreach (var angle in frame.Rotation.EnumerateAngles(maxSamples: 16))
|
||||
{
|
||||
if (part.Id == frame.Id)
|
||||
transformed.Add(new NestJobPart(proxyId, part.Geometry, 1, part.Priority,
|
||||
RotationPolicy.Fixed(0)));
|
||||
else
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (!FitsOfferedStock(frame, angle, original.Plates))
|
||||
continue;
|
||||
var local = new List<NestJobPlacement>();
|
||||
var available = new Dictionary<string, int>(remaining);
|
||||
for (var hole = 0; hole < profile.Cutouts.Count; hole++)
|
||||
{
|
||||
var remaining = part.Quantity - reserved.GetValueOrDefault(part.Id);
|
||||
if (remaining > 0)
|
||||
transformed.Add(new NestJobPart(part.Id, part.Geometry, remaining,
|
||||
part.Priority, part.Rotation));
|
||||
token.ThrowIfCancellationRequested();
|
||||
// Flat bundles: frames cannot simultaneously be inserts in another bundle.
|
||||
var candidates = original.Parts
|
||||
.Where(p => p.Id != frame.Id && available[p.Id] > 0
|
||||
&& p.Priority == frame.Priority
|
||||
&& geometry[p.Id] is { Cutouts.Count: 0 })
|
||||
.Select(p => new NestJobPart(p.Id, p.Geometry, available[p.Id],
|
||||
p.Priority, LocalRotation(p.Rotation, angle)))
|
||||
.ToArray();
|
||||
if (candidates.Length == 0)
|
||||
break;
|
||||
foreach (var pose in CutoutRouter.Fill(frame, hole, candidates, spacing, token))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var source = geometry[pose.PartId]!;
|
||||
var policy = original.Parts.Single(p => p.Id == pose.PartId).Rotation;
|
||||
if (!policy.Allows(angle + pose.Rotation)
|
||||
|| local.Any(other => !NestLayoutCheck.Clears(
|
||||
geometry[other.PartId]!, other, source, pose, spacing)))
|
||||
continue;
|
||||
local.Add(pose);
|
||||
available[pose.PartId]--;
|
||||
}
|
||||
}
|
||||
if (local.Count <= (chosen?.Count ?? 0))
|
||||
continue;
|
||||
chosen = local.ToArray();
|
||||
frameAngle = angle;
|
||||
}
|
||||
return new CutoutPipelinePrepass(new NestJob(transformed, original.Plates,
|
||||
original.Options), proxyId, frame.Id, poses);
|
||||
if (chosen == null || chosen.Count == 0)
|
||||
break;
|
||||
var proxyId = $"__cutout-proxy-{frame.Id}-{instance}";
|
||||
if (remaining.ContainsKey(proxyId) || bundles.ContainsKey(proxyId))
|
||||
throw new InvalidOperationException("Cutout proxy ID collides with a requirement.");
|
||||
bundles.Add(proxyId, new Bundle(frame.Id, chosen));
|
||||
proxies.Add(new NestJobPart(proxyId, frame.Geometry, 1, frame.Priority,
|
||||
RotationPolicy.Fixed(frameAngle)));
|
||||
remaining[frame.Id]--;
|
||||
foreach (var pose in chosen)
|
||||
remaining[pose.PartId]--;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
if (bundles.Count == 0)
|
||||
return null;
|
||||
var transformed = new List<NestJobPart>();
|
||||
foreach (var part in original.Parts)
|
||||
{
|
||||
if (byFrame.TryGetValue(part.Id, out var proxies))
|
||||
transformed.AddRange(proxies);
|
||||
if (remaining[part.Id] > 0)
|
||||
transformed.Add(new NestJobPart(part.Id, part.Geometry, remaining[part.Id],
|
||||
part.Priority, part.Rotation));
|
||||
}
|
||||
return new CutoutPipelinePrepass(new NestJob(transformed, original.Plates,
|
||||
original.Options), bundles);
|
||||
}
|
||||
|
||||
private static RotationPolicy LocalRotation(RotationPolicy original, double frameAngle) =>
|
||||
original.Kind switch
|
||||
{
|
||||
RotationPolicyKind.Automatic => RotationPolicy.Automatic,
|
||||
RotationPolicyKind.Fixed => RotationPolicy.Fixed(original.Start - frameAngle,
|
||||
original.Allow180Equivalent),
|
||||
_ => RotationPolicy.BoundedSweep(original.Start - frameAngle,
|
||||
original.End - frameAngle, original.Step, original.Allow180Equivalent),
|
||||
};
|
||||
|
||||
private static bool FitsOfferedStock(NestJobPart frame, double angle,
|
||||
IReadOnlyList<NestPlateStock> stock)
|
||||
{
|
||||
var part = new Part(DrawingJobMapper.CreateDrawing(frame));
|
||||
part.Rotate(angle);
|
||||
var bounds = NestLayoutCheck.MaterialBounds(part);
|
||||
return stock.Any(s => s.Quantity != 0 && s.Fits(bounds.Length, bounds.Width,
|
||||
NestTolerances.WorkAreaSlack));
|
||||
}
|
||||
|
||||
/// <summary>Reject untrusted accounting before expansion. A failed trial is never bindable,
|
||||
/// including with allowInvalid. Only actually placed proxies consume reserved inserts.</summary>
|
||||
internal bool TryExpand(NestJob original, NestJobResult result, out NestJobResult? expanded,
|
||||
internal bool TryExpand(NestJob original, NestJobResult result, CancellationToken token,
|
||||
out NestJobResult? expanded,
|
||||
out string failure)
|
||||
{
|
||||
expanded = null;
|
||||
@@ -81,10 +151,12 @@ internal sealed class CutoutPipelinePrepass
|
||||
var parts = EngineJob.Parts.ToDictionary(p => p.Id);
|
||||
var counts = parts.ToDictionary(p => p.Key, _ => 0);
|
||||
var indices = parts.ToDictionary(p => p.Key, _ => new HashSet<int>());
|
||||
var used = EngineJob.Plates.ToDictionary(s => s.Id, _ => 0);
|
||||
var stockById = EngineJob.Plates.ToDictionary(s => s.Id, StringComparer.Ordinal);
|
||||
var used = stockById.ToDictionary(s => s.Key, _ => 0, StringComparer.Ordinal);
|
||||
var expectedIndex = 0;
|
||||
foreach (var sheet in result.Plates)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (sheet.PlateIndex != expectedIndex++ || sheet.Placements.Count == 0
|
||||
|| !used.ContainsKey(sheet.StockId))
|
||||
return false;
|
||||
@@ -97,22 +169,32 @@ internal sealed class CutoutPipelinePrepass
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (indices.Any(row => row.Value.Any(index => index >= counts[row.Key])))
|
||||
return false;
|
||||
foreach (var row in indices)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (row.Value.Any(index => index >= counts[row.Key]))
|
||||
return false;
|
||||
}
|
||||
if (result.Fulfillment.Count != parts.Count || result.StockUsage.Count != used.Count)
|
||||
return false;
|
||||
var seenParts = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var row in result.Fulfillment)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (row.PartId == null || !parts.TryGetValue(row.PartId, out var part)
|
||||
|| !seenParts.Add(row.PartId)
|
||||
|| row.Requested != part.Quantity || row.Placed != counts[row.PartId]
|
||||
|| row.Unplaced != part.Quantity - counts[row.PartId]
|
||||
|| result.Fulfillment.Count(r => r.PartId == row.PartId) != 1)
|
||||
|| row.Unplaced != part.Quantity - counts[row.PartId])
|
||||
return false;
|
||||
}
|
||||
var seenStock = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var row in result.StockUsage)
|
||||
{
|
||||
var stock = EngineJob.Plates.FirstOrDefault(s => s.Id == row.StockId);
|
||||
if (stock == null || row.Used != used[row.StockId]
|
||||
|| row.Remaining != stock.Quantity - row.Used
|
||||
|| result.StockUsage.Count(r => r.StockId == row.StockId) != 1)
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (row.StockId == null || !stockById.TryGetValue(row.StockId, out var stock)
|
||||
|| !seenStock.Add(row.StockId)
|
||||
|| row.Used != used[row.StockId]
|
||||
|| row.Remaining != stock.Quantity - row.Used)
|
||||
return false;
|
||||
}
|
||||
var complete = parts.Values.All(p => counts[p.Id] == p.Quantity);
|
||||
@@ -121,27 +203,22 @@ internal sealed class CutoutPipelinePrepass
|
||||
|| (!complete && result.StopReason == NestJobStopReason.Completed))
|
||||
return false;
|
||||
|
||||
// An incomplete solve cannot silently reserve inserts or reach the commit boundary.
|
||||
if (!complete)
|
||||
{
|
||||
failure = "Cutout engine did not complete the transformed job";
|
||||
return false;
|
||||
}
|
||||
var builder = new NestJobResultBuilder(original);
|
||||
foreach (var sheet in result.Plates)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var poses = new List<(string PartId, double X, double Y, double Rotation)>();
|
||||
foreach (var pose in sheet.Placements)
|
||||
{
|
||||
if (pose.PartId != proxyId)
|
||||
if (!bundles.TryGetValue(pose.PartId, out var bundle))
|
||||
{
|
||||
poses.Add((pose.PartId, pose.X, pose.Y, pose.Rotation));
|
||||
continue;
|
||||
}
|
||||
poses.Add((frameId, pose.X, pose.Y, pose.Rotation));
|
||||
poses.Add((bundle.FrameId, pose.X, pose.Y, pose.Rotation));
|
||||
var sine = System.Math.Sin(pose.Rotation);
|
||||
var cosine = System.Math.Cos(pose.Rotation);
|
||||
foreach (var local in localInserts)
|
||||
foreach (var local in bundle.LocalInserts)
|
||||
poses.Add((local.PartId,
|
||||
pose.X + local.X * cosine - local.Y * sine,
|
||||
pose.Y + local.X * sine + local.Y * cosine,
|
||||
@@ -158,12 +235,8 @@ internal sealed class CutoutPipelinePrepass
|
||||
return false;
|
||||
}
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
expanded = builder.Build(result.StopReason);
|
||||
if (expanded.Status != NestJobStatus.Complete)
|
||||
{
|
||||
expanded = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -181,10 +181,12 @@ public static class NestPipeline
|
||||
// A compound may be physically invalid even when its envelope is legal.
|
||||
// Refuse every unverified trial, including with explicit invalid-result consent.
|
||||
var failure = "Cutout engine output failed reconciliation";
|
||||
if (violations.Count == 0 && prepass.TryExpand(job, raw, out var expanded, out failure))
|
||||
if (violations.Count == 0 && prepass.TryExpand(job, raw, token, out var expanded, out failure))
|
||||
{
|
||||
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
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
public class CutoutPipelinePersistenceTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExpandedMultiSheetProposalCommitsAndReloadsAsSeparateDrawings()
|
||||
{
|
||||
var frame = new OpenNest.Shapes.RingShape { OuterDiameter = 20, InnerDiameter = 10 }
|
||||
.GetDrawing();
|
||||
frame.Name = "neutral-frame";
|
||||
var program = new Program();
|
||||
program.MoveTo(0, 0);
|
||||
program.LineTo(6, 0);
|
||||
program.LineTo(6, 6);
|
||||
program.LineTo(0, 6);
|
||||
program.LineTo(0, 0);
|
||||
var insert = new Drawing("neutral-insert", program);
|
||||
var request = new NestPipelineRequest("Irregular", new[] {
|
||||
new NestItem { Drawing = frame, Quantity = 2 },
|
||||
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);
|
||||
Assert.Contains(ordinary.Raw.Fulfillment, f => f.Unplaced > 0);
|
||||
var result = NestPipeline.RunCutoutPreview(request);
|
||||
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" },
|
||||
p.Placements.Select(x => x.PartId)));
|
||||
|
||||
var nest = new Nest();
|
||||
nest.Drawings.Add(frame);
|
||||
nest.Drawings.Add(insert);
|
||||
using (var manager = new PlateManager(nest))
|
||||
{
|
||||
var committed = NestPipelineCommit.ApplyToEmptyPlates(result, manager);
|
||||
Assert.Equal(2, committed.Count);
|
||||
Assert.All(committed, plate => Assert.Equal(new[] { frame, insert },
|
||||
plate.Parts.Select(p => p.BaseDrawing)));
|
||||
}
|
||||
Assert.Equal(2, frame.Quantity.Nested);
|
||||
Assert.Equal(2, insert.Quantity.Nested);
|
||||
using var stream = new MemoryStream();
|
||||
Assert.True(new NestWriter(nest).Write(stream));
|
||||
var loaded = new NestReader(new MemoryStream(stream.ToArray())).Read();
|
||||
Assert.Equal(2, loaded.Plates.Count);
|
||||
for (var i = 0; i < loaded.Plates.Count; i++)
|
||||
{
|
||||
var expected = nest.Plates[i];
|
||||
var actual = loaded.Plates[i];
|
||||
Assert.Equal(expected.Size, actual.Size);
|
||||
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" },
|
||||
actual.Parts.Select(p => p.BaseDrawing.Name));
|
||||
for (var j = 0; j < actual.Parts.Count; j++)
|
||||
{
|
||||
Assert.Equal(expected.Parts[j].Location.X, actual.Parts[j].Location.X, 6);
|
||||
Assert.Equal(expected.Parts[j].Location.Y, actual.Parts[j].Location.Y, 6);
|
||||
Assert.Equal(expected.Parts[j].Rotation, actual.Parts[j].Rotation, 6);
|
||||
}
|
||||
}
|
||||
Assert.Equal(2, loaded.Drawings.Single(d => d.Name == "neutral-frame").Quantity.Nested);
|
||||
Assert.Equal(2, loaded.Drawings.Single(d => d.Name == "neutral-insert").Quantity.Nested);
|
||||
}
|
||||
}
|
||||
+20
-12
@@ -97,18 +97,26 @@ 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` prepares at most one quantity-one frame with one
|
||||
cutout as a fixed-zero proxy. Reserved inserts are removed from the engine's independent demand;
|
||||
remaining demand is offered once to the selected engine. Only after a complete, accounted-for
|
||||
engine result does it expand the proxy back to original requirement IDs, validate every physical
|
||||
pose, stock and quantity, then bind caller drawings. Incomplete, 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. Frames needing other
|
||||
orientations or repeated frame copies still run through the ordinary unbundled path. This is a
|
||||
narrow internal integration trial, not a production setting or a cut-ready nest; multi-frame
|
||||
routing, rotation compatibility, save/reload and cutting/post safety are still pending before
|
||||
enablement.
|
||||
The internal `NestPipeline.RunCutoutPreview` 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
|
||||
requirements; inserts with a different priority or their own cutouts stay independent, so
|
||||
bundles remain flat. It uses the largest offered spacing for every bundle because the current
|
||||
job contract cannot condition insert demand on the stock selected later. Remaining demand is
|
||||
offered to the selected engine once. Only actually placed proxies expand to original frame and
|
||||
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
|
||||
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. Cutting/post safety remains pending before enablement.
|
||||
|
||||
## Renamed engines
|
||||
|
||||
|
||||
Reference in new issue
Block a user