using System; using UnityEngine; using Framework.Support; using ConditionalAttribute = System.Diagnostics.ConditionalAttribute; namespace Framework.Geometry.Lines.Intersection { /// Provides helper methods for the navigation classes public static class Intersector { /// Tests whether a point is inside an ellipse /// Point that will be tested for being inside the ellipse /// Center of the ellipse the point will be tested against /// Extents of the ellipse /// True if the point lies within the ellipse, otherwise false public static bool IsPointInEllipse( Vector2 point, Vector2 ellipseCenter, Vector2 ellipseExtents ) { if((ellipseExtents.x == 0.0f) || (ellipseExtents.y == 0.0f)) { return false; } // Scale the point by the ellipse's aspect ratio so we can perform // a simple circle / point intersection test float aspect = ellipseExtents.x / ellipseExtents.y; point.y = (point.y - ellipseCenter.y) * aspect; point.x = (point.x - ellipseCenter.x); return point.SqrMagnitude() < (ellipseExtents.x * ellipseExtents.x); } /// Tests whether a point is within a rectangle /// Point that will be tested for being inside the rectangle /// Smaller coordinates of the rectangle /// Large coordinates of the rectangle /// True if the point is inside the rectangle, false otherwise public static bool IsPointInRectangle( Vector2 point, Vector2 rectangleMin, Vector2 rectangleMax ) { if((rectangleMax.x <= rectangleMin.x) || (rectangleMax.y <= rectangleMin.y)) { return false; } return (point.x >= rectangleMin.x) && (point.y >= rectangleMin.y) && (point.x < rectangleMax.x) && (point.y < rectangleMax.y); } /// Gets a point on a line segment at the given X coordinate /// Line segment on which a point will be determined /// X coordinate of the point that will be determined /// A point on the line segment at the specified X coordinate /// /// If the point lies outside of the line segment, it will be extrapolated. /// public static float GetLineSegmentYfromX(Segment2 segment, float x) { if(segment.From.x == segment.To.x) { return (segment.From.y + segment.To.y) / 2.0f; } // Inter- or extrapolate where we'll be on the Y axis at the specified X coordinate. Vector2 lineDirection = (segment.To - segment.From); float offsetX = x - segment.From.x; float offsetY = lineDirection.y * (offsetX / lineDirection.x); return segment.From.y + offsetY; } /// Finds the closest point to another point in a line segment /// Line segment on which the closest point will be found /// /// Point to which the closest point on the line segment will be found /// /// The closest point on the line segment to the target point public static Vector2 GetClosestPointOnLineSegment( Segment2 segment, Vector2 target ) { Vector2 lineDirection = (segment.To - segment.From); float lineLength = lineDirection.SqrMagnitude(); if(lineLength == 0.0f) { return segment.From; } float t = Vector2.Dot(target - segment.From, lineDirection); if(t < 0.0f) { return segment.From; } else if(t > lineLength) { return segment.To; } return segment.From + lineDirection * (t / lineLength); } /// Builds the union between a line segment and a rectangle /// /// Line segment that will be limited to the rectangle's area /// /// Coordinates of the rectangle's smaller corner /// Coordinates of the rectangle's larger corner /// /// The part of the line segment that was inside the rectangle or null if /// the line segment was completely outside of the rectangle /// public static Segment2? GetLineSegmentRectangleUnion( Segment2 segment, Vector2 rectangleMin, Vector2 rectangleMax ) { if((rectangleMax.x <= rectangleMin.x) || (rectangleMax.y <= rectangleMin.y)) { return null; } // Determine the direction into which the line extends Vector2 lineDirection = (segment.To - segment.From); float lineLength = lineDirection.magnitude; lineDirection /= lineLength; // Determine the contacts between the line and the disc LineContacts contacts = FindLineRectangleContacts( segment.From, lineDirection, rectangleMin, rectangleMax ); if(!contacts.HasContact) { return null; // Line never intersects the disc } if((contacts.EntryTime >= lineLength) || (contacts.ExitTime <= 0.0f)) { return null; // Intersection ouside of line segment } // Calculate the point of entry and exit into the ellipse return new Segment2( segment.From + lineDirection * Math.Max(contacts.EntryTime, 0.0f), segment.From + lineDirection * Math.Min(contacts.ExitTime, lineLength) ); } /// Builds the union between a line segment and a disc /// Line segment that will be limited to the disc's area /// Center of the disc /// Radius of the disc /// /// The part of the line segment that was inside the disc or null if /// the line segment was completely outside of the disc /// public static Segment2? GetLineSegmentEllipseUnion( Segment2 segment, Vector2 ellipseCenter, Vector2 ellipseExtents ) { if((ellipseExtents.x == 0) || (ellipseExtents.y == 0)) { return null; } // Scale the line by the ellipse's aspect ratio so we can process // the entire query as if it was a line/circle test float aspect = ellipseExtents.x / ellipseExtents.y; scaleY(ref segment, ellipseCenter.y, aspect); // Determine the direction into which the line extends Vector2 lineDirection = (segment.To - segment.From); float lineLength = lineDirection.magnitude; lineDirection /= lineLength; // Determine the contacts between the line and the disc LineContacts contacts = FindLineDiscContacts( segment.From - ellipseCenter, lineDirection, ellipseExtents.x ); if(!contacts.HasContact) { return null; // Line never intersects the disc } if((contacts.EntryTime >= lineLength) || (contacts.ExitTime <= 0.0f)) { return null; // Intersection ouside of line segment } // Descale the line and direction from the ellipse's aspect ratio into // absolute coordinates again segment.From.y = (segment.From.y - ellipseCenter.y) / aspect + ellipseCenter.y; lineDirection.y /= aspect; // Calculate the point of entry and exit on the ellipse return new Segment2( segment.From + lineDirection * Math.Max(contacts.EntryTime, 0.0f), segment.From + lineDirection * Math.Min(contacts.ExitTime, lineLength) ); } /// Locates the instants of intersection between a disc and a line /// Offset of the line from the disc's center /// Direction into which the line extends /// Radius of the disc /// The instants of intersection between the disc and the line public static LineContacts FindLineDiscContacts( Vector2 lineOffset, Vector2 lineDirection, float discRadius ) { float a0 = lineOffset.SqrMagnitude() - (discRadius * discRadius); float a1 = Vector2.Dot(lineDirection, lineOffset); float discrete = (a1 * a1) - a0; if(discrete > 0.0f) { discrete = Mathf.Sqrt(discrete); return new LineContacts(-a1 - discrete, -a1 + discrete); } else { return LineContacts.None; } } /// Finds the points where a line enters and leaves a rectangle /// Offset of the line from the coordinate system's center /// Direction into which the line extends /// Coordinates of the rectangle's smaller corner /// Coordinates of the rectangle's larger corner /// /// The entry and exit times of the line across the rectangle or LineContacts.None /// if the line didn't cross the rectangle. /// public static LineContacts FindLineRectangleContacts( Vector2 lineOffset, Vector2 lineDirection, Vector2 rectangleMin, Vector2 rectangleMax ) { LineContacts contacts = LineContacts.None; // If the line is not parallel to the Y axis, find out when it will cross // the limits of the rectangle horizontally if(lineDirection.x != 0.0f) { float leftTouchTime = (rectangleMin.x - lineOffset.x) / lineDirection.x; float rightTouchTime = (rectangleMax.x - lineOffset.x) / lineDirection.x; contacts.EntryTime = Math.Min(leftTouchTime, rightTouchTime); contacts.ExitTime = Math.Max(leftTouchTime, rightTouchTime); } else if((lineOffset.x < rectangleMin.x) || (lineOffset.x > rectangleMax.x)) { return LineContacts.None; } // If the line is not parallel to the X axis, find out when it will cross // the limits of the rectangle vertically if(lineDirection.y != 0.0f) { float topTouchTime = (rectangleMin.y - lineOffset.y) / lineDirection.y; float bottomTouchTime = (rectangleMax.y - lineOffset.y) / lineDirection.y; // If the line was parallel to the Y axis (and we already made sure that it actually // goes through the rectangle), only the Y axis needs to be taken into account. if(lineDirection.x == 0.0f) { contacts.EntryTime = Math.Min(topTouchTime, bottomTouchTime); contacts.ExitTime = Math.Max(topTouchTime, bottomTouchTime); } else { float verticalEntryTime = Math.Min(topTouchTime, bottomTouchTime); float verticalExitTime = Math.Max(topTouchTime, bottomTouchTime); // If the line already left the rectangle on one axis before entering it on // the other, it is going past the corner of the rectangle without touching it if((verticalExitTime < contacts.EntryTime) || (contacts.ExitTime < verticalEntryTime)) { return LineContacts.None; } contacts.EntryTime = Math.Max(verticalEntryTime, contacts.EntryTime); contacts.ExitTime = Math.Min(verticalExitTime, contacts.ExitTime); } } else if((lineOffset.y < rectangleMin.y) || (lineOffset.y > rectangleMax.y)) { return LineContacts.None; } return contacts; } /// Scales a line segment along the Y axis /// Line segment that will be sclaed /// Center relative to which the line segment will be scaled /// Factor by which the line segment will be scaled private static void scaleY(ref Segment2 segment, float centerY, float scaleY) { segment.From.y = (segment.From.y - centerY) * scaleY + centerY; segment.To.y = (segment.To.y - centerY) * scaleY + centerY; } } } // namespace Framework.Geometry.Lines.Intersection