From 740fd79adc06cb7bac4b8e62b54c23465a8df23f Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 27 Mar 2026 14:13:35 -0400 Subject: [PATCH] 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) --- OpenNest.Engine/Fill/FillExtents.cs | 36 ++++++++++++++++++++++++- OpenNest.Engine/Fill/StripeFiller.cs | 39 ++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index 6fae350..14ea616 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -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 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; + } } } diff --git a/OpenNest.Engine/Fill/StripeFiller.cs b/OpenNest.Engine/Fill/StripeFiller.cs index 7fb60f3..9f322d2 100644 --- a/OpenNest.Engine/Fill/StripeFiller.cs +++ b/OpenNest.Engine/Fill/StripeFiller.cs @@ -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(gridParts); var remnantParts = FillRemnant(gridParts, primaryAxis); @@ -470,4 +479,34 @@ public class StripeFiller { return axis == NestDirection.Horizontal ? box.Width : box.Length; } + + /// + /// Checks if any pair of parts geometrically overlap. Uses bounding box + /// pre-filtering for performance, then falls back to shape intersection. + /// + private static bool HasOverlappingParts(List 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; + } }