UNPKG

@jakebeamish/penplotting

Version:

A JavaScript framework for making SVG files for penplotters.

302 lines (273 loc) 7.16 kB
import { lerp } from "./utils.js"; /** * Class representing a Vector. */ export class Vector { /** * Create a vector from coordinates. * @param {number} [x=0] - x * @param {number} [y=0] - y * @param {number} [z=0] - z */ constructor(x = 0, y = 0, z = 0) { if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number") { throw new TypeError("Vector components must be numbers."); } this.x = x; this.y = y; this.z = z; } /** * Create a vector from an array. * @param {number[]} array - Vector components as numbers in an array `[x, y, z]`. * @returns {Vector} */ static fromArray(array) { return new Vector(array[0], array[1], array[2] || 0); } /** * Create an array from the vector's components. * @returns {Array} an array `[x, y, z]`. */ toArray() { return [this.x, this.y, this.z]; } /** * Create a 2D vector from an angle. * @param {number} angle - The angle of the vector in radians. * @param {number} [magnitude=1] - The magnitude of the vector. * @returns {Vector} */ static fromAngle(angle, magnitude = 1) { return new Vector(Math.cos(angle), Math.sin(angle)).multiply(magnitude); } /** * Add a vector to this vector. * @param {Vector} vector * @returns {Vector} */ add(vector) { this.x += vector.x; this.y += vector.y; this.z += vector.z; return this; } /** * Check if this vector is equivalent to another vector. * @param {Vector} vector * @returns {boolean} */ equals(vector) { return this.x === vector.x && this.y === vector.y && this.z === vector.z; } /** * Check if this vector is a point on a {@link Line}. * @param {Line} line * @returns {boolean} */ isOnLine(line) { if ( (line.b.x - line.a.x) * (this.y - line.a.y) !== (line.b.y - line.a.y) * (this.x - line.a.x) ) { return false; } return ( Math.min(line.a.x, line.b.x) <= this.x && this.x <= Math.max(line.a.x, line.b.x) && Math.min(line.a.y, line.b.y) <= this.y && this.y <= Math.max(line.a.y, line.b.y) ); } /** * Subtract a vector from this vector. * @param {Vector} vector * @returns {Vector} */ subtract(vector) { this.x -= vector.x; this.y -= vector.y; this.z -= vector.z; return this; } /** * Multiply this vector by a scalar. * @param {number} scalar * @returns {Vector} */ multiply(scalar) { this.x *= scalar; this.y *= scalar; this.z *= scalar; return this; } /** * Add two vectors. * @param {Vector} v1 * @param {Vector} v2 * @returns {Vector} */ static add(v1, v2) { return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); } /** * Subtract two vectors. * @param {Vector} v1 * @param {Vector} v2 * @returns {Vector} */ static subtract(v1, v2) { return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); } /** * Calculate the dot product of two vectors. * @param {Vector} v1 * @param {Vector} v2 * @returns {number} */ static dot(v1, v2) { return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; } /** * Calculate the cross product of two 2D vectors. * @param {Vector} v1 * @param {Vector} v2 * @returns {number} */ static cross(v1, v2) { return v1.x * v2.y - v1.y * v2.x; } /** * Get the magnitude of this 2D vector. * @returns {number} The magnitude (Euclidean distance) of this vector. */ getMagnitude() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } /** * Get the magnitude squared of this vector. This is faster than [getMagnitude]{@link Vector#getMagnitude} because it avoids using the square root. * @returns {number} The magnitude squared of this vector. */ getMagnitudeSquared() { return this.x * this.x + this.y * this.y; } /** * Get the distance squared to another vector from this one. * @param {Vector} vector - The target vector. * @returns {number} The distance squared to the input vector. */ distanceSquared(vector) { const delta = Vector.subtract(this, vector); return delta.getMagnitudeSquared(); } /** * Set the magnitude of this vector. * @param {number} magnitude * @returns {Vector} */ setMagnitude(magnitude) { return this.normalize().multiply(magnitude); } /** * Calculate the distance to another vector from this vector * @param {Vector} vector * @returns {number} */ distance(vector) { const delta = Vector.subtract(this, vector); return delta.getMagnitude(); } /** * Calculate the distance between two vectors. * @param {Vector} v1 * @param {Vector} v2 * @returns {number} */ static distance(v1, v2) { const delta = Vector.subtract(v1, v2); return delta.getMagnitude(); } /** * Calculate the distance squared between two vectors. * @param {Vector} v1 * @param {Vector} v2 * @returns {number} */ static distanceSquared(v1, v2) { const delta = Vector.subtract(v1, v2); return delta.getMagnitudeSquared(); } /** * Rotate this 2D vector by a specified angle. * @param {number} angle - The angle to rotate the vector by, in radians. * @returns {Vector} This vector after rotation. */ rotate(angle) { const x = this.x * Math.cos(angle) - this.y * Math.sin(angle); const y = this.x * Math.sin(angle) + this.y * Math.cos(angle); this.x = x; this.y = y; return this; } /** * Normalize this vector by setting it's magnitude to 1. * @returns {Vector} */ normalize() { const mag = this.getMagnitude(); if (mag === 0) { throw new Error("Cannot normalize a zero vector."); } this.x /= mag; this.y /= mag; return this; } /** * Make a copy of this vector. * @returns {Vector} */ clone() { return new Vector(this.x, this.y, this.z); } /** * Get the 2D angle of this vector with respect to the positive x-axis. * @returns {number} Angle in radians */ getAngle() { return Math.atan2(this.y, this.x); } /** * Linear interpolation between vectors. * @param {Vector} v1 * @param {Vector} v2 * @param {number} amount * @returns {Vector} */ static lerp(v1, v2, amount) { return new Vector(lerp(v1.x, v2.x, amount), lerp(v1.y, v2.y, amount), lerp(v1.z, v2.z, amount)); } /** * Finds the `n` nearest neighbours to this vector, from a given array of vectors. * @param {Array<Vector>} array - An array of Vectors to check. * @param {number} n - The number of neighbours to find (must be > 0). * @returns {Array<Vector>} */ nearestNeighbour(array, n) { let neighbours = []; for (let i = 0; i < n; i++) { let record = Infinity; let nearest = null; for (let other of array) { if (other != this && !neighbours.includes(other)) { let dist = this.distance(other); if (dist < record) { nearest = other; record = dist; } } } neighbours.push(nearest); } return neighbours; } }