using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace OpenNest.Engine.Fill
{
public class FillLinear
{
public FillLinear(Box workArea, double partSpacing)
{
PartSpacing = partSpacing;
WorkArea = new Box(workArea.X, workArea.Y, workArea.Width, workArea.Length);
}
public Box WorkArea { get; }
public double PartSpacing { get; }
public double HalfSpacing => PartSpacing / 2;
///
/// Optional multi-part patterns (e.g. interlocking pairs) to try in remainder strips.
///
public List RemainderPatterns { get; set; }
private static Vector MakeOffset(NestDirection direction, double distance)
{
return direction == NestDirection.Horizontal
? new Vector(distance, 0)
: new Vector(0, distance);
}
private static PushDirection GetPushDirection(NestDirection direction)
{
return direction == NestDirection.Horizontal
? PushDirection.Left
: PushDirection.Down;
}
private static double GetDimension(Box box, NestDirection direction)
{
return direction == NestDirection.Horizontal ? box.Width : box.Length;
}
private static double GetStart(Box box, NestDirection direction)
{
return direction == NestDirection.Horizontal ? box.Left : box.Bottom;
}
private double GetLimit(NestDirection direction)
{
return direction == NestDirection.Horizontal ? WorkArea.Right : WorkArea.Top;
}
private static NestDirection PerpendicularAxis(NestDirection direction)
{
return direction == NestDirection.Horizontal
? NestDirection.Vertical
: NestDirection.Horizontal;
}
///
/// Computes the slide distance for the push algorithm, returning the
/// geometry-aware copy distance along the given axis.
///
private double ComputeCopyDistance(double bboxDim, double slideDistance)
{
if (slideDistance >= double.MaxValue || slideDistance < 0)
return bboxDim + PartSpacing;
// The geometry-aware slide can produce a copy distance smaller than
// the part itself when inflated corner/arc vertices interact spuriously.
// Clamp to bboxDim + PartSpacing to prevent bounding box overlap.
return System.Math.Max(bboxDim - slideDistance, bboxDim + PartSpacing);
}
///
/// Finds the geometry-aware copy distance between two identical parts along an axis.
/// Both parts are inflated by half-spacing for symmetric spacing.
///
private double FindCopyDistance(Part partA, NestDirection direction, PartBoundary boundary)
{
var bboxDim = GetDimension(partA.BoundingBox, direction);
var pushDir = GetPushDirection(direction);
var locationBOffset = MakeOffset(direction, bboxDim);
// Use the most efficient array-based overload to avoid all allocations.
var slideDistance = SpatialQuery.DirectionalDistance(
boundary.GetEdges(pushDir), partA.Location + locationBOffset,
boundary.GetEdges(SpatialQuery.OppositeDirection(pushDir)), partA.Location,
pushDir);
return ComputeCopyDistance(bboxDim, slideDistance);
}
///
/// Finds the geometry-aware copy distance between two identical patterns along an axis.
/// Checks every pair of parts across adjacent patterns so that multi-part
/// patterns (e.g. interlocking pairs) maintain spacing between ALL parts.
/// Both sides are inflated by half-spacing for symmetric spacing.
///
private double FindPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary[] boundaries)
{
if (patternA.Parts.Count <= 1)
return FindSinglePartPatternCopyDistance(patternA, direction, boundaries[0]);
var bboxDim = GetDimension(patternA.BoundingBox, direction);
var pushDir = GetPushDirection(direction);
var opposite = SpatialQuery.OppositeDirection(pushDir);
// Compute a starting offset large enough that every part-pair in
// patternB has its offset geometry beyond patternA's offset geometry.
var maxUpper = double.MinValue;
var minLower = double.MaxValue;
for (var i = 0; i < patternA.Parts.Count; i++)
{
var bb = patternA.Parts[i].BoundingBox;
var upper = direction == NestDirection.Horizontal ? bb.Right : bb.Top;
var lower = direction == NestDirection.Horizontal ? bb.Left : bb.Bottom;
if (upper > maxUpper) maxUpper = upper;
if (lower < minLower) minLower = lower;
}
var startOffset = System.Math.Max(bboxDim,
maxUpper - minLower + PartSpacing + Tolerance.Epsilon);
var offset = MakeOffset(direction, startOffset);
// Pre-cache edge arrays.
var movingEdges = new (Vector start, Vector end)[patternA.Parts.Count][];
var stationaryEdges = new (Vector start, Vector end)[patternA.Parts.Count][];
for (var i = 0; i < patternA.Parts.Count; i++)
{
movingEdges[i] = boundaries[i].GetEdges(pushDir);
stationaryEdges[i] = boundaries[i].GetEdges(opposite);
}
var maxCopyDistance = 0.0;
for (var j = 0; j < patternA.Parts.Count; j++)
{
var locationB = patternA.Parts[j].Location + offset;
for (var i = 0; i < patternA.Parts.Count; i++)
{
var slideDistance = SpatialQuery.DirectionalDistance(
movingEdges[j], locationB,
stationaryEdges[i], patternA.Parts[i].Location,
pushDir);
if (slideDistance >= double.MaxValue || slideDistance < 0)
continue;
var copyDist = startOffset - slideDistance;
if (copyDist > maxCopyDistance)
maxCopyDistance = copyDist;
}
}
if (maxCopyDistance < Tolerance.Epsilon)
return bboxDim + PartSpacing;
return maxCopyDistance;
}
///
/// Fast path for single-part patterns — no cross-part conflicts possible.
///
private double FindSinglePartPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary boundary)
{
var template = patternA.Parts[0];
return FindCopyDistance(template, direction, boundary);
}
///
/// Gets offset boundary lines for all parts in a pattern using a shared boundary.
///
private static List GetPatternLines(Pattern pattern, PartBoundary boundary, PushDirection direction)
{
var lines = new List();
foreach (var part in pattern.Parts)
lines.AddRange(boundary.GetLines(part.Location, direction));
return lines;
}
///
/// Gets boundary lines for all parts in a pattern, with an additional
/// location offset applied. Avoids cloning the pattern.
///
private static List GetOffsetPatternLines(Pattern pattern, Vector offset, PartBoundary boundary, PushDirection direction)
{
var lines = new List();
foreach (var part in pattern.Parts)
lines.AddRange(boundary.GetLines(part.Location + offset, direction));
return lines;
}
///
/// Creates boundaries for all parts in a pattern. Parts that share the same
/// program geometry (same drawing and rotation) reuse the same boundary instance.
///
private PartBoundary[] CreateBoundaries(Pattern pattern)
{
var boundaries = new PartBoundary[pattern.Parts.Count];
var cache = new List<(Drawing drawing, double rotation, PartBoundary boundary)>();
for (var i = 0; i < pattern.Parts.Count; i++)
{
var part = pattern.Parts[i];
PartBoundary found = null;
foreach (var entry in cache)
{
if (entry.drawing == part.BaseDrawing && entry.rotation.IsEqualTo(part.Rotation))
{
found = entry.boundary;
break;
}
}
if (found == null)
{
found = new PartBoundary(part, HalfSpacing);
cache.Add((part.BaseDrawing, part.Rotation, found));
}
boundaries[i] = found;
}
return boundaries;
}
///
/// Tiles a pattern along the given axis, returning the cloned parts
/// (does not include the original pattern's parts). For multi-part
/// patterns, also adds individual parts from the next incomplete copy
/// that still fit within the work area.
///
private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries)
{
var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries);
if (copyDistance <= 0)
return new List();
var dim = GetDimension(basePattern.BoundingBox, direction);
var start = GetStart(basePattern.BoundingBox, direction);
var limit = GetLimit(direction);
var estimatedCopies = (int)((limit - start - dim) / copyDistance);
var result = new List(estimatedCopies * basePattern.Parts.Count);
var count = 1;
while (true)
{
var nextPos = start + copyDistance * count;
if (nextPos + dim > limit + Tolerance.Epsilon)
break;
var offset = MakeOffset(direction, copyDistance * count);
foreach (var part in basePattern.Parts)
result.Add(part.CloneAtOffset(offset));
count++;
}
// For multi-part patterns, try to place individual parts from the
// next copy that didn't fit as a whole. This handles cases where
// e.g. a 2-part pair only partially fits — one part may still be
// within the work area even though the full pattern exceeds it.
if (basePattern.Parts.Count > 1)
{
var offset = MakeOffset(direction, copyDistance * count);
foreach (var basePart in basePattern.Parts)
{
var part = basePart.CloneAtOffset(offset);
if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon &&
part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon &&
part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon &&
part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon)
{
result.Add(part);
}
}
}
return result;
}
///
/// Creates a seed pattern containing a single part positioned at the work area origin.
/// Returns an empty pattern if the part does not fit.
///
private Pattern MakeSeedPattern(Drawing drawing, double rotationAngle)
{
var pattern = new Pattern();
var template = new Part(drawing);
if (!rotationAngle.IsEqualTo(0))
template.Rotate(rotationAngle);
template.Offset(WorkArea.Location - template.BoundingBox.Location);
if (template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
return pattern;
pattern.Parts.Add(template);
pattern.UpdateBounds();
return pattern;
}
///
/// Fills the work area by tiling the pattern along the primary axis to form
/// a row, then tiling that row along the perpendicular axis to form a grid.
/// After the grid is formed, fills the remaining strip with individual parts.
///
private List FillGrid(Pattern pattern, NestDirection direction)
{
var perpAxis = PerpendicularAxis(direction);
var boundaries = CreateBoundaries(pattern);
// Step 1: Tile along primary axis
var row = new List(pattern.Parts);
row.AddRange(TilePattern(pattern, direction, boundaries));
// If primary tiling didn't produce copies, just tile along perpendicular
if (row.Count <= pattern.Parts.Count)
{
row.AddRange(TilePattern(pattern, perpAxis, boundaries));
return row;
}
// Step 2: Build row pattern and tile along perpendicular axis
var rowPattern = new Pattern();
rowPattern.Parts.AddRange(row);
rowPattern.UpdateBounds();
var rowBoundaries = CreateBoundaries(rowPattern);
var gridResult = new List(rowPattern.Parts);
gridResult.AddRange(TilePattern(rowPattern, perpAxis, rowBoundaries));
// Step 3: Fill remaining strip
var remaining = FillRemainingStrip(gridResult, pattern, perpAxis, direction);
if (remaining.Count > 0)
gridResult.AddRange(remaining);
// Step 4: Try fewer rows optimization
var fewerResult = TryFewerRows(gridResult, rowPattern, pattern, perpAxis, direction);
if (fewerResult != null && fewerResult.Count > gridResult.Count)
return fewerResult;
return gridResult;
}
///
/// Tries removing the last row/column from the grid and re-filling the
/// larger remainder strip. Returns null if this doesn't improve the total.
///
private List TryFewerRows(
List fullResult, Pattern rowPattern, Pattern seedPattern,
NestDirection tiledAxis, NestDirection primaryAxis)
{
var rowPartCount = rowPattern.Parts.Count;
if (fullResult.Count < rowPartCount * 2)
return null;
var fewerParts = new List(fullResult.Count - rowPartCount);
for (var i = 0; i < fullResult.Count - rowPartCount; i++)
fewerParts.Add(fullResult[i]);
var remaining = FillRemainingStrip(fewerParts, seedPattern, tiledAxis, primaryAxis);
if (remaining.Count <= rowPartCount)
return null;
fewerParts.AddRange(remaining);
return fewerParts;
}
///
/// After tiling full rows/columns, fills the remaining strip with individual
/// parts. The strip is the leftover space along the tiled axis between the
/// last full row/column and the work area boundary. Each unique drawing and
/// rotation from the seed pattern is tried in both directions.
///
private List FillRemainingStrip(
List placedParts, Pattern seedPattern,
NestDirection tiledAxis, NestDirection primaryAxis)
{
var placedEdge = FindPlacedEdge(placedParts, tiledAxis);
var remainingStrip = BuildRemainingStrip(placedEdge, tiledAxis);
if (remainingStrip == null)
return new List();
var rotations = BuildRotationSet(seedPattern);
var best = FindBestFill(rotations, remainingStrip);
if (RemainderPatterns != null)
{
System.Diagnostics.Debug.WriteLine($"[FillRemainingStrip] Strip: {remainingStrip.Width:F1}x{remainingStrip.Length:F1}, individual best={best?.Count ?? 0}, trying {RemainderPatterns.Count} patterns");
foreach (var pattern in RemainderPatterns)
{
var filler = new FillLinear(remainingStrip, PartSpacing);
var h = filler.Fill(pattern, NestDirection.Horizontal);
var v = filler.Fill(pattern, NestDirection.Vertical);
System.Diagnostics.Debug.WriteLine($"[FillRemainingStrip] Pattern ({pattern.Parts.Count} parts, bbox={pattern.BoundingBox.Width:F1}x{pattern.BoundingBox.Length:F1}): H={h?.Count ?? 0}, V={v?.Count ?? 0}");
if (h != null && h.Count > (best?.Count ?? 0))
best = h;
if (v != null && v.Count > (best?.Count ?? 0))
best = v;
}
System.Diagnostics.Debug.WriteLine($"[FillRemainingStrip] Final best={best?.Count ?? 0}");
}
return best ?? new List();
}
private static double FindPlacedEdge(List placedParts, NestDirection tiledAxis)
{
var placedEdge = double.MinValue;
foreach (var part in placedParts)
{
var edge = tiledAxis == NestDirection.Vertical
? part.BoundingBox.Top
: part.BoundingBox.Right;
if (edge > placedEdge)
placedEdge = edge;
}
return placedEdge;
}
private Box BuildRemainingStrip(double placedEdge, NestDirection tiledAxis)
{
if (tiledAxis == NestDirection.Vertical)
{
var bottom = placedEdge + PartSpacing;
var height = WorkArea.Top - bottom;
if (height <= Tolerance.Epsilon)
return null;
return new Box(WorkArea.X, bottom, WorkArea.Width, height);
}
else
{
var left = placedEdge + PartSpacing;
var width = WorkArea.Right - left;
if (width <= Tolerance.Epsilon)
return null;
return new Box(left, WorkArea.Y, width, WorkArea.Length);
}
}
///
/// Builds a set of (drawing, rotation) candidates: cardinal orientations
/// (0° and 90°) for each unique drawing, plus any seed pattern rotations
/// not already covered.
///
private static List<(Drawing drawing, double rotation)> BuildRotationSet(Pattern seedPattern)
{
var rotations = new List<(Drawing drawing, double rotation)>();
var drawings = new List();
foreach (var seedPart in seedPattern.Parts)
{
var found = false;
foreach (var d in drawings)
{
if (d == seedPart.BaseDrawing)
{
found = true;
break;
}
}
if (!found)
drawings.Add(seedPart.BaseDrawing);
}
foreach (var drawing in drawings)
{
rotations.Add((drawing, 0));
rotations.Add((drawing, Angle.HalfPI));
}
foreach (var seedPart in seedPattern.Parts)
{
var skip = false;
foreach (var (d, r) in rotations)
{
if (d == seedPart.BaseDrawing && r.IsEqualTo(seedPart.Rotation))
{
skip = true;
break;
}
}
if (!skip)
rotations.Add((seedPart.BaseDrawing, seedPart.Rotation));
}
return rotations;
}
///
/// Tries all rotation candidates in both directions in parallel, returns the
/// fill with the most parts.
///
private List FindBestFill(List<(Drawing drawing, double rotation)> rotations, Box strip)
{
var bag = new System.Collections.Concurrent.ConcurrentBag>();
Parallel.ForEach(rotations, entry =>
{
var filler = new FillLinear(strip, PartSpacing);
var h = filler.Fill(entry.drawing, entry.rotation, NestDirection.Horizontal);
var v = filler.Fill(entry.drawing, entry.rotation, NestDirection.Vertical);
if (h != null && h.Count > 0)
bag.Add(h);
if (v != null && v.Count > 0)
bag.Add(v);
});
List best = null;
foreach (var candidate in bag)
{
if (best == null || candidate.Count > best.Count)
best = candidate;
}
return best ?? new List();
}
///
/// Fills a single row of identical parts along one axis using geometry-aware spacing.
///
public Pattern FillRow(Drawing drawing, double rotationAngle, NestDirection direction)
{
var seed = MakeSeedPattern(drawing, rotationAngle);
if (seed.Parts.Count == 0)
return seed;
var template = seed.Parts[0];
var boundary = new PartBoundary(template, HalfSpacing);
var copyDistance = FindCopyDistance(template, direction, boundary);
if (copyDistance <= 0)
return seed;
var dim = GetDimension(template.BoundingBox, direction);
var start = GetStart(template.BoundingBox, direction);
var limit = GetLimit(direction);
var count = 1;
while (true)
{
var nextPos = start + copyDistance * count;
if (nextPos + dim > limit + Tolerance.Epsilon)
break;
var clone = template.CloneAtOffset(MakeOffset(direction, copyDistance * count));
seed.Parts.Add(clone);
count++;
}
seed.UpdateBounds();
return seed;
}
///
/// Fills the work area by tiling a pre-built pattern along both axes.
///
public List Fill(Pattern pattern, NestDirection primaryAxis)
{
if (pattern.Parts.Count == 0)
return new List();
var offset = WorkArea.Location - pattern.BoundingBox.Location;
var basePattern = pattern.Clone(offset);
if (basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
return new List();
return FillGrid(basePattern, primaryAxis);
}
///
/// Fills the work area by creating a seed part, then recursively tiling
/// along the primary axis and then the perpendicular axis.
///
public List Fill(Drawing drawing, double rotationAngle, NestDirection primaryAxis)
{
var seed = MakeSeedPattern(drawing, rotationAngle);
if (seed.Parts.Count == 0)
return new List();
return FillGrid(seed, primaryAxis);
}
}
}