Files
OpenNest/OpenNest.Engine/Fill/FillExtents.cs
AJ Isaacs d1d47b5223 refactor(engine): simplify FillExtents logic using Compactor.Push
Simplify geometry-aware positioning by replacing manual slide calculations with higher-level Compactor.Push utility. Extract pair creation into CreatePair helper, remove redundant UpdateBounds calls, and clean up column/horizontal repetition logic.
2026-03-18 20:13:55 -04:00

248 lines
9.6 KiB
C#

using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace OpenNest.Engine.Fill
{
public class FillExtents
{
private const int MaxIterations = 10;
private readonly Box workArea;
private readonly double partSpacing;
public FillExtents(Box workArea, double partSpacing)
{
this.workArea = workArea;
this.partSpacing = partSpacing;
}
public List<Part> Fill(Drawing drawing, double rotationAngle = 0,
int plateNumber = 0,
CancellationToken token = default,
IProgress<NestProgress> progress = null)
{
var initialPair = CreatePair(drawing, rotationAngle, rotationAngle + System.Math.PI);
if (initialPair == null)
return new List<Part>();
var column = BuildColumn(initialPair.Value.part1, initialPair.Value.part2, initialPair.Value.pairBbox);
if (column.Count == 0)
return new List<Part>();
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber,
column, workArea, $"Extents: initial column {column.Count} parts");
var adjusted = AdjustColumn(initialPair.Value, column, token);
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber,
adjusted, workArea, $"Extents: adjusted column {adjusted.Count} parts");
var result = RepeatColumns(adjusted, token);
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber,
result, workArea, $"Extents: {result.Count} parts total");
return result;
}
// --- Step 1: Pair Construction ---
private (Part part1, Part part2, Box pairBbox)? CreatePair(
Drawing drawing, double rotation1, double rotation2, double verticalShift2 = 0)
{
var p1 = Part.CreateAtOrigin(drawing, rotation1);
var p2 = Part.CreateAtOrigin(drawing, rotation2);
// Initial positioning: p2 to the right of p1, with optional vertical shift.
p2.Offset(p1.BoundingBox.Width + partSpacing, verticalShift2);
// Compact p2 left toward p1 using geometry-aware distance.
Compactor.Push(new List<Part> { p2 }, new List<Part> { p1 }, workArea, partSpacing, PushDirection.Left);
var pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox();
// Re-anchor pair to work area origin (bottom-left).
var anchor = new Vector(workArea.X - pairBbox.Left, workArea.Y - pairBbox.Bottom);
p1.Offset(anchor);
p2.Offset(anchor);
pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox();
// Verify pair fits in work area.
if (pairBbox.Width > workArea.Width + Tolerance.Epsilon ||
pairBbox.Length > workArea.Length + Tolerance.Epsilon)
return null;
return (p1, p2, pairBbox);
}
// --- Step 2: Build Column (tile vertically) ---
private List<Part> BuildColumn(Part part1, Part part2, Box pairBbox)
{
var column = new List<Part> { (Part)part1.Clone(), (Part)part2.Clone() };
var copyDistance = ComputeVerticalCopyDistance(part1, part2, pairBbox);
if (copyDistance <= 0)
return column;
var pairHeight = pairBbox.Length;
var currentY = pairBbox.Bottom + copyDistance;
while (currentY + pairHeight <= workArea.Top + Tolerance.Epsilon)
{
var offset = new Vector(0, currentY - pairBbox.Bottom);
column.Add(part1.CloneAtOffset(offset));
column.Add(part2.CloneAtOffset(offset));
currentY += copyDistance;
}
return column;
}
private double ComputeVerticalCopyDistance(Part p1, Part p2, Box pairBbox)
{
var pairHeight = pairBbox.Length;
// Start the test pair high enough so it doesn't overlap the original pair's bounding box initially.
var startOffset = pairHeight + partSpacing;
var testParts = new List<Part> { p1.CloneAtOffset(new Vector(0, startOffset)), p2.CloneAtOffset(new Vector(0, startOffset)) };
var obstacles = new List<Part> { p1, p2 };
// Use a large work area to prevent edge-clamping during distance measurement.
var largeWorkArea = new Box(workArea.X, workArea.Y - pairHeight, workArea.Width, workArea.Length + pairHeight * 3);
var slide = Compactor.Push(testParts, obstacles, largeWorkArea, partSpacing, PushDirection.Down);
// True copy distance = start - slide. Clamp to BB height + spacing to prevent BB overlap.
var copyDist = startOffset - slide;
return System.Math.Max(copyDist, pairHeight + partSpacing);
}
// --- Step 3: Iterative Adjustment ---
private List<Part> AdjustColumn(
(Part part1, Part part2, Box pairBbox) initialPair,
List<Part> initialColumn,
CancellationToken token)
{
var currentPair = initialPair;
var currentColumn = initialColumn;
var originalWidth = initialPair.pairBbox.Width;
for (var iteration = 0; iteration < MaxIterations; iteration++)
{
if (token.IsCancellationRequested)
break;
var columnBbox = ((IEnumerable<IBoundable>)currentColumn).GetBoundingBox();
var gap = workArea.Top - columnBbox.Top;
if (gap <= Tolerance.Epsilon)
break;
var pairCount = currentColumn.Count / 2;
var adjustment = gap / pairCount;
if (adjustment <= Tolerance.Epsilon)
break;
// Try shifting p2 up or down relative to p1 to see if we can close the gap
// without making the pair wider than its initial horizontal footprint.
var adjusted = TryAdjustPair(currentPair, adjustment, originalWidth);
if (adjusted == null)
break;
var newColumn = BuildColumn(adjusted.Value.part1, adjusted.Value.part2, adjusted.Value.pairBbox);
if (newColumn.Count <= currentColumn.Count)
break; // No improvement in part count.
currentColumn = newColumn;
currentPair = adjusted.Value;
}
return currentColumn;
}
private (Part part1, Part part2, Box pairBbox)? TryAdjustPair(
(Part part1, Part part2, Box pairBbox) pair,
double adjustment, double maxWidth)
{
// Try shifting part2 up first.
var result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation,
(pair.part2.Location.Y - pair.part1.Location.Y) + adjustment);
if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon)
return result;
// Up made it wider or didn't fit — try down instead.
result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation,
(pair.part2.Location.Y - pair.part1.Location.Y) - adjustment);
if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon)
return result;
return null;
}
// --- Step 4: Horizontal Repetition ---
private List<Part> RepeatColumns(List<Part> column, CancellationToken token)
{
if (column.Count == 0)
return column;
var columnBbox = ((IEnumerable<IBoundable>)column).GetBoundingBox();
var columnWidth = columnBbox.Width;
// Create a test column shifted right and compact it left to find the true copy distance.
var startOffset = columnWidth + partSpacing;
var testColumn = column.Select(p => p.CloneAtOffset(new Vector(startOffset, 0))).ToList();
var slide = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left);
var copyDistance = startOffset - slide;
if (copyDistance <= Tolerance.Epsilon)
copyDistance = columnWidth + partSpacing;
Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})");
var result = new List<Part>(column);
var colIndex = 1;
while (!token.IsCancellationRequested)
{
var offset = new Vector(copyDistance * colIndex, 0);
var anyFit = false;
foreach (var part in column)
{
var clone = part.CloneAtOffset(offset);
if (IsWithinWorkArea(clone))
{
result.Add(clone);
anyFit = true;
}
}
if (!anyFit)
break;
colIndex++;
}
return result;
}
private bool IsWithinWorkArea(Part part)
{
return 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;
}
}
}