mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 03:51:05 -04:00
fix(engine): route restricted-rotation Strip plates to the ordered nester
StripPlateNester sent every request to the strip fills, which try two classification angles and ignore rotation bounds. With a fixed or bounded rotation at quantity above one they proposed forbidden poses, the job validator threw "Candidate rotation is not allowed for the requirement." and the whole Strip job failed. Strip now uses the same guard as DefaultPlateNester and hands such plates to OrderedPlateNester. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ccd361dfa9
commit
36c8de825e
4 files changed
+95
-1
No files matched your search
@@ -1,4 +1,6 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Tests.NestingEngines;
|
||||
using OpenNest.Geometry;
|
||||
using Xunit;
|
||||
|
||||
@@ -55,4 +57,64 @@ public class FixedStrategyNestingEngineTests
|
||||
Assert.Throws<ArgumentException>(() => new FixedStrategyNestingEngine(null));
|
||||
Assert.Throws<ArgumentException>(() => new FixedStrategyNestingEngine(" "));
|
||||
}
|
||||
|
||||
private static Program LShape()
|
||||
{
|
||||
var program = new Program();
|
||||
program.MoveTo(0, 0);
|
||||
program.LineTo(6, 0);
|
||||
program.LineTo(6, 4);
|
||||
program.LineTo(3, 4);
|
||||
program.LineTo(3, 2);
|
||||
program.LineTo(0, 2);
|
||||
program.LineTo(0, 0);
|
||||
return program;
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, double, int> LockedRotationCases =>
|
||||
new()
|
||||
{
|
||||
{ "Fill", "rect-6x4", 0, 20 },
|
||||
{ "Strip", "rect-6x4", 0, 20 },
|
||||
{ "Strip", "rect-20x3", 0, 10 },
|
||||
{ "Strip", "rect-3x20", 0, 10 },
|
||||
{ "Strip", "rect-6x4", System.Math.PI / 2, 20 },
|
||||
{ "Strip", "rect-6x4", 0.3, 10 },
|
||||
{ "Strip", "lshape", 0.3, 10 },
|
||||
{ "Vertical Remnant", "lshape", 0.3, 10 },
|
||||
{ "Horizontal Remnant", "lshape", 0.3, 10 },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(LockedRotationCases))]
|
||||
public void LockedRotation_CompletesAtTheLockedAngle(
|
||||
string strategy,
|
||||
string shape,
|
||||
double angle,
|
||||
int quantity
|
||||
)
|
||||
{
|
||||
var program = shape switch
|
||||
{
|
||||
"rect-6x4" => TestDrawingFactory.Rectangle(6, 4),
|
||||
"rect-20x3" => TestDrawingFactory.Rectangle(20, 3),
|
||||
"rect-3x20" => TestDrawingFactory.Rectangle(3, 20),
|
||||
_ => LShape(),
|
||||
};
|
||||
var part = new NestJobPart(
|
||||
shape,
|
||||
PartGeometrySnapshot.FromProgram(program),
|
||||
quantity,
|
||||
rotation: RotationPolicy.Fixed(angle)
|
||||
);
|
||||
var stock = new NestPlateStock("stock", new Size(30, 50), 3, 1, new Spacing(1, 1, 1, 1));
|
||||
var job = new NestJob(new[] { part }, new[] { stock }, new NestJobOptions(strategy));
|
||||
|
||||
var result = new FixedStrategyNestingEngine(strategy).Solve(job);
|
||||
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(quantity, result.Plates.Sum(p => p.Placements.Count));
|
||||
// Checks NestLayoutCheck and that every pose is an angle the policy allows.
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
}
|
||||
@@ -255,6 +255,30 @@ public class PlateNesterParityTests
|
||||
Assert.All(placements, p => Assert.True(AnglesEqual(p.Rotation, 0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripRestrictedRotation_NeverTouchesFiller()
|
||||
{
|
||||
// Same safety rule for Strip, whose shrink fills use two classification angles and
|
||||
// ignore the requirement's rotation bounds. The throwing factory proves the filler is
|
||||
// never constructed; completion at the locked angle proves OrderedPlateNester handled it.
|
||||
var part = new NestJobPart(
|
||||
"fixed",
|
||||
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)),
|
||||
2,
|
||||
rotation: RotationPolicy.Fixed(0)
|
||||
);
|
||||
var nester = new StripPlateNester(_ =>
|
||||
throw new InvalidOperationException("filler must not run for restricted rotation")
|
||||
);
|
||||
|
||||
var result = Solve(nester, Job(new[] { part }, strategy: "Strip"));
|
||||
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
var placements = result.Plates.SelectMany(p => p.Placements).ToList();
|
||||
Assert.Equal(2, placements.Count);
|
||||
Assert.All(placements, p => Assert.True(AnglesEqual(p.Rotation, 0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepeatedNames_KeepIndependentIdentity()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
@@ -20,6 +21,7 @@ namespace OpenNest.Engine.Jobs.Placement;
|
||||
public sealed class StripPlateNester : IPlateNester
|
||||
{
|
||||
private readonly Func<Plate, StripPlateFiller> fillerFactory;
|
||||
private readonly OrderedPlateNester restrictedRotationNester = new();
|
||||
private readonly CandidatePlacementContext context = new();
|
||||
|
||||
public StripPlateNester()
|
||||
@@ -41,6 +43,12 @@ public sealed class StripPlateNester : IPlateNester
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// The strip fills try two classification angles and ignore the requirement's rotation
|
||||
// bounds, so they can return poses its RotationPolicy forbids. As in
|
||||
// DefaultPlateNester, restricted requirements go to the policy-aware ordered nester.
|
||||
if (request.Parts.Any(part => part.Rotation.Kind != RotationPolicyKind.Automatic))
|
||||
return restrictedRotationNester.Place(request, progress, token);
|
||||
|
||||
var plate = DrawingJobMapper.CreatePlate(request.Stock);
|
||||
var items = context.CreateItems(request.Parts);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ in `OpenNest.Engine/NestingEngines/<Name>/`, its tests in `OpenNest.Engine.Tests
|
||||
| Circles | Round parts: blanks, washers, flanges, gears by their tip circle | Each part packed as its minimum enclosing circle at centers tangent to two of walls, placed discs and clamp keep-outs, with hexagonal and square lattice seeds; bids only when every part is round; stock chosen as Rectangles does, plus whole-job runs preferring each stock and a tail re-plan |
|
||||
| Irregular | Irregular profiles | No-fit-polygon frontier packing with gap filling and best-fit pairs, six whole-job strategy variants and a tail re-plan |
|
||||
| FlatBar | Flat bar: parts about as wide as the bar | Each stock read as a bar and cut in one row: the CutList cut-list packer and a best-fill search on part lengths less their self-nesting gain, then a slide-left pass that nests shaped ends and refills freed length; stock chosen bar by bar by whole-plan cost |
|
||||
| Fill, Strip, Vertical Remnant, Horizontal Remnant | Single-strategy fills | The fixed placement strategies behind interactive fill; Fill is the multi-phase lattice fill (linear, pairs, rectangle best-fit, remainder) |
|
||||
| Fill, Strip, Vertical Remnant, Horizontal Remnant | Single-strategy fills | The fixed placement strategies behind interactive fill; Fill is the multi-phase lattice fill (linear, pairs, rectangle best-fit, remainder). In whole jobs, all four place a plate with any fixed or bounded part rotation by the rotation-aware ordered placer instead |
|
||||
|
||||
Neither Irregular nor Rectangles wins every job, even within its own lane, so Default runs both
|
||||
rather than choosing by part shape; Rectangles adds little time and is also the fallback when an
|
||||
|
||||
Reference in new issue
Block a user