@bitbybit-dev/base
Version:
Bit By Bit Developers Base CAD Library to Program Geometry
556 lines (555 loc) • 20.2 kB
JavaScript
/**
* Contains various methods for vector mathematics. Vector in bitbybit is simply an array, usually containing numbers.
* In 3D [x, y, z] form describes space, where y is the up vector.
* Because of this form Vector can be interchanged with Point, which also is an array in [x, y, z] form.
*/
export class Vector {
constructor(math, geometryHelper) {
this.math = math;
this.geometryHelper = geometryHelper;
}
/**
* Removes all duplicate vectors from the input array (keeps only unique vectors).
* Example: [[1,2,3], [4,5,6], [1,2,3], [7,8,9]] → [[1,2,3], [4,5,6], [7,8,9]]
* @param inputs Contains vectors and a tolerance value
* @returns Array of vectors without duplicates
* @group remove
* @shortname remove all duplicates
* @drawable false
*/
removeAllDuplicateVectors(inputs) {
return this.geometryHelper.removeAllDuplicateVectors(inputs.vectors, inputs.tolerance);
}
/**
* Removes consecutive duplicate vectors from the input array (only removes duplicates that appear next to each other).
* Example: [[1,2], [1,2], [3,4], [1,2]] → [[1,2], [3,4], [1,2]] (only removed consecutive duplicate)
* @param inputs Contains vectors and a tolerance value
* @returns Array of vectors without duplicates
* @group remove
* @shortname remove consecutive duplicates
* @drawable false
*/
removeConsecutiveDuplicateVectors(inputs) {
return this.geometryHelper.removeConsecutiveVectorDuplicates(inputs.vectors, inputs.checkFirstAndLast, inputs.tolerance);
}
/**
* Checks if two vectors are the same within a given tolerance (accounts for floating point precision).
* Example: [1,2,3] vs [1.0001,2.0001,3.0001] with tolerance 0.001 → true
* @param inputs Contains two vectors and a tolerance value
* @returns Boolean indicating if vectors are the same
* @group validate
* @shortname vectors the same
* @drawable false
*/
vectorsTheSame(inputs) {
return this.geometryHelper.vectorsTheSame(inputs.vec1, inputs.vec2, inputs.tolerance);
}
/**
* Measures the angle between two vectors in degrees (always returns positive angle 0-180°).
* Example: [1,0,0] and [0,1,0] → 90° (perpendicular vectors)
* @param inputs Contains two vectors represented as number arrays
* @group angles
* @shortname angle
* @returns Number in degrees
* @drawable false
*/
angleBetween(inputs) {
return this.math.radToDeg({
number: Math.acos(this.dot({ first: inputs.first, second: inputs.second }) / (this.norm({ vector: inputs.first }) * this.norm({ vector: inputs.second })))
});
}
/**
* Measures the normalized 2D angle between two vectors in degrees (considers direction, can be negative).
* Example: [1,0] to [0,1] → 90°, [0,1] to [1,0] → -90°
* @param inputs Contains two vectors represented as number arrays
* @returns Number in degrees
* @group angles
* @shortname angle normalized 2d
* @drawable false
*/
angleBetweenNormalized2d(inputs) {
const perpDot = inputs.first[0] * inputs.second[1] - inputs.first[1] * inputs.second[0];
return this.math.radToDeg({
number: Math.atan2(perpDot, this.dot({ first: inputs.first, second: inputs.second }))
});
}
/**
* Measures a positive angle between two vectors given the reference vector in degrees (always 0-360°).
* Example: converts negative signed angles to positive by adding 360° when needed
* @param inputs Contains information of two vectors and a reference vector
* @returns Number in degrees
* @group angles
* @shortname positive angle
* @drawable false
*/
positiveAngleBetween(inputs) {
const angle = this.signedAngleBetween(inputs);
return angle < 0 ? 360 + angle : angle;
}
/**
* Adds all vector xyz values together element-wise and creates a new vector.
* Example: [[1,2,3], [4,5,6], [7,8,9]] → [12,15,18] (sums each column)
* @param inputs Vectors to be added
* @returns New vector that has xyz values as sums of all the vectors
* @group sum
* @shortname add all
* @drawable false
*/
addAll(inputs) {
const res = [];
for (let i = 0; i < inputs.vectors[0].length; i++) {
let sum = 0;
for (const vector of inputs.vectors) {
sum += vector[i];
}
res.push(sum);
}
return res;
}
/**
* Adds two vectors together element-wise.
* Example: [1,2,3] + [4,5,6] → [5,7,9]
* @param inputs Two vectors to be added
* @returns Number array representing vector
* @group sum
* @shortname add
* @drawable false
*/
add(inputs) {
const res = [];
for (let i = 0; i < inputs.first.length; i++) {
res.push(inputs.first[i] + inputs.second[i]);
}
return res;
}
/**
* Checks if the boolean array contains only true values, returns false if there's a single false.
* Example: [true, true, true] → true, [true, false, true] → false
* @param inputs Vectors to be checked
* @returns Boolean indicating if vector contains only true values
* @group sum
* @shortname all
* @drawable false
*/
all(inputs) {
return inputs.vector.every(v => v);
}
/**
* Computes the cross product of two 3D vectors (perpendicular vector to both inputs).
* Example: [1,0,0] × [0,1,0] → [0,0,1] (right-hand rule)
* @param inputs Two vectors to be crossed
* @group base
* @shortname cross
* @returns Crossed vector
* @drawable false
*/
cross(inputs) {
const res = [];
res.push(inputs.first[1] * inputs.second[2] - inputs.first[2] * inputs.second[1]);
res.push(inputs.first[2] * inputs.second[0] - inputs.first[0] * inputs.second[2]);
res.push(inputs.first[0] * inputs.second[1] - inputs.first[1] * inputs.second[0]);
return res;
}
/**
* Calculates squared distance between two vectors (faster than distance, avoids sqrt).
* Example: [0,0,0] to [3,4,0] → 25 (distance 5 squared)
* @param inputs Two vectors
* @returns Number representing squared distance between two vectors
* @group distance
* @shortname dist squared
* @drawable false
*/
distSquared(inputs) {
let res = 0;
for (let i = 0; i < inputs.first.length; i++) {
res += Math.pow(inputs.first[i] - inputs.second[i], 2);
}
return res;
}
/**
* Calculates the Euclidean distance between two vectors.
* Example: [0,0,0] to [3,4,0] → 5, [1,1] to [4,5] → 5
* @param inputs Two vectors
* @returns Number representing distance between two vectors
* @group distance
* @shortname dist
* @drawable false
*/
dist(inputs) {
return Math.sqrt(this.distSquared(inputs));
}
/**
* Divides each element of the vector by a scalar value.
* Example: [10,20,30] ÷ 2 → [5,10,15]
* @param inputs Contains vector and a scalar
* @returns Vector that is a result of division by a scalar
* @group base
* @shortname div
* @drawable false
*/
div(inputs) {
const res = [];
for (let i = 0; i < inputs.vector.length; i++) {
res.push(inputs.vector[i] / inputs.scalar);
}
return res;
}
/**
* Computes the domain (range) between minimum and maximum values of the vector.
* Example: [1,3,5,9] → 8 (difference between last and first: 9-1)
* @param inputs Vector information
* @returns Number representing distance between two vectors
* @group base
* @shortname domain
* @drawable false
*/
domain(inputs) {
return inputs.vector[inputs.vector.length - 1] - inputs.vector[0];
}
/**
* Calculates the dot product between two vectors (measures similarity/projection).
* Example: [1,2,3] • [4,5,6] → 32 (1×4 + 2×5 + 3×6), perpendicular vectors → 0
* @param inputs Two vectors
* @returns Number representing dot product of the vector
* @group base
* @shortname dot
* @drawable false
*/
dot(inputs) {
let res = 0;
for (let i = 0; i < inputs.first.length; i++) {
res += inputs.first[i] * inputs.second[i];
}
return res;
}
/**
* Checks if each element in the vector is finite and returns a boolean array.
* Example: [1, 2, Infinity, 3] → [true, true, false, true]
* @param inputs Vector with possibly infinite values
* @returns Vector array that contains boolean values for each number in the input
* vector that identifies if value is finite (true) or infinite (false)
* @group validate
* @shortname finite
* @drawable false
*/
finite(inputs) {
return inputs.vector.map(v => isFinite(v));
}
/**
* Checks if the vector has zero length (all elements are zero).
* Example: [0,0,0] → true, [0,0,0.001] → false
* @param inputs Vector to be checked
* @returns Boolean that identifies if vector is zero length
* @group validate
* @shortname isZero
* @drawable false
*/
isZero(inputs) {
return this.norm({ vector: inputs.vector }) === 0;
}
/**
* Finds an interpolated vector between two vectors using a fraction (linear interpolation).
* Example: [0,0,0] to [10,10,10] at 0.5 → [5,5,5], fraction=0 → first, fraction=1 → second
* @param inputs Information for finding vector between two vectors using a fraction
* @returns Vector that is in between two vectors
* @group distance
* @shortname lerp
* @drawable false
*/
lerp(inputs) {
return this.add({
first: this.mul({ vector: inputs.first, scalar: inputs.fraction }),
second: this.mul({ vector: inputs.second, scalar: 1.0 - inputs.fraction })
});
}
/**
* Finds the maximum (largest) value in the vector.
* Example: [3, 7, 2, 9, 1] → 9
* @param inputs Vector to be checked
* @returns Largest number in the vector
* @group extract
* @shortname max
* @drawable false
*/
max(inputs) {
return Math.max(...inputs.vector);
}
/**
* Finds the minimum (smallest) value in the vector.
* Example: [3, 7, 2, 9, 1] → 1
* @param inputs Vector to be checked
* @returns Lowest number in the vector
* @group extract
* @shortname min
* @drawable false
*/
min(inputs) {
return Math.min(...inputs.vector);
}
/**
* Multiplies each element of the vector by a scalar value.
* Example: [2,3,4] × 5 → [10,15,20]
* @param inputs Vector with a scalar
* @returns Vector that results from multiplication
* @group base
* @shortname mul
* @drawable false
*/
mul(inputs) {
const res = [];
for (let i = 0; i < inputs.vector.length; i++) {
res.push(inputs.vector[i] * inputs.scalar);
}
return res;
}
/**
* Negates the vector (flips the sign of each element).
* Example: [5,-3,2] → [-5,3,-2]
* @param inputs Vector to negate
* @returns Negative vector
* @group base
* @shortname neg
* @drawable false
*/
neg(inputs) {
const res = [];
for (let i = 0; i < inputs.vector.length; i++) {
res.push(-inputs.vector[i]);
}
return res;
}
/**
* Computes the squared norm (squared magnitude/length) of the vector.
* Example: [3,4,0] → 25 (length 5 squared)
* @param inputs Vector for squared norm
* @returns Number that is squared norm
* @group base
* @shortname norm squared
* @drawable false
*/
normSquared(inputs) {
return this.dot({ first: inputs.vector, second: inputs.vector });
}
/**
* Calculates the norm (magnitude/length) of the vector.
* Example: [3,4,0] → 5, [1,0,0] → 1
* @param inputs Vector to compute the norm
* @returns Number that is norm of the vector
* @group base
* @shortname norm
* @drawable false
*/
norm(inputs) {
const norm2 = this.normSquared(inputs);
return norm2 !== 0.0 ? Math.sqrt(norm2) : norm2;
}
/**
* Normalizes the vector into a unit vector that has a length of 1 (maintains direction, scales magnitude to 1).
* Example: [3,4,0] → [0.6,0.8,0], [10,0,0] → [1,0,0]
* @param inputs Vector to normalize
* @returns Unit vector that has length of 1
* @group base
* @shortname normalized
* @drawable false
*/
normalized(inputs) {
const len = this.length({ vector: inputs.vector });
if (len <= 1e-8) {
return undefined;
}
return this.div({ scalar: this.norm(inputs), vector: inputs.vector });
}
/**
* Finds a point on a ray at a given distance from the origin along the direction vector.
* Example: Point [0,0,0] + direction [1,0,0] at distance 5 → [5,0,0]
* @param inputs Provide a point, vector and a distance for finding a point
* @returns Vector representing point on the ray
* @group base
* @shortname on ray
* @drawable false
*/
onRay(inputs) {
return this.add({ first: inputs.point, second: this.mul({ vector: inputs.vector, scalar: inputs.distance }) });
}
/**
* Creates a 3D vector from x, y, z coordinates.
* Example: x=1, y=2, z=3 → [1,2,3]
* @param inputs Vector coordinates
* @returns Create a vector of xyz values
* @group create
* @shortname vector XYZ
* @drawable true
*/
vectorXYZ(inputs) {
return [inputs.x, inputs.y, inputs.z];
}
/**
* Creates a 2D vector from x, y coordinates.
* Example: x=3, y=4 → [3,4]
* @param inputs Vector coordinates
* @returns Create a vector of xy values
* @group create
* @shortname vector XY
* @drawable true
*/
vectorXY(inputs) {
return [inputs.x, inputs.y];
}
/**
* Creates a vector of integers from 0 to max (exclusive).
* Example: max=5 → [0,1,2,3,4], max=3 → [0,1,2]
* @param inputs Max value for the range
* @returns Vector containing items from 0 to max
* @group create
* @shortname range
* @drawable false
*/
range(inputs) {
const res = [];
for (let i = 0; i < inputs.max; i++) {
res.push(i);
}
return res;
}
/**
* Computes signed angle between two vectors using a reference vector (determines rotation direction).
* Example: Returns positive or negative angle depending on rotation direction relative to reference
* @param inputs Contains information of two vectors and a reference vector
* @returns Signed angle in degrees
* @group angles
* @shortname signed angle
* @drawable false
*/
signedAngleBetween(inputs) {
const nab = this.cross({ first: inputs.first, second: inputs.second });
const al = this.norm({ vector: inputs.first });
const bl = this.norm({ vector: inputs.second });
const abl = al * bl;
const adb = this.dot({ first: inputs.first, second: inputs.second });
const sina = this.norm({ vector: nab }) / abl;
const cosa = adb / abl;
const w = Math.atan2(sina, cosa);
const s = this.dot({ first: inputs.reference, second: nab });
const res = s > 0.0 ? w : 2 * Math.PI - w;
return this.math.radToDeg({ number: res });
}
/**
* Creates a vector containing numbers from min to max at a given step increment.
* Example: min=0, max=10, step=2 → [0,2,4,6,8,10]
* @param inputs Span information containing min, max and step values
* @returns Vector containing number between min, max and increasing at a given step
* @group create
* @shortname span
* @drawable false
*/
span(inputs) {
const res = [];
for (let i = inputs.min; i <= inputs.max; i += inputs.step) {
res.push(i);
}
return res;
}
/**
* Creates a vector with numbers from min to max using an easing function for non-linear distribution.
* Example: min=0, max=100, nrItems=5, ease='easeInQuad' → creates accelerating intervals
* @param inputs Span information containing min, max and ease function
* @returns Vector containing numbers between min, max and increasing in non-linear steps defined by nr of items in the vector and type
* @group create
* @shortname span ease items
* @drawable false
*/
spanEaseItems(inputs) {
const res = [];
for (let i = 0; i < inputs.nrItems; i++) {
const x = i * 1 / (inputs.nrItems - 1);
res.push(this.math.ease({ x: x, ease: inputs.ease, min: inputs.min, max: inputs.max }));
}
if (inputs.intervals) {
return res.map((v, i, a) => i === 0 ? v : v - a[i - 1]);
}
return res;
}
/**
* Creates a vector with evenly spaced numbers from min to max with a specified number of items.
* Example: min=0, max=10, nrItems=5 → [0, 2.5, 5, 7.5, 10]
* @param inputs Span information containing min, max and step values
* @returns Vector containing number between min, max by giving nr of items
* @group create
* @shortname span linear items
* @drawable false
*/
spanLinearItems(inputs) {
const res = [];
const dist = (inputs.max - inputs.min);
for (let i = 0; i < inputs.nrItems; i++) {
const x = dist * i / (inputs.nrItems - 1);
res.push(x + inputs.min);
}
return res;
}
/**
* Subtracts the second vector from the first element-wise.
* Example: [10,20,30] - [1,2,3] → [9,18,27]
* @param inputs Two vectors
* @returns Vector that result by subtraction two vectors
* @group base
* @shortname sub
* @drawable false
*/
sub(inputs) {
const res = [];
for (let i = 0; i < inputs.first.length; i++) {
res.push(inputs.first[i] - inputs.second[i]);
}
return res;
}
/**
* Sums all values in the vector and returns a single number.
* Example: [1,2,3,4] → 10, [5,10,15] → 30
* @param inputs Vector to sum
* @returns Number that results by adding up all values in the vector
* @group base
* @shortname sum
* @drawable false
*/
sum(inputs) {
return inputs.vector.reduce((a, b) => a + b, 0);
}
/**
* Computes the squared length (squared magnitude) of a 3D vector.
* Example: [3,4,0] → 25 (length 5 squared)
* @param inputs Vector to compute the length
* @returns Number that is squared length of the vector
* @group base
* @shortname length squared
* @drawable false
*/
lengthSq(inputs) {
const v = inputs.vector;
return v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
}
/**
* Computes the length (magnitude) of a 3D vector.
* Example: [3,4,0] → 5, [1,0,0] → 1
* @param inputs Vector to compute the length
* @returns Number that is length of the vector
* @group base
* @shortname length
* @drawable false
*/
length(inputs) {
return Math.sqrt(this.lengthSq(inputs));
}
/**
* Converts an array of stringified numbers to actual numbers.
* Example: ['1', '2.5', '3'] → [1, 2.5, 3], ['10', '-5', '0.1'] → [10, -5, 0.1]
* @param inputs Array of stringified numbers
* @returns Array of numbers
* @group create
* @shortname parse numbers
* @drawable false
*/
parseNumbers(inputs) {
return inputs.vector.map(v => parseFloat(v));
}
}