// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Extension { using System; using System.Collections.Generic; using Model; using UnityEngine; /// /// Extension methods for Direction enum, providing conversions to vectors, direction combining, and directional operations. /// /// /// Thread Safety: All methods are thread-safe as they operate on value types without shared mutable state. /// Performance: Most operations are O(1) constant time. Split operations are O(8) for iterating all directions. /// public static class DirectionExtensions { private static readonly Direction[] Directions = { Direction.North, Direction.NorthEast, Direction.East, Direction.SouthEast, Direction.South, Direction.SouthWest, Direction.West, Direction.NorthWest, }; /// /// Returns the opposite direction of the given direction. /// /// The direction to get the opposite of. /// The opposite direction (e.g., North returns South, NorthEast returns SouthWest). /// /// Null handling: Direction is a value type, cannot be null. /// Thread safety: Thread-safe. No Unity main thread requirement. /// Performance: O(1) - simple switch statement. /// Allocations: No allocations. /// Edge cases: Direction.None returns Direction.None. Unknown direction values throw ArgumentException. /// /// Thrown when direction is not a recognized Direction value. public static Direction Opposite(this Direction direction) { switch (direction) { case Direction.North: return Direction.South; case Direction.NorthEast: return Direction.SouthWest; case Direction.East: return Direction.West; case Direction.SouthEast: return Direction.NorthWest; case Direction.South: return Direction.North; case Direction.SouthWest: return Direction.NorthEast; case Direction.West: return Direction.East; case Direction.NorthWest: return Direction.SouthEast; case Direction.None: return Direction.None; default: throw new ArgumentException($"Unknown direction {direction}."); } } /// /// Converts a Direction to its corresponding Vector2Int representation. /// /// The direction to convert. /// A Vector2Int representing the direction (e.g., North = (0,1), East = (1,0)). /// /// Null handling: Direction is a value type, cannot be null. /// Thread safety: Thread-safe. No Unity main thread requirement. /// Performance: O(1) - simple switch statement. /// Allocations: Allocates a Vector2Int struct (stack allocation). /// Edge cases: Direction.None returns Vector2Int.zero. Diagonal directions return unit diagonals (not normalized). Unknown direction values throw ArgumentException. /// /// Thrown when direction is not a recognized Direction value. public static Vector2Int AsVector2Int(this Direction direction) { switch (direction) { case Direction.None: return Vector2Int.zero; case Direction.North: return Vector2Int.up; case Direction.NorthEast: return new Vector2Int(1, 1); case Direction.East: return Vector2Int.right; case Direction.SouthEast: return new Vector2Int(1, -1); case Direction.South: return Vector2Int.down; case Direction.SouthWest: return new Vector2Int(-1, -1); case Direction.West: return Vector2Int.left; case Direction.NorthWest: return new Vector2Int(-1, 1); default: throw new ArgumentException($"Unknown direction {direction}."); } } /// /// Converts a Direction to its corresponding Vector2 representation. /// /// The direction to convert. /// A Vector2 representing the direction via implicit conversion from Vector2Int. /// /// Null handling: Direction is a value type, cannot be null. /// Thread safety: Thread-safe. No Unity main thread requirement. /// Performance: O(1) - delegates to AsVector2Int. /// Allocations: Allocates a Vector2 struct (stack allocation). /// Edge cases: Same behavior as AsVector2Int. Diagonal directions have magnitude ~1.41 (not normalized). /// /// Thrown when direction is not a recognized Direction value. public static Vector2 AsVector2(this Direction direction) { return direction.AsVector2Int(); } /// /// Converts a Vector3 to its closest Direction by treating it as a Vector2 (ignoring z-component). /// /// The vector to convert to a direction. /// The closest cardinal or diagonal Direction based on the vector's angle. /// /// Null handling: Vector3 is a value type, cannot be null. /// Thread safety: Thread-safe. No Unity main thread requirement. /// Performance: O(1) - delegates to Vector2 overload which performs angle calculations. /// Allocations: Minimal stack allocations for Vector2 cast. /// Edge cases: Z-component is ignored. Zero vector returns Direction.None. /// public static Direction AsDirection(this Vector3 vector3) { return ((Vector2)vector3).AsDirection(); } /// /// Splits a combined Direction flags value into individual Direction values. /// /// The direction flags to split. /// An enumerable of individual Direction values that are set in the flags. /// /// Null handling: Direction is a value type, cannot be null. /// Thread safety: Thread-safe. No Unity main thread requirement. /// Performance: O(8) - iterates through all 8 possible directions. /// Allocations: Allocates iterator state machine for yield return. /// Edge cases: If no flags are set, yields Direction.None. Multiple flags yield multiple directions. /// public static IEnumerable Split(this Direction direction) { bool foundAny = false; foreach (Direction singleDirection in Directions) { if (direction.HasFlagNoAlloc(singleDirection)) { foundAny = true; yield return singleDirection; } } if (!foundAny) { yield return Direction.None; } } /// /// Splits a combined Direction flags value into individual Direction values, storing them in a provided buffer. /// /// The direction flags to split. /// The list to clear and populate with individual directions. /// The same buffer list passed in, now populated with individual Direction values. /// /// Null handling: Throws NullReferenceException if buffer is null. /// Thread safety: Not thread-safe. No Unity main thread requirement. /// Performance: O(8) - iterates through all 8 possible directions. /// Allocations: No allocations if buffer has sufficient capacity. May allocate if buffer needs to grow. /// Edge cases: If no flags are set, buffer contains only Direction.None. Buffer is cleared before populating. /// public static List Split(this Direction direction, List buffer) { buffer.Clear(); foreach (Direction singleDirection in Directions) { if (direction.HasFlagNoAlloc(singleDirection)) { buffer.Add(singleDirection); } } if (buffer.Count == 0) { buffer.Add(Direction.None); } return buffer; } /// /// Combines multiple Direction values into a single Direction flags value using bitwise OR. /// /// The enumerable of directions to combine. /// A Direction value with all input direction flags set. /// /// Null handling: Throws ArgumentNullException if directions is null. /// Thread safety: Thread-safe for read-only collections. Not thread-safe if collection is modified during enumeration. No Unity main thread requirement. /// Performance: O(n) where n is the number of directions. Optimized for IReadOnlyList and HashSet. /// Allocations: No allocations. /// Edge cases: Empty enumerable returns Direction.None. Duplicate directions have no additional effect due to bitwise OR. /// /// Thrown when directions is null. public static Direction Combine(this IEnumerable directions) { if (directions == null) { throw new ArgumentNullException(nameof(directions)); } Direction combined = Direction.None; switch (directions) { case IReadOnlyList list: { for (int i = 0; i < list.Count; ++i) { combined |= list[i]; } break; } case HashSet set: { foreach (Direction direction in set) { combined |= direction; } break; } default: { foreach (Direction direction in directions) { combined |= direction; } break; } } return combined; } /// /// Converts a Vector2 to its closest Direction based on angle. /// /// The vector to convert to a direction. /// If true, uses wider angle ranges (60 degrees) favoring diagonal directions. If false, uses equal ranges (45 degrees). /// The closest Direction based on the vector's angle from north (up). /// /// Null handling: Vector2 is a value type, cannot be null. /// Thread safety: Thread-safe. No Unity main thread requirement. /// Performance: O(1) - calculates angle once using Atan2, then performs range checks. /// Allocations: No allocations. /// Edge cases: Zero vector returns Direction.None. preferAngles=true uses 60-degree ranges for diagonals and 30-degree for cardinals. preferAngles=false uses equal 45-degree ranges. /// public static Direction AsDirection(this Vector2 vector, bool preferAngles = false) { if (vector == Vector2.zero) { return Direction.None; } float angle; if (vector.x < 0) { angle = 360 - Mathf.Atan2(vector.x, vector.y) * Mathf.Rad2Deg * -1; } else { angle = Mathf.Atan2(vector.x, vector.y) * Mathf.Rad2Deg; } if (preferAngles) { if (345 <= angle || angle < 15) { return Direction.North; } if (angle is >= 15 and < 75) { return Direction.NorthEast; } if (angle is >= 75 and < 105) { return Direction.East; } if (angle is >= 105 and < 165) { return Direction.SouthEast; } if (angle is >= 165 and < 195) { return Direction.South; } if (angle is >= 195 and < 255) { return Direction.SouthWest; } if (angle is >= 255 and < 285) { return Direction.West; } if (angle is >= 285 and < 345) { return Direction.NorthWest; } } if (337.5 <= angle || angle < 22.5) { return Direction.North; } if (22.5 <= angle && angle < 67.5) { return Direction.NorthEast; } if (67.5 <= angle && angle < 112.5) { return Direction.East; } if (112.5 <= angle && angle < 157.5) { return Direction.SouthEast; } if (157.5 <= angle && angle < 202.5) { return Direction.South; } if (202.5 <= angle && angle < 247.5) { return Direction.SouthWest; } if (247.5 <= angle && angle < 292.5) { return Direction.West; } if (292.5 <= angle && angle < 337.5) { return Direction.NorthWest; } return Direction.None; } } }