using System; using System.Collections.Generic; using OpenNest.Geometry; namespace OpenNest.Engine.Qwen38FlashNext.Engine; using Math = System.Math; /// /// Flat-array polygon with a uniform edge grid, used as the engine's fast outer-shell /// clearance test. 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 true only in that certified case and false /// whenever anything touches, 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. /// /// internal sealed class FastPoly { /// Vertex-on-segment / collinearity tolerance for conservative touches. private const double TouchEps = 1e-9; private readonly FastPolyTemplate _template; /// Translation applied to the shared template geometry. public readonly double Dx; public readonly double Dy; private FastPoly(FastPolyTemplate template, double dx, double dy) { _template = template; Dx = dx; Dy = dy; } public double MinX => _template.MinX + Dx; public double MinY => _template.MinY + Dy; public double MaxX => _template.MaxX + Dx; public 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 FastPoly? From(Polygon polygon) { var template = FastPolyTemplate.Build(polygon); return template == null ? null : new FastPoly(template, 0, 0); } public FastPoly 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; /// /// True when this and are CERTIFIED clear: their /// boundaries neither cross nor touch (within ) and neither /// contains a vertex of the other, so the closed regions share no area. Any touch, /// crossing, or containment reports false and defers to the exact gate. /// public static bool Clears(FastPoly a, FastPoly b) => Relate(a, b) == FastRelation.Clear; /// /// Outer-shell relation between two closed polygons: crossing or containment means /// the shells share positive area; a boundary touch alone or disjoint shells means /// they do not. The Overlap verdict is about SHELLS only - callers with holes must /// still consult the exact gate, because holes can cancel shell overlap. /// public static FastRelation Relate(FastPoly a, FastPoly b) { if ( a.MaxX <= b.MinX || b.MaxX <= a.MinX || a.MaxY <= b.MinY || b.MaxY <= a.MinY ) return FastRelation.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 FastRelation.Overlap; // No transversal crossing. Cases: // 0 = boundaries fully disjoint: containment (hence positive overlap) is // decided by one vertex test per direction. // 1 = point touches only (zero shared area by themselves): positive overlap // requires a vertex strictly inside the other polygon; a tangency - the // spacing-exact contact a bottom-left packer lives on - has none. // 3 = collinear/near-degenerate contact: a shared boundary strip can hide a // same-side positive overlap with no strict-interior vertex anywhere, so // it defers to the exact gate. switch (edge) { case 0: if (ContainsPointStrictly(a, b.X(0), b.Y(0))) return FastRelation.Overlap; if (ContainsPointStrictly(b, a.X(0), a.Y(0))) return FastRelation.Overlap; return FastRelation.Clear; case 1: if (AnyVertexStrictlyInside(b, a) || AnyVertexStrictlyInside(a, b)) return FastRelation.Overlap; return FastRelation.Clear; default: return FastRelation.Unknown; } } /// /// True when any vertex of lies strictly inside /// , or any edge interior sample point does. The samples /// close the inscribed-polygon hole: positive shared area with boundaries meeting /// only at clean points, no strict-interior vertex, and no collinear contact /// requires an edge to run through the interior - its quarter points catch that. /// private static bool AnyVertexStrictlyInside(FastPoly poly, FastPoly vertexSource) { var n = vertexSource._template.Count; for (var i = 0; i < n; i++) { var vx = vertexSource.X(i); var vy = vertexSource.Y(i); if (ContainsPointStrictly(poly, vx, vy)) return true; var i2 = (i + 1) % n; var wx = vertexSource.X(i2); var wy = vertexSource.Y(i2); if (wx == vx && wy == vy) continue; for (var k = 1; k <= 3; k++) { var t = k * 0.25; if (ContainsPointStrictly(poly, vx + (wx - vx) * t, vy + (wy - vy) * t)) return true; } } return false; } /// Three-state outcome of . public enum FastRelation { /// Shells certified disjoint: any materials inside them are clear. Clear, /// Shells share positive area (crossing or containment). Overlap, /// Boundary touch too close to classify: consult the exact gate. Unknown, } /// /// True when 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(FastPoly p, FastPoly q) { var t = p._template; var n = t.Count; var seen = t.Seen; 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 = ++t.Stamp; 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 = Math.Max( 1e-30, Math.Max(Math.Abs(rx) + Math.Abs(ry), Math.Abs(sx) + Math.Abs(sy)) ); var eps = TouchEps * scale; var nearDegenerate = (Math.Abs(d1) <= eps && d1 != 0) || (Math.Abs(d2) <= eps && d2 != 0) || (Math.Abs(d3) <= eps && d3 != 0) || (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 ) => Math.Min(ax, bx) - TouchEps <= px && px <= Math.Max(ax, bx) + TouchEps && Math.Min(ay, by) - TouchEps <= py && py <= Math.Max(ay, by) + TouchEps; /// Strict ray-cast containment (boundary touches are excluded upstream). private static bool ContainsPointStrictly(FastPoly 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(FastPolyTemplate t, double a, double b) => Math.Clamp((int)Math.Floor((Math.Min(a, b) - t.MinX) / t.CellSize), 0, t.Cols); private static int ColHigh(FastPolyTemplate t, double a, double b) => Math.Clamp((int)Math.Floor((Math.Max(a, b) - t.MinX) / t.CellSize), -1, t.Cols - 1); private static int RowLow(FastPolyTemplate t, double a, double b) => Math.Clamp((int)Math.Floor((Math.Min(a, b) - t.MinY) / t.CellSize), 0, t.Rows); private static int RowHigh(FastPolyTemplate t, double a, double b) => Math.Clamp((int)Math.Floor((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. /// Stamp/Seen are mutable single-threaded scratch for the edge-walk dedupe. /// internal sealed class FastPolyTemplate { public readonly double[] X; public readonly double[] Y; public readonly int Count; public readonly double MinX; public readonly double MinY; public readonly double MaxX; public readonly double MaxY; public readonly double CellSize; public readonly int Cols; public readonly int Rows; public 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. /// public readonly int[] NodeEdge; public readonly int[] NodeNext; public readonly int NodeCount; public int Stamp; public readonly int[] Seen; private FastPolyTemplate( 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; Seen = new int[count]; var extentX = Math.Max(maxX - minX, 1e-9); var extentY = Math.Max(maxY - minY, 1e-9); CellSize = Math.Max(Math.Max(extentX, extentY) / 16.0, 1e-9); Cols = Math.Clamp((int)Math.Ceiling(extentX / CellSize) + 1, 1, 48); Rows = Math.Clamp((int)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(Math.Min(x[e], x[i2]) - minX); var c1 = ClampCol(Math.Max(x[e], x[i2]) - minX); var r0 = ClampRow(Math.Min(y[e], y[i2]) - minY); var r1 = ClampRow(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(Math.Min(x[e], x[i2]) - minX); var c1 = ClampCol(Math.Max(x[e], x[i2]) - minX); var r0 = ClampRow(Math.Min(y[e], y[i2]) - minY); var r1 = ClampRow(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++; } } NodeCount = node; } private int ClampCol(double dx) => Math.Clamp((int)Math.Floor(dx / CellSize), 0, Cols - 1); private int ClampRow(double dy) => Math.Clamp((int)Math.Floor(dy / CellSize), 0, Rows - 1); public static FastPolyTemplate? Build(Polygon polygon) { var vertices = polygon.Vertices; var n = vertices.Count; if (n >= 2 && vertices[0].Equals(vertices[n - 1])) 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 FastPolyTemplate(xs, ys, n, minX, minY, maxX, maxY); } }