Fix push broad-phase checks to preserve part spacing
This commit is contained in:
@@ -43,12 +43,16 @@ namespace OpenNest.Engine.Fill
|
|||||||
var opposite = -direction;
|
var opposite = -direction;
|
||||||
|
|
||||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||||
|
var obstacleSpacingBoxes = new Box[obstacleParts.Count];
|
||||||
var obstacleEntities = new List<Entity>[obstacleParts.Count];
|
var obstacleEntities = new List<Entity>[obstacleParts.Count];
|
||||||
|
var halfSpacing = System.Math.Max(0, partSpacing) / 2;
|
||||||
|
|
||||||
for (var i = 0; i < obstacleParts.Count; i++)
|
for (var i = 0; i < obstacleParts.Count; i++)
|
||||||
|
{
|
||||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||||
|
obstacleSpacingBoxes[i] = SpacingBounds(obstacleBoxes[i], halfSpacing);
|
||||||
|
}
|
||||||
|
|
||||||
var halfSpacing = partSpacing / 2;
|
|
||||||
var distance = double.MaxValue;
|
var distance = double.MaxValue;
|
||||||
|
|
||||||
foreach (var moving in movingParts)
|
foreach (var moving in movingParts)
|
||||||
@@ -74,17 +78,21 @@ namespace OpenNest.Engine.Fill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broad-phase bounds must enclose the spacing-offset contours.
|
||||||
|
// Raw bounds can miss near passes and overestimate the safe travel.
|
||||||
|
var movingSpacingBox = SpacingBounds(movingBox, halfSpacing);
|
||||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||||
{
|
{
|
||||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
var obstacleSpacingBox = obstacleSpacingBoxes[i];
|
||||||
|
var reverseGap = SpatialQuery.DirectionalGap(movingSpacingBox, obstacleSpacingBox, opposite);
|
||||||
if (reverseGap > 0)
|
if (reverseGap > 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
var gap = SpatialQuery.DirectionalGap(movingSpacingBox, obstacleSpacingBox, direction);
|
||||||
if (gap >= distance)
|
if (gap >= distance)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
|
if (!SpatialQuery.PerpendicularOverlap(movingSpacingBox, obstacleSpacingBox, direction))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
movingEntities ??= halfSpacing > 0
|
movingEntities ??= halfSpacing > 0
|
||||||
@@ -123,6 +131,12 @@ namespace OpenNest.Engine.Fill
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Box SpacingBounds(Box box, double spacing)
|
||||||
|
{
|
||||||
|
return new Box(box.Left - spacing, box.Bottom - spacing,
|
||||||
|
box.Length + 2 * spacing, box.Width + 2 * spacing);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IntersectsAny(Part candidate, List<Part> parts)
|
private static bool IntersectsAny(Part candidate, List<Part> parts)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < parts.Count; i++)
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
|||||||
@@ -198,6 +198,81 @@ namespace OpenNest.Tests.Fill
|
|||||||
Assert.NotEqual(distNoSpacing, distWithSpacing);
|
Assert.NotEqual(distNoSpacing, distWithSpacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(15)]
|
||||||
|
[InlineData(30)]
|
||||||
|
[InlineData(45)]
|
||||||
|
[InlineData(60)]
|
||||||
|
[InlineData(90)]
|
||||||
|
public void Push_RotatedParts_PreservesSpacingOnRepeatedPushes(double degrees)
|
||||||
|
{
|
||||||
|
var angle = OpenNest.Math.Angle.ToRadians(degrees);
|
||||||
|
var obstacle = Part.CreateAtOrigin(MakeRectDrawing(10, 10), angle);
|
||||||
|
obstacle.Offset(20, 20);
|
||||||
|
var moving = Part.CreateAtOrigin(MakeRectDrawing(10, 10), angle);
|
||||||
|
moving.Offset(60, 20);
|
||||||
|
var parts = new List<Part> { moving };
|
||||||
|
var obstacles = new List<Part> { obstacle };
|
||||||
|
var workArea = new Box(0, 0, 100, 100);
|
||||||
|
var spacing = 2.0;
|
||||||
|
|
||||||
|
var distance = Compactor.Push(parts, obstacles, workArea, spacing, PushDirection.Left);
|
||||||
|
Assert.True(distance > 0);
|
||||||
|
AssertClearance(moving, obstacle, spacing);
|
||||||
|
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
Compactor.Push(parts, obstacles, workArea, spacing, PushDirection.Left);
|
||||||
|
AssertClearance(moving, obstacle, spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(15)]
|
||||||
|
[InlineData(45)]
|
||||||
|
[InlineData(75)]
|
||||||
|
public void Push_WithSpacing_StopsBeforeNearMissOutsideRawBounds(double degrees)
|
||||||
|
{
|
||||||
|
var obstacle = MakeRectPart(20, 20, 10, 10);
|
||||||
|
var moving = Part.CreateAtOrigin(MakeRectDrawing(10, 10), OpenNest.Math.Angle.ToRadians(degrees));
|
||||||
|
moving.Offset(60, 31);
|
||||||
|
|
||||||
|
Compactor.Push(new List<Part> { moving }, new List<Part> { obstacle },
|
||||||
|
new Box(0, 0, 100, 100), 2, PushDirection.Left);
|
||||||
|
|
||||||
|
// Must stop at the first clearance boundary, not pass the obstacle
|
||||||
|
// and finish in a clear position on the far side.
|
||||||
|
Assert.True(moving.BoundingBox.Left > obstacle.BoundingBox.Left);
|
||||||
|
AssertClearance(moving, obstacle, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Push_WithSpacing_ObstacleClearanceWinsOverCloserPlateEdge()
|
||||||
|
{
|
||||||
|
var obstacle = MakeRectPart(20, 20, 10, 10);
|
||||||
|
var moving = MakeRectPart(60, 20, 10, 10);
|
||||||
|
|
||||||
|
Compactor.Push(new List<Part> { moving }, new List<Part> { obstacle },
|
||||||
|
new Box(31, 0, 100, 100), 2, PushDirection.Left);
|
||||||
|
|
||||||
|
AssertClearance(moving, obstacle, 2);
|
||||||
|
Assert.Equal(32, moving.BoundingBox.Left, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertClearance(Part moving, Part obstacle, double spacing)
|
||||||
|
{
|
||||||
|
var clearance = double.MaxValue;
|
||||||
|
foreach (var a in PartGeometry.GetPartLines(moving))
|
||||||
|
foreach (var b in PartGeometry.GetPartLines(obstacle))
|
||||||
|
{
|
||||||
|
Assert.False(Intersect.Intersects(a, b, out _));
|
||||||
|
clearance = System.Math.Min(clearance, a.StartPoint.DistanceTo(b.ClosestPointTo(a.StartPoint)));
|
||||||
|
clearance = System.Math.Min(clearance, b.StartPoint.DistanceTo(a.ClosestPointTo(b.StartPoint)));
|
||||||
|
}
|
||||||
|
Assert.True(clearance >= spacing - 1e-7, $"Clearance {clearance:R} is less than spacing {spacing:R}");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Push_Up_AllowsSharedDiagonalEdgeToSeparate()
|
public void Push_Up_AllowsSharedDiagonalEdgeToSeparate()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ OpenNest takes your part drawings, lets you define your sheet (plate) sizes, and
|
|||||||
| **Pluggable Engines** | Default multi-phase, Vertical Remnant, Horizontal Remnant, plus custom plugin DLLs |
|
| **Pluggable Engines** | Default multi-phase, Vertical Remnant, Horizontal Remnant, plus custom plugin DLLs |
|
||||||
| **Fill Strategies** | Linear grid, interlocking pairs, rectangle best-fit, and extents-based tiling |
|
| **Fill Strategies** | Linear grid, interlocking pairs, rectangle best-fit, and extents-based tiling |
|
||||||
| **Best-Fit Pair Nesting** | NFP-based pair evaluation finds tight interlocking orientations between parts |
|
| **Best-Fit Pair Nesting** | NFP-based pair evaluation finds tight interlocking orientations between parts |
|
||||||
| **Gravity Compaction** | Polygon-based directional push to close gaps after filling |
|
| **Gravity Compaction** | Geometry-based directional push that preserves part spacing, including rotated parts and near passes between outlines |
|
||||||
| **Part Rotation** | Automatic angle sweep to find better fits across allowed orientations |
|
| **Part Rotation** | Automatic angle sweep to find better fits across allowed orientations |
|
||||||
| **Multi-Plate Support** | Manage multiple plates of different sizes and materials in one nest |
|
| **Multi-Plate Support** | Manage multiple plates of different sizes and materials in one nest |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user