fix: add overlap safety check and diagnostics to FillGrid Step 2

FillGrid had no overlap check after perpendicular tiling of the row
pattern (Step 2), unlike Step 1 which had one. When geometry-aware
FindPatternCopyDistance underestimated row spacing, overlapping parts
were returned unchecked.

Changes:
- Make FillLinear.HasOverlappingParts shape-aware (bbox pre-filter +
  Part.Intersects) instead of bbox-only, preventing false positives on
  interlocking pairs while catching real overlaps
- Add missing overlap safety check after Step 2 perpendicular tiling
  with bbox fallback
- Add diagnostic Debug.WriteLine logging when overlap fallback triggers,
  including engine label, step, direction, work area, spacing, pattern
  details, and overlapping part locations/rotations for reproduction
- Add FillLinear.Label property set at all callsites for log traceability
- Refactor LinearFillStrategy and ExtentsFillStrategy to use shared
  FillHelpers.BestOverAngles helper for angle-sweep logic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 22:08:38 -04:00
parent 1c2b569ff4
commit 953429dae9
9 changed files with 133 additions and 67 deletions
+57 -6
View File
@@ -1,6 +1,7 @@
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace OpenNest.Engine.Fill
@@ -19,6 +20,11 @@ namespace OpenNest.Engine.Fill
public double HalfSpacing => PartSpacing / 2;
/// <summary>
/// Diagnostic label set by callers to identify the engine/context in overlap logs.
/// </summary>
public string Label { get; set; }
private static Vector MakeOffset(NestDirection direction, double distance)
{
return direction == NestDirection.Horizontal
@@ -323,7 +329,7 @@ namespace OpenNest.Engine.Fill
return result;
}
private static bool HasOverlappingParts(List<Part> parts)
private static bool HasOverlappingParts(List<Part> parts, out int overlapA, out int overlapB)
{
for (var i = 0; i < parts.Count; i++)
{
@@ -338,11 +344,20 @@ namespace OpenNest.Engine.Fill
var overlapY = System.Math.Min(b1.Top, b2.Top)
- System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX > Tolerance.Epsilon && overlapY > Tolerance.Epsilon)
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
continue;
if (parts[i].Intersects(parts[j], out _))
{
overlapA = i;
overlapB = j;
return true;
}
}
}
overlapA = -1;
overlapB = -1;
return false;
}
@@ -384,10 +399,9 @@ namespace OpenNest.Engine.Fill
var row = new List<Part>(pattern.Parts);
row.AddRange(TilePattern(pattern, direction, boundaries));
// Safety: if geometry-aware spacing produced overlapping parts,
// fall back to bbox-based spacing for this axis.
if (pattern.Parts.Count > 1 && HasOverlappingParts(row))
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a1, out var b1))
{
LogOverlap("Step1-Primary", direction, pattern, row, a1, b1);
row = new List<Part>(pattern.Parts);
row.AddRange(TilePatternBbox(pattern, direction));
}
@@ -397,8 +411,9 @@ namespace OpenNest.Engine.Fill
{
row.AddRange(TilePattern(pattern, perpAxis, boundaries));
if (pattern.Parts.Count > 1 && HasOverlappingParts(row))
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a2, out var b2))
{
LogOverlap("Step1-PerpOnly", perpAxis, pattern, row, a2, b2);
row = new List<Part>(pattern.Parts);
row.AddRange(TilePatternBbox(pattern, perpAxis));
}
@@ -415,9 +430,45 @@ namespace OpenNest.Engine.Fill
var gridResult = new List<Part>(rowPattern.Parts);
gridResult.AddRange(TilePattern(rowPattern, perpAxis, rowBoundaries));
if (HasOverlappingParts(gridResult, out var a3, out var b3))
{
LogOverlap("Step2-Perp", perpAxis, rowPattern, gridResult, a3, b3);
gridResult = new List<Part>(rowPattern.Parts);
gridResult.AddRange(TilePatternBbox(rowPattern, perpAxis));
}
return gridResult;
}
private void LogOverlap(string step, NestDirection tilingDir,
Pattern pattern, List<Part> parts, int idxA, int idxB)
{
var pa = parts[idxA];
var pb = parts[idxB];
var ba = pa.BoundingBox;
var bb = pb.BoundingBox;
Debug.WriteLine($"[FillLinear] OVERLAP FALLBACK ({Label ?? "unknown"})");
Debug.WriteLine($" Step: {step}, TilingDir: {tilingDir}");
Debug.WriteLine($" WorkArea: ({WorkArea.X:F4},{WorkArea.Y:F4}) {WorkArea.Width:F4}x{WorkArea.Length:F4}, Spacing: {PartSpacing}");
Debug.WriteLine($" Pattern: {pattern.Parts.Count} parts, bbox {pattern.BoundingBox.Width:F4}x{pattern.BoundingBox.Length:F4}");
Debug.WriteLine($" Total parts after tiling: {parts.Count}");
Debug.WriteLine($" Overlapping pair [{idxA}] vs [{idxB}]:");
Debug.WriteLine($" [{idxA}]: drawing={pa.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pa.Rotation):F2}° " +
$"loc=({pa.Location.X:F4},{pa.Location.Y:F4}) bbox=({ba.Left:F4},{ba.Bottom:F4})-({ba.Right:F4},{ba.Top:F4})");
Debug.WriteLine($" [{idxB}]: drawing={pb.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pb.Rotation):F2}° " +
$"loc=({pb.Location.X:F4},{pb.Location.Y:F4}) bbox=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4})");
// Log all pattern seed parts for reproduction
Debug.WriteLine($" Pattern seed parts:");
for (var i = 0; i < pattern.Parts.Count; i++)
{
var p = pattern.Parts[i];
Debug.WriteLine($" [{i}]: drawing={p.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(p.Rotation):F2}° " +
$"loc=({p.Location.X:F4},{p.Location.Y:F4}) bbox={p.BoundingBox.Width:F4}x{p.BoundingBox.Length:F4}");
}
}
/// <summary>
/// Fills a single row of identical parts along one axis using geometry-aware spacing.
/// </summary>
+2 -2
View File
@@ -195,7 +195,7 @@ namespace OpenNest.Engine.Fill
if (pattern.Parts.Count == 0)
continue;
var engine = new FillLinear(workArea, partSpacing);
var engine = new FillLinear(workArea, partSpacing) { Label = "Pairs" };
foreach (var dir in new[] { NestDirection.Horizontal, NestDirection.Vertical })
{
if (!dedup.TryAdd(pattern.BoundingBox, workArea, dir))
@@ -321,7 +321,7 @@ namespace OpenNest.Engine.Fill
return cachedResult;
}
var filler = new FillLinear(remnantBox, partSpacing);
var filler = new FillLinear(remnantBox, partSpacing) { Label = "Pairs-Remnant" };
List<Part> parts = null;
foreach (var angle in new[] { 0.0, Angle.HalfPI })
+4 -4
View File
@@ -121,7 +121,7 @@ public class StripeFiller
if (!_dedup.TryAdd(rotatedPattern.BoundingBox, workArea, primaryAxis))
return null;
var stripeEngine = new FillLinear(stripeBox, spacing);
var stripeEngine = new FillLinear(stripeBox, spacing) { Label = "Stripe" };
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
if (stripeParts == null || stripeParts.Count == 0)
@@ -136,7 +136,7 @@ public class StripeFiller
stripePattern.Parts.AddRange(stripeParts);
stripePattern.UpdateBounds();
var gridEngine = new FillLinear(workArea, spacing);
var gridEngine = new FillLinear(workArea, spacing) { Label = "Stripe-Grid" };
var gridParts = gridEngine.Fill(stripePattern, perpAxis);
if (gridParts == null || gridParts.Count == 0)
@@ -244,7 +244,7 @@ public class StripeFiller
return cachedResult;
}
var filler = new FillLinear(remnantBox, spacing);
var filler = new FillLinear(remnantBox, spacing) { Label = "Stripe-Remnant" };
List<Part> best = null;
foreach (var angle in new[] { 0.0, Angle.HalfPI })
@@ -396,7 +396,7 @@ public class StripeFiller
var stripeBox = axis == NestDirection.Horizontal
? new Box(0, 0, sheetSpan, perpDim)
: new Box(0, 0, perpDim, sheetSpan);
var engine = new FillLinear(stripeBox, spacing);
var engine = new FillLinear(stripeBox, spacing) { Label = "Stripe-EstimateRow" };
var filled = engine.Fill(rotated, axis);
var n = filled?.Count ?? 0;