// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE // ReSharper disable once CheckNamespace namespace WallstopStudios.UnityHelpers.Core.Extension { using System; using System.Collections.Generic; using DataStructure.Adapters; using Helper; using UnityEngine; public static partial class UnityExtensions { // ===================== Vector2 Convex Hull Containment ===================== public static bool IsConvexHullInsideConvexHull( this List convexHull, List maybeInside ) { int orientation = DetermineConvexHullOrientation(convexHull); foreach (Vector2 point in maybeInside) { if (!IsPointInsideConvexHull(convexHull, point, orientation)) { return false; } } return true; } private static int DetermineConvexHullOrientation(List convexHull) { if (convexHull == null || convexHull.Count < 3) { return 0; } double area = 0d; for (int i = 0; i < convexHull.Count; ++i) { Vector2 a = convexHull[i]; Vector2 b = convexHull[(i + 1) % convexHull.Count]; area += (double)a.x * b.y - (double)a.y * b.x; } if (Math.Abs(area) <= ConvexHullOrientationEpsilon) { return 0; } return area > 0d ? 1 : -1; } private static bool IsPointInsideConvexHull( List convexHull, Vector2 point, int expectedSide ) { if (convexHull == null || convexHull.Count == 0) { return true; } int requiredSide = expectedSide; for (int i = 0; i < convexHull.Count; ++i) { Vector2 lhs = convexHull[i]; Vector2 rhs = convexHull[(i + 1) % convexHull.Count]; float relation = Geometry.IsAPointLeftOfVectorOrOnTheLine(lhs, rhs, point); if (Mathf.Abs(relation) <= ConvexHullRelationEpsilon) { continue; } int side = requiredSide; if (side == 0) { side = relation > 0f ? 1 : -1; requiredSide = side; } if (relation * side < -ConvexHullRelationEpsilon) { return false; } } return true; } /// /// Determines if one convex hull is completely inside another convex hull. /// /// The outer convex hull to test against. /// The Grid used for coordinate conversion. /// The convex hull to test if it's inside. /// True if all points of maybeInside are inside convexHull; otherwise, false. /// /// Thread Safety: Must be called from Unity main thread (uses Grid). /// Null Handling: Returns true if convexHull is null or empty. Throws if grid or maybeInside is null. /// Performance: O(nm) where n is outer hull size and m is inner hull size. /// Allocations: None. /// Unity Behavior: Uses grid.CellToWorld for spatial calculations. /// Edge Cases: Empty or null outer hull returns true. Determines hull orientation automatically. /// public static bool IsConvexHullInsideConvexHull( this List convexHull, Grid grid, List maybeInside ) { int orientation = DetermineConvexHullOrientation(convexHull, grid); foreach (FastVector3Int point in maybeInside) { if (!IsPointInsideConvexHull(convexHull, grid, point, orientation)) { return false; } } return true; } /// /// Determines if a Vector3Int point is inside a convex hull. /// /// The convex hull to test against. /// The Grid used for coordinate conversion. /// The point to test. /// True if the point is inside or on the boundary of the convex hull; otherwise, false. /// /// Thread Safety: Must be called from Unity main thread (uses Grid). /// Null Handling: Returns true if convexHull is null or empty. Throws if grid is null. /// Performance: O(n) where n is the hull size. /// Allocations: None. /// Unity Behavior: Uses grid.CellToWorld for spatial calculations. /// Edge Cases: Points on the hull boundary are considered inside. Determines orientation automatically. /// public static bool IsPointInsideConvexHull( this List convexHull, Grid grid, Vector3Int point ) { int orientation = DetermineConvexHullOrientation(convexHull, grid); return IsPointInsideConvexHull(convexHull, grid, point, orientation); } /// /// Determines if a FastVector3Int point is inside a convex hull. /// /// The convex hull to test against. /// The Grid used for coordinate conversion. /// The point to test. /// True if the point is inside or on the boundary of the convex hull; otherwise, false. /// /// Thread Safety: Must be called from Unity main thread (uses Grid). /// Null Handling: Returns true if convexHull is null or empty. Throws if grid is null. /// Performance: O(n) where n is the hull size. /// Allocations: None. /// Unity Behavior: Uses grid.CellToWorld for spatial calculations. /// Edge Cases: Points on the hull boundary are considered inside. Determines orientation automatically. /// public static bool IsPointInsideConvexHull( this List convexHull, Grid grid, FastVector3Int point ) { int orientation = DetermineConvexHullOrientation(convexHull, grid); return IsPointInsideConvexHull(convexHull, grid, point, orientation); } /// /// Determines if one convex hull is completely inside another convex hull. /// /// The outer convex hull to test against. /// The Grid used for coordinate conversion. /// The convex hull to test if it's inside. /// True if all points of maybeInside are inside convexHull; otherwise, false. /// /// Thread Safety: Must be called from Unity main thread (uses Grid). /// Null Handling: Returns true if convexHull is null or empty. Throws if grid or maybeInside is null. /// Performance: O(nm) where n is outer hull size and m is inner hull size. /// Allocations: None. /// Unity Behavior: Uses grid.CellToWorld for spatial calculations. /// Edge Cases: Empty or null outer hull returns true. Determines hull orientation automatically. /// public static bool IsConvexHullInsideConvexHull( this List convexHull, Grid grid, List maybeInside ) { int orientation = DetermineConvexHullOrientation(convexHull, grid); foreach (Vector3Int point in maybeInside) { if (!IsPointInsideConvexHull(convexHull, grid, point, orientation)) { return false; } } return true; } private static int DetermineConvexHullOrientation( IReadOnlyList convexHull, Grid grid ) { if (convexHull == null || convexHull.Count < 3) { return 0; } double twiceArea = 0d; Vector3 previousWorld = grid.CellToWorld(convexHull[^1]); Vector2 previous = new(previousWorld.x, previousWorld.y); for (int i = 0; i < convexHull.Count; ++i) { Vector3 currentWorld = grid.CellToWorld(convexHull[i]); Vector2 current = new(currentWorld.x, currentWorld.y); twiceArea += (previous.x * current.y) - (current.x * previous.y); previous = current; } if (Math.Abs(twiceArea) <= ConvexHullOrientationEpsilon) { return 0; } return twiceArea > 0d ? 1 : -1; } private static int DetermineConvexHullOrientation( IReadOnlyList convexHull, Grid grid ) { if (convexHull == null || convexHull.Count < 3) { return 0; } double twiceArea = 0d; Vector3 previousWorld = grid.CellToWorld(convexHull[^1]); Vector2 previous = new(previousWorld.x, previousWorld.y); for (int i = 0; i < convexHull.Count; ++i) { Vector3 currentWorld = grid.CellToWorld(convexHull[i]); Vector2 current = new(currentWorld.x, currentWorld.y); twiceArea += (previous.x * current.y) - (current.x * previous.y); previous = current; } if (Math.Abs(twiceArea) <= ConvexHullOrientationEpsilon) { return 0; } return twiceArea > 0d ? 1 : -1; } private static bool IsPointInsideConvexHull( List convexHull, Grid grid, FastVector3Int point, int expectedSide ) { if (convexHull == null || convexHull.Count == 0) { return true; } int requiredSide = expectedSide; Vector3 pointWorld = grid.CellToWorld(point); for (int i = 0; i < convexHull.Count; ++i) { FastVector3Int lhs = convexHull[i]; FastVector3Int rhs = convexHull[(i + 1) % convexHull.Count]; float relation = Geometry.IsAPointLeftOfVectorOrOnTheLine( grid.CellToWorld(lhs), grid.CellToWorld(rhs), pointWorld ); if (Mathf.Abs(relation) <= ConvexHullRelationEpsilon) { continue; } int side = requiredSide; if (side == 0) { side = relation > 0f ? 1 : -1; requiredSide = side; } if (relation * side < -ConvexHullRelationEpsilon) { return false; } } return true; } private static bool IsPointInsideConvexHull( List convexHull, Grid grid, Vector3Int point, int expectedSide ) { if (convexHull == null || convexHull.Count == 0) { return true; } int requiredSide = expectedSide; Vector3 pointWorld = grid.CellToWorld(point); for (int i = 0; i < convexHull.Count; ++i) { Vector3Int lhs = convexHull[i]; Vector3Int rhs = convexHull[(i + 1) % convexHull.Count]; float relation = Geometry.IsAPointLeftOfVectorOrOnTheLine( grid.CellToWorld(lhs), grid.CellToWorld(rhs), pointWorld ); if (Mathf.Abs(relation) <= ConvexHullRelationEpsilon) { continue; } int side = requiredSide; if (side == 0) { side = relation > 0f ? 1 : -1; requiredSide = side; } if (relation * side < -ConvexHullRelationEpsilon) { return false; } } return true; } } }