diff --git a/editor/js/Editor.js b/editor/js/Editor.js index 2cd5ea313250ba..ee0cc210fa8a99 100644 --- a/editor/js/Editor.js +++ b/editor/js/Editor.js @@ -59,7 +59,10 @@ function Editor() { geometryChanged: new Signal(), + readyForMultipleSelect: new Signal(), + objectSelected: new Signal(), + objectsMultipleSelected: new Signal(), objectFocused: new Signal(), objectAdded: new Signal(), @@ -123,6 +126,7 @@ function Editor() { this.mixer = new THREE.AnimationMixer( this.scene ); this.selected = null; + this.selectedObjects = []; this.helpers = {}; this.cameras = {}; diff --git a/editor/js/Selector.js b/editor/js/Selector.js index db4a14144de244..310f38871d3b80 100644 --- a/editor/js/Selector.js +++ b/editor/js/Selector.js @@ -12,8 +12,15 @@ class Selector { this.editor = editor; this.signals = signals; + let readyForMultipleSelect = false; // signals + signals.readyForMultipleSelect.add( ( isReady ) => { + + readyForMultipleSelect = isReady; + + } ); + signals.intersectionsDetected.add( ( intersects ) => { if ( intersects.length > 0 ) { @@ -23,12 +30,28 @@ class Selector { if ( object.userData.object !== undefined ) { // helper + if ( readyForMultipleSelect ) { + + this.selectMultiple( object.userData.object ); + + } else { + + this.select( object.userData.object ); + + } - this.select( object.userData.object ); } else { - this.select( object ); + if ( readyForMultipleSelect ) { + + this.selectMultiple( object ); + + } else { + + this.select( object ); + + } } @@ -72,6 +95,26 @@ class Selector { } + selectMultiple( object ) { + + if ( object === null ) { + + this.editor.selectedObjects = []; + this.editor.config.setKey( 'selectedObjects', [] ); + this.signals.objectsMultipleSelected.dispatch( [] ); + this.editor.selected = null; + return; + + } + + const selectedObjects = [ ...this.editor.selectedObjects.filter( itm => itm !== object ), object ]; + this.editor.selectedObjects = selectedObjects; + this.editor.config.setKey( 'selectedObjects', selectedObjects ); + this.signals.objectsMultipleSelected.dispatch( selectedObjects ); + this.editor.selected = object; + + } + select( object ) { if ( this.editor.selected === object ) return; @@ -81,19 +124,26 @@ class Selector { if ( object !== null ) { uuid = object.uuid; + this.editor.selectedObjects = [ object ]; + + } else { + + this.editor.selectedObjects = []; } this.editor.selected = object; - this.editor.config.setKey( 'selected', uuid ); + this.editor.config.setKey( 'selected', uuid ); this.signals.objectSelected.dispatch( object ); + } deselect() { this.select( null ); + this.signals.objectsMultipleSelected.dispatch( null ); } diff --git a/editor/js/Viewport.js b/editor/js/Viewport.js index 334288367b1511..c5307118c6129c 100644 --- a/editor/js/Viewport.js +++ b/editor/js/Viewport.js @@ -62,18 +62,18 @@ function Viewport( editor ) { // - const box = new THREE.Box3(); + let selectedObjects = []; + let boxes = [ new THREE.Box3() ]; - const selectionBox = new THREE.Box3Helper( box ); - selectionBox.material.depthTest = false; - selectionBox.material.transparent = true; - selectionBox.visible = false; - sceneHelpers.add( selectionBox ); + let selectionBoxes = [ new THREE.Box3Helper( boxes[ 0 ] ) ]; + selectionBoxes[ 0 ].material.depthTest = false; + selectionBoxes[ 0 ].material.transparent = true; + selectionBoxes[ 0 ].visible = false; + sceneHelpers.add( selectionBoxes[ 0 ] ); let objectPositionOnDown = null; let objectRotationOnDown = null; let objectScaleOnDown = null; - const transformControls = new TransformControls( camera, container.dom ); transformControls.addEventListener( 'axis-changed', function () { @@ -108,7 +108,16 @@ function Viewport( editor ) { if ( ! objectPositionOnDown.equals( object.position ) ) { - editor.execute( new SetPositionCommand( editor, object, object.position, objectPositionOnDown ) ); + selectedObjects.map( ( selectedObject ) => { + + var direction = new THREE.Vector3(); + direction.subVectors( object.position, objectPositionOnDown ); + + const newPosition = selectedObject === object ? object.position : new THREE.Vector3( direction.x + selectedObject.position.x, direction.y + selectedObject.position.y, direction.z + selectedObject.position.z ); + editor.execute( new SetPositionCommand( editor, selectedObject, newPosition, objectPositionOnDown ) ); + + } ); + } @@ -118,7 +127,12 @@ function Viewport( editor ) { if ( ! objectRotationOnDown.equals( object.rotation ) ) { - editor.execute( new SetRotationCommand( editor, object, object.rotation, objectRotationOnDown ) ); + selectedObjects.map( ( selectedObject ) => { + + editor.execute( new SetRotationCommand( editor, selectedObject, object.rotation, objectRotationOnDown ) ); + + } ); + } @@ -128,7 +142,12 @@ function Viewport( editor ) { if ( ! objectScaleOnDown.equals( object.scale ) ) { - editor.execute( new SetScaleCommand( editor, object, object.scale, objectScaleOnDown ) ); + selectedObjects.map( ( selectedObject ) => { + + editor.execute( new SetScaleCommand( editor, selectedObject, object.scale, objectScaleOnDown ) ); + + } ); + } @@ -267,10 +286,34 @@ function Viewport( editor ) { } + function onKeyDown( event ) { + + if ( event.key === 'Control' ) { + + signals.readyForMultipleSelect.dispatch( true ); + + } + + + } + + function onKeyUp( event ) { + + if ( event.key === 'Control' ) { + + signals.readyForMultipleSelect.dispatch( false ); + + } + + } + container.dom.addEventListener( 'mousedown', onMouseDown ); container.dom.addEventListener( 'touchstart', onTouchStart, { passive: false } ); container.dom.addEventListener( 'dblclick', onDoubleClick ); + document.addEventListener( 'keydown', onKeyDown ); + document.addEventListener( 'keyup', onKeyUp ); + // controls need to be added *after* main logic, // otherwise controls.enabled doesn't work. @@ -404,16 +447,21 @@ function Viewport( editor ) { signals.objectSelected.add( function ( object ) { - selectionBox.visible = false; + boxes = [ new THREE.Box3() ]; + selectionBoxes.map( ( item ) => sceneHelpers.remove( item ) ); + selectionBoxes = [ new THREE.Box3Helper( boxes[ 0 ] ) ]; + selectedObjects = [ object ]; transformControls.detach(); + if ( object !== null && object !== scene && object !== camera ) { - box.setFromObject( object, true ); + boxes[ 0 ].setFromObject( object, true ); - if ( box.isEmpty() === false ) { + if ( boxes[ 0 ].isEmpty() === false ) { - selectionBox.visible = true; + selectionBoxes[ 0 ].visible = true; + sceneHelpers.add( selectionBoxes[ 0 ] ); } @@ -425,6 +473,47 @@ function Viewport( editor ) { } ); + signals.objectsMultipleSelected.add( function ( objectList ) { + + boxes = []; + selectionBoxes.map( item => sceneHelpers.remove( item ) ); + selectionBoxes = []; + selectedObjects = objectList; + + objectList.map( ( object, index ) => { + + + + if ( object !== null && object !== scene && object !== camera ) { + + boxes.push( new THREE.Box3() ); + + selectionBoxes.push( new THREE.Box3Helper( boxes[ index ] ) ); + selectionBoxes[ index ].material.depthTest = false; + selectionBoxes[ index ].material.transparent = true; + selectionBoxes[ index ].visible = false; + + sceneHelpers.add( selectionBoxes[ index ] ); + + transformControls.detach(); + boxes[ index ].setFromObject( object, true ); + + if ( boxes[ index ].isEmpty() === false ) { + + selectionBoxes[ index ].visible = true; + + } + + transformControls.attach( object ); + + } + + + } ); + render(); + + } ); + signals.objectFocused.add( function ( object ) { controls.focus( object ); @@ -435,7 +524,7 @@ function Viewport( editor ) { if ( object !== undefined ) { - box.setFromObject( object, true ); + boxes[ 0 ].setFromObject( object, true ); } @@ -446,9 +535,10 @@ function Viewport( editor ) { signals.objectChanged.add( function ( object ) { - if ( editor.selected === object ) { + const index = selectedObjects.findIndex( itm => itm == object ); + if ( editor.selected === object || editor.selectedObjects.includes( object ) ) { - box.setFromObject( object, true ); + boxes[ index !== - 1 ? index : 0 ].setFromObject( object, true ); } @@ -781,7 +871,7 @@ function Viewport( editor ) { if ( editor.selected !== null ) { editor.selected.updateWorldMatrix( false, true ); // avoid frame late effect for certain skinned meshes (e.g. Michelle.glb) - selectionBox.box.setFromObject( editor.selected, true ); // selection box should reflect current animation state + selectionBoxes[ 0 ].box.setFromObject( editor.selected, true ); // selection box should reflect current animation state }