// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.DataStructure { using System; using System.Text.Json.Serialization; using Extension; using Math; using UnityEngine; using WallstopStudios.UnityHelpers.Core.Helper; /// /// Lightweight 2D circle utility that powers overlap tests and distance checks for gameplay zones, detection ranges, and radial falloffs. /// /// /// /// public readonly struct Circle : IEquatable { public readonly Vector2 center; public readonly float radius; private readonly float _radiusSquared; /// /// Initializes a new circle with the specified center and radius. /// /// The center point of the circle. /// The radius of the circle. [JsonConstructor] public Circle(Vector2 center, float radius) { this.center = center; this.radius = radius; _radiusSquared = radius * radius; } /// /// Determines whether the circle contains the specified point. /// Points on the circumference are considered contained. /// /// The point to test. /// True if the point is inside or on the circle's circumference. public bool Contains(Vector2 point) { return (center - point).sqrMagnitude <= _radiusSquared; } /// /// Determines whether this circle intersects with the specified bounds. /// Returns true if there is any overlap between the circle and bounds. /// /// The bounds to test for intersection. /// True if the circle and bounds intersect. public bool Intersects(Bounds bounds) { return Intersects(bounds.Rect()); } /// /// Determines whether this circle intersects with the specified rectangle. /// Returns true if there is any overlap between the circle and rectangle. /// /// The rectangle to test for intersection. /// True if the circle and rectangle intersect. // https://www.geeksforgeeks.org/check-if-any-point-overlaps-the-given-circle-and-rectangle/ public bool Intersects(Rect rectangle) { // Compute the closest point on the rectangle to the circle center by clamping float xN = Mathf.Clamp(center.x, rectangle.xMin, rectangle.xMax); float yN = Mathf.Clamp(center.y, rectangle.yMin, rectangle.yMax); float dX = xN - center.x; float dY = yN - center.y; // Add a tiny tolerance to account for floating-point rounding when touching exactly at an edge/corner const float Tolerance = 1e-6f; return (dX * dX + dY * dY) <= (_radiusSquared + Tolerance); } /// /// Determines whether this circle intersects with another circle. /// Returns true if there is any overlap between the two circles. /// /// The other circle to test for intersection. /// True if the circles intersect. public bool Intersects(Circle other) { float combinedRadius = radius + other.radius; float combinedRadiusSquared = combinedRadius * combinedRadius; return (center - other.center).sqrMagnitude <= combinedRadiusSquared; } /// /// Determines whether this circle intersects with a line segment. /// Returns true if the line segment intersects or touches the circle. /// /// The line segment to test for intersection. /// True if the line segment intersects the circle. public bool Intersects(Line2D line) { return line.Intersects(this); } /// /// Calculates the shortest distance from this circle to a line segment. /// Returns 0 if the line intersects the circle. /// /// The line segment to measure distance from. /// The shortest distance from the circle's edge to the line segment. public float DistanceToLine(Line2D line) { return line.DistanceToCircle(this); } /// /// Finds the closest point on a line segment to this circle's center. /// /// The line segment. /// The closest point on the line segment to the circle's center. public Vector2 ClosestPointOnLine(Line2D line) { return line.ClosestPointOnLine(center); } /// /// Determines whether the specified bounds are completely contained within this circle. /// All corners of the bounds must be inside the circle. /// /// The bounds to test for containment. /// True if the bounds are completely contained within the circle. public bool Overlaps(Bounds bounds) { return Overlaps(bounds.Rect()); } /// /// Determines whether the specified rectangle is completely contained within this circle. /// All four corners of the rectangle must be inside the circle. /// /// The rectangle to test for containment. /// True if the rectangle is completely contained within the circle. public bool Overlaps(Rect rectangle) { // For a rectangle to be fully contained, all four corners must be within the circle // We can optimize by checking the farthest corner from the center Vector2 min = rectangle.min; Vector2 max = rectangle.max; // Check all four corners return Contains(min) && Contains(max) && Contains(new Vector2(min.x, max.y)) && Contains(new Vector2(max.x, min.y)); } /// /// Determines whether this circle equals another circle. /// /// The other circle to compare. /// True if the circles have the same center and radius. public bool Equals(Circle other) { return center.Equals(other.center) && Mathf.Approximately(radius, other.radius); } /// /// Determines whether this circle equals another object. /// /// The object to compare. /// True if the object is a Circle with the same center and radius. public override bool Equals(object obj) { return obj is Circle other && Equals(other); } /// /// Gets the hash code for this circle. /// /// A hash code for the current circle. public override int GetHashCode() { return Objects.HashCode(center, radius); } /// /// Determines whether two circles are equal. /// public static bool operator ==(Circle left, Circle right) { return left.Equals(right); } /// /// Determines whether two circles are not equal. /// public static bool operator !=(Circle left, Circle right) { return !left.Equals(right); } /// /// Returns a string representation of this circle. /// /// A string describing the circle's center and radius. public override string ToString() { return $"Circle(center: {center}, radius: {radius})"; } } }