// 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 Extension; using ProtoBuf; using UnityEngine; using WallstopStudios.UnityHelpers.Core.Helper; /// /// Represents a line segment defined by two endpoints in 2D space. /// /// /// /// var a = new Line2D(new Vector2(0,0), new Vector2(2,0)); /// var b = new Line2D(new Vector2(1,-1), new Vector2(1,1)); /// bool intersects = a.Intersects(b); // true /// /// [Serializable] [DataContract] [ProtoContract] public readonly struct Line2D : IEquatable { /// /// The starting point of the line segment. /// [DataMember] [ProtoMember(1)] public readonly Vector2 from; /// /// The ending point of the line segment. /// [DataMember] [ProtoMember(2)] public readonly Vector2 to; /// /// Constructs a line segment from two points. /// /// The starting point. /// The ending point. [JsonConstructor] public Line2D(Vector2 from, Vector2 to) { this.from = from; this.to = to; } /// /// Gets the length of the line segment. /// public float Length => Vector2.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; return dx * dx + dy * dy; } } /// /// Gets the direction vector from 'from' to 'to' (unnormalized). /// public Vector2 Direction => to - from; /// /// Gets the normalized direction vector from 'from' to 'to'. /// public Vector2 NormalizedDirection => (to - from).normalized; /// /// Checks if this line segment intersects with another line segment. /// /// The other line segment to test. /// True if the segments intersect, false otherwise. public bool Intersects(Line2D other) { return UnityExtensions.Intersects(from, to, other.from, other.to); } /// /// Checks if this line segment intersects with a circle. /// /// The circle to test for intersection. /// True if the line segment intersects or touches the circle. public bool Intersects(Circle circle) { float distanceSquared = DistanceSquaredToPoint(circle.center); float radiusSquared = circle.radius * circle.radius; return distanceSquared <= radiusSquared; } /// /// Attempts to find the intersection point between this line segment and another. /// /// The other line segment to test. /// The intersection point if found. /// True if an intersection point exists, false otherwise (including parallel/collinear cases). public bool TryGetIntersectionPoint(Line2D other, out Vector2 intersection) { Vector2 d1 = to - from; Vector2 d2 = other.to - other.from; float determinant = d1.x * d2.y - d1.y * d2.x; if (Mathf.Approximately(determinant, 0)) { intersection = default; return false; } Vector2 diff = other.from - from; float t1 = (diff.x * d2.y - diff.y * d2.x) / determinant; float t2 = (diff.x * d1.y - diff.y * d1.x) / determinant; if (t1 is >= 0 and <= 1 && t2 is >= 0 and <= 1) { intersection = from + t1 * d1; return true; } intersection = default; return false; } /// /// 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(Vector2 point) { Vector2 closestPoint = ClosestPointOnLine(point); return Vector2.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(Vector2 point) { Vector2 closestPoint = ClosestPointOnLine(point); return (point - closestPoint).sqrMagnitude; } /// /// Calculates the shortest distance from a circle to this line segment. /// Returns 0 if the line intersects the circle. /// /// The circle to measure distance from. /// The shortest distance from the circle's edge to the line segment. public float DistanceToCircle(Circle circle) { float distanceToCenter = DistanceToPoint(circle.center); return Mathf.Max(0f, distanceToCenter - circle.radius); } /// /// 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 Vector2 ClosestPointOnLine(Vector2 point) { Vector2 dir = to - from; float lengthSq = dir.sqrMagnitude; if (Mathf.Approximately(lengthSq, 0)) { return from; } float t = Vector2.Dot(point - from, dir) / lengthSq; t = Mathf.Clamp01(t); return from + t * dir; } /// /// Checks if a point lies on this line segment (within floating point tolerance). /// /// The point to check. /// True if the point lies on the line segment, false otherwise. public bool Contains(Vector2 point) { Vector2 toPoint = point - from; Vector2 toEnd = to - from; float cross = toPoint.x * toEnd.y - toPoint.y * toEnd.x; if (!Mathf.Approximately(cross, 0)) { return false; } return UnityExtensions.LiesOnSegment(from, point, to); } /// /// 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(Line2D 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 Line2D 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 $"Line2D(from: {from}, to: {to})"; } /// /// Equality operator. /// public static bool operator ==(Line2D left, Line2D right) { return left.Equals(right); } /// /// Inequality operator. /// public static bool operator !=(Line2D left, Line2D right) { return !left.Equals(right); } } }