// 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})";
}
}
}