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.
This commit is contained in:
2026-03-18 20:13:55 -04:00
parent 24ed878d8e
commit d1d47b5223
+69 -193
View File
@@ -3,6 +3,7 @@ using OpenNest.Math;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading; using System.Threading;
namespace OpenNest.Engine.Fill namespace OpenNest.Engine.Fill
@@ -13,13 +14,11 @@ namespace OpenNest.Engine.Fill
private readonly Box workArea; private readonly Box workArea;
private readonly double partSpacing; private readonly double partSpacing;
private readonly double halfSpacing;
public FillExtents(Box workArea, double partSpacing) public FillExtents(Box workArea, double partSpacing)
{ {
this.workArea = workArea; this.workArea = workArea;
this.partSpacing = partSpacing; this.partSpacing = partSpacing;
halfSpacing = partSpacing / 2;
} }
public List<Part> Fill(Drawing drawing, double rotationAngle = 0, public List<Part> Fill(Drawing drawing, double rotationAngle = 0,
@@ -27,18 +26,18 @@ namespace OpenNest.Engine.Fill
CancellationToken token = default, CancellationToken token = default,
IProgress<NestProgress> progress = null) IProgress<NestProgress> progress = null)
{ {
var pair = BuildPair(drawing, rotationAngle); var initialPair = CreatePair(drawing, rotationAngle, rotationAngle + System.Math.PI);
if (pair == null) if (initialPair == null)
return new List<Part>(); return new List<Part>();
var column = BuildColumn(pair.Value.part1, pair.Value.part2, pair.Value.pairBbox); var column = BuildColumn(initialPair.Value.part1, initialPair.Value.part2, initialPair.Value.pairBbox);
if (column.Count == 0) if (column.Count == 0)
return new List<Part>(); return new List<Part>();
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber,
column, workArea, $"Extents: initial column {column.Count} parts"); column, workArea, $"Extents: initial column {column.Count} parts");
var adjusted = AdjustColumn(pair.Value, column, token); var adjusted = AdjustColumn(initialPair.Value, column, token);
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber,
adjusted, workArea, $"Extents: adjusted column {adjusted.Count} parts"); adjusted, workArea, $"Extents: adjusted column {adjusted.Count} parts");
@@ -53,52 +52,33 @@ namespace OpenNest.Engine.Fill
// --- Step 1: Pair Construction --- // --- Step 1: Pair Construction ---
private (Part part1, Part part2, Box pairBbox)? BuildPair(Drawing drawing, double rotationAngle) private (Part part1, Part part2, Box pairBbox)? CreatePair(
Drawing drawing, double rotation1, double rotation2, double verticalShift2 = 0)
{ {
var part1 = Part.CreateAtOrigin(drawing, rotationAngle); var p1 = Part.CreateAtOrigin(drawing, rotation1);
var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI); var p2 = Part.CreateAtOrigin(drawing, rotation2);
// Check that each part fits in the work area individually. // Initial positioning: p2 to the right of p1, with optional vertical shift.
if (part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon || p2.Offset(p1.BoundingBox.Width + partSpacing, verticalShift2);
part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon)
return null;
// Slide part2 toward part1 from the right using geometry-aware distance. // Compact p2 left toward p1 using geometry-aware distance.
var boundary1 = new PartBoundary(part1, halfSpacing); Compactor.Push(new List<Part> { p2 }, new List<Part> { p1 }, workArea, partSpacing, PushDirection.Left);
var boundary2 = new PartBoundary(part2, halfSpacing);
// Position part2 to the right of part1 at bounding box width distance. var pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox();
var startOffset = part1.BoundingBox.Width + part2.BoundingBox.Width + partSpacing;
part2.Offset(startOffset, 0);
part2.UpdateBounds();
// Slide part2 left toward part1. // Re-anchor pair to work area origin (bottom-left).
var movingLines = boundary2.GetLines(part2.Location, PushDirection.Left);
var stationaryLines = boundary1.GetLines(part1.Location, PushDirection.Right);
var dist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left);
if (dist < double.MaxValue && dist > 0)
{
part2.Offset(-dist, 0);
part2.UpdateBounds();
}
// Re-anchor pair to work area origin.
var pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
var anchor = new Vector(workArea.X - pairBbox.Left, workArea.Y - pairBbox.Bottom); var anchor = new Vector(workArea.X - pairBbox.Left, workArea.Y - pairBbox.Bottom);
part1.Offset(anchor); p1.Offset(anchor);
part2.Offset(anchor); p2.Offset(anchor);
part1.UpdateBounds();
part2.UpdateBounds();
pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox(); pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox();
// Verify pair fits in work area. // Verify pair fits in work area.
if (pairBbox.Width > workArea.Width + Tolerance.Epsilon || if (pairBbox.Width > workArea.Width + Tolerance.Epsilon ||
pairBbox.Length > workArea.Length + Tolerance.Epsilon) pairBbox.Length > workArea.Length + Tolerance.Epsilon)
return null; return null;
return (part1, part2, pairBbox); return (p1, p2, pairBbox);
} }
// --- Step 2: Build Column (tile vertically) --- // --- Step 2: Build Column (tile vertically) ---
@@ -107,193 +87,104 @@ namespace OpenNest.Engine.Fill
{ {
var column = new List<Part> { (Part)part1.Clone(), (Part)part2.Clone() }; var column = new List<Part> { (Part)part1.Clone(), (Part)part2.Clone() };
// Find geometry-aware copy distance for the pair vertically. var copyDistance = ComputeVerticalCopyDistance(part1, part2, pairBbox);
var boundary1 = new PartBoundary(part1, halfSpacing);
var boundary2 = new PartBoundary(part2, halfSpacing);
// Compute vertical copy distance using bounding boxes as starting point,
// then slide down to find true geometry distance.
var pairHeight = pairBbox.Length;
var testOffset = new Vector(0, pairHeight);
// Create test parts for slide distance measurement.
var testPart1 = part1.CloneAtOffset(testOffset);
var testPart2 = part2.CloneAtOffset(testOffset);
// Find minimum distance from test pair sliding down toward original pair.
var copyDistance = FindVerticalCopyDistance(
part1, part2, testPart1, testPart2,
boundary1, boundary2, pairHeight);
if (copyDistance <= 0) if (copyDistance <= 0)
return column; return column;
var count = 1; var pairHeight = pairBbox.Length;
while (true) var currentY = pairBbox.Bottom + copyDistance;
{
var nextBottom = pairBbox.Bottom + copyDistance * count;
if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon)
break;
var offset = new Vector(0, copyDistance * count); while (currentY + pairHeight <= workArea.Top + Tolerance.Epsilon)
{
var offset = new Vector(0, currentY - pairBbox.Bottom);
column.Add(part1.CloneAtOffset(offset)); column.Add(part1.CloneAtOffset(offset));
column.Add(part2.CloneAtOffset(offset)); column.Add(part2.CloneAtOffset(offset));
count++; currentY += copyDistance;
} }
return column; return column;
} }
private double FindVerticalCopyDistance( private double ComputeVerticalCopyDistance(Part p1, Part p2, Box pairBbox)
Part origPart1, Part origPart2,
Part testPart1, Part testPart2,
PartBoundary boundary1, PartBoundary boundary2,
double pairHeight)
{ {
// Check all 4 combinations: test parts sliding down toward original parts. var pairHeight = pairBbox.Length;
var minSlide = double.MaxValue; // 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 };
// Test1 -> Orig1 // Use a large work area to prevent edge-clamping during distance measurement.
var d = SlideDistance(boundary1, testPart1.Location, boundary1, origPart1.Location, PushDirection.Down); var largeWorkArea = new Box(workArea.X, workArea.Y - pairHeight, workArea.Width, workArea.Length + pairHeight * 3);
if (d < minSlide) minSlide = d; var slide = Compactor.Push(testParts, obstacles, largeWorkArea, partSpacing, PushDirection.Down);
// Test1 -> Orig2
d = SlideDistance(boundary1, testPart1.Location, boundary2, origPart2.Location, PushDirection.Down);
if (d < minSlide) minSlide = d;
// Test2 -> Orig1
d = SlideDistance(boundary2, testPart2.Location, boundary1, origPart1.Location, PushDirection.Down);
if (d < minSlide) minSlide = d;
// Test2 -> Orig2
d = SlideDistance(boundary2, testPart2.Location, boundary2, origPart2.Location, PushDirection.Down);
if (d < minSlide) minSlide = d;
if (minSlide >= double.MaxValue || minSlide < 0)
return pairHeight + partSpacing;
// Match FillLinear.ComputeCopyDistance: copyDist = startOffset - slide,
// clamped so it never goes below pairHeight + partSpacing to prevent
// bounding-box overlap from spurious slide values.
var copyDist = pairHeight - minSlide;
// 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); return System.Math.Max(copyDist, pairHeight + partSpacing);
} }
private static double SlideDistance(
PartBoundary movingBoundary, Vector movingLocation,
PartBoundary stationaryBoundary, Vector stationaryLocation,
PushDirection direction)
{
var opposite = SpatialQuery.OppositeDirection(direction);
var movingEdges = movingBoundary.GetEdges(direction);
var stationaryEdges = stationaryBoundary.GetEdges(opposite);
return SpatialQuery.DirectionalDistance(
movingEdges, movingLocation,
stationaryEdges, stationaryLocation,
direction);
}
// --- Step 3: Iterative Adjustment --- // --- Step 3: Iterative Adjustment ---
private List<Part> AdjustColumn( private List<Part> AdjustColumn(
(Part part1, Part part2, Box pairBbox) pair, (Part part1, Part part2, Box pairBbox) initialPair,
List<Part> column, List<Part> initialColumn,
CancellationToken token) CancellationToken token)
{ {
var originalPairWidth = pair.pairBbox.Width; var currentPair = initialPair;
var currentColumn = initialColumn;
var originalWidth = initialPair.pairBbox.Width;
for (var iteration = 0; iteration < MaxIterations; iteration++) for (var iteration = 0; iteration < MaxIterations; iteration++)
{ {
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
break; break;
// Measure current gap. var columnBbox = ((IEnumerable<IBoundable>)currentColumn).GetBoundingBox();
var topEdge = double.MinValue; var gap = workArea.Top - columnBbox.Top;
foreach (var p in column)
if (p.BoundingBox.Top > topEdge)
topEdge = p.BoundingBox.Top;
var gap = workArea.Top - topEdge;
if (gap <= Tolerance.Epsilon) if (gap <= Tolerance.Epsilon)
break; break;
var pairCount = column.Count / 2; var pairCount = currentColumn.Count / 2;
if (pairCount <= 0)
break;
var adjustment = gap / pairCount; var adjustment = gap / pairCount;
if (adjustment <= Tolerance.Epsilon) if (adjustment <= Tolerance.Epsilon)
break; break;
// Try adjusting the pair and rebuilding the column. // Try shifting p2 up or down relative to p1 to see if we can close the gap
var adjusted = TryAdjustPair(pair, adjustment, originalPairWidth); // without making the pair wider than its initial horizontal footprint.
var adjusted = TryAdjustPair(currentPair, adjustment, originalWidth);
if (adjusted == null) if (adjusted == null)
break; break;
var newColumn = BuildColumn(adjusted.Value.part1, adjusted.Value.part2, adjusted.Value.pairBbox); var newColumn = BuildColumn(adjusted.Value.part1, adjusted.Value.part2, adjusted.Value.pairBbox);
if (newColumn.Count == 0) if (newColumn.Count <= currentColumn.Count)
break; break; // No improvement in part count.
column = newColumn; currentColumn = newColumn;
pair = adjusted.Value; currentPair = adjusted.Value;
} }
return column; return currentColumn;
} }
private (Part part1, Part part2, Box pairBbox)? TryAdjustPair( private (Part part1, Part part2, Box pairBbox)? TryAdjustPair(
(Part part1, Part part2, Box pairBbox) pair, (Part part1, Part part2, Box pairBbox) pair,
double adjustment, double originalPairWidth) double adjustment, double maxWidth)
{ {
// Try shifting part2 up first. // Try shifting part2 up first.
var result = TryShiftDirection(pair, adjustment, originalPairWidth); var result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation,
(pair.part2.Location.Y - pair.part1.Location.Y) + adjustment);
if (result != null) if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon)
return result; return result;
// Up made the pair wider — try down instead. // Up made it wider or didn't fit — try down instead.
return TryShiftDirection(pair, -adjustment, originalPairWidth); result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation,
} (pair.part2.Location.Y - pair.part1.Location.Y) - adjustment);
private (Part part1, Part part2, Box pairBbox)? TryShiftDirection( if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon)
(Part part1, Part part2, Box pairBbox) pair, return result;
double verticalShift, double originalPairWidth)
{
// Clone parts so we don't mutate the originals.
var p1 = (Part)pair.part1.Clone();
var p2 = (Part)pair.part2.Clone();
// Separate: shift part2 right so bounding boxes don't touch. return null;
p2.Offset(partSpacing, 0);
p2.UpdateBounds();
// Apply the vertical shift.
p2.Offset(0, verticalShift);
p2.UpdateBounds();
// Compact part2 left toward part1.
var moving = new List<Part> { p2 };
var obstacles = new List<Part> { p1 };
Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left);
// Check if the pair got wider.
var newBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox();
if (newBbox.Width > originalPairWidth + Tolerance.Epsilon)
return null;
// Re-anchor to work area origin.
var anchor = new Vector(workArea.X - newBbox.Left, workArea.Y - newBbox.Bottom);
p1.Offset(anchor);
p2.Offset(anchor);
p1.UpdateBounds();
p2.UpdateBounds();
newBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox();
return (p1, p2, newBbox);
} }
// --- Step 4: Horizontal Repetition --- // --- Step 4: Horizontal Repetition ---
@@ -306,36 +197,21 @@ namespace OpenNest.Engine.Fill
var columnBbox = ((IEnumerable<IBoundable>)column).GetBoundingBox(); var columnBbox = ((IEnumerable<IBoundable>)column).GetBoundingBox();
var columnWidth = columnBbox.Width; var columnWidth = columnBbox.Width;
// Create a test column shifted right by columnWidth + spacing. // Create a test column shifted right and compact it left to find the true copy distance.
var testOffset = columnWidth + partSpacing; var startOffset = columnWidth + partSpacing;
var testColumn = new List<Part>(column.Count); var testColumn = column.Select(p => p.CloneAtOffset(new Vector(startOffset, 0))).ToList();
foreach (var part in column)
testColumn.Add(part.CloneAtOffset(new Vector(testOffset, 0)));
// Compact the test column left against the original column. var slide = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left);
var distanceMoved = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left); var copyDistance = startOffset - slide;
// Derive the true copy distance from where the test column ended up.
var testBbox = ((IEnumerable<IBoundable>)testColumn).GetBoundingBox();
var copyDistance = testBbox.Left - columnBbox.Left;
if (copyDistance <= Tolerance.Epsilon) if (copyDistance <= Tolerance.Epsilon)
copyDistance = columnWidth + partSpacing; copyDistance = columnWidth + partSpacing;
Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})"); Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})");
// Build all columns.
var result = new List<Part>(column); var result = new List<Part>(column);
var colIndex = 1;
// Add the test column we already computed as column 2.
foreach (var part in testColumn)
{
if (IsWithinWorkArea(part))
result.Add(part);
}
// Tile additional columns at the copy distance.
var colIndex = 2;
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
var offset = new Vector(copyDistance * colIndex, 0); var offset = new Vector(copyDistance * colIndex, 0);