You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
787 lines
14 KiB
JavaScript
787 lines
14 KiB
JavaScript
import * as THREE from 'three';
|
|
|
|
import { Config } from './Config.js';
|
|
import { Loader } from './Loader.js';
|
|
import { History as _History } from './History.js';
|
|
import { Strings } from './Strings.js';
|
|
import { Storage as _Storage } from './Storage.js';
|
|
import { Selector } from './Selector.js';
|
|
|
|
var _DEFAULT_CAMERA = new THREE.PerspectiveCamera( 50, 1, 0.01, 1000 );
|
|
_DEFAULT_CAMERA.name = 'Camera';
|
|
_DEFAULT_CAMERA.position.set( 0, 5, 10 );
|
|
_DEFAULT_CAMERA.lookAt( new THREE.Vector3() );
|
|
|
|
function Editor() {
|
|
|
|
const Signal = signals.Signal; // eslint-disable-line no-undef
|
|
|
|
this.signals = {
|
|
|
|
// script
|
|
|
|
editScript: new Signal(),
|
|
|
|
// player
|
|
|
|
startPlayer: new Signal(),
|
|
stopPlayer: new Signal(),
|
|
|
|
// xr
|
|
|
|
enterXR: new Signal(),
|
|
offerXR: new Signal(),
|
|
leaveXR: new Signal(),
|
|
|
|
// notifications
|
|
|
|
editorCleared: new Signal(),
|
|
|
|
savingStarted: new Signal(),
|
|
savingFinished: new Signal(),
|
|
|
|
transformModeChanged: new Signal(),
|
|
snapChanged: new Signal(),
|
|
spaceChanged: new Signal(),
|
|
rendererCreated: new Signal(),
|
|
rendererUpdated: new Signal(),
|
|
rendererDetectKTX2Support: new Signal(),
|
|
|
|
sceneBackgroundChanged: new Signal(),
|
|
sceneEnvironmentChanged: new Signal(),
|
|
sceneFogChanged: new Signal(),
|
|
sceneFogSettingsChanged: new Signal(),
|
|
sceneGraphChanged: new Signal(),
|
|
sceneRendered: new Signal(),
|
|
|
|
cameraChanged: new Signal(),
|
|
cameraResetted: new Signal(),
|
|
|
|
geometryChanged: new Signal(),
|
|
|
|
objectSelected: new Signal(),
|
|
objectFocused: new Signal(),
|
|
|
|
objectAdded: new Signal(),
|
|
objectChanged: new Signal(),
|
|
objectRemoved: new Signal(),
|
|
|
|
cameraAdded: new Signal(),
|
|
cameraRemoved: new Signal(),
|
|
|
|
helperAdded: new Signal(),
|
|
helperRemoved: new Signal(),
|
|
|
|
materialAdded: new Signal(),
|
|
materialChanged: new Signal(),
|
|
materialRemoved: new Signal(),
|
|
|
|
scriptAdded: new Signal(),
|
|
scriptChanged: new Signal(),
|
|
scriptRemoved: new Signal(),
|
|
|
|
windowResize: new Signal(),
|
|
|
|
showHelpersChanged: new Signal(),
|
|
refreshSidebarObject3D: new Signal(),
|
|
refreshSidebarEnvironment: new Signal(),
|
|
historyChanged: new Signal(),
|
|
|
|
viewportCameraChanged: new Signal(),
|
|
viewportShadingChanged: new Signal(),
|
|
|
|
intersectionsDetected: new Signal(),
|
|
|
|
pathTracerUpdated: new Signal(),
|
|
|
|
};
|
|
|
|
this.config = new Config();
|
|
this.history = new _History( this );
|
|
this.selector = new Selector( this );
|
|
this.storage = new _Storage();
|
|
this.strings = new Strings( this.config );
|
|
|
|
this.loader = new Loader( this );
|
|
|
|
this.camera = _DEFAULT_CAMERA.clone();
|
|
|
|
this.scene = new THREE.Scene();
|
|
this.scene.name = 'Scene';
|
|
|
|
this.sceneHelpers = new THREE.Scene();
|
|
this.sceneHelpers.add( new THREE.HemisphereLight( 0xffffff, 0x888888, 2 ) );
|
|
|
|
this.object = {};
|
|
this.geometries = {};
|
|
this.materials = {};
|
|
this.textures = {};
|
|
this.scripts = {};
|
|
|
|
this.materialsRefCounter = new Map(); // tracks how often is a material used by a 3D object
|
|
|
|
this.mixer = new THREE.AnimationMixer( this.scene );
|
|
|
|
this.selected = null;
|
|
this.helpers = {};
|
|
|
|
this.cameras = {};
|
|
|
|
this.viewportCamera = this.camera;
|
|
this.viewportShading = 'default';
|
|
|
|
this.addCamera( this.camera );
|
|
|
|
}
|
|
|
|
Editor.prototype = {
|
|
|
|
setScene: function ( scene ) {
|
|
|
|
this.scene.uuid = scene.uuid;
|
|
this.scene.name = scene.name;
|
|
|
|
this.scene.background = scene.background;
|
|
this.scene.environment = scene.environment;
|
|
this.scene.fog = scene.fog;
|
|
this.scene.backgroundBlurriness = scene.backgroundBlurriness;
|
|
this.scene.backgroundIntensity = scene.backgroundIntensity;
|
|
|
|
this.scene.userData = JSON.parse( JSON.stringify( scene.userData ) );
|
|
|
|
// avoid render per object
|
|
|
|
this.signals.sceneGraphChanged.active = false;
|
|
|
|
while ( scene.children.length > 0 ) {
|
|
|
|
this.addObject( scene.children[ 0 ] );
|
|
|
|
}
|
|
|
|
this.signals.sceneGraphChanged.active = true;
|
|
this.signals.sceneGraphChanged.dispatch();
|
|
|
|
},
|
|
|
|
//
|
|
|
|
addObject: function ( object, parent, index ) {
|
|
|
|
var scope = this;
|
|
|
|
object.traverse( function ( child ) {
|
|
|
|
if ( child.geometry !== undefined ) scope.addGeometry( child.geometry );
|
|
if ( child.material !== undefined ) scope.addMaterial( child.material );
|
|
|
|
scope.addCamera( child );
|
|
scope.addHelper( child );
|
|
|
|
} );
|
|
|
|
if ( parent === undefined ) {
|
|
|
|
this.scene.add( object );
|
|
|
|
} else {
|
|
|
|
parent.children.splice( index, 0, object );
|
|
object.parent = parent;
|
|
|
|
}
|
|
|
|
this.signals.objectAdded.dispatch( object );
|
|
this.signals.sceneGraphChanged.dispatch();
|
|
|
|
},
|
|
|
|
nameObject: function ( object, name ) {
|
|
|
|
object.name = name;
|
|
this.signals.sceneGraphChanged.dispatch();
|
|
|
|
},
|
|
|
|
removeObject: function ( object ) {
|
|
|
|
if ( object.parent === null ) return; // avoid deleting the camera or scene
|
|
|
|
var scope = this;
|
|
|
|
object.traverse( function ( child ) {
|
|
|
|
scope.removeCamera( child );
|
|
scope.removeHelper( child );
|
|
|
|
if ( child.material !== undefined ) scope.removeMaterial( child.material );
|
|
|
|
} );
|
|
|
|
object.parent.remove( object );
|
|
|
|
this.signals.objectRemoved.dispatch( object );
|
|
this.signals.sceneGraphChanged.dispatch();
|
|
|
|
},
|
|
|
|
addGeometry: function ( geometry ) {
|
|
|
|
this.geometries[ geometry.uuid ] = geometry;
|
|
|
|
},
|
|
|
|
setGeometryName: function ( geometry, name ) {
|
|
|
|
geometry.name = name;
|
|
this.signals.sceneGraphChanged.dispatch();
|
|
|
|
},
|
|
|
|
addMaterial: function ( material ) {
|
|
|
|
if ( Array.isArray( material ) ) {
|
|
|
|
for ( var i = 0, l = material.length; i < l; i ++ ) {
|
|
|
|
this.addMaterialToRefCounter( material[ i ] );
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.addMaterialToRefCounter( material );
|
|
|
|
}
|
|
|
|
this.signals.materialAdded.dispatch();
|
|
|
|
},
|
|
|
|
addMaterialToRefCounter: function ( material ) {
|
|
|
|
var materialsRefCounter = this.materialsRefCounter;
|
|
|
|
var count = materialsRefCounter.get( material );
|
|
|
|
if ( count === undefined ) {
|
|
|
|
materialsRefCounter.set( material, 1 );
|
|
this.materials[ material.uuid ] = material;
|
|
|
|
} else {
|
|
|
|
count ++;
|
|
materialsRefCounter.set( material, count );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
removeMaterial: function ( material ) {
|
|
|
|
if ( Array.isArray( material ) ) {
|
|
|
|
for ( var i = 0, l = material.length; i < l; i ++ ) {
|
|
|
|
this.removeMaterialFromRefCounter( material[ i ] );
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.removeMaterialFromRefCounter( material );
|
|
|
|
}
|
|
|
|
this.signals.materialRemoved.dispatch();
|
|
|
|
},
|
|
|
|
removeMaterialFromRefCounter: function ( material ) {
|
|
|
|
var materialsRefCounter = this.materialsRefCounter;
|
|
|
|
var count = materialsRefCounter.get( material );
|
|
count --;
|
|
|
|
if ( count === 0 ) {
|
|
|
|
materialsRefCounter.delete( material );
|
|
delete this.materials[ material.uuid ];
|
|
|
|
} else {
|
|
|
|
materialsRefCounter.set( material, count );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
getMaterialById: function ( id ) {
|
|
|
|
var material;
|
|
var materials = Object.values( this.materials );
|
|
|
|
for ( var i = 0; i < materials.length; i ++ ) {
|
|
|
|
if ( materials[ i ].id === id ) {
|
|
|
|
material = materials[ i ];
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return material;
|
|
|
|
},
|
|
|
|
setMaterialName: function ( material, name ) {
|
|
|
|
material.name = name;
|
|
this.signals.sceneGraphChanged.dispatch();
|
|
|
|
},
|
|
|
|
addTexture: function ( texture ) {
|
|
|
|
this.textures[ texture.uuid ] = texture;
|
|
|
|
},
|
|
|
|
//
|
|
|
|
addCamera: function ( camera ) {
|
|
|
|
if ( camera.isCamera ) {
|
|
|
|
this.cameras[ camera.uuid ] = camera;
|
|
|
|
this.signals.cameraAdded.dispatch( camera );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
removeCamera: function ( camera ) {
|
|
|
|
if ( this.cameras[ camera.uuid ] !== undefined ) {
|
|
|
|
delete this.cameras[ camera.uuid ];
|
|
|
|
this.signals.cameraRemoved.dispatch( camera );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
//
|
|
|
|
addHelper: function () {
|
|
|
|
var geometry = new THREE.SphereGeometry( 2, 4, 2 );
|
|
var material = new THREE.MeshBasicMaterial( { color: 0xff0000, visible: false } );
|
|
|
|
return function ( object, helper ) {
|
|
|
|
if ( helper === undefined ) {
|
|
|
|
if ( object.isCamera ) {
|
|
|
|
helper = new THREE.CameraHelper( object );
|
|
|
|
} else if ( object.isPointLight ) {
|
|
|
|
helper = new THREE.PointLightHelper( object, 1 );
|
|
|
|
} else if ( object.isDirectionalLight ) {
|
|
|
|
helper = new THREE.DirectionalLightHelper( object, 1 );
|
|
|
|
} else if ( object.isSpotLight ) {
|
|
|
|
helper = new THREE.SpotLightHelper( object );
|
|
|
|
} else if ( object.isHemisphereLight ) {
|
|
|
|
helper = new THREE.HemisphereLightHelper( object, 1 );
|
|
|
|
} else if ( object.isSkinnedMesh ) {
|
|
|
|
helper = new THREE.SkeletonHelper( object.skeleton.bones[ 0 ] );
|
|
|
|
} else if ( object.isBone === true && object.parent && object.parent.isBone !== true ) {
|
|
|
|
helper = new THREE.SkeletonHelper( object );
|
|
|
|
} else {
|
|
|
|
// no helper for this object type
|
|
return;
|
|
|
|
}
|
|
|
|
const picker = new THREE.Mesh( geometry, material );
|
|
picker.name = 'picker';
|
|
picker.userData.object = object;
|
|
helper.add( picker );
|
|
|
|
}
|
|
|
|
this.sceneHelpers.add( helper );
|
|
this.helpers[ object.id ] = helper;
|
|
|
|
this.signals.helperAdded.dispatch( helper );
|
|
|
|
};
|
|
|
|
}(),
|
|
|
|
removeHelper: function ( object ) {
|
|
|
|
if ( this.helpers[ object.id ] !== undefined ) {
|
|
|
|
var helper = this.helpers[ object.id ];
|
|
helper.parent.remove( helper );
|
|
helper.dispose();
|
|
|
|
delete this.helpers[ object.id ];
|
|
|
|
this.signals.helperRemoved.dispatch( helper );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
//
|
|
|
|
addScript: function ( object, script ) {
|
|
|
|
if ( this.scripts[ object.uuid ] === undefined ) {
|
|
|
|
this.scripts[ object.uuid ] = [];
|
|
|
|
}
|
|
|
|
this.scripts[ object.uuid ].push( script );
|
|
|
|
this.signals.scriptAdded.dispatch( script );
|
|
|
|
},
|
|
|
|
removeScript: function ( object, script ) {
|
|
|
|
if ( this.scripts[ object.uuid ] === undefined ) return;
|
|
|
|
var index = this.scripts[ object.uuid ].indexOf( script );
|
|
|
|
if ( index !== - 1 ) {
|
|
|
|
this.scripts[ object.uuid ].splice( index, 1 );
|
|
|
|
}
|
|
|
|
this.signals.scriptRemoved.dispatch( script );
|
|
|
|
},
|
|
|
|
getObjectMaterial: function ( object, slot ) {
|
|
|
|
var material = object.material;
|
|
|
|
if ( Array.isArray( material ) && slot !== undefined ) {
|
|
|
|
material = material[ slot ];
|
|
|
|
}
|
|
|
|
return material;
|
|
|
|
},
|
|
|
|
setObjectMaterial: function ( object, slot, newMaterial ) {
|
|
|
|
if ( Array.isArray( object.material ) && slot !== undefined ) {
|
|
|
|
object.material[ slot ] = newMaterial;
|
|
|
|
} else {
|
|
|
|
object.material = newMaterial;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
setViewportCamera: function ( uuid ) {
|
|
|
|
this.viewportCamera = this.cameras[ uuid ];
|
|
this.signals.viewportCameraChanged.dispatch();
|
|
|
|
},
|
|
|
|
setViewportShading: function ( value ) {
|
|
|
|
this.viewportShading = value;
|
|
this.signals.viewportShadingChanged.dispatch();
|
|
|
|
},
|
|
|
|
//
|
|
|
|
select: function ( object ) {
|
|
|
|
this.selector.select( object );
|
|
|
|
},
|
|
|
|
selectById: function ( id ) {
|
|
|
|
if ( id === this.camera.id ) {
|
|
|
|
this.select( this.camera );
|
|
return;
|
|
|
|
}
|
|
|
|
this.select( this.scene.getObjectById( id ) );
|
|
|
|
},
|
|
|
|
selectByUuid: function ( uuid ) {
|
|
|
|
var scope = this;
|
|
|
|
this.scene.traverse( function ( child ) {
|
|
|
|
if ( child.uuid === uuid ) {
|
|
|
|
scope.select( child );
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
deselect: function () {
|
|
|
|
this.selector.deselect();
|
|
|
|
},
|
|
|
|
focus: function ( object ) {
|
|
|
|
if ( object !== undefined ) {
|
|
|
|
this.signals.objectFocused.dispatch( object );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
focusById: function ( id ) {
|
|
|
|
this.focus( this.scene.getObjectById( id ) );
|
|
|
|
},
|
|
|
|
clear: function () {
|
|
|
|
this.history.clear();
|
|
this.storage.clear();
|
|
|
|
this.camera.copy( _DEFAULT_CAMERA );
|
|
this.signals.cameraResetted.dispatch();
|
|
|
|
this.scene.name = 'Scene';
|
|
this.scene.userData = {};
|
|
this.scene.background = null;
|
|
this.scene.environment = null;
|
|
this.scene.fog = null;
|
|
|
|
var objects = this.scene.children;
|
|
|
|
this.signals.sceneGraphChanged.active = false;
|
|
|
|
while ( objects.length > 0 ) {
|
|
|
|
this.removeObject( objects[ 0 ] );
|
|
|
|
}
|
|
|
|
this.signals.sceneGraphChanged.active = true;
|
|
|
|
this.geometries = {};
|
|
this.materials = {};
|
|
this.textures = {};
|
|
this.scripts = {};
|
|
|
|
this.materialsRefCounter.clear();
|
|
|
|
this.animations = {};
|
|
this.mixer.stopAllAction();
|
|
|
|
this.deselect();
|
|
|
|
this.signals.editorCleared.dispatch();
|
|
|
|
},
|
|
|
|
//
|
|
|
|
fromJSON: async function ( json ) {
|
|
|
|
var loader = new THREE.ObjectLoader();
|
|
var camera = await loader.parseAsync( json.camera );
|
|
|
|
const existingUuid = this.camera.uuid;
|
|
const incomingUuid = camera.uuid;
|
|
|
|
// copy all properties, including uuid
|
|
this.camera.copy( camera );
|
|
this.camera.uuid = incomingUuid;
|
|
|
|
delete this.cameras[ existingUuid ]; // remove old entry [existingUuid, this.camera]
|
|
this.cameras[ incomingUuid ] = this.camera; // add new entry [incomingUuid, this.camera]
|
|
|
|
this.signals.cameraResetted.dispatch();
|
|
|
|
this.history.fromJSON( json.history );
|
|
this.scripts = json.scripts;
|
|
|
|
this.setScene( await loader.parseAsync( json.scene ) );
|
|
|
|
if ( json.environment === 'Room' ||
|
|
json.environment === 'ModelViewer' /* DEPRECATED */ ) {
|
|
|
|
this.signals.sceneEnvironmentChanged.dispatch( json.environment );
|
|
this.signals.refreshSidebarEnvironment.dispatch();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
toJSON: function () {
|
|
|
|
// scripts clean up
|
|
|
|
var scene = this.scene;
|
|
var scripts = this.scripts;
|
|
|
|
for ( var key in scripts ) {
|
|
|
|
var script = scripts[ key ];
|
|
|
|
if ( script.length === 0 || scene.getObjectByProperty( 'uuid', key ) === undefined ) {
|
|
|
|
delete scripts[ key ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// honor neutral environment
|
|
|
|
let environment = null;
|
|
|
|
if ( this.scene.environment !== null && this.scene.environment.isRenderTargetTexture === true ) {
|
|
|
|
environment = 'Room';
|
|
|
|
}
|
|
|
|
//
|
|
|
|
return {
|
|
|
|
metadata: {},
|
|
project: {
|
|
shadows: this.config.getKey( 'project/renderer/shadows' ),
|
|
shadowType: this.config.getKey( 'project/renderer/shadowType' ),
|
|
toneMapping: this.config.getKey( 'project/renderer/toneMapping' ),
|
|
toneMappingExposure: this.config.getKey( 'project/renderer/toneMappingExposure' )
|
|
},
|
|
camera: this.viewportCamera.toJSON(),
|
|
scene: this.scene.toJSON(),
|
|
scripts: this.scripts,
|
|
history: this.history.toJSON(),
|
|
environment: environment
|
|
|
|
};
|
|
|
|
},
|
|
|
|
objectByUuid: function ( uuid ) {
|
|
|
|
return this.scene.getObjectByProperty( 'uuid', uuid, true );
|
|
|
|
},
|
|
|
|
execute: function ( cmd, optionalName ) {
|
|
|
|
this.history.execute( cmd, optionalName );
|
|
|
|
},
|
|
|
|
undo: function () {
|
|
|
|
this.history.undo();
|
|
|
|
},
|
|
|
|
redo: function () {
|
|
|
|
this.history.redo();
|
|
|
|
},
|
|
|
|
utils: {
|
|
|
|
save: save,
|
|
saveArrayBuffer: saveArrayBuffer,
|
|
saveString: saveString,
|
|
formatNumber: formatNumber
|
|
|
|
}
|
|
|
|
};
|
|
|
|
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 formatNumber( number ) {
|
|
|
|
return new Intl.NumberFormat( 'en-us', { useGrouping: true } ).format( number );
|
|
|
|
}
|
|
|
|
export { Editor };
|