mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 07:34:14 -04:00
feat(cutouts): propose bounded mixed lattice and NFP inserts
This commit is contained in:
1 parent
d3e325f36c
commit
962ae8c825
5 files changed
+327
-4
No files matched your search
@@ -0,0 +1,156 @@
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Cutouts;
|
||||
using OpenNest.Engine.Tests.NestingEngines;
|
||||
using TestShapes = OpenNest.Engine.Tests.NestingEngines.Shapes;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs.Cutouts;
|
||||
|
||||
public class CutoutRouterTests
|
||||
{
|
||||
private static NestJobPart Frame()
|
||||
{
|
||||
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);
|
||||
return JobBuilder.Part("frame", program, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmallLatticeAndLargeResidualKeepTheirOriginalRequirementIds()
|
||||
{
|
||||
var frame = JobBuilder.Part("frame", TestShapes.Ring(20, 10), 1);
|
||||
var small = JobBuilder.Part("small", TestShapes.Rectangle(1, 1), 3);
|
||||
var large = JobBuilder.Rectangle("large", 3, 3, 1, RotationPolicy.Fixed(0));
|
||||
Assert.Equal(3, CutoutLatticeFill.Fill(frame, 0, small, 3, 0.25).Count);
|
||||
var poses = CutoutRouter.Fill(frame, 0, new[] { small, large }, 0.25);
|
||||
Assert.Equal(3, poses.Count(p => p.PartId == small.Id));
|
||||
Assert.Single(poses, p => p.PartId == large.Id);
|
||||
Assert.Equal(new[] { 0, 1, 2 }, poses.Where(p => p.PartId == small.Id).Select(p => p.InstanceIndex));
|
||||
AssertPhysical(frame, new[] { small, large }, poses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImpossibleRemainderNeverDisplacesCertifiedCopies()
|
||||
{
|
||||
var frame = Frame();
|
||||
var small = JobBuilder.Rectangle("small", 1, 1, 3, RotationPolicy.Fixed(0));
|
||||
var impossible = JobBuilder.Rectangle("impossible", 9.6, 9.6, 1, RotationPolicy.Fixed(0));
|
||||
var alone = CutoutRouter.Fill(frame, 0, new[] { small }, 0.25);
|
||||
var together = CutoutRouter.Fill(frame, 0, new[] { small, impossible }, 0.25);
|
||||
Assert.Equal(alone, together);
|
||||
Assert.Equal(3, together.Count);
|
||||
AssertPhysical(frame, new[] { small, impossible }, together);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PriorCopiesAndQuantityCannotBeBypassedByResidualSearch()
|
||||
{
|
||||
var frame = Frame();
|
||||
var small = JobBuilder.Rectangle("small", 2, 2, 4, RotationPolicy.Fixed(0));
|
||||
var large = JobBuilder.Rectangle("large", 7, 7, 1, RotationPolicy.Fixed(0));
|
||||
var poses = CutoutRouter.Fill(frame, 0, new[] { small, large }, 0.25);
|
||||
Assert.InRange(poses.Count(p => p.PartId == small.Id), 0, small.Quantity);
|
||||
Assert.InRange(poses.Count(p => p.PartId == large.Id), 0, large.Quantity);
|
||||
AssertPhysical(frame, new[] { small, large }, poses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResidualNfpDoesNotOverlapTheFirstLargeInsert()
|
||||
{
|
||||
var frame = Frame();
|
||||
var insert = JobBuilder.Rectangle("large", 6, 6, 2, RotationPolicy.Fixed(0));
|
||||
var poses = CutoutRouter.Fill(frame, 0, new[] { insert }, 0.25);
|
||||
Assert.Single(poses);
|
||||
AssertPhysical(frame, new[] { insert }, poses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DuplicateRequirementIdsAreRejectedBeforePlacement()
|
||||
{
|
||||
var frame = Frame();
|
||||
var insert = JobBuilder.Rectangle("same", 1, 1, 3);
|
||||
Assert.Throws<ArgumentException>(() => CutoutRouter.Fill(frame, 0,
|
||||
new[] { insert, insert }, 0.25));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OccupiedAwareLatticeFillsAfterAnEarlierCopyBlocksItsFirstPose()
|
||||
{
|
||||
var frame = JobBuilder.Part("frame", TestShapes.Ring(20, 10), 1);
|
||||
var first = JobBuilder.Part("first", TestShapes.Rectangle(1, 1), 1);
|
||||
var more = JobBuilder.Part("more", TestShapes.Rectangle(1, 1), 3);
|
||||
var early = CutoutLatticeFill.Fill(frame, 0, more, 3, 0.25);
|
||||
Assert.Equal(3, early.Count);
|
||||
var blocked = early[0] with { PartId = first.Id };
|
||||
var occupied = new[] { (JobPartGeometry.Read(first.Geometry), blocked) };
|
||||
var later = CutoutLatticeFill.Fill(frame, 0, more, 3, 0.25,
|
||||
CutoutLatticeFill.DefaultShiftSteps, occupied, default);
|
||||
Assert.Equal(3, later.Count);
|
||||
Assert.All(later, p => Assert.True(NestLayoutCheck.Clears(occupied[0].Item1, blocked,
|
||||
JobPartGeometry.Read(more.Geometry), p, 0.25)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TinyInsertDoesNotAllocateAnUnboundedLattice()
|
||||
{
|
||||
var frame = Frame();
|
||||
var tiny = JobBuilder.Rectangle("tiny", 0.001, 0.001, 3, RotationPolicy.Fixed(0));
|
||||
Assert.Empty(CutoutLatticeFill.Fill(frame, 0, tiny, 3, 0));
|
||||
var poses = CutoutRouter.Fill(frame, 0, new[] { tiny }, 0);
|
||||
Assert.Equal(3, poses.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancellationIsNotNoFit()
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
Assert.Throws<OperationCanceledException>(() => CutoutRouter.Fill(Frame(), 0,
|
||||
new[] { JobBuilder.Rectangle("small", 1, 1, 3) }, 0.25, cts.Token));
|
||||
}
|
||||
|
||||
private static void AssertPhysical(NestJobPart frame, NestJobPart[] inserts,
|
||||
IReadOnlyList<NestJobPlacement> poses)
|
||||
{
|
||||
var stock = JobBuilder.Stock("sheet", 22, 22, 0.25);
|
||||
var job = JobBuilder.Job(new[] { frame }.Concat(inserts).ToArray(), new[] { stock });
|
||||
var frameBounds = JobPartGeometry.Read(frame.Geometry).Bounds;
|
||||
var offsetX = 1 - frameBounds.Left;
|
||||
var offsetY = 1 - frameBounds.Bottom;
|
||||
var placements = new[] { new NestJobPlacement(frame.Id, 0, offsetX, offsetY, 0) }
|
||||
.Concat(poses.Select(p => p with { X = p.X + offsetX, Y = p.Y + offsetY })).ToArray();
|
||||
var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
|
||||
new[] { new NestJobPlateResult(0, stock, placements) },
|
||||
new[] { new PartFulfillment(frame.Id, 1, 1, 0) }
|
||||
.Concat(inserts.Select(part => new PartFulfillment(part.Id, part.Quantity,
|
||||
poses.Count(p => p.PartId == part.Id), part.Quantity - poses.Count(p => p.PartId == part.Id))))
|
||||
.ToArray(),
|
||||
new[] { new StockUsage(stock.Id, 1, null) });
|
||||
LayoutAssert.Valid(job, result);
|
||||
var frameGeometry = JobPartGeometry.Read(frame.Geometry);
|
||||
var framePose = new NestJobPlacement(frame.Id, 0, 0, 0, 0);
|
||||
foreach (var pose in poses)
|
||||
{
|
||||
var geometry = JobPartGeometry.Read(inserts.Single(p => p.Id == pose.PartId).Geometry);
|
||||
Assert.True(NestLayoutCheck.Clears(frameGeometry, framePose, geometry, pose, 0.25));
|
||||
// Independent square-wall oracle: every corner of each rectangular insert clears the hole.
|
||||
var bounds = geometry.Bounds;
|
||||
foreach (var (x, y) in new[] { (bounds.Left, bounds.Bottom), (bounds.Right, bounds.Bottom),
|
||||
(bounds.Right, bounds.Top), (bounds.Left, bounds.Top) })
|
||||
{
|
||||
var rx = pose.X + x * System.Math.Cos(pose.Rotation) - y * System.Math.Sin(pose.Rotation);
|
||||
var ry = pose.Y + x * System.Math.Sin(pose.Rotation) + y * System.Math.Cos(pose.Rotation);
|
||||
if (frameBounds.Left >= 0)
|
||||
{
|
||||
Assert.InRange(rx, 5.25, 14.75);
|
||||
Assert.InRange(ry, 5.25, 14.75);
|
||||
}
|
||||
else
|
||||
Assert.True(System.Math.Sqrt(rx * rx + ry * ry) <= 4.75 + 0.00001);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,9 @@ internal static class CutoutLatticeFill
|
||||
private const double Margin = FlattenTolerance + 0.001;
|
||||
|
||||
private const int Precision = NestTolerances.ClipperPrecision;
|
||||
// Fill materializes an entire grid before quantity trimming. Bound work even for a
|
||||
// tiny insert with demand of only three, rather than allocating millions of clones.
|
||||
private const int MaxLatticePositions = 1000;
|
||||
|
||||
/// <summary>Returns up to <paramref name="maxQuantity"/> insert poses inside the cutout,
|
||||
/// or none when no copy fits.</summary>
|
||||
@@ -45,9 +48,17 @@ internal static class CutoutLatticeFill
|
||||
|
||||
internal static IReadOnlyList<NestJobPlacement> Fill(NestJobPart frame, int cutoutIndex,
|
||||
NestJobPart insert, int maxQuantity, double spacing, int shiftSteps, CancellationToken token)
|
||||
=> Fill(frame, cutoutIndex, insert, maxQuantity, spacing, shiftSteps,
|
||||
Array.Empty<(JobPartGeometry Geometry, NestJobPlacement Pose)>(), token);
|
||||
|
||||
internal static IReadOnlyList<NestJobPlacement> Fill(NestJobPart frame, int cutoutIndex,
|
||||
NestJobPart insert, int maxQuantity, double spacing, int shiftSteps,
|
||||
IReadOnlyList<(JobPartGeometry Geometry, NestJobPlacement Pose)> occupied, CancellationToken token)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ArgumentNullException.ThrowIfNull(insert);
|
||||
ArgumentNullException.ThrowIfNull(occupied);
|
||||
token.ThrowIfCancellationRequested();
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(shiftSteps);
|
||||
if (maxQuantity <= 0 || !double.IsFinite(spacing) || spacing < 0)
|
||||
return Array.Empty<NestJobPlacement>();
|
||||
@@ -69,6 +80,10 @@ internal static class CutoutLatticeFill
|
||||
var bounds = cutout.BoundingBox;
|
||||
var insertBounds = insertGeometry.Bounds;
|
||||
var step = System.Math.Max(insertBounds.Length, insertBounds.Width) + spacing;
|
||||
var estimated = (System.Math.Ceiling(bounds.Length / step) + 4)
|
||||
* (System.Math.Ceiling(bounds.Width / step) + 4);
|
||||
if (!(step > 0) || !double.IsFinite(estimated) || estimated > MaxLatticePositions)
|
||||
return Array.Empty<NestJobPlacement>();
|
||||
|
||||
var lattice = Lattice(insert, bounds, step, spacing, token);
|
||||
if (lattice.Count == 0)
|
||||
@@ -83,7 +98,8 @@ internal static class CutoutLatticeFill
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var (dx, dy) = Shift(step, shiftSteps, i, j);
|
||||
var count = lattice.Count(p => Inside(regions[p.Rotation], p.X + dx, p.Y + dy));
|
||||
var count = lattice.Count(p => Inside(regions[p.Rotation], p.X + dx, p.Y + dy)
|
||||
&& ClearsOccupied(p, dx, dy));
|
||||
if (Better(count, i, j, best))
|
||||
best = (count, i, j);
|
||||
}
|
||||
@@ -91,13 +107,33 @@ internal static class CutoutLatticeFill
|
||||
return Array.Empty<NestJobPlacement>();
|
||||
|
||||
var (sx, sy) = Shift(step, shiftSteps, best.I, best.J);
|
||||
var kept = lattice.Where(p => Inside(regions[p.Rotation], p.X + sx, p.Y + sy))
|
||||
var kept = lattice.Where(p => Inside(regions[p.Rotation], p.X + sx, p.Y + sy)
|
||||
&& ClearsOccupied(p, sx, sy))
|
||||
.Select(p => new NestJobPlacement(insert.Id, 0, System.Math.Round(p.X + sx, 8),
|
||||
System.Math.Round(p.Y + sy, 8), p.Rotation))
|
||||
.OrderBy(p => p.Y).ThenBy(p => p.X).ThenBy(p => p.Rotation)
|
||||
.ToList();
|
||||
return Certify(frame.Id, frameGeometry, insertGeometry, kept, spacing, token).Take(maxQuantity)
|
||||
.Select((p, index) => p with { InstanceIndex = index }).ToArray();
|
||||
|
||||
bool ClearsOccupied(NestJobPlacement pose, double dx, double dy)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (occupied.Count == 0)
|
||||
return true;
|
||||
var moved = pose with
|
||||
{
|
||||
X = System.Math.Round(pose.X + dx, 8),
|
||||
Y = System.Math.Round(pose.Y + dy, 8)
|
||||
};
|
||||
foreach (var other in occupied)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (!NestLayoutCheck.Clears(other.Geometry, other.Pose, insertGeometry, moved, spacing))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fill over the cutout's bounds grown by one step on every side, in frame
|
||||
|
||||
@@ -20,10 +20,18 @@ internal static class CutoutNfpProposal
|
||||
/// <summary>Returns one frame-local pose inside a closed cutout, or null if no pose is certified.
|
||||
/// A null result is not a proof that the insert cannot fit elsewhere in this cutout.</summary>
|
||||
internal static NestJobPlacement? Find(NestJobPart frame, int cutoutIndex, NestJobPart insert,
|
||||
double spacing, CancellationToken token = default)
|
||||
double spacing, CancellationToken token = default) =>
|
||||
Find(frame, cutoutIndex, insert, spacing,
|
||||
Array.Empty<(JobPartGeometry Geometry, NestJobPlacement Pose)>(), token);
|
||||
|
||||
/// <summary>Find a pose clearing already accepted inserts as well as the enclosing frame.</summary>
|
||||
internal static NestJobPlacement? Find(NestJobPart frame, int cutoutIndex, NestJobPart insert,
|
||||
double spacing, IReadOnlyList<(JobPartGeometry Geometry, NestJobPlacement Pose)> occupied,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ArgumentNullException.ThrowIfNull(insert);
|
||||
ArgumentNullException.ThrowIfNull(occupied);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (!double.IsFinite(spacing) || spacing < 0 || spacing > MaxSpacing)
|
||||
return null;
|
||||
@@ -55,7 +63,19 @@ internal static class CutoutNfpProposal
|
||||
if (!CutoutLatticeFill.Inside(region, x, y))
|
||||
continue;
|
||||
var pose = new NestJobPlacement(insert.Id, 0, x, y, rotation);
|
||||
if (NestLayoutCheck.Clears(frameGeometry, framePose, insertGeometry, pose, spacing))
|
||||
if (!NestLayoutCheck.Clears(frameGeometry, framePose, insertGeometry, pose, spacing))
|
||||
continue;
|
||||
var clears = true;
|
||||
foreach (var other in occupied)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (NestLayoutCheck.Clears(other.Geometry, other.Pose, insertGeometry, pose, spacing))
|
||||
continue;
|
||||
clears = false;
|
||||
break;
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (clears)
|
||||
return pose;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +101,12 @@ internal static class CutoutNfpProposal
|
||||
yield return (cx, cy);
|
||||
foreach (var point in path)
|
||||
yield return (point.x * 0.99 + cx * 0.01, point.y * 0.99 + cy * 0.01);
|
||||
// Bounded interior samples let a residual insert land beside a partial lattice;
|
||||
// a center/vertex-only sample often lands directly on an earlier copy.
|
||||
for (var y = 1; y <= 9; y++)
|
||||
for (var x = 1; x <= 9; x++)
|
||||
yield return (bounds.left + (bounds.right - bounds.left) * x / 10,
|
||||
bounds.top + (bounds.bottom - bounds.top) * y / 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest.Engine.Jobs.Cutouts;
|
||||
|
||||
/// <summary>Internal frame-local cutout proposal. Never changes demand, stock or a live plate.</summary>
|
||||
/// <remarks>Tries shifted partial Fill for a type with at least three requested copies,
|
||||
/// prefers it only when it beats a single NFP pose, then fills residual demand
|
||||
/// with NFP poses. No tuned ratio cutoff is claimed: the
|
||||
/// historical 0.10/0.20 routing thresholds still require measured acceptance.
|
||||
/// Fill's equal-score pose nondeterminism is tracked as PM c98c21bd.</remarks>
|
||||
internal static class CutoutRouter
|
||||
{
|
||||
internal static IReadOnlyList<NestJobPlacement> Fill(NestJobPart frame, int cutoutIndex,
|
||||
IReadOnlyList<NestJobPart> inserts, double spacing, CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ArgumentNullException.ThrowIfNull(inserts);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (!double.IsFinite(spacing) || spacing < 0 || spacing > 100)
|
||||
return Array.Empty<NestJobPlacement>();
|
||||
var frameGeometry = JobPartGeometry.TryRead(frame.Geometry);
|
||||
if (frameGeometry == null)
|
||||
return Array.Empty<NestJobPlacement>();
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(cutoutIndex);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(cutoutIndex, frameGeometry.Cutouts.Count);
|
||||
var hole = frameGeometry.Cutouts[cutoutIndex];
|
||||
if (!hole.IsClosed() || !Supported(frameGeometry.Bounds)
|
||||
|| !Supported(hole.BoundingBox))
|
||||
return Array.Empty<NestJobPlacement>();
|
||||
if (inserts.Any(p => p == null || !SupportedGeometry(p)))
|
||||
return Array.Empty<NestJobPlacement>();
|
||||
if (inserts.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != inserts.Count
|
||||
|| inserts.Any(p => p.Id == frame.Id))
|
||||
throw new ArgumentException("Requirement IDs must be distinct from the frame and each other.",
|
||||
nameof(inserts));
|
||||
|
||||
var accepted = new List<NestJobPlacement>();
|
||||
var occupied = new List<(JobPartGeometry Geometry, NestJobPlacement Pose)>();
|
||||
// Preserve caller requirement order: priorities are a whole-job concern, not inferred here.
|
||||
foreach (var part in inserts)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var geometry = JobPartGeometry.Read(part.Geometry);
|
||||
if (part.Quantity >= 3)
|
||||
{
|
||||
// Geometric work is bounded inside Fill. Do not infer a ratio threshold
|
||||
// from material area before measuring cutout jobs; try both proposals.
|
||||
var usable = CutoutLatticeFill.Fill(frame, cutoutIndex, part, part.Quantity,
|
||||
spacing, CutoutLatticeFill.DefaultShiftSteps, occupied, token);
|
||||
// A one-copy lattice adds no value over the geometry-aware NFP search;
|
||||
// retain the latter's stable candidate order for that tie.
|
||||
if (usable.Count > 1 || (usable.Count == 1
|
||||
&& CutoutNfpProposal.Find(frame, cutoutIndex, part, spacing, occupied, token) == null))
|
||||
foreach (var pose in usable)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
Accept(pose);
|
||||
}
|
||||
}
|
||||
while (accepted.Count(p => p.PartId == part.Id) < part.Quantity)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var pose = CutoutNfpProposal.Find(frame, cutoutIndex, part, spacing, occupied, token);
|
||||
if (pose == null)
|
||||
break;
|
||||
Accept(pose with { InstanceIndex = accepted.Count(p => p.PartId == part.Id) });
|
||||
}
|
||||
|
||||
void Accept(NestJobPlacement pose)
|
||||
{
|
||||
var indexed = pose with { InstanceIndex = accepted.Count(p => p.PartId == part.Id) };
|
||||
accepted.Add(indexed);
|
||||
occupied.Add((geometry, indexed));
|
||||
}
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
return accepted.ToArray();
|
||||
}
|
||||
|
||||
private static bool SupportedGeometry(NestJobPart part)
|
||||
{
|
||||
var geometry = JobPartGeometry.TryRead(part.Geometry);
|
||||
return geometry != null && Supported(geometry.Bounds);
|
||||
}
|
||||
|
||||
private static bool Supported(OpenNest.Geometry.Box box) =>
|
||||
double.IsFinite(box.Left) && double.IsFinite(box.Right)
|
||||
&& double.IsFinite(box.Bottom) && double.IsFinite(box.Top)
|
||||
&& System.Math.Abs(box.Left) <= 1e8 && System.Math.Abs(box.Right) <= 1e8
|
||||
&& System.Math.Abs(box.Bottom) <= 1e8 && System.Math.Abs(box.Top) <= 1e8;
|
||||
}
|
||||
@@ -83,6 +83,16 @@ layout check uses. The method suits many small copies in a large cutout; a few l
|
||||
meant for no-fit-polygon placement. Fill can return different, equally scored lattices on repeated
|
||||
calls for some parts, so results are not yet guaranteed identical between runs.
|
||||
|
||||
The internal `CutoutRouter` can propose copies from the shifted lattice, then search bounded
|
||||
inner-fit/NFP sample points for remaining copies and other insert requirements. It keeps
|
||||
original requirement IDs, reindexes accepted copies and checks clearance against the frame
|
||||
and every previously accepted insert. Lattice shifts account for occupied poses before
|
||||
quantity trimming; a pre-fill work limit declines giant grids and lets bounded NFP sampling
|
||||
try instead. A null NFP proposal is not proof of geometric impossibility. No material-area
|
||||
ratio cutoff is enabled: the proposed 0.10/0.20 routing thresholds still need measured
|
||||
neutral and sanitized-job evidence. The router has no pipeline caller and does not change
|
||||
stock, live demand, cutting order or posting safety; in-hole production use remains held.
|
||||
|
||||
## Renamed engines
|
||||
|
||||
The registry maps names used by earlier releases (the first two shipped as plug-ins) so saved
|
||||
|
||||
Reference in new issue
Block a user