diff --git a/OpenNest.Engine/FillExtents.cs b/OpenNest.Engine/FillExtents.cs index c200b06..bbcbf47 100644 --- a/OpenNest.Engine/FillExtents.cs +++ b/OpenNest.Engine/FillExtents.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using OpenNest.Geometry; using OpenNest.Math; @@ -297,11 +298,76 @@ namespace OpenNest return (p1, p2, newBbox); } - // --- Step 4: Horizontal Repetition (stub for Task 4) --- + // --- Step 4: Horizontal Repetition --- private List RepeatColumns(List column, CancellationToken token) { - return column; + if (column.Count == 0) + return column; + + var columnBbox = ((IEnumerable)column).GetBoundingBox(); + var columnWidth = columnBbox.Width; + + // Create a test column shifted right by columnWidth + spacing. + var testOffset = columnWidth + partSpacing; + var testColumn = new List(column.Count); + foreach (var part in column) + testColumn.Add(part.CloneAtOffset(new Vector(testOffset, 0))); + + // Compact the test column left against the original column. + 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)testColumn).GetBoundingBox(); + var copyDistance = testBbox.Left - columnBbox.Left; + + if (copyDistance <= Tolerance.Epsilon) + copyDistance = columnWidth + partSpacing; + + Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})"); + + // Build all columns. + var result = new List(column); + + // 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) + { + 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; } } } diff --git a/OpenNest.Tests/FillExtentsTests.cs b/OpenNest.Tests/FillExtentsTests.cs index 8d3931e..3271ec6 100644 --- a/OpenNest.Tests/FillExtentsTests.cs +++ b/OpenNest.Tests/FillExtentsTests.cs @@ -84,4 +84,78 @@ public class FillExtentsTests Assert.True(gap < 1.0, $"Gap of {gap:F2} is too large — adjustment should fill close to the top"); } + + [Fact] + public void Fill_Triangle_FillsWidthWithMultipleColumns() + { + var workArea = new Box(0, 0, 120, 60); + var filler = new FillExtents(workArea, 0.5); + var drawing = MakeRightTriangle(10, 8); + + var parts = filler.Fill(drawing); + + // With a 120-wide sheet and ~10-wide parts, we should get multiple columns. + Assert.True(parts.Count >= 8, + $"Expected multiple columns but got only {parts.Count} parts"); + + // Verify all parts are within bounds. + foreach (var part in parts) + { + Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01); + Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01); + Assert.True(part.BoundingBox.Left >= workArea.Left - 0.01); + Assert.True(part.BoundingBox.Bottom >= workArea.Bottom - 0.01); + } + } + + [Fact] + public void Fill_Rect_ReturnsNonEmpty() + { + var workArea = new Box(0, 0, 120, 60); + var filler = new FillExtents(workArea, 0.5); + var drawing = MakeRect(15, 10); + + var parts = filler.Fill(drawing); + + Assert.NotNull(parts); + Assert.True(parts.Count > 0, "Rectangle should produce results"); + } + + [Fact] + public void Fill_NonZeroOriginWorkArea_PartsWithinBounds() + { + // Simulate a remnant sub-region with non-zero origin. + var workArea = new Box(30, 10, 80, 40); + var filler = new FillExtents(workArea, 0.5); + var drawing = MakeRightTriangle(10, 8); + + var parts = filler.Fill(drawing); + + Assert.True(parts.Count > 0); + + foreach (var part in parts) + { + Assert.True(part.BoundingBox.Left >= workArea.Left - 0.01, + $"Part left {part.BoundingBox.Left} below work area left {workArea.Left}"); + Assert.True(part.BoundingBox.Bottom >= workArea.Bottom - 0.01, + $"Part bottom {part.BoundingBox.Bottom} below work area bottom {workArea.Bottom}"); + Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01); + Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01); + } + } + + [Fact] + public void Fill_RespectsCancellation() + { + var cts = new System.Threading.CancellationTokenSource(); + cts.Cancel(); + + var workArea = new Box(0, 0, 120, 60); + var filler = new FillExtents(workArea, 0.5); + var drawing = MakeRightTriangle(10, 8); + + var parts = filler.Fill(drawing, token: cts.Token); + + Assert.NotNull(parts); + } }