#nullable enable using System; using System.Collections.Generic; namespace OpenNest.Geometry { /// /// Immutable triangulation of a simple, closed, lines-only perimeter and its holes. /// Cached triangles use the reference Collision clipping and hole-subtraction rules. /// Translation is a parameter; preparation never retains mutable input polygons. /// Scratch arrays and hole-piece lists are allocated per query, with a bounded /// thread-local buffer pool. Null means the caller must use Collision.HasOverlap. /// public sealed class TriangulatedRegion { // Flat vertex pool (local frame) and triangle index triples (CCW). private readonly double[] X; private readonly double[] Y; private readonly int[] _ia; private readonly int[] _ib; private readonly int[] _ic; private readonly double[] _tMinX; private readonly double[] _tMinY; private readonly double[] _tMaxX; private readonly double[] _tMaxY; private double MinX { get; } private double MinY { get; } private double MaxX { get; } private double MaxY { get; } /// Triangulated holes in the same local frame (null when none). private readonly TriangulatedRegion?[]? Holes; // Scratch bound: clipped convex pieces stay small; anything larger bails. private const int MaxClipVertices = 48; private const int MaxPieces = 2048; private TriangulatedRegion( double[] x, double[] y, int[] ia, int[] ib, int[] ic, double[] tMinX, double[] tMinY, double[] tMaxX, double[] tMaxY, TriangulatedRegion?[]? holes ) { X = x; Y = y; _ia = ia; _ib = ib; _ic = ic; _tMinX = tMinX; _tMinY = tMinY; _tMaxX = tMaxX; _tMaxY = tMaxY; Holes = holes; var minX = double.MaxValue; var minY = double.MaxValue; var maxX = double.MinValue; var maxY = double.MinValue; for (var i = 0; i < x.Length; i++) { if (x[i] < minX) minX = x[i]; if (x[i] > maxX) maxX = x[i]; if (y[i] < minY) minY = y[i]; if (y[i] > maxY) maxY = y[i]; } MinX = minX; MinY = minY; MaxX = maxX; MaxY = maxY; } /// /// Ear-clips a polygon ring into cached triangles. Returns null when /// triangulation yields nothing usable - the caller falls back to Polygon gates. /// public static TriangulatedRegion? Build(Polygon perimeter, IReadOnlyList? holes = null) { try { var tris = ConvexDecomposition.Triangulate(perimeter); var count = tris.Count; if (count == 0) return null; var xs = new double[count * 3]; var ys = new double[count * 3]; var ia = new int[count]; var ib = new int[count]; var ic = new int[count]; var minXA = new double[count]; var minYA = new double[count]; var maxXA = new double[count]; var maxYA = new double[count]; var k = 0; for (var t = 0; t < count; t++) { var v = tris[t].Vertices; // closed: prev, curr, next, prev ia[t] = k; xs[k] = v[0].X; ys[k] = v[0].Y; k++; ib[t] = k; xs[k] = v[1].X; ys[k] = v[1].Y; k++; ic[t] = k; xs[k] = v[2].X; ys[k] = v[2].Y; k++; minXA[t] = System.Math.Min(v[0].X, System.Math.Min(v[1].X, v[2].X)); minYA[t] = System.Math.Min(v[0].Y, System.Math.Min(v[1].Y, v[2].Y)); maxXA[t] = System.Math.Max(v[0].X, System.Math.Max(v[1].X, v[2].X)); maxYA[t] = System.Math.Max(v[0].Y, System.Math.Max(v[1].Y, v[2].Y)); } TriangulatedRegion[]? holeSets = null; if (holes != null && holes.Count > 0) { holeSets = new TriangulatedRegion[holes.Count]; for (var h = 0; h < holes.Count; h++) { var holeTris = ConvexDecomposition.Triangulate(holes[h]); if (holeTris.Count == 0) continue; var hx = new double[holeTris.Count * 3]; var hy = new double[holeTris.Count * 3]; var hia = new int[holeTris.Count]; var hib = new int[holeTris.Count]; var hic = new int[holeTris.Count]; var hminX = new double[holeTris.Count]; var hminY = new double[holeTris.Count]; var hmaxX = new double[holeTris.Count]; var hmaxY = new double[holeTris.Count]; var hk = 0; for (var t = 0; t < holeTris.Count; t++) { var v = holeTris[t].Vertices; hia[t] = hk; hx[hk] = v[0].X; hy[hk] = v[0].Y; hk++; hib[t] = hk; hx[hk] = v[1].X; hy[hk] = v[1].Y; hk++; hic[t] = hk; hx[hk] = v[2].X; hy[hk] = v[2].Y; hk++; hminX[t] = System.Math.Min(v[0].X, System.Math.Min(v[1].X, v[2].X)); hminY[t] = System.Math.Min(v[0].Y, System.Math.Min(v[1].Y, v[2].Y)); hmaxX[t] = System.Math.Max(v[0].X, System.Math.Max(v[1].X, v[2].X)); hmaxY[t] = System.Math.Max(v[0].Y, System.Math.Max(v[1].Y, v[2].Y)); } holeSets[h] = new TriangulatedRegion(hx, hy, hia, hib, hic, hminX, hminY, hmaxX, hmaxY, null); } } return new TriangulatedRegion(xs, ys, ia, ib, ic, minXA, minYA, maxXA, maxYA, holeSets); } catch (Exception) { return null; } } /// /// Positive shared area (surviving both polygons' hole sets) between this /// translated by (adx, ady) and other translated by (bdx, bdy). Returns null /// when the scratch bounds are exceeded and the question cannot be decided. /// Inputs and translations must have finite coordinates. /// public bool? Overlaps(TriangulatedRegion other, double adx, double ady, double bdx, double bdy) { // Same bbox rule as Collision.BoundingBoxesOverlap: overlap must exceed // Tolerance.Epsilon on both axes, so a hairline box overlap never reaches the // clip stage. var eps = OpenNest.Math.Tolerance.Epsilon; var overlapX = System.Math.Min(MaxX + adx, other.MaxX + bdx) - System.Math.Max(MinX + adx, other.MinX + bdx); var overlapY = System.Math.Min(MaxY + ady, other.MaxY + bdy) - System.Math.Max(MinY + ady, other.MinY + bdy); if (overlapX <= eps || overlapY <= eps) return false; var areaFloor = 2 * OpenNest.Math.Tolerance.Epsilon; var clipA = new double[MaxClipVertices * 2]; var clipB = new double[MaxClipVertices * 2]; var piece = new double[MaxClipVertices * 2]; for (var ta = 0; ta < _ia.Length; ta++) { var aMinX = _tMinX[ta] + adx; var aMaxX = _tMaxX[ta] + adx; var aMinY = _tMinY[ta] + ady; var aMaxY = _tMaxY[ta] + ady; for (var tb = 0; tb < other._ia.Length; tb++) { var bMinX = other._tMinX[tb] + bdx; var bMaxX = other._tMaxX[tb] + bdx; var bMinY = other._tMinY[tb] + bdy; var bMaxY = other._tMaxY[tb] + bdy; if ( System.Math.Min(aMaxX, bMaxX) - System.Math.Max(aMinX, bMinX) <= eps || System.Math.Min(aMaxY, bMaxY) - System.Math.Max(aMinY, bMinY) <= eps ) continue; var count = ClipTriangle( ta, adx, ady, other, tb, bdx, bdy, clipA, clipB, piece ); if (count >= MaxClipVertices) return null; if (count < 3) continue; if (TwiceArea(piece, count) <= areaFloor) continue; var (hasHoles, undecided, survived) = SubtractAllHoles( other, adx, ady, bdx, bdy, piece, count, areaFloor ); if (undecided) return null; if (hasHoles) { if (survived) return true; } else { return true; // no holes on either side: the clipped region is overlap } } } return false; } /// /// Subtracts both polygons' hole triangles from one clipped region, mirroring /// Collision.SubtractHoles: for every hole triangle, every surviving piece is /// split per edge into outside pieces (survivors) and the inside remainder /// (consumed). True means a positive-area piece survived ALL holes. /// [ThreadStatic] private static List? s_pool; [ThreadStatic] private static double[]? s_tmpA; [ThreadStatic] private static double[]? s_tmpB; private static double[] AcquireBuffer() { var pool = s_pool ??= new List(); var n = pool.Count; if (n == 0) return new double[MaxClipVertices * 2]; var buf = pool[n - 1]; pool.RemoveAt(n - 1); return buf; } private static void ReleaseBuffer(double[] buf) { var pool = s_pool ??= new List(); if (pool.Count < 64) pool.Add(buf); } private static (double[] Tmp, double[] Inside) ScratchPair() { s_tmpA ??= new double[MaxClipVertices * 2]; s_tmpB ??= new double[MaxClipVertices * 2]; return (s_tmpA, s_tmpB); } private (bool hasHoles, bool undecided, bool survived) SubtractAllHoles( TriangulatedRegion other, double adx, double ady, double bdx, double bdy, double[] piece, int count, double areaFloor ) { var allHoles = 0; if (Holes != null) allHoles += Holes.Length; if (other.Holes != null) allHoles += other.Holes.Length; if (allHoles == 0) return (false, false, false); // pieces[0] is the caller's own buffer - never release it back to the pool. var pieces = new List<(double[] Buf, int Count)> { (piece, count) }; var owned = new HashSet(); bool SubtractOwner(TriangulatedRegion owner, double odx, double ody) { if (owner.Holes == null) return true; for (var h = 0; h < owner.Holes.Length && pieces.Count > 0; h++) { var hole = owner.Holes[h]; if (hole == null) continue; // untriangulatable hole: nothing to subtract for (var t = 0; t < hole._ia.Length && pieces.Count > 0; t++) { var hMinX = hole._tMinX[t] + odx; var hMaxX = hole._tMaxX[t] + odx; var hMinY = hole._tMinY[t] + ody; var hMaxY = hole._tMaxY[t] + ody; var next = new List<(double[], int)>(); for (var p = 0; p < pieces.Count; p++) { var (buf, pc) = pieces[p]; // Piece bbox (built-in uses <=: touching skips subtraction). var pMinX = double.MaxValue; var pMinY = double.MaxValue; var pMaxX = double.MinValue; var pMaxY = double.MinValue; for (var v = 0; v < pc; v++) { var px = buf[v * 2]; var py = buf[v * 2 + 1]; if (px < pMinX) pMinX = px; if (px > pMaxX) pMaxX = px; if (py < pMinY) pMinY = py; if (py > pMaxY) pMaxY = py; } if (pMaxX <= hMinX || hMaxX <= pMinX || pMaxY <= hMinY || hMaxY <= pMinY) { if (next.Count >= MaxPieces) return false; next.Add((buf, pc)); continue; } // Clip the piece against the hole triangle's three edges: the // outside of each edge survives as its own piece; the inside // remainder continues into the next edge. The remainder inside // all three edges is consumed (the hole ate it). var rem = AcquireBuffer(); owned.Add(rem); Array.Copy(buf, rem, pc * 2); var remCount = pc; var (tmp, insideBuf) = ScratchPair(); for (var e = 0; e < 3 && remCount >= 3; e++) { var ei = e == 0 ? hole._ia[t] : e == 1 ? hole._ib[t] : hole._ic[t]; var ej = e == 0 ? hole._ib[t] : e == 1 ? hole._ic[t] : hole._ia[t]; var sx = hole.X[ei] + odx; var sy = hole.Y[ei] + ody; var ex = hole.X[ej] + odx; var ey = hole.Y[ej] + ody; var outCount = ClipHalfSpace(rem, remCount, sx, sy, ex, ey, false, tmp); if (outCount >= MaxClipVertices) return false; if (outCount >= 3 && TwiceArea(tmp, outCount) > areaFloor) { if (next.Count >= MaxPieces) return false; // undecided var keep = AcquireBuffer(); owned.Add(keep); Array.Copy(tmp, keep, outCount * 2); next.Add((keep, outCount)); } remCount = ClipHalfSpace(rem, remCount, sx, sy, ex, ey, true, insideBuf); if (remCount >= MaxClipVertices) return false; // undecided Array.Copy(insideBuf, rem, remCount * 2); } // The inside-all-edges remainder is consumed by the hole: drop it. owned.Remove(rem); ReleaseBuffer(rem); if (owned.Remove(buf)) ReleaseBuffer(buf); } pieces = next; } } return true; } try { if (!SubtractOwner(this, adx, ady) || !SubtractOwner(other, bdx, bdy)) return (true, true, false); foreach (var (buf, pc) in pieces) if (pc >= 3 && TwiceArea(buf, pc) > areaFloor) return (true, false, true); return (true, false, false); } finally { foreach (var buffer in owned) ReleaseBuffer(buffer); } } /// Clip this' triangle against other's triangle; returns count into piece. private int ClipTriangle( int ta, double adx, double ady, TriangulatedRegion other, int tb, double bdx, double bdy, double[] bufA, double[] bufB, double[] piece ) { var ia = _ia[ta]; var ib = _ib[ta]; var ic = _ic[ta]; bufA[0] = X[ia] + adx; bufA[1] = Y[ia] + ady; bufA[2] = X[ib] + adx; bufA[3] = Y[ib] + ady; bufA[4] = X[ic] + adx; bufA[5] = Y[ic] + ady; var count = 3; for (var e = 0; e < 3 && count >= 3; e++) { var ei = e == 0 ? other._ia[tb] : e == 1 ? other._ib[tb] : other._ic[tb]; var ej = e == 0 ? other._ib[tb] : e == 1 ? other._ic[tb] : other._ia[tb]; var sx = other.X[ei] + bdx; var sy = other.Y[ei] + bdy; var ex = other.X[ej] + bdx; var ey = other.Y[ej] + bdy; count = ClipHalfSpace(bufA, count, sx, sy, ex, ey, true, bufB); if (count >= MaxClipVertices) return count; for (var v = 0; v < count * 2; v++) bufA[v] = bufB[v]; } for (var v = 0; v < System.Math.Min(count, MaxClipVertices) * 2; v++) piece[v] = bufA[v]; return count; } /// /// Sutherland-Hodgman clip against one directed edge's half-plane; identical /// classification, interpolation and dedupe to Collision.ClipHalfSpace. /// private static int ClipHalfSpace( double[] verts, int count, double sx, double sy, double ex, double ey, bool inside, double[] outBuf ) { var kept = 0; var cap = outBuf.Length / 2; var edgeX = ex - sx; var edgeY = ey - sy; for (var i = 0; i < count; i++) { var j = (i + 1) % count; var cx = verts[i * 2]; var cy = verts[i * 2 + 1]; var nx = verts[j * 2]; var ny = verts[j * 2 + 1]; var cd = edgeX * (cy - sy) - edgeY * (cx - sx); var nd = edgeX * (ny - sy) - edgeY * (nx - sx); if (inside ? cd >= 0 : cd <= 0) { if (kept >= cap) return cap; // overflow: caller treats as undecided kept = AddDistinct(outBuf, kept, cx, cy); } if ((cd < 0 && nd > 0) || (cd > 0 && nd < 0)) { if (kept >= cap) return cap; // overflow var t = cd / (cd - nd); kept = AddDistinct( outBuf, kept, cx + t * (nx - cx), cy + t * (ny - cy) ); } } if (kept > 1 && outBuf[0] == outBuf[(kept - 1) * 2] && outBuf[1] == outBuf[(kept - 1) * 2 + 1]) kept--; return kept; } private static int AddDistinct(double[] buf, int count, double x, double y) { if (count > 0 && buf[(count - 1) * 2] == x && buf[(count - 1) * 2 + 1] == y) return count; buf[count * 2] = x; buf[count * 2 + 1] = y; return count + 1; } /// Twice the area, relative to vertex 0 (cancellation-safe). private static double TwiceArea(double[] verts, int count) { var twiceArea = 0.0; for (var i = 1; i + 1 < count; i++) twiceArea += (verts[i * 2] - verts[0]) * (verts[(i + 1) * 2 + 1] - verts[1]) - (verts[i * 2 + 1] - verts[1]) * (verts[(i + 1) * 2] - verts[0]); return System.Math.Abs(twiceArea); } } }