using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.Fill
{
///
/// Tries to fit more parts at the end of a fill's outermost row. A lattice usually leaves
/// slack between its last row and the work-area edge and a gap at the end of each row that
/// is too short for one more copy. This pass pushes the outer row into that slack, slides
/// it toward one end of the row, then slides copies of the row's end part in from the freed
/// end. Lifting staggered hexagons straight over the row below, for instance, opens a notch
/// beside the last pair where one more hexagon nests.
///
///
/// Every move is a push, so the parts keep the part spacing from
/// each other and stay in the work area by construction; an inserted copy is kept only when
/// it stops inside the work area. The caller's comparer decides whether the result wins.
///
public static class EndRowRefiller
{
/// A row is the parts whose outer bounds lie this close to the outermost.
private const double RowTolerance = 1e-3;
/// Most positions across the row a copy is slid in from.
private const int MaxSamples = 96;
private static readonly (PushDirection Outward, PushDirection[] Along)[] Sides =
{
(PushDirection.Up, new[] { PushDirection.Left, PushDirection.Right }),
(PushDirection.Right, new[] { PushDirection.Down, PushDirection.Up }),
(PushDirection.Down, new[] { PushDirection.Left, PushDirection.Right }),
(PushDirection.Left, new[] { PushDirection.Down, PushDirection.Up }),
};
///
/// The best layout finds among and
/// its end-row refills, or itself when none beats it.
/// caps the part count when positive.
///
public static List Improve(
List parts,
Box workArea,
double partSpacing,
IFillComparer comparer,
int maxQuantity,
CancellationToken token
)
{
if (parts == null || parts.Count < 2)
return parts;
if (maxQuantity > 0 && parts.Count >= maxQuantity)
return parts;
var best = parts;
foreach (var (outward, along) in Sides)
{
foreach (var slide in along)
{
token.ThrowIfCancellationRequested();
var candidate = TrySide(parts, workArea, partSpacing, outward, slide, maxQuantity);
if (candidate != null && comparer.IsBetter(candidate, best, workArea))
best = candidate;
}
}
return best;
}
private static List TrySide(
List parts,
Box workArea,
double spacing,
PushDirection outward,
PushDirection slide,
int maxQuantity
)
{
var outerEdge = parts.Max(p => OuterEdge(p.BoundingBox, outward));
var row = new List();
var rest = new List();
foreach (var part in parts)
{
if (OuterEdge(part.BoundingBox, outward) >= outerEdge - RowTolerance)
row.Add(part.CloneAtOffset(Vector.Zero));
else
rest.Add(part);
}
if (row.Count < 2)
return null;
// Into the edge slack first, so the row can pass over the one below it, then
// along the row. The outward push is what frees the slide.
var lifted = Compactor.Push(row, rest, workArea, spacing, outward);
var slid = Compactor.Push(row, rest, workArea, spacing, slide);
if (lifted <= 0 && slid <= 0)
return null;
var obstacles = new List(rest.Count + row.Count);
obstacles.AddRange(rest);
obstacles.AddRange(row);
var templates = row
.GroupBy(p => (p.BaseDrawing, System.Math.Round(p.Rotation, 9)))
.Select(g => g.First())
.ToList();
var inserted = new List();
var limit = maxQuantity > 0 ? maxQuantity - parts.Count : row.Count;
while (inserted.Count < limit)
{
var next = InsertOne(templates, obstacles, row, workArea, spacing, outward, slide);
if (next == null)
break;
inserted.Add(next);
obstacles.Add(next);
}
if (inserted.Count == 0)
return null;
var result = new List(rest.Count + row.Count + inserted.Count);
result.AddRange(rest);
result.AddRange(row);
result.AddRange(inserted);
return result;
}
///
/// A copy slid in along from beyond the work area at the
/// position across the row band that lets it travel deepest, or null when no position
/// leaves it inside the work area.
///
private static Part InsertOne(
List templates,
List obstacles,
List row,
Box workArea,
double spacing,
PushDirection outward,
PushDirection slide
)
{
var vertical = outward == PushDirection.Up || outward == PushDirection.Down;
var rowBox = row.GetBoundingBox();
var direction = SpatialQuery.DirectionToOffset(slide, 1.0);
// One set for the whole sweep, so each obstacle's offset contour is built once.
var obstacleSet = new Compactor.ObstacleSet(obstacles, spacing);
var moving = new List(1);
Part best = null;
var bestDepth = double.MaxValue;
foreach (var template in templates)
{
var box = template.BoundingBox;
var size = vertical ? box.Width : box.Length;
// Across the row: from one part size inside the row's inner edge to the
// work-area edge, so a copy can drop into notches between the row and the
// one below it.
double lo, hi;
if (vertical)
{
lo = System.Math.Max(workArea.Bottom, rowBox.Bottom - size);
hi = System.Math.Min(workArea.Top - size, rowBox.Top);
}
else
{
lo = System.Math.Max(workArea.Left, rowBox.Left - size);
hi = System.Math.Min(workArea.Right - size, rowBox.Right);
}
if (hi < lo)
continue;
var step = System.Math.Max(spacing / 2, Tolerance.Epsilon);
var samples = (int)System.Math.Ceiling((hi - lo) / step);
samples = System.Math.Clamp(samples, 1, MaxSamples);
for (var i = 0; i <= samples; i++)
{
var across = lo + (hi - lo) * i / samples;
var copy = StartBeyond(template, workArea, spacing, slide, across, vertical);
moving.Clear();
moving.Add(copy);
var travel = Compactor.Push(moving, workArea, direction, obstacleSet);
if (travel <= 0)
continue;
var landed = copy.BoundingBox;
if (!Inside(landed, workArea))
continue;
var depth = LeadingEdge(landed, slide);
if (depth < bestDepth - Tolerance.Epsilon)
{
bestDepth = depth;
best = copy;
}
}
}
return best;
}
///
/// A copy of just beyond the work-area edge the slide
/// starts from, its inner bound at across the row.
///
private static Part StartBeyond(
Part template,
Box workArea,
double spacing,
PushDirection slide,
double across,
bool vertical
)
{
var box = template.BoundingBox;
var gap = spacing + 1.0;
double dx, dy;
if (vertical)
{
dy = across - box.Bottom;
dx = slide == PushDirection.Left
? workArea.Right + gap - box.Left
: workArea.Left - gap - box.Right;
}
else
{
dx = across - box.Left;
dy = slide == PushDirection.Down
? workArea.Top + gap - box.Bottom
: workArea.Bottom - gap - box.Top;
}
var copy = template.CloneAtOffset(new Vector(dx, dy));
copy.UpdateBounds();
return copy;
}
/// How far the copy got: smaller is deeper along the slide.
private static double LeadingEdge(Box box, PushDirection slide) =>
slide switch
{
PushDirection.Left => box.Left,
PushDirection.Right => -box.Right,
PushDirection.Down => box.Bottom,
_ => -box.Top,
};
private static double OuterEdge(Box box, PushDirection outward) =>
outward switch
{
PushDirection.Up => box.Top,
PushDirection.Right => box.Right,
PushDirection.Down => -box.Bottom,
_ => -box.Left,
};
private static bool Inside(Box box, Box workArea) =>
box.Left >= workArea.Left - Tolerance.Epsilon
&& box.Right <= workArea.Right + Tolerance.Epsilon
&& box.Bottom >= workArea.Bottom - Tolerance.Epsilon
&& box.Top <= workArea.Top + Tolerance.Epsilon;
}
}