// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.DataStructure { using System; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Helper; using Math; using UnityEngine; /// /// Compact 3D sphere helper for distance checks, containment tests, and broad-phase overlap queries. /// Ideal for vision cones, trigger volumes, and physics culling. /// /// /// /// public readonly struct Sphere : IEquatable { public readonly Vector3 center; public readonly float radius; private readonly float _radiusSquared; /// /// Initializes a new sphere with the specified center and radius. /// /// The center point of the sphere. /// The radius of the sphere. [JsonConstructor] public Sphere(Vector3 center, float radius) { this.center = center; this.radius = radius; _radiusSquared = radius * radius; } /// /// Determines whether the sphere contains the specified point. /// Points on the surface are considered contained. /// /// The point to test. /// True if the point is inside or on the sphere's surface. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Contains(Vector3 point) { float dx = center.x - point.x; float dy = center.y - point.y; float dz = center.z - point.z; return dx * dx + dy * dy + dz * dz <= _radiusSquared; } /// /// Determines whether this sphere intersects with the specified Unity Bounds. /// Returns true if there is any overlap between the sphere and bounds. /// /// The Unity Bounds to test for intersection. /// True if the sphere and bounds intersect. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Intersects(Bounds bounds) { return Intersects(BoundingBox3D.FromClosedBounds(bounds)); } /// /// Determines whether this sphere intersects with the specified bounding box. /// Returns true if there is any overlap between the sphere and bounds. /// /// The bounding box to test for intersection. /// True if the sphere and bounds intersect. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Intersects(BoundingBox3D bounds) { Vector3 closest = bounds.ClosestPoint(center); float dx = closest.x - center.x; float dy = closest.y - center.y; float dz = closest.z - center.z; float distanceSquared = dx * dx + dy * dy + dz * dz; // Add a tiny tolerance to account for floating-point rounding when touching exactly at an edge/corner const float Tolerance = 1e-6f; return distanceSquared <= (_radiusSquared + Tolerance); } /// /// Determines whether this sphere intersects with another sphere. /// Returns true if there is any overlap between the two spheres. /// /// The other sphere to test for intersection. /// True if the spheres intersect. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Intersects(Sphere other) { float combinedRadius = radius + other.radius; float combinedRadiusSquared = combinedRadius * combinedRadius; float dx = center.x - other.center.x; float dy = center.y - other.center.y; float dz = center.z - other.center.z; return dx * dx + dy * dy + dz * dz <= combinedRadiusSquared; } /// /// Determines whether this sphere intersects with a line segment. /// Returns true if the line segment intersects or touches the sphere. /// /// The line segment to test for intersection. /// True if the line segment intersects the sphere. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Intersects(Line3D line) { return line.Intersects(this); } /// /// Calculates the shortest distance from this sphere to a line segment. /// Returns 0 if the line intersects the sphere. /// /// The line segment to measure distance from. /// The shortest distance from the sphere's surface to the line segment. [MethodImpl(MethodImplOptions.AggressiveInlining)] public float DistanceToLine(Line3D line) { return line.DistanceToSphere(this); } /// /// Finds the closest point on a line segment to this sphere's center. /// /// The line segment. /// The closest point on the line segment to the sphere's center. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector3 ClosestPointOnLine(Line3D line) { return line.ClosestPointOnLine(center); } /// /// Determines whether the specified Unity Bounds is completely contained within this sphere. /// All corners of the bounds must be inside the sphere. /// /// The Unity Bounds to test for containment. /// True if the bounds is completely contained within the sphere. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Overlaps(Bounds bounds) { return Overlaps(BoundingBox3D.FromClosedBounds(bounds)); } /// /// Determines whether the specified bounding box is completely contained within this sphere. /// All corners of the bounding box must be inside the sphere. /// /// The bounding box to test for containment. /// True if the bounding box is completely contained within the sphere. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Overlaps(BoundingBox3D bounds) { // Empty bounds are considered to overlap any sphere if (bounds.IsEmpty) { return true; } // Special case: if the bounds.min is at the sphere center and the bounds is very small // (nearly a point due to half-open semantics), it should be considered contained. // This handles zero-size Bounds converted to BoundingBox3D where max is nudged to be exclusive. float minDx = bounds.min.x - center.x; float minDy = bounds.min.y - center.y; float minDz = bounds.min.z - center.z; float minDistSquared = minDx * minDx + minDy * minDy + minDz * minDz; if (minDistSquared <= _radiusSquared) { // Check if bounds is very small (point-like) float sizeX = bounds.max.x - bounds.min.x; float sizeY = bounds.max.y - bounds.min.y; float sizeZ = bounds.max.z - bounds.min.z; float maxSize = sizeX > sizeY ? (sizeX > sizeZ ? sizeX : sizeZ) : (sizeY > sizeZ ? sizeY : sizeZ); // If bounds is tiny and min is inside sphere, consider it overlapping if (maxSize < 1e-5f) { return true; } } // A sphere overlaps (contains) a bounds if the farthest corner of the bounds is within the sphere // For an axis-aligned bounding box, the farthest point from sphere center is one of the 8 corners float toMinX = bounds.min.x - center.x; float toMinY = bounds.min.y - center.y; float toMinZ = bounds.min.z - center.z; float toMaxX = bounds.max.x - center.x; float toMaxY = bounds.max.y - center.y; float toMaxZ = bounds.max.z - center.z; // Find the corner farthest from the sphere center by choosing the coordinate with max absolute distance float absMinX = toMinX < 0 ? -toMinX : toMinX; float absMaxX = toMaxX < 0 ? -toMaxX : toMaxX; float farthestX = absMinX > absMaxX ? toMinX : toMaxX; float absMinY = toMinY < 0 ? -toMinY : toMinY; float absMaxY = toMaxY < 0 ? -toMaxY : toMaxY; float farthestY = absMinY > absMaxY ? toMinY : toMaxY; float absMinZ = toMinZ < 0 ? -toMinZ : toMinZ; float absMaxZ = toMaxZ < 0 ? -toMaxZ : toMaxZ; float farthestZ = absMinZ > absMaxZ ? toMinZ : toMaxZ; float farthestDistanceSquared = farthestX * farthestX + farthestY * farthestY + farthestZ * farthestZ; return farthestDistanceSquared <= _radiusSquared; } /// /// Determines whether this sphere equals another sphere. /// /// The other sphere to compare. /// True if the spheres have the same center and radius. public bool Equals(Sphere other) { return center.Equals(other.center) && Mathf.Approximately(radius, other.radius); } /// /// Determines whether this sphere equals another object. /// /// The object to compare. /// True if the object is a Sphere with the same center and radius. public override bool Equals(object obj) { return obj is Sphere other && Equals(other); } /// /// Gets the hash code for this sphere. /// /// A hash code for the current sphere. public override int GetHashCode() { return Objects.HashCode(center, radius); } /// /// Determines whether two spheres are equal. /// public static bool operator ==(Sphere left, Sphere right) { return left.Equals(right); } /// /// Determines whether two spheres are not equal. /// public static bool operator !=(Sphere left, Sphere right) { return !left.Equals(right); } /// /// Returns a string representation of this sphere. /// /// A string describing the sphere's center and radius. public override string ToString() { return $"Sphere(center: {center}, radius: {radius})"; } } }