Source: scene/layers/RenderLayer.ts

import { type InstructionSet } from '../../rendering/renderers/shared/instructions/InstructionSet';
import { type Renderer } from '../../rendering/renderers/types';
import { warn } from '../../utils/logging/warn';
import { type Bounds } from '../container/bounds/Bounds';
import { Container } from '../container/Container';

import type EventEmitter from 'eventemitter3';
// TODO make it clear render layer cannot have 'filters'

/**
 * Options for configuring a RenderLayer.
 * @memberof scene
 */
export interface RenderLayerOptions
{
    /**
     * If true, the layer's children will be sorted by zIndex before rendering
     * If false, you can manually sort the children using sortRenderLayerChildren when ever you need!
     * @default false
     */
    sortableChildren?: boolean;

    /**
     * Custom sort function to sort layer children. Default sorts by zIndex.
     * @param a - First container to compare
     * @param b - Second container to compare
     * @returns Negative if a should render before b, positive if b should render before a
     */
    sortFunction?: (a: Container, b: Container) => number;
}

/*
 * Here we are essentially hiding the Container API even though this class extends a Container.
 * This is just so it fits into the current architecture. When users use a RenderLayer,
 * the Container properties will be hidden from them, as they don't do anything in renderLayers.
 */
type ContainerKeys = keyof Container;
type PartialContainerKeys = Exclude<ContainerKeys,
'parent' | 'didChange' | '_updateFlags' | keyof EventEmitter | 'parentRenderLayer' |
'destroyed' | 'layerParentId' | 'sortableChildren' | 'getFastGlobalBounds'
>;
export type IRenderLayer = Omit<RenderLayerClass, PartialContainerKeys>;

/**
 * The RenderLayer API provides a way to control the rendering order of objects independently
 * of their logical parent-child relationships in the scene graph.
 * This allows developers to decouple how objects are transformed
 * (via their logical parent) from how they are rendered on the screen.
 *
 * ### Key Concepts
 *
 * #### RenderLayers Control Rendering Order:
 * - RenderLayers define where in the render stack objects are drawn,
 * but they do not affect an object's transformations (e.g., position, scale, rotation) or logical hierarchy.
 * - RenderLayers can be added anywhere in the scene graph.
 *
 * #### Logical Parenting Remains Unchanged:
 * - Objects still have a logical parent for transformations via addChild.
 * - Assigning an object to a layer does not reparent it.
 *
 * #### Explicit Control:
 * - Developers assign objects to layers using renderLayer.add and remove them using renderLayer.remove.
 * ---
 * ### API Details
 *
 * #### 1. Creating a RenderLayer
 * A RenderLayer is a lightweight object responsible for controlling render order.
 * It has no children or transformations of its own
 * but can be inserted anywhere in the scene graph to define its render position.
 * ```js
 * const layer = new RenderLayer();
 * app.stage.addChild(layer); // Insert the layer into the scene graph
 * ```
 *
 * #### 2. Adding Objects to a Layer
 * Use renderLayer.add to assign an object to a layer.
 * This overrides the object's default render order defined by its logical parent.
 * ```js
 * const rect = new PIXI.Graphics();
 * container.addChild(rect);    // Add to logical parent
 * layer.attach(rect);      // Control render order via the layer
 * ```
 *
 * #### 3. Removing Objects from a Layer
 * To stop an object from being rendered in the layer, use remove.
 * ```js
 * layer.remove(rect); // Stop rendering rect via the layer
 * ```
 * When an object is removed from its logical parent (removeChild), it is automatically removed from the layer.
 *
 * #### 4. Re-Adding Objects to Layers
 * If an object is re-added to a logical parent, it does not automatically reassign itself to the layer.
 * Developers must explicitly reassign it.
 * ```js
 * container.addChild(rect);    // Logical parent
 * layer.attach(rect);      // Explicitly reassign to the layer
 * ```
 *
 * #### 5. Layer Position in Scene Graph
 * A layer's position in the scene graph determines its render priority relative to other layers and objects.
 * Layers can be inserted anywhere in the scene graph.
 * ```js
 * const backgroundLayer = new RenderLayer();
 * const uiLayer = new RenderLayer();
 *
 * app.stage.addChild(backgroundLayer);
 * app.stage.addChild(world);
 * app.stage.addChild(uiLayer);
 * ```
 * This is a new API and therefore considered experimental at this stage.
 * While the core is pretty robust, there are still a few tricky issues we need to tackle.
 * However, even with the known issues below, we believe this API is incredibly useful!
 *
 * Known issues:
 *  - Interaction may not work as expected since hit testing does not account for the visual render order created by layers.
 *    For example, if an object is visually moved to the front via a layer, hit testing will still use its original position.
 *  - RenderLayers and their children must all belong to the same renderGroup to work correctly
 * @memberof scene
 * @class RenderLayer
 * @extends null
 */
export class RenderLayerClass extends Container
{
    /**
     * Default options for RenderLayer instances
     * @property {boolean} sortableChildren - If true, layer children will be automatically sorted each render.
     * Default false.
     * @property {Function} sortFunction - Function used to sort layer children. Default sorts by zIndex.
     */
    public static defaultOptions: RenderLayerOptions = {
        sortableChildren: false,
        sortFunction: (a, b) => a.zIndex - b.zIndex,
    };

    /** Function used to sort layer children if sortableChildren is true */
    public sortFunction: (a: Container, b: Container) => number;

    /** List of objects to be rendered by this layer */
    public renderLayerChildren: Container[] = [];

    /**
     * Creates a new RenderLayer instance
     * @param options - Configuration options for the RenderLayer
     * @param {boolean} [options.sortableChildren=false] - If true, layer children will be automatically sorted each render
     * @param {Function} [options.sortFunction] - Custom function to sort layer children. Default sorts by zIndex
     */
    constructor(options: RenderLayerOptions = {})
    {
        options = { ...RenderLayerClass.defaultOptions, ...options };

        super();

        this.sortableChildren = options.sortableChildren;
        this.sortFunction = options.sortFunction;
    }

    /**
     * Add an Container to this render layer. The Container will be rendered as part of this layer
     * while maintaining its original parent in the scene graph.
     * If the Container already belongs to a layer, it will be removed from the old layer before being added to this one.
     * @param children - The Container(s) to add to this layer
     */
    public attach<U extends Container[]>(...children: U): U[0]
    {
        for (let i = 0; i < children.length; i++)
        {
            const child = children[i];

            if (child.parentRenderLayer)
            {
                if (child.parentRenderLayer === this) continue;

                child.parentRenderLayer.detach(child);
            }

            this.renderLayerChildren.push(child);

            child.parentRenderLayer = this;

            const renderGroup = this.renderGroup || this.parentRenderGroup;

            if (renderGroup)
            {
                renderGroup.structureDidChange = true;
            }
        }

        return children[0];
    }

    /**
     * Remove an Container from this render layer. The Container will no longer be rendered
     * as part of this layer but maintains its original parent.
     * @param children - The Container(s) to remove from this layer
     */
    public detach<U extends Container[]>(...children: U): U[0]
    {
        for (let i = 0; i < children.length; i++)
        {
            const child = children[i];

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

            if (index !== -1)
            {
                this.renderLayerChildren.splice(index, 1);
            }

            child.parentRenderLayer = null;

            const renderGroup = this.renderGroup || this.parentRenderGroup;

            if (renderGroup)
            {
                renderGroup.structureDidChange = true;
            }
        }

        return children[0];
    }

    /** Remove all objects from this render layer. */
    public detachAll()
    {
        const layerChildren = this.renderLayerChildren;

        for (let i = 0; i < layerChildren.length; i++)
        {
            layerChildren[i].parentRenderLayer = null;
        }

        this.renderLayerChildren.length = 0;
    }

    public override collectRenderables(instructionSet: InstructionSet, renderer: Renderer, _currentLayer: RenderLayerClass
    ): void
    {
        const layerChildren = this.renderLayerChildren;
        const length = layerChildren.length;

        if (this.sortableChildren)
        {
            this.sortRenderLayerChildren();
        }

        for (let i = 0; i < length; i++)
        {
            if (!layerChildren[i].parent)
            {
                // eslint-disable-next-line max-len
                warn('Container must be added to both layer and scene graph. Layers only handle render order - the scene graph is required for transforms (addChild)',
                    layerChildren[i]);
            }

            layerChildren[i].collectRenderables(instructionSet, renderer, this);
        }
    }

    /**
     * Sort the layer's children using the defined sort function.
     * Will be called each render if sortableChildren is true.
     * Otherwise can call this manually.
     */
    public sortRenderLayerChildren()
    {
        this.renderLayerChildren.sort(this.sortFunction);
    }

    public override _getGlobalBoundsRecursive(
        factorRenderLayers: boolean,
        bounds: Bounds,
        _currentLayer: RenderLayerClass,
    ): void
    {
        if (!factorRenderLayers) return;

        const children = this.renderLayerChildren;

        for (let i = 0; i < children.length; i++)
        {
            children[i]._getGlobalBoundsRecursive(true, bounds, this);
        }
    }
}

export const RenderLayer = RenderLayerClass as {
    new (options?: RenderLayerOptions): IRenderLayer;
};