// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Math { using System; using System.Runtime.Serialization; using System.Text.Json.Serialization; using DataStructure; using ProtoBuf; using UnityEngine; using WallstopStudios.UnityHelpers.Core.Helper; /// /// Represents a line segment defined by two endpoints in 3D space. /// [Serializable] [DataContract] [ProtoContract] public readonly struct Line3D : IEquatable { /// /// The starting point of the line segment. /// [DataMember] [ProtoMember(1)] public readonly Vector3 from; /// /// The ending point of the line segment. /// [DataMember] [ProtoMember(2)] public readonly Vector3 to; /// /// Constructs a line segment from two points. /// /// The starting point. /// The ending point. [JsonConstructor] public Line3D(Vector3 from, Vector3 to) { this.from = from; this.to = to; } /// /// Gets the length of the line segment. /// public float Length => Vector3.Distance(from, to); /// /// Gets the squared length of the line segment (more performant than Length). /// public float LengthSquared { get { float dx = to.x - from.x; float dy = to.y - from.y; float dz = to.z - from.z; return dx * dx + dy * dy + dz * dz; } } /// /// Gets the direction vector from 'from' to 'to' (unnormalized). /// public Vector3 Direction => to - from; /// /// Gets the normalized direction vector from 'from' to 'to'. /// public Vector3 NormalizedDirection => (to - from).normalized; /// /// Checks if this line segment intersects with a sphere. /// /// The sphere to test for intersection. /// True if the line segment intersects or touches the sphere. public bool Intersects(Sphere sphere) { float distanceSquared = DistanceSquaredToPoint(sphere.center); float radiusSquared = sphere.radius * sphere.radius; return distanceSquared <= radiusSquared; } /// /// Checks if this line segment intersects with a bounding box. /// /// The bounding box to test for intersection. /// True if the line segment intersects the bounding box. public bool Intersects(BoundingBox3D bounds) { if (bounds.IsEmpty) { return false; } return TryClipSegmentAABB(bounds, out _, out _); } /// /// Finds the closest points between this line segment and another line segment. /// For skew lines (lines that don't intersect and aren't parallel), this finds the unique closest pair. /// /// The other line segment. /// The closest point on this line segment. /// The closest point on the other line segment. /// True if the lines are not parallel, false if they are parallel or nearly parallel. public bool TryGetClosestPoints( Line3D other, out Vector3 thisClosest, out Vector3 otherClosest ) { Vector3 d1 = Direction; Vector3 d2 = other.Direction; Vector3 r = from - other.from; float a = Vector3.Dot(d1, d1); float b = Vector3.Dot(d1, d2); float c = Vector3.Dot(d1, r); float e = Vector3.Dot(d2, d2); float f = Vector3.Dot(d2, r); float denom = a * e - b * b; if (Mathf.Approximately(denom, 0)) { thisClosest = from; otherClosest = other.ClosestPointOnLine(from); return false; } float s = Mathf.Clamp01((b * f - c * e) / denom); float t = Mathf.Clamp01((a * f - b * c) / denom); thisClosest = from + s * d1; otherClosest = other.from + t * d2; return true; } /// /// Calculates the shortest distance between this line segment and another line segment. /// /// The other line segment. /// The shortest distance between the two line segments. public float DistanceToLine(Line3D other) { TryGetClosestPoints(other, out Vector3 thisClosest, out Vector3 otherClosest); return Vector3.Distance(thisClosest, otherClosest); } /// /// Calculates the shortest distance from a point to this line segment. /// /// The point to measure distance from. /// The shortest distance from the point to the line segment. public float DistanceToPoint(Vector3 point) { Vector3 closestPoint = ClosestPointOnLine(point); return Vector3.Distance(point, closestPoint); } /// /// Calculates the squared distance from a point to this line segment. /// More performant than DistanceToPoint when only comparing distances. /// /// The point to measure distance from. /// The squared distance from the point to the line segment. public float DistanceSquaredToPoint(Vector3 point) { Vector3 closestPoint = ClosestPointOnLine(point); return (point - closestPoint).sqrMagnitude; } /// /// Calculates the shortest distance from a sphere to this line segment. /// Returns 0 if the line intersects the sphere. /// /// The sphere to measure distance from. /// The shortest distance from the sphere's surface to the line segment. public float DistanceToSphere(Sphere sphere) { float distanceToCenter = DistanceToPoint(sphere.center); return Mathf.Max(0f, distanceToCenter - sphere.radius); } /// /// Calculates the shortest distance from a bounding box to this line segment. /// Returns 0 if the line intersects the bounding box. /// /// The bounding box to measure distance from. /// The shortest distance from the bounding box to the line segment. public float DistanceToBounds(BoundingBox3D bounds) { if (bounds.IsEmpty) { return float.PositiveInfinity; } Vector3 closestOnLine = ClosestPointOnBounds(bounds); Vector3 closestOnBounds = ClampToBounds(closestOnLine, bounds); return Vector3.Distance(closestOnLine, closestOnBounds); } /// /// Finds the closest point on this line segment to the given point. /// /// The point to project onto the line. /// The closest point on the line segment. public Vector3 ClosestPointOnLine(Vector3 point) { Vector3 dir = to - from; float lengthSq = dir.sqrMagnitude; if (Mathf.Approximately(lengthSq, 0)) { return from; } float t = Vector3.Dot(point - from, dir) / lengthSq; t = Mathf.Clamp01(t); return from + t * dir; } /// /// Finds the closest point on this line segment to a bounding box. /// /// The bounding box. /// The closest point on the line segment to the bounding box. public Vector3 ClosestPointOnBounds(BoundingBox3D bounds) { if (bounds.IsEmpty) { return from; } // If it intersects, return the first intersection point along the segment. if (TryClipSegmentAABB(bounds, out float tEnter, out _)) { float tHit = Mathf.Clamp01(tEnter); return from + (to - from) * tHit; } // Otherwise, find the exact closest point on the segment to the AABB using // convex 1D minimization of f(t) = ||p(t) - clamp(p(t))||^2 over t in [0,1]. Vector3 d = to - from; float lenSq = d.sqrMagnitude; if (lenSq <= 1e-20f) { return from; } Vector3 localFrom = from; float g0 = G(0f); float g1 = G(1f); if (g0 >= 0f) { return from; } if (g1 <= 0f) { return to; } float a = 0f; float b = 1f; for (int i = 0; i < 50; i++) { float m = 0.5f * (a + b); float gm = G(m); if (gm > 0f) { b = m; } else { a = m; } } float tStar = 0.5f * (a + b); return from + d * tStar; float G(float t) { Vector3 p = localFrom + d * t; Vector3 c = ClampToBounds(p, bounds); Vector3 diff = p - c; return diff.x * d.x + diff.y * d.y + diff.z * d.z; } } private static Vector3 ClampToBounds(Vector3 p, BoundingBox3D bounds) { return new Vector3( Mathf.Clamp(p.x, bounds.min.x, bounds.max.x), Mathf.Clamp(p.y, bounds.min.y, bounds.max.y), Mathf.Clamp(p.z, bounds.min.z, bounds.max.z) ); } private bool TryClipSegmentAABB(BoundingBox3D bounds, out float tEnter, out float tExit) { Vector3 d = to - from; tEnter = 0f; tExit = 1f; // X axis if (Mathf.Abs(d.x) < 1e-8f) { if (from.x < bounds.min.x || from.x > bounds.max.x) { return false; } } else { float inv = 1f / d.x; float t1 = (bounds.min.x - from.x) * inv; float t2 = (bounds.max.x - from.x) * inv; if (t1 > t2) { (t1, t2) = (t2, t1); } tEnter = Mathf.Max(tEnter, t1); tExit = Mathf.Min(tExit, t2); if (tEnter > tExit) { return false; } } // Y axis if (Mathf.Abs(d.y) < 1e-8f) { if (from.y < bounds.min.y || from.y > bounds.max.y) { return false; } } else { float inv = 1f / d.y; float t1 = (bounds.min.y - from.y) * inv; float t2 = (bounds.max.y - from.y) * inv; if (t1 > t2) { (t1, t2) = (t2, t1); } tEnter = Mathf.Max(tEnter, t1); tExit = Mathf.Min(tExit, t2); if (tEnter > tExit) { return false; } } // Z axis if (Mathf.Abs(d.z) < 1e-8f) { if (from.z < bounds.min.z || from.z > bounds.max.z) { return false; } } else { float inv = 1f / d.z; float t1 = (bounds.min.z - from.z) * inv; float t2 = (bounds.max.z - from.z) * inv; if (t1 > t2) { (t1, t2) = (t2, t1); } tEnter = Mathf.Max(tEnter, t1); tExit = Mathf.Min(tExit, t2); if (tEnter > tExit) { return false; } } return tExit >= 0f && tEnter <= 1f && tExit >= tEnter; } /// /// Checks if a point lies on this line segment (within a specified tolerance). /// /// The point to check. /// The maximum distance from the line to consider the point as contained. /// True if the point lies on the line segment within the tolerance, false otherwise. public bool Contains(Vector3 point, float tolerance = 0.0001f) { Vector3 closestPoint = ClosestPointOnLine(point); return Vector3.Distance(point, closestPoint) <= tolerance; } /// /// Checks if this line is equal to another line. /// Two lines are equal if they have the same endpoints (in the same order). /// public bool Equals(Line3D other) { return from == other.from && to == other.to; } /// /// Checks if this line is equal to another object. /// public override bool Equals(object obj) { return obj is Line3D other && Equals(other); } /// /// Gets the hash code for this line. /// public override int GetHashCode() { return Objects.HashCode(from, to); } /// /// Returns a string representation of this line. /// public override string ToString() { return $"Line3D(from: {from}, to: {to})"; } /// /// Equality operator. /// public static bool operator ==(Line3D left, Line3D right) { return left.Equals(right); } /// /// Inequality operator. /// public static bool operator !=(Line3D left, Line3D right) { return !left.Equals(right); } } }