Source: LayerCombiner.js

import { Layer } from './Layer.js'

/**
 * @typedef {Object} LayerCombinerOptions
 * @property {Layer[]} layers - Array of layers to be combined (required)
 * @property {Object.<string, Shader>} [shaders] - Map of available shaders
 * @property {string} [type='combiner'] - Must be 'combiner' when using Layer factory
 * @property {boolean} [visible=true] - Whether the combined output is visible
 * @extends LayerOptions
 */

/**
 * LayerCombiner provides functionality to combine multiple layers using framebuffer operations
 * and custom shaders. It enables complex visual effects by compositing layers in real-time
 * using GPU-accelerated rendering.
 * 
 * Features:
 * - Real-time layer composition
 * - Custom shader-based effects
 * - Framebuffer management
 * - Dynamic texture allocation
 * - Resolution-independent rendering
 * - GPU-accelerated compositing
 * 
 * Use Cases:
 * - Layer blending and mixing
 * - Image comparison tools
 * - Lens effects (see {@link LayerLens})
 * - Custom visual effects
 * - Multi-layer compositing
 * 
 * Technical Details:
 * - Creates framebuffers for each input layer
 * - Manages WebGL textures and resources
 * - Supports dynamic viewport resizing
 * - Handles shader-based composition
 * - Maintains proper resource cleanup
 * 
 * @extends Layer
 * 
 * @example
 * ```javascript
 * // Create two base layers
 * const baseLayer = new OpenLIME.Layer({
 *   type: 'image',
 *   url: 'base.jpg'
 * });
 * 
 * const overlayLayer = new OpenLIME.Layer({
 *   type: 'image',
 *   url: 'overlay.jpg'
 * });
 * 
 * // Create combiner with custom shader
 * const combiner = new OpenLIME.Layer({
 *   type: 'combiner',
 *   layers: [baseLayer, overlayLayer],
 *   visible: true
 * });
 * 
 * // Set up blend shader
 * const shader = new OpenLIME.ShaderCombiner();
 * shader.mode = 'blend';
 * combiner.shaders = { 'standard': shader };
 * combiner.setShader('standard');
 * 
 * // Add to viewer
 * viewer.addLayer('combined', combiner);
 * ```
 */
class LayerCombiner extends Layer {
    /**
     * Creates a new LayerCombiner instance.
     * 
     * @param {LayerCombinerOptions} options - Configuration options
     * @throws {Error} If rasters option is not empty (rasters should be defined in source layers)
     */
    constructor(options) {
        options = Object.assign({
            isLinear: true,
        }, options);

        super(options);

        if (Object.keys(this.rasters).length != 0)
            throw "Rasters options should be empty!";

        this.textures = [];
        this.framebuffers = [];
        this.status = 'ready';
    }

    /**
     * Cleans up WebGL resources by deleting framebuffers and textures.
     * Should be called before recreating buffers or when the layer is destroyed.
     * Prevents memory leaks by properly releasing GPU resources.
     * @private
     */
    deleteFramebuffers() {
        if (!this.gl) return;

        // Clean up textures
        for (let i = 0; i < this.textures.length; i++) {
            if (this.textures[i]) {
                this.gl.deleteTexture(this.textures[i]);
            }
        }

        // Clean up framebuffers
        for (let i = 0; i < this.framebuffers.length; i++) {
            if (this.framebuffers[i]) {
                this.gl.deleteFramebuffer(this.framebuffers[i]);
            }
        }

        this.textures = [];
        this.framebuffers = [];
    }

    /**
     * Renders the combined layers using framebuffer operations.
     * Handles framebuffer creation, layer rendering, and final composition.
     * 
     * @param {Transform} transform - Current view transform
     * @param {Object} viewport - Current viewport parameters
     * @param {number} viewport.x - Viewport X position
     * @param {number} viewport.y - Viewport Y position
     * @param {number} viewport.dx - Viewport width
     * @param {number} viewport.dy - Viewport height
     * @param {number} viewport.w - Total width
     * @param {number} viewport.h - Total height
     * @throws {Error} If shader is not specified
     * @private
     */
    draw(transform, viewport) {
        for (let layer of this.layers)
            if (layer.status != 'ready')
                return;

        if (!this.shader)
            throw "Shader not specified!";

        let w = viewport.dx;
        let h = viewport.dy;

        // Recreate framebuffers if viewport size changes
        if (!this.framebuffers.length || this.layout.width != w || this.layout.height != h) {
            this.deleteFramebuffers();
            this.layout.width = w;
            this.layout.height = h;
            this.createFramebuffers();
        }

        let gl = this.gl;
        var b = [0, 0, 0, 0];
        gl.clearColor(b[0], b[1], b[2], b[3]);

        // Save the active framebuffer before starting operations
        const activeFramebuffer = this.canvas.getActiveFramebuffer();

        // Render each layer to its corresponding framebuffer
        for (let i = 0; i < this.layers.length; i++) {
            gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[i]);
            gl.clear(gl.COLOR_BUFFER_BIT);
            this.layers[i].draw(transform, { x: 0, y: 0, dx: w, dy: h, w: w, h: h });
        }

        // Restore the active framebuffer for final rendering
        this.canvas.setActiveFramebuffer(activeFramebuffer);

        this.prepareWebGL();

        // Bind textures and set shader uniforms
        for (let i = 0; i < this.layers.length; i++) {
            gl.uniform1i(this.shader.samplers[i].location, i);
            gl.activeTexture(gl.TEXTURE0 + i);
            gl.bindTexture(gl.TEXTURE_2D, this.textures[i]);
        }

        // Update tile buffers and draw the final composition
        this.updateTileBuffers(
            new Float32Array([-1, -1, 0, -1, 1, 0, 1, 1, 0, 1, -1, 0]),
            new Float32Array([0, 0, 0, 1, 1, 1, 1, 0]));
        gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
    }

    /**
     * Creates framebuffers and textures for layer composition.
     * Initializes WebGL resources for each input layer.
     * @private
     */
    createFramebuffers() {
        let gl = this.gl;
        for (let i = 0; i < this.layers.length; i++) {
            const texture = gl.createTexture();

            gl.bindTexture(gl.TEXTURE_2D, texture);

            const level = 0;
            const internalFormat = gl.RGBA;
            const border = 0;
            const format = gl.RGBA;
            const type = gl.UNSIGNED_BYTE;
            gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
                this.layout.width, this.layout.height, border, format, type, null);

            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

            const framebuffer = gl.createFramebuffer();
            gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
            gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);

            // Verify that the framebuffer is complete
            const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
            if (status !== gl.FRAMEBUFFER_COMPLETE) {
                console.error("LayerCombiner framebuffer not complete. Status:", status);
            }

            gl.bindFramebuffer(gl.FRAMEBUFFER, null);

            this.textures[i] = texture;
            this.framebuffers[i] = framebuffer;
        }
    }

    /**
     * Computes the combined bounding box of all input layers.
     * 
     * @returns {BoundingBox} Combined bounding box
     * @override
     * @private
     */
    boundingBox() {
        const discardHidden = false;
        let result = Layer.computeLayersBBox(this.layers, discardHidden);
        if (result && this.transform != null && this.transform != undefined) {
            result = this.transform.transformBox(result);
        }
        return result;
    }

    /**
     * Computes the minimum scale across all input layers.
     * 
     * @returns {number} Combined scale factor
     * @override
     * @private
     */
    scale() {
        const discardHidden = false;
        let scale = Layer.computeLayersMinScale(this.layers, discardHidden);
        scale *= this.transform.z;
        return scale;
    }
}

/**
 * Registers this layer type with the Layer factory.
 * 
 * @type {Function}
 * @private
 */
Layer.prototype.types['combiner'] = (options) => { return new LayerCombiner(options); }

export { LayerCombiner }