diff options
Diffstat (limited to 'packages/math/point.ts')
| -rw-r--r-- | packages/math/point.ts | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/packages/math/point.ts b/packages/math/point.ts new file mode 100644 index 0000000..92df187 --- /dev/null +++ b/packages/math/point.ts @@ -0,0 +1,231 @@ +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 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, +): 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 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 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]) + ); +}; |
