import { Bounds } from 'types' import vec from 'utils/vec' import { isAngleBetween } from './utils' interface Intersection { didIntersect: boolean message: string points: number[][] } function getIntersection(message: string, ...points: number[][]) { return { didIntersect: points.length > 0, message, points } } export function intersectLineSegments( a1: number[], a2: number[], b1: number[], b2: number[] ): Intersection { const AB = vec.sub(a1, b1) const BV = vec.sub(b2, b1) const AV = vec.sub(a2, a1) const ua_t = BV[0] * AB[1] - BV[1] * AB[0] const ub_t = AV[0] * AB[1] - AV[1] * AB[0] const u_b = BV[1] * AV[0] - BV[0] * AV[1] if (ua_t === 0 || ub_t === 0) { return getIntersection('coincident') } if (u_b === 0) { return getIntersection('parallel') } if (u_b != 0) { const ua = ua_t / u_b const ub = ub_t / u_b if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { return getIntersection('intersection', vec.add(a1, vec.mul(AV, ua))) } } return getIntersection('no intersection') } export function intersectCircleLineSegment( c: number[], r: number, a1: number[], a2: number[] ): Intersection { const a = (a2[0] - a1[0]) * (a2[0] - a1[0]) + (a2[1] - a1[1]) * (a2[1] - a1[1]) const b = 2 * ((a2[0] - a1[0]) * (a1[0] - c[0]) + (a2[1] - a1[1]) * (a1[1] - c[1])) const cc = c[0] * c[0] + c[1] * c[1] + a1[0] * a1[0] + a1[1] * a1[1] - 2 * (c[0] * a1[0] + c[1] * a1[1]) - r * r const deter = b * b - 4 * a * cc if (deter < 0) { return getIntersection('outside') } if (deter === 0) { return getIntersection('tangent') } const e = Math.sqrt(deter) const u1 = (-b + e) / (2 * a) const u2 = (-b - e) / (2 * a) if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) { if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) { return getIntersection('outside') } else { return getIntersection('inside') } } const results: number[][] = [] if (0 <= u1 && u1 <= 1) results.push(vec.lrp(a1, a2, u1)) if (0 <= u2 && u2 <= 1) results.push(vec.lrp(a1, a2, u2)) return getIntersection('intersection', ...results) } export function intersectEllipseLineSegment( center: number[], rx: number, ry: number, a1: number[], a2: number[], rotation = 0 ): Intersection { // If the ellipse or line segment are empty, return no tValues. if (rx === 0 || ry === 0 || vec.isEqual(a1, a2)) { return getIntersection('No intersection') } // Get the semimajor and semiminor axes. rx = rx < 0 ? rx : -rx ry = ry < 0 ? ry : -ry // Rotate points and translate so the ellipse is centered at the origin. a1 = vec.sub(vec.rotWith(a1, center, -rotation), center) a2 = vec.sub(vec.rotWith(a2, center, -rotation), center) // Calculate the quadratic parameters. const diff = vec.sub(a2, a1) const A = (diff[0] * diff[0]) / rx / rx + (diff[1] * diff[1]) / ry / ry const B = (2 * a1[0] * diff[0]) / rx / rx + (2 * a1[1] * diff[1]) / ry / ry const C = (a1[0] * a1[0]) / rx / rx + (a1[1] * a1[1]) / ry / ry - 1 // Make a list of t values (normalized points on the line where intersections occur). const tValues: number[] = [] // Calculate the discriminant. const discriminant = B * B - 4 * A * C if (discriminant === 0) { // One real solution. tValues.push(-B / 2 / A) } else if (discriminant > 0) { const root = Math.sqrt(discriminant) // Two real solutions. tValues.push((-B + root) / 2 / A) tValues.push((-B - root) / 2 / A) } // Filter to only points that are on the segment. // Solve for points, then counter-rotate points. const points = tValues .filter((t) => t >= 0 && t <= 1) .map((t) => vec.add(center, vec.add(a1, vec.mul(vec.sub(a2, a1), t)))) .map((p) => vec.rotWith(p, center, rotation)) return getIntersection('intersection', ...points) } export function intersectArcLineSegment( start: number[], end: number[], center: number[], radius: number, A: number[], B: number[] ): Intersection { const sa = vec.angle(center, start) const ea = vec.angle(center, end) const ellipseTest = intersectEllipseLineSegment(center, radius, radius, A, B) if (!ellipseTest.didIntersect) return getIntersection('No intersection') const points = ellipseTest.points.filter((point) => isAngleBetween(sa, ea, vec.angle(center, point)) ) if (points.length === 0) { return getIntersection('No intersection') } return getIntersection('intersection', ...points) } export function intersectCircleRectangle( c: number[], r: number, point: number[], size: number[] ): Intersection[] { const tl = point const tr = vec.add(point, [size[0], 0]) const br = vec.add(point, size) const bl = vec.add(point, [0, size[1]]) const intersections: Intersection[] = [] const topIntersection = intersectCircleLineSegment(c, r, tl, tr) const rightIntersection = intersectCircleLineSegment(c, r, tr, br) const bottomIntersection = intersectCircleLineSegment(c, r, bl, br) const leftIntersection = intersectCircleLineSegment(c, r, tl, bl) if (topIntersection.didIntersect) { intersections.push({ ...topIntersection, message: 'top' }) } if (rightIntersection.didIntersect) { intersections.push({ ...rightIntersection, message: 'right' }) } if (bottomIntersection.didIntersect) { intersections.push({ ...bottomIntersection, message: 'bottom' }) } if (leftIntersection.didIntersect) { intersections.push({ ...leftIntersection, message: 'left' }) } return intersections } export function intersectEllipseRectangle( c: number[], rx: number, ry: number, point: number[], size: number[], rotation = 0 ): Intersection[] { const tl = point const tr = vec.add(point, [size[0], 0]) const br = vec.add(point, size) const bl = vec.add(point, [0, size[1]]) const intersections: Intersection[] = [] const topIntersection = intersectEllipseLineSegment( c, rx, ry, tl, tr, rotation ) const rightIntersection = intersectEllipseLineSegment( c, rx, ry, tr, br, rotation ) const bottomIntersection = intersectEllipseLineSegment( c, rx, ry, bl, br, rotation ) const leftIntersection = intersectEllipseLineSegment( c, rx, ry, tl, bl, rotation ) if (topIntersection.didIntersect) { intersections.push({ ...topIntersection, message: 'top' }) } if (rightIntersection.didIntersect) { intersections.push({ ...rightIntersection, message: 'right' }) } if (bottomIntersection.didIntersect) { intersections.push({ ...bottomIntersection, message: 'bottom' }) } if (leftIntersection.didIntersect) { intersections.push({ ...leftIntersection, message: 'left' }) } return intersections } export function intersectRectangleLineSegment( point: number[], size: number[], a1: number[], a2: number[] ): Intersection[] { const tl = point const tr = vec.add(point, [size[0], 0]) const br = vec.add(point, size) const bl = vec.add(point, [0, size[1]]) const intersections: Intersection[] = [] const topIntersection = intersectLineSegments(a1, a2, tl, tr) const rightIntersection = intersectLineSegments(a1, a2, tr, br) const bottomIntersection = intersectLineSegments(a1, a2, bl, br) const leftIntersection = intersectLineSegments(a1, a2, tl, bl) if (topIntersection.didIntersect) { intersections.push({ ...topIntersection, message: 'top' }) } if (rightIntersection.didIntersect) { intersections.push({ ...rightIntersection, message: 'right' }) } if (bottomIntersection.didIntersect) { intersections.push({ ...bottomIntersection, message: 'bottom' }) } if (leftIntersection.didIntersect) { intersections.push({ ...leftIntersection, message: 'left' }) } return intersections } export function intersectArcRectangle( start: number[], end: number[], center: number[], radius: number, point: number[], size: number[] ): Intersection[] { const tl = point const tr = vec.add(point, [size[0], 0]) const br = vec.add(point, size) const bl = vec.add(point, [0, size[1]]) const intersections: Intersection[] = [] const topIntersection = intersectArcLineSegment( start, end, center, radius, tl, tr ) const rightIntersection = intersectArcLineSegment( start, end, center, radius, tr, br ) const bottomIntersection = intersectArcLineSegment( start, end, center, radius, bl, br ) const leftIntersection = intersectArcLineSegment( start, end, center, radius, tl, bl ) if (topIntersection.didIntersect) { intersections.push({ ...topIntersection, message: 'top' }) } if (rightIntersection.didIntersect) { intersections.push({ ...rightIntersection, message: 'right' }) } if (bottomIntersection.didIntersect) { intersections.push({ ...bottomIntersection, message: 'bottom' }) } if (leftIntersection.didIntersect) { intersections.push({ ...leftIntersection, message: 'left' }) } return intersections } /* -------------------------------------------------- */ /* Shape vs. Bounds */ /* -------------------------------------------------- */ export function intersectCircleBounds( c: number[], r: number, bounds: Bounds ): Intersection[] { const { minX, minY, width, height } = bounds return intersectCircleRectangle(c, r, [minX, minY], [width, height]) } export function intersectEllipseBounds( c: number[], rx: number, ry: number, bounds: Bounds, rotation = 0 ): Intersection[] { const { minX, minY, width, height } = bounds return intersectEllipseRectangle( c, rx, ry, [minX, minY], [width, height], rotation ) } export function intersectLineSegmentBounds( a1: number[], a2: number[], bounds: Bounds ): Intersection[] { const { minX, minY, width, height } = bounds return intersectRectangleLineSegment([minX, minY], [width, height], a1, a2) } export function intersectPolylineBounds( points: number[][], bounds: Bounds ): Intersection[] { const { minX, minY, width, height } = bounds const intersections: Intersection[] = [] for (let i = 1; i < points.length; i++) { intersections.push( ...intersectRectangleLineSegment( [minX, minY], [width, height], points[i - 1], points[i] ) ) } return intersections } export function intersectPolygonBounds( points: number[][], bounds: Bounds ): Intersection[] { const { minX, minY, width, height } = bounds const intersections: Intersection[] = [] for (let i = 1; i < points.length + 1; i++) { intersections.push( ...intersectRectangleLineSegment( [minX, minY], [width, height], points[i - 1], points[i % points.length] ) ) } return intersections } export function intersectArcBounds( start: number[], end: number[], center: number[], radius: number, bounds: Bounds ): Intersection[] { const { minX, minY, width, height } = bounds return intersectArcRectangle( start, end, center, radius, [minX, minY], [width, height] ) }