three
Version:
JavaScript 3D library
502 lines (319 loc) • 10.5 kB
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
*/
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 };