Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests pass after reformat, full solution builds 0 errors. Added .csharpierignore so csproj/config XML keeps its existing layout (CSharpier's XML wrapping churns attributes with zero benefit). Formatting is now enforceable: dotnet csharpier check . passes.
328 lines
11 KiB
C#
328 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest.Engine.Fill
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static class Compactor
|
|
{
|
|
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
|
{
|
|
var obstacleParts = plate
|
|
.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
|
.ToList();
|
|
|
|
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
|
|
/// </summary>
|
|
public static double Push(List<Part> movingParts, Plate plate, double angle)
|
|
{
|
|
var obstacleParts = plate
|
|
.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
|
.ToList();
|
|
|
|
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
|
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
|
|
/// </summary>
|
|
public static double Push(
|
|
List<Part> movingParts,
|
|
List<Part> obstacleParts,
|
|
Box workArea,
|
|
double partSpacing,
|
|
Vector direction
|
|
)
|
|
{
|
|
var opposite = -direction;
|
|
|
|
var obstacleBoxes = new Box[obstacleParts.Count];
|
|
var obstacleSpacingBoxes = new Box[obstacleParts.Count];
|
|
var obstacleEntities = new List<Entity>[obstacleParts.Count];
|
|
var halfSpacing = System.Math.Max(0, partSpacing) / 2;
|
|
|
|
for (var i = 0; i < obstacleParts.Count; i++)
|
|
{
|
|
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
|
obstacleSpacingBoxes[i] = SpacingBounds(obstacleBoxes[i], halfSpacing);
|
|
}
|
|
|
|
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<Entity> movingEntities = null;
|
|
|
|
// Check if any obstacle is inside the moving part — only then
|
|
// do we need cutout entities on the moving part.
|
|
var needCutouts = false;
|
|
for (var i = 0; i < obstacleBoxes.Length; i++)
|
|
{
|
|
if (movingBox.Contains(obstacleBoxes[i]))
|
|
{
|
|
needCutouts = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Broad-phase bounds must enclose the spacing-offset contours.
|
|
// Raw bounds can miss near passes and overestimate the safe travel.
|
|
var movingSpacingBox = SpacingBounds(movingBox, halfSpacing);
|
|
for (var i = 0; i < obstacleBoxes.Length; i++)
|
|
{
|
|
var obstacleSpacingBox = obstacleSpacingBoxes[i];
|
|
var reverseGap = SpatialQuery.DirectionalGap(
|
|
movingSpacingBox,
|
|
obstacleSpacingBox,
|
|
opposite
|
|
);
|
|
if (reverseGap > 0)
|
|
continue;
|
|
|
|
var gap = SpatialQuery.DirectionalGap(
|
|
movingSpacingBox,
|
|
obstacleSpacingBox,
|
|
direction
|
|
);
|
|
if (gap >= distance)
|
|
continue;
|
|
|
|
if (
|
|
!SpatialQuery.PerpendicularOverlap(
|
|
movingSpacingBox,
|
|
obstacleSpacingBox,
|
|
direction
|
|
)
|
|
)
|
|
continue;
|
|
|
|
movingEntities ??=
|
|
halfSpacing > 0
|
|
? (
|
|
needCutouts
|
|
? PartGeometry.GetOffsetPartEntities(moving, halfSpacing)
|
|
: PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing)
|
|
)
|
|
: (
|
|
needCutouts
|
|
? PartGeometry.GetPartEntities(moving)
|
|
: PartGeometry.GetPerimeterEntities(moving)
|
|
);
|
|
|
|
obstacleEntities[i] ??=
|
|
halfSpacing > 0
|
|
? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing)
|
|
: PartGeometry.GetPerimeterEntities(obstacleParts[i]);
|
|
|
|
var d = SpatialQuery.DirectionalDistance(
|
|
movingEntities,
|
|
obstacleEntities[i],
|
|
direction
|
|
);
|
|
if (
|
|
d <= Tolerance.Epsilon
|
|
&& partSpacing <= Tolerance.Epsilon
|
|
&& CanNudgeWithoutOverlap(moving, obstacleParts[i], direction)
|
|
)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private static Box SpacingBounds(Box box, double spacing)
|
|
{
|
|
return new Box(
|
|
box.Left - spacing,
|
|
box.Bottom - spacing,
|
|
box.Length + 2 * spacing,
|
|
box.Width + 2 * spacing
|
|
);
|
|
}
|
|
|
|
private static bool IntersectsAny(Part candidate, List<Part> parts)
|
|
{
|
|
for (var i = 0; i < parts.Count; i++)
|
|
{
|
|
if (candidate.Intersects(parts[i], out _))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool CanNudgeWithoutOverlap(Part moving, Part obstacle, Vector direction)
|
|
{
|
|
var nudge = direction * (Tolerance.Epsilon * 10);
|
|
|
|
moving.Offset(nudge);
|
|
try
|
|
{
|
|
return !moving.Intersects(obstacle, out _);
|
|
}
|
|
finally
|
|
{
|
|
moving.Offset(-nudge);
|
|
}
|
|
}
|
|
|
|
public static double Push(
|
|
List<Part> movingParts,
|
|
List<Part> obstacleParts,
|
|
Box workArea,
|
|
double partSpacing,
|
|
PushDirection direction
|
|
)
|
|
{
|
|
var vector = SpatialQuery.DirectionToOffset(direction, 1.0);
|
|
return Push(movingParts, obstacleParts, workArea, partSpacing, vector);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static double PushBoundingBox(
|
|
List<Part> movingParts,
|
|
Plate plate,
|
|
PushDirection direction
|
|
)
|
|
{
|
|
var obstacleParts = plate
|
|
.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
|
.ToList();
|
|
|
|
return PushBoundingBox(
|
|
movingParts,
|
|
obstacleParts,
|
|
plate.WorkArea(),
|
|
plate.PartSpacing,
|
|
direction
|
|
);
|
|
}
|
|
|
|
public static double PushBoundingBox(
|
|
List<Part> movingParts,
|
|
List<Part> 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 - 0.002;
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Repeatedly pushes parts left then down until total movement per
|
|
/// iteration falls below the given threshold.
|
|
/// </summary>
|
|
public static void Settle(
|
|
List<Part> parts,
|
|
Box workArea,
|
|
double partSpacing,
|
|
double threshold = 0.01,
|
|
int maxIterations = 20
|
|
)
|
|
{
|
|
if (parts.Count < 2)
|
|
return;
|
|
|
|
var noObstacles = new List<Part>();
|
|
|
|
for (var i = 0; i < maxIterations; i++)
|
|
{
|
|
var moved = 0.0;
|
|
moved += Push(parts, noObstacles, workArea, partSpacing, PushDirection.Left);
|
|
moved += Push(parts, noObstacles, workArea, partSpacing, PushDirection.Down);
|
|
|
|
if (moved < threshold)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|