Revert "refactor(engine): simplify FillExtents logic using Compactor.Push"

This reverts commit d1d47b5223.
This commit is contained in:
2026-03-18 20:17:57 -04:00
parent 794ef16629
commit dddc890a96

View File

@@ -3,7 +3,6 @@ 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
@@ -14,11 +13,13 @@ 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,
@@ -26,18 +27,18 @@ namespace OpenNest.Engine.Fill
CancellationToken token = default, CancellationToken token = default,
IProgress<NestProgress> progress = null) IProgress<NestProgress> progress = null)
{ {
var initialPair = CreatePair(drawing, rotationAngle, rotationAngle + System.Math.PI); var pair = BuildPair(drawing, rotationAngle);
if (initialPair == null) if (pair == null)
return new List<Part>(); return new List<Part>();
var column = BuildColumn(initialPair.Value.part1, initialPair.Value.part2, initialPair.Value.pairBbox); var column = BuildColumn(pair.Value.part1, pair.Value.part2, pair.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(initialPair.Value, column, token); var adjusted = AdjustColumn(pair.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");
@@ -52,33 +53,52 @@ namespace OpenNest.Engine.Fill
// --- Step 1: Pair Construction --- // --- Step 1: Pair Construction ---
private (Part part1, Part part2, Box pairBbox)? CreatePair( private (Part part1, Part part2, Box pairBbox)? BuildPair(Drawing drawing, double rotationAngle)
Drawing drawing, double rotation1, double rotation2, double verticalShift2 = 0)
{ {
var p1 = Part.CreateAtOrigin(drawing, rotation1); var part1 = Part.CreateAtOrigin(drawing, rotationAngle);
var p2 = Part.CreateAtOrigin(drawing, rotation2); var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI);
// Initial positioning: p2 to the right of p1, with optional vertical shift. // Check that each part fits in the work area individually.
p2.Offset(p1.BoundingBox.Width + partSpacing, verticalShift2); if (part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon ||
part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon)
return null;
// Compact p2 left toward p1 using geometry-aware distance. // Slide part2 toward part1 from the right using geometry-aware distance.
Compactor.Push(new List<Part> { p2 }, new List<Part> { p1 }, workArea, partSpacing, PushDirection.Left); var boundary1 = new PartBoundary(part1, halfSpacing);
var boundary2 = new PartBoundary(part2, halfSpacing);
var pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox(); // Position part2 to the right of part1 at bounding box width distance.
var startOffset = part1.BoundingBox.Width + part2.BoundingBox.Width + partSpacing;
part2.Offset(startOffset, 0);
part2.UpdateBounds();
// Re-anchor pair to work area origin (bottom-left). // Slide part2 left toward part1.
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);
p1.Offset(anchor); part1.Offset(anchor);
p2.Offset(anchor); part2.Offset(anchor);
part1.UpdateBounds();
part2.UpdateBounds();
pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox(); pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).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 (p1, p2, pairBbox); return (part1, part2, pairBbox);
} }
// --- Step 2: Build Column (tile vertically) --- // --- Step 2: Build Column (tile vertically) ---
@@ -87,104 +107,193 @@ 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() };
var copyDistance = ComputeVerticalCopyDistance(part1, part2, pairBbox); // Find geometry-aware copy distance for the pair vertically.
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 pairHeight = pairBbox.Length; var count = 1;
var currentY = pairBbox.Bottom + copyDistance; while (true)
while (currentY + pairHeight <= workArea.Top + Tolerance.Epsilon)
{ {
var offset = new Vector(0, currentY - pairBbox.Bottom); var nextBottom = pairBbox.Bottom + copyDistance * count;
if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon)
break;
var offset = new Vector(0, copyDistance * count);
column.Add(part1.CloneAtOffset(offset)); column.Add(part1.CloneAtOffset(offset));
column.Add(part2.CloneAtOffset(offset)); column.Add(part2.CloneAtOffset(offset));
currentY += copyDistance; count++;
} }
return column; return column;
} }
private double ComputeVerticalCopyDistance(Part p1, Part p2, Box pairBbox) private double FindVerticalCopyDistance(
Part origPart1, Part origPart2,
Part testPart1, Part testPart2,
PartBoundary boundary1, PartBoundary boundary2,
double pairHeight)
{ {
var pairHeight = pairBbox.Length; // Check all 4 combinations: test parts sliding down toward original parts.
// Start the test pair high enough so it doesn't overlap the original pair's bounding box initially. var minSlide = double.MaxValue;
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. // Test1 -> Orig1
var largeWorkArea = new Box(workArea.X, workArea.Y - pairHeight, workArea.Width, workArea.Length + pairHeight * 3); var d = SlideDistance(boundary1, testPart1.Location, boundary1, origPart1.Location, PushDirection.Down);
var slide = Compactor.Push(testParts, obstacles, largeWorkArea, partSpacing, PushDirection.Down); if (d < minSlide) minSlide = d;
// 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) initialPair, (Part part1, Part part2, Box pairBbox) pair,
List<Part> initialColumn, List<Part> column,
CancellationToken token) CancellationToken token)
{ {
var currentPair = initialPair; var originalPairWidth = pair.pairBbox.Width;
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;
var columnBbox = ((IEnumerable<IBoundable>)currentColumn).GetBoundingBox(); // Measure current gap.
var gap = workArea.Top - columnBbox.Top; var topEdge = double.MinValue;
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 = currentColumn.Count / 2; var pairCount = column.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 shifting p2 up or down relative to p1 to see if we can close the gap // Try adjusting the pair and rebuilding the column.
// without making the pair wider than its initial horizontal footprint. var adjusted = TryAdjustPair(pair, adjustment, originalPairWidth);
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 <= currentColumn.Count) if (newColumn.Count == 0)
break; // No improvement in part count. break;
currentColumn = newColumn; column = newColumn;
currentPair = adjusted.Value; pair = adjusted.Value;
} }
return currentColumn; return column;
} }
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 maxWidth) double adjustment, double originalPairWidth)
{ {
// Try shifting part2 up first. // Try shifting part2 up first.
var result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation, var result = TryShiftDirection(pair, adjustment, originalPairWidth);
(pair.part2.Location.Y - pair.part1.Location.Y) + adjustment);
if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon) if (result != null)
return result; return result;
// Up made it wider or didn't fit — try down instead. // Up made the pair wider — try down instead.
result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation, return TryShiftDirection(pair, -adjustment, originalPairWidth);
(pair.part2.Location.Y - pair.part1.Location.Y) - adjustment); }
if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon) private (Part part1, Part part2, Box pairBbox)? TryShiftDirection(
return result; (Part part1, Part part2, Box pairBbox) pair,
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();
return null; // Separate: shift part2 right so bounding boxes don't touch.
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 ---
@@ -197,21 +306,36 @@ 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 and compact it left to find the true copy distance. // Create a test column shifted right by columnWidth + spacing.
var startOffset = columnWidth + partSpacing; var testOffset = columnWidth + partSpacing;
var testColumn = column.Select(p => p.CloneAtOffset(new Vector(startOffset, 0))).ToList(); var testColumn = new List<Part>(column.Count);
foreach (var part in column)
testColumn.Add(part.CloneAtOffset(new Vector(testOffset, 0)));
var slide = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left); // Compact the test column left against the original column.
var copyDistance = startOffset - slide; var distanceMoved = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left);
// 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);