@mathigon/euclid
Version:
Euclidean geometry classes and tools for JavaScript
158 lines (117 loc) • 5.18 kB
text/typescript
// =============================================================================
// Euclid.js | Intersection utilities
// (c) Mathigon
// =============================================================================
import {flatten} from '@mathigon/core';
import {isBetween, nearlyEquals, square, subsets} from '@mathigon/fermat';
import {Arc} from './arc';
import {Circle} from './circle';
import {Line, Ray, Segment} from './line';
import {Point} from './point';
import {isAngle, isArc, isCircle, isLineLike, isPolygonLike, isRay, isSegment} from './types';
import {GeoShape} from './utilities';
// -----------------------------------------------------------------------------
// Helper functions
function liesOnSegment(s: Segment, p: Point) {
if (nearlyEquals(s.p1.x, s.p2.x)) return isBetween(p.y, s.p1.y, s.p2.y);
return isBetween(p.x, s.p1.x, s.p2.x);
}
function liesOnRay(r: Ray, p: Point) {
if (nearlyEquals(r.p1.x, r.p2.x)) return (p.y - r.p1.y) / (r.p2.y - r.p1.y) > 0;
return (p.x - r.p1.x) / (r.p2.x - r.p1.x) > 0;
}
function liesOnArc(a: Arc, p: Point) {
return isBetween(a.offset(p), 0, 1);
}
// -----------------------------------------------------------------------------
// Foundations
function lineLineIntersection(l1: Line, l2: Line) {
const d1x = l1.p1.x - l1.p2.x;
const d1y = l1.p1.y - l1.p2.y;
const d2x = l2.p1.x - l2.p2.x;
const d2y = l2.p1.y - l2.p2.y;
const d = d1x * d2y - d1y * d2x;
if (nearlyEquals(d, 0)) return []; // Colinear lines never intersect
const q1 = l1.p1.x * l1.p2.y - l1.p1.y * l1.p2.x;
const q2 = l2.p1.x * l2.p2.y - l2.p1.y * l2.p2.x;
const x = q1 * d2x - d1x * q2;
const y = q1 * d2y - d1y * q2;
return [new Point(x / d, y / d)];
}
function circleCircleIntersection(c1: Circle, c2: Circle) {
const d = Point.distance(c1.c, c2.c);
// Circles are separate:
if (d > c1.r + c2.r) return [];
// One circles contains the other:
if (d < Math.abs(c1.r - c2.r)) return [];
// Circles are the same:
if (nearlyEquals(d, 0) && nearlyEquals(c1.r, c2.r)) return [];
// Circles touch:
if (nearlyEquals(d, c1.r + c2.r)) return [new Line(c1.c, c2.c).midpoint];
const a = (square(c1.r) - square(c2.r) + square(d)) / (2 * d);
const b = Math.sqrt(square(c1.r) - square(a));
const px = (c2.c.x - c1.c.x) * a / d + (c2.c.y - c1.c.y) * b / d + c1.c.x;
const py = (c2.c.y - c1.c.y) * a / d - (c2.c.x - c1.c.x) * b / d + c1.c.y;
const qx = (c2.c.x - c1.c.x) * a / d - (c2.c.y - c1.c.y) * b / d + c1.c.x;
const qy = (c2.c.y - c1.c.y) * a / d + (c2.c.x - c1.c.x) * b / d + c1.c.y;
return [new Point(px, py), new Point(qx, qy)];
}
// From http://mathworld.wolfram.com/Circle-LineIntersection.html
function lineCircleIntersection(l: Line, c: Circle) {
const dx = l.p2.x - l.p1.x;
const dy = l.p2.y - l.p1.y;
const dr2 = square(dx) + square(dy);
const cx = c.c.x;
const cy = c.c.y;
const D = (l.p1.x - cx) * (l.p2.y - cy) - (l.p2.x - cx) * (l.p1.y - cy);
const disc = square(c.r) * dr2 - square(D);
if (disc < 0) return []; // No solution
const xa = D * dy / dr2;
const ya = -D * dx / dr2;
if (nearlyEquals(disc, 0)) return [c.c.shift(xa, ya)]; // One solution
const xb = dx * (dy < 0 ? -1 : 1) * Math.sqrt(disc) / dr2;
const yb = Math.abs(dy) * Math.sqrt(disc) / dr2;
return [c.c.shift(xa + xb, ya + yb), c.c.shift(xa - xb, ya - yb)];
}
// -----------------------------------------------------------------------------
// Exported functions
function simpleIntersection(a: Line|Circle|Arc, b: Line|Circle|Arc): Point[] {
let results: Point[] = [];
const a1 = isArc(a) ? a.circle : a;
const b1 = isArc(b) ? b.circle : b;
if (isLineLike(a) && isLineLike(b)) {
results = lineLineIntersection(a, b);
} else if (isLineLike(a1) && isCircle(b1)) {
results = lineCircleIntersection(a1, b1);
} else if (isCircle(a1) && isLineLike(b1)) {
results = lineCircleIntersection(b1, a1);
} else if (isCircle(a1) && isCircle(b1)) {
results = circleCircleIntersection(a1, b1);
}
for (const x of [a, b]) {
if (isSegment(x)) results = results.filter(i => liesOnSegment(x, i));
if (isRay(x)) results = results.filter(i => liesOnRay(x, i));
if (isArc(x)) results = results.filter(i => liesOnArc(x, i));
}
return results;
}
/** Returns the intersection of two or more geometry objects. */
export function intersections(...elements: GeoShape[]): Point[] {
if (elements.length < 2) return [];
if (elements.length > 2) {
return flatten(subsets(elements, 2).map(e => intersections(...e)));
}
let [a, b] = elements;
if (isAngle(a)) a = a.shape(true);
if (isAngle(b)) b = b.shape(true);
if (isPolygonLike(b)) [a, b] = [b, a];
if (isPolygonLike(a)) {
// This hack is necessary to capture intersections between a line and a
// vertex of a polygon. There are more edge cases to consider!
const results = isLineLike(b) ? a.points.filter(p => (b as Line).contains(p)) : [];
for (const e of a.edges) results.push(...intersections(e, b));
return results;
}
// TODO Handle arcs, sectors and angles!
return simpleIntersection(a as (Line|Circle|Arc), b as (Line|Circle|Arc));
}