fix: resolve grid overlap bug and parallelize fill loops

The push algorithm's copy distance formula (bboxDim - slideDistance)
produced distances smaller than the part width when inflated boundary
arc vertices interacted spuriously, causing ~0.05 unit overlaps between
all adjacent grid parts.

Two fixes applied:
- Clamp ComputeCopyDistance to bboxDim + PartSpacing minimum
- Use circumscribed polygons (R/cos(halfStep)) for PartBoundary arc
  discretization so chord segments never cut inside the true arc,
  eliminating the ChordTolerance offset workaround

Also parallelized three sequential fill loops using Parallel.ForEach:
- FindBestFill angle sweep (up to 38 angles x 2 directions)
- FillPattern angle sweep for group/pair fills
- FillRemainingStrip rotation loop

Added diagnostic logging to HasOverlaps, FindCopyDistance, and
FillRecursive for debugging fill issues.

Test result: 45 parts @ 79.6% -> 47 parts @ 83.1%, zero overlaps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 00:36:48 -04:00
parent 0e3bf3ccaa
commit 78ee65d946
6 changed files with 126 additions and 39 deletions

View File

@@ -85,18 +85,31 @@ namespace OpenNest
}
}
List<Part> best = null;
var linearBag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List<Part> parts)>();
foreach (var angle in angles)
System.Threading.Tasks.Parallel.ForEach(angles, angle =>
{
var h = engine.Fill(item.Drawing, angle, NestDirection.Horizontal);
var v = engine.Fill(item.Drawing, angle, NestDirection.Vertical);
var localEngine = new FillLinear(workArea, Plate.PartSpacing);
var h = localEngine.Fill(item.Drawing, angle, NestDirection.Horizontal);
var v = localEngine.Fill(item.Drawing, angle, NestDirection.Vertical);
if (IsBetterFill(h, best, workArea))
best = h;
if (h != null && h.Count > 0)
linearBag.Add((FillScore.Compute(h, workArea), h));
if (IsBetterFill(v, best, workArea))
best = v;
if (v != null && v.Count > 0)
linearBag.Add((FillScore.Compute(v, workArea), v));
});
List<Part> best = null;
var bestScore = default(FillScore);
foreach (var (score, parts) in linearBag)
{
if (best == null || score > bestScore)
{
best = parts;
bestScore = score;
}
}
var bestLinearScore = best != null ? FillScore.Compute(best, workArea) : default;
@@ -294,7 +307,14 @@ namespace OpenNest
List<Vector> pts;
if (parts[i].Intersects(parts[j], out pts))
{
var b1 = parts[i].BoundingBox;
var b2 = parts[j].BoundingBox;
Debug.WriteLine($"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({b1.Left:F2},{b1.Bottom:F2})-({b1.Right:F2},{b1.Top:F2}) rot={parts[i].Rotation:F2}" +
$" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({b2.Left:F2},{b2.Bottom:F2})-({b2.Right:F2},{b2.Top:F2}) rot={parts[j].Rotation:F2}" +
$" intersections={pts?.Count ?? 0}");
return true;
}
}
}
@@ -315,7 +335,10 @@ namespace OpenNest
private bool IsBetterValidFill(List<Part> candidate, List<Part> current, Box workArea)
{
if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing))
{
Debug.WriteLine($"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})");
return false;
}
return IsBetterFill(candidate, current, workArea);
}
@@ -475,23 +498,36 @@ namespace OpenNest
private List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles, Box workArea)
{
List<Part> best = null;
var bag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List<Part> parts)>();
foreach (var angle in angles)
System.Threading.Tasks.Parallel.ForEach(angles, angle =>
{
var pattern = BuildRotatedPattern(groupParts, angle);
if (pattern.Parts.Count == 0)
continue;
return;
var h = engine.Fill(pattern, NestDirection.Horizontal);
var v = engine.Fill(pattern, NestDirection.Vertical);
var localEngine = new FillLinear(workArea, engine.PartSpacing);
var h = localEngine.Fill(pattern, NestDirection.Horizontal);
var v = localEngine.Fill(pattern, NestDirection.Vertical);
if (IsBetterValidFill(h, best, workArea))
best = h;
if (h != null && h.Count > 0 && !HasOverlaps(h, engine.PartSpacing))
bag.Add((FillScore.Compute(h, workArea), h));
if (IsBetterValidFill(v, best, workArea))
best = v;
if (v != null && v.Count > 0 && !HasOverlaps(v, engine.PartSpacing))
bag.Add((FillScore.Compute(v, workArea), v));
});
List<Part> best = null;
var bestScore = default(FillScore);
foreach (var (score, parts) in bag)
{
if (best == null || score > bestScore)
{
best = parts;
bestScore = score;
}
}
return best;