// MIT License - Copyright (c) 2023 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.Math
{
using System;
using UnityEngine;
///
/// Provides methods for determining if a point is inside a polygon.
///
public static class PointPolygonCheck
{
///
/// Determines if a 2D point is inside a polygon using the ray-casting algorithm.
///
/// The point to test.
/// The vertices of the polygon in order (clockwise or counter-clockwise).
///
/// true if the point is inside the polygon; otherwise, false.
/// Returns false for null or degenerate polygons (fewer than 3 vertices).
///
///
/// Points exactly on polygon edges or vertices may return inconsistent results due to floating-point precision.
/// The polygon is assumed to be simple (non-self-intersecting) for predictable results.
///
public static bool IsPointInsidePolygon(Vector2 point, Vector2[] polygon)
{
if (polygon == null || polygon.Length < 3)
{
return false;
}
return IsPointInsidePolygon(point, new ReadOnlySpan(polygon));
}
///
/// Determines if a 2D point is inside a polygon using the ray-casting algorithm.
///
/// The point to test.
/// The vertices of the polygon in order (clockwise or counter-clockwise).
///
/// true if the point is inside the polygon; otherwise, false.
/// Returns false for degenerate polygons (fewer than 3 vertices).
///
///
/// Points exactly on polygon edges or vertices may return inconsistent results due to floating-point precision.
/// The polygon is assumed to be simple (non-self-intersecting) for predictable results.
/// This overload is more performant as it avoids array allocations.
///
public static bool IsPointInsidePolygon(Vector2 point, ReadOnlySpan polygon)
{
if (polygon.Length < 3)
{
return false;
}
bool inside = false;
int j = polygon.Length - 1;
for (int i = 0; i < polygon.Length; i++)
{
Vector2 vi = polygon[i];
Vector2 vj = polygon[j];
// Check if the edge crosses the horizontal ray through the point
if ((vi.y < point.y && vj.y >= point.y) || (vj.y < point.y && vi.y >= point.y))
{
// Calculate x-coordinate of edge intersection with horizontal ray at point.y
float intersectX = vi.x + (point.y - vi.y) / (vj.y - vi.y) * (vj.x - vi.x);
// Count intersection if it's to the left of the point
if (intersectX < point.x)
{
inside = !inside;
}
}
j = i;
}
return inside;
}
///
/// Determines if a 3D point is inside a 3D polygon by projecting onto a plane.
///
/// The 3D point to test.
/// The vertices of the 3D polygon in order (must be coplanar).
/// The normal vector of the plane containing the polygon. Must be normalized.
///
/// true if the point (projected onto the polygon's plane) is inside the polygon; otherwise, false.
/// Returns false for null or degenerate polygons (fewer than 3 vertices).
///
///
/// The point is first projected onto the plane defined by the polygon and plane normal.
/// Then a 2D point-in-polygon test is performed using a coordinate system aligned with the plane.
/// The polygon vertices must be coplanar for accurate results.
///
public static bool IsPointInsidePolygon(
Vector3 point,
Vector3[] polygon,
Vector3 planeNormal
)
{
if (polygon == null || polygon.Length < 3)
{
return false;
}
return IsPointInsidePolygon(point, new ReadOnlySpan(polygon), planeNormal);
}
///
/// Determines if a 3D point is inside a 3D polygon by projecting onto a plane.
///
/// The 3D point to test.
/// The vertices of the 3D polygon in order (must be coplanar).
/// The normal vector of the plane containing the polygon. Must be normalized.
///
/// true if the point (projected onto the polygon's plane) is inside the polygon; otherwise, false.
/// Returns false for degenerate polygons (fewer than 3 vertices).
///
///
/// The point is first projected onto the plane defined by the polygon and plane normal.
/// Then a 2D point-in-polygon test is performed using a coordinate system aligned with the plane.
/// The polygon vertices must be coplanar for accurate results.
/// This overload is more performant as it avoids array allocations.
///
public static bool IsPointInsidePolygon(
Vector3 point,
ReadOnlySpan polygon,
Vector3 planeNormal
)
{
if (polygon.Length < 3)
{
return false;
}
// Create a coordinate system on the plane
// Find two orthogonal vectors in the plane
Vector3 tangent;
if (Mathf.Abs(planeNormal.x) > 0.9f)
{
tangent = Vector3.Cross(planeNormal, Vector3.up).normalized;
}
else
{
tangent = Vector3.Cross(planeNormal, Vector3.right).normalized;
}
Vector3 bitangent = Vector3.Cross(planeNormal, tangent);
// Project the point onto the plane using the polygon's first vertex as origin
Vector3 origin = polygon[0];
Vector3 relativePoint = point - origin;
// Convert to 2D coordinates in the plane's coordinate system
Vector2 point2D = new(
Vector3.Dot(relativePoint, tangent),
Vector3.Dot(relativePoint, bitangent)
);
// Convert all polygon vertices to 2D
Span polygon2D = stackalloc Vector2[polygon.Length];
for (int i = 0; i < polygon.Length; i++)
{
Vector3 relativeVertex = polygon[i] - origin;
polygon2D[i] = new Vector2(
Vector3.Dot(relativeVertex, tangent),
Vector3.Dot(relativeVertex, bitangent)
);
}
return IsPointInsidePolygon(point2D, polygon2D);
}
}
}