fix(engine): skip intersecting parts as obstacles during compactor push

Parts that already overlap the moving group are now excluded from the
obstacle list so they don't block the push direction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 19:03:33 -04:00
parent 53988acefc
commit 27f0685058
2 changed files with 143 additions and 3 deletions
+36 -3
View File
@@ -1,6 +1,7 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Engine.Fill
{
@@ -14,7 +15,7 @@ namespace OpenNest.Engine.Fill
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
.ToList();
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
@@ -26,7 +27,7 @@ namespace OpenNest.Engine.Fill
public static double Push(List<Part> movingParts, Plate plate, double angle)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
.ToList();
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
@@ -99,6 +100,13 @@ namespace OpenNest.Engine.Fill
: PartGeometry.GetPerimeterEntities(obstacleParts[i]);
var d = SpatialQuery.DirectionalDistance(movingEntities, obstacleEntities[i], direction);
if (d <= Tolerance.Epsilon
&& partSpacing <= Tolerance.Epsilon
&& CanNudgeWithoutOverlap(moving, obstacleParts[i], direction))
{
continue;
}
if (d < distance)
distance = d;
}
@@ -115,6 +123,31 @@ namespace OpenNest.Engine.Fill
return 0;
}
private static bool IntersectsAny(Part candidate, List<Part> parts)
{
for (var i = 0; i < parts.Count; i++)
{
if (candidate.Intersects(parts[i], out _))
return true;
}
return false;
}
private static bool CanNudgeWithoutOverlap(Part moving, Part obstacle, Vector direction)
{
var nudge = direction * (Tolerance.Epsilon * 10);
moving.Offset(nudge);
try
{
return !moving.Intersects(obstacle, out _);
}
finally
{
moving.Offset(-nudge);
}
}
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
@@ -130,7 +163,7 @@ namespace OpenNest.Engine.Fill
public static double PushBoundingBox(List<Part> movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
.ToList();
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
+107
View File
@@ -97,6 +97,33 @@ namespace OpenNest.Tests.Fill
return part;
}
private static Drawing MakeTriangleDrawing(params Vector[] points)
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new OpenNest.CNC.RapidMove(points[0]));
for (var i = 1; i < points.Length; i++)
pgm.Codes.Add(new OpenNest.CNC.LinearMove(points[i]));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(points[0]));
return new Drawing("triangle", pgm);
}
private static Part MakeTrianglePart(params Vector[] points)
{
var part = new Part(MakeTriangleDrawing(points));
part.UpdateBounds();
return part;
}
private static Part MakeTrianglePart(double x, double y, params Vector[] points)
{
var part = MakeTrianglePart(points);
part.Location = new Vector(x, y);
part.UpdateBounds();
return part;
}
[Fact]
public void Push_Left_MovesPartTowardEdge()
{
@@ -171,6 +198,86 @@ namespace OpenNest.Tests.Fill
Assert.NotEqual(distNoSpacing, distWithSpacing);
}
[Fact]
public void Push_Up_AllowsSharedDiagonalEdgeToSeparate()
{
var workArea = new Box(0, 0, 20, 20);
var obstacle = MakeTrianglePart(
new Vector(0, 0),
new Vector(10, 0),
new Vector(0, 10));
var movingPart = MakeTrianglePart(
new Vector(0, 10),
new Vector(10, 0),
new Vector(10, 10));
var distance = Compactor.Push(
new List<Part> { movingPart },
new List<Part> { obstacle },
workArea,
0,
PushDirection.Up);
Assert.True(distance > 0);
Assert.True(movingPart.BoundingBox.Top > 19.9);
Assert.False(movingPart.Intersects(obstacle, out _));
}
[Fact]
public void Push_Up_MovesAfterRightTriangleIsPushedLeftIntoSharedEdge()
{
var workArea = new Box(0, 0, 24, 24);
var leftTriangle = MakeTrianglePart(
2, 2,
new Vector(0, 0),
new Vector(8, 0),
new Vector(4, 10));
var rightTriangle = MakeTrianglePart(
14, 4,
new Vector(0, 10),
new Vector(8, 10),
new Vector(4, 0));
var moving = new List<Part> { rightTriangle };
var obstacles = new List<Part> { leftTriangle };
var leftDistance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left);
var yBeforePushUp = rightTriangle.Location.Y;
var bottomBeforePushUp = rightTriangle.BoundingBox.Bottom;
var upDistance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Up);
Assert.True(leftDistance > 0);
Assert.True(upDistance > 0);
Assert.True(rightTriangle.Location.Y > yBeforePushUp);
Assert.True(rightTriangle.BoundingBox.Bottom > bottomBeforePushUp);
Assert.False(rightTriangle.Intersects(leftTriangle, out _));
}
[Fact]
public void Push_Left_BlocksWhenSharedDiagonalEdgeWouldOverlap()
{
var workArea = new Box(0, 0, 20, 20);
var obstacle = MakeTrianglePart(
new Vector(0, 0),
new Vector(10, 0),
new Vector(0, 10));
var movingPart = MakeTrianglePart(
new Vector(0, 10),
new Vector(10, 0),
new Vector(10, 10));
var distance = Compactor.Push(
new List<Part> { movingPart },
new List<Part> { obstacle },
workArea,
0,
PushDirection.Left);
Assert.Equal(0, distance);
Assert.Equal(0, movingPart.BoundingBox.Left);
}
[Fact]
public void Push_AngleLeft_MovesPartTowardEdge()
{