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 };