using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Engine.Fill
{
///
/// A remnant box with a priority tier.
/// 0 = within the used envelope (best), 1 = extends past one edge, 2 = fully outside.
///
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 Obstacles { get; } = new();
private struct CellGrid
{
public bool[,] Empty;
public List XCoords;
public List YCoords;
public int Rows;
public int Cols;
}
public RemnantFinder(Box workArea, List obstacles = null)
{
this.workArea = workArea;
if (obstacles != null)
Obstacles.AddRange(obstacles);
}
public void AddObstacle(Box obstacle) => Obstacles.Add(obstacle);
public void AddObstacles(IEnumerable obstacles) => Obstacles.AddRange(obstacles);
public void ClearObstacles() => Obstacles.Clear();
public List FindRemnants(double minDimension = 0)
{
var grid = BuildGrid();
if (grid.Rows <= 0 || grid.Cols <= 0)
return new List();
var merged = MergeCells(grid);
var sized = FilterBySize(merged, minDimension);
var unique = RemoveDominated(sized);
SortByEdgeProximity(unique);
return unique;
}
///
/// 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.
///
public List 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();
foreach (var remnant in remnants)
{
var before = results.Count;
SplitAtEnvelope(remnant, envelope, minDimension, results);
// If all splits fell below minDim, keep the original unsplit.
if (results.Count == before)
results.Add(new TieredRemnant(remnant, 1));
}
results.Sort((a, b) =>
{
if (a.Priority != b.Priority)
return a.Priority.CompareTo(b.Priority);
return b.Box.Area().CompareTo(a.Box.Area());
});
return results;
}
public static RemnantFinder FromPlate(Plate plate)
{
var obstacles = new List(plate.Parts.Count);
foreach (var part in plate.Parts)
obstacles.Add(part.BoundingBox.Offset(plate.PartSpacing));
return new RemnantFinder(plate.WorkArea(), obstacles);
}
private CellGrid BuildGrid()
{
var clipped = ClipObstacles();
var xs = new SortedSet { workArea.Left, workArea.Right };
var ys = new SortedSet { 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 ClipObstacles()
{
var clipped = new List(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 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 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((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 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 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);
var bottom = System.Math.Max(obs.Bottom, workArea.Bottom);
var right = System.Math.Min(obs.Right, workArea.Right);
var top = System.Math.Min(obs.Top, workArea.Top);
if (right <= left || top <= bottom)
return Box.Empty;
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;
}
}
}