Source: light/directional-light.js

import { Light } from './light.js';

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

/**
 * Index of the X component in a vector.
 *
 * @type {number}
 */
const VECTOR_X_INDEX = 0;

/**
 * Index of the Y component in a vector.
 *
 * @type {number}
 */
const VECTOR_Y_INDEX = 1;

/**
 * Index of the Z component in a vector.
 *
 * @type {number}
 */
const VECTOR_Z_INDEX = 2;

/**
 * Default light direction (world space).
 * Matches the engine's historical default.
 *
 * @type {Float32Array}
 */
const DEFAULT_DIRECTION = new Float32Array([0.5, 0.7, 1.0]);

/**
 * Default directional light strength.
 *
 * @type {number}
 */
const DEFAULT_DIRECTIONAL_STRENGTH = 1.0;

/**
 * Minimum directional light strength.
 *
 * @type {number}
 */
const MIN_DIRECTIONAL_STRENGTH = 0.0;

/**
 * Maximum directional light strength.
 *
 * @type {number}
 */
const MAX_DIRECTIONAL_STRENGTH = 3.0;

/**
 * Minimum allowed squared length for a direction vector.
 *
 * @type {number}
 */
const MIN_DIRECTION_LENGTH_SQUARED = 0.0;

/**
 * Numerator used for inverse length computations.
 *
 * @type {number}
 */
const INVERSE_LENGTH_NUMERATOR = 1.0;

/**
 * Default roll angle (rotation around the Z axis).
 *
 * @type {number}
 */
const DEFAULT_ROLL_RADIANS = 0.0;

/**
 * Minimum clamp value for asin.
 *
 * @type {number}
 */
const ASIN_CLAMP_MIN = -1.0;

/**
 * Maximum clamp value for asin.
 *
 * @type {number}
 */
const ASIN_CLAMP_MAX = 1.0;

/**
 * World matrix index for Z axis X component.
 *
 * @type {number}
 */
const WORLD_Z_AXIS_X_INDEX = 8;

/**
 * World matrix index for Z axis Y component.
 *
 * @type {number}
 */
const WORLD_Z_AXIS_Y_INDEX = 9;

/**
 * World matrix index for Z axis Z component.
 *
 * @type {number}
 */
const WORLD_Z_AXIS_Z_INDEX = 10;

/**
 * Error message for invalid direction type.
 *
 * @type {string}
 */
const ERROR_DIRECTION_TYPE = '`DirectionalLight.setDirection` expects a number[] or `Float32Array`.';

/**
 * Error message for invalid direction component count.
 *
 * @type {string}
 */
const ERROR_DIRECTION_COMPONENTS = '`DirectionalLight.setDirection` expects exactly 3 components.';

/**
 * Error message for invalid direction component values.
 *
 * @type {string}
 */
const ERROR_DIRECTION_COMPONENTS_FINITE = '`DirectionalLight.setDirection` expects finite components.';

/**
 * Error message for invalid direction length.
 *
 * @type {string}
 */
const ERROR_DIRECTION_LENGTH = '`DirectionalLight.setDirection` expects a non-zero direction vector.';

/**
 * Error message for invalid strength values.
 *
 * @type {string}
 */
const ERROR_STRENGTH_TYPE = '`DirectionalLight.setStrength` expects a finite number.';

/**
 * Directional light source.
 */
export class DirectionalLight extends Light {

    /**
     * Cached normalized direction buffer.
     *
     * @type {Float32Array}
     * @private
     */
    #direction = new Float32Array(VECTOR3_ELEMENT_COUNT);

    /**
     * Directional light strength multiplier.
     *
     * @type {number}
     * @private
     */
    #strength = DEFAULT_DIRECTIONAL_STRENGTH;

    /**
     * Creates a new directional light with the default direction.
     */
    constructor() {
        super();
        this.setDirection(DEFAULT_DIRECTION);
        this.#strength = DEFAULT_DIRECTIONAL_STRENGTH;
    }

    /**
     * Sets the light direction by updating the light rotation.
     *
     * @param {Float32Array | number[]} direction - Direction vector (world space).
     * @returns {void}
     * @throws {TypeError} When the direction is invalid.
     */
    setDirection(direction) {
        DirectionalLight.#assertVector3(direction);

        // Read the direction components and compute the squared length (avoids the premature `sqrt`):
        const directionX    = direction[VECTOR_X_INDEX];
        const directionY    = direction[VECTOR_Y_INDEX];
        const directionZ    = direction[VECTOR_Z_INDEX];
        const lengthSquared = (directionX * directionX) + (directionY * directionY) + (directionZ * directionZ);

        // Reject the zero-length and non-finite vectors (can't be normalized safely):
        if (!Number.isFinite(lengthSquared) || lengthSquared <= MIN_DIRECTION_LENGTH_SQUARED) {
            throw new TypeError(ERROR_DIRECTION_LENGTH);
        }

        // Normalize the direction to unit the length for stable trigonometry:
        const inverseLength = INVERSE_LENGTH_NUMERATOR / Math.sqrt(lengthSquared);
        const normalizedX   = directionX * inverseLength;
        const normalizedY   = directionY * inverseLength;
        const normalizedZ   = directionZ * inverseLength;

        // Convert the normalized direction to Euler angles (pitch/yaw), with `asin` clamping for numeric safety:
        const clampedY  = Math.min(ASIN_CLAMP_MAX, Math.max(ASIN_CLAMP_MIN, normalizedY));
        const rotationX = -Math.asin(clampedY);
        const rotationY = Math.atan2(normalizedX, normalizedZ);

        this.rotation.x = rotationX;
        this.rotation.y = rotationY;
        this.rotation.z = DEFAULT_ROLL_RADIANS;
    }

    /**
     * Returns the normalized light direction in world space.
     *
     * @returns {Float32Array}
     */
    getDirection() {
        const worldMatrix   = this.worldMatrix;
        const axisX         = worldMatrix[WORLD_Z_AXIS_X_INDEX];
        const axisY         = worldMatrix[WORLD_Z_AXIS_Y_INDEX];
        const axisZ         = worldMatrix[WORLD_Z_AXIS_Z_INDEX];
        const lengthSquared = (axisX * axisX) + (axisY * axisY) + (axisZ * axisZ);

        if (!Number.isFinite(lengthSquared) || lengthSquared <= MIN_DIRECTION_LENGTH_SQUARED) {
            this.#direction.set(DEFAULT_DIRECTION);
            return this.#direction;
        }

        const inverseLength = INVERSE_LENGTH_NUMERATOR / Math.sqrt(lengthSquared);
        this.#direction[VECTOR_X_INDEX] = axisX * inverseLength;
        this.#direction[VECTOR_Y_INDEX] = axisY * inverseLength;
        this.#direction[VECTOR_Z_INDEX] = axisZ * inverseLength;
        return this.#direction;
    }

    /**
     * Sets the directional light strength multiplier.
     *
     * @param {number} strength - Directional strength multiplier.
     * @returns {void}
     * @throws {TypeError} When the strength is invalid.
     */
    setStrength(strength) {
        if (typeof strength !== 'number' || !Number.isFinite(strength)) {
            throw new TypeError(ERROR_STRENGTH_TYPE);
        }

        this.#strength = Math.min(MAX_DIRECTIONAL_STRENGTH, Math.max(MIN_DIRECTIONAL_STRENGTH, strength));
    }

    /**
     * Returns the directional light strength multiplier.
     *
     * @returns {number}
     */
    getStrength() {
        return this.#strength;
    }

    /**
     * Validates a vector3-like input.
     *
     * @param {Float32Array | number[]} vector - Vector to validate.
     * @returns {void}
     * @throws {TypeError} When the vector is invalid.
     * @private
     */
    static #assertVector3(vector) {
        if (!Array.isArray(vector) && !(vector instanceof Float32Array)) {
            throw new TypeError(ERROR_DIRECTION_TYPE);
        }

        if (vector.length !== VECTOR3_ELEMENT_COUNT) {
            throw new TypeError(ERROR_DIRECTION_COMPONENTS);
        }

        if (!Number.isFinite(vector[VECTOR_X_INDEX])
            || !Number.isFinite(vector[VECTOR_Y_INDEX])
            || !Number.isFinite(vector[VECTOR_Z_INDEX])) {
            throw new TypeError(ERROR_DIRECTION_COMPONENTS_FINITE);
        }
    }
}