feat(engine): add CompactIndividual to Compactor (disabled in strip nester)

Add plate-free Push overload and CompactIndividual method that pushes
each part individually against all others as obstacles. Disabled in
StripNestEngine pending investigation — compaction opens irregular gaps
that the remnant finder scatters parts into.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 14:35:07 -04:00
parent 195e29da52
commit 66050c68f6
2 changed files with 72 additions and 2 deletions

View File

@@ -81,6 +81,12 @@ namespace OpenNest
.Where(p => !movingParts.Contains(p))
.ToList();
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var obstacleBoxes = new Box[obstacleParts.Count];
var obstacleLines = new List<Line>[obstacleParts.Count];
@@ -88,9 +94,8 @@ namespace OpenNest
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var opposite = SpatialQuery.OppositeDirection(direction);
var halfSpacing = plate.PartSpacing / 2;
var halfSpacing = partSpacing / 2;
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
var workArea = plate.WorkArea();
var distance = double.MaxValue;
// BB gap at which offset geometries are expected to be touching.
@@ -152,5 +157,60 @@ namespace OpenNest
return 0;
}
/// <summary>
/// Compacts parts individually toward the bottom-left of the work area.
/// Each part is pushed against all others as obstacles, closing geometry-based gaps.
/// Does not require parts to be on a plate.
/// </summary>
public static void CompactIndividual(List<Part> parts, Box workArea, double partSpacing)
{
if (parts == null || parts.Count < 2)
return;
var savedPositions = SavePositions(parts);
var leftFirst = CompactIndividualLoop(parts, workArea, partSpacing,
PushDirection.Left, PushDirection.Down);
RestorePositions(parts, savedPositions);
var downFirst = CompactIndividualLoop(parts, workArea, partSpacing,
PushDirection.Down, PushDirection.Left);
if (leftFirst > downFirst)
{
RestorePositions(parts, savedPositions);
CompactIndividualLoop(parts, workArea, partSpacing,
PushDirection.Left, PushDirection.Down);
}
}
private static double CompactIndividualLoop(List<Part> parts, Box workArea,
double partSpacing, PushDirection first, PushDirection second)
{
var total = 0.0;
for (var pass = 0; pass < MaxIterations; pass++)
{
var moved = 0.0;
foreach (var part in parts)
{
var single = new List<Part>(1) { part };
var obstacles = new List<Part>(parts.Count - 1);
foreach (var p in parts)
if (p != part) obstacles.Add(p);
moved += Push(single, obstacles, workArea, partSpacing, first);
moved += Push(single, obstacles, workArea, partSpacing, second);
}
total += moved;
if (moved <= RepeatThreshold)
break;
}
return total;
}
}
}

View File

@@ -214,6 +214,16 @@ namespace OpenNest
: trialPlacedBox.Right - workArea.X;
}
// TODO: Compact strip parts individually to close geometry-based gaps.
// Disabled pending investigation — remnant finder picks up gaps created
// by compaction and scatters parts into them.
// Compactor.CompactIndividual(bestParts, workArea, Plate.PartSpacing);
//
// var compactedBox = bestParts.Cast<IBoundable>().GetBoundingBox();
// bestDim = direction == StripDirection.Bottom
// ? compactedBox.Top - workArea.Y
// : compactedBox.Right - workArea.X;
// Build remnant box with spacing gap.
var spacing = Plate.PartSpacing;
var remnantBox = direction == StripDirection.Bottom