mirror of
https://github.com/Dvorinka/excalidraw-full.git
synced 2026-06-05 06:32:56 +00:00
use submodule
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { PRECISION } from "./utils";
|
||||
|
||||
import type {
|
||||
Degrees,
|
||||
GlobalPoint,
|
||||
LocalPoint,
|
||||
PolarCoords,
|
||||
Radians,
|
||||
} from "./types";
|
||||
|
||||
export const normalizeRadians = (angle: Radians): Radians =>
|
||||
angle < 0
|
||||
? (((angle % (2 * Math.PI)) + 2 * Math.PI) as Radians)
|
||||
: ((angle % (2 * Math.PI)) as Radians);
|
||||
|
||||
/**
|
||||
* 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),
|
||||
normalizeRadians(Math.atan2(y, x) as Radians),
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function radiansBetweenAngles(
|
||||
a: Radians,
|
||||
min: Radians,
|
||||
max: Radians,
|
||||
): boolean {
|
||||
a = normalizeRadians(a);
|
||||
min = normalizeRadians(min);
|
||||
max = normalizeRadians(max);
|
||||
|
||||
if (min < max) {
|
||||
return a >= min && a <= max;
|
||||
}
|
||||
|
||||
// The range wraps around the 0 angle
|
||||
return a >= min || a <= max;
|
||||
}
|
||||
|
||||
export function radiansDifference(a: Radians, b: Radians): Radians {
|
||||
a = normalizeRadians(a);
|
||||
b = normalizeRadians(b);
|
||||
|
||||
let diff = a - b;
|
||||
|
||||
if (diff < -Math.PI) {
|
||||
diff = (diff + 2 * Math.PI) as Radians;
|
||||
} else if (diff > Math.PI) {
|
||||
diff = (diff - 2 * Math.PI) as Radians;
|
||||
}
|
||||
|
||||
return Math.abs(diff) as Radians;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
export const PRECISION = 10e-5;
|
||||
|
||||
// Legendre-Gauss abscissae (x values) and weights for n=24
|
||||
// Refeerence: https://pomax.github.io/bezierinfo/legendre-gauss.html
|
||||
export const LegendreGaussN24TValues = [
|
||||
-0.0640568928626056260850430826247450385909,
|
||||
0.0640568928626056260850430826247450385909,
|
||||
-0.1911188674736163091586398207570696318404,
|
||||
0.1911188674736163091586398207570696318404,
|
||||
-0.3150426796961633743867932913198102407864,
|
||||
0.3150426796961633743867932913198102407864,
|
||||
-0.4337935076260451384870842319133497124524,
|
||||
0.4337935076260451384870842319133497124524,
|
||||
-0.5454214713888395356583756172183723700107,
|
||||
0.5454214713888395356583756172183723700107,
|
||||
-0.6480936519369755692524957869107476266696,
|
||||
0.6480936519369755692524957869107476266696,
|
||||
-0.7401241915785543642438281030999784255232,
|
||||
0.7401241915785543642438281030999784255232,
|
||||
-0.8200019859739029219539498726697452080761,
|
||||
0.8200019859739029219539498726697452080761,
|
||||
-0.8864155270044010342131543419821967550873,
|
||||
0.8864155270044010342131543419821967550873,
|
||||
-0.9382745520027327585236490017087214496548,
|
||||
0.9382745520027327585236490017087214496548,
|
||||
-0.9747285559713094981983919930081690617411,
|
||||
0.9747285559713094981983919930081690617411,
|
||||
-0.9951872199970213601799974097007368118745,
|
||||
0.9951872199970213601799974097007368118745,
|
||||
];
|
||||
|
||||
export const LegendreGaussN24CValues = [
|
||||
0.1279381953467521569740561652246953718517,
|
||||
0.1279381953467521569740561652246953718517,
|
||||
0.1258374563468282961213753825111836887264,
|
||||
0.1258374563468282961213753825111836887264,
|
||||
0.121670472927803391204463153476262425607,
|
||||
0.121670472927803391204463153476262425607,
|
||||
0.1155056680537256013533444839067835598622,
|
||||
0.1155056680537256013533444839067835598622,
|
||||
0.1074442701159656347825773424466062227946,
|
||||
0.1074442701159656347825773424466062227946,
|
||||
0.0976186521041138882698806644642471544279,
|
||||
0.0976186521041138882698806644642471544279,
|
||||
0.086190161531953275917185202983742667185,
|
||||
0.086190161531953275917185202983742667185,
|
||||
0.0733464814110803057340336152531165181193,
|
||||
0.0733464814110803057340336152531165181193,
|
||||
0.0592985849154367807463677585001085845412,
|
||||
0.0592985849154367807463677585001085845412,
|
||||
0.0442774388174198061686027482113382288593,
|
||||
0.0442774388174198061686027482113382288593,
|
||||
0.0285313886289336631813078159518782864491,
|
||||
0.0285313886289336631813078159518782864491,
|
||||
0.0123412297999871995468056670700372915759,
|
||||
0.0123412297999871995468056670700372915759,
|
||||
];
|
||||
@@ -0,0 +1,505 @@
|
||||
import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point";
|
||||
import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector";
|
||||
import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants";
|
||||
|
||||
import type { Curve, GlobalPoint, LineSegment, LocalPoint } 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>;
|
||||
}
|
||||
|
||||
function gradient(
|
||||
f: (t: number, s: number) => number,
|
||||
t0: number,
|
||||
s0: number,
|
||||
delta: number = 1e-6,
|
||||
): number[] {
|
||||
return [
|
||||
(f(t0 + delta, s0) - f(t0 - delta, s0)) / (2 * delta),
|
||||
(f(t0, s0 + delta) - f(t0, s0 - delta)) / (2 * delta),
|
||||
];
|
||||
}
|
||||
|
||||
function solve(
|
||||
f: (t: number, s: number) => [number, number],
|
||||
t0: number,
|
||||
s0: number,
|
||||
tolerance: number = 1e-3,
|
||||
iterLimit: number = 10,
|
||||
): number[] | null {
|
||||
let error = Infinity;
|
||||
let iter = 0;
|
||||
|
||||
while (error >= tolerance) {
|
||||
if (iter >= iterLimit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const y0 = f(t0, s0);
|
||||
const jacobian = [
|
||||
gradient((t, s) => f(t, s)[0], t0, s0),
|
||||
gradient((t, s) => f(t, s)[1], t0, s0),
|
||||
];
|
||||
const b = [[-y0[0]], [-y0[1]]];
|
||||
const det =
|
||||
jacobian[0][0] * jacobian[1][1] - jacobian[0][1] * jacobian[1][0];
|
||||
|
||||
if (det === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iJ = [
|
||||
[jacobian[1][1] / det, -jacobian[0][1] / det],
|
||||
[-jacobian[1][0] / det, jacobian[0][0] / det],
|
||||
];
|
||||
const h = [
|
||||
[iJ[0][0] * b[0][0] + iJ[0][1] * b[1][0]],
|
||||
[iJ[1][0] * b[0][0] + iJ[1][1] * b[1][0]],
|
||||
];
|
||||
|
||||
t0 = t0 + h[0][0];
|
||||
s0 = s0 + h[1][0];
|
||||
|
||||
const [tErr, sErr] = f(t0, s0);
|
||||
error = Math.max(Math.abs(tErr), Math.abs(sErr));
|
||||
iter += 1;
|
||||
}
|
||||
|
||||
return [t0, s0];
|
||||
}
|
||||
|
||||
export const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
t: number,
|
||||
) =>
|
||||
pointFrom<Point>(
|
||||
(1 - t) ** 3 * c[0][0] +
|
||||
3 * (1 - t) ** 2 * t * c[1][0] +
|
||||
3 * (1 - t) * t ** 2 * c[2][0] +
|
||||
t ** 3 * c[3][0],
|
||||
(1 - t) ** 3 * c[0][1] +
|
||||
3 * (1 - t) ** 2 * t * c[1][1] +
|
||||
3 * (1 - t) * t ** 2 * c[2][1] +
|
||||
t ** 3 * c[3][1],
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes the intersection between a cubic spline and a line segment.
|
||||
*/
|
||||
export function curveIntersectLineSegment<
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(c: Curve<Point>, l: LineSegment<Point>): Point[] {
|
||||
const line = (s: number) =>
|
||||
pointFrom<Point>(
|
||||
l[0][0] + s * (l[1][0] - l[0][0]),
|
||||
l[0][1] + s * (l[1][1] - l[0][1]),
|
||||
);
|
||||
|
||||
const initial_guesses: [number, number][] = [
|
||||
[0.5, 0],
|
||||
[0.2, 0],
|
||||
[0.8, 0],
|
||||
];
|
||||
|
||||
const calculate = ([t0, s0]: [number, number]) => {
|
||||
const solution = solve(
|
||||
(t: number, s: number) => {
|
||||
const bezier_point = bezierEquation(c, t);
|
||||
const line_point = line(s);
|
||||
|
||||
return [
|
||||
bezier_point[0] - line_point[0],
|
||||
bezier_point[1] - line_point[1],
|
||||
];
|
||||
},
|
||||
t0,
|
||||
s0,
|
||||
);
|
||||
|
||||
if (!solution) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [t, s] = solution;
|
||||
|
||||
if (t < 0 || t > 1 || s < 0 || s > 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bezierEquation(c, t);
|
||||
};
|
||||
|
||||
let solution = calculate(initial_guesses[0]);
|
||||
if (solution) {
|
||||
return [solution];
|
||||
}
|
||||
|
||||
solution = calculate(initial_guesses[1]);
|
||||
if (solution) {
|
||||
return [solution];
|
||||
}
|
||||
|
||||
solution = calculate(initial_guesses[2]);
|
||||
if (solution) {
|
||||
return [solution];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the closest point on the Bezier curve from another point
|
||||
*
|
||||
* @param x
|
||||
* @param y
|
||||
* @param P0
|
||||
* @param P1
|
||||
* @param P2
|
||||
* @param P3
|
||||
* @param tolerance
|
||||
* @param maxLevel
|
||||
* @returns
|
||||
*/
|
||||
export function curveClosestPoint<Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
p: Point,
|
||||
tolerance: number = 1e-3,
|
||||
): Point | null {
|
||||
const localMinimum = (
|
||||
min: number,
|
||||
max: number,
|
||||
f: (t: number) => number,
|
||||
e: number = tolerance,
|
||||
) => {
|
||||
let m = min;
|
||||
let n = max;
|
||||
let k;
|
||||
|
||||
while (n - m > e) {
|
||||
k = (n + m) / 2;
|
||||
if (f(k - e) < f(k + e)) {
|
||||
n = k;
|
||||
} else {
|
||||
m = k;
|
||||
}
|
||||
}
|
||||
|
||||
return k;
|
||||
};
|
||||
|
||||
const maxSteps = 30;
|
||||
let closestStep = 0;
|
||||
for (let min = Infinity, step = 0; step < maxSteps; step++) {
|
||||
const d = pointDistance(p, bezierEquation(c, step / maxSteps));
|
||||
if (d < min) {
|
||||
min = d;
|
||||
closestStep = step;
|
||||
}
|
||||
}
|
||||
|
||||
const t0 = Math.max((closestStep - 1) / maxSteps, 0);
|
||||
const t1 = Math.min((closestStep + 1) / maxSteps, 1);
|
||||
const solution = localMinimum(t0, t1, (t) =>
|
||||
pointDistance(p, bezierEquation(c, t)),
|
||||
);
|
||||
|
||||
if (!solution) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bezierEquation(c, solution);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the distance between a point and the closest point on the
|
||||
* Bezier curve.
|
||||
*
|
||||
* @param c The curve to test
|
||||
* @param p The point to measure from
|
||||
*/
|
||||
export function curvePointDistance<Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
p: Point,
|
||||
) {
|
||||
const closest = curveClosestPoint(c, p);
|
||||
|
||||
if (!closest) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return pointDistance(p, closest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the parameter is a Curve
|
||||
*/
|
||||
export function isCurve<P extends GlobalPoint | LocalPoint>(
|
||||
v: unknown,
|
||||
): v is Curve<P> {
|
||||
return (
|
||||
Array.isArray(v) &&
|
||||
v.length === 4 &&
|
||||
isPoint(v[0]) &&
|
||||
isPoint(v[1]) &&
|
||||
isPoint(v[2]) &&
|
||||
isPoint(v[3])
|
||||
);
|
||||
}
|
||||
|
||||
export function curveTangent<Point extends GlobalPoint | LocalPoint>(
|
||||
[p0, p1, p2, p3]: Curve<Point>,
|
||||
t: number,
|
||||
) {
|
||||
return vector(
|
||||
-3 * (1 - t) * (1 - t) * p0[0] +
|
||||
3 * (1 - t) * (1 - t) * p1[0] -
|
||||
6 * t * (1 - t) * p1[0] -
|
||||
3 * t * t * p2[0] +
|
||||
6 * t * (1 - t) * p2[0] +
|
||||
3 * t * t * p3[0],
|
||||
-3 * (1 - t) * (1 - t) * p0[1] +
|
||||
3 * (1 - t) * (1 - t) * p1[1] -
|
||||
6 * t * (1 - t) * p1[1] -
|
||||
3 * t * t * p2[1] +
|
||||
6 * t * (1 - t) * p2[1] +
|
||||
3 * t * t * p3[1],
|
||||
);
|
||||
}
|
||||
|
||||
export function curveCatmullRomQuadraticApproxPoints(
|
||||
points: GlobalPoint[],
|
||||
tension = 0.5,
|
||||
) {
|
||||
if (points.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointSets: [GlobalPoint, GlobalPoint][] = [];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
const cpX = p1[0] + ((p2[0] - p0[0]) * tension) / 2;
|
||||
const cpY = p1[1] + ((p2[1] - p0[1]) * tension) / 2;
|
||||
|
||||
pointSets.push([
|
||||
pointFrom<GlobalPoint>(cpX, cpY),
|
||||
pointFrom<GlobalPoint>(p2[0], p2[1]),
|
||||
]);
|
||||
}
|
||||
|
||||
return pointSets;
|
||||
}
|
||||
|
||||
export function curveCatmullRomCubicApproxPoints<
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(points: Point[], tension = 0.5) {
|
||||
if (points.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointSets: Curve<Point>[] = [];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2];
|
||||
const tangent1 = [(p2[0] - p0[0]) * tension, (p2[1] - p0[1]) * tension];
|
||||
const tangent2 = [(p3[0] - p1[0]) * tension, (p3[1] - p1[1]) * tension];
|
||||
const cp1x = p1[0] + tangent1[0] / 3;
|
||||
const cp1y = p1[1] + tangent1[1] / 3;
|
||||
const cp2x = p2[0] - tangent2[0] / 3;
|
||||
const cp2y = p2[1] - tangent2[1] / 3;
|
||||
|
||||
pointSets.push(
|
||||
curve(
|
||||
pointFrom(p1[0], p1[1]),
|
||||
pointFrom(cp1x, cp1y),
|
||||
pointFrom(cp2x, cp2y),
|
||||
pointFrom(p2[0], p2[1]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return pointSets;
|
||||
}
|
||||
|
||||
export function curveOffsetPoints(
|
||||
[p0, p1, p2, p3]: Curve<GlobalPoint>,
|
||||
offset: number,
|
||||
steps = 50,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const c = curve(p0, p1, p2, p3);
|
||||
const point = bezierEquation(c, t);
|
||||
const tangent = vectorNormalize(curveTangent(c, t));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offset), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
||||
export function offsetPointsForQuadraticBezier(
|
||||
p0: GlobalPoint,
|
||||
p1: GlobalPoint,
|
||||
p2: GlobalPoint,
|
||||
offsetDist: number,
|
||||
steps = 50,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const t1 = 1 - t;
|
||||
const point = pointFrom<GlobalPoint>(
|
||||
t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0],
|
||||
t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1],
|
||||
);
|
||||
const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]);
|
||||
const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]);
|
||||
const tangent = vectorNormalize(vector(tangentX, tangentY));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation based on Legendre-Gauss quadrature for more accurate arc
|
||||
* length calculation.
|
||||
*
|
||||
* Reference: https://pomax.github.io/bezierinfo/#arclength
|
||||
*
|
||||
* @param c The curve to calculate the length of
|
||||
* @returns The approximated length of the curve
|
||||
*/
|
||||
export function curveLength<P extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<P>,
|
||||
): number {
|
||||
const z2 = 0.5;
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const t = z2 * LegendreGaussN24TValues[i] + z2;
|
||||
const derivativeVector = curveTangent(c, t);
|
||||
const magnitude = Math.sqrt(
|
||||
derivativeVector[0] * derivativeVector[0] +
|
||||
derivativeVector[1] * derivativeVector[1],
|
||||
);
|
||||
sum += LegendreGaussN24CValues[i] * magnitude;
|
||||
}
|
||||
|
||||
return z2 * sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the curve length from t=0 to t=parameter using the same
|
||||
* Legendre-Gauss quadrature method used in curveLength
|
||||
*
|
||||
* @param c The curve to calculate the partial length for
|
||||
* @param t The parameter value (0 to 1) to calculate length up to
|
||||
* @returns The length of the curve from beginning to parameter t
|
||||
*/
|
||||
export function curveLengthAtParameter<P extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<P>,
|
||||
t: number,
|
||||
): number {
|
||||
if (t <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (t >= 1) {
|
||||
return curveLength(c);
|
||||
}
|
||||
|
||||
// Scale and shift the integration interval from [0,t] to [-1,1]
|
||||
// which is what the Legendre-Gauss quadrature expects
|
||||
const z1 = t / 2;
|
||||
const z2 = t / 2;
|
||||
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const parameter = z1 * LegendreGaussN24TValues[i] + z2;
|
||||
const derivativeVector = curveTangent(c, parameter);
|
||||
const magnitude = Math.sqrt(
|
||||
derivativeVector[0] * derivativeVector[0] +
|
||||
derivativeVector[1] * derivativeVector[1],
|
||||
);
|
||||
sum += LegendreGaussN24CValues[i] * magnitude;
|
||||
}
|
||||
|
||||
return z1 * sum; // Scale the result back to the original interval
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the point at a specific percentage of a curve's total length
|
||||
* using binary search for improved efficiency and accuracy.
|
||||
*
|
||||
* @param c The curve to calculate point on
|
||||
* @param percent A value between 0 and 1 representing the percentage of the curve's length
|
||||
* @returns The point at the specified percentage of curve length
|
||||
*/
|
||||
export function curvePointAtLength<P extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<P>,
|
||||
percent: number,
|
||||
): P {
|
||||
if (percent <= 0) {
|
||||
return bezierEquation(c, 0);
|
||||
}
|
||||
|
||||
if (percent >= 1) {
|
||||
return bezierEquation(c, 1);
|
||||
}
|
||||
|
||||
const totalLength = curveLength(c);
|
||||
const targetLength = totalLength * percent;
|
||||
|
||||
// Binary search to find parameter t where length at t equals target length
|
||||
let tMin = 0;
|
||||
let tMax = 1;
|
||||
let t = percent; // Start with a reasonable guess (t = percent)
|
||||
let currentLength = 0;
|
||||
|
||||
// Tolerance for length comparison and iteration limit to avoid infinite loops
|
||||
const tolerance = totalLength * 0.0001;
|
||||
const maxIterations = 20;
|
||||
|
||||
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
||||
currentLength = curveLengthAtParameter(c, t);
|
||||
const error = Math.abs(currentLength - targetLength);
|
||||
|
||||
if (error < tolerance) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentLength < targetLength) {
|
||||
tMin = t;
|
||||
} else {
|
||||
tMax = t;
|
||||
}
|
||||
|
||||
t = (tMin + tMax) / 2;
|
||||
}
|
||||
|
||||
return bezierEquation(c, t);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import {
|
||||
pointFrom,
|
||||
pointDistance,
|
||||
pointFromVector,
|
||||
pointsEqual,
|
||||
} from "./point";
|
||||
import { PRECISION } from "./utils";
|
||||
import {
|
||||
vector,
|
||||
vectorAdd,
|
||||
vectorDot,
|
||||
vectorFromPoint,
|
||||
vectorScale,
|
||||
} from "./vector";
|
||||
|
||||
import type {
|
||||
Ellipse,
|
||||
GlobalPoint,
|
||||
Line,
|
||||
LineSegment,
|
||||
LocalPoint,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Construct an Ellipse object from the parameters
|
||||
*
|
||||
* @param center The center of the ellipse
|
||||
* @param angle The slanting of the ellipse in radians
|
||||
* @param halfWidth Half of the width of a non-slanted version of the ellipse
|
||||
* @param halfHeight Half of the height of a non-slanted version of the ellipse
|
||||
* @returns The constructed Ellipse object
|
||||
*/
|
||||
export function ellipse<Point extends GlobalPoint | LocalPoint>(
|
||||
center: Point,
|
||||
halfWidth: number,
|
||||
halfHeight: number,
|
||||
): Ellipse<Point> {
|
||||
return {
|
||||
center,
|
||||
halfWidth,
|
||||
halfHeight,
|
||||
} as Ellipse<Point>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a point is inside or on the ellipse outline
|
||||
*
|
||||
* @param p The point to test
|
||||
* @param ellipse The ellipse to compare against
|
||||
* @returns TRUE if the point is inside or on the outline of the ellipse
|
||||
*/
|
||||
export const ellipseIncludesPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||
p: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
) => {
|
||||
const { center, halfWidth, halfHeight } = ellipse;
|
||||
const normalizedX = (p[0] - center[0]) / halfWidth;
|
||||
const normalizedY = (p[1] - center[1]) / halfHeight;
|
||||
|
||||
return normalizedX * normalizedX + normalizedY * normalizedY <= 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests whether a point lies on the outline of the ellipse within a given
|
||||
* tolerance
|
||||
*
|
||||
* @param point The point to test
|
||||
* @param ellipse The ellipse to compare against
|
||||
* @param threshold The distance to consider a point close enough to be "on" the outline
|
||||
* @returns TRUE if the point is on the ellise outline
|
||||
*/
|
||||
export const ellipseTouchesPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||
point: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
threshold = PRECISION,
|
||||
) => {
|
||||
return ellipseDistanceFromPoint(point, ellipse) <= threshold;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the shortest euclidean distance from a point to the
|
||||
* outline of the ellipse
|
||||
*
|
||||
* @param p The point to consider
|
||||
* @param ellipse The ellipse to calculate the distance to
|
||||
* @returns The eucledian distance
|
||||
*/
|
||||
export const ellipseDistanceFromPoint = <
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(
|
||||
p: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
): number => {
|
||||
const { halfWidth, halfHeight, center } = ellipse;
|
||||
const a = halfWidth;
|
||||
const b = halfHeight;
|
||||
const translatedPoint = vectorAdd(
|
||||
vectorFromPoint(p),
|
||||
vectorScale(vectorFromPoint(center), -1),
|
||||
);
|
||||
|
||||
const px = Math.abs(translatedPoint[0]);
|
||||
const py = Math.abs(translatedPoint[1]);
|
||||
|
||||
let tx = 0.707;
|
||||
let ty = 0.707;
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const x = a * tx;
|
||||
const y = b * ty;
|
||||
|
||||
const ex = ((a * a - b * b) * tx ** 3) / a;
|
||||
const ey = ((b * b - a * a) * ty ** 3) / b;
|
||||
|
||||
const rx = x - ex;
|
||||
const ry = y - ey;
|
||||
|
||||
const qx = px - ex;
|
||||
const qy = py - ey;
|
||||
|
||||
const r = Math.hypot(ry, rx);
|
||||
const q = Math.hypot(qy, qx);
|
||||
|
||||
tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
|
||||
ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
|
||||
const t = Math.hypot(ty, tx);
|
||||
tx /= t;
|
||||
ty /= t;
|
||||
}
|
||||
|
||||
const [minX, minY] = [
|
||||
a * tx * Math.sign(translatedPoint[0]),
|
||||
b * ty * Math.sign(translatedPoint[1]),
|
||||
];
|
||||
|
||||
return pointDistance(pointFromVector(translatedPoint), pointFrom(minX, minY));
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate a maximum of two intercept points for a line going throug an
|
||||
* ellipse.
|
||||
*/
|
||||
export function ellipseSegmentInterceptPoints<
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(e: Readonly<Ellipse<Point>>, s: Readonly<LineSegment<Point>>): Point[] {
|
||||
const rx = e.halfWidth;
|
||||
const ry = e.halfHeight;
|
||||
|
||||
const dir = vectorFromPoint(s[1], s[0]);
|
||||
const diff = vector(s[0][0] - e.center[0], s[0][1] - e.center[1]);
|
||||
const mDir = vector(dir[0] / (rx * rx), dir[1] / (ry * ry));
|
||||
const mDiff = vector(diff[0] / (rx * rx), diff[1] / (ry * ry));
|
||||
|
||||
const a = vectorDot(dir, mDir);
|
||||
const b = vectorDot(dir, mDiff);
|
||||
const c = vectorDot(diff, mDiff) - 1.0;
|
||||
const d = b * b - a * c;
|
||||
|
||||
const intersections: Point[] = [];
|
||||
|
||||
if (d > 0) {
|
||||
const t_a = (-b - Math.sqrt(d)) / a;
|
||||
const t_b = (-b + Math.sqrt(d)) / a;
|
||||
|
||||
if (0 <= t_a && t_a <= 1) {
|
||||
intersections.push(
|
||||
pointFrom(
|
||||
s[0][0] + (s[1][0] - s[0][0]) * t_a,
|
||||
s[0][1] + (s[1][1] - s[0][1]) * t_a,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (0 <= t_b && t_b <= 1) {
|
||||
intersections.push(
|
||||
pointFrom(
|
||||
s[0][0] + (s[1][0] - s[0][0]) * t_b,
|
||||
s[0][1] + (s[1][1] - s[0][1]) * t_b,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (d === 0) {
|
||||
const t = -b / a;
|
||||
if (0 <= t && t <= 1) {
|
||||
intersections.push(
|
||||
pointFrom(
|
||||
s[0][0] + (s[1][0] - s[0][0]) * t,
|
||||
s[0][1] + (s[1][1] - s[0][1]) * t,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
export function ellipseLineIntersectionPoints<
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(
|
||||
{ center, halfWidth, halfHeight }: Ellipse<Point>,
|
||||
[g, h]: Line<Point>,
|
||||
): Point[] {
|
||||
const [cx, cy] = center;
|
||||
const x1 = g[0] - cx;
|
||||
const y1 = g[1] - cy;
|
||||
const x2 = h[0] - cx;
|
||||
const y2 = h[1] - cy;
|
||||
const a =
|
||||
Math.pow(x2 - x1, 2) / Math.pow(halfWidth, 2) +
|
||||
Math.pow(y2 - y1, 2) / Math.pow(halfHeight, 2);
|
||||
const b =
|
||||
2 *
|
||||
((x1 * (x2 - x1)) / Math.pow(halfWidth, 2) +
|
||||
(y1 * (y2 - y1)) / Math.pow(halfHeight, 2));
|
||||
const c =
|
||||
Math.pow(x1, 2) / Math.pow(halfWidth, 2) +
|
||||
Math.pow(y1, 2) / Math.pow(halfHeight, 2) -
|
||||
1;
|
||||
const t1 = (-b + Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a);
|
||||
const t2 = (-b - Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a);
|
||||
const candidates = [
|
||||
pointFrom<Point>(x1 + t1 * (x2 - x1) + cx, y1 + t1 * (y2 - y1) + cy),
|
||||
pointFrom<Point>(x1 + t2 * (x2 - x1) + cx, y1 + t2 * (y2 - y1) + cy),
|
||||
].filter((p) => !isNaN(p[0]) && !isNaN(p[1]));
|
||||
|
||||
if (candidates.length === 2 && pointsEqual(candidates[0], candidates[1])) {
|
||||
return [candidates[0]];
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export * from "./angle";
|
||||
export * from "./curve";
|
||||
export * from "./ellipse";
|
||||
export * from "./line";
|
||||
export * from "./point";
|
||||
export * from "./polygon";
|
||||
export * from "./range";
|
||||
export * from "./rectangle";
|
||||
export * from "./segment";
|
||||
export * from "./triangle";
|
||||
export * from "./types";
|
||||
export * from "./vector";
|
||||
export * from "./utils";
|
||||
@@ -0,0 +1,39 @@
|
||||
import { pointFrom } from "./point";
|
||||
|
||||
import type { GlobalPoint, Line, LocalPoint } 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>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the intersection point (unless the lines are parallel) of two
|
||||
* lines
|
||||
*
|
||||
* @param a
|
||||
* @param b
|
||||
* @returns
|
||||
*/
|
||||
export function linesIntersectAt<Point extends GlobalPoint | LocalPoint>(
|
||||
a: Line<Point>,
|
||||
b: Line<Point>,
|
||||
): Point | null {
|
||||
const A1 = a[1][1] - a[0][1];
|
||||
const B1 = a[0][0] - a[1][0];
|
||||
const A2 = b[1][1] - b[0][1];
|
||||
const B2 = b[0][0] - b[1][0];
|
||||
const D = A1 * B2 - A2 * B1;
|
||||
if (D !== 0) {
|
||||
const C1 = A1 * a[0][0] + B1 * a[0][1];
|
||||
const C2 = A2 * b[0][0] + B2 * b[0][1];
|
||||
return pointFrom<Point>((C1 * B2 - C2 * B1) / D, (A1 * C2 - A2 * C1) / D);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { degreesToRadians } from "./angle";
|
||||
import { PRECISION } from "./utils";
|
||||
import { vectorFromPoint, vectorScale } from "./vector";
|
||||
|
||||
import type {
|
||||
LocalPoint,
|
||||
GlobalPoint,
|
||||
Radians,
|
||||
Degrees,
|
||||
Vector,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* 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 pointFrom<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
|
||||
? pointFrom<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,
|
||||
offset: P = pointFrom(0, 0),
|
||||
): P {
|
||||
return pointFrom<P>(offset[0] + v[0], offset[1] + v[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
tolerance: number = PRECISION,
|
||||
): boolean {
|
||||
const abs = Math.abs;
|
||||
return abs(a[0] - b[0]) < tolerance && abs(a[1] - b[1]) < tolerance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate 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 pointFrom(
|
||||
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
||||
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate 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 pointFrom(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 pointFrom((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
const xDiff = b[0] - a[0];
|
||||
const yDiff = b[1] - a[1];
|
||||
|
||||
return xDiff * xDiff + yDiff * yDiff;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,101 @@
|
||||
import { pointsEqual } from "./point";
|
||||
import { lineSegment, pointOnLineSegment } from "./segment";
|
||||
import { PRECISION } from "./utils";
|
||||
|
||||
import type { GlobalPoint, LocalPoint, Polygon } from "./types";
|
||||
|
||||
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 polygonIncludesPointNonZero = <Point extends [number, number]>(
|
||||
point: Point,
|
||||
polygon: Point[],
|
||||
): boolean => {
|
||||
const [x, y] = point;
|
||||
let windingNumber = 0;
|
||||
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
const j = (i + 1) % polygon.length;
|
||||
const [xi, yi] = polygon[i];
|
||||
const [xj, yj] = polygon[j];
|
||||
|
||||
if (yi <= y) {
|
||||
if (yj > y) {
|
||||
if ((xj - xi) * (y - yi) - (x - xi) * (yj - yi) > 0) {
|
||||
windingNumber++;
|
||||
}
|
||||
}
|
||||
} else if (yj <= y) {
|
||||
if ((xj - xi) * (y - yi) - (x - xi) * (yj - yi) < 0) {
|
||||
windingNumber--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return windingNumber !== 0;
|
||||
};
|
||||
|
||||
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,83 @@
|
||||
import { toBrandedType } from "@excalidraw/common";
|
||||
|
||||
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,39 @@
|
||||
import { pointFrom } from "./point";
|
||||
import { lineSegment, lineSegmentIntersectionPoints } from "./segment";
|
||||
|
||||
import type { GlobalPoint, LineSegment, LocalPoint, Rectangle } from "./types";
|
||||
|
||||
export function rectangle<P extends GlobalPoint | LocalPoint>(
|
||||
topLeft: P,
|
||||
bottomRight: P,
|
||||
): Rectangle<P> {
|
||||
return [topLeft, bottomRight] as Rectangle<P>;
|
||||
}
|
||||
|
||||
export function rectangleFromNumberSequence<
|
||||
Point extends LocalPoint | GlobalPoint,
|
||||
>(minX: number, minY: number, maxX: number, maxY: number) {
|
||||
return rectangle(pointFrom<Point>(minX, minY), pointFrom<Point>(maxX, maxY));
|
||||
}
|
||||
|
||||
export function rectangleIntersectLineSegment<
|
||||
Point extends LocalPoint | GlobalPoint,
|
||||
>(r: Rectangle<Point>, l: LineSegment<Point>): Point[] {
|
||||
return [
|
||||
lineSegment(r[0], pointFrom(r[1][0], r[0][1])),
|
||||
lineSegment(pointFrom(r[1][0], r[0][1]), r[1]),
|
||||
lineSegment(r[1], pointFrom(r[0][0], r[1][1])),
|
||||
lineSegment(pointFrom(r[0][0], r[1][1]), r[0]),
|
||||
]
|
||||
.map((s) => lineSegmentIntersectionPoints(l, s))
|
||||
.filter((i): i is Point => !!i);
|
||||
}
|
||||
|
||||
export function rectangleIntersectRectangle<
|
||||
Point extends LocalPoint | GlobalPoint,
|
||||
>(rectangle1: Rectangle<Point>, rectangle2: Rectangle<Point>): boolean {
|
||||
const [[minX1, minY1], [maxX1, maxY1]] = rectangle1;
|
||||
const [[minX2, minY2], [maxX2, maxY2]] = rectangle2;
|
||||
|
||||
return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2;
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { line, linesIntersectAt } from "./line";
|
||||
import {
|
||||
isPoint,
|
||||
pointCenter,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
} from "./point";
|
||||
import { PRECISION } from "./utils";
|
||||
import {
|
||||
vectorAdd,
|
||||
vectorCross,
|
||||
vectorFromPoint,
|
||||
vectorScale,
|
||||
vectorSubtract,
|
||||
} from "./vector";
|
||||
|
||||
import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types";
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the intersection point of a segment and a line
|
||||
*
|
||||
* @param l
|
||||
* @param s
|
||||
* @returns
|
||||
*/
|
||||
export function lineSegmentIntersectionPoints<
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(
|
||||
l: LineSegment<Point>,
|
||||
s: LineSegment<Point>,
|
||||
threshold?: number,
|
||||
): Point | null {
|
||||
const candidate = linesIntersectAt(line(l[0], l[1]), line(s[0], s[1]));
|
||||
|
||||
if (
|
||||
!candidate ||
|
||||
!pointOnLineSegment(candidate, s, threshold) ||
|
||||
!pointOnLineSegment(candidate, l, threshold)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
@@ -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,142 @@
|
||||
//
|
||||
// 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";
|
||||
};
|
||||
|
||||
/**
|
||||
* A rectangular shape represented by 4 points at its corners
|
||||
*/
|
||||
export type Rectangle<P extends GlobalPoint | LocalPoint> = [a: P, b: P] & {
|
||||
_brand: "excalimath__rectangle";
|
||||
};
|
||||
|
||||
//
|
||||
// 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,
|
||||
];
|
||||
|
||||
/**
|
||||
An ellipse is specified by its center, angle, and its major and minor axes
|
||||
but for the sake of simplicity, we've used halfWidth and halfHeight instead
|
||||
in replace of semi major and semi minor axes
|
||||
*/
|
||||
export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
|
||||
center: Point;
|
||||
halfWidth: number;
|
||||
halfHeight: number;
|
||||
} & {
|
||||
_brand: "excalimath_ellipse";
|
||||
};
|
||||
|
||||
export type ElementsSegmentsMap = Map<string, LineSegment<GlobalPoint>[]>;
|
||||
@@ -0,0 +1,33 @@
|
||||
export const PRECISION = 10e-5;
|
||||
|
||||
export const clamp = (value: number, min: number, max: number) => {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
};
|
||||
|
||||
export const round = (
|
||||
value: number,
|
||||
precision: number,
|
||||
func: "round" | "floor" | "ceil" = "round",
|
||||
) => {
|
||||
const multiplier = Math.pow(10, precision);
|
||||
|
||||
return Math[func]((value + Number.EPSILON) * multiplier) / multiplier;
|
||||
};
|
||||
|
||||
export const roundToStep = (
|
||||
value: number,
|
||||
step: number,
|
||||
func: "round" | "floor" | "ceil" = "round",
|
||||
): number => {
|
||||
const factor = 1 / step;
|
||||
return Math[func](value * factor) / factor;
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
export const isCloseTo = (a: number, b: number, precision = PRECISION) =>
|
||||
Math.abs(a - b) < precision;
|
||||
@@ -0,0 +1,160 @@
|
||||
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
|
||||
* @param threshold The threshold to consider the vector as 'undefined'
|
||||
* @param defaultValue The default value to return if the vector is 'undefined'
|
||||
* @returns The created vector from the point and the origin or default
|
||||
*/
|
||||
export function vectorFromPoint<Point extends GlobalPoint | LocalPoint>(
|
||||
p: Point,
|
||||
origin: Point = [0, 0] as Point,
|
||||
threshold?: number,
|
||||
defaultValue: Vector = [0, 1] as Vector,
|
||||
): Vector {
|
||||
const vec = vector(p[0] - origin[0], p[1] - origin[1]);
|
||||
|
||||
if (threshold && vectorMagnitudeSq(vec) < threshold * threshold) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if (m === 0) {
|
||||
return vector(0, 0);
|
||||
}
|
||||
|
||||
return vector(v[0] / m, v[1] / m);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the right-hand normal of the vector.
|
||||
*/
|
||||
export const vectorNormal = (v: Vector): Vector => vector(v[1], -v[0]);
|
||||
Reference in New Issue
Block a user