Files
OpenNest/OpenNest.Tests/Fill/CompactorTests.cs
T
ajandClaude Opus 5.5 d133f6879b fix(geometry): leave leads out of push and collision material
A part with lead-ins applied could not be pushed away once another part
touched it. PartGeometry kept LEADIN/LEADOUT entities as material, so the
leads chained onto the perimeter as open spurs and the spacing offset no
longer closed. The slide-contact classifier cannot tell the material side
of an unclosed boundary and blocks every contact, so parts resting against
it could not move up or right off it. Leads are cut into scrap, and the
spacing review, overlap analyzer and support checks already drop them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:44:43 -05:00

682 lines
27 KiB
C#

using System.Collections.Generic;
using OpenNest;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using Xunit;
namespace OpenNest.Tests.Fill
{
public class CompactorTests
{
[Fact]
public void DirectionalDistance_ArcVsInclinedLine_DoesNotOverPush()
{
// Arc (top semicircle) pushed upward toward a 45° inclined line.
// The critical angle on the arc gives a shorter distance than any
// sampled vertex (endpoints + cardinal extremes).
var arc = new Arc(5, 0, 2, 0, System.Math.PI);
var line = new Line(new Vector(3, 4), new Vector(7, 6));
var moving = new List<Entity> { arc };
var stationary = new List<Entity> { line };
var direction = new Vector(0, 1); // push up
var dist = SpatialQuery.DirectionalDistance(moving, stationary, direction);
// Move the arc up by the computed distance, then verify no overlap.
// The topmost reachable point on the arc at the critical angle θ ≈ 2.034
// (between π/2 and π) should just touch the line.
Assert.True(dist < double.MaxValue, "Should find a finite distance");
Assert.True(dist > 0, "Should be a positive distance");
// Verify: after moving, the closest point on the arc should be within
// tolerance of the line, not past it.
var theta = System.Math.Atan2(line.pt2.X - line.pt1.X, -(line.pt2.Y - line.pt1.Y));
theta = OpenNest.Math.Angle.NormalizeRad(theta + System.Math.PI);
var qx = arc.Center.X + arc.Radius * System.Math.Cos(theta);
var qy = arc.Center.Y + arc.Radius * System.Math.Sin(theta) + dist;
// The moved point should be on or just touching the line, not past it.
// Line equation: (y - 4) / (x - 3) = (6 - 4) / (7 - 3) = 0.5
// y = 0.5x + 2.5
var lineYAtQx = 0.5 * qx + 2.5;
Assert.True(
qy <= lineYAtQx + 0.001,
$"Arc point ({qx:F4}, {qy:F4}) should not be past line (line Y={lineYAtQx:F4} at X={qx:F4}). "
+ $"dist={dist:F6}, overshot by {qy - lineYAtQx:F6}"
);
}
[Fact]
public void DirectionalDistance_ArcVsInclinedLine_BetterThanVertexSampling()
{
// Same geometry — verify the analytical Phase 3 finds a shorter
// distance than the Phase 1/2 vertex sampling alone would.
var arc = new Arc(5, 0, 2, 0, System.Math.PI);
var line = new Line(new Vector(3, 4), new Vector(7, 6));
// Phase 1/2 vertex-only distance: sample arc endpoints + cardinal extreme.
var vertices = new[]
{
new Vector(7, 0), // arc endpoint θ=0
new Vector(3, 0), // arc endpoint θ=π
new Vector(5, 2), // cardinal extreme θ=π/2
};
var vertexMin = double.MaxValue;
foreach (var v in vertices)
{
var d = SpatialQuery.RayEdgeDistance(
v.X,
v.Y,
line.pt1.X,
line.pt1.Y,
line.pt2.X,
line.pt2.Y,
0,
1
);
if (d < vertexMin)
vertexMin = d;
}
// Full directional distance (includes Phase 3 arc-to-line).
var moving = new List<Entity> { arc };
var stationary = new List<Entity> { line };
var fullDist = SpatialQuery.DirectionalDistance(moving, stationary, new Vector(0, 1));
Assert.True(
fullDist < vertexMin,
$"Full distance ({fullDist:F6}) should be less than vertex-only ({vertexMin:F6})"
);
}
private static Part MakeRectPart(double x, double y, double w, double h)
{
var drawing = TestHelpers.MakeRectDrawing(w, h);
var part = new Part(drawing) { Location = new Vector(x, y) };
part.UpdateBounds();
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()
{
var workArea = new Box(0, 0, 100, 100);
var part = MakeRectPart(50, 0, 10, 10);
var moving = new List<Part> { part };
var obstacles = new List<Part>();
var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left);
Assert.True(distance > 0);
Assert.True(part.BoundingBox.Left < 1);
}
[Fact]
public void Push_Left_StopsAtObstacle()
{
var workArea = new Box(0, 0, 100, 100);
var obstacle = MakeRectPart(0, 0, 10, 10);
var part = MakeRectPart(50, 0, 10, 10);
var moving = new List<Part> { part };
var obstacles = new List<Part> { obstacle };
Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left);
Assert.True(part.BoundingBox.Left >= obstacle.BoundingBox.Right - 0.1);
}
[Fact]
public void Push_Down_MovesPartTowardEdge()
{
var workArea = new Box(0, 0, 100, 100);
var part = MakeRectPart(0, 50, 10, 10);
var moving = new List<Part> { part };
var obstacles = new List<Part>();
var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Down);
Assert.True(distance > 0);
Assert.True(part.BoundingBox.Bottom < 1);
}
[Fact]
public void Push_ReturnsZero_WhenAlreadyAtEdge()
{
var workArea = new Box(0, 0, 100, 100);
var part = MakeRectPart(0, 0, 10, 10);
var moving = new List<Part> { part };
var obstacles = new List<Part>();
var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left);
Assert.Equal(0, distance);
}
[Fact]
public void Push_WithSpacing_MovesLessThanWithout()
{
var workArea = new Box(0, 0, 100, 100);
// Push without spacing.
var obstacle1 = MakeRectPart(0, 0, 10, 10);
var part1 = MakeRectPart(50, 0, 10, 10);
var distNoSpacing = Compactor.Push(
new List<Part> { part1 },
new List<Part> { obstacle1 },
workArea,
0,
PushDirection.Left
);
// Push with spacing.
var obstacle2 = MakeRectPart(0, 0, 10, 10);
var part2 = MakeRectPart(50, 0, 10, 10);
var distWithSpacing = Compactor.Push(
new List<Part> { part2 },
new List<Part> { obstacle2 },
workArea,
2,
PushDirection.Left
);
// Spacing should cause the part to stop at a different position than without spacing.
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(TestHelpers.MakeRectDrawing(10, 10), angle);
obstacle.Offset(20, 20);
var moving = Part.CreateAtOrigin(TestHelpers.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(
TestHelpers.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);
}
[Theory]
[InlineData(PushDirection.Right)]
[InlineData(PushDirection.Up)]
[InlineData(PushDirection.Down)]
public void Push_WithSpacing_ContactFromPreviousPushDoesNotBlockOtherDirections(
PushDirection next
)
{
var workArea = new Box(0, 0, 100, 100);
var obstacle = MakeRectPart(20, 40, 10, 10);
var moving = MakeRectPart(60, 40, 10, 10);
var parts = new List<Part> { moving };
var obstacles = new List<Part> { obstacle };
Assert.True(Compactor.Push(parts, obstacles, workArea, 2, PushDirection.Left) > 0);
Assert.Equal(32, moving.BoundingBox.Left, 7);
var before = moving.Location;
var distance = Compactor.Push(parts, obstacles, workArea, 2, next);
Assert.True(distance > 1, $"Push {next} after contact moved only {distance:R}");
Assert.NotEqual(before, moving.Location);
AssertClearance(moving, obstacle, 2);
}
[Theory]
[InlineData(PushDirection.Right)]
[InlineData(PushDirection.Up)]
public void Push_WithSpacing_ContactWithLeadInPartDoesNotBlockOtherDirections(
PushDirection next
)
{
// Leads are cut into scrap, not material: chained into the obstacle's perimeter
// they left its offset contour open, so every contact with it blocked.
var workArea = new Box(0, 0, 100, 100);
var obstacle = MakeRectPart(20, 20, 40, 10);
obstacle.ApplyLeadIns(new OpenNest.CNC.CuttingStrategy.CuttingParameters
{
ExternalLeadIn = new OpenNest.CNC.CuttingStrategy.LineLeadIn { Length = 0.25 },
ExternalLeadOut = new OpenNest.CNC.CuttingStrategy.LineLeadOut { Length = 0.25 },
}, new Vector(0, 0));
var moving = MakeRectPart(30, 60, 5, 5);
var parts = new List<Part> { moving };
var obstacles = new List<Part> { obstacle };
Assert.True(Compactor.Push(parts, obstacles, workArea, 2, PushDirection.Down) > 0);
Assert.Equal(32, moving.BoundingBox.Bottom, 7);
var distance = Compactor.Push(parts, obstacles, workArea, 2, next);
Assert.True(distance > 1, $"Push {next} after contact moved only {distance:R}");
AssertClearance(moving, obstacle, 2);
}
[Fact]
public void Push_WithSpacing_ContactStillBlocksTheSameDirection()
{
var workArea = new Box(0, 0, 100, 100);
var obstacle = MakeRectPart(20, 40, 10, 10);
var moving = MakeRectPart(60, 40, 10, 10);
var parts = new List<Part> { moving };
var obstacles = new List<Part> { obstacle };
Compactor.Push(parts, obstacles, workArea, 2, PushDirection.Left);
var distance = Compactor.Push(parts, obstacles, workArea, 2, PushDirection.Left);
Assert.Equal(0, distance);
Assert.Equal(32, moving.BoundingBox.Left, 7);
AssertClearance(moving, obstacle, 2);
}
[Theory]
[InlineData(0)]
[InlineData(2)]
public void Push_SlidingAlongWall_StopsAtItsLaterHook(double spacing)
{
var hook = MakeTrianglePart(new Vector(10, 10), new Vector(20, 10),
new Vector(20, 50), new Vector(50, 50), new Vector(50, 60), new Vector(10, 60));
var moving = MakeRectPart(20 + spacing, 20, 5, 5);
var distance = Compactor.Push(new List<Part> { moving }, new List<Part> { hook },
new Box(0, 0, 100, 100), spacing, PushDirection.Up);
Assert.Equal(25 - spacing, distance, 7);
Assert.False(moving.Intersects(hook, out _));
if (spacing > 0)
AssertClearance(moving, hook, spacing);
}
[Theory]
[InlineData(0, false)]
[InlineData(2, false)]
[InlineData(0, true)]
[InlineData(2, true)]
public void Push_InsideStationaryHole_CannotPassThroughItsWall(double spacing, bool plateEntry)
{
var program = TestHelpers.MakeRectDrawing(60, 60).Program;
program.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(10, 10)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(50, 10)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(50, 50)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(10, 50)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(10, 10)));
var frame = new Part(new Drawing("frame", program));
var moving = MakeRectPart(10 + spacing, 20, 5, 5);
var plate = new Plate(100, 100) { PartSpacing = spacing };
plate.Parts.Add(frame);
plate.Parts.Add(moving);
var distance = plateEntry
? Compactor.Push(new List<Part> { moving }, plate, PushDirection.Right)
: Compactor.Push(new List<Part> { moving }, new List<Part> { frame },
new Box(0, 0, 100, 100), spacing, PushDirection.Right);
Assert.Equal(35 - 2 * spacing, distance, 7);
// Independent rectangular-hole oracle; Part.Intersects ignores cutouts.
Assert.Equal(50 - spacing, moving.BoundingBox.Right, 7);
Assert.InRange(moving.BoundingBox.Bottom, 10 + spacing, 50 - spacing);
Assert.InRange(moving.BoundingBox.Top, 10 + spacing, 50 - spacing);
if (spacing > 0)
AssertClearance(moving, frame, spacing);
}
[Theory]
[InlineData(0)]
[InlineData(0.25)]
public void Push_RestingInHoleOfFrameWithUnclosedSourceContour_SlidesAwayFromFloor(
double spacing
)
{
// PEP-rounded contours can miss closing by 1e-6, inside ShapeBuilder's chaining
// tolerance. The slide classifier must chain them the same way, or the frame's
// material sides are unknown and the floor contact blocks every direction.
var program = new OpenNest.CNC.Program();
program.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(60, 0)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(60, 60)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 60)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0.000001)));
program.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(10, 10)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(50, 10)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(50, 50)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(10, 50)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(10, 10)));
var frame = new Part(new Drawing("frame", program));
var moving = MakeRectPart(10 + spacing, 10 + spacing, 5, 5);
var plate = new Plate(100, 100) { PartSpacing = spacing };
plate.Parts.Add(frame);
plate.Parts.Add(moving);
var distance = Compactor.Push(new List<Part> { moving }, plate, PushDirection.Up);
Assert.Equal(35 - 2 * spacing, distance, 7);
Assert.Equal(50 - spacing, moving.BoundingBox.Top, 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]
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()
{
var workArea = new Box(0, 0, 100, 100);
var part = MakeRectPart(50, 0, 10, 10);
var moving = new List<Part> { part };
var obstacles = new List<Part>();
// 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);
}
[Fact]
public void Push_AngleDown_MovesPartTowardEdge()
{
var workArea = new Box(0, 0, 100, 100);
var part = MakeRectPart(0, 50, 10, 10);
var moving = new List<Part> { part };
var obstacles = new List<Part>();
// 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);
}
[Fact]
public void SettlePlacement_RepeatsAfterVerticalMovementOpensHorizontalPath()
{
var plate = new Plate(100, 100);
plate.Parts.Add(MakeRectPart(20, 20, 20, 30));
var moving = MakeRectPart(60, 35, 10, 10);
Compactor.SettlePlacement(new List<Part> { moving }, plate,
PushDirection.Left, PushDirection.Down);
Assert.Equal(0, moving.BoundingBox.Left, 6);
Assert.Equal(0, moving.BoundingBox.Bottom, 6);
Assert.False(moving.Intersects(plate.Parts[0], out _));
}
[Fact]
public void SettlePlacement_MovesCopiedGroupTogether()
{
var plate = new Plate(100, 100);
var left = MakeRectPart(40, 40, 5, 5);
var right = MakeRectPart(50, 40, 5, 5);
var originalGap = right.Location.X - left.Location.X;
Compactor.SettlePlacement(new List<Part> { left, right }, plate,
PushDirection.Left, PushDirection.Down);
Assert.Equal(0, left.BoundingBox.Left, 6);
Assert.Equal(0, left.BoundingBox.Bottom, 6);
Assert.Equal(originalGap, right.Location.X - left.Location.X, 6);
}
[Fact]
public void SettlePlacement_GeometryRepeatsWhenInitialBoxPassIsSkipped()
{
var plate = new Plate(100, 100);
plate.Parts.Add(MakeRectPart(20, 20, 20, 30));
// Its bounding box covers the start, but its material is above it.
plate.Parts.Add(MakeTrianglePart(new Vector(0, 100),
new Vector(100, 100), new Vector(100, 40)));
var moving = MakeRectPart(60, 35, 10, 10);
Compactor.SettlePlacement(new List<Part> { moving }, plate,
PushDirection.Left, PushDirection.Down);
Assert.Equal(0, moving.BoundingBox.Left, 6);
Assert.Equal(0, moving.BoundingBox.Bottom, 6);
Assert.All(plate.Parts, obstacle => Assert.False(moving.Intersects(obstacle, out _)));
}
[Fact]
public void SettlePlacement_SkipsBoxPassWhenAlreadyInsideObstacleBounds()
{
// Triangle material sits below the diagonal; the free pocket at (25,25)
// is within its bounding box. A box-only slide would falsely block it.
var plate = new Plate(100, 100);
plate.Parts.Add(MakeTrianglePart(new Vector(0, 0),
new Vector(40, 0), new Vector(0, 40)));
var moving = MakeRectPart(25, 25, 5, 5);
var start = moving.Location;
Compactor.SettlePlacement(new List<Part> { moving }, plate,
PushDirection.Left, PushDirection.Down, maxIterations: 0);
Assert.Equal(start, moving.Location);
}
[Fact]
public void PushBoundingBox_Left_MovesPartTowardEdge()
{
var workArea = new Box(0, 0, 100, 100);
var part = MakeRectPart(50, 0, 10, 10);
var moving = new List<Part> { part };
var obstacles = new List<Part>();
var distance = Compactor.PushBoundingBox(
moving,
obstacles,
workArea,
0,
PushDirection.Left
);
Assert.True(distance > 0);
Assert.True(part.BoundingBox.Left < 1);
}
[Fact]
public void PushBoundingBox_StopsAtObstacle()
{
var workArea = new Box(0, 0, 100, 100);
var obstacle = MakeRectPart(0, 0, 10, 10);
var part = MakeRectPart(50, 0, 10, 10);
var moving = new List<Part> { part };
var obstacles = new List<Part> { obstacle };
Compactor.PushBoundingBox(moving, obstacles, workArea, 0, PushDirection.Left);
Assert.True(part.BoundingBox.Left >= obstacle.BoundingBox.Right - 0.1);
}
}
}