diff --git a/OpenNest.Engine/RemnantFinder.cs b/OpenNest.Engine/RemnantFinder.cs new file mode 100644 index 0000000..b5e2cef --- /dev/null +++ b/OpenNest.Engine/RemnantFinder.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenNest.Geometry; + +namespace OpenNest +{ + public class RemnantFinder + { + private readonly Box workArea; + + public List Obstacles { get; } = new(); + + 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 xs = new SortedSet { workArea.Left, workArea.Right }; + var ys = new SortedSet { workArea.Bottom, workArea.Top }; + + 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) + return new List(); + + var empty = new bool[rows, cols]; + + for (var r = 0; r < rows; r++) + { + 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]); + + empty[r, c] = !OverlapsAnyObstacle(cell); + } + } + + var merged = MergeCells(empty, xList, yList, rows, cols); + + var results = new List(); + + foreach (var box in merged) + { + if (box.Width >= minDimension && box.Length >= minDimension) + results.Add(box); + } + + results.Sort((a, b) => b.Area().CompareTo(a.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 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); + } + + private bool OverlapsAnyObstacle(Box cell) + { + foreach (var obs in Obstacles) + { + 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; + } + + return false; + } + + private static List MergeCells(bool[,] empty, List xList, List yList, int rows, int cols) + { + var used = new bool[rows, cols]; + var results = new List(); + + for (var r = 0; r < rows; r++) + { + for (var c = 0; c < cols; c++) + { + if (!empty[r, c] || used[r, c]) + continue; + + var maxC = c; + while (maxC + 1 < cols && empty[r, maxC + 1] && !used[r, maxC + 1]) + maxC++; + + var maxR = r; + while (maxR + 1 < rows) + { + var rowOk = true; + for (var cc = c; cc <= maxC; cc++) + { + if (!empty[maxR + 1, cc] || used[maxR + 1, cc]) + { + rowOk = false; + break; + } + } + + if (!rowOk) break; + maxR++; + } + + 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); + } + } + + return results; + } + } +}