fix(clamps): judge tiny keep-outs tolerance-free in interactive fill

InteractiveClamps.MaterialInKeepOut flattened a sub-tolerance keep-out with
the 0.001 polygon tolerance and called a part covering a 1e-5 clamp clear,
so the fill safety net could pass it. Keep-outs below the tolerance now use
KeepOutOverlap. Adds the 1e-5 zone sweep through every fill strategy and a
push (characterization; both already stay clear).

PM b0062c73-40ef-440e-8efb-b94908f7c2d1
This commit is contained in:
aj committed 2026-10-13 18:15:49 -04:00
1 parent 8f0c0692fd
commit 9b5885e5a2
2 files changed
+60

No files matched your search

@@ -1,5 +1,7 @@
using OpenNest.CNC;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Engine.NestingEngines.Default;
using OpenNest.Engine.NestingEngines.Rectangles;
using OpenNest.Geometry;
@@ -131,4 +133,57 @@ public class TinyClampZoneTests
Assert.Equal(overlaps, ClampViolations(job, result).Count > 0);
}
[Theory]
[InlineData(7.5, 0.0, true)]
[InlineData(6.0, 0.0, false)]
[InlineData(8.00001, 0.0, false)]
[InlineData(3.0, 0.0, false)]
public void InteractiveKeepOutTestJudgesATinyClamp(double x, double y, bool overlaps)
{
var drawing = new Drawing("square", SquareProgram());
var part = new Part(drawing, new Vector(x, y));
Assert.Equal(overlaps, InteractiveClamps.MaterialInKeepOut(part, new Box(8, 0, Tiny, Tiny)));
}
private static Plate TinyZonePlate()
{
var plate = new Plate(new Size(10, 12)); // zero edge and part spacing
plate.ClampZones.Add(TinyZone);
return plate;
}
// Positive-area overlap with the zone, admitting 1e-9 of floating closure (a ten
// thousandth of the zone): a push that stops flush on the zone is clear.
private static bool EntersTinyZone(Part part) =>
System.Math.Min(part.BoundingBox.Right, 8 + Tiny) - System.Math.Max(part.BoundingBox.Left, 8) > 1e-9
&& System.Math.Min(part.BoundingBox.Top, Tiny) - System.Math.Max(part.BoundingBox.Bottom, 0) > 1e-9;
[Theory]
[InlineData("Fill")]
[InlineData("Strip")]
[InlineData("Vertical Remnant")]
[InlineData("Horizontal Remnant")]
public void InteractiveFillNeverCoversATinyClamp(string strategy)
{
var plate = TinyZonePlate();
var item = new NestItem { Drawing = new Drawing("square", SquareProgram()), Quantity = 30 };
var parts = PlateFillService.FillItem(strategy, plate, item, plate.WorkArea(), null,
System.Threading.CancellationToken.None);
Assert.NotEmpty(parts);
Assert.DoesNotContain(parts, EntersTinyZone);
}
[Fact]
public void PushDownStopsBeforeCoveringATinyClamp()
{
var plate = TinyZonePlate();
var mover = new Part(new Drawing("square", SquareProgram()), new Vector(7.5, 5));
plate.Parts.Add(mover);
Compactor.Push(new List<Part> { mover }, plate, PushDirection.Down);
Assert.False(EntersTinyZone(mover), $"pushed to ({mover.Location.X:R},{mover.Location.Y:R})");
}
}
@@ -97,6 +97,11 @@ internal static class InteractiveClamps
var partProfile = new ShapeProfile(PartGeometry.GetPartEntities(part));
if (partProfile.Perimeter == null)
return true;
// A keep-out below the polygon tolerance collapses in the tolerant test and would
// read as clear (PM b0062c73); judge it tolerance-free instead.
if (KeepOutOverlap.NeedsToleranceFreeTest(keepOut))
return KeepOutOverlap.MaterialEnters(partProfile.Perimeter.ToPolygonWithTolerance(0.001),
HolePolygons(partProfile), keepOut);
var zoneProfile = new ShapeProfile(RectangleEntities(keepOut));
if (zoneProfile.Perimeter == null)
return true;