Source: export/export_glb.js

import { Reporter } from '../reporter/reporter.js';

import { View } from '../view/view.js';
import { DefaultView } from '../view/default_view.js';

import { GLTFExporter } from '../../node_modules/three/examples/jsm/exporters/GLTFExporter.js';

import { checkPropTypes, UUIDRegex } from '../lib.js';
import { Scene, Group, Box3Helper } from '../../node_modules/three/build/three.module.js';

//These imports are currently done by using an import map in the index.html!

// import { WebIO } from '../../node_modules/@gltf-transform/core/dist/index.modern.js';
// import { dedup, flatten, instance, join, palette, prune, unpartition } from '../../node_modules/@gltf-transform/functions/dist/functions.modern.js';
// import { KHRONOS_EXTENSIONS, KHRDracoMeshCompression, EXTMeshGPUInstancing } from '../../node_modules/@gltf-transform/extensions/dist/index.modern.js';
// import { DracoEncoderModule } from '../../node_modules/three/examples/jsm/libs/draco/draco_encoder.js';

// import * as PropertyGraph from  '../../node_modules/property-graph/dist/property-graph.modern.js';



/**
 * The GLB Exporter exports the scene/confiurations into .glb file.
 * It can return the glb file or it can send it to the server and return a link pointing to the glb file on the server.
 * 
 * https://discourse.threejs.org/t/glb-model-too-heavy/38988
 * 
 * https://github.com/GitHubDragonFly/GitHubDragonFly.github.io/blob/93a6707d44914d27556ac8f8167c0176e86f9c15/viewers/templates/GLTF%20Viewer.html#L1085
 * 
 * https://github.com/google/draco/issues/977
 * https://github.com/mrdoob/three.js/issues/26403
 * 
 * https://github.com/mrdoob/three.js/issues/21492
 * https://gltf-transform.dev/modules/extensions/classes/KHRDracoMeshCompression
 * 
 * GLB Inspector Viewers:
 * https://github.khronos.org/glTF-Validator/
 * https://gltf-viewer.donmccurdy.com/
 * https://sandbox.babylonjs.com/
 */



class GLBExporter {

	constructor(reporter, settings) {
		checkPropTypes(
			settings,
			{},
			{}
		);

		this.params = {
			trs: false,
			onlyVisible: true,
			binary: true,
			maxTextureSize: 4096
		};

		this.tex_fmt = '';
	}

	/* - Methods -  */

	save(blob, filename) {
		const link = document.createElement('a');
		link.style.display = 'none';
		document.body.appendChild(link); // Firefox workaround, see #6594

		link.href = URL.createObjectURL(blob);
		link.download = filename;
		link.click();

		link.remove()
	}

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

	saveString(text, filename) {
		this.save(new Blob([text], { type: 'text/plain' }), filename);
	}

	clearUserData(scene) {
		scene.traverse(function (object) {
			// Clears the userData object
			object.userData = {};
		});
	}

	hideBoundingBoxes(scene){

		scene.traverse(function (child) {
			
			if ( child instanceof Box3Helper ) {
				child.visible=false;
			}
			if ( child.name === "Vertex Helper" ) {
				child.visible=false;
			}
			
		});

	}

	saveArrayBuffer( buffer, filename ) {

		this.save( new Blob( [ buffer ], { type: 'application/octet-stream' } ), filename );
	
	}

	save( blob, filename ) {

		const link = document.createElement( 'a' );

		if ( link.href ) {
	
			URL.revokeObjectURL( link.href );
	
		}
	
		link.href = URL.createObjectURL( blob );
		link.download = filename || 'data.json';
		link.dispatchEvent( new MouseEvent( 'click' ) );
	
	}


	/**
	 * Exports the scene as glb and returns it in a blob.
	 */
	async export(configurators) {

		console.log("Exporting and downloading scene as .glb file")

		if (!Array.isArray(configurators)) {
			console.error("The configurator array is not of type array");
		}
		else if (configurators.length === 0) {
			console.warn("The specified configurator array is empty");
		}
		else {
			const gltfExporter = new GLTFExporter();

			const options = {
				trs: this.params.trs,
				onlyVisible: this.params.onlyVisible,
				binary: this.params.binary,
				maxTextureSize: this.params.maxTextureSize
			};

			const scene = new Scene();

			let fileName;

			// Clone the configurations / configurators
			for (const configurator of configurators) {

				if (configurator.visible) {

					//console.log( configurator.configuration.lastAssignedMaterials )

					

				

					const [ firstKey, firstValue ] = Object.entries(configurator.configuration.lastAssignedMaterials)[0];
					const lastAssignedMaterialName = firstValue.name //null // 
					

					fileName = "scene.glb"
					if (lastAssignedMaterialName) {
						fileName = configurator.configuration.name + "_" + lastAssignedMaterialName + ".glb"
					} else {
						fileName = configurator.configuration.name + ".glb"
					}

					//console.log(fileName)

					// Clone the configuration
					const clonedConfiguration = configurator.configuration.content.main.medium.clone()

					// Add the position of the configurator
					clonedConfiguration.applyMatrix4(configurator.body.matrixWorld)

					// Add the clone to the new scene
					scene.add(clonedConfiguration)
				}
			}

			// Clean the scene of any unwanted data
			this.clearUserData(scene);
			this.hideBoundingBoxes(scene);

			

			// Export the scene as gltf or glb
			// gltfExporter.parse(
			// 	scene,
			// 	async (result) => {
			// 		if (result instanceof ArrayBuffer) {
			// 			const compressedResult = await this.draco_compress(new Uint8Array(result));
			// 			this.saveArrayBuffer(compressedResult, fileName);
			// 		} else {
			// 			const output = JSON.stringify(result, null, 2);
			// 			console.log(output);
			// 			this.saveString(output, 'scene.gltf');
			// 		}
			// 	},
			// 	(error) => {
			// 		console.error('An error happened during parsing', error);
			// 	},
			// 	options
			// );

			// This is directly taken from the threejs editor v173..
			gltfExporter.parse(scene, (result) => {
				this.saveArrayBuffer(result, fileName);
			}, undefined, { binary: true });

		}
	}

	/**
	 * Exports the scene as glb and sends it to the server.
	 */
	exportToServer() {
		console.log("exporting glb to server")
	}


	// this example makes use of draco compression for the glb file size!
	// This code has not been implemented but still uses importmaps!!

	// async export_gltf(binary = false, alternative = false, draco = false, meshopt = false) {
	// 	// Not implemented yet
	// }

      /* Encode with draco compression - geometry */

    async draco_compress( arrayBuffer ) {
		//import("@gltf-transform/core").then( ( module ) => { console.log("gltf-transform", module) }).catch(console.error);
   
		//const { WebIO } = await import("https://esm.sh/@gltf-transform/core");
        const { WebIO } = await import( "@gltf-transform/core" );
        const { dedup, flatten, instance, join, palette, prune, unpartition } = await import( "@gltf-transform/functions" );
        const { KHRONOS_EXTENSIONS, KHRDracoMeshCompression, EXTMeshGPUInstancing } = await import( "@gltf-transform/extensions" );

        const io = new WebIO();

        io.registerExtensions( KHRONOS_EXTENSIONS );
        io.registerExtensions( [ EXTMeshGPUInstancing ] );  // read instanced meshes
        io.registerDependencies({
          'draco3d.encoder': DracoEncoderModule(),
        });

        const doc = await io.readBinary( arrayBuffer ); // read GLB from ArrayBuffer

        await doc.transform(
          unpartition(),
          palette({ min: 5 }),
          dedup(),
          prune(),
          instance( { min: 2 } ),
          flatten(),
          join()
        );

        if (this.tex_fmt !== '') {
          const textures = doc.getRoot().listTextures();

          if (tex_fmt.startsWith( 'ktx' )) {
            const { listTextureSlots } = await import( "@gltf-transform/functions" );
            const { KHRTextureBasisu } = await import( "@gltf-transform/extensions" );
            const { encodeToKTX2 } = await import( "ktx2-encoder" );

            const uastc_textures = [ 'normalTexture', 'occlusionTexture', 'metallicRoughnessTexture' ];

            for (const texture of textures) {
              const slots = await listTextureSlots( texture );
              const size = await texture.getSize();

              const uastc = (tex_fmt.endsWith( 'e' ) === false) && (uastc_textures.some( k => k === slots[ 0 ] ) || tex_fmt.endsWith( 'u' ) === true);

              // check if texture dimensions are multiples of 4
              const width_p2 = size[ 0 ] % 4 === 0;
              const height_p2 = size[ 1 ] % 4 === 0;

              if (!width_p2 || !height_p2) {
                // this should produce minimal scale down
                const width = width_p2 === true ? size[ 0 ] : Math.max( 4, ( size[ 0 ] - ( size[ 0 ] % 4 ) ) );
                const height = height_p2 === true ? size[ 1 ] : Math.max( 4, ( size[ 1 ] - ( size[ 1 ] % 4 ) ) );

                const file_reader = new FileReader();

                await new Promise( resolve => {
                  file_reader.onload = function( e ) {
                    const img = new Image();

                    img.onload = async function() {
                      const canvas = document.createElement('canvas');

                      canvas.width = width;
                      canvas.height = height;

                      const ctx = canvas.getContext('2d');
                      ctx.drawImage( img, 0, 0, canvas.width, canvas.height );

                      const base64data = canvas.toDataURL( `image/${texture.getMimeType()}`, 1 ).replace( /^data:image\/(png|jpg|jpeg);base64,/, '' );
                      const a2b = atob( base64data );
                      const buff = new Uint8Array( a2b.length );

                      for ( let i = 0, l = buff.length; i < l; i ++ ) {
                        buff[ i ] = a2b.charCodeAt( i );
                      }

                      await encodeToKTX2( buff, { isUASTC: uastc } )
                      .then( ktx_texture => {
                        texture.setImage( ktx_texture );
                        texture.setMimeType( 'image/ktx2' );
                        resolve( doc.createExtension( KHRTextureBasisu ).setRequired( true ) );
                      });
                    }

                    img.src = e.target.result;
                  }

                  file_reader.readAsDataURL( new Blob( [ texture.getImage() ], { type: texture.getMimeType() } ));
                });
              } else {
                await encodeToKTX2( texture.getImage(), { isUASTC: uastc } )
                .then( ktx_texture => {
                  texture.setImage( ktx_texture );
                  texture.setMimeType( 'image/ktx2' );
                  doc.createExtension( KHRTextureBasisu ).setRequired( true );
                });
              }
            }
          } else {
            const { compressTexture } = await import( "@gltf-transform/functions" );
            const { EXTTextureWebP } = await import( "@gltf-transform/extensions" );

            for (const texture of textures) {
              await compressTexture( texture, { targetFormat: tex_fmt } );
              doc.createExtension( EXTTextureWebP ).setRequired( true );
              io.registerExtensions( [ EXTTextureWebP ] );
            }
          }
        }

        doc.createExtension( KHRDracoMeshCompression )
        .setRequired( true )
        .setEncoderOptions({
          method: KHRDracoMeshCompression.EncoderMethod.EDGEBREAKER,
          encodeSpeed: 5        
		});

        const compressedArrayBuffer = io.writeBinary( doc );

        for (const texture of doc.getRoot().listTextures()) { texture.dispose(); }
        for (const material of doc.getRoot().listMaterials()) { material.dispose(); }
        for (const mesh of doc.getRoot().listMeshes()) { mesh.dispose(); }

        return compressedArrayBuffer;
    }

}

export { GLBExporter };