renderer/projectors/Projector.tstypescript
import type { DepthCamera } from "../../control/depthCamera/DepthCamera";
import type { NumberArray3 } from "../../math/Vec3";
import type { NumberArray4 } from "../../math/Vec4";
import { linearToSrgbArr, srgbToLinear } from "../../utils/colorSpace";
import { htmlColorToRgba, rgbToStringHTML, TypedArray } from "../../utils/shared";
import type { ProjectorManager } from "./ProjectorManager";

export type ProjectorSourceType = "color" | "image" | "video" | "texture";
export type ProjectorRenderMode = "color" | "light";
export type ProjectorColor = NumberArray3 | NumberArray4 | TypedArray | string;
export const PROJECTOR_RENDER_MODE_COLOR = 0;
export const PROJECTOR_RENDER_MODE_LIGHT = 1;

export interface IProjectorParams {
    enabled?: boolean;
    depthCamera: DepthCamera;
    color?: ProjectorColor;
    sourceType?: ProjectorSourceType;
    renderMode?: ProjectorRenderMode;
    priority?: number;
}

export class Projector {
    protected static __staticCounter__ = 0;

    public readonly id: number;
    public depthCamera: DepthCamera;
    public color: Float32Array;
    public sourceType: ProjectorSourceType;
    protected _enabled: boolean;
    protected _renderMode: number;
    protected _priority: number;

    /**
     * Layer index in the manager-owned depth array texture. -1 if not yet added.
     */
    public _slot: number = -1;

    /**
     * Manager that owns this projector. Set in ProjectorManager.add(), cleared on remove().
     */
    public _manager: ProjectorManager | null = null;

    constructor(params: IProjectorParams) {
        this.id = Projector.__staticCounter__++;
        this._enabled = params.enabled ?? true;
        this.depthCamera = params.depthCamera;
        this.color = Projector._resolveColor(params.color);
        this.sourceType = params.sourceType || "color";
        this._renderMode = Projector._resolveRenderMode(params.renderMode);
        this._priority = params.priority || 0;
    }

    public get enabled(): boolean {
        return this._enabled;
    }

    public set enabled(enabled: boolean) {
        if (this._enabled === enabled) return;

        this._enabled = enabled;
        this._manager?.update(this);
    }

    public get renderMode(): number {
        return this._renderMode;
    }

    public set renderMode(renderMode: ProjectorRenderMode | number) {
        const value = Projector._resolveRenderMode(renderMode);

        if (this._renderMode === value) return;

        this._renderMode = value;
        this._manager?.update(this);
    }

    public get priority(): number {
        return this._priority;
    }

    public set priority(priority: number) {
        if (this._priority === priority) return;

        this._priority = priority;
        this._manager?.update(this);
    }

    protected static _resolveRenderMode(renderMode?: ProjectorRenderMode | number): number {
        if (typeof renderMode === "number") return renderMode;

        return renderMode === "light" ? PROJECTOR_RENDER_MODE_LIGHT : PROJECTOR_RENDER_MODE_COLOR;
    }

    protected static _resolveColor(color?: ProjectorColor): Float32Array {
        if (typeof color === "string") {
            const c = htmlColorToRgba(color);
            return new Float32Array([srgbToLinear(c.x), srgbToLinear(c.y), srgbToLinear(c.z), c.w]);
        }

        if (color) {
            return new Float32Array([
                srgbToLinear(color[0] ?? 1.0),
                srgbToLinear(color[1] ?? 1.0),
                srgbToLinear(color[2] ?? 1.0),
                color[3] ?? 1.0
            ]);
        }

        return new Float32Array([1.0, 1.0, 1.0, 1.0]);
    }

    public setColorHTML(color: string): void {
        const c = htmlColorToRgba(color);
        this.setColor(c.x, c.y, c.z, c.w);
    }

    public setColor(r: number, g: number, b: number, a?: number): void {
        this.color[0] = srgbToLinear(r);
        this.color[1] = srgbToLinear(g);
        this.color[2] = srgbToLinear(b);
        a !== undefined && (this.color[3] = a);
    }

    public setOpacity(a: number): void {
        this.color[3] = a;
    }

    public getColor(): string {
        const srgb = linearToSrgbArr([this.color[0], this.color[1], this.color[2]]);
        const htmlColor: NumberArray3 = [
            Math.round(srgb[0] * 255),
            Math.round(srgb[1] * 255),
            Math.round(srgb[2] * 255)
        ];
        return rgbToStringHTML(htmlColor);
    }

    public getOpacity(): number {
        return this.color[3];
    }

    /**
     * @public
     * @return number
     * Gets allocated layer index inside the depth array texture.
     */
    public get slot(): number {
        return this._slot;
    }

    /**
     * Gets Manager-owned TEXTURE_2D_ARRAY used as the depth target.
     * @return WebGLTexture | null
     */
    public get arrayTexture(): WebGLTexture | null {
        return this._manager ? this._manager.depthArrayTexture : null;
    }

    /**
     * Gets original framebuffer color texture (TEXTURE_2D) used by projector depth pass.
     * ProjectorManager temporarily rebinds framebuffer attachment to array-layer,
     * but framebuffer.textures[0] remains the original texture reference.
     */
    public get depthTexture(): WebGLTexture | null {
        return this.depthCamera.framebuffer.textures[0] || null;
    }
}