feat(engine): implement FillExtents horizontal column repetition
Replace RepeatColumns stub with real implementation: compacts a test column left against column 1 to derive the copy distance, tiles further columns at that interval, and clips partial columns to the work area bounds. Adds 4 new FillExtentsTests covering multi-column fill, rect shapes, non-zero-origin work areas, and cancellation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
@@ -297,11 +298,76 @@ namespace OpenNest
|
|||||||
return (p1, p2, newBbox);
|
return (p1, p2, newBbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Step 4: Horizontal Repetition (stub for Task 4) ---
|
// --- Step 4: Horizontal Repetition ---
|
||||||
|
|
||||||
private List<Part> RepeatColumns(List<Part> column, CancellationToken token)
|
private List<Part> RepeatColumns(List<Part> column, CancellationToken token)
|
||||||
{
|
{
|
||||||
return column;
|
if (column.Count == 0)
|
||||||
|
return column;
|
||||||
|
|
||||||
|
var columnBbox = ((IEnumerable<IBoundable>)column).GetBoundingBox();
|
||||||
|
var columnWidth = columnBbox.Width;
|
||||||
|
|
||||||
|
// Create a test column shifted right by columnWidth + spacing.
|
||||||
|
var testOffset = columnWidth + partSpacing;
|
||||||
|
var testColumn = new List<Part>(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<IBoundable>)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<Part>(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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,4 +84,78 @@ public class FillExtentsTests
|
|||||||
Assert.True(gap < 1.0,
|
Assert.True(gap < 1.0,
|
||||||
$"Gap of {gap:F2} is too large — adjustment should fill close to the top");
|
$"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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user