using System.Collections.Generic; using UnityEngine; namespace InteractiveVideo { /// /// Allocation-free polygon helpers. All functions work in whatever 2D space the caller uses, /// as long as every input is in the same space. /// public static class PolygonUtility { /// /// Even-odd (ray casting) point-in-polygon test. Handles concave polygons. /// Returns false for null polygons or fewer than 3 points. Allocates nothing. /// public static bool ContainsPoint(IReadOnlyList polygon, Vector2 point) { if (polygon == null) return false; int n = polygon.Count; if (n < 3) return false; bool inside = false; for (int i = 0, j = n - 1; i < n; j = i++) { Vector2 a = polygon[i]; Vector2 b = polygon[j]; // Does the horizontal ray from `point` towards +X cross edge (a,b)? if ((a.y > point.y) != (b.y > point.y)) { float xAtY = (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x; if (point.x < xAtY) inside = !inside; } } return inside; } /// Axis-aligned bounds of a polygon. Returns an empty rect at the origin for an empty polygon. public static Rect ComputeBounds(IReadOnlyList polygon) { if (polygon == null || polygon.Count == 0) return new Rect(0, 0, 0, 0); float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue; for (int i = 0; i < polygon.Count; i++) { Vector2 p = polygon[i]; if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; } return Rect.MinMaxRect(minX, minY, maxX, maxY); } /// Twice the signed area; positive when the winding is counter-clockwise in a Y-up space. public static float SignedAreaTimesTwo(IReadOnlyList polygon) { float sum = 0f; int n = polygon.Count; for (int i = 0, j = n - 1; i < n; j = i++) sum += (polygon[j].x * polygon[i].y) - (polygon[i].x * polygon[j].y); return sum; } /// /// Writes Lerp(a[i], b[i], t) for every point into (cleared first). /// Requires equal point counts; returns false (result untouched) otherwise. /// public static bool LerpPolygon(IReadOnlyList a, IReadOnlyList b, float t, List result) { if (a == null || b == null || a.Count != b.Count) return false; result.Clear(); for (int i = 0; i < a.Count; i++) result.Add(Vector2.LerpUnclamped(a[i], b[i], t)); return true; } // Scratch list for ear clipping. Main thread only. private static readonly List s_Remaining = new List(128); /// /// Ear-clipping triangulation for simple polygons (no self-intersections, either winding). /// Appends triangle indices (indices into the polygon, plus ) to /// . O(n^2), fine for the 20-80 point polygons preprocessing produces. /// Degenerate input (collinear runs, small self-intersections) falls back to a triangle fan so the /// mask still shows something instead of failing. /// public static void Triangulate(IReadOnlyList polygon, List indices, int indexOffset = 0) { int n = polygon.Count; if (n < 3) return; if (n == 3) { indices.Add(indexOffset); indices.Add(indexOffset + 1); indices.Add(indexOffset + 2); return; } bool ccw = SignedAreaTimesTwo(polygon) > 0f; var remaining = s_Remaining; remaining.Clear(); for (int i = 0; i < n; i++) remaining.Add(i); int guard = 0; int maxIterations = n * n; while (remaining.Count > 3 && guard++ < maxIterations) { bool clipped = false; int count = remaining.Count; for (int i = 0; i < count; i++) { int iPrev = remaining[(i + count - 1) % count]; int iCur = remaining[i]; int iNext = remaining[(i + 1) % count]; Vector2 a = polygon[iPrev], b = polygon[iCur], c = polygon[iNext]; if (!IsConvex(a, b, c, ccw)) continue; if (AnyPointInside(polygon, remaining, a, b, c, iPrev, iCur, iNext)) continue; indices.Add(indexOffset + iPrev); indices.Add(indexOffset + iCur); indices.Add(indexOffset + iNext); remaining.RemoveAt(i); clipped = true; break; } if (!clipped) { // Degenerate polygon: fan the rest from the first remaining vertex. for (int i = 1; i + 1 < remaining.Count; i++) { indices.Add(indexOffset + remaining[0]); indices.Add(indexOffset + remaining[i]); indices.Add(indexOffset + remaining[i + 1]); } remaining.Clear(); return; } } if (remaining.Count == 3) { indices.Add(indexOffset + remaining[0]); indices.Add(indexOffset + remaining[1]); indices.Add(indexOffset + remaining[2]); } } private static bool IsConvex(Vector2 a, Vector2 b, Vector2 c, bool ccw) { float cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); return ccw ? cross > 1e-9f : cross < -1e-9f; } private static bool AnyPointInside(IReadOnlyList polygon, List remaining, Vector2 a, Vector2 b, Vector2 c, int ia, int ib, int ic) { for (int k = 0; k < remaining.Count; k++) { int idx = remaining[k]; if (idx == ia || idx == ib || idx == ic) continue; if (PointInTriangle(polygon[idx], a, b, c)) return true; } return false; } private static bool PointInTriangle(Vector2 p, Vector2 a, Vector2 b, Vector2 c) { float d1 = Sign(p, a, b); float d2 = Sign(p, b, c); float d3 = Sign(p, c, a); bool hasNeg = (d1 < 0f) || (d2 < 0f) || (d3 < 0f); bool hasPos = (d1 > 0f) || (d2 > 0f) || (d3 > 0f); return !(hasNeg && hasPos); } private static float Sign(Vector2 p1, Vector2 p2, Vector2 p3) => (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); } }