fix(engine): use required spacing, not a sampled gap, when resequencing shrink-fill strips

SortStrips measured the gap between only the first two strips in original
placement order and replayed that single value between every strip after
reordering by height/width. Real (non-uniform) geometry produces varying
inter-strip gaps, so resequencing could expand the total footprint beyond
the plate's already-fitted work area, crashing StripPlateNester with
"Candidate placement falls outside the usable stock area." Using the
actual required spacing guarantees the resequenced span never exceeds
the original, since real gaps are always >= spacing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-19 10:04:33 -04:00
co-authored by Claude Sonnet 5
parent aa88eee484
commit 9b69c67572
3 changed files with 52 additions and 3 deletions
@@ -0,0 +1,46 @@
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests.Fill;
public class SortStripsTests
{
private static Part MakeRectPart(double x, double y, double w, double h)
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
var drawing = new Drawing("rect", pgm);
return new Part(drawing, new Vector(x, y));
}
[Fact]
public void SortColumnsByHeight_NonUniformGaps_DoesNotExceedOriginalSpan()
{
// Three columns with non-uniform gaps between them (5, then 1) and heights
// ordered so the sort-by-height pass must reorder them (tallest first, then
// shortest, then medium). The tallest column's original position leaves a
// 5-unit gap to its neighbor; that single sampled gap must not get replayed
// as the spacing for the whole staircase once it's no longer the leading pair.
var tall = MakeRectPart(0, 0, 10, 30); // Left 0-10, gap of 5 to next
var shortCol = MakeRectPart(15, 0, 5, 5); // Left 15-20, gap of 1 to next
var medium = MakeRectPart(21, 0, 20, 15); // Left 21-41
var originalRight = new[] { tall, shortCol, medium }.Max(p => p.BoundingBox.Right);
var originalLeft = new[] { tall, shortCol, medium }.Min(p => p.BoundingBox.Left);
var originalSpan = originalRight - originalLeft;
var parts = new List<Part> { tall, shortCol, medium };
IterativeShrinkFiller.SortColumnsByHeight(parts, spacing: 1.0);
var newRight = parts.Max(p => p.BoundingBox.Right);
var newLeft = parts.Min(p => p.BoundingBox.Left);
var newSpan = newRight - newLeft;
Assert.True(newSpan <= originalSpan + 1e-9,
$"Resequenced columns must not exceed the original footprint: original span {originalSpan}, new span {newSpan}");
}
}
@@ -219,8 +219,10 @@ namespace OpenNest.Engine.Fill
if (strips.Count <= 1) if (strips.Count <= 1)
return; return;
var gap = stripMin(strips[1]) - stripMax(strips[0]); // Use the required clearance as the inter-strip gap, not a gap sampled from one
// original pair: actual placement gaps vary for irregular/mixed-size geometry, and
// replaying a larger sampled gap across every reordered pair can push the trailing
// strip past the original (already plate-fitted) footprint.
strips.Sort((a, b) => sortMetric(a).CompareTo(sortMetric(b))); strips.Sort((a, b) => sortMetric(a).CompareTo(sortMetric(b)));
var pos = primaryEdge(parts[0].BoundingBox); var pos = primaryEdge(parts[0].BoundingBox);
@@ -236,7 +238,7 @@ namespace OpenNest.Engine.Fill
part.Offset(offset); part.Offset(offset);
} }
pos = stripMax(s) + gap; pos = stripMax(s) + spacing;
} }
parts.Clear(); parts.Clear();
+1
View File
@@ -6,6 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<InternalsVisibleTo Include="OpenNest.Tests" /> <InternalsVisibleTo Include="OpenNest.Tests" />
<InternalsVisibleTo Include="OpenNest.Engine.Tests" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" /> <ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />