mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-30 16:31:07 -04:00
fix(nesting): let parts with bolt holes go into other parts' cutouts
The cutout pre-pass offered lattice/NFP inserts only when they had no cutouts at all, so a strap with 0.25" bolt holes was never tried in a frame's window. On N26-264 that left both PT11s loose on the sheet and Rectangles' staggered PT10s won, although both PT11s fit one PT10's end window. Holes matter only if another part could land in them, so a part whose every cutout is smaller in area than the job's smallest outline is now an ordinary insert; proxies and parts with usable holes stay out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8419761e0
commit
4179b53c97
3 files changed
+102
-5
No files matched your search
@@ -0,0 +1,89 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
using TestShapes = OpenNest.Engine.Tests.NestingEngines.Shapes;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs.Cutouts;
|
||||
|
||||
/// <summary>An insert with holes of its own goes into a frame's cutout when none of its holes
|
||||
/// fits any part of the job (bolt holes in a strap); otherwise it stays independent, since a
|
||||
/// later insert could land in its hole.</summary>
|
||||
[Collection("Cutout pipeline registry")]
|
||||
public class CutoutPipelineHoledInsertTests
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// A 20 x 20 frame with a 12 x 14 window.
|
||||
private static Program Frame() => WithSquareHoles(TestShapes.Rectangle(20, 20), (4, 3, 12, 14));
|
||||
|
||||
// A 13 x 1.25 strap with two 0.25 bolt holes: it fits the window only upright.
|
||||
private static Program Strap()
|
||||
{
|
||||
var program = TestShapes.Rectangle(13, 1.25);
|
||||
foreach (var x in new[] { 1.0, 12.0 })
|
||||
{
|
||||
program.Codes.Add(new RapidMove(x + 0.125, 0.625));
|
||||
program.Codes.Add(new ArcMove(x - 0.125, 0.625, x, 0.625, RotationType.CCW));
|
||||
program.Codes.Add(new ArcMove(x + 0.125, 0.625, x, 0.625, RotationType.CCW));
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
private static Program WithSquareHoles(Program program, params (double X, double Y, double W, double H)[] holes)
|
||||
{
|
||||
foreach (var (x, y, w, h) in holes)
|
||||
{
|
||||
program.Codes.Add(new RapidMove(x, y));
|
||||
program.Codes.Add(new LinearMove(x + w, y));
|
||||
program.Codes.Add(new LinearMove(x + w, y + h));
|
||||
program.Codes.Add(new LinearMove(x, y + h));
|
||||
program.Codes.Add(new LinearMove(x, y));
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
private static NestPipelineRequest Request(string engine, params Program[] programs) =>
|
||||
new(engine,
|
||||
programs.Select((p, i) => new NestItem { Drawing = new Drawing($"d{i}", p), Quantity = 1 }).ToArray(),
|
||||
// Only the frame fits beside nothing else: the strap must go in its window.
|
||||
new[] { new NestPlateStock("sheet", new Size(20.5, 20.5), 1, 0.25) });
|
||||
|
||||
[Theory]
|
||||
[InlineData("Irregular")]
|
||||
[InlineData("Default")]
|
||||
public void StrapWithBoltHolesGoesInTheFrameWindow(string engine)
|
||||
{
|
||||
var request = Request(engine, Frame(), Strap());
|
||||
Assert.Equal(NestJobStatus.Incomplete, NestPipeline.RunWithoutCutouts(request).Status);
|
||||
|
||||
var result = NestPipeline.Run(request);
|
||||
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(new[] { request.Items[1].Drawing, request.Items[0].Drawing },
|
||||
Assert.Single(result.Plates).Parts.Select(p => p.BaseDrawing));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertWhoseHoleFitsAnotherPartStaysIndependent()
|
||||
{
|
||||
// The 10 x 10 part's 4 x 4 hole fits the 2 x 2 part, so it is never reserved in the
|
||||
// frame's window; the 2 x 2 part goes into that hole, the smaller frame, instead.
|
||||
var holed = WithSquareHoles(TestShapes.Rectangle(10, 10), (3, 3, 4, 4));
|
||||
var engine = new Stub(job => new NestJobResultBuilder(job).Build(NestJobStopReason.NoPlacementFound));
|
||||
NestPipeline.RunCutoutTrial(engine, "stub", Request("stub", Frame(), holed, TestShapes.Rectangle(2, 2)));
|
||||
|
||||
Assert.Equal(new[] { "part-0", "__cutout-proxy-part-1-0" },
|
||||
Assert.Single(engine.Jobs).Parts.Select(p => p.Id));
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,12 @@ internal sealed class CutoutPipelinePrepass
|
||||
// An insert's outline cannot fit a hole of smaller area; bolt holes skip the searches.
|
||||
var outline = original.Parts.ToDictionary(p => p.Id, p => geometry[p.Id] is { } g
|
||||
? System.Math.Abs(g.Perimeter.Area()) : double.PositiveInfinity);
|
||||
// Lattice and NFP placement read a part's holes as free space, so they take as inserts
|
||||
// only parts whose holes no part of the job fits by area: no later insert can land in
|
||||
// one. Proxies are never solid; their frames' holes hold reserved inserts.
|
||||
var smallestOutline = outline.Values.DefaultIfEmpty(double.PositiveInfinity).Min();
|
||||
var solid = original.Parts.ToDictionary(p => p.Id, p => geometry[p.Id] is { } g
|
||||
&& g.Cutouts.All(c => System.Math.Abs(c.Area()) < smallestOutline));
|
||||
var bundles = new Dictionary<string, Bundle>(StringComparer.Ordinal);
|
||||
var byFrame = new Dictionary<string, List<NestJobPart>>(StringComparer.Ordinal);
|
||||
// A proposal depends on the frame, the hole and the offered inserts with their local
|
||||
@@ -104,10 +110,10 @@ internal sealed class CutoutPipelinePrepass
|
||||
.Select(p => new NestJobPart(p.Id, p.Geometry, available[p.Id],
|
||||
p.Priority, LocalRotation(p.Rotation, angle)))
|
||||
.ToArray();
|
||||
var flat = eligible.Where(p => geometry[p.Id] is { Cutouts.Count: 0 }).ToArray();
|
||||
var flat = eligible.Where(p => solid.GetValueOrDefault(p.Id)).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();
|
||||
: eligible.Where(p => round[p.Id] && !solid.GetValueOrDefault(p.Id)).ToArray();
|
||||
if (flat.Length == 0 && nested.Length == 0)
|
||||
continue;
|
||||
foreach (var pose in Propose(frame, hole, flat, nested))
|
||||
|
||||
@@ -304,13 +304,15 @@ closed-cutout frames. It considers legal frame rotations without outer-perimeter
|
||||
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 stay independent, and a hole smaller in area than
|
||||
an insert's outline (a bolt hole) is not searched for it. Frames are bundled smallest
|
||||
an insert's outline (a bolt hole) is not searched for it. A part whose every cutout is smaller in
|
||||
area than the job's smallest part outline (a strap with bolt holes) is an ordinary insert: no part
|
||||
can land in its holes. 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
|
||||
with a cutout some part fits (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
|
||||
with such 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, recursively, to original frame and
|
||||
|
||||
Reference in new issue
Block a user