Source: dom/DOMPipe.ts

import { ExtensionType } from '../extensions/Extensions';
import { type DOMContainer } from './DOMContainer';

import type { InstructionSet } from '../rendering/renderers/shared/instructions/InstructionSet';
import type { RenderPipe } from '../rendering/renderers/shared/instructions/RenderPipe';
import type { Renderer } from '../rendering/renderers/types';
import type { Container } from '../scene/container/Container';

/**
 * The DOMPipe class is responsible for managing and rendering DOM elements within a PixiJS scene.
 * It maps dom elements to the canvas and ensures they are correctly positioned and visible.
 */
export class DOMPipe implements RenderPipe<DOMContainer>
{
    /**
     * Static property defining the extension type and name for the DOMPipe.
     * This is used to register the DOMPipe with different rendering pipelines.
     */
    public static extension = {
        type: [
            ExtensionType.WebGLPipes,
            ExtensionType.WebGPUPipes,
            ExtensionType.CanvasPipes,
        ],
        name: 'dom',
    } as const;

    private _renderer: Renderer;
    private readonly _destroyRenderableBound = this.destroyRenderable.bind(this) as (renderable: Container) => void;

    /** Array to keep track of attached DOM elements */
    private readonly _attachedDomElements: DOMContainer[] = [];
    /** The main DOM element that acts as a container for other DOM elements */
    private readonly _domElement: HTMLDivElement;

    /**
     * Constructor for the DOMPipe class.
     * @param renderer - The renderer instance that this DOMPipe will be associated with.
     */
    constructor(renderer: Renderer)
    {
        this._renderer = renderer;

        // Add this DOMPipe to the postrender runner of the renderer
        // we want to dom elements are calculated after all things have been rendered
        this._renderer.runners.postrender.add(this);

        // Create a main DOM element to contain other DOM elements
        this._domElement = document.createElement('div');
        this._domElement.style.position = 'absolute';
        this._domElement.style.top = '0';
        this._domElement.style.left = '0';
        this._domElement.style.pointerEvents = 'none';
        this._domElement.style.zIndex = '1000';
    }

    /**
     * Adds a renderable DOM container to the list of attached elements.
     * @param domContainer - The DOM container to be added.
     * @param _instructionSet - The instruction set (unused).
     */
    public addRenderable(domContainer: DOMContainer, _instructionSet: InstructionSet): void
    {
        if (!this._attachedDomElements.includes(domContainer))
        {
            this._attachedDomElements.push(domContainer);
            domContainer.on('destroyed', this._destroyRenderableBound);
        }
    }

    /**
     * Updates a renderable DOM container.
     * @param _domContainer - The DOM container to be updated (unused).
     */
    public updateRenderable(_domContainer: DOMContainer): void
    {
        // Updates happen in postrender
    }

    /**
     * Validates a renderable DOM container.
     * @param _domContainer - The DOM container to be validated (unused).
     * @returns Always returns true as validation is not required.
     */
    public validateRenderable(_domContainer: DOMContainer): boolean
    {
        return true;
    }

    /**
     * Destroys a renderable DOM container, removing it from the list of attached elements.
     * @param domContainer - The DOM container to be destroyed.
     */
    public destroyRenderable(domContainer: DOMContainer): void
    {
        const index = this._attachedDomElements.indexOf(domContainer);

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

        domContainer.off('destroyed', this._destroyRenderableBound);
    }

    /** Handles the post-rendering process, ensuring DOM elements are correctly positioned and visible. */
    public postrender(): void
    {
        const attachedDomElements = this._attachedDomElements;

        if (attachedDomElements.length === 0)
        {
            this._domElement.remove();

            return;
        }

        const canvas = this._renderer.view.canvas as HTMLCanvasElement;

        if (this._domElement.parentNode !== canvas.parentNode)
        {
            canvas.parentNode?.appendChild(this._domElement);
        }

        this._domElement.style.transform = `translate(${canvas.offsetLeft}px, ${canvas.offsetTop}px)`;

        for (let i = 0; i < attachedDomElements.length; i++)
        {
            const domContainer = attachedDomElements[i];
            const element = domContainer.element;

            if (!domContainer.parent || domContainer.globalDisplayStatus < 0b111)
            {
                element.remove();
                attachedDomElements.splice(i, 1);
                i--;
            }
            else
            {
                if (!this._domElement.contains(element))
                {
                    element.style.position = 'absolute';
                    element.style.pointerEvents = 'auto';
                    this._domElement.appendChild(element);
                }

                const wt = domContainer.worldTransform;
                const anchor = domContainer._anchor;
                const ax = domContainer.width * anchor.x;
                const ay = domContainer.height * anchor.y;

                element.style.transformOrigin = `${ax}px ${ay}px`;
                element.style.transform = `matrix(${wt.a}, ${wt.b}, ${wt.c}, ${wt.d}, ${wt.tx - ax}, ${wt.ty - ay})`;
                element.style.opacity = domContainer.groupAlpha.toString();
            }
        }
    }

    /** Destroys the DOMPipe, removing all attached DOM elements and cleaning up resources. */
    public destroy(): void
    {
        this._renderer.runners.postrender.remove(this);

        for (let i = 0; i < this._attachedDomElements.length; i++)
        {
            const domContainer = this._attachedDomElements[i];

            domContainer.off('destroyed', this._destroyRenderableBound);
            domContainer.element.remove();
        }

        this._attachedDomElements.length = 0;
        this._domElement.remove();
        this._renderer = null;
    }
}