fix(engine): fix remnant finder missing L-shaped and split remnants
Two bugs caused the remnant finder to miss valid empty regions: 1. RemoveDominated used an 80% overlap-area threshold that incorrectly removed L-shaped remnants. A tall strip to one side would "dominate" wide strips above/below it even though they represent different usable space. Replaced with geometric containment check — only remove a box if it's fully inside a larger one. 2. FindTieredRemnants split remnants at the obstacle envelope boundary, and both pieces could fall below minDimension even though the original remnant passed the filter (e.g., 6.6" remnant split into 5.35" + 1.25" with minDim=5.38"). Added fallback to keep the original unsplit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,12 +5,37 @@ using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// A remnant box with a priority tier.
|
||||
/// 0 = within the used envelope (best), 1 = extends past one edge, 2 = fully outside.
|
||||
/// </summary>
|
||||
public struct TieredRemnant
|
||||
{
|
||||
public Box Box;
|
||||
public int Priority;
|
||||
|
||||
public TieredRemnant(Box box, int priority)
|
||||
{
|
||||
Box = box;
|
||||
Priority = priority;
|
||||
}
|
||||
}
|
||||
|
||||
public class RemnantFinder
|
||||
{
|
||||
private readonly Box workArea;
|
||||
|
||||
public List<Box> Obstacles { get; } = new();
|
||||
|
||||
private struct CellGrid
|
||||
{
|
||||
public bool[,] Empty;
|
||||
public List<double> XCoords;
|
||||
public List<double> YCoords;
|
||||
public int Rows;
|
||||
public int Cols;
|
||||
}
|
||||
|
||||
public RemnantFinder(Box workArea, List<Box> obstacles = null)
|
||||
{
|
||||
this.workArea = workArea;
|
||||
@@ -27,54 +52,52 @@ namespace OpenNest
|
||||
|
||||
public List<Box> FindRemnants(double minDimension = 0)
|
||||
{
|
||||
var xs = new SortedSet<double> { workArea.Left, workArea.Right };
|
||||
var ys = new SortedSet<double> { workArea.Bottom, workArea.Top };
|
||||
var grid = BuildGrid();
|
||||
|
||||
foreach (var obs in Obstacles)
|
||||
{
|
||||
var clipped = ClipToWorkArea(obs);
|
||||
if (clipped.Width <= 0 || clipped.Length <= 0)
|
||||
continue;
|
||||
|
||||
xs.Add(clipped.Left);
|
||||
xs.Add(clipped.Right);
|
||||
ys.Add(clipped.Bottom);
|
||||
ys.Add(clipped.Top);
|
||||
}
|
||||
|
||||
var xList = xs.ToList();
|
||||
var yList = ys.ToList();
|
||||
|
||||
var cols = xList.Count - 1;
|
||||
var rows = yList.Count - 1;
|
||||
|
||||
if (cols <= 0 || rows <= 0)
|
||||
if (grid.Rows <= 0 || grid.Cols <= 0)
|
||||
return new List<Box>();
|
||||
|
||||
var empty = new bool[rows, cols];
|
||||
var merged = MergeCells(grid);
|
||||
var sized = FilterBySize(merged, minDimension);
|
||||
var unique = RemoveDominated(sized);
|
||||
SortByEdgeProximity(unique);
|
||||
return unique;
|
||||
}
|
||||
|
||||
for (var r = 0; r < rows; r++)
|
||||
/// <summary>
|
||||
/// Finds remnants and splits them into priority tiers based on the
|
||||
/// bounding box of all placed parts (the "used envelope").
|
||||
/// Priority 0: fully within the used envelope — compact, preferred.
|
||||
/// Priority 1: extends past one edge of the envelope.
|
||||
/// Priority 2: fully outside the envelope — last resort.
|
||||
/// </summary>
|
||||
public List<TieredRemnant> FindTieredRemnants(double minDimension = 0)
|
||||
{
|
||||
var remnants = FindRemnants(minDimension);
|
||||
|
||||
if (Obstacles.Count == 0 || remnants.Count == 0)
|
||||
return remnants.Select(r => new TieredRemnant(r, 0)).ToList();
|
||||
|
||||
var envelope = ComputeEnvelope();
|
||||
var results = new List<TieredRemnant>();
|
||||
|
||||
foreach (var remnant in remnants)
|
||||
{
|
||||
for (var c = 0; c < cols; c++)
|
||||
{
|
||||
var cell = new Box(xList[c], yList[r],
|
||||
xList[c + 1] - xList[c], yList[r + 1] - yList[r]);
|
||||
var before = results.Count;
|
||||
SplitAtEnvelope(remnant, envelope, minDimension, results);
|
||||
|
||||
empty[r, c] = !OverlapsAnyObstacle(cell);
|
||||
}
|
||||
// If all splits fell below minDim, keep the original unsplit.
|
||||
if (results.Count == before)
|
||||
results.Add(new TieredRemnant(remnant, 1));
|
||||
}
|
||||
|
||||
var merged = MergeCells(empty, xList, yList, rows, cols);
|
||||
|
||||
var results = new List<Box>();
|
||||
|
||||
foreach (var box in merged)
|
||||
results.Sort((a, b) =>
|
||||
{
|
||||
if (box.Width >= minDimension && box.Length >= minDimension)
|
||||
results.Add(box);
|
||||
}
|
||||
if (a.Priority != b.Priority)
|
||||
return a.Priority.CompareTo(b.Priority);
|
||||
return b.Box.Area().CompareTo(a.Box.Area());
|
||||
});
|
||||
|
||||
results.Sort((a, b) => b.Area().CompareTo(a.Area()));
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -88,6 +111,231 @@ namespace OpenNest
|
||||
return new RemnantFinder(plate.WorkArea(), obstacles);
|
||||
}
|
||||
|
||||
private CellGrid BuildGrid()
|
||||
{
|
||||
var clipped = ClipObstacles();
|
||||
|
||||
var xs = new SortedSet<double> { workArea.Left, workArea.Right };
|
||||
var ys = new SortedSet<double> { workArea.Bottom, workArea.Top };
|
||||
|
||||
foreach (var obs in clipped)
|
||||
{
|
||||
xs.Add(obs.Left);
|
||||
xs.Add(obs.Right);
|
||||
ys.Add(obs.Bottom);
|
||||
ys.Add(obs.Top);
|
||||
}
|
||||
|
||||
var grid = new CellGrid
|
||||
{
|
||||
XCoords = xs.ToList(),
|
||||
YCoords = ys.ToList(),
|
||||
};
|
||||
|
||||
grid.Cols = grid.XCoords.Count - 1;
|
||||
grid.Rows = grid.YCoords.Count - 1;
|
||||
|
||||
if (grid.Cols <= 0 || grid.Rows <= 0)
|
||||
{
|
||||
grid.Empty = new bool[0, 0];
|
||||
return grid;
|
||||
}
|
||||
|
||||
grid.Empty = new bool[grid.Rows, grid.Cols];
|
||||
|
||||
for (var r = 0; r < grid.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < grid.Cols; c++)
|
||||
{
|
||||
var cell = new Box(grid.XCoords[c], grid.YCoords[r],
|
||||
grid.XCoords[c + 1] - grid.XCoords[c],
|
||||
grid.YCoords[r + 1] - grid.YCoords[r]);
|
||||
|
||||
grid.Empty[r, c] = !OverlapsAny(cell, clipped);
|
||||
}
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
private List<Box> ClipObstacles()
|
||||
{
|
||||
var clipped = new List<Box>(Obstacles.Count);
|
||||
|
||||
foreach (var obs in Obstacles)
|
||||
{
|
||||
var c = ClipToWorkArea(obs);
|
||||
if (c.Width > 0 && c.Length > 0)
|
||||
clipped.Add(c);
|
||||
}
|
||||
|
||||
return clipped;
|
||||
}
|
||||
|
||||
private static bool OverlapsAny(Box cell, List<Box> obstacles)
|
||||
{
|
||||
foreach (var obs in obstacles)
|
||||
{
|
||||
if (cell.Left < obs.Right && cell.Right > obs.Left &&
|
||||
cell.Bottom < obs.Top && cell.Top > obs.Bottom)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<Box> FilterBySize(List<Box> boxes, double minDimension)
|
||||
{
|
||||
if (minDimension <= 0)
|
||||
return boxes;
|
||||
|
||||
var result = new List<Box>();
|
||||
|
||||
foreach (var box in boxes)
|
||||
{
|
||||
if (box.Width >= minDimension && box.Length >= minDimension)
|
||||
result.Add(box);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<Box> RemoveDominated(List<Box> boxes)
|
||||
{
|
||||
boxes.Sort((a, b) => b.Area().CompareTo(a.Area()));
|
||||
var results = new List<Box>();
|
||||
|
||||
foreach (var box in boxes)
|
||||
{
|
||||
var dominated = false;
|
||||
|
||||
foreach (var larger in results)
|
||||
{
|
||||
if (IsContainedIn(box, larger))
|
||||
{
|
||||
dominated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dominated)
|
||||
results.Add(box);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static bool IsContainedIn(Box inner, Box outer)
|
||||
{
|
||||
var eps = Math.Tolerance.Epsilon;
|
||||
return inner.Left >= outer.Left - eps &&
|
||||
inner.Right <= outer.Right + eps &&
|
||||
inner.Bottom >= outer.Bottom - eps &&
|
||||
inner.Top <= outer.Top + eps;
|
||||
}
|
||||
|
||||
private void SortByEdgeProximity(List<Box> boxes)
|
||||
{
|
||||
boxes.Sort((a, b) =>
|
||||
{
|
||||
var aEdge = TouchesEdge(a) ? 1 : 0;
|
||||
var bEdge = TouchesEdge(b) ? 1 : 0;
|
||||
|
||||
if (aEdge != bEdge)
|
||||
return bEdge.CompareTo(aEdge);
|
||||
|
||||
return b.Area().CompareTo(a.Area());
|
||||
});
|
||||
}
|
||||
|
||||
private bool TouchesEdge(Box box)
|
||||
{
|
||||
return box.Left <= workArea.Left + Math.Tolerance.Epsilon
|
||||
|| box.Right >= workArea.Right - Math.Tolerance.Epsilon
|
||||
|| box.Bottom <= workArea.Bottom + Math.Tolerance.Epsilon
|
||||
|| box.Top >= workArea.Top - Math.Tolerance.Epsilon;
|
||||
}
|
||||
|
||||
private Box ComputeEnvelope()
|
||||
{
|
||||
var envLeft = double.MaxValue;
|
||||
var envBottom = double.MaxValue;
|
||||
var envRight = double.MinValue;
|
||||
var envTop = double.MinValue;
|
||||
|
||||
foreach (var obs in Obstacles)
|
||||
{
|
||||
if (obs.Left < envLeft) envLeft = obs.Left;
|
||||
if (obs.Bottom < envBottom) envBottom = obs.Bottom;
|
||||
if (obs.Right > envRight) envRight = obs.Right;
|
||||
if (obs.Top > envTop) envTop = obs.Top;
|
||||
}
|
||||
|
||||
return new Box(envLeft, envBottom, envRight - envLeft, envTop - envBottom);
|
||||
}
|
||||
|
||||
private static void SplitAtEnvelope(Box remnant, Box envelope, double minDim, List<TieredRemnant> results)
|
||||
{
|
||||
var eps = Math.Tolerance.Epsilon;
|
||||
|
||||
// Fully within the envelope.
|
||||
if (remnant.Left >= envelope.Left - eps && remnant.Right <= envelope.Right + eps &&
|
||||
remnant.Bottom >= envelope.Bottom - eps && remnant.Top <= envelope.Top + eps)
|
||||
{
|
||||
results.Add(new TieredRemnant(remnant, 0));
|
||||
return;
|
||||
}
|
||||
|
||||
// Fully outside the envelope (no overlap).
|
||||
if (remnant.Left >= envelope.Right - eps || remnant.Right <= envelope.Left + eps ||
|
||||
remnant.Bottom >= envelope.Top - eps || remnant.Top <= envelope.Bottom + eps)
|
||||
{
|
||||
results.Add(new TieredRemnant(remnant, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Partially overlapping — split at envelope edges.
|
||||
var innerLeft = System.Math.Max(remnant.Left, envelope.Left);
|
||||
var innerBottom = System.Math.Max(remnant.Bottom, envelope.Bottom);
|
||||
var innerRight = System.Math.Min(remnant.Right, envelope.Right);
|
||||
var innerTop = System.Math.Min(remnant.Top, envelope.Top);
|
||||
|
||||
// Inner portion (priority 0).
|
||||
TryAdd(results, innerLeft, innerBottom, innerRight - innerLeft, innerTop - innerBottom, 0, minDim);
|
||||
|
||||
// Edge extensions (priority 1).
|
||||
if (remnant.Right > envelope.Right + eps)
|
||||
TryAdd(results, envelope.Right, remnant.Bottom, remnant.Right - envelope.Right, remnant.Length, 1, minDim);
|
||||
|
||||
if (remnant.Left < envelope.Left - eps)
|
||||
TryAdd(results, remnant.Left, remnant.Bottom, envelope.Left - remnant.Left, remnant.Length, 1, minDim);
|
||||
|
||||
if (remnant.Top > envelope.Top + eps)
|
||||
TryAdd(results, innerLeft, envelope.Top, innerRight - innerLeft, remnant.Top - envelope.Top, 1, minDim);
|
||||
|
||||
if (remnant.Bottom < envelope.Bottom - eps)
|
||||
TryAdd(results, innerLeft, remnant.Bottom, innerRight - innerLeft, envelope.Bottom - remnant.Bottom, 1, minDim);
|
||||
|
||||
// Corner extensions (priority 2).
|
||||
if (remnant.Right > envelope.Right + eps && remnant.Top > envelope.Top + eps)
|
||||
TryAdd(results, envelope.Right, envelope.Top, remnant.Right - envelope.Right, remnant.Top - envelope.Top, 2, minDim);
|
||||
|
||||
if (remnant.Right > envelope.Right + eps && remnant.Bottom < envelope.Bottom - eps)
|
||||
TryAdd(results, envelope.Right, remnant.Bottom, remnant.Right - envelope.Right, envelope.Bottom - remnant.Bottom, 2, minDim);
|
||||
|
||||
if (remnant.Left < envelope.Left - eps && remnant.Top > envelope.Top + eps)
|
||||
TryAdd(results, remnant.Left, envelope.Top, envelope.Left - remnant.Left, remnant.Top - envelope.Top, 2, minDim);
|
||||
|
||||
if (remnant.Left < envelope.Left - eps && remnant.Bottom < envelope.Bottom - eps)
|
||||
TryAdd(results, remnant.Left, remnant.Bottom, envelope.Left - remnant.Left, envelope.Bottom - remnant.Bottom, 2, minDim);
|
||||
}
|
||||
|
||||
private static void TryAdd(List<TieredRemnant> results, double x, double y, double w, double h, int priority, double minDim)
|
||||
{
|
||||
if (w >= minDim && h >= minDim)
|
||||
results.Add(new TieredRemnant(new Box(x, y, w, h), priority));
|
||||
}
|
||||
|
||||
private Box ClipToWorkArea(Box obs)
|
||||
{
|
||||
var left = System.Math.Max(obs.Left, workArea.Left);
|
||||
@@ -101,72 +349,49 @@ namespace OpenNest
|
||||
return new Box(left, bottom, right - left, top - bottom);
|
||||
}
|
||||
|
||||
private bool OverlapsAnyObstacle(Box cell)
|
||||
/// <summary>
|
||||
/// Finds maximal empty rectangles using the histogram method.
|
||||
/// For each row, builds a height histogram of consecutive empty cells
|
||||
/// above, then extracts the largest rectangles from the histogram.
|
||||
/// </summary>
|
||||
private static List<Box> MergeCells(CellGrid grid)
|
||||
{
|
||||
foreach (var obs in Obstacles)
|
||||
var height = new int[grid.Rows, grid.Cols];
|
||||
|
||||
for (var c = 0; c < grid.Cols; c++)
|
||||
{
|
||||
var clipped = ClipToWorkArea(obs);
|
||||
|
||||
if (clipped.Width <= 0 || clipped.Length <= 0)
|
||||
continue;
|
||||
|
||||
if (cell.Left < clipped.Right &&
|
||||
cell.Right > clipped.Left &&
|
||||
cell.Bottom < clipped.Top &&
|
||||
cell.Top > clipped.Bottom)
|
||||
return true;
|
||||
for (var r = 0; r < grid.Rows; r++)
|
||||
height[r, c] = grid.Empty[r, c] ? (r > 0 ? height[r - 1, c] + 1 : 1) : 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
var candidates = new List<Box>();
|
||||
|
||||
private static List<Box> MergeCells(bool[,] empty, List<double> xList, List<double> yList, int rows, int cols)
|
||||
{
|
||||
var used = new bool[rows, cols];
|
||||
var results = new List<Box>();
|
||||
|
||||
for (var r = 0; r < rows; r++)
|
||||
for (var r = 0; r < grid.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < cols; c++)
|
||||
var stack = new Stack<(int startCol, int h)>();
|
||||
|
||||
for (var c = 0; c <= grid.Cols; c++)
|
||||
{
|
||||
if (!empty[r, c] || used[r, c])
|
||||
continue;
|
||||
var h = c < grid.Cols ? height[r, c] : 0;
|
||||
var startCol = c;
|
||||
|
||||
var maxC = c;
|
||||
while (maxC + 1 < cols && empty[r, maxC + 1] && !used[r, maxC + 1])
|
||||
maxC++;
|
||||
|
||||
var maxR = r;
|
||||
while (maxR + 1 < rows)
|
||||
while (stack.Count > 0 && stack.Peek().h > h)
|
||||
{
|
||||
var rowOk = true;
|
||||
for (var cc = c; cc <= maxC; cc++)
|
||||
{
|
||||
if (!empty[maxR + 1, cc] || used[maxR + 1, cc])
|
||||
{
|
||||
rowOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var top = stack.Pop();
|
||||
startCol = top.startCol;
|
||||
|
||||
if (!rowOk) break;
|
||||
maxR++;
|
||||
candidates.Add(new Box(
|
||||
grid.XCoords[top.startCol], grid.YCoords[r - top.h + 1],
|
||||
grid.XCoords[c] - grid.XCoords[top.startCol],
|
||||
grid.YCoords[r + 1] - grid.YCoords[r - top.h + 1]));
|
||||
}
|
||||
|
||||
for (var rr = r; rr <= maxR; rr++)
|
||||
for (var cc = c; cc <= maxC; cc++)
|
||||
used[rr, cc] = true;
|
||||
|
||||
var box = new Box(
|
||||
xList[c], yList[r],
|
||||
xList[maxC + 1] - xList[c],
|
||||
yList[maxR + 1] - yList[r]);
|
||||
|
||||
results.Add(box);
|
||||
if (h > 0)
|
||||
stack.Push((startCol, h));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
return candidates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
@@ -269,4 +270,101 @@ public class RemnantFinderTests
|
||||
// Should find gaps between obstacles
|
||||
Assert.True(remnants.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleObstacle_NearEdge_FindsRemnantsOnAllSides()
|
||||
{
|
||||
// Obstacle near top-left: should find remnants above, below, and to the right.
|
||||
var finder = new RemnantFinder(new Box(0, 0, 120, 60));
|
||||
finder.AddObstacle(new Box(0, 47, 21, 6));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
var above = remnants.FirstOrDefault(r => r.Bottom >= 53 - 0.1 && r.Width > 50);
|
||||
var below = remnants.FirstOrDefault(r => r.Top <= 47 + 0.1 && r.Width > 50);
|
||||
var right = remnants.FirstOrDefault(r => r.Left >= 21 - 0.1 && r.Length > 50);
|
||||
|
||||
Assert.NotNull(above);
|
||||
Assert.NotNull(below);
|
||||
Assert.NotNull(right);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadNestFile_FindsGapAboveMainGrid()
|
||||
{
|
||||
var nestFile = @"C:\Users\AJ\Desktop\no_remnant_found.nest";
|
||||
if (!File.Exists(nestFile))
|
||||
return; // Skip if file not available.
|
||||
|
||||
var reader = new NestReader(nestFile);
|
||||
var nest = reader.Read();
|
||||
var plate = nest.Plates[0];
|
||||
|
||||
var finder = RemnantFinder.FromPlate(plate);
|
||||
|
||||
// Use smallest drawing bbox dimension as minDim (same as UI).
|
||||
var minDim = nest.Drawings.Min(d =>
|
||||
System.Math.Min(d.Program.BoundingBox().Width, d.Program.BoundingBox().Length));
|
||||
|
||||
var tiered = finder.FindTieredRemnants(minDim);
|
||||
|
||||
// Should find a remnant near (0.25, 53.13) — the gap above the main grid.
|
||||
var topGap = tiered.FirstOrDefault(t =>
|
||||
t.Box.Bottom > 50 && t.Box.Bottom < 55 &&
|
||||
t.Box.Left < 1 &&
|
||||
t.Box.Width > 100 &&
|
||||
t.Box.Length > 5);
|
||||
|
||||
Assert.True(topGap.Box.Width > 0, "Expected remnant above main grid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DensePack_FindsGapAtTop()
|
||||
{
|
||||
// Reproduce real plate: 120x60, 68 parts of SULLYS-004.
|
||||
// Main grid tops out at y=53.14 (obstacle). Two rotated parts on the
|
||||
// right extend to y=58.49 but only at x > 106. The gap at x < 106
|
||||
// from y=53.14 to y=59.8 is ~106 x 6.66 — should be found.
|
||||
var workArea = new Box(0.2, 0.8, 119.5, 59.0);
|
||||
var obstacles = new List<Box>();
|
||||
var spacing = 0.25;
|
||||
|
||||
// Main grid: 5 columns x 12 rows (6 pairs).
|
||||
// Even rows: bbox bottom offsets, odd rows: different offsets.
|
||||
double[] colX = { 0.25, 21.08, 41.90, 62.73, 83.56 };
|
||||
double[] colXOdd = { 0.81, 21.64, 42.46, 63.29, 84.12 };
|
||||
double[] evenY = { 3.67, 12.41, 21.14, 29.87, 38.60, 47.33 };
|
||||
double[] oddY = { 0.75, 9.48, 18.21, 26.94, 35.67, 44.40 };
|
||||
|
||||
foreach (var cx in colX)
|
||||
foreach (var ey in evenY)
|
||||
obstacles.Add(new Box(cx - spacing, ey - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2));
|
||||
foreach (var cx in colXOdd)
|
||||
foreach (var oy in oddY)
|
||||
obstacles.Add(new Box(cx - spacing, oy - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2));
|
||||
|
||||
// Right-side rotated parts (only 2 extend high: parts 62 and 66).
|
||||
obstacles.Add(new Box(106.70 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
obstacles.Add(new Box(114.19 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
// Parts 63, 67 (lower rotated)
|
||||
obstacles.Add(new Box(105.02 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
obstacles.Add(new Box(112.51 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
// Parts 60, 64 (upper-right rotated, lower)
|
||||
obstacles.Add(new Box(106.70 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
obstacles.Add(new Box(114.19 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
// Parts 61, 65
|
||||
obstacles.Add(new Box(105.02 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
obstacles.Add(new Box(112.51 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
|
||||
var finder = new RemnantFinder(workArea, obstacles);
|
||||
var remnants = finder.FindRemnants(5.375);
|
||||
|
||||
// The gap at x < 106 from y=53.14 to y=59.8 should be found.
|
||||
Assert.True(remnants.Count > 0, "Should find gap above main grid");
|
||||
var topRemnant = remnants.FirstOrDefault(r => r.Length >= 5.375 && r.Width > 50);
|
||||
Assert.NotNull(topRemnant);
|
||||
|
||||
// Verify dimensions are close to the expected ~104 x 6.6 gap.
|
||||
Assert.True(topRemnant.Width > 100, $"Expected width > 100, got {topRemnant.Width:F1}");
|
||||
Assert.True(topRemnant.Length > 6, $"Expected length > 6, got {topRemnant.Length:F1}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user