mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 05:01:05 -04:00
feat(engine): nest round frames inside larger round frames
The cutout prepass now bundles frames smallest first, so a larger frame's round cutout can take a ring part or an earlier bundle as one insert through the circle packer: a flange holds a washer that holds a smaller washer that holds a slug. Chains stop at four parts deep. Lattice and NFP proposals never take these inserts, since they could seat a part in a ring's bore. Expansion and released demand recurse, so placed order cuts innermost first at every level and an unplaced outer frame returns its whole chain to demand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f3a6af7f09
commit
a430da89d0
3 files changed
+258
-37
No files matched your search
@@ -0,0 +1,137 @@
|
||||
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;
|
||||
|
||||
/// <summary>Round frames nest inside larger round frames: a flange's bore holds a washer whose
|
||||
/// bore holds a smaller washer, and so on, as one bundle the engine places as its outer frame.</summary>
|
||||
[Collection("Cutout pipeline registry")]
|
||||
public class CutoutPipelineChainTests
|
||||
{
|
||||
private const double Sheet = 42;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Each ring's bore holds the next part with room for the 0.25 spacing.
|
||||
private static NestPipelineRequest Request(params Program[] programs) =>
|
||||
new("stub",
|
||||
programs.Select((p, i) => new NestItem { Drawing = new Drawing($"d{i}", p), Quantity = 1 }).ToArray(),
|
||||
new[] { new NestPlateStock("sheet", new Size(Sheet, Sheet), 1, 0.25) });
|
||||
|
||||
private static readonly Program[] Chain =
|
||||
{
|
||||
TestShapes.Ring(40, 30), TestShapes.Ring(28, 18), TestShapes.Ring(16, 8), TestShapes.Disc(3),
|
||||
};
|
||||
|
||||
private static NestJobResult PlaceProxies(NestJob job)
|
||||
{
|
||||
var builder = new NestJobResultBuilder(job);
|
||||
builder.AddSheet(job.Plates[0], job.Parts.Where(p => p.Id.StartsWith("__cutout-proxy-"))
|
||||
.Select(p => (p.Id, Sheet / 2, Sheet / 2, p.Rotation.EnumerateAngles(1)[0])));
|
||||
var complete = job.Parts.All(p => p.Id.StartsWith("__cutout-proxy-"));
|
||||
return builder.Build(complete ? NestJobStopReason.Completed : NestJobStopReason.NoPlacementFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FourLevelChainIsOneBundleThatCutsInnermostFirst()
|
||||
{
|
||||
var request = Request(Chain);
|
||||
var engine = new Stub(PlaceProxies);
|
||||
var result = NestPipeline.RunCutoutTrial(engine, "stub", request);
|
||||
|
||||
var proxy = Assert.Single(Assert.Single(engine.Jobs).Parts);
|
||||
Assert.Equal("__cutout-proxy-part-0-0", proxy.Id);
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(new[] { "part-3", "part-2", "part-1", "part-0" },
|
||||
Assert.Single(result.Raw.Plates).Placements.Select(p => p.PartId));
|
||||
// A single insert sits concentric at every level.
|
||||
Assert.All(result.Raw.Plates[0].Placements, p =>
|
||||
Assert.True(new Vector(p.X, p.Y).DistanceTo(new Vector(Sheet / 2, Sheet / 2)) < 1e-6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisteredEngineNestsTheWholeChainOnOneSheet()
|
||||
{
|
||||
var request = Request(Chain) with { EngineName = "Irregular" };
|
||||
var result = NestPipeline.Run(request);
|
||||
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(request.Items.Reverse().Select(i => i.Drawing),
|
||||
Assert.Single(result.Plates).Parts.Select(p => p.BaseDrawing));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RingWithAnEmptyBoreIsAnInsert()
|
||||
{
|
||||
var engine = new Stub(PlaceProxies);
|
||||
var result = NestPipeline.RunCutoutTrial(engine, "stub", Request(Chain[0], Chain[1]));
|
||||
|
||||
Assert.Single(Assert.Single(engine.Jobs).Parts);
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
Assert.Equal(new[] { "part-1", "part-0" },
|
||||
Assert.Single(result.Raw.Plates).Placements.Select(p => p.PartId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnplacedOuterFrameReleasesItsWholeChain()
|
||||
{
|
||||
var request = Request(Chain);
|
||||
var result = NestPipeline.RunCutoutTrial(new Stub(job =>
|
||||
new NestJobResultBuilder(job).Build(NestJobStopReason.NoPlacementFound)), "stub", request);
|
||||
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
Assert.All(result.Raw.Fulfillment, f => Assert.Equal((1, 0, 1), (f.Requested, f.Placed, f.Unplaced)));
|
||||
Assert.Equal(new[] {
|
||||
("part-0", NestUnplacedReason.NoPlacementFound),
|
||||
("part-1", NestUnplacedReason.FrameUnplaced),
|
||||
("part-2", NestUnplacedReason.FrameUnplaced),
|
||||
("part-3", NestUnplacedReason.FrameUnplaced) },
|
||||
result.Unplaced.Select(u => (u.PartId, u.Reason)).OrderBy(u => u.PartId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChainsStopAtTheLevelLimit()
|
||||
{
|
||||
// Five levels: the outermost flange cannot take the four-level bundle, so it stays
|
||||
// independent and the engine places both.
|
||||
var programs = new[] { TestShapes.Ring(40, 34) }.Concat(new[] {
|
||||
TestShapes.Ring(33, 26), TestShapes.Ring(25, 18), TestShapes.Ring(17, 10), TestShapes.Disc(4.5) }).ToArray();
|
||||
var engine = new Stub(job => new NestJobResultBuilder(job).Build(NestJobStopReason.NoPlacementFound));
|
||||
var request = Request(programs) with
|
||||
{
|
||||
Stock = new[] { new NestPlateStock("sheet", new Size(Sheet, Sheet), 2, 0.25) },
|
||||
};
|
||||
NestPipeline.RunCutoutTrial(engine, "stub", request);
|
||||
|
||||
var job = Assert.Single(engine.Jobs);
|
||||
Assert.Equal(new[] { "part-0", "__cutout-proxy-part-1-0" }, job.Parts.Select(p => p.Id));
|
||||
Assert.Equal(CutoutPipelinePrepass.MaxLevels, programs.Length - 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPriorityStaysIndependentAtEveryLevel()
|
||||
{
|
||||
var request = Request(Chain);
|
||||
request.Items[3].Priority = 1;
|
||||
var engine = new Stub(job => new NestJobResultBuilder(job).Build(NestJobStopReason.NoPlacementFound));
|
||||
NestPipeline.RunCutoutTrial(engine, "stub", request);
|
||||
|
||||
Assert.Equal(new[] { "__cutout-proxy-part-0-0", "part-3" },
|
||||
Assert.Single(engine.Jobs).Parts.Select(p => p.Id));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.CirclePacking;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
|
||||
namespace OpenNest.Engine.Jobs.Cutouts;
|
||||
@@ -10,12 +11,23 @@ namespace OpenNest.Engine.Jobs.Cutouts;
|
||||
/// <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>
|
||||
/// <remarks>Bundles may nest: smaller frames are bundled first, so a larger round cutout can
|
||||
/// take a ring part or an earlier bundle (a ring holding its own inserts) as one round insert.
|
||||
/// Those go only through <see cref="CutoutCirclePacker"/>, which keeps every disc clear of the
|
||||
/// others' whole enclosing circles; lattice and NFP placement could seat a part in a ring's
|
||||
/// empty bore, or over a nested bundle's reserved inserts.</remarks>
|
||||
internal sealed class CutoutPipelinePrepass
|
||||
{
|
||||
// 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);
|
||||
|
||||
/// <summary>Most parts on one frame-in-frame chain: flange, washer, smaller washer, slug.</summary>
|
||||
internal const int MaxLevels = 4;
|
||||
|
||||
// Levels: parts on the deepest chain through the frame; Parts: every physical part.
|
||||
private sealed record Bundle(string FrameId, IReadOnlyList<NestJobPlacement> LocalInserts,
|
||||
int Levels, int Parts);
|
||||
private readonly IReadOnlyDictionary<string, Bundle> bundles;
|
||||
|
||||
private CutoutPipelinePrepass(NestJob engineJob, IReadOnlyDictionary<string, Bundle> bundles)
|
||||
@@ -36,16 +48,24 @@ internal sealed class CutoutPipelinePrepass
|
||||
var spacing = original.Plates.Max(p => p.PartSpacing);
|
||||
var remaining = original.Parts.ToDictionary(p => p.Id, p => p.Quantity);
|
||||
var geometry = original.Parts.ToDictionary(p => p.Id, p => JobPartGeometry.TryRead(p.Geometry));
|
||||
// Proxies join these as they are built, so a larger frame can take one as an insert.
|
||||
var offers = original.Parts.ToList();
|
||||
var requirements = original.Parts.ToDictionary(p => p.Id);
|
||||
var round = original.Parts.ToDictionary(p => p.Id, p => geometry[p.Id] is { } g
|
||||
&& Item.Create(g).FillRatio >= CutoutCirclePacker.MinFillRatio);
|
||||
var bundles = new Dictionary<string, Bundle>(StringComparer.Ordinal);
|
||||
var byFrame = new Dictionary<string, List<NestJobPart>>(StringComparer.Ordinal);
|
||||
foreach (var frame in original.Parts)
|
||||
// Smallest first: a frame can only hold smaller parts, so its candidate inner bundles
|
||||
// already exist, and small holes take small inserts before large holes are tried.
|
||||
var frames = original.Parts.Where(p => geometry[p.Id] is { Cutouts.Count: > 0 })
|
||||
.OrderBy(p => System.Math.Abs(geometry[p.Id]!.Perimeter.Area()))
|
||||
.ToArray();
|
||||
foreach (var frame in frames)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (bundles.Count >= MaxBundlesPerJob)
|
||||
break;
|
||||
var profile = geometry[frame.Id];
|
||||
if (profile == null || profile.Cutouts.Count == 0)
|
||||
continue;
|
||||
var profile = geometry[frame.Id]!;
|
||||
var proxies = new List<NestJobPart>();
|
||||
byFrame[frame.Id] = proxies;
|
||||
for (var instance = 0; instance < frame.Quantity; instance++)
|
||||
@@ -66,22 +86,25 @@ internal sealed class CutoutPipelinePrepass
|
||||
for (var hole = 0; hole < profile.Cutouts.Count; hole++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
// Flat bundles: frames cannot simultaneously be inserts in another bundle.
|
||||
var candidates = original.Parts
|
||||
var eligible = offers
|
||||
.Where(p => p.Id != frame.Id && available[p.Id] > 0
|
||||
&& p.Priority == frame.Priority
|
||||
&& geometry[p.Id] is { Cutouts.Count: 0 })
|
||||
&& (!bundles.TryGetValue(p.Id, out var b)
|
||||
|| (b.FrameId != frame.Id && b.Levels < MaxLevels)))
|
||||
.Select(p => new NestJobPart(p.Id, p.Geometry, available[p.Id],
|
||||
p.Priority, LocalRotation(p.Rotation, angle)))
|
||||
.ToArray();
|
||||
if (candidates.Length == 0)
|
||||
var flat = eligible.Where(p => geometry[p.Id] is { Cutouts.Count: 0 }).ToArray();
|
||||
var nested = CutoutCirclePacker.RoundHole(profile.Cutouts[hole]) == null
|
||||
? Array.Empty<NestJobPart>()
|
||||
: eligible.Where(p => round[p.Id] && geometry[p.Id]!.Cutouts.Count > 0).ToArray();
|
||||
if (flat.Length == 0 && nested.Length == 0)
|
||||
break;
|
||||
foreach (var pose in CutoutRouter.Fill(frame, hole, candidates, spacing, token))
|
||||
foreach (var pose in Propose(frame, hole, flat, nested))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var source = geometry[pose.PartId]!;
|
||||
var policy = original.Parts.Single(p => p.Id == pose.PartId).Rotation;
|
||||
if (!policy.Allows(angle + pose.Rotation)
|
||||
if (!requirements[pose.PartId].Rotation.Allows(angle + pose.Rotation)
|
||||
|| local.Any(other => !NestLayoutCheck.Clears(
|
||||
geometry[other.PartId]!, other, source, pose, spacing)))
|
||||
continue;
|
||||
@@ -89,7 +112,7 @@ internal sealed class CutoutPipelinePrepass
|
||||
available[pose.PartId]--;
|
||||
}
|
||||
}
|
||||
if (local.Count <= (chosen?.Count ?? 0))
|
||||
if (Weight(local) <= Weight(chosen ?? Array.Empty<NestJobPlacement>()))
|
||||
continue;
|
||||
chosen = local.ToArray();
|
||||
frameAngle = angle;
|
||||
@@ -97,14 +120,21 @@ internal sealed class CutoutPipelinePrepass
|
||||
if (chosen == null || chosen.Count == 0)
|
||||
break;
|
||||
var proxyId = $"__cutout-proxy-{frame.Id}-{instance}";
|
||||
if (remaining.ContainsKey(proxyId) || bundles.ContainsKey(proxyId))
|
||||
if (requirements.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)));
|
||||
var levels = 1 + chosen.Max(p => bundles.TryGetValue(p.PartId, out var b) ? b.Levels : 1);
|
||||
bundles.Add(proxyId, new Bundle(frame.Id, chosen, levels, 1 + Weight(chosen)));
|
||||
var proxy = new NestJobPart(proxyId, frame.Geometry, 1, frame.Priority,
|
||||
RotationPolicy.Fixed(frameAngle));
|
||||
proxies.Add(proxy);
|
||||
remaining[frame.Id]--;
|
||||
foreach (var pose in chosen)
|
||||
remaining[pose.PartId]--;
|
||||
offers.Add(proxy);
|
||||
requirements.Add(proxyId, proxy);
|
||||
geometry.Add(proxyId, profile);
|
||||
round.Add(proxyId, round[frame.Id]);
|
||||
remaining.Add(proxyId, 1);
|
||||
}
|
||||
}
|
||||
if (bundles.Count == 0)
|
||||
@@ -112,14 +142,36 @@ internal sealed class CutoutPipelinePrepass
|
||||
var transformed = new List<NestJobPart>();
|
||||
foreach (var part in original.Parts)
|
||||
{
|
||||
// A proxy another frame took as an insert is reserved inside that frame's bundle.
|
||||
if (byFrame.TryGetValue(part.Id, out var proxies))
|
||||
transformed.AddRange(proxies);
|
||||
transformed.AddRange(proxies.Where(p => remaining[p.Id] > 0));
|
||||
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);
|
||||
|
||||
// Ties keep the flat router's proposal, which predates nesting.
|
||||
IReadOnlyList<NestJobPlacement> Propose(NestJobPart frame, int hole,
|
||||
NestJobPart[] flat, NestJobPart[] nested)
|
||||
{
|
||||
IReadOnlyList<NestJobPlacement> poses = Array.Empty<NestJobPlacement>();
|
||||
if (nested.Length > 0)
|
||||
poses = CutoutCirclePacker.Fill(frame, hole,
|
||||
flat.Where(p => round[p.Id]).Concat(nested).ToArray(), spacing, token);
|
||||
// Seating every flat copy as a disc leaves the slower proposals nothing to beat.
|
||||
var flatIds = flat.Select(p => p.Id).ToHashSet(StringComparer.Ordinal);
|
||||
if (flat.Length == 0
|
||||
|| poses.Count(p => flatIds.Contains(p.PartId)) == flat.Sum(p => (long)p.Quantity))
|
||||
return poses;
|
||||
var router = CutoutRouter.Fill(frame, hole, flat, spacing, token);
|
||||
return Weight(router) >= Weight(poses) ? router : poses;
|
||||
}
|
||||
|
||||
// Physical parts a proposal places: a nested bundle counts its frame and contents.
|
||||
int Weight(IReadOnlyList<NestJobPlacement> poses) =>
|
||||
poses.Sum(p => bundles.TryGetValue(p.PartId, out var b) ? b.Parts : 1);
|
||||
}
|
||||
|
||||
private static RotationPolicy LocalRotation(RotationPolicy original, double frameAngle) =>
|
||||
@@ -139,11 +191,25 @@ internal sealed class CutoutPipelinePrepass
|
||||
var placed = engineResult.Plates.SelectMany(p => p.Placements)
|
||||
.Select(p => p.PartId).ToHashSet(StringComparer.Ordinal);
|
||||
var released = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var (proxyId, bundle) in bundles)
|
||||
if (!placed.Contains(proxyId))
|
||||
foreach (var insert in bundle.LocalInserts)
|
||||
released[insert.PartId] = released.GetValueOrDefault(insert.PartId) + 1;
|
||||
// Nested proxies are never offered to the engine; their outermost frame releases them.
|
||||
foreach (var part in EngineJob.Parts)
|
||||
if (bundles.TryGetValue(part.Id, out var bundle) && !placed.Contains(part.Id))
|
||||
Release(bundle);
|
||||
return released;
|
||||
|
||||
void Release(Bundle bundle)
|
||||
{
|
||||
foreach (var insert in bundle.LocalInserts)
|
||||
{
|
||||
if (bundles.TryGetValue(insert.PartId, out var nested))
|
||||
{
|
||||
released[nested.FrameId] = released.GetValueOrDefault(nested.FrameId) + 1;
|
||||
Release(nested);
|
||||
}
|
||||
else
|
||||
released[insert.PartId] = released.GetValueOrDefault(insert.PartId) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reject untrusted accounting before expansion. A failed trial is never bindable,
|
||||
@@ -222,16 +288,7 @@ internal sealed class CutoutPipelinePrepass
|
||||
poses.Add((pose.PartId, pose.X, pose.Y, pose.Rotation));
|
||||
continue;
|
||||
}
|
||||
// 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)
|
||||
poses.Add((local.PartId,
|
||||
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));
|
||||
Expand(bundle, pose.X, pose.Y, pose.Rotation, poses);
|
||||
}
|
||||
// Canonical stock reference, not an engine-supplied object with altered settings.
|
||||
var stock = original.Plates.Single(s => s.Id == sheet.StockId);
|
||||
@@ -248,4 +305,23 @@ internal sealed class CutoutPipelinePrepass
|
||||
expanded = builder.Build(result.StopReason);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inserts precede their frame at every level: cutting a frame's cutout releases the slug
|
||||
// each insert is cut from, so placed order (innermost first) is the safe cutting order.
|
||||
private void Expand(Bundle bundle, double x, double y, double rotation,
|
||||
List<(string PartId, double X, double Y, double Rotation)> poses)
|
||||
{
|
||||
var sine = System.Math.Sin(rotation);
|
||||
var cosine = System.Math.Cos(rotation);
|
||||
foreach (var local in bundle.LocalInserts)
|
||||
{
|
||||
var insertX = x + local.X * cosine - local.Y * sine;
|
||||
var insertY = y + local.X * sine + local.Y * cosine;
|
||||
if (bundles.TryGetValue(local.PartId, out var nested))
|
||||
Expand(nested, insertX, insertY, rotation + local.Rotation, poses);
|
||||
else
|
||||
poses.Add((local.PartId, insertX, insertY, rotation + local.Rotation));
|
||||
}
|
||||
poses.Add((bundle.FrameId, x, y, rotation));
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -210,11 +210,18 @@ proposals win ties.
|
||||
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
|
||||
requirements; inserts with a different priority stay independent. Frames are bundled smallest
|
||||
first, so a larger frame's round cutout can take, through `CutoutCirclePacker` only, a round part
|
||||
with its own cutouts (a washer with an empty bore) or an earlier bundle as one insert: a flange
|
||||
holds a washer that holds a smaller washer that holds a slug. Chains stop at four parts deep
|
||||
(`CutoutPipelinePrepass.MaxLevels`). Lattice and NFP proposals never take such inserts, since they
|
||||
could seat a part in a ring's bore or over a nested bundle's reserved inserts; non-round frames
|
||||
with cutouts stay independent. A frame's proposals are compared by physical parts placed, a nested
|
||||
bundle counting its frame and contents, and ties keep the flat router's proposal. 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;
|
||||
offered to the selected engine once. Only actually placed proxies expand, recursively, to original frame and
|
||||
insert identities. An unplaced proxy's reservations, including every nested frame and insert,
|
||||
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. Forged or geometrically invalid composites return no bindable
|
||||
@@ -239,7 +246,8 @@ offer) and returns:
|
||||
|
||||
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.
|
||||
at every level (innermost first), 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
|
||||
|
||||
Reference in new issue
Block a user