You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
577 lines
12 KiB
577 lines
12 KiB
import * as THREE from 'three';
|
|
|
|
import { zipSync, strToU8 } from 'three/addons/libs/fflate.module.js';
|
|
|
|
import { UIPanel, UIRow, UIHorizontalRule } from '../libs/ui.js';
|
|
import { AddObjectCommand } from '../commands/AddObjectCommand.js';
|
|
function MenubarFile( editor ) {
|
|
|
|
const config = editor.config;
|
|
const strings = editor.strings;
|
|
|
|
const container = new UIPanel();
|
|
container.setClass( 'menu' );
|
|
|
|
const title = new UIPanel();
|
|
title.setClass( 'title' );
|
|
title.setTextContent( strings.getKey( 'menubar/file' ) );
|
|
container.add( title );
|
|
|
|
const options = new UIPanel();
|
|
options.setClass( 'options' );
|
|
container.add( options );
|
|
|
|
// New
|
|
|
|
let option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/new' ) );
|
|
option.onClick( function () {
|
|
|
|
if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) {
|
|
|
|
editor.clear();
|
|
initLight();
|
|
|
|
|
|
}
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
function initLight( ) {
|
|
const color = 0x222222;
|
|
|
|
const light = new THREE.AmbientLight(color);
|
|
light.name = '环境光源AmbientLight';
|
|
|
|
editor.execute(new AddObjectCommand(editor, light));
|
|
|
|
const color1 = 0xffffff;
|
|
const intensity = 1;
|
|
|
|
const directionalLight = new THREE.DirectionalLight(color1, intensity);
|
|
directionalLight.name = '平行光源DirectionalLight';
|
|
directionalLight.target.name = 'DirectionalLight Target';
|
|
|
|
directionalLight.position.set(5, 10, 7.5);
|
|
|
|
editor.execute(new AddObjectCommand(editor, directionalLight));
|
|
}
|
|
//
|
|
|
|
options.add( new UIHorizontalRule() );
|
|
|
|
// Import
|
|
|
|
const form = document.createElement( 'form' );
|
|
form.style.display = 'none';
|
|
document.body.appendChild( form );
|
|
|
|
const fileInput = document.createElement( 'input' );
|
|
fileInput.multiple = true;
|
|
fileInput.type = 'file';
|
|
fileInput.addEventListener( 'change', function () {
|
|
|
|
editor.loader.loadFiles( fileInput.files );
|
|
form.reset();
|
|
|
|
} );
|
|
form.appendChild( fileInput );
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/import' ) );
|
|
option.onClick( function () {
|
|
|
|
fileInput.click();
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
//
|
|
|
|
options.add( new UIHorizontalRule() );
|
|
|
|
// Export Geometry
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/geometry' ) );
|
|
option.onClick( function () {
|
|
|
|
const object = editor.selected;
|
|
|
|
if ( object === null ) {
|
|
|
|
alert( 'No object selected.' );
|
|
return;
|
|
|
|
}
|
|
|
|
const geometry = object.geometry;
|
|
|
|
if ( geometry === undefined ) {
|
|
|
|
alert( 'The selected object doesn\'t have geometry.' );
|
|
return;
|
|
|
|
}
|
|
|
|
let output = geometry.toJSON();
|
|
|
|
try {
|
|
|
|
output = JSON.stringify( output, null, '\t' );
|
|
output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
|
|
|
|
} catch ( e ) {
|
|
|
|
output = JSON.stringify( output );
|
|
|
|
}
|
|
|
|
saveString( output, 'geometry.json' );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export Object
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/object' ) );
|
|
option.onClick( function () {
|
|
|
|
const object = editor.selected;
|
|
|
|
if ( object === null ) {
|
|
|
|
alert( 'No object selected' );
|
|
return;
|
|
|
|
}
|
|
|
|
let output = object.toJSON();
|
|
|
|
try {
|
|
|
|
output = JSON.stringify( output, null, '\t' );
|
|
output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
|
|
|
|
} catch ( e ) {
|
|
|
|
output = JSON.stringify( output );
|
|
|
|
}
|
|
|
|
saveString( output, 'model.json' );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export Scene
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/scene' ) );
|
|
option.onClick( function () {
|
|
|
|
let output = editor.scene.toJSON();
|
|
|
|
try {
|
|
|
|
output = JSON.stringify( output, null, '\t' );
|
|
output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
|
|
|
|
} catch ( e ) {
|
|
|
|
output = JSON.stringify( output );
|
|
|
|
}
|
|
|
|
saveString( output, 'scene.json' );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
option = new UIRow();
|
|
option.setClass('option');
|
|
option.setTextContent('导出editor');
|
|
option.onClick(function () {
|
|
|
|
let output = editor.toJSON();
|
|
|
|
try {
|
|
|
|
output = JSON.stringify(output, null, '\t');
|
|
output = output.replace(/[\n\t]+([\d\.e\-\[\]]+)/g, '$1');
|
|
|
|
} catch (e) {
|
|
|
|
output = JSON.stringify(output);
|
|
|
|
}
|
|
|
|
saveString(output, 'app.json');
|
|
|
|
});
|
|
options.add(option);
|
|
|
|
|
|
//
|
|
|
|
options.add( new UIHorizontalRule() );
|
|
|
|
// Export DAE
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/dae' ) );
|
|
option.onClick( async function () {
|
|
|
|
const { ColladaExporter } = await import( 'three/addons/exporters/ColladaExporter.js' );
|
|
|
|
const exporter = new ColladaExporter();
|
|
|
|
exporter.parse( editor.scene, function ( result ) {
|
|
|
|
saveString( result.data, 'scene.dae' );
|
|
|
|
} );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export DRC
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/drc' ) );
|
|
option.onClick( async function () {
|
|
|
|
const object = editor.selected;
|
|
|
|
if ( object === null || object.isMesh === undefined ) {
|
|
|
|
alert( 'No mesh selected' );
|
|
return;
|
|
|
|
}
|
|
|
|
const { DRACOExporter } = await import( 'three/addons/exporters/DRACOExporter.js' );
|
|
|
|
const exporter = new DRACOExporter();
|
|
|
|
const options = {
|
|
decodeSpeed: 5,
|
|
encodeSpeed: 5,
|
|
encoderMethod: DRACOExporter.MESH_EDGEBREAKER_ENCODING,
|
|
quantization: [ 16, 8, 8, 8, 8 ],
|
|
exportUvs: true,
|
|
exportNormals: true,
|
|
exportColor: object.geometry.hasAttribute( 'color' )
|
|
};
|
|
|
|
// TODO: Change to DRACOExporter's parse( geometry, onParse )?
|
|
const result = exporter.parse( object, options );
|
|
saveArrayBuffer( result, 'model.drc' );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export GLB
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/glb' ) );
|
|
option.onClick( async function () {
|
|
|
|
const scene = editor.scene;
|
|
const animations = getAnimations( scene );
|
|
|
|
const { GLTFExporter } = await import( 'three/addons/exporters/GLTFExporter.js' );
|
|
|
|
const exporter = new GLTFExporter();
|
|
|
|
exporter.parse( scene, function ( result ) {
|
|
|
|
saveArrayBuffer( result, 'scene.glb' );
|
|
|
|
}, undefined, { binary: true, animations: animations } );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export GLTF
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/gltf' ) );
|
|
option.onClick( async function () {
|
|
|
|
const scene = editor.scene;
|
|
const animations = getAnimations( scene );
|
|
|
|
const { GLTFExporter } = await import( 'three/addons/exporters/GLTFExporter.js' );
|
|
|
|
const exporter = new GLTFExporter();
|
|
|
|
exporter.parse( scene, function ( result ) {
|
|
|
|
saveString( JSON.stringify( result, null, 2 ), 'scene.gltf' );
|
|
|
|
}, undefined, { animations: animations } );
|
|
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export OBJ
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/obj' ) );
|
|
option.onClick( async function () {
|
|
|
|
const object = editor.selected;
|
|
|
|
if ( object === null ) {
|
|
|
|
alert( 'No object selected.' );
|
|
return;
|
|
|
|
}
|
|
|
|
const { OBJExporter } = await import( 'three/addons/exporters/OBJExporter.js' );
|
|
|
|
const exporter = new OBJExporter();
|
|
|
|
saveString( exporter.parse( object ), 'model.obj' );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export PLY (ASCII)
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/ply' ) );
|
|
option.onClick( async function () {
|
|
|
|
const { PLYExporter } = await import( 'three/addons/exporters/PLYExporter.js' );
|
|
|
|
const exporter = new PLYExporter();
|
|
|
|
exporter.parse( editor.scene, function ( result ) {
|
|
|
|
saveArrayBuffer( result, 'model.ply' );
|
|
|
|
} );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export PLY (Binary)
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/ply_binary' ) );
|
|
option.onClick( async function () {
|
|
|
|
const { PLYExporter } = await import( 'three/addons/exporters/PLYExporter.js' );
|
|
|
|
const exporter = new PLYExporter();
|
|
|
|
exporter.parse( editor.scene, function ( result ) {
|
|
|
|
saveArrayBuffer( result, 'model-binary.ply' );
|
|
|
|
}, { binary: true } );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export STL (ASCII)
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/stl' ) );
|
|
option.onClick( async function () {
|
|
|
|
const { STLExporter } = await import( 'three/addons/exporters/STLExporter.js' );
|
|
|
|
const exporter = new STLExporter();
|
|
|
|
saveString( exporter.parse( editor.scene ), 'model.stl' );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export STL (Binary)
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/stl_binary' ) );
|
|
option.onClick( async function () {
|
|
|
|
const { STLExporter } = await import( 'three/addons/exporters/STLExporter.js' );
|
|
|
|
const exporter = new STLExporter();
|
|
|
|
saveArrayBuffer( exporter.parse( editor.scene, { binary: true } ), 'model-binary.stl' );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
// Export USDZ
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/export/usdz' ) );
|
|
option.onClick( async function () {
|
|
|
|
const { USDZExporter } = await import( 'three/addons/exporters/USDZExporter.js' );
|
|
|
|
const exporter = new USDZExporter();
|
|
|
|
saveArrayBuffer( await exporter.parse( editor.scene, { binary: true } ), 'model.usdz' );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
//
|
|
|
|
options.add( new UIHorizontalRule() );
|
|
|
|
// Publish
|
|
|
|
option = new UIRow();
|
|
option.setClass( 'option' );
|
|
option.setTextContent( strings.getKey( 'menubar/file/publish' ) );
|
|
option.onClick( function () {
|
|
|
|
const toZip = {};
|
|
|
|
//
|
|
|
|
let output = editor.toJSON();
|
|
// debugger
|
|
output.metadata.type = 'App';
|
|
delete output.history;
|
|
|
|
output = JSON.stringify( output, null, '\t' );
|
|
output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
|
|
|
|
toZip[ 'app.json' ] = strToU8( output );
|
|
|
|
//
|
|
|
|
const title = config.getKey( 'project/title' );
|
|
|
|
const manager = new THREE.LoadingManager( function () {
|
|
|
|
const zipped = zipSync( toZip, { level: 9 } );
|
|
|
|
const blob = new Blob( [ zipped.buffer ], { type: 'application/zip' } );
|
|
|
|
save( blob, ( title !== '' ? title : 'untitled' ) + '.zip' );
|
|
|
|
} );
|
|
|
|
const loader = new THREE.FileLoader( manager );
|
|
loader.load( 'js/libs/app/index.html', function ( content ) {
|
|
|
|
content = content.replace( '<!-- title -->', title );
|
|
|
|
const includes = [];
|
|
|
|
content = content.replace( '<!-- includes -->', includes.join( '\n\t\t' ) );
|
|
|
|
let editButton = '';
|
|
|
|
if ( config.getKey( 'project/editable' ) ) {
|
|
|
|
editButton = [
|
|
' let button = document.createElement( \'a\' );',
|
|
' button.href = \'https://threejs.org/editor/#file=\' + location.href.split( \'/\' ).slice( 0, - 1 ).join( \'/\' ) + \'/app.json\';',
|
|
' button.style.cssText = \'position: absolute; bottom: 20px; right: 20px; padding: 10px 16px; color: #fff; border: 1px solid #fff; border-radius: 20px; text-decoration: none;\';',
|
|
' button.target = \'_blank\';',
|
|
' button.textContent = \'EDIT\';',
|
|
' document.body.appendChild( button );',
|
|
].join( '\n' );
|
|
|
|
}
|
|
|
|
content = content.replace( '\t\t\t/* edit button */', editButton );
|
|
|
|
toZip[ 'index.html' ] = strToU8( content );
|
|
|
|
} );
|
|
loader.load( 'js/libs/app.js', function ( content ) {
|
|
|
|
toZip[ 'js/app.js' ] = strToU8( content );
|
|
|
|
} );
|
|
loader.load( '../build/three.module.js', function ( content ) {
|
|
|
|
toZip[ 'js/three.module.js' ] = strToU8( content );
|
|
|
|
} );
|
|
loader.load( '../examples/jsm/webxr/VRButton.js', function ( content ) {
|
|
|
|
toZip[ 'js/VRButton.js' ] = strToU8( content );
|
|
|
|
} );
|
|
|
|
} );
|
|
options.add( option );
|
|
|
|
//
|
|
|
|
const link = document.createElement( 'a' );
|
|
function save( blob, filename ) {
|
|
|
|
if ( link.href ) {
|
|
|
|
URL.revokeObjectURL( link.href );
|
|
|
|
}
|
|
|
|
link.href = URL.createObjectURL( blob );
|
|
link.download = filename || 'data.json';
|
|
link.dispatchEvent( new MouseEvent( 'click' ) );
|
|
|
|
}
|
|
|
|
function saveArrayBuffer( buffer, filename ) {
|
|
|
|
save( new Blob( [ buffer ], { type: 'application/octet-stream' } ), filename );
|
|
|
|
}
|
|
|
|
function saveString( text, filename ) {
|
|
|
|
save( new Blob( [ text ], { type: 'text/plain' } ), filename );
|
|
|
|
}
|
|
|
|
function getAnimations( scene ) {
|
|
|
|
const animations = [];
|
|
|
|
scene.traverse( function ( object ) {
|
|
|
|
animations.push( ... object.animations );
|
|
|
|
} );
|
|
|
|
return animations;
|
|
|
|
}
|
|
|
|
return container;
|
|
|
|
}
|
|
|
|
export { MenubarFile };
|
|
|