#nullable enable
using System;
namespace OpenNest.Geometry
{
///
/// Immutable flat-array polygon with a uniform edge grid, used as an outer-shell
/// clearance prefilter. Two closed polygons share positive area only when an edge pair
/// crosses/touches or one polygon's vertex lies strictly inside the other; neither
/// happening certifies the two closed regions (hence any materials inside them) are
/// clear. returns Clear only in that certified case and Unknown
/// for uncertain contacts, so it can only ever skip the exact
/// gate when the exact gate would also find no overlap - the exact gate triangulates
/// both polygons per call and dominates runtime on finely flattened arc geometry.
///
/// A holds the shared geometry;
/// produces a placement in world coordinates in O(1) - translation leaves the grid and
/// all cell indices unchanged, only the predicate coordinates shift.
///
///
public sealed class EdgeGridPolygon
{
/// Vertex-on-segment / collinearity tolerance for conservative touches.
private const double TouchEps = 1e-9;
private readonly EdgeGridPolygonTemplate _template;
/// Translation applied to the shared template geometry.
private readonly double Dx;
private readonly double Dy;
private EdgeGridPolygon(EdgeGridPolygonTemplate template, double dx, double dy)
{
_template = template;
Dx = dx;
Dy = dy;
}
private double MinX => _template.MinX + Dx;
private double MinY => _template.MinY + Dy;
private double MaxX => _template.MaxX + Dx;
private double MaxY => _template.MaxY + Dy;
///
/// Builds from a closed (last vertex may repeat the first).
/// Returns null when the polygon has no usable ring - callers treat that as
/// "no information" and fall through to the exact gate.
///
public static EdgeGridPolygon? From(Polygon polygon)
{
var template = EdgeGridPolygonTemplate.Build(polygon);
return template == null ? null : new EdgeGridPolygon(template, 0, 0);
}
/// Returns a placement sharing immutable geometry, with an added translation.
public EdgeGridPolygon Translated(double dx, double dy) => new(_template, Dx + dx, Dy + dy);
private double X(int i) => _template.X[i] + Dx;
private double Y(int i) => _template.Y[i] + Dy;
///
/// Certifies disjoint filled perimeters. Any crossing, containment or uncertain
/// boundary contact returns Unknown and must defer to the exact collision test.
/// Holes need not be supplied: removing material cannot invalidate Clear.
///
public static ShellRelation Relate(EdgeGridPolygon a, EdgeGridPolygon b)
{
if (
a.MaxX <= b.MinX
|| b.MaxX <= a.MinX
|| a.MaxY <= b.MinY
|| b.MaxY <= a.MinY
)
return ShellRelation.Clear; // disjoint bounding boxes
// One walk per direction reports the strongest edge relation: a transversal
// crossing shares a positive-area wedge (overlap); a mere touch shares zero
// area but may hide a crossing in near-degenerate coordinates (unknown).
var edge = EdgeRelation(a, b);
if (edge < 2)
{
var back = EdgeRelation(b, a);
if (back > edge)
edge = back;
}
if (edge == 2)
return ShellRelation.Unknown;
// Only fully disjoint boundaries can certify clearance. Point touches and
// collinear/near-degenerate contacts always defer to the reference test.
switch (edge)
{
case 0:
if (ContainsPointStrictly(a, b.X(0), b.Y(0)))
return ShellRelation.Unknown;
if (ContainsPointStrictly(b, a.X(0), a.Y(0)))
return ShellRelation.Unknown;
return ShellRelation.Clear;
default:
return ShellRelation.Unknown;
}
}
///
/// Classifies whether any edge of crosses or touches the boundary of
/// . Walks p's grid using each query edge's own bbox cells.
/// p's grid lives in p's LOCAL frame (the template's own coordinates), so the
/// query edge is converted by subtracting p's translation first.
///
private static int EdgeRelation(EdgeGridPolygon p, EdgeGridPolygon q)
{
var t = p._template;
var n = t.Count;
Span seen = n <= 1024 ? stackalloc int[n] : new int[n];
seen.Clear();
var head = t.Head;
var nodeEdge = t.NodeEdge;
var nodeNext = t.NodeNext;
var no = q._template.Count;
var strongest = 0;
for (var e = 0; e < no; e++)
{
// Stamp per QUERY edge: a grid edge may need testing against every query
// edge; the dedupe only collapses cells an individual query edge crosses
// more than once.
var stamp = e + 1;
var i2 = (e + 1) % no;
var p0x = q.X(e) - p.Dx;
var p0y = q.Y(e) - p.Dy;
var p1x = q.X(i2) - p.Dx;
var p1y = q.Y(i2) - p.Dy;
var c0 = ColLow(t, p0x, p1x);
if (c0 > ColHigh(t, p0x, p1x))
continue;
var c1 = ColHigh(t, p0x, p1x);
var r0 = RowLow(t, p0y, p1y);
if (r0 > RowHigh(t, p0y, p1y))
continue;
var r1 = RowHigh(t, p0y, p1y);
for (var r = r0; r <= r1; r++)
for (var c = c0; c <= c1; c++)
for (var nIdx = head[r * t.Cols + c]; nIdx >= 0; nIdx = nodeNext[nIdx])
{
var ea = nodeEdge[nIdx];
if (seen[ea] == stamp)
continue;
seen[ea] = stamp;
var a2 = (ea + 1) % n;
var relation = SegmentRelation(
t.X[ea], t.Y[ea], t.X[a2], t.Y[a2], p0x, p0y, p1x, p1y
);
if (relation == 2)
return 2; // transversal crossing
if (relation > strongest)
strongest = relation;
}
}
return strongest;
}
///
/// Segment-pair relation: 2 = transversal crossing (strict sign flips on both
/// orientations - the regions share a positive-area wedge); 1 = a clean endpoint
/// touch (zero shared area by itself; callers decide via interior-vertex tests);
/// 3 = collinear or near-degenerate contact (a shared boundary segment can hide
/// either a same-side positive overlap or an opposite-side tangency, so it must
/// defer to the exact gate); 0 = disjoint.
///
private static int SegmentRelation(
double ax, double ay, double bx, double by, double cx, double cy, double dx, double dy
)
{
var rx = bx - ax;
var ry = by - ay;
var sx = dx - cx;
var sy = dy - cy;
var d1 = rx * (cy - ay) - ry * (cx - ax);
var d2 = rx * (dy - ay) - ry * (dx - ax);
var d3 = sx * (ay - cy) - sy * (ax - cx);
var d4 = sx * (by - cy) - sy * (bx - cx);
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)))
return 2; // proper crossing
// A near-zero orientation means the configuration is collinear or too close to
// classify; only exact-zero orientations get the clean point-touch verdict.
var scale = System.Math.Max(
1e-30,
System.Math.Max(System.Math.Abs(rx) + System.Math.Abs(ry), System.Math.Abs(sx) + System.Math.Abs(sy))
);
var eps = TouchEps * scale;
var nearDegenerate =
(System.Math.Abs(d1) <= eps && d1 != 0)
|| (System.Math.Abs(d2) <= eps && d2 != 0)
|| (System.Math.Abs(d3) <= eps && d3 != 0)
|| (System.Math.Abs(d4) <= eps && d4 != 0);
var exactDegenerate = d1 == 0 || d2 == 0 || d3 == 0 || d4 == 0;
var touch =
(d1 == 0 && PointOnSegment(cx, cy, ax, ay, bx, by))
|| (d2 == 0 && PointOnSegment(dx, dy, ax, ay, bx, by))
|| (d3 == 0 && PointOnSegment(ax, ay, cx, cy, dx, dy))
|| (d4 == 0 && PointOnSegment(bx, by, cx, cy, dx, dy));
if (nearDegenerate)
return 3;
if (exactDegenerate)
// Collinear: contact along a segment (or too close to tell) must defer to
// the exact gate; collinear but disjoint edges simply do not touch.
return touch ? 3 : 0;
if (touch)
return 1;
return 0;
}
private static bool PointOnSegment(
double px, double py, double ax, double ay, double bx, double by
) =>
System.Math.Min(ax, bx) - TouchEps <= px
&& px <= System.Math.Max(ax, bx) + TouchEps
&& System.Math.Min(ay, by) - TouchEps <= py
&& py <= System.Math.Max(ay, by) + TouchEps;
/// Strict ray-cast containment (boundary touches are excluded upstream).
private static bool ContainsPointStrictly(EdgeGridPolygon poly, double px, double py)
{
var t = poly._template;
var inside = false;
var n = t.Count;
for (var i = 0; i < n; i++)
{
var j = (i + 1) % n;
var yi = poly.Y(i);
var yj = poly.Y(j);
if ((yi > py) != (yj > py))
{
var xAt = poly.X(i) + (py - yi) / (yj - yi) * (poly.X(j) - poly.X(i));
if (px < xAt)
inside = !inside;
}
}
return inside;
}
private static int ColLow(EdgeGridPolygonTemplate t, double a, double b) =>
System.Math.Clamp((int)System.Math.Floor((System.Math.Min(a, b) - t.MinX) / t.CellSize), 0, t.Cols);
private static int ColHigh(EdgeGridPolygonTemplate t, double a, double b) =>
System.Math.Clamp((int)System.Math.Floor((System.Math.Max(a, b) - t.MinX) / t.CellSize), -1, t.Cols - 1);
private static int RowLow(EdgeGridPolygonTemplate t, double a, double b) =>
System.Math.Clamp((int)System.Math.Floor((System.Math.Min(a, b) - t.MinY) / t.CellSize), 0, t.Rows);
private static int RowHigh(EdgeGridPolygonTemplate t, double a, double b) =>
System.Math.Clamp((int)System.Math.Floor((System.Math.Max(a, b) - t.MinY) / t.CellSize), -1, t.Rows - 1);
///
/// Shared, immutable grid geometry for ; the grid is defined
/// relative to the shape's own local coordinates, so translated instances reuse it.
/// Per-query deduplication scratch is local, so placements may be queried concurrently.
///
private sealed class EdgeGridPolygonTemplate
{
internal readonly double[] X;
internal readonly double[] Y;
internal readonly int Count;
internal readonly double MinX;
internal readonly double MinY;
internal readonly double MaxX;
internal readonly double MaxY;
internal readonly double CellSize;
internal readonly int Cols;
internal readonly int Rows;
internal readonly int[] Head;
///
/// Grid nodes as parallel (edge, next) arrays: an edge spanning several cells gets
/// one node PER cell - a single next-per-edge chain would corrupt the other cells'
/// chains and silently drop edges from the walk.
///
internal readonly int[] NodeEdge;
internal readonly int[] NodeNext;
private EdgeGridPolygonTemplate(
double[] x,
double[] y,
int count,
double minX,
double minY,
double maxX,
double maxY
)
{
X = x;
Y = y;
Count = count;
MinX = minX;
MinY = minY;
MaxX = maxX;
MaxY = maxY;
var extentX = System.Math.Max(maxX - minX, 1e-9);
var extentY = System.Math.Max(maxY - minY, 1e-9);
CellSize = System.Math.Max(System.Math.Max(extentX, extentY) / 16.0, 1e-9);
Cols = System.Math.Clamp((int)System.Math.Ceiling(extentX / CellSize) + 1, 1, 48);
Rows = System.Math.Clamp((int)System.Math.Ceiling(extentY / CellSize) + 1, 1, 48);
Head = new int[Cols * Rows];
Array.Fill(Head, -1);
// Pass 1: count nodes; pass 2: fill (edge, next) node arrays.
var cellsPerEdge = new int[count];
var total = 0;
for (var e = 0; e < count; e++)
{
var i2 = (e + 1) % count;
var c0 = ClampCol(System.Math.Min(x[e], x[i2]) - minX);
var c1 = ClampCol(System.Math.Max(x[e], x[i2]) - minX);
var r0 = ClampRow(System.Math.Min(y[e], y[i2]) - minY);
var r1 = ClampRow(System.Math.Max(y[e], y[i2]) - minY);
cellsPerEdge[e] = (c1 - c0 + 1) * (r1 - r0 + 1);
total += cellsPerEdge[e];
}
NodeEdge = new int[total];
NodeNext = new int[total];
var node = 0;
for (var e = 0; e < count; e++)
{
var i2 = (e + 1) % count;
var c0 = ClampCol(System.Math.Min(x[e], x[i2]) - minX);
var c1 = ClampCol(System.Math.Max(x[e], x[i2]) - minX);
var r0 = ClampRow(System.Math.Min(y[e], y[i2]) - minY);
var r1 = ClampRow(System.Math.Max(y[e], y[i2]) - minY);
for (var r = r0; r <= r1; r++)
for (var c = c0; c <= c1; c++)
{
var cell = r * Cols + c;
NodeEdge[node] = e;
NodeNext[node] = Head[cell];
Head[cell] = node;
node++;
}
}
}
private int ClampCol(double dx) =>
System.Math.Clamp((int)System.Math.Floor(dx / CellSize), 0, Cols - 1);
private int ClampRow(double dy) =>
System.Math.Clamp((int)System.Math.Floor(dy / CellSize), 0, Rows - 1);
internal static EdgeGridPolygonTemplate? Build(Polygon polygon)
{
var vertices = polygon.Vertices;
var n = vertices.Count;
if (n >= 2 && vertices[0].X == vertices[n - 1].X && vertices[0].Y == vertices[n - 1].Y)
n--;
if (n < 3)
return null;
var xs = new double[n];
var ys = new double[n];
var minX = double.MaxValue;
var minY = double.MaxValue;
var maxX = double.MinValue;
var maxY = double.MinValue;
for (var i = 0; i < n; i++)
{
var vx = vertices[i].X;
var vy = vertices[i].Y;
xs[i] = vx;
ys[i] = vy;
if (vx < minX)
minX = vx;
if (vx > maxX)
maxX = vx;
if (vy < minY)
minY = vy;
if (vy > maxY)
maxY = vy;
}
return new EdgeGridPolygonTemplate(xs, ys, n, minX, minY, maxX, maxY);
}
}
}
/// Conservative result of an outer-perimeter prefilter.
public enum ShellRelation
{
/// Filled perimeters, and therefore their material, are disjoint.
Clear,
/// Run an exact collision test; the prefilter cannot certify clearance.
Unknown,
}
}