using System;
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.
///
private const double RepeatThreshold = 0.01;
private const int MaxIterations = 20;
public static void Compact(List movingParts, Plate plate)
{
if (movingParts == null || movingParts.Count == 0)
return;
var savedPositions = SavePositions(movingParts);
// 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 double CompactLoop(List 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 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 parts, Vector[] positions)
{
for (var i = 0; i < parts.Count; i++)
parts[i].Location = positions[i];
}
public static double Push(List movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
///
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
///
public static double Push(List movingParts, Plate plate, double angle)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle);
}
///
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
///
public static double Push(List movingParts, List obstacleParts,
Box workArea, double partSpacing, double angle)
{
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
var opposite = -direction;
var obstacleBoxes = new Box[obstacleParts.Count];
var obstacleLines = new List[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var halfSpacing = partSpacing / 2;
var distance = double.MaxValue;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
List movingLines = null;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
if (gap >= distance)
continue;
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
continue;
movingLines ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
obstacleLines[i] ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = direction * distance;
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
public static double Push(List movingParts, List obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var obstacleBoxes = new Box[obstacleParts.Count];
var obstacleLines = new List[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var opposite = SpatialQuery.OppositeDirection(direction);
var halfSpacing = partSpacing / 2;
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
var distance = double.MaxValue;
// BB gap at which offset geometries are expected to be touching.
var contactGap = (halfSpacing + ChordTolerance) * 2;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
List movingLines = null;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
// Use the reverse-direction gap to check if the obstacle is entirely
// behind the moving part. The forward gap (gap < 0) is unreliable for
// irregular shapes whose bounding boxes overlap even when the actual
// geometry still has a valid contact in the push direction.
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
if (gap >= distance)
continue;
var perpOverlap = isHorizontal
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
if (!perpOverlap)
continue;
movingLines ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
obstacleLines[i] ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = SpatialQuery.DirectionToOffset(direction, distance);
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
///
/// Pushes movingParts using bounding-box distances only (no geometry lines).
/// Much faster but less precise — use as a coarse positioning pass before
/// a full geometry Push.
///
public static double PushBoundingBox(List movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
public static double PushBoundingBox(List movingParts, List obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var obstacleBoxes = new Box[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var opposite = SpatialQuery.OppositeDirection(direction);
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
var distance = double.MaxValue;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var perpOverlap = isHorizontal
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
if (!perpOverlap)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
var d = gap - partSpacing;
if (d < 0) d = 0;
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = SpatialQuery.DirectionToOffset(direction, distance);
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
///
/// 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.
///
public static void CompactIndividual(List 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 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(1) { part };
var obstacles = new List(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;
}
}
}