feat(cutouts): trial physical composite reconciliation in pipeline

This commit is contained in:
aj committed 2026-10-09 18:57:31 -04:00
1 parent 71bd964344
commit 0b29c683b9
4 files changed
+562 -6

No files matched your search

@@ -0,0 +1,304 @@
using OpenNest.CNC;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Cutouts;
using OpenNest.Geometry;
using TestShapes = OpenNest.Engine.Tests.NestingEngines.Shapes;
namespace OpenNest.Engine.Tests.Jobs.Cutouts;
public class CutoutPipelinePreviewTests
{
private static NestPipelineRequest Request(string engine = "Irregular")
{
var frame = new Drawing("frame", TestShapes.Ring(20, 10));
var insert = new Drawing("insert", TestShapes.Rectangle(3, 3));
return new NestPipelineRequest(engine,
new[] { new NestItem { Drawing = frame, Quantity = 1 },
new NestItem { Drawing = insert, Quantity = 1 } },
new[] { new NestPlateStock("sheet", new Size(22, 22), 1, 0.25) });
}
[Theory]
[InlineData("Irregular")]
[InlineData("Default")]
public void RegisteredEngineExpandsOnlyPhysicalOriginalDrawings(string engine)
{
var request = Request(engine);
var result = NestPipeline.RunCutoutPreview(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));
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);
}
private sealed class Stub(Func<NestJob, NestJobResult> solve) : INestingEngine
{
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default) => solve(job);
}
private static NestJobResult Result(NestJob job, params NestJobPlacement[] poses)
{
var placed = poses.GroupBy(p => p.PartId).ToDictionary(g => g.Key, g => g.Count());
var complete = job.Parts.All(p => placed.GetValueOrDefault(p.Id) == p.Quantity);
return new NestJobResult(complete ? NestJobStatus.Complete : NestJobStatus.Incomplete,
complete ? NestJobStopReason.Completed : NestJobStopReason.NoPlacementFound,
poses.Length == 0 ? Array.Empty<NestJobPlateResult>() :
new[] { new NestJobPlateResult(0, job.Plates[0], poses) },
job.Parts.Select(p => new PartFulfillment(p.Id, p.Quantity,
placed.GetValueOrDefault(p.Id), p.Quantity - placed.GetValueOrDefault(p.Id))),
new[] { new StockUsage(job.Plates[0].Id, poses.Length == 0 ? 0 : 1,
job.Plates[0].Quantity - (poses.Length == 0 ? 0 : 1)) });
}
[Fact]
public void SingleSolveReceivesOneProxyAndNoDuplicatedInsertDemand()
{
var called = 0;
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
{
called++;
var proxy = Assert.Single(job.Parts);
Assert.StartsWith("__cutout-proxy-", proxy.Id);
Assert.True(proxy.Rotation.Allows(0));
Assert.False(proxy.Rotation.Allows(System.Math.PI / 2));
return Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11, 0));
}), "stub", request);
Assert.Equal(1, called);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(2, result.Plates.Single().Parts.Count);
}
[Fact]
public void IncompleteEngineDoesNotBindReservedInsertEvenWithInvalidConsent()
{
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(new Stub(job => Result(job)), "stub", request);
Assert.False(result.CanKeep);
Assert.Empty(result.Plates);
Assert.Contains(result.Violations, v => v.Contains("did not complete"));
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 id = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-")).Id;
var pose = new NestJobPlacement(id, 1, 11, 11, 0);
return Result(job, pose);
}), "stub", request);
Assert.False(result.CanKeep);
Assert.Empty(result.Plates);
Assert.Contains(result.Violations, v => v.Contains("accounting"));
}
[Fact]
public void PartialBundleKeepsIndependentResidualByOriginalRequirement()
{
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 proxy = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-"));
var residual = job.Parts.Single(p => p.Id == "part-1");
Assert.Equal(1, residual.Quantity);
return Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11, 0),
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" },
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));
Assert.All(result.Plates.Single().Parts.Where(p => p.BaseDrawing == request.Items[1].Drawing),
p => Assert.Same(request.Items[1].Drawing, p.BaseDrawing));
}
[Fact]
public void UntrustedStockOverdrawOrRotationCannotBindAComposite()
{
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(new Stub(job =>
{
var proxy = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-"));
return Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11,
System.Math.PI / 2));
}), "stub", request);
Assert.False(result.CanKeep);
Assert.Empty(result.Plates);
Assert.Contains(result.Violations, v => v.Contains("rotation constraint"));
var overdraw = NestPipeline.RunCutoutPreview(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) }),
new NestJobPlateResult(1, job.Plates[0], new[] {
new NestJobPlacement(job.Parts[0].Id, 1, 11, 11, 0) }) },
Array.Empty<PartFulfillment>(), Array.Empty<StockUsage>())), "stub", request);
Assert.False(overdraw.CanKeep);
Assert.Empty(overdraw.Plates);
Assert.Contains(overdraw.Violations, v => v.Contains("only 1 are available"));
}
[Fact]
public void CancellationBeforePreparationDoesNotCallEngine()
{
using var cts = new CancellationTokenSource();
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));
}
[Fact]
public void EnvelopeValidResidualInsideReservedHoleFailsAfterPhysicalExpansion()
{
var request = Request("stub");
request.Items[1].Quantity = 2;
var result = NestPipeline.RunCutoutPreview(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),
new NestJobPlacement("part-1", 0, 11, 11, 0));
}), "stub", request);
Assert.False(result.CanKeep);
Assert.Empty(result.Plates);
Assert.Contains(result.Violations, v => v.Contains("required spacing"));
AssertCannotCommit(result, request);
}
private static void AssertCannotCommit(NestPipelineResult result, NestPipelineRequest request)
{
var nest = new Nest();
foreach (var item in request.Items)
nest.Drawings.Add(item.Drawing);
var empty = nest.CreatePlate();
using var manager = new PlateManager(nest);
Assert.Throws<InvalidOperationException>(() =>
NestPipelineCommit.ApplyToEmptyPlates(result, manager, allowInvalid: true));
Assert.Same(empty, Assert.Single(nest.Plates));
Assert.Empty(empty.Parts);
Assert.All(request.Items, item => Assert.Equal(0, item.Drawing.Quantity.Nested));
}
private sealed class CollectProgress : IProgress<NestJobProgress>
{
public List<NestJobProgress> Events { get; } = new();
public void Report(NestJobProgress value) => Events.Add(value);
}
[Fact]
public void EvaluationProgressSurvivesButUnverifiedCommitsNeverEscape()
{
var request = Request("stub");
var progress = new CollectProgress();
var result = NestPipeline.RunCutoutPreview(new ProgressStub(), "stub", request, progress);
Assert.False(result.CanKeep);
Assert.Single(progress.Events);
Assert.Equal(NestJobStage.EvaluatingCandidate, progress.Events[0].Stage);
Assert.Equal(0, progress.Events[0].CommittedPlates);
Assert.Equal(0, progress.Events[0].CommittedParts);
}
private sealed class ProgressStub : INestingEngine
{
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default)
{
progress?.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate,
job.Plates[0].Id, 0, 1, 1));
progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted,
job.Plates[0].Id, 0, 1, 1));
return Result(job);
}
}
[Fact]
public void ReorderedButContiguousEngineIndicesAreAccepted()
{
var request = Request("stub");
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 proxy = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-"));
Assert.Equal(2, job.Parts.Single(p => p.Id == "part-1").Quantity);
return Result(job, new NestJobPlacement(proxy.Id, 0, 11, 11, 0),
new NestJobPlacement("part-1", 1, 22, 1, 0),
new NestJobPlacement("part-1", 0, 22, 9, 0));
}), "stub", request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(new[] { 0, 1, 2 }, result.Raw.Plates.Single().Placements
.Where(p => p.PartId == "part-1").Select(p => p.InstanceIndex));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void ForgedFulfillmentOrStockMetadataCannotBind(bool fulfillment)
{
var request = Request("stub");
var result = NestPipeline.RunCutoutPreview(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));
return new NestJobResult(good.Status, good.StopReason, good.Plates,
fulfillment ? new[] { new PartFulfillment(proxy.Id, 1, 0, 1) } : good.Fulfillment,
fulfillment ? good.StockUsage : new[] { new StockUsage("sheet", 0, 1) });
}), "stub", request);
Assert.False(result.CanKeep);
Assert.Empty(result.Plates);
Assert.Contains(result.Violations, v => v.Contains("accounting"));
}
[Fact]
public void SameDrawingInTwoRequirementRowsRetainsSeparateIds()
{
var initial = Request("stub");
var request = initial with
{
Items = new[] { initial.Items[0], initial.Items[1],
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 proxy = job.Parts.Single(p => p.Id.StartsWith("__cutout-proxy-"));
var residual = job.Parts.Where(p => p.Id != proxy.Id)
.Select((p, i) => new NestJobPlacement(p.Id, 0, 22, 1 + 5 * i, 0));
return Result(job, new[] { new NestJobPlacement(proxy.Id, 0, 11, 11, 0) }
.Concat(residual).ToArray());
}), "stub", request);
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.Equal(new[] { "part-0", "part-1", "part-2" },
result.Raw.Plates.Single().Placements.Select(p => p.PartId));
Assert.Same(result.Plates.Single().Parts[1].BaseDrawing,
result.Plates.Single().Parts[2].BaseDrawing);
}
[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,169 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace OpenNest.Engine.Jobs.Cutouts;
/// <summary>Internal-only single-frame composite trial. No normal pipeline caller enables this.</summary>
internal sealed class CutoutPipelinePrepass
{
private readonly string proxyId;
private readonly string frameId;
private readonly IReadOnlyList<NestJobPlacement> localInserts;
private CutoutPipelinePrepass(NestJob engineJob, string proxyId, string frameId,
IReadOnlyList<NestJobPlacement> localInserts)
{
EngineJob = engineJob;
this.proxyId = proxyId;
this.frameId = frameId;
this.localInserts = localInserts;
}
internal NestJob EngineJob { get; }
internal static CutoutPipelinePrepass? Prepare(NestJob original, CancellationToken token)
{
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.
var spacing = original.Plates.Max(p => p.PartSpacing);
foreach (var frame in original.Parts.Where(p => p.Quantity == 1 && p.Rotation.Allows(0)))
{
var geometry = JobPartGeometry.TryRead(frame.Geometry);
if (geometry == null || geometry.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++)
{
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 (part.Id == frame.Id)
transformed.Add(new NestJobPart(proxyId, part.Geometry, 1, part.Priority,
RotationPolicy.Fixed(0)));
else
{
var remaining = part.Quantity - reserved.GetValueOrDefault(part.Id);
if (remaining > 0)
transformed.Add(new NestJobPart(part.Id, part.Geometry, remaining,
part.Priority, part.Rotation));
}
}
return new CutoutPipelinePrepass(new NestJob(transformed, original.Plates,
original.Options), proxyId, frame.Id, poses);
}
}
return null;
}
/// <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,
out string failure)
{
expanded = null;
failure = "Invalid cutout engine accounting";
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 expectedIndex = 0;
foreach (var sheet in result.Plates)
{
if (sheet.PlateIndex != expectedIndex++ || sheet.Placements.Count == 0
|| !used.ContainsKey(sheet.StockId))
return false;
used[sheet.StockId]++;
foreach (var pose in sheet.Placements)
{
if (pose.PartId == null || !parts.ContainsKey(pose.PartId)
|| pose.InstanceIndex < 0 || !indices[pose.PartId].Add(pose.InstanceIndex)
|| ++counts[pose.PartId] > parts[pose.PartId].Quantity)
return false;
}
}
if (indices.Any(row => row.Value.Any(index => index >= counts[row.Key])))
return false;
if (result.Fulfillment.Count != parts.Count || result.StockUsage.Count != used.Count)
return false;
foreach (var row in result.Fulfillment)
if (row.PartId == null || !parts.TryGetValue(row.PartId, out var part)
|| 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)
return false;
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)
return false;
}
var complete = parts.Values.All(p => counts[p.Id] == p.Quantity);
if (result.Status != (complete ? NestJobStatus.Complete : NestJobStatus.Incomplete)
|| (complete && result.StopReason != NestJobStopReason.Completed)
|| (!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)
{
var poses = new List<(string PartId, double X, double Y, double Rotation)>();
foreach (var pose in sheet.Placements)
{
if (pose.PartId != proxyId)
{
poses.Add((pose.PartId, pose.X, pose.Y, pose.Rotation));
continue;
}
poses.Add((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)
poses.Add((local.PartId,
pose.X + local.X * cosine - local.Y * sine,
pose.Y + local.X * sine + local.Y * cosine,
pose.Rotation + local.Rotation));
}
// Canonical stock reference, not an engine-supplied object with altered settings.
var stock = original.Plates.Single(s => s.Id == sheet.StockId);
try
{
builder.AddSheet(stock, poses);
}
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
{
return false;
}
}
expanded = builder.Build(result.StopReason);
if (expanded.Status != NestJobStatus.Complete)
{
expanded = null;
return false;
}
return true;
}
}
+74 -2
View File
@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Engine.Jobs.Cutouts;
namespace OpenNest.Engine.Jobs;
@@ -72,6 +73,15 @@ public sealed class NestPipelineResult
/// </summary>
public static class NestPipeline
{
private sealed class PreviewProgress(IProgress<NestJobProgress> inner) : IProgress<NestJobProgress>
{
public void Report(NestJobProgress value)
{
if (value != null && value.Stage == NestJobStage.EvaluatingCandidate)
inner.Report(value with { CommittedPlates = 0, CommittedParts = 0 });
}
}
/// <summary>Resolves <see cref="NestPipelineRequest.EngineName"/> through
/// <see cref="NestingEngineRegistry"/>; unknown names throw <see cref="NotSupportedException"/>.
/// Engine cancellation propagates unchanged.</summary>
@@ -93,6 +103,32 @@ public static class NestPipeline
NestPipelineRequest request,
IProgress<NestJobProgress> progress = null,
CancellationToken token = default
) => RunCore(engine, engineName, request, progress, token, cutoutPreview: false);
/// <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(
INestingEngine engine,
string engineName,
NestPipelineRequest request,
IProgress<NestJobProgress> progress = null,
CancellationToken token = default
) => RunCore(engine, engineName, request, progress, token, cutoutPreview: true);
internal static NestPipelineResult RunCutoutPreview(
NestPipelineRequest request,
IProgress<NestJobProgress> progress = null,
CancellationToken token = default
) => RunCutoutPreview(NestingEngineRegistry.Create(request.EngineName),
request.EngineName, request, progress, token);
private static NestPipelineResult RunCore(
INestingEngine engine,
string engineName,
NestPipelineRequest request,
IProgress<NestJobProgress> progress,
CancellationToken token,
bool cutoutPreview
)
{
ArgumentNullException.ThrowIfNull(engine);
@@ -115,10 +151,15 @@ public static class NestPipeline
var job = new NestJob(parts, request.Stock, request.Options);
NestJobValidator.Validate(job);
var prepass = cutoutPreview ? CutoutPipelinePrepass.Prepare(job, token) : null;
var engineJob = prepass?.EngineJob ?? job;
if (prepass != null)
NestJobValidator.Validate(engineJob);
var clock = Stopwatch.StartNew();
var raw =
engine.Solve(job, progress, token)
engine.Solve(engineJob, prepass == null || progress == null
? progress : new PreviewProgress(progress), token)
?? throw new InvalidOperationException($"Engine '{engineName}' returned no result.");
var solveTime = clock.Elapsed;
token.ThrowIfCancellationRequested();
@@ -129,7 +170,27 @@ public static class NestPipeline
kv => kv.Value.Name ?? kv.Key,
StringComparer.Ordinal
);
var violations = NestLayoutCheck.Violations(job, raw, names, out var canKeep);
var checkedNames = prepass == null ? names : engineJob.Parts.ToDictionary(p => p.Id,
p => names.GetValueOrDefault(p.Id, p.Id), StringComparer.Ordinal);
var violations = NestLayoutCheck.Violations(engineJob, raw, checkedNames, out var canKeep).ToList();
if (prepass != null)
{
// 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))
{
raw = expanded;
violations.AddRange(NestLayoutCheck.Violations(job, raw, names, out var physicalKeep));
canKeep = physicalKeep && violations.Count == 0;
}
else
{
if (violations.Count == 0)
violations.Add(failure);
canKeep = false;
}
}
var validationTime = clock.Elapsed;
var plates = canKeep
@@ -141,6 +202,17 @@ public static class NestPipeline
.ToList()
: new List<ProposedPlate>();
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,
+15 -4
View File
@@ -70,8 +70,9 @@ remain separate sequencer/verification work.
## Filling cutouts (not yet in production)
Placing parts inside another part's enclosed cutout is being built as a step that runs before any
engine, so every engine benefits. Nothing calls it yet: a part inside a cutout must be cut before
the cutout's contour, and the sequencer does not enforce that order.
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, and the cutting/post safety gates are
not complete. Ordinary whole-job calls do not enable this path.
`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
@@ -96,8 +97,18 @@ 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 router has no pipeline caller and does not change
stock, live demand, cutting order or posting safety; in-hole production use remains held.
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.
## Renamed engines