mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-01 14:18:48 -04:00
The histogram search over a free/blocked cell grid never depended on the obstacles being boxes. Move it to OpenNest.Core as MaximalRectangles.FromGrid so other callers can build their own grids; RemnantFinder keeps building its obstacle grid and calls it. No behavior change.
409 lines
13 KiB
C#
409 lines
13 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.Fill
|
|
{
|
|
/// <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;
|
|
|
|
if (obstacles != null)
|
|
Obstacles.AddRange(obstacles);
|
|
}
|
|
|
|
public void AddObstacle(Box obstacle) => Obstacles.Add(obstacle);
|
|
|
|
public void AddObstacles(IEnumerable<Box> obstacles) => Obstacles.AddRange(obstacles);
|
|
|
|
public void ClearObstacles() => Obstacles.Clear();
|
|
|
|
public List<Box> FindRemnants(double minDimension = 0)
|
|
{
|
|
var grid = BuildGrid();
|
|
|
|
if (grid.Rows <= 0 || grid.Cols <= 0)
|
|
return new List<Box>();
|
|
|
|
var unique = MaximalRectangles.FromGrid(
|
|
grid.XCoords,
|
|
grid.YCoords,
|
|
grid.Empty,
|
|
minDimension
|
|
);
|
|
SortByEdgeProximity(unique);
|
|
return unique;
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
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<Box>(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<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 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.Width,
|
|
1,
|
|
minDim
|
|
);
|
|
|
|
if (remnant.Left < envelope.Left - eps)
|
|
TryAdd(
|
|
results,
|
|
remnant.Left,
|
|
remnant.Bottom,
|
|
envelope.Left - remnant.Left,
|
|
remnant.Width,
|
|
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);
|
|
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);
|
|
}
|
|
}
|
|
}
|