diff --git a/OpenNest.Core/Geometry/MaximalRectangles.cs b/OpenNest.Core/Geometry/MaximalRectangles.cs
new file mode 100644
index 0000000..7200056
--- /dev/null
+++ b/OpenNest.Core/Geometry/MaximalRectangles.cs
@@ -0,0 +1,128 @@
+using System.Collections.Generic;
+
+namespace OpenNest.Geometry;
+
+///
+/// Maximal empty axis-aligned rectangles: rectangles of free space that cannot grow in any
+/// direction. The first result is the largest by area.
+///
+public static class MaximalRectangles
+{
+ ///
+ /// Finds the maximal rectangles of empty cells in a rectilinear grid, using the histogram
+ /// method: for each row, a height histogram of consecutive empty cells below it, scanned
+ /// with a stack.
+ ///
+ /// Ascending column boundaries; column c spans xs[c] to xs[c + 1].
+ /// Ascending row boundaries; row r spans ys[r] to ys[r + 1].
+ /// Free cells, indexed [row, column].
+ /// Rectangles narrower than this in either axis are dropped.
+ /// Rectangles not contained in another result, largest area first.
+ public static List FromGrid(
+ IReadOnlyList xs,
+ IReadOnlyList ys,
+ bool[,] empty,
+ double minDimension = 0
+ )
+ {
+ var merged = MergeCells(xs, ys, empty);
+ var sized = FilterBySize(merged, minDimension);
+ return RemoveDominated(sized);
+ }
+
+ private static List MergeCells(IReadOnlyList xs, IReadOnlyList ys, bool[,] empty)
+ {
+ var rows = empty.GetLength(0);
+ var cols = empty.GetLength(1);
+ var height = new int[rows, cols];
+
+ for (var c = 0; c < cols; c++)
+ {
+ for (var r = 0; r < rows; r++)
+ height[r, c] = empty[r, c] ? (r > 0 ? height[r - 1, c] + 1 : 1) : 0;
+ }
+
+ var candidates = new List();
+
+ for (var r = 0; r < rows; r++)
+ {
+ var stack = new Stack<(int startCol, int h)>();
+
+ for (var c = 0; c <= cols; c++)
+ {
+ var h = c < cols ? height[r, c] : 0;
+ var startCol = c;
+
+ while (stack.Count > 0 && stack.Peek().h > h)
+ {
+ var top = stack.Pop();
+ startCol = top.startCol;
+
+ candidates.Add(
+ new Box(
+ xs[top.startCol],
+ ys[r - top.h + 1],
+ xs[c] - xs[top.startCol],
+ ys[r + 1] - ys[r - top.h + 1]
+ )
+ );
+ }
+
+ if (h > 0)
+ stack.Push((startCol, h));
+ }
+ }
+
+ return candidates;
+ }
+
+ private static List FilterBySize(List boxes, double minDimension)
+ {
+ if (minDimension <= 0)
+ return boxes;
+
+ var result = new List();
+
+ foreach (var box in boxes)
+ {
+ if (box.Width >= minDimension && box.Length >= minDimension)
+ result.Add(box);
+ }
+
+ return result;
+ }
+
+ private static List RemoveDominated(List boxes)
+ {
+ boxes.Sort((a, b) => b.Area().CompareTo(a.Area()));
+ var results = new List();
+
+ 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;
+ }
+}
diff --git a/OpenNest.Engine/Fill/RemnantFinder.cs b/OpenNest.Engine/Fill/RemnantFinder.cs
index bd6c3ed..dbc2c72 100644
--- a/OpenNest.Engine/Fill/RemnantFinder.cs
+++ b/OpenNest.Engine/Fill/RemnantFinder.cs
@@ -56,9 +56,12 @@ namespace OpenNest.Engine.Fill
if (grid.Rows <= 0 || grid.Cols <= 0)
return new List();
- var merged = MergeCells(grid);
- var sized = FilterBySize(merged, minDimension);
- var unique = RemoveDominated(sized);
+ var unique = MaximalRectangles.FromGrid(
+ grid.XCoords,
+ grid.YCoords,
+ grid.Empty,
+ minDimension
+ );
SortByEdgeProximity(unique);
return unique;
}
@@ -188,56 +191,6 @@ namespace OpenNest.Engine.Fill
return false;
}
- private static List FilterBySize(List boxes, double minDimension)
- {
- if (minDimension <= 0)
- return boxes;
-
- var result = new List();
-
- foreach (var box in boxes)
- {
- if (box.Width >= minDimension && box.Length >= minDimension)
- result.Add(box);
- }
-
- return result;
- }
-
- private static List RemoveDominated(List boxes)
- {
- boxes.Sort((a, b) => b.Area().CompareTo(a.Area()));
- var results = new List();
-
- 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 boxes)
{
boxes.Sort(
@@ -451,54 +404,5 @@ namespace OpenNest.Engine.Fill
return new Box(left, bottom, right - left, top - bottom);
}
-
- ///
- /// 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.
- ///
- private static List MergeCells(CellGrid grid)
- {
- var height = new int[grid.Rows, grid.Cols];
-
- for (var c = 0; c < grid.Cols; c++)
- {
- for (var r = 0; r < grid.Rows; r++)
- height[r, c] = grid.Empty[r, c] ? (r > 0 ? height[r - 1, c] + 1 : 1) : 0;
- }
-
- var candidates = new List();
-
- for (var r = 0; r < grid.Rows; r++)
- {
- var stack = new Stack<(int startCol, int h)>();
-
- for (var c = 0; c <= grid.Cols; c++)
- {
- var h = c < grid.Cols ? height[r, c] : 0;
- var startCol = c;
-
- while (stack.Count > 0 && stack.Peek().h > h)
- {
- var top = stack.Pop();
- startCol = top.startCol;
-
- 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]
- )
- );
- }
-
- if (h > 0)
- stack.Push((startCol, h));
- }
- }
-
- return candidates;
- }
}
}