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