503 lines
10 KiB
JavaScript
503 lines
10 KiB
JavaScript
import {
|
|
Controls,
|
|
Matrix4,
|
|
Plane,
|
|
Raycaster,
|
|
Vector2,
|
|
Vector3,
|
|
MOUSE,
|
|
TOUCH
|
|
} from 'three';
|
|
|
|
const _plane = new Plane();
|
|
|
|
const _pointer = new Vector2();
|
|
const _offset = new Vector3();
|
|
const _diff = new Vector2();
|
|
const _previousPointer = new Vector2();
|
|
const _intersection = new Vector3();
|
|
const _worldPosition = new Vector3();
|
|
const _inverseMatrix = new Matrix4();
|
|
|
|
const _up = new Vector3();
|
|
const _right = new Vector3();
|
|
|
|
let _selected = null, _hovered = null;
|
|
const _intersections = [];
|
|
|
|
const STATE = {
|
|
NONE: - 1,
|
|
PAN: 0,
|
|
ROTATE: 1
|
|
};
|
|
|
|
/**
|
|
* This class can be used to provide a drag'n'drop interaction.
|
|
*
|
|
* ```js
|
|
* const controls = new DragControls( objects, camera, renderer.domElement );
|
|
*
|
|
* // add event listener to highlight dragged objects
|
|
* controls.addEventListener( 'dragstart', function ( event ) {
|
|
*
|
|
* event.object.material.emissive.set( 0xaaaaaa );
|
|
*
|
|
* } );
|
|
*
|
|
* controls.addEventListener( 'dragend', function ( event ) {
|
|
*
|
|
* event.object.material.emissive.set( 0x000000 );
|
|
*
|
|
* } );
|
|
* ```
|
|
*
|
|
* @augments Controls
|
|
* @three_import import { DragControls } from 'three/addons/controls/DragControls.js';
|
|
*/
|
|
class DragControls extends Controls {
|
|
|
|
/**
|
|
* Constructs a new controls instance.
|
|
*
|
|
* @param {Array<Object3D>} objects - An array of draggable 3D objects.
|
|
* @param {Camera} camera - The camera of the rendered scene.
|
|
* @param {?HTMLDOMElement} [domElement=null] - The HTML DOM element used for event listeners.
|
|
*/
|
|
constructor( objects, camera, domElement = null ) {
|
|
|
|
super( camera, domElement );
|
|
|
|
/**
|
|
* An array of draggable 3D objects.
|
|
*
|
|
* @type {Array<Object3D>}
|
|
*/
|
|
this.objects = objects;
|
|
|
|
/**
|
|
* Whether children of draggable objects can be dragged independently from their parent.
|
|
*
|
|
* @type {boolean}
|
|
* @default true
|
|
*/
|
|
this.recursive = true;
|
|
|
|
/**
|
|
* This option only works if the `objects` array contains a single draggable group object.
|
|
* If set to `true`, the controls does not transform individual objects but the entire group.
|
|
*
|
|
* @type {boolean}
|
|
* @default false
|
|
*/
|
|
this.transformGroup = false;
|
|
|
|
/**
|
|
* The speed at which the object will rotate when dragged in `rotate` mode.
|
|
* The higher the number the faster the rotation.
|
|
*
|
|
* @type {number}
|
|
* @default 1
|
|
*/
|
|
this.rotateSpeed = 1;
|
|
|
|
/**
|
|
* The raycaster used for detecting 3D objects.
|
|
*
|
|
* @type {Raycaster}
|
|
*/
|
|
this.raycaster = new Raycaster();
|
|
|
|
// interaction
|
|
|
|
this.mouseButtons = { LEFT: MOUSE.PAN, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.ROTATE };
|
|
this.touches = { ONE: TOUCH.PAN };
|
|
|
|
// event listeners
|
|
|
|
this._onPointerMove = onPointerMove.bind( this );
|
|
this._onPointerDown = onPointerDown.bind( this );
|
|
this._onPointerCancel = onPointerCancel.bind( this );
|
|
this._onContextMenu = onContextMenu.bind( this );
|
|
|
|
//
|
|
|
|
if ( domElement !== null ) {
|
|
|
|
this.connect( domElement );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
connect( element ) {
|
|
|
|
super.connect( element );
|
|
|
|
this.domElement.addEventListener( 'pointermove', this._onPointerMove );
|
|
this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
|
|
this.domElement.addEventListener( 'pointerup', this._onPointerCancel );
|
|
this.domElement.addEventListener( 'pointerleave', this._onPointerCancel );
|
|
this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
|
|
|
|
this.domElement.style.touchAction = 'none'; // disable touch scroll
|
|
|
|
}
|
|
|
|
disconnect() {
|
|
|
|
this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
|
|
this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
|
|
this.domElement.removeEventListener( 'pointerup', this._onPointerCancel );
|
|
this.domElement.removeEventListener( 'pointerleave', this._onPointerCancel );
|
|
this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
|
|
|
|
this.domElement.style.touchAction = 'auto';
|
|
this.domElement.style.cursor = '';
|
|
|
|
}
|
|
|
|
dispose() {
|
|
|
|
this.disconnect();
|
|
|
|
}
|
|
|
|
_updatePointer( event ) {
|
|
|
|
const rect = this.domElement.getBoundingClientRect();
|
|
|
|
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
|
|
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
|
|
|
|
}
|
|
|
|
_updateState( event ) {
|
|
|
|
// determine action
|
|
|
|
let action;
|
|
|
|
if ( event.pointerType === 'touch' ) {
|
|
|
|
action = this.touches.ONE;
|
|
|
|
} else {
|
|
|
|
switch ( event.button ) {
|
|
|
|
case 0:
|
|
|
|
action = this.mouseButtons.LEFT;
|
|
break;
|
|
|
|
case 1:
|
|
|
|
action = this.mouseButtons.MIDDLE;
|
|
break;
|
|
|
|
case 2:
|
|
|
|
action = this.mouseButtons.RIGHT;
|
|
break;
|
|
|
|
default:
|
|
|
|
action = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// determine state
|
|
|
|
switch ( action ) {
|
|
|
|
case MOUSE.PAN:
|
|
case TOUCH.PAN:
|
|
|
|
this.state = STATE.PAN;
|
|
|
|
break;
|
|
|
|
case MOUSE.ROTATE:
|
|
case TOUCH.ROTATE:
|
|
|
|
this.state = STATE.ROTATE;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
this.state = STATE.NONE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
getRaycaster() {
|
|
|
|
console.warn( 'THREE.DragControls: getRaycaster() has been deprecated. Use controls.raycaster instead.' ); // @deprecated r169
|
|
|
|
return this.raycaster;
|
|
|
|
}
|
|
|
|
setObjects( objects ) {
|
|
|
|
console.warn( 'THREE.DragControls: setObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169
|
|
|
|
this.objects = objects;
|
|
|
|
}
|
|
|
|
getObjects() {
|
|
|
|
console.warn( 'THREE.DragControls: getObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169
|
|
|
|
return this.objects;
|
|
|
|
}
|
|
|
|
activate() {
|
|
|
|
console.warn( 'THREE.DragControls: activate() has been renamed to connect().' ); // @deprecated r169
|
|
this.connect();
|
|
|
|
}
|
|
|
|
deactivate() {
|
|
|
|
console.warn( 'THREE.DragControls: deactivate() has been renamed to disconnect().' ); // @deprecated r169
|
|
this.disconnect();
|
|
|
|
}
|
|
|
|
set mode( value ) {
|
|
|
|
console.warn( 'THREE.DragControls: The .mode property has been removed. Define the type of transformation via the .mouseButtons or .touches properties.' ); // @deprecated r169
|
|
|
|
}
|
|
|
|
get mode() {
|
|
|
|
console.warn( 'THREE.DragControls: The .mode property has been removed. Define the type of transformation via the .mouseButtons or .touches properties.' ); // @deprecated r169
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function onPointerMove( event ) {
|
|
|
|
const camera = this.object;
|
|
const domElement = this.domElement;
|
|
const raycaster = this.raycaster;
|
|
|
|
if ( this.enabled === false ) return;
|
|
|
|
this._updatePointer( event );
|
|
|
|
raycaster.setFromCamera( _pointer, camera );
|
|
|
|
if ( _selected ) {
|
|
|
|
if ( this.state === STATE.PAN ) {
|
|
|
|
if ( raycaster.ray.intersectPlane( _plane, _intersection ) ) {
|
|
|
|
_selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
|
|
|
|
}
|
|
|
|
} else if ( this.state === STATE.ROTATE ) {
|
|
|
|
_diff.subVectors( _pointer, _previousPointer ).multiplyScalar( this.rotateSpeed );
|
|
_selected.rotateOnWorldAxis( _up, _diff.x );
|
|
_selected.rotateOnWorldAxis( _right.normalize(), - _diff.y );
|
|
|
|
}
|
|
|
|
this.dispatchEvent( { type: 'drag', object: _selected } );
|
|
|
|
_previousPointer.copy( _pointer );
|
|
|
|
} else {
|
|
|
|
// hover support
|
|
|
|
if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) {
|
|
|
|
_intersections.length = 0;
|
|
|
|
raycaster.setFromCamera( _pointer, camera );
|
|
raycaster.intersectObjects( this.objects, this.recursive, _intersections );
|
|
|
|
if ( _intersections.length > 0 ) {
|
|
|
|
const object = _intersections[ 0 ].object;
|
|
|
|
_plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) );
|
|
|
|
if ( _hovered !== object && _hovered !== null ) {
|
|
|
|
this.dispatchEvent( { type: 'hoveroff', object: _hovered } );
|
|
|
|
domElement.style.cursor = 'auto';
|
|
_hovered = null;
|
|
|
|
}
|
|
|
|
if ( _hovered !== object ) {
|
|
|
|
this.dispatchEvent( { type: 'hoveron', object: object } );
|
|
|
|
domElement.style.cursor = 'pointer';
|
|
_hovered = object;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ( _hovered !== null ) {
|
|
|
|
this.dispatchEvent( { type: 'hoveroff', object: _hovered } );
|
|
|
|
domElement.style.cursor = 'auto';
|
|
_hovered = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_previousPointer.copy( _pointer );
|
|
|
|
}
|
|
|
|
function onPointerDown( event ) {
|
|
|
|
const camera = this.object;
|
|
const domElement = this.domElement;
|
|
const raycaster = this.raycaster;
|
|
|
|
if ( this.enabled === false ) return;
|
|
|
|
this._updatePointer( event );
|
|
this._updateState( event );
|
|
|
|
_intersections.length = 0;
|
|
|
|
raycaster.setFromCamera( _pointer, camera );
|
|
raycaster.intersectObjects( this.objects, this.recursive, _intersections );
|
|
|
|
if ( _intersections.length > 0 ) {
|
|
|
|
if ( this.transformGroup === true ) {
|
|
|
|
// look for the outermost group in the object's upper hierarchy
|
|
|
|
_selected = findGroup( _intersections[ 0 ].object );
|
|
|
|
} else {
|
|
|
|
_selected = _intersections[ 0 ].object;
|
|
|
|
}
|
|
|
|
_plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
|
|
|
|
if ( raycaster.ray.intersectPlane( _plane, _intersection ) ) {
|
|
|
|
if ( this.state === STATE.PAN ) {
|
|
|
|
_inverseMatrix.copy( _selected.parent.matrixWorld ).invert();
|
|
_offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
|
|
|
|
} else if ( this.state === STATE.ROTATE ) {
|
|
|
|
// the controls only support Y+ up
|
|
_up.set( 0, 1, 0 ).applyQuaternion( camera.quaternion ).normalize();
|
|
_right.set( 1, 0, 0 ).applyQuaternion( camera.quaternion ).normalize();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
domElement.style.cursor = 'move';
|
|
|
|
this.dispatchEvent( { type: 'dragstart', object: _selected } );
|
|
|
|
}
|
|
|
|
_previousPointer.copy( _pointer );
|
|
|
|
}
|
|
|
|
function onPointerCancel() {
|
|
|
|
if ( this.enabled === false ) return;
|
|
|
|
if ( _selected ) {
|
|
|
|
this.dispatchEvent( { type: 'dragend', object: _selected } );
|
|
|
|
_selected = null;
|
|
|
|
}
|
|
|
|
this.domElement.style.cursor = _hovered ? 'pointer' : 'auto';
|
|
|
|
this.state = STATE.NONE;
|
|
|
|
}
|
|
|
|
function onContextMenu( event ) {
|
|
|
|
if ( this.enabled === false ) return;
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
function findGroup( obj, group = null ) {
|
|
|
|
if ( obj.isGroup ) group = obj;
|
|
|
|
if ( obj.parent === null ) return group;
|
|
|
|
return findGroup( obj.parent, group );
|
|
|
|
}
|
|
|
|
/**
|
|
* Fires when the user drags a 3D object.
|
|
*
|
|
* @event DragControls#drag
|
|
* @type {Object}
|
|
*/
|
|
|
|
/**
|
|
* Fires when the user has finished dragging a 3D object.
|
|
*
|
|
* @event DragControls#dragend
|
|
* @type {Object}
|
|
*/
|
|
|
|
/**
|
|
* Fires when the pointer is moved onto a 3D object, or onto one of its children.
|
|
*
|
|
* @event DragControls#hoveron
|
|
* @type {Object}
|
|
*/
|
|
|
|
/**
|
|
* Fires when the pointer is moved out of a 3D object.
|
|
*
|
|
* @event DragControls#hoveroff
|
|
* @type {Object}
|
|
*/
|
|
|
|
export { DragControls };
|