- Fix router import path in main.js - Handle Django REST Framework pagination format in API calls - Add getTemplates function to project API - Restart frontend development server
325 lines
9.4 KiB
TypeScript
325 lines
9.4 KiB
TypeScript
import { Point } from './Point';
|
|
import { createVector } from './util/misc/vectors';
|
|
|
|
/* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */
|
|
|
|
export type IntersectionType = 'Intersection' | 'Coincident' | 'Parallel';
|
|
|
|
export class Intersection {
|
|
declare points: Point[];
|
|
|
|
declare status?: IntersectionType;
|
|
|
|
constructor(status?: IntersectionType) {
|
|
this.status = status;
|
|
this.points = [];
|
|
}
|
|
|
|
/**
|
|
* Used to verify if a point is alredy in the collection
|
|
* @param {Point} point
|
|
* @returns {boolean}
|
|
*/
|
|
private includes(point: Point): boolean {
|
|
return this.points.some((p) => p.eq(point));
|
|
}
|
|
|
|
/**
|
|
* Appends points of intersection
|
|
* @param {...Point[]} points
|
|
* @return {Intersection} thisArg
|
|
*/
|
|
private append(...points: Point[]): Intersection {
|
|
this.points = this.points.concat(
|
|
points.filter((point) => {
|
|
return !this.includes(point);
|
|
}),
|
|
);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* check if point T is on the segment or line defined between A and B
|
|
*
|
|
* @param {Point} T the point we are checking for
|
|
* @param {Point} A one extremity of the segment
|
|
* @param {Point} B the other extremity of the segment
|
|
* @param [infinite] if true checks if `T` is on the line defined by `A` and `B`
|
|
* @returns true if `T` is contained
|
|
*/
|
|
static isPointContained(T: Point, A: Point, B: Point, infinite = false) {
|
|
if (A.eq(B)) {
|
|
// Edge case: the segment is a point, we check for coincidence,
|
|
// infinite param has no meaning because there are infinite lines to consider
|
|
return T.eq(A);
|
|
} else if (A.x === B.x) {
|
|
// Edge case: horizontal line.
|
|
// we first check if T.x has the same value, and then if T.y is contained between A.y and B.y
|
|
return (
|
|
T.x === A.x &&
|
|
(infinite || (T.y >= Math.min(A.y, B.y) && T.y <= Math.max(A.y, B.y)))
|
|
);
|
|
} else if (A.y === B.y) {
|
|
// Edge case: vertical line.
|
|
// we first check if T.y has the same value, and then if T.x is contained between A.x and B.x
|
|
return (
|
|
T.y === A.y &&
|
|
(infinite || (T.x >= Math.min(A.x, B.x) && T.x <= Math.max(A.x, B.x)))
|
|
);
|
|
} else {
|
|
// Generic case: sloped line.
|
|
// we check that AT has the same slope as AB
|
|
// for the segment case we need both the vectors to have the same direction and for AT to be lte AB in size
|
|
// for the infinite case we check the absolute value of the slope, since direction is meaningless
|
|
const AB = createVector(A, B);
|
|
const AT = createVector(A, T);
|
|
const s = AT.divide(AB);
|
|
return infinite
|
|
? Math.abs(s.x) === Math.abs(s.y)
|
|
: s.x === s.y && s.x >= 0 && s.x <= 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use the ray casting algorithm to determine if point is in the polygon defined by points
|
|
* @see https://en.wikipedia.org/wiki/Point_in_polygon
|
|
* @param point
|
|
* @param points polygon points
|
|
* @returns
|
|
*/
|
|
static isPointInPolygon(point: Point, points: Point[]) {
|
|
const other = new Point(point).setX(
|
|
Math.min(point.x - 1, ...points.map((p) => p.x)),
|
|
);
|
|
let hits = 0;
|
|
for (let index = 0; index < points.length; index++) {
|
|
const inter = this.intersectSegmentSegment(
|
|
// polygon side
|
|
points[index],
|
|
points[(index + 1) % points.length],
|
|
// ray
|
|
point,
|
|
other,
|
|
);
|
|
if (inter.includes(point)) {
|
|
// point is on the polygon side
|
|
return true;
|
|
}
|
|
hits += Number(inter.status === 'Intersection');
|
|
}
|
|
return hits % 2 === 1;
|
|
}
|
|
|
|
/**
|
|
* Checks if a line intersects another
|
|
* @see {@link https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection line intersection}
|
|
* @see {@link https://en.wikipedia.org/wiki/Cramer%27s_rule Cramer's rule}
|
|
* @param {Point} a1
|
|
* @param {Point} a2
|
|
* @param {Point} b1
|
|
* @param {Point} b2
|
|
* @param {boolean} [aInfinite=true] check segment intersection by passing `false`
|
|
* @param {boolean} [bInfinite=true] check segment intersection by passing `false`
|
|
* @return {Intersection}
|
|
*/
|
|
static intersectLineLine(
|
|
a1: Point,
|
|
a2: Point,
|
|
b1: Point,
|
|
b2: Point,
|
|
aInfinite = true,
|
|
bInfinite = true,
|
|
): Intersection {
|
|
const a2xa1x = a2.x - a1.x,
|
|
a2ya1y = a2.y - a1.y,
|
|
b2xb1x = b2.x - b1.x,
|
|
b2yb1y = b2.y - b1.y,
|
|
a1xb1x = a1.x - b1.x,
|
|
a1yb1y = a1.y - b1.y,
|
|
uaT = b2xb1x * a1yb1y - b2yb1y * a1xb1x,
|
|
ubT = a2xa1x * a1yb1y - a2ya1y * a1xb1x,
|
|
uB = b2yb1y * a2xa1x - b2xb1x * a2ya1y;
|
|
if (uB !== 0) {
|
|
const ua = uaT / uB,
|
|
ub = ubT / uB;
|
|
if (
|
|
(aInfinite || (0 <= ua && ua <= 1)) &&
|
|
(bInfinite || (0 <= ub && ub <= 1))
|
|
) {
|
|
return new Intersection('Intersection').append(
|
|
new Point(a1.x + ua * a2xa1x, a1.y + ua * a2ya1y),
|
|
);
|
|
} else {
|
|
return new Intersection();
|
|
}
|
|
} else {
|
|
if (uaT === 0 || ubT === 0) {
|
|
const segmentsCoincide =
|
|
aInfinite ||
|
|
bInfinite ||
|
|
Intersection.isPointContained(a1, b1, b2) ||
|
|
Intersection.isPointContained(a2, b1, b2) ||
|
|
Intersection.isPointContained(b1, a1, a2) ||
|
|
Intersection.isPointContained(b2, a1, a2);
|
|
return new Intersection(segmentsCoincide ? 'Coincident' : undefined);
|
|
} else {
|
|
return new Intersection('Parallel');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a segment intersects a line
|
|
* @see {@link intersectLineLine} for line intersection
|
|
* @param {Point} s1 boundary point of segment
|
|
* @param {Point} s2 other boundary point of segment
|
|
* @param {Point} l1 point on line
|
|
* @param {Point} l2 other point on line
|
|
* @return {Intersection}
|
|
*/
|
|
static intersectSegmentLine(
|
|
s1: Point,
|
|
s2: Point,
|
|
l1: Point,
|
|
l2: Point,
|
|
): Intersection {
|
|
return Intersection.intersectLineLine(s1, s2, l1, l2, false, true);
|
|
}
|
|
|
|
/**
|
|
* Checks if a segment intersects another
|
|
* @see {@link intersectLineLine} for line intersection
|
|
* @param {Point} a1 boundary point of segment
|
|
* @param {Point} a2 other boundary point of segment
|
|
* @param {Point} b1 boundary point of segment
|
|
* @param {Point} b2 other boundary point of segment
|
|
* @return {Intersection}
|
|
*/
|
|
static intersectSegmentSegment(
|
|
a1: Point,
|
|
a2: Point,
|
|
b1: Point,
|
|
b2: Point,
|
|
): Intersection {
|
|
return Intersection.intersectLineLine(a1, a2, b1, b2, false, false);
|
|
}
|
|
|
|
/**
|
|
* Checks if line intersects polygon
|
|
*
|
|
* @todo account for stroke
|
|
*
|
|
* @see {@link intersectSegmentPolygon} for segment intersection
|
|
* @param {Point} a1 point on line
|
|
* @param {Point} a2 other point on line
|
|
* @param {Point[]} points polygon points
|
|
* @param {boolean} [infinite=true] check segment intersection by passing `false`
|
|
* @return {Intersection}
|
|
*/
|
|
static intersectLinePolygon(
|
|
a1: Point,
|
|
a2: Point,
|
|
points: Point[],
|
|
infinite = true,
|
|
): Intersection {
|
|
const result = new Intersection();
|
|
const length = points.length;
|
|
|
|
for (let i = 0, b1, b2, inter; i < length; i++) {
|
|
b1 = points[i];
|
|
b2 = points[(i + 1) % length];
|
|
inter = Intersection.intersectLineLine(a1, a2, b1, b2, infinite, false);
|
|
if (inter.status === 'Coincident') {
|
|
return inter;
|
|
}
|
|
result.append(...inter.points);
|
|
}
|
|
|
|
if (result.points.length > 0) {
|
|
result.status = 'Intersection';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Checks if segment intersects polygon
|
|
* @see {@link intersectLinePolygon} for line intersection
|
|
* @param {Point} a1 boundary point of segment
|
|
* @param {Point} a2 other boundary point of segment
|
|
* @param {Point[]} points polygon points
|
|
* @return {Intersection}
|
|
*/
|
|
static intersectSegmentPolygon(
|
|
a1: Point,
|
|
a2: Point,
|
|
points: Point[],
|
|
): Intersection {
|
|
return Intersection.intersectLinePolygon(a1, a2, points, false);
|
|
}
|
|
|
|
/**
|
|
* Checks if polygon intersects another polygon
|
|
*
|
|
* @todo account for stroke
|
|
*
|
|
* @param {Point[]} points1
|
|
* @param {Point[]} points2
|
|
* @return {Intersection}
|
|
*/
|
|
static intersectPolygonPolygon(
|
|
points1: Point[],
|
|
points2: Point[],
|
|
): Intersection {
|
|
const result = new Intersection(),
|
|
length = points1.length;
|
|
const coincidences: Intersection[] = [];
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
const a1 = points1[i],
|
|
a2 = points1[(i + 1) % length],
|
|
inter = Intersection.intersectSegmentPolygon(a1, a2, points2);
|
|
if (inter.status === 'Coincident') {
|
|
coincidences.push(inter);
|
|
result.append(a1, a2);
|
|
} else {
|
|
result.append(...inter.points);
|
|
}
|
|
}
|
|
|
|
if (coincidences.length > 0 && coincidences.length === points1.length) {
|
|
return new Intersection('Coincident');
|
|
} else if (result.points.length > 0) {
|
|
result.status = 'Intersection';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Checks if polygon intersects rectangle
|
|
* @see {@link intersectPolygonPolygon} for polygon intersection
|
|
* @param {Point[]} points polygon points
|
|
* @param {Point} r1 top left point of rect
|
|
* @param {Point} r2 bottom right point of rect
|
|
* @return {Intersection}
|
|
*/
|
|
static intersectPolygonRectangle(
|
|
points: Point[],
|
|
r1: Point,
|
|
r2: Point,
|
|
): Intersection {
|
|
const min = r1.min(r2),
|
|
max = r1.max(r2),
|
|
topRight = new Point(max.x, min.y),
|
|
bottomLeft = new Point(min.x, max.y);
|
|
|
|
return Intersection.intersectPolygonPolygon(points, [
|
|
min,
|
|
topRight,
|
|
max,
|
|
bottomLeft,
|
|
]);
|
|
}
|
|
}
|