Source: Util.js

// HELPERS
window.structuredClone = typeof (structuredClone) == "function" ? structuredClone : function (value) { return JSON.parse(JSON.stringify(value)); };

/**
 * Utility class providing various helper functions for OpenLIME.
 * Includes methods for SVG manipulation, file loading, image processing, and string handling.
 * 
 * 
 * @static
 */
class Util {

    /**
     * Pads a number with leading zeros
     * @param {number} num - Number to pad
     * @param {number} size - Desired string length
     * @returns {string} Zero-padded number string
     * 
     * @example
     * ```javascript
     * Util.padZeros(42, 5); // Returns "00042"
     * ```
     */
    static padZeros(num, size) {
        num = num.toString();
        while (num.length < size) num = "0" + num;
        return num;
    }

    /**
     * Prints source code with line numbers
     * Useful for shader debugging
     * @param {string} str - Source code to print
     * @private
     */
    static printSrcCode(str) {
        let i = 1;
        let result = '';
        for (let l of str.split(/\r\n|\r|\n/)) {
            const nline = Util.padZeros(i, 5);
            result += nline + '   ' + l + '\n';
            i++;
        }
        console.log(result);
    }

    /**
     * Creates an SVG element with optional attributes
     * @param {string} tag - SVG element tag name
     * @param {Object} [attributes] - Key-value pairs of attributes
     * @returns {SVGElement} Created SVG element
     * 
     * @example
     * ```javascript
     * const circle = Util.createSVGElement('circle', {
     *     cx: '50',
     *     cy: '50',
     *     r: '40'
     * });
     * ```
     */
    static createSVGElement(tag, attributes) {
        let e = document.createElementNS('http://www.w3.org/2000/svg', tag);
        if (attributes)
            for (const [key, value] of Object.entries(attributes))
                e.setAttribute(key, value);
        return e;
    }

    /**
     * Parses SVG string into DOM element
     * @param {string} text - SVG content string
     * @returns {SVGElement} Parsed SVG element
     * @throws {Error} If parsing fails
     */
    static SVGFromString(text) {
        const parser = new DOMParser();
        return parser.parseFromString(text, "image/svg+xml").documentElement;
    }

    /**
     * Loads SVG file from URL
     * @param {string} url - URL to SVG file
     * @returns {Promise<SVGElement>} Loaded and parsed SVG
     * @throws {Error} If fetch fails or content isn't SVG
     * 
     * @example
     * ```javascript
     * const svg = await Util.loadSVG('icons/icon.svg');
     * document.body.appendChild(svg);
     * ```
     */
    static async loadSVG(url) {
        let response = await fetch(url);
        if (!response.ok) {
            const message = `An error has occured: ${response.status}`;
            throw new Error(message);
        }
        let data = await response.text();
        let result = null;
        if (Util.isSVGString(data)) {
            result = Util.SVGFromString(data);
        } else {
            const message = `${url} is not an SVG file`;
            throw new Error(message);
        }
        return result;
    };

    /**
     * Loads HTML content from URL
     * @param {string} url - URL to HTML file
     * @returns {Promise<string>} HTML content
     * @throws {Error} If fetch fails
     */
    static async loadHTML(url) {
        let response = await fetch(url);
        if (!response.ok) {
            const message = `An error has occured: ${response.status}`;
            throw new Error(message);
        }
        let data = await response.text();
        return data;
    };

    /**
     * Loads and parses JSON from URL
     * @param {string} url - URL to JSON file
     * @returns {Promise<Object>} Parsed JSON data
     * @throws {Error} If fetch or parsing fails
     */
    static async loadJSON(url) {
        let response = await fetch(url);
        if (!response.ok) {
            const message = `An error has occured: ${response.status}`;
            throw new Error(message);
        }
        let data = await response.json();
        return data;
    }

    /**
     * Loads image from URL
     * @param {string} url - Image URL
     * @returns {Promise<HTMLImageElement>} Loaded image
     * @throws {Error} If image loading fails
     */
    static async loadImage(url) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.addEventListener('load', () => resolve(img));
            img.addEventListener('error', (err) => reject(err));
            img.src = url;
        });
    }

    /**
     * Appends loaded image to container
     * @param {HTMLElement} container - Target container
     * @param {string} url - Image URL
     * @param {string} [imgClass] - Optional CSS class
     * @returns {Promise<void>}
     */
    static async appendImg(container, url, imgClass = null) {
        const img = await Util.loadImage(url);
        if (imgClass) img.classList.add(imgClass);
        container.appendChild(img);
        return img;
    }

    /**
      * Appends multiple images to container
      * @param {HTMLElement} container - Target container
      * @param {string[]} urls - Array of image URLs
      * @param {string} [imgClass] - Optional CSS class
      * @returns {Promise<void>}
      */
    static async appendImgs(container, urls, imgClass = null) {
        for (const u of urls) {
            const img = await Util.loadImage(u);
            if (imgClass) img.classList.add(imgClass);
            container.appendChild(img);
        }
    }

    /**
     * Tests if string is valid SVG content
     * @param {string} input - String to test
     * @returns {boolean} True if string is valid SVG
     */
    static isSVGString(input) {
        const regex = /^\s*(?:<\?xml[^>]*>\s*)?(?:<!doctype svg[^>]*\s*(?:\[?(?:\s*<![^>]*>\s*)*\]?)*[^>]*>\s*)?(?:<svg[^>]*>[^]*<\/svg>|<svg[^/>]*\/\s*>)\s*$/i
        if (input == undefined || input == null)
            return false;
        input = input.toString().replace(/\s*<!Entity\s+\S*\s*(?:"|')[^"]+(?:"|')\s*>/img, '');
        input = input.replace(/<!--([\s\S]*?)-->/g, '');
        return Boolean(input) && regex.test(input);
    }

    /**
     * Computes Signed Distance Field from image data
     * Implementation based on Felzenszwalb & Huttenlocher algorithm
     * 
     * @param {Uint8Array} buffer - Input image data
     * @param {number} w - Image width
     * @param {number} h - Image height
     * @param {number} [cutoff=0.25] - Distance field cutoff
     * @param {number} [radius=8] - Maximum distance to compute
     * @returns {Float32Array|Array} Computed distance field
     * 
     * Technical Details:
     * - Uses 2D Euclidean distance transform
     * - Separate inner/outer distance fields
     * - Optimized grid computation
     * - Sub-pixel accuracy
     */
    static computeSDF(buffer, w, h, cutoff = 0.25, radius = 8) {

        // 2D Euclidean distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/dt/
        function edt(data, width, height, f, d, v, z) {
            for (let x = 0; x < width; x++) {
                for (let y = 0; y < height; y++) {
                    f[y] = data[y * width + x]
                }
                edt1d(f, d, v, z, height)
                for (let y = 0; y < height; y++) {
                    data[y * width + x] = d[y]
                }
            }
            for (let y = 0; y < height; y++) {
                for (let x = 0; x < width; x++) {
                    f[x] = data[y * width + x]
                }
                edt1d(f, d, v, z, width)
                for (let x = 0; x < width; x++) {
                    data[y * width + x] = Math.sqrt(d[x])
                }
            }
        }

        // 1D squared distance transform
        function edt1d(f, d, v, z, n) {
            v[0] = 0;
            z[0] = -INF
            z[1] = +INF

            for (let q = 1, k = 0; q < n; q++) {
                var s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k])
                while (s <= z[k]) {
                    k--
                    s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k])
                }
                k++
                v[k] = q
                z[k] = s
                z[k + 1] = +INF
            }

            for (let q = 0, k = 0; q < n; q++) {
                while (z[k + 1] < q) k++
                d[q] = (q - v[k]) * (q - v[k]) + f[v[k]]
            }
        }

        var data = new Uint8ClampedArray(buffer);
        const INF = 1e20;
        const size = Math.max(w, h);

        // temporary arrays for the distance transform
        const gridOuter = Array(w * h);
        const gridInner = Array(w * h);
        const f = Array(size);
        const d = Array(size);
        const z = Array(size + 1);
        const v = Array(size);

        for (let i = 0; i < w * h; i++) {
            var a = data[i] / 255.0;
            gridOuter[i] = a === 1 ? 0 : a === 0 ? INF : Math.pow(Math.max(0, 0.5 - a), 2);
            gridInner[i] = a === 1 ? INF : a === 0 ? 0 : Math.pow(Math.max(0, a - 0.5), 2);
        }

        edt(gridOuter, w, h, f, d, v, z)
        edt(gridInner, w, h, f, d, v, z)

        const dist = window.Float32Array ? new Float32Array(w * h) : new Array(w * h)

        for (let i = 0; i < w * h; i++) {
            dist[i] = Math.min(Math.max(1 - ((gridOuter[i] - gridInner[i]) / radius + cutoff), 0), 1)
        }
        return dist;
    }

    /**
     * Rasterizes SVG to ImageData
     * @param {string} url - SVG URL
     * @param {number[]} [size=[64,64]] - Output dimensions [width, height]
     * @returns {Promise<ImageData>} Rasterized image data
     * 
     * Processing steps:
     * 1. Loads SVG file
     * 2. Sets up canvas context
     * 3. Handles aspect ratio
     * 4. Centers image
     * 5. Renders to ImageData
     * 
     * @example
     * ```javascript
     * const imageData = await Util.rasterizeSVG('icon.svg', [128, 128]);
     * context.putImageData(imageData, 0, 0);
     * ```
     */
    static async rasterizeSVG(url, size = [64, 64]) {
        const svg = await Util.loadSVG(url);
        const svgWidth = svg.getAttribute('width');
        const svgHeight = svg.getAttribute('height');

        const canvas = document.createElement("canvas");
        canvas.width = size[0];
        canvas.height = size[1];

        svg.setAttributeNS(null, 'width', `100%`);
        svg.setAttributeNS(null, 'height', `100%`);

        const ctx = canvas.getContext("2d");
        const data = (new XMLSerializer()).serializeToString(svg);
        const DOMURL = window.URL || window.webkitURL || window;

        const img = new Image();
        const svgBlob = new Blob([data], { type: 'image/svg+xml;charset=utf-8' });
        const svgurl = DOMURL.createObjectURL(svgBlob);
        img.src = svgurl;

        return new Promise((resolve, reject) => {
            img.onload = () => {
                const aCanvas = size[0] / size[1];
                const aSvg = svgWidth / svgHeight;
                let wSvg = 0;
                let hSvg = 0;
                if (aSvg < aCanvas) {
                    hSvg = size[1];
                    wSvg = hSvg * aSvg;
                } else {
                    wSvg = size[0];
                    hSvg = wSvg / aSvg;
                }

                let dy = (size[1] - hSvg) * 0.5;
                let dx = (size[0] - wSvg) * 0.5;

                ctx.translate(dx, dy);
                ctx.drawImage(img, 0, 0);

                DOMURL.revokeObjectURL(svgurl);

                const imageData = ctx.getImageData(0, 0, size[0], size[1]);

                // const imgURI = canvas
                //     .toDataURL('image/png')
                //     .replace('image/png', 'image/octet-stream');

                // console.log(imgURI);

                resolve(imageData);
            };
            img.onerror = (e) => reject(e);
        });
    }

}

/**
 * Implementation Notes:
 * 
 * File Loading:
 * - Consistent error handling across loaders
 * - Promise-based async operations
 * - Resource cleanup (URL revocation)
 * 
 * SVG Processing:
 * - Namespace-aware element creation
 * - Robust SVG validation
 * - Attribute management
 * 
 * Image Processing:
 * - Canvas-based rasterization
 * - Aspect ratio preservation
 * - Memory efficient operations
 * 
 * SDF Computation:
 * - Efficient distance field generation
 * - Configurable parameters
 * - TypedArray support
 * 
 * Error Handling:
 * - Input validation
 * - Descriptive error messages
 * - Resource cleanup on failure
 * 
 * Browser Compatibility:
 * - Fallbacks for older browsers
 * - Polyfill for structuredClone
 * - Vendor prefix handling
 */

export { Util }