import { degreesToRadians, radiansToDegrees } from "./angle"; import type { LocalPoint, GlobalPoint, Radians, Degrees, Vector, } from "./types"; import { PRECISION } from "./utils"; import { vectorDot, 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 pointFrom( 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( numberArray: number[], ): Point | undefined { return numberArray.length === 2 ? pointFrom(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( 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

( v: Vector, offset: P = pointFrom(0, 0), ): P { return pointFrom

(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( 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( [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, ); } /** * 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: 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

(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

( 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

( 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: 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: 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]) ); }; /** * Calculates the perpendicular distance from a point to a line segment defined by two endpoints. * * If the segment is of zero length, the function returns the distance from the point to the start. * * @typeParam P - The point type, restricted to LocalPoint or GlobalPoint. * @param p - The point from which the perpendicular distance is measured. * @param start - The starting point of the line segment. * @param end - The ending point of the line segment. * @returns The perpendicular distance from point p to the line segment defined by start and end. */ export const perpendicularDistance =

( p: P, start: P, end: P): number => { const dx = end[0] - start[0]; const dy = end[1] - start[1]; if (dx === 0 && dy === 0) { return Math.hypot(p[0] - start[0], p[1] - start[1]); } // Equation of line distance const numerator = Math.abs(dy * p[0] - dx * p[1] + end[0] * start[1] - end[1] * start[0]); const denom = Math.hypot(dx, dy); return numerator / denom; } /** * Calculates the angle between three points in degrees. * The angle is calculated at the first point (p0) using the second (p1) and third (p2) points. * The angle is measured in degrees and is always positive. * The function uses the dot product and the arccosine function to calculate the angle. * The result is clamped to the range [-1, 1] to avoid precision errors. * @param p0 The first point used to form the angle. * @param p1 The vertex point where the angle is calculated. * @param p2 The second point used to form the angle. * @returns The angle in degrees between the three points. **/ export const angleBetween =

( p0: P, p1: P, p2: P, ): Degrees => { const v1 = vectorFromPoint(p0, p1); const v2 = vectorFromPoint(p1, p2); // dot and cross product const magnitude1 = Math.hypot(v1[0], v1[1]), magnitude2 = Math.hypot(v2[0], v2[1]); if (magnitude1 === 0 || magnitude2 === 0) return 0 as Degrees; const dot = vectorDot(v1, v2); let cos = dot / (magnitude1 * magnitude2); // Clamp cos to [-1,1] to avoid precision errors cos = Math.max(-1, Math.min(1, cos)); const rad = Math.acos(cos) as Radians; return radiansToDegrees(rad); }