diff --git a/OpenNest.Engine/Compactor.cs b/OpenNest.Engine/Compactor.cs
new file mode 100644
index 0000000..b5ff5ab
--- /dev/null
+++ b/OpenNest.Engine/Compactor.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using System.Linq;
+using OpenNest.Geometry;
+
+namespace OpenNest
+{
+ ///
+ /// Pushes a group of parts left and down to close gaps after placement.
+ /// Uses the same directional-distance logic as PlateView.PushSelected
+ /// but operates on Part objects directly.
+ ///
+ public static class Compactor
+ {
+ private const double ChordTolerance = 0.001;
+
+ ///
+ /// Compacts movingParts toward the bottom-left of the plate work area.
+ /// Everything already on the plate (excluding movingParts) is treated
+ /// as stationary obstacles.
+ ///
+ public static void Compact(List movingParts, Plate plate)
+ {
+ if (movingParts == null || movingParts.Count == 0)
+ return;
+
+ Push(movingParts, plate, PushDirection.Left);
+ Push(movingParts, plate, PushDirection.Down);
+ }
+
+ private static void Push(List movingParts, Plate plate, PushDirection direction)
+ {
+ var stationaryParts = plate.Parts
+ .Where(p => !movingParts.Contains(p))
+ .ToList();
+
+ var stationaryBoxes = new Box[stationaryParts.Count];
+
+ for (var i = 0; i < stationaryParts.Count; i++)
+ stationaryBoxes[i] = stationaryParts[i].BoundingBox;
+
+ var stationaryLines = new List[stationaryParts.Count];
+ var opposite = Helper.OppositeDirection(direction);
+ var halfSpacing = plate.PartSpacing / 2;
+ var isHorizontal = Helper.IsHorizontalDirection(direction);
+ var workArea = plate.WorkArea();
+
+ foreach (var moving in movingParts)
+ {
+ var distance = double.MaxValue;
+ var movingBox = moving.BoundingBox;
+
+ // Plate edge distance.
+ var edgeDist = Helper.EdgeDistance(movingBox, workArea, direction);
+ if (edgeDist > 0 && edgeDist < distance)
+ distance = edgeDist;
+
+ List movingLines = null;
+
+ for (var i = 0; i < stationaryBoxes.Length; i++)
+ {
+ var gap = Helper.DirectionalGap(movingBox, stationaryBoxes[i], direction);
+ if (gap < 0 || gap >= distance)
+ continue;
+
+ var perpOverlap = isHorizontal
+ ? movingBox.IsHorizontalTo(stationaryBoxes[i], out _)
+ : movingBox.IsVerticalTo(stationaryBoxes[i], out _);
+
+ if (!perpOverlap)
+ continue;
+
+ movingLines ??= halfSpacing > 0
+ ? Helper.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
+ : Helper.GetPartLines(moving, direction, ChordTolerance);
+
+ stationaryLines[i] ??= halfSpacing > 0
+ ? Helper.GetOffsetPartLines(stationaryParts[i], halfSpacing, opposite, ChordTolerance)
+ : Helper.GetPartLines(stationaryParts[i], opposite, ChordTolerance);
+
+ var d = Helper.DirectionalDistance(movingLines, stationaryLines[i], direction);
+ if (d < distance)
+ distance = d;
+ }
+
+ if (distance < double.MaxValue && distance > 0)
+ {
+ var offset = Helper.DirectionToOffset(direction, distance);
+ moving.Offset(offset);
+ }
+ }
+ }
+ }
+}