Last active
May 13, 2022 06:33
-
-
Save steveruizok/abdb58181428313d610568c7c1139a4e to your computer and use it in GitHub Desktop.
Find the point(s) where a line segment intersects an ellipse.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Find the point(s) where a line segment intersects an ellipse. | |
* @param x0 The x-axis coordinate of the line's start point. | |
* @param y0 The y-axis coordinate of the line's start point. | |
* @param x1 The x-axis coordinate of the line's end point. | |
* @param y1 The y-axis coordinate of the line's end point. | |
* @param cx The x-axis (horizontal) coordinate of the ellipse's center. | |
* @param cy The y-axis (vertical) coordinate of the ellipse's center. | |
* @param rx The ellipse's major-axis radius. Must be non-negative. | |
* @param ry The ellipse's minor-axis radius. Must be non-negative. | |
* @param rotation The rotation of the ellipse, expressed in radians. | |
* @param segment_only When true, will test the segment as a line (of infinite length). | |
*/ | |
export function getEllipseSegmentIntersections( | |
x0: number, | |
y0: number, | |
x1: number, | |
y1: number, | |
cx: number, | |
cy: number, | |
rx: number, | |
ry: number, | |
rotation = 0, | |
segment_only = true | |
) { | |
// If the ellipse or line segment are empty, return no tValues. | |
if (rx === 0 || ry === 0 || (x0 === x1 && y0 === y1)) { | |
return [] | |
} | |
// Get the semimajor and semiminor axes. | |
rx = rx < 0 ? rx : -rx | |
ry = ry < 0 ? ry : -ry | |
// Rotate points. | |
if (rotation !== 0) { | |
;[x0, y0] = rotatePoint(x0, y0, cx, cy, -rotation) | |
;[x1, y1] = rotatePoint(x1, y1, cx, cy, -rotation) | |
} | |
// Translate so the ellipse is centered at the origin. | |
x0 -= cx | |
y0 -= cy | |
x1 -= cx | |
y1 -= cy | |
// Calculate the quadratic parameters. | |
var A = ((x1 - x0) * (x1 - x0)) / rx / rx + ((y1 - y0) * (y1 - y0)) / ry / ry | |
var B = (2 * x0 * (x1 - x0)) / rx / rx + (2 * y0 * (y1 - y0)) / ry / ry | |
var C = (x0 * x0) / rx / rx + (y0 * y0) / ry / ry - 1 | |
// Make a list of t values (normalized points on the line where intersections occur). | |
var tValues: number[] = [] | |
// Calculate the discriminant. | |
var discriminant = B * B - 4 * A * C | |
if (discriminant === 0) { | |
// One real solution. | |
tValues.push(-B / 2 / A) | |
} else if (discriminant > 0) { | |
// Two real solutions. | |
tValues.push((-B + Math.sqrt(discriminant)) / 2 / A) | |
tValues.push((-B - Math.sqrt(discriminant)) / 2 / A) | |
} | |
return ( | |
tValues | |
// Filter to only points that are on the segment. | |
.filter(t => !segment_only || (t >= 0 && t <= 1)) | |
// Solve for points. | |
.map(t => [x0 + (x1 - x0) * t + cx, y0 + (y1 - y0) * t + cy]) | |
// Counter-rotate points | |
.map(p => | |
rotation === 0 ? p : rotatePoint(p[0], p[1], cx, cy, rotation) | |
) | |
) | |
} | |
/** | |
* Rotate a point around a center. | |
* @param x0 The x-axis coordinate of the point. | |
* @param y0 The y-axis coordinate of the point. | |
* @param cx The x-axis coordinate of rotation point. | |
* @param cy The y-axis coordinate of rotation point. | |
* @param rotation The rotation, expressed in radians. | |
*/ | |
export function rotatePoint( | |
x: number, | |
y: number, | |
cx: number, | |
cy: number, | |
rotation: number | |
) { | |
const s = Math.sin(rotation) | |
const c = Math.cos(rotation) | |
const px = x - cx | |
const py = y - cy | |
const nx = px * c - py * s | |
const ny = px * s + py * c | |
return [nx + cx, ny + cy] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// It may not matter to you whether the intersection points returned by the above function | |
// are in a consistent order (e.g. clockwise or anti-clockwise order), but if you do need | |
// to check this, here's a function you can use to do so. (It does matter in my case, so I | |
// reverse the array returned by `getEllipseSegmentIntersections` if the points are not in | |
// clockwise order). | |
/** | |
* Get whether points are in clockwise order relative to a third point. | |
* @param x0 The x-axis coordinate of the first point. | |
* @param y0 The y-axis coordinate of the first point. | |
* @param x1 The x-axis coordinate of the second point. | |
* @param y1 The y-axis coordinate of the second point. | |
* @param cx The x-axis coordinate of the center point. | |
* @param cy The y-axis coordinate of the center point. | |
*/ | |
export function pointsAreClockwise( | |
x0: number, | |
y0: number, | |
x1: number, | |
y1: number, | |
cx: number, | |
cy: number | |
) { | |
return Math.atan2(y0 - cy, x0 - cx) - Math.atan2(y1 - cy, x1 - cx) > 0 | |
} |
Added a second file with a function (arePointsClockwise
) to check whether the returned points are in clockwise order. This may not be necessary in every case, so I've split it out from the main function. I use it like this:
const ints = getEllipseSegmentIntersections(
px0,
py0,
px1,
py1,
cx,
cy,
rx,
ry,
angle
)
if (
!pointsAreClockwise(
ints[0],
ints[0],
ints[1],
ints[1],
cx,
cy
)
) {
ints.reverse()
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Adapted from Rod Stephen's original C# code here. Adds support for rotation.