Files
unity-zone-system/Runtime/PolygonUtils.cs
Sebastian Bularca 823e146df0 copy from github
2026-03-27 15:13:27 +01:00

201 lines
7.0 KiB
C#

using System.Collections.Generic;
using UnityEngine;
namespace Jovian.ZoneSystem {
public static class PolygonUtils {
/// <summary>
/// Ray-casting point-in-polygon test (Jordan curve theorem).
/// Works on any plane — caller projects the world position first via MapPlaneUtility.
/// Handles edge and vertex cases robustly.
/// </summary>
/// <param name="point">2D point already projected onto the polygon's plane.</param>
/// <param name="polygon">Polygon vertices in the same 2D space.</param>
public static bool PointInPolygon(Vector2 point, List<Vector2> polygon) {
if(polygon == null || polygon.Count < 3) {
return false;
}
float px = point.x;
float py = point.y;
bool inside = false;
int count = polygon.Count;
int j = count - 1;
for(int i = 0; i < count; i++) {
float xi = polygon[i].x, yi = polygon[i].y;
float xj = polygon[j].x, yj = polygon[j].y;
// Crossing test: does the edge (j→i) cross the horizontal ray from point?
bool crosses = (yi > py) != (yj > py) &&
px < ((xj - xi) * (py - yi) / (yj - yi)) + xi;
if(crosses) {
inside = !inside;
}
j = i;
}
return inside;
}
/// <summary>
/// Overload that accepts a world position and projects it onto the given plane
/// before testing — this is the primary API used by ZoneManager.
/// </summary>
public static bool PointInPolygon(Vector3 worldPos, List<Vector2> polygon, MapPlane plane) {
Vector2 projected = MapPlaneUtility.ProjectToPlane(worldPos, plane);
return PointInPolygon(projected, polygon);
}
/// <summary>
/// Returns the centroid of a polygon (for label placement in the editor).
/// </summary>
public static Vector2 Centroid(List<Vector2> polygon) {
if(polygon == null || polygon.Count == 0) {
return Vector2.zero;
}
Vector2 sum = Vector2.zero;
foreach(Vector2 pt in polygon) {
sum += pt;
}
return sum / polygon.Count;
}
/// <summary>
/// Returns the approximate axis-aligned bounding box of a polygon.
/// Useful for a cheap pre-check before running the full ray-cast test.
/// </summary>
public static (Vector2 min, Vector2 max) Bounds(List<Vector2> polygon) {
if(polygon == null || polygon.Count == 0) {
return (Vector2.zero, Vector2.zero);
}
Vector2 min = polygon[0], max = polygon[0];
foreach(Vector2 pt in polygon) {
if(pt.x < min.x) {
min.x = pt.x;
}
if(pt.y < min.y) {
min.y = pt.y;
}
if(pt.x > max.x) {
max.x = pt.x;
}
if(pt.y > max.y) {
max.y = pt.y;
}
}
return (min, max);
}
/// <summary>
/// Fast AABB pre-check. Call this before PointInPolygon to skip the
/// ray-cast for points clearly outside the bounding box.
/// </summary>
public static bool PointInBounds(Vector2 point, Vector2 min, Vector2 max) {
return point.x >= min.x && point.x <= max.x &&
point.y >= min.y && point.y <= max.y;
}
/// <summary>
/// Ear-clipping triangulation for simple (non-self-intersecting) polygons.
/// Returns a list of triangle index triplets into the original vertex list.
/// Supports both convex and concave polygons.
/// </summary>
public static List<int> Triangulate(List<Vector2> polygon) {
List<int> triangles = new List<int>();
int n = polygon.Count;
if(n < 3) {
return triangles;
}
// Build index list
List<int> indices = new List<int>(n);
bool clockwise = SignedArea(polygon) < 0f;
for(int i = 0; i < n; i++) {
indices.Add(clockwise ? i : n - 1 - i);
}
int remaining = n;
int failSafe = remaining * 2;
int v = remaining - 1;
while(remaining > 2) {
if(failSafe-- <= 0) {
break;
}
int u = v;
if(u >= remaining) {
u = 0;
}
v = u + 1;
if(v >= remaining) {
v = 0;
}
int w = v + 1;
if(w >= remaining) {
w = 0;
}
if(IsEar(polygon, indices, u, v, w, remaining)) {
triangles.Add(indices[u]);
triangles.Add(indices[v]);
triangles.Add(indices[w]);
indices.RemoveAt(v);
remaining--;
failSafe = remaining * 2;
}
}
return triangles;
}
private static float SignedArea(List<Vector2> polygon) {
float area = 0f;
int count = polygon.Count;
for(int i = 0; i < count; i++) {
Vector2 a = polygon[i];
Vector2 b = polygon[(i + 1) % count];
area += (b.x - a.x) * (b.y + a.y);
}
return area;
}
private static bool IsEar(List<Vector2> polygon, List<int> indices, int u, int v, int w, int remaining) {
Vector2 a = polygon[indices[u]];
Vector2 b = polygon[indices[v]];
Vector2 c = polygon[indices[w]];
// Must be convex (counter-clockwise winding after we've ensured CCW order)
float cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
if(cross <= 0f) {
return false;
}
// No other vertex must be inside this triangle
for(int p = 0; p < remaining; p++) {
if(p == u || p == v || p == w) {
continue;
}
if(PointInTriangle(polygon[indices[p]], a, b, c)) {
return false;
}
}
return true;
}
private static bool PointInTriangle(Vector2 p, Vector2 a, Vector2 b, Vector2 c) {
float d1 = (p.x - b.x) * (a.y - b.y) - (a.x - b.x) * (p.y - b.y);
float d2 = (p.x - c.x) * (b.y - c.y) - (b.x - c.x) * (p.y - c.y);
float d3 = (p.x - a.x) * (c.y - a.y) - (c.x - a.x) * (p.y - a.y);
bool hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);
bool hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);
return !(hasNeg && hasPos);
}
}
}