fix(engine): stop rejecting arc pairs placed at the part spacing

On a plate with clamps, MaterialsTooClose measured gaps on polygons faceted
at 0.001, which understate an arc gap by more than the 0.0005 slack. A pair
interlocked exactly at the spacing read as too close, so FillGroup discarded
every copy and the fill silently placed nothing. Confirm the gap on
NativeContours, as the layout check and pre-post review already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-30 08:41:22 -04:00
1 parent 4ca8b0debf
commit 508b7db0df
2 files changed
+44 -10

No files matched your search

@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading;
using OpenNest.CNC;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Geometry;
using Xunit;
@@ -188,6 +189,45 @@ public class ClampZoneInteractiveFillTests
Assert.Equal(0, parts.Count % 2);
}
/// <summary>An interlocked pair of crescents, their arcs exactly <c>0.25</c> apart at
/// <paramref name="nudge"/> zero. Faceting those arcs at 0.001 reads about 0.2493.</summary>
private static List<Part> InterlockedCrescents(double nudge = 0)
{
var program = new Program();
program.MoveTo(0, 0);
program.ArcTo(new Vector(-7.3094835341, 0), new Vector(-3.6547417671, 2.625426178),
RotationType.CCW);
program.ArcTo(new Vector(0, 0), new Vector(-3.6547417671, 3.4121639199), RotationType.CW);
var drawing = new Drawing("crescent", program);
var flipped = new Part(drawing);
flipped.Rotate(System.Math.PI);
flipped.Offset(new Vector(17.790933720357735 + nudge, 41.393296427203666));
return [new Part(drawing, new Vector(23.975496771940087, 35.352829360961906)), flipped];
}
[Fact]
public void FillGroupKeepsAnArcPairInterlockedExactlyAtThePartSpacing()
{
var plate = new Plate(new Size(60, 120)) { PartSpacing = 0.25 };
plate.ClampZones.Add(new ClampZone("front", new Box(19.25, 0, 1.5, 0.625)));
var parts = PlateFillService.FillGroup("Fill", plate, InterlockedCrescents(),
plate.WorkArea(), null, CancellationToken.None);
Assert.True(parts.Count >= 100, $"expected a full sheet of pairs, got {parts.Count}");
}
[Fact]
public void MaterialsTooCloseMeasuresArcsRatherThanTheirFacets()
{
var atSpacing = InterlockedCrescents();
Assert.False(InteractiveClamps.MaterialsTooClose(atSpacing[0], atSpacing[1], 0.25));
var tooClose = InterlockedCrescents(nudge: -0.01);
Assert.True(InteractiveClamps.MaterialsTooClose(tooClose[0], tooClose[1], 0.25));
}
[Fact]
public void PackAreaKeepsAllowedOverhangOnlyAtThePlateBoundary()
{
+4 -10
View File
@@ -124,16 +124,10 @@ internal static class InteractiveClamps
return true;
if (spacing <= NestTolerances.SpacingSlack)
return false;
var ringsA = new List<Polygon> { outerA };
var ringsB = new List<Polygon> { outerB };
if (holesA != null) ringsA.AddRange(holesA);
if (holesB != null) ringsB.AddRange(holesB);
foreach (var ringA in ringsA)
foreach (var ringB in ringsB)
if (Clearance.BoundaryDistance(ringA, ringB).Distance
< spacing - NestTolerances.SpacingSlack)
return true;
return false;
// The profiles are already in world space. Facets at the polygons' tolerance understate
// an arc gap by more than the slack, which would reject a pair placed at the spacing.
return NativeContours.Of(a, new Vector(0, 0))
.TooClose(NativeContours.Of(b, new Vector(0, 0)), spacing);
}
private static List<Polygon> HolePolygons(ShapeProfile profile)