chore: Unify math types, utils and functions (#8389)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>pull/8282/merge
parent
e3d1dee9d0
commit
f4dd23fc31
@ -1,99 +0,0 @@
|
||||
import {
|
||||
isPointOnSymmetricArc,
|
||||
rangeIntersection,
|
||||
rangesOverlap,
|
||||
rotate,
|
||||
} from "./math";
|
||||
|
||||
describe("rotate", () => {
|
||||
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
|
||||
const x1 = 10;
|
||||
const y1 = 20;
|
||||
const x2 = 20;
|
||||
const y2 = 30;
|
||||
const angle = Math.PI / 2;
|
||||
const [rotatedX, rotatedY] = rotate(x1, y1, x2, y2, angle);
|
||||
expect([rotatedX, rotatedY]).toEqual([30, 20]);
|
||||
const res2 = rotate(rotatedX, rotatedY, x2, y2, -angle);
|
||||
expect(res2).toEqual([x1, x2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("range overlap", () => {
|
||||
it("should overlap when range a contains range b", () => {
|
||||
expect(rangesOverlap([1, 4], [2, 3])).toBe(true);
|
||||
expect(rangesOverlap([1, 4], [1, 4])).toBe(true);
|
||||
expect(rangesOverlap([1, 4], [1, 3])).toBe(true);
|
||||
expect(rangesOverlap([1, 4], [2, 4])).toBe(true);
|
||||
});
|
||||
|
||||
it("should overlap when range b contains range a", () => {
|
||||
expect(rangesOverlap([2, 3], [1, 4])).toBe(true);
|
||||
expect(rangesOverlap([1, 3], [1, 4])).toBe(true);
|
||||
expect(rangesOverlap([2, 4], [1, 4])).toBe(true);
|
||||
});
|
||||
|
||||
it("should overlap when range a and b intersect", () => {
|
||||
expect(rangesOverlap([1, 4], [2, 5])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("range intersection", () => {
|
||||
it("should intersect completely with itself", () => {
|
||||
expect(rangeIntersection([1, 4], [1, 4])).toEqual([1, 4]);
|
||||
});
|
||||
|
||||
it("should intersect irrespective of order", () => {
|
||||
expect(rangeIntersection([1, 4], [2, 3])).toEqual([2, 3]);
|
||||
expect(rangeIntersection([2, 3], [1, 4])).toEqual([2, 3]);
|
||||
expect(rangeIntersection([1, 4], [3, 5])).toEqual([3, 4]);
|
||||
expect(rangeIntersection([3, 5], [1, 4])).toEqual([3, 4]);
|
||||
});
|
||||
|
||||
it("should intersect at the edge", () => {
|
||||
expect(rangeIntersection([1, 4], [4, 5])).toEqual([4, 4]);
|
||||
});
|
||||
|
||||
it("should not intersect", () => {
|
||||
expect(rangeIntersection([1, 4], [5, 7])).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point on arc", () => {
|
||||
it("should detect point on simple arc", () => {
|
||||
expect(
|
||||
isPointOnSymmetricArc(
|
||||
{
|
||||
radius: 1,
|
||||
startAngle: -Math.PI / 4,
|
||||
endAngle: Math.PI / 4,
|
||||
},
|
||||
[0.92291667, 0.385],
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
it("should not detect point outside of a simple arc", () => {
|
||||
expect(
|
||||
isPointOnSymmetricArc(
|
||||
{
|
||||
radius: 1,
|
||||
startAngle: -Math.PI / 4,
|
||||
endAngle: Math.PI / 4,
|
||||
},
|
||||
[-0.92291667, 0.385],
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
it("should not detect point with good angle but incorrect radius", () => {
|
||||
expect(
|
||||
isPointOnSymmetricArc(
|
||||
{
|
||||
radius: 1,
|
||||
startAngle: -Math.PI / 4,
|
||||
endAngle: Math.PI / 4,
|
||||
},
|
||||
[-0.5, 0.5],
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
@ -0,0 +1,21 @@
|
||||
# @excalidraw/math
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @excalidraw/math
|
||||
```
|
||||
|
||||
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
|
||||
|
||||
```bash
|
||||
yarn add @excalidraw/math
|
||||
```
|
||||
|
||||
With PNPM, similarly install the package with this command:
|
||||
|
||||
```bash
|
||||
pnpm add @excalidraw/math
|
||||
```
|
||||
|
||||
## API
|
@ -0,0 +1,47 @@
|
||||
import type {
|
||||
Degrees,
|
||||
GlobalPoint,
|
||||
LocalPoint,
|
||||
PolarCoords,
|
||||
Radians,
|
||||
} from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
|
||||
// TODO: Simplify with modulo and fix for angles beyond 4*Math.PI and - 4*Math.PI
|
||||
export const normalizeRadians = (angle: Radians): Radians => {
|
||||
if (angle < 0) {
|
||||
return (angle + 2 * Math.PI) as Radians;
|
||||
}
|
||||
if (angle >= 2 * Math.PI) {
|
||||
return (angle - 2 * Math.PI) as Radians;
|
||||
}
|
||||
return angle;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the polar coordinates for the given cartesian point represented by
|
||||
* (x, y) for the center point 0,0 where the first number returned is the radius,
|
||||
* the second is the angle in radians.
|
||||
*/
|
||||
export const cartesian2Polar = <P extends GlobalPoint | LocalPoint>([
|
||||
x,
|
||||
y,
|
||||
]: P): PolarCoords => [Math.hypot(x, y), Math.atan2(y, x)];
|
||||
|
||||
export function degreesToRadians(degrees: Degrees): Radians {
|
||||
return ((degrees * Math.PI) / 180) as Radians;
|
||||
}
|
||||
|
||||
export function radiansToDegrees(degrees: Radians): Degrees {
|
||||
return ((degrees * 180) / Math.PI) as Degrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the provided angle is a right angle.
|
||||
*
|
||||
* @param rads The angle to measure
|
||||
* @returns TRUE if the provided angle is a right angle
|
||||
*/
|
||||
export function isRightAngleRads(rads: Radians): boolean {
|
||||
return Math.abs(Math.sin(2 * rads)) < PRECISION;
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { isPointOnSymmetricArc } from "./arc";
|
||||
import { point } from "./point";
|
||||
|
||||
describe("point on arc", () => {
|
||||
it("should detect point on simple arc", () => {
|
||||
expect(
|
||||
isPointOnSymmetricArc(
|
||||
{
|
||||
radius: 1,
|
||||
startAngle: -Math.PI / 4,
|
||||
endAngle: Math.PI / 4,
|
||||
},
|
||||
point(0.92291667, 0.385),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
it("should not detect point outside of a simple arc", () => {
|
||||
expect(
|
||||
isPointOnSymmetricArc(
|
||||
{
|
||||
radius: 1,
|
||||
startAngle: -Math.PI / 4,
|
||||
endAngle: Math.PI / 4,
|
||||
},
|
||||
point(-0.92291667, 0.385),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
it("should not detect point with good angle but incorrect radius", () => {
|
||||
expect(
|
||||
isPointOnSymmetricArc(
|
||||
{
|
||||
radius: 1,
|
||||
startAngle: -Math.PI / 4,
|
||||
endAngle: Math.PI / 4,
|
||||
},
|
||||
point(-0.5, 0.5),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
import { cartesian2Polar } from "./angle";
|
||||
import type { GlobalPoint, LocalPoint, SymmetricArc } from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
|
||||
/**
|
||||
* Determines if a cartesian point lies on a symmetric arc, i.e. an arc which
|
||||
* is part of a circle contour centered on 0, 0.
|
||||
*/
|
||||
export const isPointOnSymmetricArc = <P extends GlobalPoint | LocalPoint>(
|
||||
{ radius: arcRadius, startAngle, endAngle }: SymmetricArc,
|
||||
point: P,
|
||||
): boolean => {
|
||||
const [radius, angle] = cartesian2Polar(point);
|
||||
|
||||
return startAngle < endAngle
|
||||
? Math.abs(radius - arcRadius) < PRECISION &&
|
||||
startAngle <= angle &&
|
||||
endAngle >= angle
|
||||
: startAngle <= angle || endAngle >= angle;
|
||||
};
|
@ -0,0 +1,223 @@
|
||||
import { point, pointRotateRads } from "./point";
|
||||
import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param a
|
||||
* @param b
|
||||
* @param c
|
||||
* @param d
|
||||
* @returns
|
||||
*/
|
||||
export function curve<Point extends GlobalPoint | LocalPoint>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
c: Point,
|
||||
d: Point,
|
||||
) {
|
||||
return [a, b, c, d] as Curve<Point>;
|
||||
}
|
||||
|
||||
export const curveRotate = <Point extends LocalPoint | GlobalPoint>(
|
||||
curve: Curve<Point>,
|
||||
angle: Radians,
|
||||
origin: Point,
|
||||
) => {
|
||||
return curve.map((p) => pointRotateRads(p, origin, angle));
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pointsIn
|
||||
* @param curveTightness
|
||||
* @returns
|
||||
*/
|
||||
export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
|
||||
pointsIn: readonly Point[],
|
||||
curveTightness = 0,
|
||||
): Point[] {
|
||||
const len = pointsIn.length;
|
||||
if (len < 3) {
|
||||
throw new Error("A curve must have at least three points.");
|
||||
}
|
||||
const out: Point[] = [];
|
||||
if (len === 3) {
|
||||
out.push(
|
||||
point(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
|
||||
point(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
|
||||
point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
||||
point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
||||
);
|
||||
} else {
|
||||
const points: Point[] = [];
|
||||
points.push(pointsIn[0], pointsIn[0]);
|
||||
for (let i = 1; i < pointsIn.length; i++) {
|
||||
points.push(pointsIn[i]);
|
||||
if (i === pointsIn.length - 1) {
|
||||
points.push(pointsIn[i]);
|
||||
}
|
||||
}
|
||||
const b: Point[] = [];
|
||||
const s = 1 - curveTightness;
|
||||
out.push(point(points[0][0], points[0][1]));
|
||||
for (let i = 1; i + 2 < points.length; i++) {
|
||||
const cachedVertArray = points[i];
|
||||
b[0] = point(cachedVertArray[0], cachedVertArray[1]);
|
||||
b[1] = point(
|
||||
cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
|
||||
cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
|
||||
);
|
||||
b[2] = point(
|
||||
points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
|
||||
points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
|
||||
);
|
||||
b[3] = point(points[i + 1][0], points[i + 1][1]);
|
||||
out.push(b[1], b[2], b[3]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param t
|
||||
* @param controlPoints
|
||||
* @returns
|
||||
*/
|
||||
export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||
t: number,
|
||||
controlPoints: Curve<Point>,
|
||||
): Point => {
|
||||
const [p0, p1, p2, p3] = controlPoints;
|
||||
|
||||
const x =
|
||||
Math.pow(1 - t, 3) * p0[0] +
|
||||
3 * Math.pow(1 - t, 2) * t * p1[0] +
|
||||
3 * (1 - t) * Math.pow(t, 2) * p2[0] +
|
||||
Math.pow(t, 3) * p3[0];
|
||||
|
||||
const y =
|
||||
Math.pow(1 - t, 3) * p0[1] +
|
||||
3 * Math.pow(1 - t, 2) * t * p1[1] +
|
||||
3 * (1 - t) * Math.pow(t, 2) * p2[1] +
|
||||
Math.pow(t, 3) * p3[1];
|
||||
|
||||
return point(x, y);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param point
|
||||
* @param controlPoints
|
||||
* @returns
|
||||
*/
|
||||
export const cubicBezierDistance = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
controlPoints: Curve<Point>,
|
||||
) => {
|
||||
// Calculate the closest point on the Bezier curve to the given point
|
||||
const t = findClosestParameter(point, controlPoints);
|
||||
|
||||
// Calculate the coordinates of the closest point on the curve
|
||||
const [closestX, closestY] = cubicBezierPoint(t, controlPoints);
|
||||
|
||||
// Calculate the distance between the given point and the closest point on the curve
|
||||
const distance = Math.sqrt(
|
||||
(point[0] - closestX) ** 2 + (point[1] - closestY) ** 2,
|
||||
);
|
||||
|
||||
return distance;
|
||||
};
|
||||
|
||||
const solveCubic = (a: number, b: number, c: number, d: number) => {
|
||||
// This function solves the cubic equation ax^3 + bx^2 + cx + d = 0
|
||||
const roots: number[] = [];
|
||||
|
||||
const discriminant =
|
||||
18 * a * b * c * d -
|
||||
4 * Math.pow(b, 3) * d +
|
||||
Math.pow(b, 2) * Math.pow(c, 2) -
|
||||
4 * a * Math.pow(c, 3) -
|
||||
27 * Math.pow(a, 2) * Math.pow(d, 2);
|
||||
|
||||
if (discriminant >= 0) {
|
||||
const C = Math.cbrt((discriminant + Math.sqrt(discriminant)) / 2);
|
||||
const D = Math.cbrt((discriminant - Math.sqrt(discriminant)) / 2);
|
||||
|
||||
const root1 = (-b - C - D) / (3 * a);
|
||||
const root2 = (-b + (C + D) / 2) / (3 * a);
|
||||
const root3 = (-b + (C + D) / 2) / (3 * a);
|
||||
|
||||
roots.push(root1, root2, root3);
|
||||
} else {
|
||||
const realPart = -b / (3 * a);
|
||||
|
||||
const root1 =
|
||||
2 * Math.sqrt(-b / (3 * a)) * Math.cos(Math.acos(realPart) / 3);
|
||||
const root2 =
|
||||
2 *
|
||||
Math.sqrt(-b / (3 * a)) *
|
||||
Math.cos((Math.acos(realPart) + 2 * Math.PI) / 3);
|
||||
const root3 =
|
||||
2 *
|
||||
Math.sqrt(-b / (3 * a)) *
|
||||
Math.cos((Math.acos(realPart) + 4 * Math.PI) / 3);
|
||||
|
||||
roots.push(root1, root2, root3);
|
||||
}
|
||||
|
||||
return roots;
|
||||
};
|
||||
|
||||
const findClosestParameter = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
controlPoints: Curve<Point>,
|
||||
) => {
|
||||
// This function finds the parameter t that minimizes the distance between the point
|
||||
// and any point on the cubic Bezier curve.
|
||||
|
||||
const [p0, p1, p2, p3] = controlPoints;
|
||||
|
||||
// Use the direct formula to find the parameter t
|
||||
const a = p3[0] - 3 * p2[0] + 3 * p1[0] - p0[0];
|
||||
const b = 3 * p2[0] - 6 * p1[0] + 3 * p0[0];
|
||||
const c = 3 * p1[0] - 3 * p0[0];
|
||||
const d = p0[0] - point[0];
|
||||
|
||||
const rootsX = solveCubic(a, b, c, d);
|
||||
|
||||
// Do the same for the y-coordinate
|
||||
const e = p3[1] - 3 * p2[1] + 3 * p1[1] - p0[1];
|
||||
const f = 3 * p2[1] - 6 * p1[1] + 3 * p0[1];
|
||||
const g = 3 * p1[1] - 3 * p0[1];
|
||||
const h = p0[1] - point[1];
|
||||
|
||||
const rootsY = solveCubic(e, f, g, h);
|
||||
|
||||
// Select the real root that is between 0 and 1 (inclusive)
|
||||
const validRootsX = rootsX.filter((root) => root >= 0 && root <= 1);
|
||||
const validRootsY = rootsY.filter((root) => root >= 0 && root <= 1);
|
||||
|
||||
if (validRootsX.length === 0 || validRootsY.length === 0) {
|
||||
// No valid roots found, use the midpoint as a fallback
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
// Choose the parameter t that minimizes the distance
|
||||
let minDistance = Infinity;
|
||||
let closestT = 0;
|
||||
|
||||
for (const rootX of validRootsX) {
|
||||
for (const rootY of validRootsY) {
|
||||
const distance = Math.sqrt(
|
||||
(rootX - point[0]) ** 2 + (rootY - point[1]) ** 2,
|
||||
);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestT = (rootX + rootY) / 2; // Use the average for a smoother result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return closestT;
|
||||
};
|
@ -1,8 +1,8 @@
|
||||
import * as GA from "../ga";
|
||||
import { point, toString, direction, offset } from "../ga";
|
||||
import * as GAPoint from "../gapoints";
|
||||
import * as GALine from "../galines";
|
||||
import * as GATransform from "../gatransforms";
|
||||
import * as GA from "./ga";
|
||||
import { point, toString, direction, offset } from "./ga";
|
||||
import * as GAPoint from "./gapoints";
|
||||
import * as GALine from "./galines";
|
||||
import * as GATransform from "./gatransforms";
|
||||
|
||||
describe("geometric algebra", () => {
|
||||
describe("points", () => {
|
@ -0,0 +1,12 @@
|
||||
export * from "./arc";
|
||||
export * from "./angle";
|
||||
export * from "./curve";
|
||||
export * from "./line";
|
||||
export * from "./point";
|
||||
export * from "./polygon";
|
||||
export * from "./range";
|
||||
export * from "./segment";
|
||||
export * from "./triangle";
|
||||
export * from "./types";
|
||||
export * from "./vector";
|
||||
export * from "./utils";
|
@ -0,0 +1,52 @@
|
||||
import { pointCenter, pointRotateRads } from "./point";
|
||||
import type { GlobalPoint, Line, LocalPoint, Radians } from "./types";
|
||||
|
||||
/**
|
||||
* Create a line from two points.
|
||||
*
|
||||
* @param points The two points lying on the line
|
||||
* @returns The line on which the points lie
|
||||
*/
|
||||
export function line<P extends GlobalPoint | LocalPoint>(a: P, b: P): Line<P> {
|
||||
return [a, b] as Line<P>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient point creation from an array of two points.
|
||||
*
|
||||
* @param param0 The array with the two points to convert to a line
|
||||
* @returns The created line
|
||||
*/
|
||||
export function lineFromPointPair<P extends GlobalPoint | LocalPoint>([a, b]: [
|
||||
P,
|
||||
P,
|
||||
]): Line<P> {
|
||||
return line(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param pointArray
|
||||
* @returns
|
||||
*/
|
||||
export function lineFromPointArray<P extends GlobalPoint | LocalPoint>(
|
||||
pointArray: P[],
|
||||
): Line<P> | undefined {
|
||||
return pointArray.length === 2
|
||||
? line<P>(pointArray[0], pointArray[1])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// return the coordinates resulting from rotating the given line about an origin by an angle in degrees
|
||||
// note that when the origin is not given, the midpoint of the given line is used as the origin
|
||||
export const lineRotate = <Point extends LocalPoint | GlobalPoint>(
|
||||
l: Line<Point>,
|
||||
angle: Radians,
|
||||
origin?: Point,
|
||||
): Line<Point> => {
|
||||
return line(
|
||||
pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle),
|
||||
pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle),
|
||||
);
|
||||
};
|
@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@excalidraw/math",
|
||||
"version": "0.1.0",
|
||||
"main": "./dist/prod/index.js",
|
||||
"type": "module",
|
||||
"module": "./dist/prod/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./dist/dev/index.js",
|
||||
"default": "./dist/prod/index.js"
|
||||
}
|
||||
},
|
||||
"types": "./dist/utils/index.d.ts",
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"description": "Excalidraw math functions",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"excalidraw",
|
||||
"excalidraw-math",
|
||||
"math",
|
||||
"vector",
|
||||
"algebra",
|
||||
"2d"
|
||||
],
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all",
|
||||
"not safari < 12",
|
||||
"not kaios <= 2.5",
|
||||
"not edge < 79",
|
||||
"not chrome < 70",
|
||||
"not and_uc < 13",
|
||||
"not samsung < 10"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
"dependencies": {
|
||||
"@excalidraw/utils": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"gen:types": "rm -rf types && tsc",
|
||||
"build:umd": "cross-env NODE_ENV=production webpack --config webpack.prod.config.js",
|
||||
"build:esm": "rm -rf dist && node ../../scripts/buildUtils.js && yarn gen:types",
|
||||
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
|
||||
"pack": "yarn build:umd && yarn pack"
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { point, pointRotateRads } from "./point";
|
||||
import type { Radians } from "./types";
|
||||
|
||||
describe("rotate", () => {
|
||||
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
|
||||
const x1 = 10;
|
||||
const y1 = 20;
|
||||
const x2 = 20;
|
||||
const y2 = 30;
|
||||
const angle = (Math.PI / 2) as Radians;
|
||||
const [rotatedX, rotatedY] = pointRotateRads(
|
||||
point(x1, y1),
|
||||
point(x2, y2),
|
||||
angle,
|
||||
);
|
||||
expect([rotatedX, rotatedY]).toEqual([30, 20]);
|
||||
const res2 = pointRotateRads(
|
||||
point(rotatedX, rotatedY),
|
||||
point(x2, y2),
|
||||
-angle as Radians,
|
||||
);
|
||||
expect(res2).toEqual([x1, x2]);
|
||||
});
|
||||
});
|
@ -0,0 +1,257 @@
|
||||
import { degreesToRadians } from "./angle";
|
||||
import type {
|
||||
LocalPoint,
|
||||
GlobalPoint,
|
||||
Radians,
|
||||
Degrees,
|
||||
Vector,
|
||||
} from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
import { vectorFromPoint, vectorScale } from "./vector";
|
||||
|
||||
/**
|
||||
* Create a properly typed Point instance from the X and Y coordinates.
|
||||
*
|
||||
* @param x The X coordinate
|
||||
* @param y The Y coordinate
|
||||
* @returns The branded and created point
|
||||
*/
|
||||
export function point<Point extends GlobalPoint | LocalPoint>(
|
||||
x: number,
|
||||
y: number,
|
||||
): Point {
|
||||
return [x, y] as Point;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts and remaps an array containing a pair of numbers to Point.
|
||||
*
|
||||
* @param numberArray The number array to check and to convert to Point
|
||||
* @returns The point instance
|
||||
*/
|
||||
export function pointFromArray<Point extends GlobalPoint | LocalPoint>(
|
||||
numberArray: number[],
|
||||
): Point | undefined {
|
||||
return numberArray.length === 2
|
||||
? point<Point>(numberArray[0], numberArray[1])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts and remaps a pair of numbers to Point.
|
||||
*
|
||||
* @param pair A number pair to convert to Point
|
||||
* @returns The point instance
|
||||
*/
|
||||
export function pointFromPair<Point extends GlobalPoint | LocalPoint>(
|
||||
pair: [number, number],
|
||||
): Point {
|
||||
return pair as Point;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a vector to a point.
|
||||
*
|
||||
* @param v The vector to convert
|
||||
* @returns The point the vector points at with origin 0,0
|
||||
*/
|
||||
export function pointFromVector<P extends GlobalPoint | LocalPoint>(
|
||||
v: Vector,
|
||||
): P {
|
||||
return v as unknown as P;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided value has the shape of a Point.
|
||||
*
|
||||
* @param p The value to attempt verification on
|
||||
* @returns TRUE if the provided value has the shape of a local or global point
|
||||
*/
|
||||
export function isPoint(p: unknown): p is LocalPoint | GlobalPoint {
|
||||
return (
|
||||
Array.isArray(p) &&
|
||||
p.length === 2 &&
|
||||
typeof p[0] === "number" &&
|
||||
!isNaN(p[0]) &&
|
||||
typeof p[1] === "number" &&
|
||||
!isNaN(p[1])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two points coordinate-by-coordinate and if
|
||||
* they are closer than INVERSE_PRECISION it returns TRUE.
|
||||
*
|
||||
* @param a Point The first point to compare
|
||||
* @param b Point The second point to compare
|
||||
* @returns TRUE if the points are sufficiently close to each other
|
||||
*/
|
||||
export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
): boolean {
|
||||
const abs = Math.abs;
|
||||
return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Roate a point by [angle] radians.
|
||||
*
|
||||
* @param point The point to rotate
|
||||
* @param center The point to rotate around, the center point
|
||||
* @param angle The radians to rotate the point by
|
||||
* @returns The rotated point
|
||||
*/
|
||||
export function pointRotateRads<Point extends GlobalPoint | LocalPoint>(
|
||||
[x, y]: Point,
|
||||
[cx, cy]: Point,
|
||||
angle: Radians,
|
||||
): Point {
|
||||
return point(
|
||||
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
||||
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Roate a point by [angle] degree.
|
||||
*
|
||||
* @param point The point to rotate
|
||||
* @param center The point to rotate around, the center point
|
||||
* @param angle The degree to rotate the point by
|
||||
* @returns The rotated point
|
||||
*/
|
||||
export function pointRotateDegs<Point extends GlobalPoint | LocalPoint>(
|
||||
point: Point,
|
||||
center: Point,
|
||||
angle: Degrees,
|
||||
): Point {
|
||||
return pointRotateRads(point, center, degreesToRadians(angle));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a point by a vector.
|
||||
*
|
||||
* WARNING: This is not for translating Excalidraw element points!
|
||||
* You need to account for rotation on base coordinates
|
||||
* on your own.
|
||||
* CONSIDER USING AN APPROPRIATE ELEMENT-AWARE TRANSLATE!
|
||||
*
|
||||
* @param p The point to apply the translation on
|
||||
* @param v The vector to translate by
|
||||
* @returns
|
||||
*/
|
||||
// TODO 99% of use is translating between global and local coords, which need to be formalized
|
||||
export function pointTranslate<
|
||||
From extends GlobalPoint | LocalPoint,
|
||||
To extends GlobalPoint | LocalPoint,
|
||||
>(p: From, v: Vector = [0, 0] as Vector): To {
|
||||
return point(p[0] + v[0], p[1] + v[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the center point at equal distance from both points.
|
||||
*
|
||||
* @param a One of the points to create the middle point for
|
||||
* @param b The other point to create the middle point for
|
||||
* @returns The middle point
|
||||
*/
|
||||
export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P {
|
||||
return point((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add together two points by their coordinates like you'd apply a translation
|
||||
* to a point by a vector.
|
||||
*
|
||||
* @param a One point to act as a basis
|
||||
* @param b The other point to act like the vector to translate by
|
||||
* @returns
|
||||
*/
|
||||
export function pointAdd<Point extends LocalPoint | GlobalPoint>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
): Point {
|
||||
return point(a[0] + b[0], a[1] + b[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract a point from another point like you'd translate a point by an
|
||||
* invese vector.
|
||||
*
|
||||
* @param a The point to translate
|
||||
* @param b The point which will act like a vector
|
||||
* @returns The resulting point
|
||||
*/
|
||||
export function pointSubtract<Point extends LocalPoint | GlobalPoint>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
): Point {
|
||||
return point(a[0] - b[0], a[1] - b[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the distance between two points.
|
||||
*
|
||||
* @param a First point
|
||||
* @param b Second point
|
||||
* @returns The euclidean distance between the two points.
|
||||
*/
|
||||
export function pointDistance<P extends LocalPoint | GlobalPoint>(
|
||||
a: P,
|
||||
b: P,
|
||||
): number {
|
||||
return Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the squared distance between two points.
|
||||
*
|
||||
* Note: Use this if you only compare distances, it saves a square root.
|
||||
*
|
||||
* @param a First point
|
||||
* @param b Second point
|
||||
* @returns The euclidean distance between the two points.
|
||||
*/
|
||||
export function pointDistanceSq<P extends LocalPoint | GlobalPoint>(
|
||||
a: P,
|
||||
b: P,
|
||||
): number {
|
||||
return Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale a point from a given origin by the multiplier.
|
||||
*
|
||||
* @param p The point to scale
|
||||
* @param mid The origin to scale from
|
||||
* @param multiplier The scaling factor
|
||||
* @returns
|
||||
*/
|
||||
export const pointScaleFromOrigin = <P extends GlobalPoint | LocalPoint>(
|
||||
p: P,
|
||||
mid: P,
|
||||
multiplier: number,
|
||||
) => pointTranslate(mid, vectorScale(vectorFromPoint(p, mid), multiplier));
|
||||
|
||||
/**
|
||||
* Returns whether `q` lies inside the segment/rectangle defined by `p` and `r`.
|
||||
* This is an approximation to "does `q` lie on a segment `pr`" check.
|
||||
*
|
||||
* @param p The first point to compare against
|
||||
* @param q The actual point this function checks whether is in between
|
||||
* @param r The other point to compare against
|
||||
* @returns TRUE if q is indeed between p and r
|
||||
*/
|
||||
export const isPointWithinBounds = <P extends GlobalPoint | LocalPoint>(
|
||||
p: P,
|
||||
q: P,
|
||||
r: P,
|
||||
) => {
|
||||
return (
|
||||
q[0] <= Math.max(p[0], r[0]) &&
|
||||
q[0] >= Math.min(p[0], r[0]) &&
|
||||
q[1] <= Math.max(p[1], r[1]) &&
|
||||
q[1] >= Math.min(p[1], r[1])
|
||||
);
|
||||
};
|
@ -0,0 +1,72 @@
|
||||
import { pointsEqual } from "./point";
|
||||
import { lineSegment, pointOnLineSegment } from "./segment";
|
||||
import type { GlobalPoint, LocalPoint, Polygon } from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
|
||||
export function polygon<Point extends GlobalPoint | LocalPoint>(
|
||||
...points: Point[]
|
||||
) {
|
||||
return polygonClose(points) as Polygon<Point>;
|
||||
}
|
||||
|
||||
export function polygonFromPoints<Point extends GlobalPoint | LocalPoint>(
|
||||
points: Point[],
|
||||
) {
|
||||
return polygonClose(points) as Polygon<Point>;
|
||||
}
|
||||
|
||||
export const polygonIncludesPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
polygon: Polygon<Point>,
|
||||
) => {
|
||||
const x = point[0];
|
||||
const y = point[1];
|
||||
let inside = false;
|
||||
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const xi = polygon[i][0];
|
||||
const yi = polygon[i][1];
|
||||
const xj = polygon[j][0];
|
||||
const yj = polygon[j][1];
|
||||
|
||||
if (
|
||||
((yi > y && yj <= y) || (yi <= y && yj > y)) &&
|
||||
x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
|
||||
) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
|
||||
return inside;
|
||||
};
|
||||
|
||||
export const pointOnPolygon = <Point extends LocalPoint | GlobalPoint>(
|
||||
p: Point,
|
||||
poly: Polygon<Point>,
|
||||
threshold = PRECISION,
|
||||
) => {
|
||||
let on = false;
|
||||
|
||||
for (let i = 0, l = poly.length - 1; i < l; i++) {
|
||||
if (pointOnLineSegment(p, lineSegment(poly[i], poly[i + 1]), threshold)) {
|
||||
on = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return on;
|
||||
};
|
||||
|
||||
function polygonClose<Point extends LocalPoint | GlobalPoint>(
|
||||
polygon: Point[],
|
||||
) {
|
||||
return polygonIsClosed(polygon)
|
||||
? polygon
|
||||
: ([...polygon, polygon[0]] as Polygon<Point>);
|
||||
}
|
||||
|
||||
function polygonIsClosed<Point extends LocalPoint | GlobalPoint>(
|
||||
polygon: Point[],
|
||||
) {
|
||||
return pointsEqual(polygon[0], polygon[polygon.length - 1]);
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import { rangeInclusive, rangeIntersection, rangesOverlap } from "./range";
|
||||
|
||||
describe("range overlap", () => {
|
||||
const range1_4 = rangeInclusive(1, 4);
|
||||
|
||||
it("should overlap when range a contains range b", () => {
|
||||
expect(rangesOverlap(range1_4, rangeInclusive(2, 3))).toBe(true);
|
||||
expect(rangesOverlap(range1_4, range1_4)).toBe(true);
|
||||
expect(rangesOverlap(range1_4, rangeInclusive(1, 3))).toBe(true);
|
||||
expect(rangesOverlap(range1_4, rangeInclusive(2, 4))).toBe(true);
|
||||
});
|
||||
|
||||
it("should overlap when range b contains range a", () => {
|
||||
expect(rangesOverlap(rangeInclusive(2, 3), range1_4)).toBe(true);
|
||||
expect(rangesOverlap(rangeInclusive(1, 3), range1_4)).toBe(true);
|
||||
expect(rangesOverlap(rangeInclusive(2, 4), range1_4)).toBe(true);
|
||||
});
|
||||
|
||||
it("should overlap when range a and b intersect", () => {
|
||||
expect(rangesOverlap(range1_4, rangeInclusive(2, 5))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("range intersection", () => {
|
||||
const range1_4 = rangeInclusive(1, 4);
|
||||
|
||||
it("should intersect completely with itself", () => {
|
||||
expect(rangeIntersection(range1_4, range1_4)).toEqual(range1_4);
|
||||
});
|
||||
|
||||
it("should intersect irrespective of order", () => {
|
||||
expect(rangeIntersection(range1_4, rangeInclusive(2, 3))).toEqual([2, 3]);
|
||||
expect(rangeIntersection(rangeInclusive(2, 3), range1_4)).toEqual([2, 3]);
|
||||
expect(rangeIntersection(range1_4, rangeInclusive(3, 5))).toEqual(
|
||||
rangeInclusive(3, 4),
|
||||
);
|
||||
expect(rangeIntersection(rangeInclusive(3, 5), range1_4)).toEqual(
|
||||
rangeInclusive(3, 4),
|
||||
);
|
||||
});
|
||||
|
||||
it("should intersect at the edge", () => {
|
||||
expect(rangeIntersection(range1_4, rangeInclusive(4, 5))).toEqual(
|
||||
rangeInclusive(4, 4),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not intersect", () => {
|
||||
expect(rangeIntersection(range1_4, rangeInclusive(5, 7))).toEqual(null);
|
||||
});
|
||||
});
|
@ -0,0 +1,82 @@
|
||||
import { toBrandedType } from "../excalidraw/utils";
|
||||
import type { InclusiveRange } from "./types";
|
||||
|
||||
/**
|
||||
* Create an inclusive range from the two numbers provided.
|
||||
*
|
||||
* @param start Start of the range
|
||||
* @param end End of the range
|
||||
* @returns
|
||||
*/
|
||||
export function rangeInclusive(start: number, end: number): InclusiveRange {
|
||||
return toBrandedType<InclusiveRange>([start, end]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a number pair into an inclusive range.
|
||||
*
|
||||
* @param pair The number pair to convert to an inclusive range
|
||||
* @returns The new inclusive range
|
||||
*/
|
||||
export function rangeInclusiveFromPair(pair: [start: number, end: number]) {
|
||||
return toBrandedType<InclusiveRange>(pair);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two ranges, return if the two ranges overlap with each other e.g.
|
||||
* [1, 3] overlaps with [2, 4] while [1, 3] does not overlap with [4, 5].
|
||||
*
|
||||
* @param param0 One of the ranges to compare
|
||||
* @param param1 The other range to compare against
|
||||
* @returns TRUE if the ranges overlap
|
||||
*/
|
||||
export const rangesOverlap = (
|
||||
[a0, a1]: InclusiveRange,
|
||||
[b0, b1]: InclusiveRange,
|
||||
): boolean => {
|
||||
if (a0 <= b0) {
|
||||
return a1 >= b0;
|
||||
}
|
||||
|
||||
if (a0 >= b0) {
|
||||
return b1 >= a0;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given two ranges,return ther intersection of the two ranges if any e.g. the
|
||||
* intersection of [1, 3] and [2, 4] is [2, 3].
|
||||
*
|
||||
* @param param0 The first range to compare
|
||||
* @param param1 The second range to compare
|
||||
* @returns The inclusive range intersection or NULL if no intersection
|
||||
*/
|
||||
export const rangeIntersection = (
|
||||
[a0, a1]: InclusiveRange,
|
||||
[b0, b1]: InclusiveRange,
|
||||
): InclusiveRange | null => {
|
||||
const rangeStart = Math.max(a0, b0);
|
||||
const rangeEnd = Math.min(a1, b1);
|
||||
|
||||
if (rangeStart <= rangeEnd) {
|
||||
return toBrandedType<InclusiveRange>([rangeStart, rangeEnd]);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if a value is inside a range.
|
||||
*
|
||||
* @param value The value to check
|
||||
* @param range The range
|
||||
* @returns
|
||||
*/
|
||||
export const rangeIncludesValue = (
|
||||
value: number,
|
||||
[min, max]: InclusiveRange,
|
||||
): boolean => {
|
||||
return value >= min && value <= max;
|
||||
};
|
@ -0,0 +1,158 @@
|
||||
import {
|
||||
isPoint,
|
||||
pointCenter,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
} from "./point";
|
||||
import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
import {
|
||||
vectorAdd,
|
||||
vectorCross,
|
||||
vectorFromPoint,
|
||||
vectorScale,
|
||||
vectorSubtract,
|
||||
} from "./vector";
|
||||
|
||||
/**
|
||||
* Create a line segment from two points.
|
||||
*
|
||||
* @param points The two points delimiting the line segment on each end
|
||||
* @returns The line segment delineated by the points
|
||||
*/
|
||||
export function lineSegment<P extends GlobalPoint | LocalPoint>(
|
||||
a: P,
|
||||
b: P,
|
||||
): LineSegment<P> {
|
||||
return [a, b] as LineSegment<P>;
|
||||
}
|
||||
|
||||
export function lineSegmentFromPointArray<P extends GlobalPoint | LocalPoint>(
|
||||
pointArray: P[],
|
||||
): LineSegment<P> | undefined {
|
||||
return pointArray.length === 2
|
||||
? lineSegment<P>(pointArray[0], pointArray[1])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param segment
|
||||
* @returns
|
||||
*/
|
||||
export const isLineSegment = <Point extends GlobalPoint | LocalPoint>(
|
||||
segment: unknown,
|
||||
): segment is LineSegment<Point> =>
|
||||
Array.isArray(segment) &&
|
||||
segment.length === 2 &&
|
||||
isPoint(segment[0]) &&
|
||||
isPoint(segment[0]);
|
||||
|
||||
/**
|
||||
* Return the coordinates resulting from rotating the given line about an origin by an angle in radians
|
||||
* note that when the origin is not given, the midpoint of the given line is used as the origin.
|
||||
*
|
||||
* @param l
|
||||
* @param angle
|
||||
* @param origin
|
||||
* @returns
|
||||
*/
|
||||
export const lineSegmentRotate = <Point extends LocalPoint | GlobalPoint>(
|
||||
l: LineSegment<Point>,
|
||||
angle: Radians,
|
||||
origin?: Point,
|
||||
): LineSegment<Point> => {
|
||||
return lineSegment(
|
||||
pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle),
|
||||
pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the point two line segments with a definite start and end point
|
||||
* intersect at.
|
||||
*/
|
||||
export const segmentsIntersectAt = <Point extends GlobalPoint | LocalPoint>(
|
||||
a: Readonly<LineSegment<Point>>,
|
||||
b: Readonly<LineSegment<Point>>,
|
||||
): Point | null => {
|
||||
const a0 = vectorFromPoint(a[0]);
|
||||
const a1 = vectorFromPoint(a[1]);
|
||||
const b0 = vectorFromPoint(b[0]);
|
||||
const b1 = vectorFromPoint(b[1]);
|
||||
const r = vectorSubtract(a1, a0);
|
||||
const s = vectorSubtract(b1, b0);
|
||||
const denominator = vectorCross(r, s);
|
||||
|
||||
if (denominator === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const i = vectorSubtract(vectorFromPoint(b[0]), vectorFromPoint(a[0]));
|
||||
const u = vectorCross(i, r) / denominator;
|
||||
const t = vectorCross(i, s) / denominator;
|
||||
|
||||
if (u === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const p = vectorAdd(a0, vectorScale(r, t));
|
||||
|
||||
if (t >= 0 && t < 1 && u >= 0 && u < 1) {
|
||||
return pointFromVector<Point>(p);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const pointOnLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
line: LineSegment<Point>,
|
||||
threshold = PRECISION,
|
||||
) => {
|
||||
const distance = distanceToLineSegment(point, line);
|
||||
|
||||
if (distance === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return distance < threshold;
|
||||
};
|
||||
|
||||
export const distanceToLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
line: LineSegment<Point>,
|
||||
) => {
|
||||
const [x, y] = point;
|
||||
const [[x1, y1], [x2, y2]] = line;
|
||||
|
||||
const A = x - x1;
|
||||
const B = y - y1;
|
||||
const C = x2 - x1;
|
||||
const D = y2 - y1;
|
||||
|
||||
const dot = A * C + B * D;
|
||||
const len_sq = C * C + D * D;
|
||||
let param = -1;
|
||||
if (len_sq !== 0) {
|
||||
param = dot / len_sq;
|
||||
}
|
||||
|
||||
let xx;
|
||||
let yy;
|
||||
|
||||
if (param < 0) {
|
||||
xx = x1;
|
||||
yy = y1;
|
||||
} else if (param > 1) {
|
||||
xx = x2;
|
||||
yy = y2;
|
||||
} else {
|
||||
xx = x1 + param * C;
|
||||
yy = y1 + param * D;
|
||||
}
|
||||
|
||||
const dx = x - xx;
|
||||
const dy = y - yy;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
import type { GlobalPoint, LocalPoint, Triangle } from "./types";
|
||||
|
||||
// Types
|
||||
|
||||
/**
|
||||
* Tests if a point lies inside a triangle. This function
|
||||
* will return FALSE if the point lies exactly on the sides
|
||||
* of the triangle.
|
||||
*
|
||||
* @param triangle The triangle to test the point for
|
||||
* @param p The point to test whether is in the triangle
|
||||
* @returns TRUE if the point is inside of the triangle
|
||||
*/
|
||||
export function triangleIncludesPoint<P extends GlobalPoint | LocalPoint>(
|
||||
[a, b, c]: Triangle<P>,
|
||||
p: P,
|
||||
): boolean {
|
||||
const triangleSign = (p1: P, p2: P, p3: P) =>
|
||||
(p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]);
|
||||
const d1 = triangleSign(p, a, b);
|
||||
const d2 = triangleSign(p, b, c);
|
||||
const d3 = triangleSign(p, c, a);
|
||||
|
||||
const has_neg = d1 < 0 || d2 < 0 || d3 < 0;
|
||||
const has_pos = d1 > 0 || d2 > 0 || d3 > 0;
|
||||
|
||||
return !(has_neg && has_pos);
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
//
|
||||
// Measurements
|
||||
//
|
||||
|
||||
/**
|
||||
* By definition one radian is the angle subtended at the centre
|
||||
* of a circle by an arc that is equal in length to the radius.
|
||||
*/
|
||||
export type Radians = number & { _brand: "excalimath__radian" };
|
||||
|
||||
/**
|
||||
* An angle measurement of a plane angle in which one full
|
||||
* rotation is 360 degrees.
|
||||
*/
|
||||
export type Degrees = number & { _brand: "excalimath_degree" };
|
||||
|
||||
//
|
||||
// Range
|
||||
//
|
||||
|
||||
/**
|
||||
* A number range which includes the start and end numbers in the range.
|
||||
*/
|
||||
export type InclusiveRange = [number, number] & { _brand: "excalimath_degree" };
|
||||
|
||||
//
|
||||
// Point
|
||||
//
|
||||
|
||||
/**
|
||||
* Represents a 2D position in world or canvas space. A
|
||||
* global coordinate.
|
||||
*/
|
||||
export type GlobalPoint = [x: number, y: number] & {
|
||||
_brand: "excalimath__globalpoint";
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a 2D position in whatever local space it's
|
||||
* needed. A local coordinate.
|
||||
*/
|
||||
export type LocalPoint = [x: number, y: number] & {
|
||||
_brand: "excalimath__localpoint";
|
||||
};
|
||||
|
||||
// Line
|
||||
|
||||
/**
|
||||
* A line is an infinitely long object with no width, depth, or curvature.
|
||||
*/
|
||||
export type Line<P extends GlobalPoint | LocalPoint> = [p: P, q: P] & {
|
||||
_brand: "excalimath_line";
|
||||
};
|
||||
|
||||
/**
|
||||
* In geometry, a line segment is a part of a straight
|
||||
* line that is bounded by two distinct end points, and
|
||||
* contains every point on the line that is between its endpoints.
|
||||
*/
|
||||
export type LineSegment<P extends GlobalPoint | LocalPoint> = [a: P, b: P] & {
|
||||
_brand: "excalimath_linesegment";
|
||||
};
|
||||
|
||||
//
|
||||
// Vector
|
||||
//
|
||||
|
||||
/**
|
||||
* Represents a 2D vector
|
||||
*/
|
||||
export type Vector = [u: number, v: number] & {
|
||||
_brand: "excalimath__vector";
|
||||
};
|
||||
|
||||
// Triangles
|
||||
|
||||
/**
|
||||
* A triangle represented by 3 points
|
||||
*/
|
||||
export type Triangle<P extends GlobalPoint | LocalPoint> = [
|
||||
a: P,
|
||||
b: P,
|
||||
c: P,
|
||||
] & {
|
||||
_brand: "excalimath__triangle";
|
||||
};
|
||||
|
||||
//
|
||||
// Polygon
|
||||
//
|
||||
|
||||
/**
|
||||
* A polygon is a closed shape by connecting the given points
|
||||
* rectangles and diamonds are modelled by polygons
|
||||
*/
|
||||
export type Polygon<Point extends GlobalPoint | LocalPoint> = Point[] & {
|
||||
_brand: "excalimath_polygon";
|
||||
};
|
||||
|
||||
//
|
||||
// Curve
|
||||
//
|
||||
|
||||
/**
|
||||
* Cubic bezier curve with four control points
|
||||
*/
|
||||
export type Curve<Point extends GlobalPoint | LocalPoint> = [
|
||||
Point,
|
||||
Point,
|
||||
Point,
|
||||
Point,
|
||||
] & {
|
||||
_brand: "excalimath_curve";
|
||||
};
|
||||
|
||||
export type PolarCoords = [
|
||||
radius: number,
|
||||
/** angle in radians */
|
||||
angle: number,
|
||||
];
|
||||
|
||||
/**
|
||||
* Angles are in radians and centered on 0, 0. Zero radians on a 1 radius circle
|
||||
* corresponds to (1, 0) cartesian coordinates (point), i.e. to the "right".
|
||||
*/
|
||||
export type SymmetricArc = {
|
||||
radius: number;
|
||||
startAngle: number;
|
||||
endAngle: number;
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
export const PRECISION = 10e-5;
|
||||
|
||||
export function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
export function round(value: number, precision: number) {
|
||||
const multiplier = Math.pow(10, precision);
|
||||
|
||||
return Math.round((value + Number.EPSILON) * multiplier) / multiplier;
|
||||
}
|
||||
|
||||
export const average = (a: number, b: number) => (a + b) / 2;
|
||||
|
||||
export const isFiniteNumber = (value: any): value is number => {
|
||||
return typeof value === "number" && Number.isFinite(value);
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
import { isVector } from ".";
|
||||
|
||||
describe("Vector", () => {
|
||||
test("isVector", () => {
|
||||
expect(isVector([5, 5])).toBe(true);
|
||||
expect(isVector([-5, -5])).toBe(true);
|
||||
expect(isVector([5, 0.5])).toBe(true);
|
||||
expect(isVector(null)).toBe(false);
|
||||
expect(isVector(undefined)).toBe(false);
|
||||
expect(isVector([5, NaN])).toBe(false);
|
||||
});
|
||||
});
|
@ -0,0 +1,141 @@
|
||||
import type { GlobalPoint, LocalPoint, Vector } from "./types";
|
||||
|
||||
/**
|
||||
* Create a vector from the x and y coordiante elements.
|
||||
*
|
||||
* @param x The X aspect of the vector
|
||||
* @param y T Y aspect of the vector
|
||||
* @returns The constructed vector with X and Y as the coordinates
|
||||
*/
|
||||
export function vector(
|
||||
x: number,
|
||||
y: number,
|
||||
originX: number = 0,
|
||||
originY: number = 0,
|
||||
): Vector {
|
||||
return [x - originX, y - originY] as Vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a point into a vector with the origin point.
|
||||
*
|
||||
* @param p The point to turn into a vector
|
||||
* @param origin The origin point in a given coordiante system
|
||||
* @returns The created vector from the point and the origin
|
||||
*/
|
||||
export function vectorFromPoint<Point extends GlobalPoint | LocalPoint>(
|
||||
p: Point,
|
||||
origin: Point = [0, 0] as Point,
|
||||
): Vector {
|
||||
return vector(p[0] - origin[0], p[1] - origin[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross product is a binary operation on two vectors in 2D space.
|
||||
* It results in a vector that is perpendicular to both vectors.
|
||||
*
|
||||
* @param a One of the vectors to use for the directed area calculation
|
||||
* @param b The other vector to use for the directed area calculation
|
||||
* @returns The directed area value for the two vectos
|
||||
*/
|
||||
export function vectorCross(a: Vector, b: Vector): number {
|
||||
return a[0] * b[1] - b[0] * a[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dot product is defined as the sum of the products of the
|
||||
* two vectors.
|
||||
*
|
||||
* @param a One of the vectors for which the sum of products is calculated
|
||||
* @param b The other vector for which the sum of products is calculated
|
||||
* @returns The sum of products of the two vectors
|
||||
*/
|
||||
export function vectorDot(a: Vector, b: Vector) {
|
||||
return a[0] * b[0] + a[1] * b[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the value has the shape of a Vector.
|
||||
*
|
||||
* @param v The value to test
|
||||
* @returns TRUE if the value has the shape and components of a Vectors
|
||||
*/
|
||||
export function isVector(v: unknown): v is Vector {
|
||||
return (
|
||||
Array.isArray(v) &&
|
||||
v.length === 2 &&
|
||||
typeof v[0] === "number" &&
|
||||
!isNaN(v[0]) &&
|
||||
typeof v[1] === "number" &&
|
||||
!isNaN(v[1])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add two vectors by adding their coordinates.
|
||||
*
|
||||
* @param a One of the vectors to add
|
||||
* @param b The other vector to add
|
||||
* @returns The sum vector of the two provided vectors
|
||||
*/
|
||||
export function vectorAdd(a: Readonly<Vector>, b: Readonly<Vector>): Vector {
|
||||
return [a[0] + b[0], a[1] + b[1]] as Vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add two vectors by adding their coordinates.
|
||||
*
|
||||
* @param start One of the vectors to add
|
||||
* @param end The other vector to add
|
||||
* @returns The sum vector of the two provided vectors
|
||||
*/
|
||||
export function vectorSubtract(
|
||||
start: Readonly<Vector>,
|
||||
end: Readonly<Vector>,
|
||||
): Vector {
|
||||
return [start[0] - end[0], start[1] - end[1]] as Vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale vector by a scalar.
|
||||
*
|
||||
* @param v The vector to scale
|
||||
* @param scalar The scalar to multiply the vector components with
|
||||
* @returns The new scaled vector
|
||||
*/
|
||||
export function vectorScale(v: Vector, scalar: number): Vector {
|
||||
return vector(v[0] * scalar, v[1] * scalar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the sqare magnitude of a vector. Use this if you compare
|
||||
* magnitudes as it saves you an SQRT.
|
||||
*
|
||||
* @param v The vector to measure
|
||||
* @returns The scalar squared magnitude of the vector
|
||||
*/
|
||||
export function vectorMagnitudeSq(v: Vector) {
|
||||
return v[0] * v[0] + v[1] * v[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the magnitude of a vector.
|
||||
*
|
||||
* @param v The vector to measure
|
||||
* @returns The scalar magnitude of the vector
|
||||
*/
|
||||
export function vectorMagnitude(v: Vector) {
|
||||
return Math.sqrt(vectorMagnitudeSq(v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the vector (i.e. make the vector magnitue equal 1).
|
||||
*
|
||||
* @param v The vector to normalize
|
||||
* @returns The new normalized vector
|
||||
*/
|
||||
export const vectorNormalize = (v: Vector): Vector => {
|
||||
const m = vectorMagnitude(v);
|
||||
|
||||
return vector(v[0] / m, v[1] / m);
|
||||
};
|
@ -0,0 +1,55 @@
|
||||
const webpack = require("webpack");
|
||||
const path = require("path");
|
||||
const BundleAnalyzerPlugin =
|
||||
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||
|
||||
module.exports = {
|
||||
mode: "production",
|
||||
entry: { "excalidraw-math.min": "./index.js" },
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "[name].js",
|
||||
library: "ExcalidrawMath",
|
||||
libraryTarget: "umd",
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js", ".css", ".scss"],
|
||||
},
|
||||
optimization: {
|
||||
runtimeChunk: false,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|tsx|js)$/,
|
||||
use: [
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
configFile: path.resolve(__dirname, "../tsconfig.prod.json"),
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: "babel-loader",
|
||||
|
||||
options: {
|
||||
presets: [
|
||||
"@babel/preset-env",
|
||||
["@babel/preset-react", { runtime: "automatic" }],
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
plugins: [["@babel/plugin-transform-runtime"]],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []),
|
||||
],
|
||||
};
|
@ -0,0 +1,87 @@
|
||||
import type { Curve, Degrees, GlobalPoint } from "../math";
|
||||
import {
|
||||
curve,
|
||||
degreesToRadians,
|
||||
lineSegment,
|
||||
lineSegmentRotate,
|
||||
point,
|
||||
pointRotateDegs,
|
||||
} from "../math";
|
||||
import { pointOnCurve, pointOnPolyline } from "./collision";
|
||||
import type { Polyline } from "./geometry/shape";
|
||||
|
||||
describe("point and curve", () => {
|
||||
const c: Curve<GlobalPoint> = curve(
|
||||
point(1.4, 1.65),
|
||||
point(1.9, 7.9),
|
||||
point(5.9, 1.65),
|
||||
point(6.44, 4.84),
|
||||
);
|
||||
|
||||
it("point on curve", () => {
|
||||
expect(pointOnCurve(c[0], c, 10e-5)).toBe(true);
|
||||
expect(pointOnCurve(c[3], c, 10e-5)).toBe(true);
|
||||
|
||||
expect(pointOnCurve(point(2, 4), c, 0.1)).toBe(true);
|
||||
expect(pointOnCurve(point(4, 4.4), c, 0.1)).toBe(true);
|
||||
expect(pointOnCurve(point(5.6, 3.85), c, 0.1)).toBe(true);
|
||||
|
||||
expect(pointOnCurve(point(5.6, 4), c, 0.1)).toBe(false);
|
||||
expect(pointOnCurve(c[1], c, 0.1)).toBe(false);
|
||||
expect(pointOnCurve(c[2], c, 0.1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and polylines", () => {
|
||||
const polyline: Polyline<GlobalPoint> = [
|
||||
lineSegment(point(1, 0), point(1, 2)),
|
||||
lineSegment(point(1, 2), point(2, 2)),
|
||||
lineSegment(point(2, 2), point(2, 1)),
|
||||
lineSegment(point(2, 1), point(3, 1)),
|
||||
];
|
||||
|
||||
it("point on the line", () => {
|
||||
expect(pointOnPolyline(point(1, 0), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(1, 2), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(2, 2), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(2, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(3, 1), polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline(point(1, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(2, 1.5), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(2.5, 1), polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline(point(0, 1), polyline)).toBe(false);
|
||||
expect(pointOnPolyline(point(2.1, 1.5), polyline)).toBe(false);
|
||||
});
|
||||
|
||||
it("point on the line with rotation", () => {
|
||||
const truePoints = [
|
||||
point(1, 0),
|
||||
point(1, 2),
|
||||
point(2, 2),
|
||||
point(2, 1),
|
||||
point(3, 1),
|
||||
];
|
||||
|
||||
truePoints.forEach((p) => {
|
||||
const rotation = (Math.random() * 360) as Degrees;
|
||||
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
|
||||
const rotatedPolyline = polyline.map((line) =>
|
||||
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
||||
});
|
||||
|
||||
const falsePoints = [point(0, 1), point(2.1, 1.5)];
|
||||
|
||||
falsePoints.forEach((p) => {
|
||||
const rotation = (Math.random() * 360) as Degrees;
|
||||
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
|
||||
const rotatedPolyline = polyline.map((line) =>
|
||||
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,249 +1,122 @@
|
||||
import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math";
|
||||
import {
|
||||
lineIntersectsLine,
|
||||
lineRotate,
|
||||
pointInEllipse,
|
||||
pointInPolygon,
|
||||
pointLeftofLine,
|
||||
pointOnCurve,
|
||||
pointOnEllipse,
|
||||
pointOnLine,
|
||||
point,
|
||||
lineSegment,
|
||||
polygon,
|
||||
pointOnLineSegment,
|
||||
pointOnPolygon,
|
||||
pointOnPolyline,
|
||||
pointRightofLine,
|
||||
pointRotate,
|
||||
} from "./geometry";
|
||||
import type { Curve, Ellipse, Line, Point, Polygon, Polyline } from "./shape";
|
||||
polygonIncludesPoint,
|
||||
segmentsIntersectAt,
|
||||
} from "../../math";
|
||||
import { pointInEllipse, pointOnEllipse, type Ellipse } from "./shape";
|
||||
|
||||
describe("point and line", () => {
|
||||
const line: Line = [
|
||||
[1, 0],
|
||||
[1, 2],
|
||||
];
|
||||
|
||||
it("point on left or right of line", () => {
|
||||
expect(pointLeftofLine([0, 1], line)).toBe(true);
|
||||
expect(pointLeftofLine([1, 1], line)).toBe(false);
|
||||
expect(pointLeftofLine([2, 1], line)).toBe(false);
|
||||
|
||||
expect(pointRightofLine([0, 1], line)).toBe(false);
|
||||
expect(pointRightofLine([1, 1], line)).toBe(false);
|
||||
expect(pointRightofLine([2, 1], line)).toBe(true);
|
||||
});
|
||||
|
||||
it("point on the line", () => {
|
||||
expect(pointOnLine([0, 1], line)).toBe(false);
|
||||
expect(pointOnLine([1, 1], line, 0)).toBe(true);
|
||||
expect(pointOnLine([2, 1], line)).toBe(false);
|
||||
});
|
||||
});
|
||||
// const l: Line<GlobalPoint> = line(point(1, 0), point(1, 2));
|
||||
|
||||
describe("point and polylines", () => {
|
||||
const polyline: Polyline = [
|
||||
[
|
||||
[1, 0],
|
||||
[1, 2],
|
||||
],
|
||||
[
|
||||
[1, 2],
|
||||
[2, 2],
|
||||
],
|
||||
[
|
||||
[2, 2],
|
||||
[2, 1],
|
||||
],
|
||||
[
|
||||
[2, 1],
|
||||
[3, 1],
|
||||
],
|
||||
];
|
||||
// it("point on left or right of line", () => {
|
||||
// expect(pointLeftofLine(point(0, 1), l)).toBe(true);
|
||||
// expect(pointLeftofLine(point(1, 1), l)).toBe(false);
|
||||
// expect(pointLeftofLine(point(2, 1), l)).toBe(false);
|
||||
|
||||
it("point on the line", () => {
|
||||
expect(pointOnPolyline([1, 0], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([1, 2], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([2, 2], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([2, 1], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([3, 1], polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline([1, 1], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([2, 1.5], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([2.5, 1], polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline([0, 1], polyline)).toBe(false);
|
||||
expect(pointOnPolyline([2.1, 1.5], polyline)).toBe(false);
|
||||
});
|
||||
|
||||
it("point on the line with rotation", () => {
|
||||
const truePoints = [
|
||||
[1, 0],
|
||||
[1, 2],
|
||||
[2, 2],
|
||||
[2, 1],
|
||||
[3, 1],
|
||||
] as Point[];
|
||||
|
||||
truePoints.forEach((point) => {
|
||||
const rotation = Math.random() * 360;
|
||||
const rotatedPoint = pointRotate(point, rotation);
|
||||
const rotatedPolyline: Polyline = polyline.map((line) =>
|
||||
lineRotate(line, rotation, [0, 0]),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
||||
});
|
||||
// expect(pointRightofLine(point(0, 1), l)).toBe(false);
|
||||
// expect(pointRightofLine(point(1, 1), l)).toBe(false);
|
||||
// expect(pointRightofLine(point(2, 1), l)).toBe(true);
|
||||
// });
|
||||
|
||||
const falsePoints = [
|
||||
[0, 1],
|
||||
[2.1, 1.5],
|
||||
] as Point[];
|
||||
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
|
||||
|
||||
falsePoints.forEach((point) => {
|
||||
const rotation = Math.random() * 360;
|
||||
const rotatedPoint = pointRotate(point, rotation);
|
||||
const rotatedPolyline: Polyline = polyline.map((line) =>
|
||||
lineRotate(line, rotation, [0, 0]),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
|
||||
});
|
||||
it("point on the line", () => {
|
||||
expect(pointOnLineSegment(point(0, 1), s)).toBe(false);
|
||||
expect(pointOnLineSegment(point(1, 1), s, 0)).toBe(true);
|
||||
expect(pointOnLineSegment(point(2, 1), s)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and polygon", () => {
|
||||
const polygon: Polygon = [
|
||||
[10, 10],
|
||||
[50, 10],
|
||||
[50, 50],
|
||||
[10, 50],
|
||||
];
|
||||
const poly: Polygon<GlobalPoint> = polygon(
|
||||
point(10, 10),
|
||||
point(50, 10),
|
||||
point(50, 50),
|
||||
point(10, 50),
|
||||
);
|
||||
|
||||
it("point on polygon", () => {
|
||||
expect(pointOnPolygon([30, 10], polygon)).toBe(true);
|
||||
expect(pointOnPolygon([50, 30], polygon)).toBe(true);
|
||||
expect(pointOnPolygon([30, 50], polygon)).toBe(true);
|
||||
expect(pointOnPolygon([10, 30], polygon)).toBe(true);
|
||||
expect(pointOnPolygon([30, 30], polygon)).toBe(false);
|
||||
expect(pointOnPolygon([30, 70], polygon)).toBe(false);
|
||||
expect(pointOnPolygon(point(30, 10), poly)).toBe(true);
|
||||
expect(pointOnPolygon(point(50, 30), poly)).toBe(true);
|
||||
expect(pointOnPolygon(point(30, 50), poly)).toBe(true);
|
||||
expect(pointOnPolygon(point(10, 30), poly)).toBe(true);
|
||||
expect(pointOnPolygon(point(30, 30), poly)).toBe(false);
|
||||
expect(pointOnPolygon(point(30, 70), poly)).toBe(false);
|
||||
});
|
||||
|
||||
it("point in polygon", () => {
|
||||
const polygon: Polygon = [
|
||||
[0, 0],
|
||||
[2, 0],
|
||||
[2, 2],
|
||||
[0, 2],
|
||||
];
|
||||
expect(pointInPolygon([1, 1], polygon)).toBe(true);
|
||||
expect(pointInPolygon([3, 3], polygon)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and curve", () => {
|
||||
const curve: Curve = [
|
||||
[1.4, 1.65],
|
||||
[1.9, 7.9],
|
||||
[5.9, 1.65],
|
||||
[6.44, 4.84],
|
||||
];
|
||||
|
||||
it("point on curve", () => {
|
||||
expect(pointOnCurve(curve[0], curve)).toBe(true);
|
||||
expect(pointOnCurve(curve[3], curve)).toBe(true);
|
||||
|
||||
expect(pointOnCurve([2, 4], curve, 0.1)).toBe(true);
|
||||
expect(pointOnCurve([4, 4.4], curve, 0.1)).toBe(true);
|
||||
expect(pointOnCurve([5.6, 3.85], curve, 0.1)).toBe(true);
|
||||
|
||||
expect(pointOnCurve([5.6, 4], curve, 0.1)).toBe(false);
|
||||
expect(pointOnCurve(curve[1], curve, 0.1)).toBe(false);
|
||||
expect(pointOnCurve(curve[2], curve, 0.1)).toBe(false);
|
||||
const poly: Polygon<GlobalPoint> = polygon(
|
||||
point(0, 0),
|
||||
point(2, 0),
|
||||
point(2, 2),
|
||||
point(0, 2),
|
||||
);
|
||||
expect(polygonIncludesPoint(point(1, 1), poly)).toBe(true);
|
||||
expect(polygonIncludesPoint(point(3, 3), poly)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and ellipse", () => {
|
||||
const ellipse: Ellipse = {
|
||||
center: [0, 0],
|
||||
angle: 0,
|
||||
const ellipse: Ellipse<GlobalPoint> = {
|
||||
center: point(0, 0),
|
||||
angle: 0 as Radians,
|
||||
halfWidth: 2,
|
||||
halfHeight: 1,
|
||||
};
|
||||
|
||||
it("point on ellipse", () => {
|
||||
[
|
||||
[0, 1],
|
||||
[0, -1],
|
||||
[2, 0],
|
||||
[-2, 0],
|
||||
].forEach((point) => {
|
||||
expect(pointOnEllipse(point as Point, ellipse)).toBe(true);
|
||||
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
||||
expect(pointOnEllipse(p, ellipse)).toBe(true);
|
||||
});
|
||||
expect(pointOnEllipse([-1.4, 0.7], ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse([-1.4, 0.71], ellipse, 0.01)).toBe(true);
|
||||
expect(pointOnEllipse(point(-1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(point(-1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse([1.4, 0.7], ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse([1.4, 0.71], ellipse, 0.01)).toBe(true);
|
||||
expect(pointOnEllipse(point(1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(point(1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse([1, -0.86], ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse([1, -0.86], ellipse, 0.01)).toBe(true);
|
||||
expect(pointOnEllipse(point(1, -0.86), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(point(1, -0.86), ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse([-1, -0.86], ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse([-1, -0.86], ellipse, 0.01)).toBe(true);
|
||||
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse([-1, 0.8], ellipse)).toBe(false);
|
||||
expect(pointOnEllipse([1, -0.8], ellipse)).toBe(false);
|
||||
expect(pointOnEllipse(point(-1, 0.8), ellipse)).toBe(false);
|
||||
expect(pointOnEllipse(point(1, -0.8), ellipse)).toBe(false);
|
||||
});
|
||||
|
||||
it("point in ellipse", () => {
|
||||
[
|
||||
[0, 1],
|
||||
[0, -1],
|
||||
[2, 0],
|
||||
[-2, 0],
|
||||
].forEach((point) => {
|
||||
expect(pointInEllipse(point as Point, ellipse)).toBe(true);
|
||||
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
||||
expect(pointInEllipse(p, ellipse)).toBe(true);
|
||||
});
|
||||
|
||||
expect(pointInEllipse([-1, 0.8], ellipse)).toBe(true);
|
||||
expect(pointInEllipse([1, -0.8], ellipse)).toBe(true);
|
||||
expect(pointInEllipse(point(-1, 0.8), ellipse)).toBe(true);
|
||||
expect(pointInEllipse(point(1, -0.8), ellipse)).toBe(true);
|
||||
|
||||
expect(pointInEllipse([-1, 1], ellipse)).toBe(false);
|
||||
expect(pointInEllipse([-1.4, 0.8], ellipse)).toBe(false);
|
||||
expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false);
|
||||
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("line and line", () => {
|
||||
const lineA: Line = [
|
||||
[1, 4],
|
||||
[3, 4],
|
||||
];
|
||||
const lineB: Line = [
|
||||
[2, 1],
|
||||
[2, 7],
|
||||
];
|
||||
const lineC: Line = [
|
||||
[1, 8],
|
||||
[3, 8],
|
||||
];
|
||||
const lineD: Line = [
|
||||
[1, 8],
|
||||
[3, 8],
|
||||
];
|
||||
const lineE: Line = [
|
||||
[1, 9],
|
||||
[3, 9],
|
||||
];
|
||||
const lineF: Line = [
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
];
|
||||
const lineG: Line = [
|
||||
[0, 1],
|
||||
[2, 3],
|
||||
];
|
||||
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
|
||||
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
|
||||
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
||||
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
||||
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
|
||||
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
|
||||
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
|
||||
|
||||
it("intersection", () => {
|
||||
expect(lineIntersectsLine(lineA, lineB)).toBe(true);
|
||||
expect(lineIntersectsLine(lineA, lineC)).toBe(false);
|
||||
expect(lineIntersectsLine(lineB, lineC)).toBe(false);
|
||||
expect(lineIntersectsLine(lineC, lineD)).toBe(true);
|
||||
expect(lineIntersectsLine(lineE, lineD)).toBe(false);
|
||||
expect(lineIntersectsLine(lineF, lineG)).toBe(true);
|
||||
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
|
||||
expect(segmentsIntersectAt(lineA, lineC)).toBe(null);
|
||||
expect(segmentsIntersectAt(lineB, lineC)).toBe(null);
|
||||
expect(segmentsIntersectAt(lineC, lineD)).toBe(null); // Line overlapping line is not intersection!
|
||||
expect(segmentsIntersectAt(lineE, lineD)).toBe(null);
|
||||
expect(segmentsIntersectAt(lineF, lineG)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,108 @@
|
||||
const fs = require("fs");
|
||||
const { build } = require("esbuild");
|
||||
|
||||
const browserConfig = {
|
||||
entryPoints: ["index.ts"],
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
};
|
||||
|
||||
// Will be used later for treeshaking
|
||||
|
||||
// function getFiles(dir, files = []) {
|
||||
// const fileList = fs.readdirSync(dir);
|
||||
// for (const file of fileList) {
|
||||
// const name = `${dir}/${file}`;
|
||||
// if (
|
||||
// name.includes("node_modules") ||
|
||||
// name.includes("config") ||
|
||||
// name.includes("package.json") ||
|
||||
// name.includes("main.js") ||
|
||||
// name.includes("index-node.ts") ||
|
||||
// name.endsWith(".d.ts") ||
|
||||
// name.endsWith(".md")
|
||||
// ) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// if (fs.statSync(name).isDirectory()) {
|
||||
// getFiles(name, files);
|
||||
// } else if (
|
||||
// name.match(/\.(sa|sc|c)ss$/) ||
|
||||
// name.match(/\.(woff|woff2|eot|ttf|otf)$/) ||
|
||||
// name.match(/locales\/[^/]+\.json$/)
|
||||
// ) {
|
||||
// continue;
|
||||
// } else {
|
||||
// files.push(name);
|
||||
// }
|
||||
// }
|
||||
// return files;
|
||||
// }
|
||||
const createESMBrowserBuild = async () => {
|
||||
// Development unminified build with source maps
|
||||
const browserDev = await build({
|
||||
...browserConfig,
|
||||
outdir: "dist/browser/dev",
|
||||
sourcemap: true,
|
||||
metafile: true,
|
||||
define: {
|
||||
"import.meta.env": JSON.stringify({ DEV: true }),
|
||||
},
|
||||
});
|
||||
fs.writeFileSync(
|
||||
"meta-browser-dev.json",
|
||||
JSON.stringify(browserDev.metafile),
|
||||
);
|
||||
|
||||
// production minified build without sourcemaps
|
||||
const browserProd = await build({
|
||||
...browserConfig,
|
||||
outdir: "dist/browser/prod",
|
||||
minify: true,
|
||||
metafile: true,
|
||||
define: {
|
||||
"import.meta.env": JSON.stringify({ PROD: true }),
|
||||
},
|
||||
});
|
||||
fs.writeFileSync(
|
||||
"meta-browser-prod.json",
|
||||
JSON.stringify(browserProd.metafile),
|
||||
);
|
||||
};
|
||||
|
||||
const rawConfig = {
|
||||
entryPoints: ["index.ts"],
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
packages: "external",
|
||||
};
|
||||
|
||||
const createESMRawBuild = async () => {
|
||||
// Development unminified build with source maps
|
||||
const rawDev = await build({
|
||||
...rawConfig,
|
||||
outdir: "dist/dev",
|
||||
sourcemap: true,
|
||||
metafile: true,
|
||||
define: {
|
||||
"import.meta.env": JSON.stringify({ DEV: true }),
|
||||
},
|
||||
});
|
||||
fs.writeFileSync("meta-raw-dev.json", JSON.stringify(rawDev.metafile));
|
||||
|
||||
// production minified build without sourcemaps
|
||||
const rawProd = await build({
|
||||
...rawConfig,
|
||||
outdir: "dist/prod",
|
||||
minify: true,
|
||||
metafile: true,
|
||||
define: {
|
||||
"import.meta.env": JSON.stringify({ PROD: true }),
|
||||
},
|
||||
});
|
||||
fs.writeFileSync("meta-raw-prod.json", JSON.stringify(rawProd.metafile));
|
||||
};
|
||||
|
||||
createESMRawBuild();
|
||||
createESMBrowserBuild();
|
Loading…
Reference in New Issue