diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs
index 5471123..b21e289 100644
--- a/OpenNest.Engine/Fill/FillLinear.cs
+++ b/OpenNest.Engine/Fill/FillLinear.cs
@@ -61,92 +61,91 @@ namespace OpenNest.Engine.Fill
: NestDirection.Horizontal;
}
- ///
- /// Computes the slide distance for the push algorithm, returning the
- /// geometry-aware copy distance along the given axis.
- ///
- private double ComputeCopyDistance(double bboxDim, double slideDistance)
- {
- if (slideDistance >= double.MaxValue || slideDistance < 0)
- return bboxDim + PartSpacing;
-
- // The geometry-aware slide can produce a copy distance smaller than
- // the part itself when inflated corner/arc vertices interact spuriously.
- // Clamp to bboxDim + PartSpacing to prevent bounding box overlap.
- return System.Math.Max(bboxDim - slideDistance, bboxDim + PartSpacing);
- }
-
///
/// Finds the geometry-aware copy distance between two identical parts along an axis.
- /// Both parts are inflated by half-spacing for symmetric spacing.
+ /// Uses native Line/Arc entities (inflated by half-spacing) so curves are handled
+ /// exactly without polygon sampling error.
///
- private double FindCopyDistance(Part partA, NestDirection direction, PartBoundary boundary)
+ private double FindCopyDistance(Part partA, NestDirection direction)
{
var bboxDim = GetDimension(partA.BoundingBox, direction);
var pushDir = GetPushDirection(direction);
+ var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
+ var offset = MakeOffset(direction, startOffset);
- var locationBOffset = MakeOffset(direction, bboxDim);
+ var stationaryEntities = PartGeometry.GetOffsetPerimeterEntities(partA, HalfSpacing);
+ var movingEntities = PartGeometry.GetOffsetPerimeterEntities(
+ partA.CloneAtOffset(offset), HalfSpacing);
- // Use the most efficient array-based overload to avoid all allocations.
var slideDistance = SpatialQuery.DirectionalDistance(
- boundary.GetEdges(pushDir), partA.Location + locationBOffset,
- boundary.GetEdges(SpatialQuery.OppositeDirection(pushDir)), partA.Location,
- pushDir);
+ movingEntities, stationaryEntities, pushDir);
- return ComputeCopyDistance(bboxDim, slideDistance);
+ if (slideDistance >= double.MaxValue || slideDistance < 0)
+ return bboxDim + PartSpacing;
+
+ return startOffset - slideDistance;
}
///
/// Finds the geometry-aware copy distance between two identical patterns along an axis.
- /// Checks every pair of parts across adjacent patterns so that multi-part
- /// patterns (e.g. interlocking pairs) maintain spacing between ALL parts.
- /// Both sides are inflated by half-spacing for symmetric spacing.
+ /// Checks every pair of parts across adjacent pattern copies so multi-part patterns
+ /// (e.g. interlocking pairs) maintain spacing between ALL parts. Uses native entity
+ /// geometry inflated by half-spacing — same primitive the Compactor uses — so arcs
+ /// are exact and no bbox clamp is needed.
///
- private double FindPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary[] boundaries)
+ private double FindPatternCopyDistance(Pattern patternA, NestDirection direction)
{
- if (patternA.Parts.Count <= 1)
- return FindSinglePartPatternCopyDistance(patternA, direction, boundaries[0]);
+ if (patternA.Parts.Count == 1)
+ return FindCopyDistance(patternA.Parts[0], direction);
var bboxDim = GetDimension(patternA.BoundingBox, direction);
var pushDir = GetPushDirection(direction);
var opposite = SpatialQuery.OppositeDirection(pushDir);
+ var dirVec = SpatialQuery.DirectionToOffset(pushDir, 1.0);
// bboxDim already spans max(upper) - min(lower) across all parts,
// so the start offset just needs to push beyond that plus spacing.
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
var offset = MakeOffset(direction, startOffset);
- var maxCopyDistance = FindMaxPairDistance(
- patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
+ var parts = patternA.Parts;
+ var stationaryBoxes = new Box[parts.Count];
+ var movingBoxes = new Box[parts.Count];
+ var stationaryEntities = new List[parts.Count];
+ var movingEntities = new List[parts.Count];
- // The copy distance must be at least bboxDim + PartSpacing to prevent
- // bounding box overlap. Cross-pair slides can underestimate when the
- // circumscribed polygon boundary overshoots the true arc, creating
- // spurious contacts between diagonal parts in adjacent copies.
- return System.Math.Max(maxCopyDistance, bboxDim + PartSpacing);
- }
+ for (var i = 0; i < parts.Count; i++)
+ {
+ stationaryBoxes[i] = parts[i].BoundingBox;
+ movingBoxes[i] = stationaryBoxes[i].Translate(offset);
+ }
- ///
- /// Tests every pair of parts across adjacent pattern copies and returns the
- /// maximum copy distance found. Returns 0 if no valid slide was found.
- ///
- private static double FindMaxPairDistance(
- List parts, PartBoundary[] boundaries, Vector offset,
- PushDirection pushDir, PushDirection opposite, double startOffset)
- {
var maxCopyDistance = 0.0;
for (var j = 0; j < parts.Count; j++)
{
- var movingEdges = boundaries[j].GetEdges(pushDir);
- var locationB = parts[j].Location + offset;
+ var movingBox = movingBoxes[j];
for (var i = 0; i < parts.Count; i++)
{
+ var stationaryBox = stationaryBoxes[i];
+
+ // Skip if stationary is already ahead of moving in the push direction
+ // (sliding forward would take them further apart).
+ if (SpatialQuery.DirectionalGap(movingBox, stationaryBox, opposite) > 0)
+ continue;
+
+ // Skip if bboxes can't overlap along the axis perpendicular to the push.
+ if (!SpatialQuery.PerpendicularOverlap(movingBox, stationaryBox, dirVec))
+ continue;
+
+ stationaryEntities[i] ??= PartGeometry.GetOffsetPerimeterEntities(
+ parts[i], HalfSpacing);
+ movingEntities[j] ??= PartGeometry.GetOffsetPerimeterEntities(
+ parts[j].CloneAtOffset(offset), HalfSpacing);
+
var slideDistance = SpatialQuery.DirectionalDistance(
- movingEdges, locationB,
- boundaries[i].GetEdges(opposite), parts[i].Location,
- pushDir);
+ movingEntities[j], stationaryEntities[i], pushDir);
if (slideDistance >= double.MaxValue || slideDistance < 0)
continue;
@@ -161,86 +160,15 @@ namespace OpenNest.Engine.Fill
return maxCopyDistance;
}
- ///
- /// Fast path for single-part patterns — no cross-part conflicts possible.
- ///
- private double FindSinglePartPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary boundary)
- {
- var template = patternA.Parts[0];
- return FindCopyDistance(template, direction, boundary);
- }
-
- ///
- /// Gets offset boundary lines for all parts in a pattern using a shared boundary.
- ///
- private static List GetPatternLines(Pattern pattern, PartBoundary boundary, PushDirection direction)
- {
- var lines = new List();
-
- foreach (var part in pattern.Parts)
- lines.AddRange(boundary.GetLines(part.Location, direction));
-
- return lines;
- }
-
- ///
- /// Gets boundary lines for all parts in a pattern, with an additional
- /// location offset applied. Avoids cloning the pattern.
- ///
- private static List GetOffsetPatternLines(Pattern pattern, Vector offset, PartBoundary boundary, PushDirection direction)
- {
- var lines = new List();
-
- foreach (var part in pattern.Parts)
- lines.AddRange(boundary.GetLines(part.Location + offset, direction));
-
- return lines;
- }
-
- ///
- /// Creates boundaries for all parts in a pattern. Parts that share the same
- /// program geometry (same drawing and rotation) reuse the same boundary instance.
- ///
- private PartBoundary[] CreateBoundaries(Pattern pattern)
- {
- var boundaries = new PartBoundary[pattern.Parts.Count];
- var cache = new List<(Drawing drawing, double rotation, PartBoundary boundary)>();
-
- for (var i = 0; i < pattern.Parts.Count; i++)
- {
- var part = pattern.Parts[i];
- PartBoundary found = null;
-
- foreach (var entry in cache)
- {
- if (entry.drawing == part.BaseDrawing && entry.rotation.IsEqualTo(part.Rotation))
- {
- found = entry.boundary;
- break;
- }
- }
-
- if (found == null)
- {
- found = new PartBoundary(part, HalfSpacing);
- cache.Add((part.BaseDrawing, part.Rotation, found));
- }
-
- boundaries[i] = found;
- }
-
- return boundaries;
- }
-
///
/// Tiles a pattern along the given axis, returning the cloned parts
/// (does not include the original pattern's parts). For multi-part
/// patterns, also adds individual parts from the next incomplete copy
/// that still fit within the work area.
///
- private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries)
+ private List TilePattern(Pattern basePattern, NestDirection direction)
{
- var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries);
+ var copyDistance = FindPatternCopyDistance(basePattern, direction);
if (copyDistance <= 0)
return new List();
@@ -394,11 +322,10 @@ namespace OpenNest.Engine.Fill
private List FillGrid(Pattern pattern, NestDirection direction)
{
var perpAxis = PerpendicularAxis(direction);
- var boundaries = CreateBoundaries(pattern);
// Step 1: Tile along primary axis
var row = new List(pattern.Parts);
- row.AddRange(TilePattern(pattern, direction, boundaries));
+ row.AddRange(TilePattern(pattern, direction));
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a1, out var b1))
{
@@ -410,7 +337,7 @@ namespace OpenNest.Engine.Fill
// If primary tiling didn't produce copies, just tile along perpendicular
if (row.Count <= pattern.Parts.Count)
{
- row.AddRange(TilePattern(pattern, perpAxis, boundaries));
+ row.AddRange(TilePattern(pattern, perpAxis));
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a2, out var b2))
{
@@ -427,9 +354,8 @@ namespace OpenNest.Engine.Fill
rowPattern.Parts.AddRange(row);
rowPattern.UpdateBounds();
- var rowBoundaries = CreateBoundaries(rowPattern);
var gridResult = new List(rowPattern.Parts);
- gridResult.AddRange(TilePattern(rowPattern, perpAxis, rowBoundaries));
+ gridResult.AddRange(TilePattern(rowPattern, perpAxis));
if (HasOverlappingParts(gridResult, out var a3, out var b3))
{
@@ -481,9 +407,8 @@ namespace OpenNest.Engine.Fill
return seed;
var template = seed.Parts[0];
- var boundary = new PartBoundary(template, HalfSpacing);
- var copyDistance = FindCopyDistance(template, direction, boundary);
+ var copyDistance = FindCopyDistance(template, direction);
if (copyDistance <= 0)
return seed;