Source: math/vector3.js

/** @type {number} */
const ZERO_COMPONENT = 0;

/** @type {number} */
const UNIT_COMPONENT = 1;

/**
 * 3D vector with observable components. Can invoke an onChange callback, when `x, y, z` changes.
 */
export class Vector3 {
    /** @type {number} */
    #x;

    /** @type {number} */
    #y;

    /** @type {number} */
    #z;

    /** @type {Function | null} */
    #onChange;

    /**
     * @param {number} [x = 0] - X component.
     * @param {number} [y = 0] - Y component.
     * @param {number} [z = 0] - Z component.
     * @param {Function | null} [onChange = null] - Called when any component changes.
     */
    constructor(x = ZERO_COMPONENT, y = ZERO_COMPONENT, z = ZERO_COMPONENT, onChange = null) {
        if (onChange !== null && typeof onChange !== 'function') {
            throw new TypeError('Vector3 constructor expects `onChange` as a function or null.');
        }

        this.#x = ZERO_COMPONENT;
        this.#y = ZERO_COMPONENT;
        this.#z = ZERO_COMPONENT;
        this.#onChange = onChange;
        this.set(x, y, z);
    }

    /**
     * Creates a new (0, 0, 0) vector.
     *
     * @param {Function | null} [onChange=null] - Optional callback invoked when the vector changes, or null to disable change notifications.
     * @returns {Vector3}                       - A new Vector3 instance with all components set to zero (0, 0, 0).
     */
    static createZero(onChange = null) {
        return new Vector3(
            ZERO_COMPONENT,
            ZERO_COMPONENT,
            ZERO_COMPONENT,
            onChange
        );
    }

    /**
     * Creates a new (1, 1, 1) vector (unit scale vector).
     *
     * @param {Function | null} [onChange=null] - Optional callback invoked when the vector changes, or null to disable change notifications.
     * @returns {Vector3}                       - A new Vector3 instance with all components set to one (1, 1, 1).
     */
    static createUnitScale(onChange = null) {
        return new Vector3(
            UNIT_COMPONENT,
            UNIT_COMPONENT,
            UNIT_COMPONENT,
            onChange
        );
    }

    /**
     * @returns {number} - The current X component value.
     */
    get x() {
        return this.#x;
    }

    /**
     * @param {number} value - New X component value.
     */
    set x(value) {
        Vector3.#assertNumber(value, 'x');

        if (value === this.#x) {
            return;
        }

        this.#x = value;
        this.#emitChange();
    }

    /**
     * @returns {number} - The current Y component value.
     */
    get y() {
        return this.#y;
    }

    /**
     * @param {number} value - New Y component value.
     */
    set y(value) {
        Vector3.#assertNumber(value, 'y');

        if (value === this.#y) {
            return;
        }

        this.#y = value;
        this.#emitChange();
    }

    /**
     * @returns {number} - The current Z component value.
     */
    get z() {
        return this.#z;
    }

    /**
     * @param {number} value - New Z component value.
     */
    set z(value) {
        Vector3.#assertNumber(value, 'z');

        if (value === this.#z) {
            return;
        }

        this.#z = value;
        this.#emitChange();
    }

    /**
     * Sets all components at once.
     * Calls onChange at most once.
     *
     * @param {number} x  - New X component value.
     * @param {number} y  - New Y component value.
     * @param {number} z  - New Z component value.
     * @returns {Vector3} - This vector instance (for chaining).
     */
    set(x, y, z) {
        Vector3.#assertNumber(x, 'x');
        Vector3.#assertNumber(y, 'y');
        Vector3.#assertNumber(z, 'z');

        const changed = (x !== this.#x) || (y !== this.#y) || (z !== this.#z);
        this.#x = x;
        this.#y = y;
        this.#z = z;

        if (changed) {
            this.#emitChange();
        }

        return this;
    }

    /**
     * Copies components from another `Vector3`.
     *
     * @param {Vector3} other - Source vector to copy components from.
     * @returns {Vector3}     - This vector instance after copying components from the source vector (for chaining).
     */
    copyFrom(other) {
        if (!(other instanceof Vector3)) {
            throw new TypeError('Vector3.copyFrom expects a Vector3 instance.');
        }

        return this.set(other.x, other.y, other.z);
    }

    /**
     * Sets/updates the onChange callback.
     *
     * @param {Function | null} onChange - Callback invoked when any component changes, or null to disable change notifications.
     * @returns {Vector3}                - This vector instance (for chaining).
     */
    setOnChange(onChange) {
        if (onChange !== null && typeof onChange !== 'function') {
            throw new TypeError('Vector3.setOnChange expects a function or null.');
        }

        this.#onChange = onChange;
        return this;
    }

    /**
     * @private
     */
    #emitChange() {
        if (this.#onChange) {
            this.#onChange();
        }
    }

    /**
     * @param {number} value - Value to validate (must be a number and not NaN).
     * @param {string} name  - Component name used in error messages.
     * @private
     */
    static #assertNumber(value, name) {
        if (typeof value !== 'number' || Number.isNaN(value)) {
            throw new TypeError(`Vector3 component "${name}" must be a valid number.`);
        }
    }
}