fix: add overlap validation guards to FillExtents and StripeFiller

FillExtents falls back to the unadjusted column when iterative pair
adjustment shifts parts enough to cause genuine overlap. StripeFiller
rejects grid results where bounding boxes overlap, which can occur when
angle convergence produces slightly off-axis rotations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 14:13:35 -04:00
parent e1b6752ede
commit 740fd79adc
2 changed files with 74 additions and 1 deletions

View File

@@ -47,13 +47,21 @@ namespace OpenNest.Engine.Fill
var adjusted = AdjustColumn(pair.Value, column, token);
// The iterative pair adjustment can shift parts enough to cause
// genuine overlap. Fall back to the unadjusted column when this happens.
if (HasOverlappingParts(adjusted))
{
Debug.WriteLine("[FillExtents] Adjusted column has overlaps, using unadjusted");
adjusted = column;
}
NestEngineBase.ReportProgress(progress, new ProgressReport
{
Phase = NestPhase.Extents,
PlateNumber = plateNumber,
Parts = adjusted,
WorkArea = workArea,
Description = $"Extents: adjusted column {adjusted.Count} parts",
Description = $"Extents: column {adjusted.Count} parts",
});
var result = RepeatColumns(adjusted, token);
@@ -386,5 +394,31 @@ namespace OpenNest.Engine.Fill
part.BoundingBox.Left >= workArea.Left - Tolerance.Epsilon &&
part.BoundingBox.Bottom >= workArea.Bottom - Tolerance.Epsilon;
}
private static bool HasOverlappingParts(List<Part> parts)
{
for (var i = 0; i < parts.Count; i++)
{
var b1 = parts[i].BoundingBox;
for (var j = i + 1; j < parts.Count; j++)
{
var b2 = parts[j].BoundingBox;
var overlapX = System.Math.Min(b1.Right, b2.Right)
- System.Math.Max(b1.Left, b2.Left);
var overlapY = System.Math.Min(b1.Top, b2.Top)
- System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
continue;
if (parts[i].Intersects(parts[j], out _))
return true;
}
}
return false;
}
}
}

View File

@@ -158,6 +158,15 @@ public class StripeFiller
if (gridParts.Count == 0)
return null;
// Reject results where bounding boxes overlap — the angle convergence
// can produce slightly off-axis rotations where FillLinear's copy
// distance calculation doesn't fully account for the rotated geometry.
if (HasOverlappingParts(gridParts))
{
Debug.WriteLine($"[StripeFiller] Rejected grid: overlapping bounding boxes detected");
return null;
}
var allParts = new List<Part>(gridParts);
var remnantParts = FillRemnant(gridParts, primaryAxis);
@@ -470,4 +479,34 @@ public class StripeFiller
{
return axis == NestDirection.Horizontal ? box.Width : box.Length;
}
/// <summary>
/// Checks if any pair of parts geometrically overlap. Uses bounding box
/// pre-filtering for performance, then falls back to shape intersection.
/// </summary>
private static bool HasOverlappingParts(List<Part> parts)
{
for (var i = 0; i < parts.Count; i++)
{
var b1 = parts[i].BoundingBox;
for (var j = i + 1; j < parts.Count; j++)
{
var b2 = parts[j].BoundingBox;
var overlapX = System.Math.Min(b1.Right, b2.Right)
- System.Math.Max(b1.Left, b2.Left);
var overlapY = System.Math.Min(b1.Top, b2.Top)
- System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
continue;
if (parts[i].Intersects(parts[j], out _))
return true;
}
}
return false;
}
}