mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-14 18:21:55 -04:00
fix(irregular): retain sub-grid clamp obstacles
This commit is contained in:
1 parent
c374e02614
commit
fbca54637b
3 files changed
+123
-6
No files matched your search
@@ -193,6 +193,67 @@ public class IrregularClampZoneTests
|
||||
Assert.Throws<OperationCanceledException>(() => packer.Fill([1], new CancellationToken(true)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoteSubGridClampDoesNotCrashWholeSolve()
|
||||
{
|
||||
// Exact reported reproducer: Stock takes Y width, then X length.
|
||||
var stock = Stock("sheet", 10, 12,
|
||||
clampZones: [new("remote-small-clamp", new Box(8, 0, 1e-5, 1e-5))]);
|
||||
var job = Job([Rectangle("part", 2, 2, 1)], [stock]);
|
||||
NestJobValidator.Validate(job);
|
||||
|
||||
var result = new IrregularNestingEngine().Solve(job);
|
||||
|
||||
AssertCompleteRectangleClearsClamps(result, 0, 0);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> SubGridClampCases()
|
||||
{
|
||||
foreach (var quadrant in Enumerable.Range(1, 4))
|
||||
foreach (var fixture in Enumerable.Range(0, 6))
|
||||
yield return [quadrant, fixture];
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SubGridClampCases))]
|
||||
public void SubGridClampsBlockTheOriginalRectanglePlacement(int quadrant, int fixture)
|
||||
{
|
||||
var (bounds, originX, originY, clearance) = fixture switch
|
||||
{
|
||||
0 => (new Box(0, 0, 1e-5, 1e-5), 0.0, 0.0, 0.0),
|
||||
1 => (new Box(0.75, 0.5, 1e-5, 1e-5), 7.25, -3.125, 0.0),
|
||||
2 => (new Box(0.75, 0.5, 1e-5, 0.75), 7.25, -3.125, 0.0),
|
||||
3 => (new Box(0.5, 0.75, 0.75, 1e-5), 7.25, -3.125, 0.0),
|
||||
// Positive dimensions can disappear even before grid conversion.
|
||||
4 => (new Box(1, 1, 1e-20, 1e-20), 7.25, -3.125, 0.0),
|
||||
_ => (new Box(0, 0, 1e-5, 1e-5), 0.0, 0.0, 1e-6),
|
||||
};
|
||||
var edge = new Spacing(0, clearance, 0, 0);
|
||||
var stock = Stock("sheet", 10, 12, quadrant: quadrant, edge: edge,
|
||||
clampZones: [new("tiny", bounds)]);
|
||||
var part = Rectangle("part", 2, 2, 1, x: originX, y: originY);
|
||||
var job = Job([part], [stock]);
|
||||
NestJobValidator.Validate(job);
|
||||
var zone = Assert.Single(stock.ClampZones);
|
||||
if (fixture == 4)
|
||||
{
|
||||
Assert.Equal(zone.Bounds.Left, zone.Bounds.Right);
|
||||
Assert.Equal(zone.Bounds.Bottom, zone.Bounds.Top);
|
||||
}
|
||||
|
||||
// A drop-clamp mutation must fail: the same job without a clamp enters it.
|
||||
var unclamped = Job([part], [Stock("sheet", 10, 12, quadrant: quadrant, edge: edge)]);
|
||||
var original = new IrregularNestingEngine().Solve(unclamped);
|
||||
AssertCompleteRectangleClearsClamps(original, originX, originY);
|
||||
var originalPose = Assert.Single(Assert.Single(original.Plates).Placements);
|
||||
Assert.True(RectangleEntersInflatedClamp(originalPose, originX, originY, zone.Bounds, clearance),
|
||||
"The fixture must block the unclamped placement.");
|
||||
|
||||
var result = new IrregularNestingEngine().Solve(job);
|
||||
|
||||
AssertCompleteRectangleClearsClamps(result, originX, originY);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, "0F90BA3EF6C6392CF1C1E01A6209D55CA88B4F968D6DD0D01850AE15A349FE88")]
|
||||
[InlineData(1, "A970D56C5768763CE339999F91ADD7FFB9EC2AF6C1585CC51C4B0869D45D0C53")]
|
||||
@@ -214,6 +275,45 @@ public class IrregularClampZoneTests
|
||||
Assert.Equal(expected, hash);
|
||||
}
|
||||
|
||||
private static void AssertCompleteRectangleClearsClamps(NestJobResult result, double originX, double originY)
|
||||
{
|
||||
// Do not call NestLayoutCheck here: its separate S1 tiny-clamp null-outline
|
||||
// defect can throw when broad-phase bounds touch. This is an analytic oracle.
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(NestJobStopReason.Completed, result.StopReason);
|
||||
Assert.Equal(new PartFulfillment("part", 1, 1, 0), Assert.Single(result.Fulfillment));
|
||||
Assert.Equal(1, Assert.Single(result.StockUsage).Used);
|
||||
var sheet = Assert.Single(result.Plates);
|
||||
var pose = Assert.Single(sheet.Placements);
|
||||
Assert.Equal("part", pose.PartId);
|
||||
Assert.Equal(0, pose.InstanceIndex);
|
||||
Assert.Equal(0, pose.Rotation);
|
||||
var work = sheet.Stock.WorkArea;
|
||||
var slack = NestTolerances.WorkAreaSlack;
|
||||
var left = pose.X + originX;
|
||||
var bottom = pose.Y + originY;
|
||||
Assert.True(left >= work.Left - slack && bottom >= work.Bottom - slack
|
||||
&& left + 2 <= work.Right + slack && bottom + 2 <= work.Top + slack);
|
||||
foreach (var zone in sheet.Stock.ClampZones)
|
||||
Assert.False(RectangleEntersInflatedClamp(pose, originX, originY, zone.Bounds,
|
||||
sheet.Stock.EdgeSpacing.Bottom),
|
||||
$"Rectangle at ({pose.X:R}, {pose.Y:R}) enters inflated clamp {zone.Id}");
|
||||
}
|
||||
|
||||
private static bool RectangleEntersInflatedClamp(NestJobPlacement pose, double originX, double originY,
|
||||
Box bounds, double clearance)
|
||||
{
|
||||
// Decimal arithmetic retains the positive 1e-20 fixture dimensions even
|
||||
// when Box.Right == Box.Left in double arithmetic. No Clipper/checker reuse.
|
||||
var left = (decimal)pose.X + (decimal)originX;
|
||||
var bottom = (decimal)pose.Y + (decimal)originY;
|
||||
var c = (decimal)clearance;
|
||||
return left < (decimal)bounds.Left + (decimal)bounds.Length + c
|
||||
&& left + 2 > (decimal)bounds.Left - c
|
||||
&& bottom < (decimal)bounds.Bottom + (decimal)bounds.Width + c
|
||||
&& bottom + 2 > (decimal)bounds.Bottom - c;
|
||||
}
|
||||
|
||||
private static void AssertDiscsClearClamps(NestJobResult result, double radius)
|
||||
{
|
||||
foreach (var sheet in result.Plates)
|
||||
|
||||
@@ -21,10 +21,8 @@ internal sealed class ClampObstacles
|
||||
for (var i = 0; i < rectangles.Length; i++)
|
||||
{
|
||||
var box = stock.ClampZones[i].Bounds;
|
||||
var left = box.Left - clearance;
|
||||
var bottom = box.Bottom - clearance;
|
||||
var right = box.Right + clearance;
|
||||
var top = box.Top + clearance;
|
||||
var (left, right) = EncloseOnGrid(box.Left - clearance, box.Right + clearance);
|
||||
var (bottom, top) = EncloseOnGrid(box.Bottom - clearance, box.Top + clearance);
|
||||
rectangles[i] = new Orientation
|
||||
{
|
||||
// Real part types are nonnegative; the private cache cannot alias them.
|
||||
@@ -44,6 +42,17 @@ internal sealed class ClampObstacles
|
||||
}
|
||||
}
|
||||
|
||||
// NoFitCache quantizes before adding its safety margin. Enclose the keep-out
|
||||
// on that existing lattice so a sub-grid rectangle cannot disappear first.
|
||||
private static (double Lower, double Upper) EncloseOnGrid(double lower, double upper)
|
||||
{
|
||||
var scale = System.Math.Pow(10, NoFitCache.Precision);
|
||||
var first = System.Math.Floor(lower * scale);
|
||||
// Positive dimensions can be smaller than a coordinate ulp (upper == lower).
|
||||
var last = System.Math.Max(System.Math.Ceiling(upper * scale), first + 1);
|
||||
return (first / scale, last / scale);
|
||||
}
|
||||
|
||||
public int Count => rectangles.Length;
|
||||
|
||||
// No additional physical spacing. NoFitCache retains the established arc/grid safety
|
||||
|
||||
+10
-2
@@ -79,8 +79,16 @@ per stock per solve, separately from the part-spacing cache. Every orientation's
|
||||
free region excludes these obstacles, including extra orientations introduced by pairs
|
||||
or Fill blocks. Blocks still size their proposals from work-area/placed-part rectangles;
|
||||
member free regions determine whether the whole proposal can actually fit around clamps.
|
||||
Open notches and material between clamps remain available. Existing arc/grid safety
|
||||
margins, covered-hole certification and cancellation apply without relaxed tolerances.
|
||||
Open notches and material between clamps remain available. Inflated synthetic rectangles
|
||||
are enclosed on the existing clipping grid before NFP preparation, retaining at least one
|
||||
grid interval per axis so positive sub-grid clamps cannot disappear. Existing arc/grid
|
||||
safety margins, covered-hole certification and cancellation apply without relaxed tolerances.
|
||||
|
||||
Known clamp edge cases remain in the earlier shared/Rectangles implementation:
|
||||
large part spacing can make Rectangles overlap a clamp; rectangles below the clipping
|
||||
grid can make layout validation (including Default's candidate scoring) throw. Irregular's
|
||||
static-obstacle preparation does not repair these separate paths. The ordinary clamp
|
||||
fixtures pass for both lanes and Default; this is not an all-input clamp-safety guarantee.
|
||||
|
||||
This is whole-job engine support, not interactive fill/push, desktop clamp editing or
|
||||
cutoff/post-verification support; those consumers are separate work. Other engines and
|
||||
|
||||
Reference in new issue
Block a user