fix: add overlap detection safety net for pair tiling

Shape.OffsetOutward produces inward offsets for certain rotated polygons,
causing geometry-aware copy distances to be too small and placing
overlapping parts. Root cause is in the offset winding direction
detection — this commit adds safety nets while that is investigated.

- FillLinear.FillGrid: detect bbox overlaps after geometry-aware tiling,
  fall back to bbox-based spacing when overlaps found
- FillExtents.RepeatColumns: detect overlaps after Compactor computes
  copy distance, fall back to columnWidth + spacing
- PairFiller/StripeFiller remnant fills: use FillLinear directly instead
  of spawning full engine pipeline (avoids strategies with the bug)
- Add PairOverlapDiagnosticTests reproducing the issue
- MCP config: use shadow-copy wrapper for dev hot-reload

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 23:52:50 -04:00
parent d7eb3ebd7a
commit 80e8693da3
6 changed files with 361 additions and 22 deletions
+18 -17
View File
@@ -244,28 +244,29 @@ public class StripeFiller
return cachedResult;
}
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear");
try
var filler = new FillLinear(remnantBox, spacing);
List<Part> best = null;
foreach (var angle in new[] { 0.0, Angle.HalfPI })
{
var engine = CreateRemnantEngine(_context.Plate);
var item = new NestItem { Drawing = drawing };
var parts = engine.Fill(item, remnantBox, _context.Progress, _context.Token);
_context.Token.ThrowIfCancellationRequested();
var result = FillHelpers.FillWithDirectionPreference(
dir => filler.Fill(drawing, angle, dir),
null, _comparer, remnantBox);
Debug.WriteLine($"[StripeFiller] Remnant engine ({engine.Name}): {parts?.Count ?? 0} parts, " +
$"winner={engine.WinnerPhase}");
if (parts != null && parts.Count > 0)
{
FillResultCache.Store(drawing, remnantBox, spacing, parts);
return parts;
}
return null;
if (result != null && result.Count > (best?.Count ?? 0))
best = result;
}
finally
Debug.WriteLine($"[StripeFiller] Remnant linear: {best?.Count ?? 0} parts");
if (best != null && best.Count > 0)
{
FillStrategyRegistry.SetEnabled(null);
FillResultCache.Store(drawing, remnantBox, spacing, best);
return best;
}
return null;
}
public static double FindAngleForTargetSpan(