scene_Scene.ts

import { Renderer } from "../renderer/Renderer";
import type { EntityCollection } from "../entity/EntityCollection";
import { Quat } from "../math/Quat";
import { Vec3 } from "../math/Vec3";
import type { Planet } from "./Planet";

/**
 * Render node is a logical part of a render mechanism. Represents scene rendering.
 * For example one scene node for rendering the Earth, another one for rendering the Moon, another node for rendering stars etc.
 * Each render node has own model view space defined with matrices(scale, rotation, translation, transformation).
 * There are collections of light sources, entities and so on in the node.
 * Access to the node is renderer.scenes["Earth"]
 * @class
 * @param {string} name - Node name.
 */

class Scene {
    static __counter__: number = 0;

    protected __id: number;

    /**
     * Node name.
     * @protected
     * @type {string}
     */
    protected _name: string;

    /**
     * Top scene tree node pointer.
     * @public
     * @type {Scene}
     */
    public topNode: Scene;

    /**
     * Children nodes.
     * @public
     * @type {Array.<Scene>}
     */
    public childNodes: Scene[];

    /**
     * Parent node pointer.
     * @public
     * @type {Scene}
     */
    public parentNode: Scene | null;

    /**
     * Renderer that calls frame() callback.
     * @public
     * @type {Renderer}
     */
    public renderer: Renderer | null;

    public drawMode: number;

    /** Show rendering.
     * @public
     */
    public show: boolean;

    protected _isActive: boolean;

    /**
     * Entity collection array.
     * @public
     * @type {Array.<EntityCollection>}
     */
    public entityCollections: EntityCollection[];

    protected _entityCollectionsByDepthOrder: EntityCollection[][];

    protected _pickingId: number;

    constructor(name?: string) {
        this.__id = Scene.__counter__++;

        this._name = name || `${this.__id}`;

        this.topNode = this;

        this.childNodes = [];

        this.parentNode = null;

        this.renderer = null;

        this.drawMode = 0;

        this.show = true;

        this._isActive = true;

        this.entityCollections = [];

        this._entityCollectionsByDepthOrder = [];

        this._pickingId = -1;
    }

    public get name(): string {
        return this._name;
    }

    public getFrameRotation(cartesian: Vec3): Quat {
        return Quat.IDENTITY;
    }

    /**
     * Adds node to the current hierarchy.
     * @public
     * @type {Scene}
     */
    public addNode(node: Scene) {
        if (this.parentNode == null) {
            node.topNode = this;
        } else {
            node.topNode = this.topNode;
        }
        node.parentNode = this;
        this.childNodes.push(node);
        this.renderer && node.assign(this.renderer);
    }

    /**
     * Assign render node with renderer.
     * @public
     * @param {Renderer} renderer - Render node's renderer.
     */
    public assign(renderer: Renderer) {
        this.renderer = renderer;
        this._pickingId = renderer.addPickingCallback(this, this._entityCollectionPickingCallback);
        this.initialize();
    }

    public initialize() {
        if (this.renderer && this.renderer.isInitialized()) {
            for (let i = 0; i < this.entityCollections.length; i++) {
                this.entityCollections[i].bindScene(this);
            }
            this.init();
        }
    }

    public init() {
        //virtual
    }

    public onremove() {
        //virtual
    }

    public remove() {
        let r = this.renderer,
            n = this.name;

        if (r) {
            // TODO: replace to renderer
            if (r.scenes[n] && r.scenes[n].isEqual(this)) {
                // @ts-ignore
                r.scenes[n] = null;
                delete r.scenes[n];
            }

            for (let i = 0; i < r._scenesArr.length; i++) {
                if (r._scenesArr[i].isEqual(this)) {
                    r._scenesArr.splice(i, 1);
                    break;
                }
            }
            r.removePickingCallback(this._pickingId);
            this._pickingId = -1;
            this.onremove && this.onremove();
        }
    }

    /**
     * Destroy node.
     * @public
     */
    public destroy() {
        for (let i = 0; i < this.childNodes.length; i++) {
            this.childNodes[i].destroy();
        }
        this._clear();
    }

    /**
     * Clear current node.
     * @protected
     */
    protected _clear() {
        this.parentNode = null;
        this.topNode = this;
        this.childNodes.length = 0;
    }

    /**
     * Adds entity collection.
     * @public
     * @param {EntityCollection} entityCollection - Entity collection.
     * @param {boolean} [isHidden] - If it's true that this collection has specific rendering.
     * @returns {Scene} -
     */
    public addEntityCollection(entityCollection: EntityCollection, isHidden?: boolean): void {
        if (!entityCollection.scene) {
            entityCollection.scene = this;

            if (!isHidden) {
                this.entityCollections.push(entityCollection);
                this.updateEntityCollectionsDepthOrder();
            }

            //@ts-ignore
            (this as Planet).ellipsoid && entityCollection._updateGeodeticCoordinates((this as Planet).ellipsoid);

            entityCollection.bindScene(this);

            entityCollection.events.dispatch(entityCollection.events.add, this);
        }
    }

    /**
     * Removes entity collection.
     * @public
     * @param {EntityCollection} entityCollection - Entity collection for remove.
     */
    public removeEntityCollection(entityCollection: EntityCollection) {
        for (let i = 0; i < this.entityCollections.length; i++) {
            if (this.entityCollections[i].isEqual(entityCollection)) {
                this.entityCollections.splice(i, 1);
                this.updateEntityCollectionsDepthOrder();
                return;
            }
        }
    }

    public updateEntityCollectionsDepthOrder() {
        let grouped: Record<number, EntityCollection[]> = { 0: [] };
        for (const ec of this.entityCollections) {
            if (ec.getVisibility()) {
                if (!grouped[ec.depthOrder]) {
                    grouped[ec.depthOrder] = [];
                }
                grouped[ec.depthOrder].push(ec);
            }
        }

        this._entityCollectionsByDepthOrder.length = 0;
        this._entityCollectionsByDepthOrder = [];
        this._entityCollectionsByDepthOrder = Object.keys(grouped)
            .sort((a, b) => Number(a) - Number(b))
            .map((key) => grouped[Number(key)]);
    }

    /**
     * Calls render frame node's callback. Used in renderer.
     * @public
     */
    public preDraw() {
        this._isActive && this._preDraw();
    }

    /**
     * Calls render frame node's callback. Used in renderer.
     * @public
     */
    public draw() {
        this._isActive && this._draw();
    }

    /**
     * Gets render node activity.
     * @public
     * @returns {Boolean} -
     */
    public isActive(): boolean {
        return this._isActive;
    }

    /**
     * Rendering activation.
     * @public
     * @param {boolean} isActive - Activation flag.
     */
    public setActive(isActive: boolean) {
        this._isActive = isActive;

        if (this.renderer) {
            if (this._isActive && this._pickingId === -1) {
                // This picking callback MUST be the first picking callback
                // in the rendering queue in the renderer. It affects on blending.
                this._pickingId = this.renderer.addPickingCallback(this, this._entityCollectionPickingCallback);
            } else if (!this._isActive && this._pickingId !== -1) {
                this.renderer.removePickingCallback(this._pickingId);
                this._pickingId = -1;
            }
        }

        for (let i = 0; i < this.childNodes.length; i++) {
            this.childNodes[i].setActive(isActive);
        }
    }

    /**
     * Sets draw mode
     * @public
     * @param {Number} mode - Draw mode, such as gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.LINES etc.
     */
    public setDrawMode(mode: number) {
        this.drawMode = mode;
        for (let i = 0; i < this.childNodes.length; i++) {
            this.childNodes[i].setDrawMode(mode);
        }
    }

    /*
        @todo: use one atlas for both handlers?
     */
    public updateBillboardsTexCoords() {
        for (let i = 0; i < this.entityCollections.length; i++) {
            this.entityCollections[i].billboardHandler.refreshTexCoordsArr();
        }
    }

    public updateStrokeTexCoords() {
        for (let i = 0; i < this.entityCollections.length; i++) {
            let ei = this.entityCollections[i];
            ei.rayHandler.refreshTexCoordsArr();
            ei.polylineHandler.refreshTexCoordsArr();
            //Strips etc.
            //@todo
        }
    }

    public frame() {
        // virtual
    }

    public preFrame() {
        // virtual
    }

    protected _preDraw() {
        for (let i = 0; i < this.childNodes.length; i++) {
            if (this.childNodes[i]._isActive) {
                this.childNodes[i]._preDraw();
            }
        }

        if (this.show) {
            this.preFrame();
            for (let i = 0; i < this._entityCollectionsByDepthOrder.length; i++) {
                this.drawEntityCollections(this._entityCollectionsByDepthOrder[i], i);
            }
        }
    }

    protected _draw() {
        for (let i = 0; i < this.childNodes.length; i++) {
            if (this.childNodes[i]._isActive) {
                this.childNodes[i]._draw();
            }
        }

        if (this.show) {
            this.frame();
        }
    }

    public drawEntityCollections(ec: EntityCollection[], depthOrder: number = 0) {
        this.renderer!.enqueueEntityCollectionsToDraw(ec, depthOrder);
    }

    /**
     * Draw entity collections picking frame.
     * @public
     * @param {Array<EntityCollection>} ec - Entity collection array.
     */
    public drawPickingEntityCollections(ec: EntityCollection[]) {
        if (ec.length) {
            // billboard pass
            let i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].billboardHandler.drawPicking();
            }

            // geoObject pass
            i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].geoObjectHandler.drawPicking();
            }

            // label pass
            i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].labelHandler.drawPicking();
            }

            // ray pass
            i = ec.length;
            while (i--) {
                ec[i]._fadingOpacity && ec[i].rayHandler.drawPicking();
            }

            // polyline pass
            i = ec.length;
            while (i--) {
                ec[i]._visibility && ec[i].polylineHandler.drawPicking();
            }

            //Strip pass
            i = ec.length;
            while (i--) {
                ec[i]._visibility && ec[i].stripHandler.drawPicking();
            }

            // //pointClouds pass
            // i = ec.length;
            // while (i--) {
            //    ec[i]._visibility && ec[i].pointCloudHandler.drawPicking();
            // }
        }
    }

    protected _entityCollectionPickingCallback() {
        // for (let i = 0; i < this._entityCollectionsByDepthOrder.length; i++) {
        //     this.drawPickingEntityCollections(this._entityCollectionsByDepthOrder[i]);
        // }
    }

    public isEqual(node: Scene): boolean {
        return node.__id === this.__id;
    }
}

export { Scene };