Source: scene/container/RenderGroup.ts

import { Matrix } from '../../maths/matrix/Matrix';
import { InstructionSet } from '../../rendering/renderers/shared/instructions/InstructionSet';

import type { Instruction } from '../../rendering/renderers/shared/instructions/Instruction';
import type { Container } from './Container';

/**
 * The render group is the base class for all render groups
 * It is used to render a group of containers together
 * @memberof rendering
 */
export class RenderGroup implements Instruction
{
    public renderPipeId = 'renderGroup';
    public root: Container = null;

    public canBundle = false;

    public renderGroupParent: RenderGroup = null;
    public renderGroupChildren: RenderGroup[] = [];

    private readonly _children: Container[] = [];

    public worldTransform: Matrix = new Matrix();
    public worldColorAlpha = 0xffffffff;
    public worldColor = 0xffffff;
    public worldAlpha = 1;

    // these updates are transform changes..
    public readonly childrenToUpdate: Record<number, { list: Container[]; index: number; }> = Object.create(null);
    public updateTick = 0;

    // these update are renderable changes..
    public readonly childrenRenderablesToUpdate: { list: Container[]; index: number; } = { list: [], index: 0 };

    // other
    public structureDidChange = true;

    public instructionSet: InstructionSet = new InstructionSet();

    private readonly _onRenderContainers: Container[] = [];

    constructor(root: Container)
    {
        this.root = root;

        this.addChild(root);
    }

    get localTransform()
    {
        return this.root.localTransform;
    }

    public addRenderGroupChild(renderGroupChild: RenderGroup)
    {
        if (renderGroupChild.renderGroupParent)
        {
            renderGroupChild.renderGroupParent._removeRenderGroupChild(renderGroupChild);
        }

        renderGroupChild.renderGroupParent = this;

        this.onChildUpdate(renderGroupChild.root);

        this.renderGroupChildren.push(renderGroupChild);
    }

    private _removeRenderGroupChild(renderGroupChild: RenderGroup)
    {
        if (renderGroupChild.root.didChange)
        {
            this._removeChildFromUpdate(renderGroupChild.root);
        }

        const index = this.renderGroupChildren.indexOf(renderGroupChild);

        if (index > -1)
        {
            this.renderGroupChildren.splice(index, 1);
        }

        renderGroupChild.renderGroupParent = null;
    }

    public addChild(child: Container)
    {
        this.structureDidChange = true;

        // TODO this can be optimized..
        if (child !== this.root)
        {
            this._children.push(child);

            child.updateTick = -1;

            if (child.parent === this.root)
            {
                child.relativeRenderGroupDepth = 1;
            }

            else
            {
                child.relativeRenderGroupDepth = child.parent.relativeRenderGroupDepth + 1;
            }

            if (child._onRender)
            {
                this.addOnRender(child);
            }
        }

        if (child.renderGroup)
        {
            if (child.renderGroup.root === child)
            {
                // its already its own render group..
                this.addRenderGroupChild(child.renderGroup);

                return;
            }
        }
        else
        {
            child.renderGroup = this;
            child.didChange = true;
        }

        const children = child.children;

        if (!child.isRenderGroupRoot)
        {
            this.onChildUpdate(child);
        }

        for (let i = 0; i < children.length; i++)
        {
            this.addChild(children[i]);
        }
    }

    public removeChild(child: Container)
    {
        // remove all the children...
        this.structureDidChange = true;

        if (child._onRender)
        {
            this.removeOnRender(child);
        }

        if (child.renderGroup.root !== child)
        {
            const children = child.children;

            for (let i = 0; i < children.length; i++)
            {
                this.removeChild(children[i]);
            }

            if (child.didChange)
            {
                child.renderGroup._removeChildFromUpdate(child);
            }

            child.renderGroup = null;
        }

        else
        {
            this._removeRenderGroupChild(child.renderGroup);
        }

        const index = this._children.indexOf(child);

        if (index > -1)
        {
            this._children.splice(index, 1);
        }
    }

    public onChildUpdate(child: Container)
    {
        let childrenToUpdate = this.childrenToUpdate[child.relativeRenderGroupDepth];

        if (!childrenToUpdate)
        {
            childrenToUpdate = this.childrenToUpdate[child.relativeRenderGroupDepth] = {
                index: 0,
                list: [],
            };
        }

        childrenToUpdate.list[childrenToUpdate.index++] = child;
    }

    // SHOULD THIS BE HERE?
    public updateRenderable(container: Container)
    {
        // only update if its visible!
        if (container.globalDisplayStatus < 0b111) return;

        container.didViewUpdate = false;
        // actually updates the renderable..
        this.instructionSet.renderPipes[container.renderPipeId].updateRenderable(container);
    }

    public onChildViewUpdate(child: Container)
    {
        this.childrenRenderablesToUpdate.list[this.childrenRenderablesToUpdate.index++] = child;
    }

    private _removeChildFromUpdate(child: Container)
    {
        const childrenToUpdate = this.childrenToUpdate[child.relativeRenderGroupDepth];

        if (!childrenToUpdate)
        { return; }

        const index = childrenToUpdate.list.indexOf(child);

        // TODO this should be optimized - don't really want to change array size on the fly if we can avoid!
        if (index > -1)
        {
            childrenToUpdate.list.splice(index, 1);
        }

        childrenToUpdate.index--;
    }

    get isRenderable(): boolean
    {
        return (this.root.localDisplayStatus === 0b111 && this.worldAlpha > 0);
    }

    /**
     * adding a container to the onRender list will make sure the user function
     * passed in to the user defined 'onRender` callBack
     * @param container - the container to add to the onRender list
     */
    public addOnRender(container: Container)
    {
        this._onRenderContainers.push(container);
    }

    public removeOnRender(container: Container)
    {
        this._onRenderContainers.splice(this._onRenderContainers.indexOf(container), 1);
    }

    public runOnRender()
    {
        for (let i = 0; i < this._onRenderContainers.length; i++)
        {
            this._onRenderContainers[i]._onRender();
        }
    }
}