fix(engine): Compactor treats pushed parts as obstacles for subsequent pushes

Previously each moving part only checked against the original stationary
set. Parts pushed earlier in the loop were invisible to later parts,
causing overlaps (utilization > 100%). Now each pushed part is added to
the obstacle set so subsequent parts collide correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 12:54:25 -04:00
parent 56592f909a
commit ef12cf2966
+28 -13
View File
@@ -29,16 +29,20 @@ namespace OpenNest
private static void Push(List<Part> movingParts, Plate plate, PushDirection direction) private static void Push(List<Part> movingParts, Plate plate, PushDirection direction)
{ {
var stationaryParts = plate.Parts // Start with parts already on the plate (excluding the moving group).
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p)) .Where(p => !movingParts.Contains(p))
.ToList(); .ToList();
var stationaryBoxes = new Box[stationaryParts.Count]; var obstacleBoxes = new List<Box>(obstacleParts.Count + movingParts.Count);
var obstacleLines = new List<List<Line>>(obstacleParts.Count + movingParts.Count);
for (var i = 0; i < stationaryParts.Count; i++) for (var i = 0; i < obstacleParts.Count; i++)
stationaryBoxes[i] = stationaryParts[i].BoundingBox; {
obstacleBoxes.Add(obstacleParts[i].BoundingBox);
obstacleLines.Add(null); // lazy
}
var stationaryLines = new List<Line>[stationaryParts.Count];
var opposite = Helper.OppositeDirection(direction); var opposite = Helper.OppositeDirection(direction);
var halfSpacing = plate.PartSpacing / 2; var halfSpacing = plate.PartSpacing / 2;
var isHorizontal = Helper.IsHorizontalDirection(direction); var isHorizontal = Helper.IsHorizontalDirection(direction);
@@ -56,15 +60,15 @@ namespace OpenNest
List<Line> movingLines = null; List<Line> movingLines = null;
for (var i = 0; i < stationaryBoxes.Length; i++) for (var i = 0; i < obstacleBoxes.Count; i++)
{ {
var gap = Helper.DirectionalGap(movingBox, stationaryBoxes[i], direction); var gap = Helper.DirectionalGap(movingBox, obstacleBoxes[i], direction);
if (gap < 0 || gap >= distance) if (gap < 0 || gap >= distance)
continue; continue;
var perpOverlap = isHorizontal var perpOverlap = isHorizontal
? movingBox.IsHorizontalTo(stationaryBoxes[i], out _) ? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
: movingBox.IsVerticalTo(stationaryBoxes[i], out _); : movingBox.IsVerticalTo(obstacleBoxes[i], out _);
if (!perpOverlap) if (!perpOverlap)
continue; continue;
@@ -73,11 +77,17 @@ namespace OpenNest
? Helper.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance) ? Helper.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
: Helper.GetPartLines(moving, direction, ChordTolerance); : Helper.GetPartLines(moving, direction, ChordTolerance);
stationaryLines[i] ??= halfSpacing > 0 var obstaclePart = i < obstacleParts.Count ? obstacleParts[i] : null;
? Helper.GetOffsetPartLines(stationaryParts[i], halfSpacing, opposite, ChordTolerance)
: Helper.GetPartLines(stationaryParts[i], opposite, ChordTolerance);
var d = Helper.DirectionalDistance(movingLines, stationaryLines[i], direction); obstacleLines[i] ??= obstaclePart != null
? (halfSpacing > 0
? Helper.GetOffsetPartLines(obstaclePart, halfSpacing, opposite, ChordTolerance)
: Helper.GetPartLines(obstaclePart, opposite, ChordTolerance))
: (halfSpacing > 0
? Helper.GetOffsetPartLines(moving, halfSpacing, opposite, ChordTolerance)
: Helper.GetPartLines(moving, opposite, ChordTolerance));
var d = Helper.DirectionalDistance(movingLines, obstacleLines[i], direction);
if (d < distance) if (d < distance)
distance = d; distance = d;
} }
@@ -87,6 +97,11 @@ namespace OpenNest
var offset = Helper.DirectionToOffset(direction, distance); var offset = Helper.DirectionToOffset(direction, distance);
moving.Offset(offset); moving.Offset(offset);
} }
// This part is now an obstacle for subsequent moving parts.
obstacleBoxes.Add(moving.BoundingBox);
obstacleParts.Add(moving);
obstacleLines.Add(null); // will be lazily computed if needed
} }
} }
} }