diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs
index 66af511..676f625 100644
--- a/OpenNest.Engine/Fill/Compactor.cs
+++ b/OpenNest.Engine/Fill/Compactor.cs
@@ -31,16 +31,16 @@ namespace OpenNest.Engine.Fill
.Where(p => !movingParts.Contains(p))
.ToList();
- var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
- return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
+ return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle);
}
///
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
///
public static double Push(List movingParts, List obstacleParts,
- Box workArea, double partSpacing, Vector direction)
+ Box workArea, double partSpacing, double angle)
{
+ var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
var opposite = -direction;
var obstacleBoxes = new Box[obstacleParts.Count];
@@ -104,8 +104,72 @@ namespace OpenNest.Engine.Fill
public static double Push(List movingParts, List obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
- var vector = SpatialQuery.DirectionToOffset(direction, 1.0);
- return Push(movingParts, obstacleParts, workArea, partSpacing, vector);
+ var obstacleBoxes = new Box[obstacleParts.Count];
+ var obstacleLines = new List[obstacleParts.Count];
+
+ for (var i = 0; i < obstacleParts.Count; i++)
+ obstacleBoxes[i] = obstacleParts[i].BoundingBox;
+
+ var opposite = SpatialQuery.OppositeDirection(direction);
+ var halfSpacing = partSpacing / 2;
+ var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
+ var distance = double.MaxValue;
+
+ foreach (var moving in movingParts)
+ {
+ var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
+ if (edgeDist <= 0)
+ distance = 0;
+ else if (edgeDist < distance)
+ distance = edgeDist;
+
+ var movingBox = moving.BoundingBox;
+ List movingLines = null;
+
+ for (var i = 0; i < obstacleBoxes.Length; i++)
+ {
+ // Use the reverse-direction gap to check if the obstacle is entirely
+ // behind the moving part. The forward gap (gap < 0) is unreliable for
+ // irregular shapes whose bounding boxes overlap even when the actual
+ // geometry still has a valid contact in the push direction.
+ var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
+ if (reverseGap > 0)
+ continue;
+
+ var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
+ if (gap >= distance)
+ continue;
+
+ var perpOverlap = isHorizontal
+ ? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
+ : movingBox.IsVerticalTo(obstacleBoxes[i], out _);
+
+ if (!perpOverlap)
+ continue;
+
+ movingLines ??= halfSpacing > 0
+ ? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
+ : PartGeometry.GetPartLines(moving, direction, ChordTolerance);
+
+ obstacleLines[i] ??= halfSpacing > 0
+ ? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
+ : PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
+
+ var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
+ if (d < distance)
+ distance = d;
+ }
+ }
+
+ if (distance < double.MaxValue && distance > 0)
+ {
+ var offset = SpatialQuery.DirectionToOffset(direction, distance);
+ foreach (var moving in movingParts)
+ moving.Offset(offset);
+ return distance;
+ }
+
+ return 0;
}
///
diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs
index e5ccd6f..40f7dee 100644
--- a/OpenNest.Engine/Fill/FillExtents.cs
+++ b/OpenNest.Engine/Fill/FillExtents.cs
@@ -1,10 +1,8 @@
-using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Linq;
using System.Threading;
namespace OpenNest.Engine.Fill
@@ -107,7 +105,7 @@ namespace OpenNest.Engine.Fill
private List BuildColumn(Part part1, Part part2, Box pairBbox)
{
- var pairParts = new List { (Part)part1.Clone(), (Part)part2.Clone() };
+ var column = new List { (Part)part1.Clone(), (Part)part2.Clone() };
// Find geometry-aware copy distance for the pair vertically.
var boundary1 = new PartBoundary(part1, halfSpacing);
@@ -128,11 +126,22 @@ namespace OpenNest.Engine.Fill
boundary1, boundary2, pairHeight);
if (copyDistance <= 0)
- return pairParts;
+ return column;
- var result = new List(pairParts);
- result.AddRange(FillHelpers.Tile(pairParts, workArea, copyDistance, NestDirection.Vertical, allowPartial: false));
- return result;
+ var count = 1;
+ while (true)
+ {
+ var nextBottom = pairBbox.Bottom + copyDistance * count;
+ if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon)
+ break;
+
+ var offset = new Vector(0, copyDistance * count);
+ column.Add(part1.CloneAtOffset(offset));
+ column.Add(part2.CloneAtOffset(offset));
+ count++;
+ }
+
+ return column;
}
private double FindVerticalCopyDistance(
@@ -315,10 +324,48 @@ namespace OpenNest.Engine.Fill
Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})");
+ // Build all columns.
var result = new List(column);
- result.AddRange(FillHelpers.Tile(column, workArea, copyDistance, NestDirection.Horizontal, allowPartial: true));
+
+ // Add the test column we already computed as column 2.
+ foreach (var part in testColumn)
+ {
+ if (IsWithinWorkArea(part))
+ result.Add(part);
+ }
+
+ // Tile additional columns at the copy distance.
+ var colIndex = 2;
+ while (!token.IsCancellationRequested)
+ {
+ var offset = new Vector(copyDistance * colIndex, 0);
+ var anyFit = false;
+
+ foreach (var part in column)
+ {
+ var clone = part.CloneAtOffset(offset);
+ if (IsWithinWorkArea(clone))
+ {
+ result.Add(clone);
+ anyFit = true;
+ }
+ }
+
+ if (!anyFit)
+ break;
+
+ colIndex++;
+ }
return result;
}
+
+ private bool IsWithinWorkArea(Part part)
+ {
+ return part.BoundingBox.Right <= workArea.Right + Tolerance.Epsilon &&
+ part.BoundingBox.Top <= workArea.Top + Tolerance.Epsilon &&
+ part.BoundingBox.Left >= workArea.Left - Tolerance.Epsilon &&
+ part.BoundingBox.Bottom >= workArea.Bottom - Tolerance.Epsilon;
+ }
}
}
diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs
index 98e88ed..674bff3 100644
--- a/OpenNest.Engine/Fill/FillLinear.cs
+++ b/OpenNest.Engine/Fill/FillLinear.cs
@@ -1,4 +1,3 @@
-using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
@@ -250,7 +249,57 @@ namespace OpenNest.Engine.Fill
private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries)
{
var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries);
- return FillHelpers.Tile(basePattern.Parts, WorkArea, copyDistance, direction, allowPartial: true);
+
+ if (copyDistance <= 0)
+ return new List();
+
+ var dim = GetDimension(basePattern.BoundingBox, direction);
+ var start = GetStart(basePattern.BoundingBox, direction);
+ var limit = GetLimit(direction);
+
+ var estimatedCopies = (int)((limit - start - dim) / copyDistance);
+ var result = new List(estimatedCopies * basePattern.Parts.Count);
+
+ var count = 1;
+
+ while (true)
+ {
+ var nextPos = start + copyDistance * count;
+
+ if (nextPos + dim > limit + Tolerance.Epsilon)
+ break;
+
+ var offset = MakeOffset(direction, copyDistance * count);
+
+ foreach (var part in basePattern.Parts)
+ result.Add(part.CloneAtOffset(offset));
+
+ count++;
+ }
+
+ // For multi-part patterns, try to place individual parts from the
+ // next copy that didn't fit as a whole. This handles cases where
+ // e.g. a 2-part pair only partially fits — one part may still be
+ // within the work area even though the full pattern exceeds it.
+ if (basePattern.Parts.Count > 1)
+ {
+ var offset = MakeOffset(direction, copyDistance * count);
+
+ foreach (var basePart in basePattern.Parts)
+ {
+ var part = basePart.CloneAtOffset(offset);
+
+ if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon &&
+ part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon &&
+ part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon &&
+ part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon)
+ {
+ result.Add(part);
+ }
+ }
+ }
+
+ return result;
}
///
diff --git a/OpenNest.Tests/CompactorTests.cs b/OpenNest.Tests/CompactorTests.cs
index 8d3e2c5..a205d24 100644
--- a/OpenNest.Tests/CompactorTests.cs
+++ b/OpenNest.Tests/CompactorTests.cs
@@ -109,9 +109,8 @@ namespace OpenNest.Tests
var moving = new List { part };
var obstacles = new List();
- // direction = left
- var direction = new Vector(System.Math.Cos(System.Math.PI), System.Math.Sin(System.Math.PI));
- var distance = Compactor.Push(moving, obstacles, workArea, 0, direction);
+ // angle = π = push left
+ var distance = Compactor.Push(moving, obstacles, workArea, 0, System.Math.PI);
Assert.True(distance > 0);
Assert.True(part.BoundingBox.Left < 1);
@@ -125,10 +124,8 @@ namespace OpenNest.Tests
var moving = new List { part };
var obstacles = new List();
- // direction = down
- var angle = 3 * System.Math.PI / 2;
- var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
- var distance = Compactor.Push(moving, obstacles, workArea, 0, direction);
+ // angle = 3π/2 = push down
+ var distance = Compactor.Push(moving, obstacles, workArea, 0, 3 * System.Math.PI / 2);
Assert.True(distance > 0);
Assert.True(part.BoundingBox.Bottom < 1);
diff --git a/OpenNest/Forms/PatternTileForm.cs b/OpenNest/Forms/PatternTileForm.cs
index 5d2a52d..acbde4d 100644
--- a/OpenNest/Forms/PatternTileForm.cs
+++ b/OpenNest/Forms/PatternTileForm.cs
@@ -213,14 +213,12 @@ namespace OpenNest.Forms
if (System.Math.Sqrt(dx * dx + dy * dy) < 0.01)
continue;
- var direction = new Vector(dx, dy);
- var len = System.Math.Sqrt(dx * dx + dy * dy);
- if (len > 0) direction = new Vector(dx / len, dy / len);
+ var angle = System.Math.Atan2(dy, dx);
var single = new List { part };
var obstacles = parts.Where(p => p != part).ToList();
totalMoved += Compactor.Push(single, obstacles,
- syntheticWorkArea, spacing, direction);
+ syntheticWorkArea, spacing, angle);
}
if (totalMoved < 0.01)