Skip to content

⬅️ Back to Table of Contents

📄 DragControls.js

📊 Analysis Summary

Metric Count
🔧 Functions 15
🧱 Classes 1
📦 Imports 8
📊 Variables & Constants 22

📚 Table of Contents

🛠️ File Location:

📂 examples/jsm/controls/DragControls.js

📦 Imports

Name Source
Controls three
Matrix4 three
Plane three
Raycaster three
Vector2 three
Vector3 three
MOUSE three
TOUCH three

Variables & Constants

Name Type Kind Value Exported
_plane any let/var new Plane()
_pointer any let/var new Vector2()
_offset any let/var new Vector3()
_diff any let/var new Vector2()
_previousPointer any let/var new Vector2()
_intersection any let/var new Vector3()
_worldPosition any let/var new Vector3()
_inverseMatrix any let/var new Matrix4()
_up any let/var new Vector3()
_right any let/var new Vector3()
_selected any let/var null
_hovered any let/var null
_intersections any[] let/var []
STATE { NONE: number; PAN: number; ROTATE: ... let/var { NONE: - 1, PAN: 0, ROTATE: 1 }
action any let/var *not shown*
camera any let/var this.object
domElement any let/var this.domElement
raycaster any let/var this.raycaster
object any let/var _intersections[ 0 ].object
camera any let/var this.object
domElement any let/var this.domElement
raycaster any let/var this.raycaster

Functions

DragControls.connect(element: any): void

Parameters:

  • element any

Returns: void

Calls:

  • super.connect
  • this.domElement.addEventListener
Code
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

    }

DragControls.disconnect(): void

Returns: void

Calls:

  • this.domElement.removeEventListener
Code
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 = '';

    }

DragControls.dispose(): void

Returns: void

Calls:

  • this.disconnect
Code
dispose() {

        this.disconnect();

    }

DragControls._updatePointer(event: any): void

Parameters:

  • event any

Returns: void

Calls:

  • this.domElement.getBoundingClientRect
Code
_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;

    }

DragControls._updateState(event: any): void

Parameters:

  • event any

Returns: void

Internal Comments:

// determine action (x2)
// determine state

Code
_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;

        }

    }

DragControls.getRaycaster(): Raycaster

Returns: Raycaster

Calls:

  • console.warn
Code
getRaycaster() {

        console.warn( 'THREE.DragControls: getRaycaster() has been deprecated. Use controls.raycaster instead.' ); // @deprecated r169

        return this.raycaster;

    }

DragControls.setObjects(objects: any): void

Parameters:

  • objects any

Returns: void

Calls:

  • console.warn
Code
setObjects( objects ) {

        console.warn( 'THREE.DragControls: setObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169

        this.objects = objects;

    }

DragControls.getObjects(): Object3D[]

Returns: Object3D[]

Calls:

  • console.warn
Code
getObjects() {

        console.warn( 'THREE.DragControls: getObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169

        return this.objects;

    }

DragControls.activate(): void

Returns: void

Calls:

  • console.warn
  • this.connect
Code
activate() {

        console.warn( 'THREE.DragControls: activate() has been renamed to connect().' ); // @deprecated r169
        this.connect();

    }

DragControls.deactivate(): void

Returns: void

Calls:

  • console.warn
  • this.disconnect
Code
deactivate() {

        console.warn( 'THREE.DragControls: deactivate() has been renamed to disconnect().' ); // @deprecated r169
        this.disconnect();

    }

onPointerMove(event: any): void

Parameters:

  • event any

Returns: void

Calls:

  • this._updatePointer
  • raycaster.setFromCamera
  • raycaster.ray.intersectPlane
  • _selected.position.copy
  • _intersection.sub( _offset ).applyMatrix4
  • _diff.subVectors( _pointer, _previousPointer ).multiplyScalar
  • _selected.rotateOnWorldAxis
  • _right.normalize
  • this.dispatchEvent
  • _previousPointer.copy
  • raycaster.intersectObjects
  • _plane.setFromNormalAndCoplanarPoint
  • camera.getWorldDirection
  • _worldPosition.setFromMatrixPosition

Internal Comments:

// hover support

Code
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 );

}

onPointerDown(event: any): void

Parameters:

  • event any

Returns: void

Calls:

  • this._updatePointer
  • this._updateState
  • raycaster.setFromCamera
  • raycaster.intersectObjects
  • findGroup
  • _plane.setFromNormalAndCoplanarPoint
  • camera.getWorldDirection
  • _worldPosition.setFromMatrixPosition
  • raycaster.ray.intersectPlane
  • _inverseMatrix.copy( _selected.parent.matrixWorld ).invert
  • _offset.copy( _intersection ).sub
  • _up.set( 0, 1, 0 ).applyQuaternion( camera.quaternion ).normalize
  • _right.set( 1, 0, 0 ).applyQuaternion( camera.quaternion ).normalize
  • this.dispatchEvent
  • _previousPointer.copy

Internal Comments:

// look for the outermost group in the object's upper hierarchy (x3)
// the controls only support Y+ up (x8)

Code
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 );

}

onPointerCancel(): void

Returns: void

Calls:

  • this.dispatchEvent
Code
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;

}

onContextMenu(event: any): void

Parameters:

  • event any

Returns: void

Calls:

  • event.preventDefault
Code
function onContextMenu( event ) {

    if ( this.enabled === false ) return;

    event.preventDefault();

}

findGroup(obj: any, group: any): any

Parameters:

  • obj any
  • group any

Returns: any

Calls:

  • findGroup
Code
function findGroup( obj, group = null ) {

    if ( obj.isGroup ) group = obj;

    if ( obj.parent === null ) return group;

    return findGroup( obj.parent, group );

}

Classes

DragControls

Class Code
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

    }

}

Methods

connect(element: any): void
Code
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(): void
Code
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(): void
Code
dispose() {

        this.disconnect();

    }
_updatePointer(event: any): void
Code
_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: any): void
Code
_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(): Raycaster
Code
getRaycaster() {

        console.warn( 'THREE.DragControls: getRaycaster() has been deprecated. Use controls.raycaster instead.' ); // @deprecated r169

        return this.raycaster;

    }
setObjects(objects: any): void
Code
setObjects( objects ) {

        console.warn( 'THREE.DragControls: setObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169

        this.objects = objects;

    }
getObjects(): Object3D[]
Code
getObjects() {

        console.warn( 'THREE.DragControls: getObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169

        return this.objects;

    }
activate(): void
Code
activate() {

        console.warn( 'THREE.DragControls: activate() has been renamed to connect().' ); // @deprecated r169
        this.connect();

    }
deactivate(): void
Code
deactivate() {

        console.warn( 'THREE.DragControls: deactivate() has been renamed to disconnect().' ); // @deprecated r169
        this.disconnect();

    }