renderer/depthCamera/DepthCameraHandler.tstypescript
import * as math from "../../math";
import type { Renderer } from "../Renderer";
import type { Planet } from "../../scene/Planet";
import { Scene } from "../../scene/Scene";
import type { Entity } from "../../entity/Entity";
import { EntityCollection } from "../../entity/EntityCollection";
import { DepthCamera } from "./DepthCamera";
import { depth_camera } from "./depth_camera";

const POLYLINE_DEPTH_OFFSET = -100;

/**
 * Renderer depth cameras. A camera index in the list is its slot in the planet quad tree strategies.
 * The cameras are bound to the first planet the renderer has when it starts. A planet added later is not
 * picked up: give it to DepthCamera as the planet option, though the footprints still show on the start planet
 * only. A removed planet is not released.
 *
 * @todo Several planets: keep the footprint and frustum collections per planet, in the planet a camera is
 * bound to. Now they live in the start planet, so the footprint of a camera bound to another planet lands
 * on the start planet ellipsoid.
 */
export class DepthCameraHandler {
    public readonly renderer: Renderer;

    public _depthCameras: DepthCamera[];

    public readonly cameraFrustumCollection: EntityCollection;
    public readonly cameraFootprintCollection: EntityCollection;

    protected _initialized: boolean;
    protected _planet: Planet | null;
    protected _scene: Scene;

    constructor(renderer: Renderer) {
        this.renderer = renderer;
        this._depthCameras = [];
        this._initialized = false;
        this._planet = null;

        this.cameraFootprintCollection = new EntityCollection({
            pickingEnabled: false,
            receiveProjectors: false,
            receiveShadows: false,
            depthOffset: POLYLINE_DEPTH_OFFSET
        });

        this.cameraFrustumCollection = new EntityCollection({
            pickingEnabled: true,
            receiveProjectors: false,
            receiveShadows: false,
            shadeMode: "phong",
            scaleByDistance: [100, 1000000, math.MAX32]
        });

        this._scene = new Scene("depthCameraHandler");
        this._scene.disableDepthCamera = true;
    }

    /**
     * True after the renderer has initialized the handler: an added camera is initialized at once.
     */
    public get initialized(): boolean {
        return this._initialized;
    }

    public get depthCameras(): DepthCamera[] {
        return [...this._depthCameras];
    }

    /**
     * First planet of the renderer at its start, the default one to bind the cameras to.
     */
    public get planet(): Planet | null {
        return this._planet;
    }

    public init(): void {
        if (this._initialized) return;

        this._initialized = true;

        this.renderer.handler.addProgram(depth_camera());

        this._bindPlanet();

        if (this._planet) {
            this.cameraFootprintCollection.addTo(this._planet);
            this.cameraFrustumCollection.addTo(this._planet);
        } else {
            this.renderer.addScene(this._scene);
            this.cameraFrustumCollection.addTo(this._scene);
        }

        for (let i = 0; i < this._depthCameras.length; i++) {
            this._depthCameras[i].init(this._planet, this.renderer);
            this._addDepthCameraEntities(this._depthCameras[i]);
        }
    }

    public add(depthCamera: DepthCamera): DepthCamera {
        if (depthCamera._handlerIndex !== -1) {
            return depthCamera;
        }

        depthCamera._handler = this;
        depthCamera._handlerIndex = this._depthCameras.length;
        this._depthCameras.push(depthCamera);

        if (this._initialized) {
            depthCamera.init(this._planet, this.renderer);
            this._addDepthCameraEntities(depthCamera);
        }

        this.renderer.requestRedraw();

        return depthCamera;
    }

    public remove(depthCamera: DepthCamera): boolean {
        const index = depthCamera._handlerIndex;
        if (index === -1 || this._depthCameras[index] !== depthCamera) {
            return false;
        }

        this._removeDepthCameraEntities(depthCamera);
        depthCamera.destroy();

        const last = this._depthCameras.length - 1;
        if (index !== last) {
            const moved = this._depthCameras[last];
            this._depthCameras[index] = moved;
            moved._handlerIndex = index;
        }
        this._depthCameras.pop();

        depthCamera._handler = null;
        depthCamera._handlerIndex = -1;

        this.renderer.requestRedraw();

        return true;
    }

    public clear(): void {
        while (this._depthCameras.length) {
            this.remove(this._depthCameras[this._depthCameras.length - 1]);
        }
    }

    public destroy(): void {
        this.clear();
        this.cameraFootprintCollection.remove();
        this.cameraFrustumCollection.remove();
        this._scene.remove();
        this._planet = null;
        this._initialized = false;
    }

    /**
     * Puts the cameras in place, before the planets collect their nodes.
     */
    public prepareFrame(): void {
        for (let i = 0; i < this._depthCameras.length; i++) {
            const depthCamera = this._depthCameras[i];
            if (depthCamera.enabled && depthCamera.initialized) {
                depthCamera.prepareFrame();
            }
        }
    }

    /**
     * Renders the depth frames, after the planets have collected the nodes and before the scenes are drawn.
     */
    public frame(): void {
        for (let i = 0; i < this._depthCameras.length; i++) {
            this._depthCameras[i].frame();
        }
    }

    protected _bindPlanet(): void {
        if (this._planet) return;

        const scenes = this.renderer._scenesArr;

        for (let i = scenes.length - 1; i >= 0; i--) {
            const planet = scenes[i] as Planet;
            if (planet.quadTreeStrategy) {
                this._planet = planet;
                return;
            }
        }
    }

    public _showFrustumEntity(entity: Entity, show: boolean): void {
        if (!this._initialized) return;
        show ? this.cameraFrustumCollection.add(entity) : this.cameraFrustumCollection.removeEntity(entity);
    }

    public _showFootprintEntity(entity: Entity, show: boolean): void {
        if (!this._initialized || !this._planet) return;
        show ? this.cameraFootprintCollection.add(entity) : this.cameraFootprintCollection.removeEntity(entity);
    }

    protected _addDepthCameraEntities(depthCamera: DepthCamera): void {
        const cameraFootprintEntity = depthCamera.cameraFootprintEntity;
        if (depthCamera.showFootprint && cameraFootprintEntity) {
            this._showFootprintEntity(cameraFootprintEntity, true);
        }

        const cameraFrustumEntity = depthCamera.cameraFrustumEntity;
        if (depthCamera.showFrustum && cameraFrustumEntity) {
            this._showFrustumEntity(cameraFrustumEntity, true);
        }
    }

    protected _removeDepthCameraEntities(depthCamera: DepthCamera): void {
        const cameraFootprintEntity = depthCamera.cameraFootprintEntity;
        if (cameraFootprintEntity) {
            this._showFootprintEntity(cameraFootprintEntity, false);
        }

        const cameraFrustumEntity = depthCamera.cameraFrustumEntity;
        if (cameraFrustumEntity) {
            this._showFrustumEntity(cameraFrustumEntity, false);
        }
    }
}