diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs
index 676f625..66af511 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();
- return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle);
+ var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
+ return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
///
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
///
public static double Push(List movingParts, List obstacleParts,
- Box workArea, double partSpacing, double angle)
+ Box workArea, double partSpacing, Vector direction)
{
- var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
var opposite = -direction;
var obstacleBoxes = new Box[obstacleParts.Count];
@@ -104,72 +104,8 @@ namespace OpenNest.Engine.Fill
public static double Push(List movingParts, List obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
- 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;
+ var vector = SpatialQuery.DirectionToOffset(direction, 1.0);
+ return Push(movingParts, obstacleParts, workArea, partSpacing, vector);
}
///
diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs
index 40f7dee..e5ccd6f 100644
--- a/OpenNest.Engine/Fill/FillExtents.cs
+++ b/OpenNest.Engine/Fill/FillExtents.cs
@@ -1,8 +1,10 @@
+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
@@ -105,7 +107,7 @@ namespace OpenNest.Engine.Fill
private List BuildColumn(Part part1, Part part2, Box pairBbox)
{
- var column = new List { (Part)part1.Clone(), (Part)part2.Clone() };
+ var pairParts = new List { (Part)part1.Clone(), (Part)part2.Clone() };
// Find geometry-aware copy distance for the pair vertically.
var boundary1 = new PartBoundary(part1, halfSpacing);
@@ -126,22 +128,11 @@ namespace OpenNest.Engine.Fill
boundary1, boundary2, pairHeight);
if (copyDistance <= 0)
- return column;
+ return pairParts;
- 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;
+ var result = new List(pairParts);
+ result.AddRange(FillHelpers.Tile(pairParts, workArea, copyDistance, NestDirection.Vertical, allowPartial: false));
+ return result;
}
private double FindVerticalCopyDistance(
@@ -324,48 +315,10 @@ 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);
-
- // 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++;
- }
+ result.AddRange(FillHelpers.Tile(column, workArea, copyDistance, NestDirection.Horizontal, allowPartial: true));
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 674bff3..98e88ed 100644
--- a/OpenNest.Engine/Fill/FillLinear.cs
+++ b/OpenNest.Engine/Fill/FillLinear.cs
@@ -1,3 +1,4 @@
+using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
@@ -249,57 +250,7 @@ namespace OpenNest.Engine.Fill
private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries)
{
var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries);
-
- 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;
+ return FillHelpers.Tile(basePattern.Parts, WorkArea, copyDistance, direction, allowPartial: true);
}
///
diff --git a/OpenNest.Tests/CompactorTests.cs b/OpenNest.Tests/CompactorTests.cs
index a205d24..8d3e2c5 100644
--- a/OpenNest.Tests/CompactorTests.cs
+++ b/OpenNest.Tests/CompactorTests.cs
@@ -109,8 +109,9 @@ namespace OpenNest.Tests
var moving = new List { part };
var obstacles = new List();
- // angle = π = push left
- var distance = Compactor.Push(moving, obstacles, workArea, 0, System.Math.PI);
+ // 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);
Assert.True(distance > 0);
Assert.True(part.BoundingBox.Left < 1);
@@ -124,8 +125,10 @@ namespace OpenNest.Tests
var moving = new List { part };
var obstacles = new List();
- // angle = 3π/2 = push down
- var distance = Compactor.Push(moving, obstacles, workArea, 0, 3 * System.Math.PI / 2);
+ // 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);
Assert.True(distance > 0);
Assert.True(part.BoundingBox.Bottom < 1);
diff --git a/OpenNest/Forms/PatternTileForm.cs b/OpenNest/Forms/PatternTileForm.cs
index acbde4d..5d2a52d 100644
--- a/OpenNest/Forms/PatternTileForm.cs
+++ b/OpenNest/Forms/PatternTileForm.cs
@@ -213,12 +213,14 @@ namespace OpenNest.Forms
if (System.Math.Sqrt(dx * dx + dy * dy) < 0.01)
continue;
- var angle = System.Math.Atan2(dy, dx);
+ 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 single = new List { part };
var obstacles = parts.Where(p => p != part).ToList();
totalMoved += Compactor.Push(single, obstacles,
- syntheticWorkArea, spacing, angle);
+ syntheticWorkArea, spacing, direction);
}
if (totalMoved < 0.01)