Source: scene/camera.js

import { Object3D }   from './object3d.js';
import { CameraMath } from '../math/camera-math.js';

/**
 * Element count for a 4x4 matrix stored in a flat array.
 *
 * @type {number}
 */
const MATRIX_4x4_ELEMENT_COUNT = 16;

/**
 * Base camera class. Responsibilities:
 * - Caches view matrix (inverse of local TRS transform).
 * - Defines a common camera contract for derived camera types.
 */
export class Camera extends Object3D {

    /**
     * Cached view matrix buffer. Reused between frames to avoid allocations.
     *
     * @type {Float32Array}
     * @private
     */
    #viewMatrix;

    /**
     * Cached local `position X` component used to detect transform changes.
     *
     * @type {number}
     * @private
     */
    #cachedPositionX = Number.NaN;

    /**
     * Cached local `position Y` component used to detect transform changes.
     *
     * @type {number}
     * @private
     */
    #cachedPositionY = Number.NaN;

    /**
     * Cached local `position Z` component used to detect transform changes.
     *
     * @type {number}
     * @private
     */
    #cachedPositionZ = Number.NaN;

    /**
     * Cached local `rotation X` component (radians) used to detect transform changes.
     *
     * @type {number}
     * @private
     */
    #cachedRotationX = Number.NaN;

    /**
     * Cached local `rotation Y` component (radians) used to detect transform changes.
     *
     * @type {number}
     * @private
     */
    #cachedRotationY = Number.NaN;

    /**
     * Cached local `rotation Z` component (radians) used to detect transform changes.
     *
     * @type {number}
     * @private
     */
    #cachedRotationZ = Number.NaN;

    /**
     * Cached local `scale X` component used to detect transform changes.
     *
     * @type {number}
     * @private
     */
    #cachedScaleX = Number.NaN;

    /**
     * Cached local `scale Y` component used to detect transform changes.
     *
     * @type {number}
     * @private
     */
    #cachedScaleY = Number.NaN;

    /**
     * Cached local `scale Z` component used to detect transform changes.
     *
     * @type {number}
     * @private
     */
    #cachedScaleZ = Number.NaN;

    constructor() {
        super();
        this.#viewMatrix = new Float32Array(MATRIX_4x4_ELEMENT_COUNT);
    }

    /**
     * Returns the view matrix (inverse of camera local TRS transform).
     * The returned matrix is cached and reused between calls.
     *
     * @returns {Float32Array} - Cached view matrix.
     */
    getViewMatrix() {
        this.#updateViewMatrixIfRequired();
        return this.#viewMatrix;
    }

    /**
     * Returns the projection matrix for this camera.
     * Derived camera classes must implement this method.
     *
     * @throws {Error} Always throws in the base class.
     * @returns {Float32Array} - Projection matrix.
     */
    getProjectionMatrix() {
        throw new Error('`Camera.getProjectionMatrix` must be implemented in a derived camera class.');
    }

    /**
     * Updates the camera aspect ratio (width/height).
     * Base camera class does not define, how aspect ratio affects the projection.
     *
     * @param {number} aspectRatio - New viewport aspect ratio.
     */
    setAspectRatio(aspectRatio) {
        if (typeof aspectRatio !== 'number') {
            throw new TypeError('`Camera.setAspectRatio` expects `aspectRatio` as a number.');
        }

        throw new Error('`Camera.setAspectRatio` must be implemented in a derived camera class.');
    }

    /**
     * Recomputes the view matrix only, when local transform changes since the last call.
     *
     * @private
     */
    #updateViewMatrixIfRequired() {
        const position    = this.position;
        const rotation    = this.rotation;
        const scale       = this.scale;
        const isViewDirty = this.#isTransformChanged(position, rotation, scale);

        if (isViewDirty === true) {
            CameraMath.writeViewMatrixTo(this.#viewMatrix, position, rotation, scale);
            this.#cacheTransform(position, rotation, scale);
        }
    }

    /**
     * Checks whether the local transform has changed, since the last cached snapshot.
     *
     * @param {Object} position - Camera position vector.
     * @param {Object} rotation - Camera rotation vector in radians.
     * @param {Object} scale    - Camera scale vector.
     * @returns {boolean}       - True, when local transform differs from cached snapshot.
     * @private
     */
    #isTransformChanged(position, rotation, scale) {
        if (this.#isPositionChanged(position) === true) {
            return true;
        }

        if (this.#isRotationChanged(rotation) === true) {
            return true;
        }

        if (this.#isScaleChanged(scale) === true) {
            return true;
        }

        return false;
    }

    /* eslint-disable indent */

    /**
     * @param {Object} position - Position vector.
     * @returns {boolean}       - True, when local position differs from the cached snapshot.
     * @private
     */
    #isPositionChanged(position) {
        return (
               position.x !== this.#cachedPositionX
            || position.y !== this.#cachedPositionY
            || position.z !== this.#cachedPositionZ
        );
    }

    /**
     * @param {Object} rotation - Rotation vector.
     * @returns {boolean}       - True, when local rotation differs from the cached snapshot.
     * @private
     */
    #isRotationChanged(rotation) {
        return (
               rotation.x !== this.#cachedRotationX
            || rotation.y !== this.#cachedRotationY
            || rotation.z !== this.#cachedRotationZ
        );
    }

    /**
     * @param {Object} scale - Scale vector.
     * @returns {boolean}    - True, when local scale differs from the cached snapshot.
     * @private
     */
    #isScaleChanged(scale) {
        return (
               scale.x !== this.#cachedScaleX
            || scale.y !== this.#cachedScaleY
            || scale.z !== this.#cachedScaleZ
        );
    }

    /* eslint-enable indent */

    /**
     * Stores the current local transform components as a cached snapshot for future comparisons.
     *
     * @param {Object} position - Position vector.
     * @param {Object} rotation - Rotation vector.
     * @param {Object} scale    - Scale vector.
     * @private
     */
    #cacheTransform(position, rotation, scale) {
        this.#cachedPositionX = position.x;
        this.#cachedPositionY = position.y;
        this.#cachedPositionZ = position.z;

        this.#cachedRotationX = rotation.x;
        this.#cachedRotationY = rotation.y;
        this.#cachedRotationZ = rotation.z;

        this.#cachedScaleX = scale.x;
        this.#cachedScaleY = scale.y;
        this.#cachedScaleZ = scale.z;
    }
}