Source: utils.js

import * as THREE from "three";
import {GLTFExporter} from "../libs/exporters/GLTFExporter.js";

function notify(message, type) {
    // eslint-disable-next-line no-undef
    Metro.notify.create(message, type, {
        keepOpen: true
    });
}

function randItem(array){
    return array[Math.floor(Math.random() * array.length)];
}

const emptyElem = {
    position: new THREE.Vector3(),
    quaternion: new THREE.Quaternion(),
    scale: new THREE.Vector3(),
    color: new THREE.Color()
};

// https://discourse.threejs.org/t/how-to-ignore-the-transparent-part-of-sprite-by-ray-pickup/7773/3
function getClosestOpaque(intersectObjects) {
    // For every intersected object returned by the raycaster
    for (const intersect of intersectObjects) {
        const {object} = intersect;
        // If the material has a texture
        if (object.material.map) {
            // Get the image from the intersect object mesh
            const {image} = object.material.map;
            // 2D ratio of where the mouse is on the image
            const {uv: {x: UX, y: UY}} = intersect;
            // The actual w, h of the image
            const {width: OW, height: OH} = image;
            // Get the image data
            const imageData = getImageData(image);
            // the X & Y of the image
            const x = Math.round(UX * OW);
            const y = OH - Math.round(UY * OH); // reverse because threejs
            // the index of image data y is every row of pixels, x is on a row of pixels
            const I = (x + y * OW) * 4;
            // the fourth (3) of every 4 numbers is the alpha value
            const A = imageData.data[I + 3];
            // if A is 0 then it's transparent
            if (A === 0) {
                continue;
            }
        }
        return intersect;
    }
}

function getImageData(image) {
    const canvas = document.createElement("canvas");
    const context = canvas.getContext("2d");
    let {naturalWidth: w, naturalHeight: h} = image;
    canvas.width = w;
    canvas.height = h;
    context.drawImage(image, 0, 0, w, h);
    const imageData = context.getImageData(0, 0, w, h);
    return imageData;
}


function exportGLTF(scene, binary=false, name="scene") {
    // Instantiate an exporter
    let exporter = new GLTFExporter();
    let options = {
        binary: binary
    };

    // Removes instances (the glTF exporter cannot yet support instances
    // and the Blender glTF importer doesn't either)
    const deinstancedScene = deinstantiate(scene);

    // Parse the input and generate the glTF output
    exporter.parse(deinstancedScene,
        result => {
            if (result instanceof ArrayBuffer) {
                saveArrayBuffer(result, `${name}.glb`);
            } else {
                let output = JSON.stringify(result, null, 2);
                saveString(output, `${name}.gltf`);
            }
        },
        error => {
            console.log("An error happened during parsing", error);
        },
        options
    );
}

/**
 * Recursive function to replace instanced objects (cohort trees) with ordinary meshes
 * @param {THREE.Object3D} object Root object to deinstantiate
 * @param {Map<number, THREE.Material>} materialMap (optional) Avoids duplicate materials
 * @returns A clone of the object, with all instanced objects replaced with ordinary meshes
 */
function deinstantiate(object, materialMap) {
    if (materialMap === undefined) {
        materialMap = new Map();
    }

    if (object.isInstancedMesh !== true) {
        const clone = object.clone(false);
        if (object.children.length > 0) {
            // Recursively call this function for all children
            clone.children = object.children.map(c=>deinstantiate(c, materialMap));
        }
        return clone;
    } else {
        const count = object.count;
        const matrix = new THREE.Matrix4();
        const color = new THREE.Color();

        const group = new THREE.Group();
        for (let i = 0; i < count; i++) {
            object.getColorAt(i, color);
            const hexColor = color.getHex();

            if (!materialMap.has(hexColor)) {
                const m = object.material.clone();
                m.color.copy(color);
                materialMap.set(hexColor, m);
            }
            const mesh = new THREE.Mesh(
                object.geometry,
                materialMap.get(hexColor)
            );

            object.getMatrixAt(i, matrix);
            matrix.decompose(
                mesh.position,
                mesh.quaternion,
                mesh.scale
            );

            // Don't include empty elements
            // Otherwise all cohorts get 1000 meshes
            if (!mesh.scale.equals(emptyElem.scale)) {
                group.add(mesh);
            }
        }
        return group;
    }
}

const link = document.createElement("a");
link.style.display = "none";
document.body.appendChild(link); // Firefox workaround, see #6594 threejs
function save( blob, filename ) {
    link.href = URL.createObjectURL(blob);
    link.download = filename;
    link.click();
}

function saveString( text, filename ) {
    save(new Blob([text], {type: "text/plain"}), filename);
}

function saveArrayBuffer(buffer, filename) {
    save(new Blob([buffer], {type: "application/octet-stream"}), filename);
}

export {notify, randItem, emptyElem, getClosestOpaque, exportGLTF, saveString};