Source: math/vector3-math.js

import { Vector3 } from './vector3.js';

/**
 * Number of components in a 3D vector.
 *
 * @type {number}
 */
const VECTOR3_COMPONENT_COUNT = 3;

/**
 * Component index for X.
 *
 * @type {number}
 */
const VECTOR3_X_INDEX = 0;

/**
 * Component index for Y.
 *
 * @type {number}
 */
const VECTOR3_Y_INDEX = 1;

/**
 * Component index for Z.
 *
 * @type {number}
 */
const VECTOR3_Z_INDEX = 2;

/**
 * Zero value constant.
 *
 * @type {number}
 */
const ZERO_VALUE = 0;

/**
 * One value constant.
 *
 * @type {number}
 */
const ONE_VALUE = 1;

/**
 * Default epsilon for approximate comparisons.
 *
 * @type {number}
 */
const DEFAULT_EPSILON = 1e-6;

/**
 * Minimum length for safe normalization.
 *
 * @type {number}
 */
const MIN_NORMALIZE_LENGTH = 1e-8;

/**
 * 3D vector math utilities.
 */
export class Vector3Math {

    /**
     * Adds two vectors.
     *
     * @param {Vector3 | Float32Array} outputVector - Output vector.
     * @param {Vector3 | Float32Array} firstVector  - First vector.
     * @param {Vector3 | Float32Array} secondVector - Second vector.
     * @returns {Vector3 | Float32Array}
     * @throws {TypeError}  When any vector is invalid.
     * @throws {RangeError} When `Float32Array` vectors are not `length 3`.
     */
    static add(outputVector, firstVector, secondVector) {
        Vector3Math.#assertVector3Like(outputVector, 'outputVector');
        Vector3Math.#assertVector3Like(firstVector, 'firstVector');
        Vector3Math.#assertVector3Like(secondVector, 'secondVector');

        const x = Vector3Math.#getX(firstVector) + Vector3Math.#getX(secondVector);
        const y = Vector3Math.#getY(firstVector) + Vector3Math.#getY(secondVector);
        const z = Vector3Math.#getZ(firstVector) + Vector3Math.#getZ(secondVector);
        return Vector3Math.#write(outputVector, x, y, z);
    }

    /**
     * Subtracts the second vector from the first vector.
     *
     * @param {Vector3 | Float32Array} outputVector - Output vector.
     * @param {Vector3 | Float32Array} firstVector  - First vector.
     * @param {Vector3 | Float32Array} secondVector - Second vector.
     * @returns {Vector3 | Float32Array}
     * @throws {TypeError}  When any vector is invalid.
     * @throws {RangeError} When `Float32Array` vectors are not `length 3`.
     */
    static sub(outputVector, firstVector, secondVector) {
        Vector3Math.#assertVector3Like(outputVector, 'outputVector');
        Vector3Math.#assertVector3Like(firstVector, 'firstVector');
        Vector3Math.#assertVector3Like(secondVector, 'secondVector');

        const x = Vector3Math.#getX(firstVector) - Vector3Math.#getX(secondVector);
        const y = Vector3Math.#getY(firstVector) - Vector3Math.#getY(secondVector);
        const z = Vector3Math.#getZ(firstVector) - Vector3Math.#getZ(secondVector);
        return Vector3Math.#write(outputVector, x, y, z);
    }

    /**
     * Scales a vector by a scalar.
     *
     * @param {Vector3 | Float32Array} outputVector - Output vector.
     * @param {Vector3 | Float32Array} inputVector  - Input vector.
     * @param {number} scalar                       - Scalar multiplier.
     * @returns {Vector3 | Float32Array}
     * @throws {TypeError}  When vectors or scalar are invalid.
     * @throws {RangeError} When `Float32Array` vectors are not `length 3`.
     */
    static scale(outputVector, inputVector, scalar) {
        Vector3Math.#assertVector3Like(outputVector, 'outputVector');
        Vector3Math.#assertVector3Like(inputVector, 'inputVector');

        if (typeof scalar !== 'number' || !Number.isFinite(scalar)) {
            throw new TypeError('`Vector3Math.scale` expects `scalar` as a finite number.');
        }

        const x = Vector3Math.#getX(inputVector) * scalar;
        const y = Vector3Math.#getY(inputVector) * scalar;
        const z = Vector3Math.#getZ(inputVector) * scalar;
        return Vector3Math.#write(outputVector, x, y, z);
    }

    /**
     * Computes vector length.
     *
     * @param {Vector3 | Float32Array} inputVector - Input vector.
     * @returns {number}
     * @throws {TypeError}  When vector is invalid.
     * @throws {RangeError} When `Float32Array` vectors are not `length 3`.
     */
    static length(inputVector) {
        Vector3Math.#assertVector3Like(inputVector, 'inputVector');

        const x = Vector3Math.#getX(inputVector);
        const y = Vector3Math.#getY(inputVector);
        const z = Vector3Math.#getZ(inputVector);
        return Math.sqrt((x * x) + (y * y) + (z * z));
    }

    /**
     * Computes distance between two vectors.
     *
     * @param {Vector3 | Float32Array} firstVector  - First vector.
     * @param {Vector3 | Float32Array} secondVector - Second vector.
     * @returns {number}
     * @throws {TypeError}  When vectors are invalid.
     * @throws {RangeError} When `Float32Array` vectors are not `length 3`.
     */
    static distance(firstVector, secondVector) {
        Vector3Math.#assertVector3Like(firstVector, 'firstVector');
        Vector3Math.#assertVector3Like(secondVector, 'secondVector');

        const deltaX = Vector3Math.#getX(firstVector) - Vector3Math.#getX(secondVector);
        const deltaY = Vector3Math.#getY(firstVector) - Vector3Math.#getY(secondVector);
        const deltaZ = Vector3Math.#getZ(firstVector) - Vector3Math.#getZ(secondVector);
        return Math.sqrt((deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ));
    }

    /**
     * Normalizes a vector.
     *
     * @param {Vector3 | Float32Array} outputVector - Output vector.
     * @param {Vector3 | Float32Array} inputVector  - Input vector.
     * @returns {Vector3 | Float32Array}
     * @throws {TypeError}  When vectors are invalid.
     * @throws {RangeError} When `Float32Array` vectors are not `length 3`.
     */
    static normalize(outputVector, inputVector) {
        Vector3Math.#assertVector3Like(outputVector, 'outputVector');
        Vector3Math.#assertVector3Like(inputVector, 'inputVector');

        const vectorLength = Vector3Math.length(inputVector);

        if (vectorLength <= MIN_NORMALIZE_LENGTH) {
            return Vector3Math.#write(outputVector, ZERO_VALUE, ZERO_VALUE, ZERO_VALUE);
        }

        const inverseLength = ONE_VALUE / vectorLength;
        return Vector3Math.scale(outputVector, inputVector, inverseLength);
    }

    /**
     * Computes dot product.
     *
     * @param {Vector3 | Float32Array} firstVector  - First vector.
     * @param {Vector3 | Float32Array} secondVector - Second vector.
     * @returns {number}
     * @throws {TypeError}  When vectors are invalid.
     * @throws {RangeError} When `Float32Array` vectors are not `length 3`.
     */
    static dot(firstVector, secondVector) {
        Vector3Math.#assertVector3Like(firstVector, 'firstVector');
        Vector3Math.#assertVector3Like(secondVector, 'secondVector');

        return (Vector3Math.#getX(firstVector) * Vector3Math.#getX(secondVector))
            + (Vector3Math.#getY(firstVector) * Vector3Math.#getY(secondVector))
            + (Vector3Math.#getZ(firstVector) * Vector3Math.#getZ(secondVector));
    }

    /**
     * Computes cross product.
     *
     * @param {Vector3 | Float32Array} outputVector - Output vector.
     * @param {Vector3 | Float32Array} firstVector  - First vector.
     * @param {Vector3 | Float32Array} secondVector - Second vector.
     * @returns {Vector3 | Float32Array}
     * @throws {TypeError}  When vectors are invalid.
     * @throws {RangeError} When `Float32Array` vectors are not `length 3`.
     */
    static cross(outputVector, firstVector, secondVector) {
        Vector3Math.#assertVector3Like(outputVector, 'outputVector');
        Vector3Math.#assertVector3Like(firstVector, 'firstVector');
        Vector3Math.#assertVector3Like(secondVector, 'secondVector');

        const firstX  = Vector3Math.#getX(firstVector);
        const firstY  = Vector3Math.#getY(firstVector);
        const firstZ  = Vector3Math.#getZ(firstVector);
        const secondX = Vector3Math.#getX(secondVector);
        const secondY = Vector3Math.#getY(secondVector);
        const secondZ = Vector3Math.#getZ(secondVector);

        const x = (firstY * secondZ) - (firstZ * secondY);
        const y = (firstZ * secondX) - (firstX * secondZ);
        const z = (firstX * secondY) - (firstY * secondX);
        return Vector3Math.#write(outputVector, x, y, z);
    }

    /**
     * Linearly interpolates between vectors.
     *
     * @param {Vector3 | Float32Array} outputVector - Output vector.
     * @param {Vector3 | Float32Array} startVector  - Start vector.
     * @param {Vector3 | Float32Array} endVector    - End vector.
     * @param {number} interpolationFactor          - Interpolation factor in [0..1].
     * @returns {Vector3 | Float32Array}
     * @throws {TypeError}  When vectors or interpolation factor are invalid.
     * @throws {RangeError} When Float32Array vectors are not `length 3`.
     */
    static lerp(outputVector, startVector, endVector, interpolationFactor) {
        Vector3Math.#assertVector3Like(outputVector, 'outputVector');
        Vector3Math.#assertVector3Like(startVector, 'startVector');
        Vector3Math.#assertVector3Like(endVector, 'endVector');

        if (typeof interpolationFactor !== 'number' || !Number.isFinite(interpolationFactor)) {
            throw new TypeError('`Vector3Math.lerp` expects `interpolationFactor` as a finite number.');
        }

        const x = Vector3Math.#getX(startVector) + (Vector3Math.#getX(endVector) - Vector3Math.#getX(startVector)) * interpolationFactor;
        const y = Vector3Math.#getY(startVector) + (Vector3Math.#getY(endVector) - Vector3Math.#getY(startVector)) * interpolationFactor;
        const z = Vector3Math.#getZ(startVector) + (Vector3Math.#getZ(endVector) - Vector3Math.#getZ(startVector)) * interpolationFactor;
        return Vector3Math.#write(outputVector, x, y, z);
    }

    /**
     * Clamps vector components to the [min..max] range.
     *
     * @param {Vector3 | Float32Array} outputVector - Output vector.
     * @param {Vector3 | Float32Array} inputVector  - Input vector.
     * @param {number} min                          - Minimum component value.
     * @param {number} max                          - Maximum component value.
     * @returns {Vector3 | Float32Array}
     * @throws {TypeError}  When vectors or bounds are invalid.
     * @throws {RangeError} When Float32Array vectors are not `length 3`.
     */
    static clamp(outputVector, inputVector, min, max) {
        Vector3Math.#assertVector3Like(outputVector, 'outputVector');
        Vector3Math.#assertVector3Like(inputVector, 'inputVector');

        if (typeof min !== 'number' || typeof max !== 'number' || !Number.isFinite(min) || !Number.isFinite(max)) {
            throw new TypeError('`Vector3Math.clamp` expects `min` and `max` as finite numbers.');
        }

        const x = Math.max(min, Math.min(max, Vector3Math.#getX(inputVector)));
        const y = Math.max(min, Math.min(max, Vector3Math.#getY(inputVector)));
        const z = Math.max(min, Math.min(max, Vector3Math.#getZ(inputVector)));
        return Vector3Math.#write(outputVector, x, y, z);
    }

    /**
     * Compares two vectors with a tolerance.
     *
     * @param {Vector3 | Float32Array} firstVector  - First vector.
     * @param {Vector3 | Float32Array} secondVector - Second vector.
     * @param {number} [epsilon = 1e-6]             - Tolerance.
     * @returns {boolean}
     * @throws {TypeError}  When vectors or epsilon are invalid.
     * @throws {RangeError} When Float32Array vectors are not `length 3`.
     */
    static approxEquals(firstVector, secondVector, epsilon = DEFAULT_EPSILON) {
        Vector3Math.#assertVector3Like(firstVector, 'firstVector');
        Vector3Math.#assertVector3Like(secondVector, 'secondVector');

        if (typeof epsilon !== 'number' || !Number.isFinite(epsilon)) {
            throw new TypeError('`Vector3Math.approxEquals` expects `epsilon` as a finite number.');
        }

        return Math.abs(Vector3Math.#getX(firstVector) - Vector3Math.#getX(secondVector)) <= epsilon
            && Math.abs(Vector3Math.#getY(firstVector) - Vector3Math.#getY(secondVector)) <= epsilon
            && Math.abs(Vector3Math.#getZ(firstVector) - Vector3Math.#getZ(secondVector)) <= epsilon;
    }

    /**
     * @param {Vector3 | Float32Array} vector - Vector to validate.
     * @param {string} argumentName           - Argument name for error message.
     * @private
     */
    static #assertVector3Like(vector, argumentName) {
        if (!(vector instanceof Vector3) && !(vector instanceof Float32Array)) {
            throw new TypeError(`\`Vector3Math\` expects \`${argumentName}\` as Vector3 or Float32Array.`);
        }

        if (vector instanceof Float32Array && vector.length !== VECTOR3_COMPONENT_COUNT) {
            throw new RangeError('`Vector3Math` expects Float32Array(3) vectors.');
        }
    }

    /**
     * @param {Vector3 | Float32Array} vector - Input vector.
     * @returns {number}
     * @private
     */
    static #getX(vector) {
        return vector instanceof Vector3 ? vector.x : vector[VECTOR3_X_INDEX];
    }

    /**
     * @param {Vector3 | Float32Array} vector - Input vector.
     * @returns {number}
     * @private
     */
    static #getY(vector) {
        return vector instanceof Vector3 ? vector.y : vector[VECTOR3_Y_INDEX];
    }

    /**
     * @param {Vector3 | Float32Array} vector - Input vector.
     * @returns {number}
     * @private
     */
    static #getZ(vector) {
        return vector instanceof Vector3 ? vector.z : vector[VECTOR3_Z_INDEX];
    }

    /**
     * @param {Vector3 | Float32Array} outputVector - Output vector.
     * @param {number} x                            - X component.
     * @param {number} y                            - Y component.
     * @param {number} z                            - Z component.
     * @returns {Vector3 | Float32Array}
     * @private
     */
    static #write(outputVector, x, y, z) {
        if (outputVector instanceof Vector3) {
            outputVector.set(x, y, z);
            return outputVector;
        }

        outputVector[VECTOR3_X_INDEX] = x;
        outputVector[VECTOR3_Y_INDEX] = y;
        outputVector[VECTOR3_Z_INDEX] = z;
        return outputVector;
    }
}