refactor(engine): delegate PlateView.PushSelected to Compactor and add iterative compaction
PushSelected now calls Compactor.Push instead of duplicating the push logic. Compactor.Push moves parts as a group (single min distance) to preserve grid layouts. Compact tries both left-first and down-first orderings, iterating up to 20 times until movement drops below threshold, and keeps whichever ordering traveled further. Also includes a cancellation check in FillWithProgress to avoid accepting parts after the user stops a nest. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,49 +18,91 @@ namespace OpenNest
|
|||||||
/// Everything already on the plate (excluding movingParts) is treated
|
/// Everything already on the plate (excluding movingParts) is treated
|
||||||
/// as stationary obstacles.
|
/// as stationary obstacles.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
private const double RepeatThreshold = 0.01;
|
||||||
|
private const int MaxIterations = 20;
|
||||||
|
|
||||||
public static void Compact(List<Part> movingParts, Plate plate)
|
public static void Compact(List<Part> movingParts, Plate plate)
|
||||||
{
|
{
|
||||||
if (movingParts == null || movingParts.Count == 0)
|
if (movingParts == null || movingParts.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Push(movingParts, plate, PushDirection.Left);
|
var savedPositions = SavePositions(movingParts);
|
||||||
Push(movingParts, plate, PushDirection.Down);
|
|
||||||
|
// Try left-first.
|
||||||
|
var leftFirst = CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down);
|
||||||
|
|
||||||
|
// Restore and try down-first.
|
||||||
|
RestorePositions(movingParts, savedPositions);
|
||||||
|
var downFirst = CompactLoop(movingParts, plate, PushDirection.Down, PushDirection.Left);
|
||||||
|
|
||||||
|
// Keep left-first if it traveled further.
|
||||||
|
if (leftFirst > downFirst)
|
||||||
|
{
|
||||||
|
RestorePositions(movingParts, savedPositions);
|
||||||
|
CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
private static double CompactLoop(List<Part> parts, Plate plate,
|
||||||
|
PushDirection first, PushDirection second)
|
||||||
|
{
|
||||||
|
var total = 0.0;
|
||||||
|
|
||||||
|
for (var i = 0; i < MaxIterations; i++)
|
||||||
|
{
|
||||||
|
var a = Push(parts, plate, first);
|
||||||
|
var b = Push(parts, plate, second);
|
||||||
|
total += a + b;
|
||||||
|
|
||||||
|
if (a <= RepeatThreshold && b <= RepeatThreshold)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector[] SavePositions(List<Part> parts)
|
||||||
|
{
|
||||||
|
var positions = new Vector[parts.Count];
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
positions[i] = parts[i].Location;
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RestorePositions(List<Part> parts, Vector[] positions)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
parts[i].Location = positions[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||||
{
|
{
|
||||||
// Start with parts already on the plate (excluding the moving group).
|
|
||||||
var obstacleParts = plate.Parts
|
var obstacleParts = plate.Parts
|
||||||
.Where(p => !movingParts.Contains(p))
|
.Where(p => !movingParts.Contains(p))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var obstacleBoxes = new List<Box>(obstacleParts.Count + movingParts.Count);
|
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||||
var obstacleLines = new List<List<Line>>(obstacleParts.Count + movingParts.Count);
|
var obstacleLines = new List<Line>[obstacleParts.Count];
|
||||||
|
|
||||||
for (var i = 0; i < obstacleParts.Count; i++)
|
for (var i = 0; i < obstacleParts.Count; i++)
|
||||||
{
|
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||||
obstacleBoxes.Add(obstacleParts[i].BoundingBox);
|
|
||||||
obstacleLines.Add(null); // lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
var workArea = plate.WorkArea();
|
var workArea = plate.WorkArea();
|
||||||
|
var distance = double.MaxValue;
|
||||||
|
|
||||||
foreach (var moving in movingParts)
|
foreach (var moving in movingParts)
|
||||||
{
|
{
|
||||||
var distance = double.MaxValue;
|
var edgeDist = Helper.EdgeDistance(moving.BoundingBox, workArea, direction);
|
||||||
var movingBox = moving.BoundingBox;
|
|
||||||
|
|
||||||
// Plate edge distance.
|
|
||||||
var edgeDist = Helper.EdgeDistance(movingBox, workArea, direction);
|
|
||||||
if (edgeDist > 0 && edgeDist < distance)
|
if (edgeDist > 0 && edgeDist < distance)
|
||||||
distance = edgeDist;
|
distance = edgeDist;
|
||||||
|
|
||||||
|
var movingBox = moving.BoundingBox;
|
||||||
List<Line> movingLines = null;
|
List<Line> movingLines = null;
|
||||||
|
|
||||||
for (var i = 0; i < obstacleBoxes.Count; i++)
|
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||||
{
|
{
|
||||||
var gap = Helper.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
var gap = Helper.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||||
if (gap < 0 || gap >= distance)
|
if (gap < 0 || gap >= distance)
|
||||||
@@ -77,32 +119,25 @@ namespace OpenNest
|
|||||||
? Helper.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
|
? Helper.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
|
||||||
: Helper.GetPartLines(moving, direction, ChordTolerance);
|
: Helper.GetPartLines(moving, direction, ChordTolerance);
|
||||||
|
|
||||||
var obstaclePart = i < obstacleParts.Count ? obstacleParts[i] : null;
|
obstacleLines[i] ??= halfSpacing > 0
|
||||||
|
? Helper.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
|
||||||
obstacleLines[i] ??= obstaclePart != null
|
: Helper.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
|
||||||
? (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);
|
var d = Helper.DirectionalDistance(movingLines, obstacleLines[i], direction);
|
||||||
if (d < distance)
|
if (d < distance)
|
||||||
distance = d;
|
distance = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distance < double.MaxValue && distance > 0)
|
|
||||||
{
|
|
||||||
var offset = Helper.DirectionToOffset(direction, distance);
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (distance < double.MaxValue && distance > 0)
|
||||||
|
{
|
||||||
|
var offset = Helper.DirectionToOffset(direction, distance);
|
||||||
|
foreach (var moving in movingParts)
|
||||||
|
moving.Offset(offset);
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -837,7 +837,7 @@ namespace OpenNest.Controls
|
|||||||
var parts = await Task.Run(() =>
|
var parts = await Task.Run(() =>
|
||||||
engine.Fill(groupParts, workArea, progress, cts.Token));
|
engine.Fill(groupParts, workArea, progress, cts.Token));
|
||||||
|
|
||||||
if (parts.Count > 0)
|
if (parts.Count > 0 && !cts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
AcceptTemporaryParts();
|
AcceptTemporaryParts();
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
@@ -937,65 +937,9 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
public void PushSelected(PushDirection direction)
|
public void PushSelected(PushDirection direction)
|
||||||
{
|
{
|
||||||
var stationaryParts = parts.Where(p => !p.IsSelected && !SelectedParts.Contains(p)).ToList();
|
var movingParts = SelectedParts.Select(p => p.BasePart).ToList();
|
||||||
var stationaryBoxes = new Box[stationaryParts.Count];
|
Compactor.Push(movingParts, Plate, direction);
|
||||||
var stationaryLines = new List<Line>[stationaryParts.Count];
|
Invalidate();
|
||||||
|
|
||||||
var opposite = Helper.OppositeDirection(direction);
|
|
||||||
var halfSpacing = Plate.PartSpacing / 2;
|
|
||||||
var isHorizontal = Helper.IsHorizontalDirection(direction);
|
|
||||||
|
|
||||||
for (var i = 0; i < stationaryParts.Count; i++)
|
|
||||||
stationaryBoxes[i] = stationaryParts[i].BoundingBox;
|
|
||||||
|
|
||||||
var workArea = Plate.WorkArea();
|
|
||||||
var distance = double.MaxValue;
|
|
||||||
|
|
||||||
foreach (var selected in SelectedParts)
|
|
||||||
{
|
|
||||||
// Check plate edge first to tighten the upper bound.
|
|
||||||
var edgeDist = Helper.EdgeDistance(selected.BoundingBox, workArea, direction);
|
|
||||||
if (edgeDist > 0 && edgeDist < distance)
|
|
||||||
distance = edgeDist;
|
|
||||||
|
|
||||||
var movingBox = selected.BoundingBox;
|
|
||||||
List<Line> movingLines = null;
|
|
||||||
|
|
||||||
for (var i = 0; i < stationaryBoxes.Length; i++)
|
|
||||||
{
|
|
||||||
// Skip parts not ahead in the push direction or further than current best.
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Compute lines lazily — only for parts that survive bounding box checks.
|
|
||||||
movingLines ??= halfSpacing > 0
|
|
||||||
? Helper.GetOffsetPartLines(selected.BasePart, halfSpacing, direction, OffsetTolerance)
|
|
||||||
: Helper.GetPartLines(selected.BasePart, direction, OffsetTolerance);
|
|
||||||
|
|
||||||
stationaryLines[i] ??= halfSpacing > 0
|
|
||||||
? Helper.GetOffsetPartLines(stationaryParts[i].BasePart, halfSpacing, opposite, OffsetTolerance)
|
|
||||||
: Helper.GetPartLines(stationaryParts[i].BasePart, opposite, OffsetTolerance);
|
|
||||||
|
|
||||||
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);
|
|
||||||
SelectedParts.ForEach(p => p.Offset(offset));
|
|
||||||
Invalidate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetDisplayName(Type type)
|
private string GetDisplayName(Type type)
|
||||||
|
|||||||
Reference in New Issue
Block a user