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;
}
}