diff --git a/examples/r3f.html b/examples/r3f.html new file mode 100644 index 00000000..06b0e826 --- /dev/null +++ b/examples/r3f.html @@ -0,0 +1,68 @@ + + + + + +=^.^= + + + +
+

GitHub repo

+ + + +
+ + + + + + + diff --git a/src/CameraControls.ts b/src/CameraControls.ts index ebbda397..ac8e1e4d 100644 --- a/src/CameraControls.ts +++ b/src/CameraControls.ts @@ -341,7 +341,7 @@ export class CameraControls extends EventDispatcher { protected _yAxisUpSpaceInverse: _THREE.Quaternion; protected _state: ACTION = ACTION.NONE; - protected _domElement: HTMLElement; + protected _domElement?: HTMLElement; protected _viewport: _THREE.Vector4 | null = null; // the location of focus, where the object orbits around @@ -401,7 +401,7 @@ export class CameraControls extends EventDispatcher { */ constructor( camera: _THREE.PerspectiveCamera | _THREE.OrthographicCamera, - domElement: HTMLElement, + domElement?: HTMLElement, ) { super(); @@ -418,12 +418,6 @@ export class CameraControls extends EventDispatcher { this._yAxisUpSpaceInverse = quatInvertCompat( this._yAxisUpSpace.clone() ); this._state = ACTION.NONE; - this._domElement = domElement; - this._domElement.style.touchAction = 'none'; - this._domElement.style.userSelect = 'none'; - this._domElement.style.webkitUserSelect = 'none'; - if ( 'setAttribute' in this._domElement ) this._domElement.setAttribute( 'data-camera-controls-version', VERSION ); - // the location this._target = new THREE.Vector3(); this._targetEnd = this._target.clone(); @@ -482,168 +476,129 @@ export class CameraControls extends EventDispatcher { three: ACTION.TOUCH_TRUCK, }; - if ( this._domElement ) { - - const dragStartPosition = new THREE.Vector2() as _THREE.Vector2; - const lastDragPosition = new THREE.Vector2() as _THREE.Vector2; - const dollyStart = new THREE.Vector2() as _THREE.Vector2; + const dragStartPosition = new THREE.Vector2() as _THREE.Vector2; + const lastDragPosition = new THREE.Vector2() as _THREE.Vector2; + const dollyStart = new THREE.Vector2() as _THREE.Vector2; - const onPointerDown = ( event: PointerEvent ) => { + const onPointerDown = ( event: PointerEvent ) => { - if ( ! this._enabled ) return; + if ( ! this._enabled || ! this._domElement ) return; - // Don't call `event.preventDefault()` on the pointerdown event - // to keep receiving pointermove evens outside dragging iframe - // https://taye.me/blog/tips/2015/11/16/mouse-drag-outside-iframe/ - - const pointer = { - pointerId: event.pointerId, - clientX: event.clientX, - clientY: event.clientY, - deltaX: 0, - deltaY: 0, - }; - this._activePointers.push( pointer ); - - // eslint-disable-next-line no-undef - this._domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove, { passive: false } as AddEventListenerOptions ); - this._domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); - - this._domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove, { passive: false } ); - this._domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp ); - - startDragging( event ); + // Don't call `event.preventDefault()` on the pointerdown event + // to keep receiving pointermove evens outside dragging iframe + // https://taye.me/blog/tips/2015/11/16/mouse-drag-outside-iframe/ + const pointer = { + pointerId: event.pointerId, + clientX: event.clientX, + clientY: event.clientY, + deltaX: 0, + deltaY: 0, }; + this._activePointers.push( pointer ); - const onMouseDown = ( event: MouseEvent ) => { + // eslint-disable-next-line no-undef + this._domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove, { passive: false } as AddEventListenerOptions ); + this._domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); - if ( ! this._enabled ) return; + this._domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove, { passive: false } ); + this._domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp ); - const pointer = { - pointerId: 0, - clientX: event.clientX, - clientY: event.clientY, - deltaX: 0, - deltaY: 0, - }; - this._activePointers.push( pointer ); + startDragging( event ); - // see https://github.com/microsoft/TypeScript/issues/32912#issuecomment-522142969 - // eslint-disable-next-line no-undef - this._domElement.ownerDocument.removeEventListener( 'mousemove', onMouseMove ); - this._domElement.ownerDocument.removeEventListener( 'mouseup', onMouseUp ); + }; - this._domElement.ownerDocument.addEventListener( 'mousemove', onMouseMove ); - this._domElement.ownerDocument.addEventListener( 'mouseup', onMouseUp ); + const onMouseDown = ( event: MouseEvent ) => { - startDragging( event ); + if ( ! this._enabled || ! this._domElement ) return; + const pointer = { + pointerId: 0, + clientX: event.clientX, + clientY: event.clientY, + deltaX: 0, + deltaY: 0, }; + this._activePointers.push( pointer ); - const onTouchStart = ( event:TouchEvent ): void => { - - if ( ! this._enabled ) return; + // see https://github.com/microsoft/TypeScript/issues/32912#issuecomment-522142969 + // eslint-disable-next-line no-undef + this._domElement.ownerDocument.removeEventListener( 'mousemove', onMouseMove ); + this._domElement.ownerDocument.removeEventListener( 'mouseup', onMouseUp ); - event.preventDefault(); - - Array.prototype.forEach.call( event.changedTouches, ( touch ) => { - - const pointer = { - pointerId: touch.identifier, - clientX: touch.clientX, - clientY: touch.clientY, - deltaX: 0, - deltaY: 0, - }; - this._activePointers.push( pointer ); - - } ); - - // eslint-disable-next-line no-undef - this._domElement.ownerDocument.removeEventListener( 'touchmove', onTouchMove, { passive: false } as AddEventListenerOptions ); - this._domElement.ownerDocument.removeEventListener( 'touchend', onTouchEnd ); + this._domElement.ownerDocument.addEventListener( 'mousemove', onMouseMove ); + this._domElement.ownerDocument.addEventListener( 'mouseup', onMouseUp ); - this._domElement.ownerDocument.addEventListener( 'touchmove', onTouchMove, { passive: false } ); - this._domElement.ownerDocument.addEventListener( 'touchend', onTouchEnd ); + startDragging( event ); - startDragging( event ); - - }; - - const onPointerMove = ( event: PointerEvent ) => { - - if ( event.cancelable ) event.preventDefault(); + }; - const pointerId = event.pointerId; - const pointer = this._findPointerById( pointerId ); + const onTouchStart = ( event:TouchEvent ): void => { - if ( ! pointer ) return; + if ( ! this._enabled || ! this._domElement ) return; - pointer.clientX = event.clientX; - pointer.clientY = event.clientY; - pointer.deltaX = event.movementX; - pointer.deltaY = event.movementY; + event.preventDefault(); - if ( event.pointerType === 'touch' ) { + Array.prototype.forEach.call( event.changedTouches, ( touch ) => { - switch ( this._activePointers.length ) { + const pointer = { + pointerId: touch.identifier, + clientX: touch.clientX, + clientY: touch.clientY, + deltaX: 0, + deltaY: 0, + }; + this._activePointers.push( pointer ); - case 1: + } ); - this._state = this.touches.one; - break; + // eslint-disable-next-line no-undef + this._domElement.ownerDocument.removeEventListener( 'touchmove', onTouchMove, { passive: false } as AddEventListenerOptions ); + this._domElement.ownerDocument.removeEventListener( 'touchend', onTouchEnd ); - case 2: + this._domElement.ownerDocument.addEventListener( 'touchmove', onTouchMove, { passive: false } ); + this._domElement.ownerDocument.addEventListener( 'touchend', onTouchEnd ); - this._state = this.touches.two; - break; + startDragging( event ); - case 3: + }; - this._state = this.touches.three; - break; + const onPointerMove = ( event: PointerEvent ) => { - } + if ( event.cancelable ) event.preventDefault(); - } else { + const pointerId = event.pointerId; + const pointer = this._findPointerById( pointerId ); - this._state = 0; + if ( ! pointer ) return; - if ( ( event.buttons & MOUSE_BUTTON.LEFT ) === MOUSE_BUTTON.LEFT ) { + pointer.clientX = event.clientX; + pointer.clientY = event.clientY; + pointer.deltaX = event.movementX; + pointer.deltaY = event.movementY; - this._state = this._state | this.mouseButtons.left; + if ( event.pointerType === 'touch' ) { - } + switch ( this._activePointers.length ) { - if ( ( event.buttons & MOUSE_BUTTON.MIDDLE ) === MOUSE_BUTTON.MIDDLE ) { + case 1: - this._state = this._state | this.mouseButtons.middle; + this._state = this.touches.one; + break; - } + case 2: - if ( ( event.buttons & MOUSE_BUTTON.RIGHT ) === MOUSE_BUTTON.RIGHT ) { + this._state = this.touches.two; + break; - this._state = this._state | this.mouseButtons.right; + case 3: - } + this._state = this.touches.three; + break; } - dragging(); - - }; - - const onMouseMove = ( event: MouseEvent ) => { - - const pointer = this._findPointerById( 0 ); - - if ( ! pointer ) return; - - pointer.clientX = event.clientX; - pointer.clientY = event.clientY; - pointer.deltaX = event.movementX; - pointer.deltaY = event.movementY; + } else { this._state = 0; @@ -665,92 +620,75 @@ export class CameraControls extends EventDispatcher { } - dragging(); - - }; - - const onTouchMove = ( event: TouchEvent ) => { - - if ( event.cancelable ) event.preventDefault(); - - Array.prototype.forEach.call( event.changedTouches, ( touch: Touch ) => { - - const pointerId = touch.identifier; - const pointer = this._findPointerById( pointerId ); + } - if ( ! pointer ) return; + dragging(); - pointer.clientX = touch.clientX; - pointer.clientY = touch.clientY; - // touch event does not have movementX and movementY. + }; - } ); + const onMouseMove = ( event: MouseEvent ) => { - dragging(); + const pointer = this._findPointerById( 0 ); - }; + if ( ! pointer ) return; - const onPointerUp = ( event: PointerEvent ) => { - - const pointerId = event.pointerId; - const pointer = this._findPointerById( pointerId ); - pointer && this._activePointers.splice( this._activePointers.indexOf( pointer ), 1 ); + pointer.clientX = event.clientX; + pointer.clientY = event.clientY; + pointer.deltaX = event.movementX; + pointer.deltaY = event.movementY; - if ( event.pointerType === 'touch' ) { + this._state = 0; - switch ( this._activePointers.length ) { + if ( ( event.buttons & MOUSE_BUTTON.LEFT ) === MOUSE_BUTTON.LEFT ) { - case 0: + this._state = this._state | this.mouseButtons.left; - this._state = ACTION.NONE; - break; + } - case 1: + if ( ( event.buttons & MOUSE_BUTTON.MIDDLE ) === MOUSE_BUTTON.MIDDLE ) { - this._state = this.touches.one; - break; + this._state = this._state | this.mouseButtons.middle; - case 2: + } - this._state = this.touches.two; - break; + if ( ( event.buttons & MOUSE_BUTTON.RIGHT ) === MOUSE_BUTTON.RIGHT ) { - case 3: + this._state = this._state | this.mouseButtons.right; - this._state = this.touches.three; - break; + } - } + dragging(); - } else { + }; - this._state = ACTION.NONE; + const onTouchMove = ( event: TouchEvent ) => { - } + if ( event.cancelable ) event.preventDefault(); - endDragging(); + Array.prototype.forEach.call( event.changedTouches, ( touch: Touch ) => { - }; + const pointerId = touch.identifier; + const pointer = this._findPointerById( pointerId ); - const onMouseUp = () => { + if ( ! pointer ) return; - const pointer = this._findPointerById( 0 ); - pointer && this._activePointers.splice( this._activePointers.indexOf( pointer ), 1 ); - this._state = ACTION.NONE; + pointer.clientX = touch.clientX; + pointer.clientY = touch.clientY; + // touch event does not have movementX and movementY. - endDragging(); + } ); - }; + dragging(); - const onTouchEnd = ( event: TouchEvent ) => { + }; - Array.prototype.forEach.call( event.changedTouches, ( touch: Touch ) => { + const onPointerUp = ( event: PointerEvent ) => { - const pointerId = touch.identifier; - const pointer = this._findPointerById( pointerId ); - pointer && this._activePointers.splice( this._activePointers.indexOf( pointer ), 1 ); + const pointerId = event.pointerId; + const pointer = this._findPointerById( pointerId ); + pointer && this._activePointers.splice( this._activePointers.indexOf( pointer ), 1 ); - } ); + if ( event.pointerType === 'touch' ) { switch ( this._activePointers.length ) { @@ -776,283 +714,346 @@ export class CameraControls extends EventDispatcher { } - endDragging(); - - }; + } else { - let lastScrollTimeStamp = - 1; + this._state = ACTION.NONE; - const onMouseWheel = ( event: WheelEvent ): void => { + } - if ( ! this._enabled || this.mouseButtons.wheel === ACTION.NONE ) return; + endDragging(); - event.preventDefault(); + }; - if ( - this.dollyToCursor || - this.mouseButtons.wheel === ACTION.ROTATE || - this.mouseButtons.wheel === ACTION.TRUCK - ) { + const onMouseUp = () => { - const now = performance.now(); + const pointer = this._findPointerById( 0 ); + pointer && this._activePointers.splice( this._activePointers.indexOf( pointer ), 1 ); + this._state = ACTION.NONE; - // only need to fire this at scroll start. - if ( lastScrollTimeStamp - now < 1000 ) this._getClientRect( this._elementRect ); - lastScrollTimeStamp = now; + endDragging(); - } + }; - // Ref: https://github.com/cedricpinson/osgjs/blob/00e5a7e9d9206c06fdde0436e1d62ab7cb5ce853/sources/osgViewer/input/source/InputSourceMouse.js#L89-L103 - const deltaYFactor = isMac ? - 1 : - 3; - const delta = ( event.deltaMode === 1 ) ? event.deltaY / deltaYFactor : event.deltaY / ( deltaYFactor * 10 ); - const x = this.dollyToCursor ? ( event.clientX - this._elementRect.x ) / this._elementRect.width * 2 - 1 : 0; - const y = this.dollyToCursor ? ( event.clientY - this._elementRect.y ) / this._elementRect.height * - 2 + 1 : 0; + const onTouchEnd = ( event: TouchEvent ) => { - switch ( this.mouseButtons.wheel ) { + Array.prototype.forEach.call( event.changedTouches, ( touch: Touch ) => { - case ACTION.ROTATE: { + const pointerId = touch.identifier; + const pointer = this._findPointerById( pointerId ); + pointer && this._activePointers.splice( this._activePointers.indexOf( pointer ), 1 ); - this._rotateInternal( event.deltaX, event.deltaY ); - break; + } ); - } + switch ( this._activePointers.length ) { - case ACTION.TRUCK: { + case 0: - this._truckInternal( event.deltaX, event.deltaY, false ); - break; + this._state = ACTION.NONE; + break; - } + case 1: - case ACTION.OFFSET: { + this._state = this.touches.one; + break; - this._truckInternal( event.deltaX, event.deltaY, true ); - break; + case 2: - } + this._state = this.touches.two; + break; - case ACTION.DOLLY: { + case 3: - this._dollyInternal( - delta, x, y ); - break; + this._state = this.touches.three; + break; - } + } - case ACTION.ZOOM: { + endDragging(); - this._zoomInternal( - delta, x, y ); - break; + }; - } + let lastScrollTimeStamp = - 1; - } + const onMouseWheel = ( event: WheelEvent ): void => { - this.dispatchEvent( { type: 'control' } ); + if ( ! this._enabled || this.mouseButtons.wheel === ACTION.NONE ) return; - }; + event.preventDefault(); - const onContextMenu = ( event: Event ): void => { + if ( + this.dollyToCursor || + this.mouseButtons.wheel === ACTION.ROTATE || + this.mouseButtons.wheel === ACTION.TRUCK + ) { - if ( ! this._enabled ) return; + const now = performance.now(); - event.preventDefault(); + // only need to fire this at scroll start. + if ( lastScrollTimeStamp - now < 1000 ) this._getClientRect( this._elementRect ); + lastScrollTimeStamp = now; - }; + } - const startDragging = ( event: PointerEvent | MouseEvent | TouchEvent ): void => { + // Ref: https://github.com/cedricpinson/osgjs/blob/00e5a7e9d9206c06fdde0436e1d62ab7cb5ce853/sources/osgViewer/input/source/InputSourceMouse.js#L89-L103 + const deltaYFactor = isMac ? - 1 : - 3; + const delta = ( event.deltaMode === 1 ) ? event.deltaY / deltaYFactor : event.deltaY / ( deltaYFactor * 10 ); + const x = this.dollyToCursor ? ( event.clientX - this._elementRect.x ) / this._elementRect.width * 2 - 1 : 0; + const y = this.dollyToCursor ? ( event.clientY - this._elementRect.y ) / this._elementRect.height * - 2 + 1 : 0; - if ( ! this._enabled ) return; + switch ( this.mouseButtons.wheel ) { - extractClientCoordFromEvent( this._activePointers, _v2 ); + case ACTION.ROTATE: { - this._getClientRect( this._elementRect ); - dragStartPosition.copy( _v2 ); - lastDragPosition.copy( _v2 ); + this._rotateInternal( event.deltaX, event.deltaY ); + break; - const isMultiTouch = this._activePointers.length >= 2; + } - if ( isMultiTouch ) { + case ACTION.TRUCK: { - // 2 finger pinch - const dx = _v2.x - this._activePointers[ 1 ].clientX; - const dy = _v2.y - this._activePointers[ 1 ].clientY; - const distance = Math.sqrt( dx * dx + dy * dy ); + this._truckInternal( event.deltaX, event.deltaY, false ); + break; - dollyStart.set( 0, distance ); + } - // center coords of 2 finger truck - const x = ( this._activePointers[ 0 ].clientX + this._activePointers[ 1 ].clientX ) * 0.5; - const y = ( this._activePointers[ 0 ].clientY + this._activePointers[ 1 ].clientY ) * 0.5; + case ACTION.OFFSET: { - lastDragPosition.set( x, y ); + this._truckInternal( event.deltaX, event.deltaY, true ); + break; } - if ( - 'touches' in event || - 'pointerType' in event && event.pointerType === 'touch' - ) { + case ACTION.DOLLY: { + + this._dollyInternal( - delta, x, y ); + break; + + } - switch ( this._activePointers.length ) { + case ACTION.ZOOM: { - case 1: + this._zoomInternal( - delta, x, y ); + break; - this._state = this.touches.one; - break; + } - case 2: + } - this._state = this.touches.two; - break; + this.dispatchEvent( { type: 'control' } ); - case 3: + }; - this._state = this.touches.three; - break; + const onContextMenu = ( event: Event ): void => { - } + if ( ! this._enabled ) return; - } else { + event.preventDefault(); - this._state = 0; + }; - if ( ( event.buttons & MOUSE_BUTTON.LEFT ) === MOUSE_BUTTON.LEFT ) { + const startDragging = ( event: PointerEvent | MouseEvent | TouchEvent ): void => { - this._state = this._state | this.mouseButtons.left; + if ( ! this._enabled ) return; - } + extractClientCoordFromEvent( this._activePointers, _v2 ); - if ( ( event.buttons & MOUSE_BUTTON.MIDDLE ) === MOUSE_BUTTON.MIDDLE ) { + this._getClientRect( this._elementRect ); + dragStartPosition.copy( _v2 ); + lastDragPosition.copy( _v2 ); - this._state = this._state | this.mouseButtons.middle; + const isMultiTouch = this._activePointers.length >= 2; - } + if ( isMultiTouch ) { - if ( ( event.buttons & MOUSE_BUTTON.RIGHT ) === MOUSE_BUTTON.RIGHT ) { + // 2 finger pinch + const dx = _v2.x - this._activePointers[ 1 ].clientX; + const dy = _v2.y - this._activePointers[ 1 ].clientY; + const distance = Math.sqrt( dx * dx + dy * dy ); - this._state = this._state | this.mouseButtons.right; + dollyStart.set( 0, distance ); - } + // center coords of 2 finger truck + const x = ( this._activePointers[ 0 ].clientX + this._activePointers[ 1 ].clientX ) * 0.5; + const y = ( this._activePointers[ 0 ].clientY + this._activePointers[ 1 ].clientY ) * 0.5; - } + lastDragPosition.set( x, y ); - this.dispatchEvent( { type: 'controlstart' } ); + } - }; + if ( + 'touches' in event || + 'pointerType' in event && event.pointerType === 'touch' + ) { - const dragging = (): void => { + switch ( this._activePointers.length ) { - if ( ! this._enabled ) return; + case 1: - extractClientCoordFromEvent( this._activePointers, _v2 ); + this._state = this.touches.one; + break; - // When pointer lock is enabled clientX, clientY, screenX, and screenY remain 0. - // If pointer lock is enabled, use the Delta directory, and assume active-pointer is not multiple. - const isPointerLockActive = this._domElement && document.pointerLockElement === this._domElement; - const deltaX = isPointerLockActive ? - this._activePointers[ 0 ].deltaX : lastDragPosition.x - _v2.x; - const deltaY = isPointerLockActive ? - this._activePointers[ 0 ].deltaY : lastDragPosition.y - _v2.y; + case 2: - lastDragPosition.copy( _v2 ); + this._state = this.touches.two; + break; - if ( - ( this._state & ACTION.ROTATE ) === ACTION.ROTATE || - ( this._state & ACTION.TOUCH_ROTATE ) === ACTION.TOUCH_ROTATE || - ( this._state & ACTION.TOUCH_DOLLY_ROTATE ) === ACTION.TOUCH_DOLLY_ROTATE || - ( this._state & ACTION.TOUCH_ZOOM_ROTATE ) === ACTION.TOUCH_ZOOM_ROTATE - ) { + case 3: - this._rotateInternal( deltaX, deltaY ); + this._state = this.touches.three; + break; } - if ( - ( this._state & ACTION.DOLLY ) === ACTION.DOLLY || - ( this._state & ACTION.ZOOM ) === ACTION.ZOOM - ) { + } else { - const dollyX = this.dollyToCursor ? ( dragStartPosition.x - this._elementRect.x ) / this._elementRect.width * 2 - 1 : 0; - const dollyY = this.dollyToCursor ? ( dragStartPosition.y - this._elementRect.y ) / this._elementRect.height * - 2 + 1 : 0; - ( this._state & ACTION.DOLLY ) === ACTION.DOLLY ? - this._dollyInternal( deltaY * TOUCH_DOLLY_FACTOR, dollyX, dollyY ) : - this._zoomInternal( deltaY * TOUCH_DOLLY_FACTOR, dollyX, dollyY ); + this._state = 0; - } + if ( ( event.buttons & MOUSE_BUTTON.LEFT ) === MOUSE_BUTTON.LEFT ) { - if ( - ( this._state & ACTION.TOUCH_DOLLY ) === ACTION.TOUCH_DOLLY || - ( this._state & ACTION.TOUCH_ZOOM ) === ACTION.TOUCH_ZOOM || - ( this._state & ACTION.TOUCH_DOLLY_TRUCK ) === ACTION.TOUCH_DOLLY_TRUCK || - ( this._state & ACTION.TOUCH_ZOOM_TRUCK ) === ACTION.TOUCH_ZOOM_TRUCK || - ( this._state & ACTION.TOUCH_DOLLY_OFFSET ) === ACTION.TOUCH_DOLLY_OFFSET || - ( this._state & ACTION.TOUCH_ZOOM_OFFSET ) === ACTION.TOUCH_ZOOM_OFFSET || - ( this._state & ACTION.TOUCH_DOLLY_ROTATE ) === ACTION.TOUCH_DOLLY_ROTATE || - ( this._state & ACTION.TOUCH_ZOOM_ROTATE ) === ACTION.TOUCH_ZOOM_ROTATE - ) { - - const dx = _v2.x - this._activePointers[ 1 ].clientX; - const dy = _v2.y - this._activePointers[ 1 ].clientY; - const distance = Math.sqrt( dx * dx + dy * dy ); - const dollyDelta = dollyStart.y - distance; - dollyStart.set( 0, distance ); - - const dollyX = this.dollyToCursor ? ( lastDragPosition.x - this._elementRect.x ) / this._elementRect.width * 2 - 1 : 0; - const dollyY = this.dollyToCursor ? ( lastDragPosition.y - this._elementRect.y ) / this._elementRect.height * - 2 + 1 : 0; - - ( this._state & ACTION.TOUCH_DOLLY ) === ACTION.TOUCH_DOLLY || - ( this._state & ACTION.TOUCH_DOLLY_ROTATE ) === ACTION.TOUCH_DOLLY_ROTATE || - ( this._state & ACTION.TOUCH_DOLLY_TRUCK ) === ACTION.TOUCH_DOLLY_TRUCK || - ( this._state & ACTION.TOUCH_DOLLY_OFFSET ) === ACTION.TOUCH_DOLLY_OFFSET ? - this._dollyInternal( dollyDelta * TOUCH_DOLLY_FACTOR, dollyX, dollyY ) : - this._zoomInternal( dollyDelta * TOUCH_DOLLY_FACTOR, dollyX, dollyY ); + this._state = this._state | this.mouseButtons.left; } - if ( - ( this._state & ACTION.TRUCK ) === ACTION.TRUCK || - ( this._state & ACTION.TOUCH_TRUCK ) === ACTION.TOUCH_TRUCK || - ( this._state & ACTION.TOUCH_DOLLY_TRUCK ) === ACTION.TOUCH_DOLLY_TRUCK || - ( this._state & ACTION.TOUCH_ZOOM_TRUCK ) === ACTION.TOUCH_ZOOM_TRUCK - ) { + if ( ( event.buttons & MOUSE_BUTTON.MIDDLE ) === MOUSE_BUTTON.MIDDLE ) { - this._truckInternal( deltaX, deltaY, false ); + this._state = this._state | this.mouseButtons.middle; } - if ( - ( this._state & ACTION.OFFSET ) === ACTION.OFFSET || - ( this._state & ACTION.TOUCH_OFFSET ) === ACTION.TOUCH_OFFSET || - ( this._state & ACTION.TOUCH_DOLLY_OFFSET ) === ACTION.TOUCH_DOLLY_OFFSET || - ( this._state & ACTION.TOUCH_ZOOM_OFFSET ) === ACTION.TOUCH_ZOOM_OFFSET - ) { + if ( ( event.buttons & MOUSE_BUTTON.RIGHT ) === MOUSE_BUTTON.RIGHT ) { - this._truckInternal( deltaX, deltaY, true ); + this._state = this._state | this.mouseButtons.right; } - this.dispatchEvent( { type: 'control' } ); + } - }; + this.dispatchEvent( { type: 'controlstart' } ); - const endDragging = (): void => { + }; - extractClientCoordFromEvent( this._activePointers, _v2 ); - lastDragPosition.copy( _v2 ); + const dragging = (): void => { - if ( this._activePointers.length === 0 ) { + if ( ! this._enabled ) return; - // eslint-disable-next-line no-undef - this._domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove, { passive: false } as AddEventListenerOptions ); - this._domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); + extractClientCoordFromEvent( this._activePointers, _v2 ); - // eslint-disable-next-line no-undef - this._domElement.ownerDocument.removeEventListener( 'touchmove', onTouchMove, { passive: false } as AddEventListenerOptions ); - this._domElement.ownerDocument.removeEventListener( 'touchend', onTouchEnd ); + // When pointer lock is enabled clientX, clientY, screenX, and screenY remain 0. + // If pointer lock is enabled, use the Delta directory, and assume active-pointer is not multiple. + const isPointerLockActive = this._domElement && document.pointerLockElement === this._domElement; + const deltaX = isPointerLockActive ? - this._activePointers[ 0 ].deltaX : lastDragPosition.x - _v2.x; + const deltaY = isPointerLockActive ? - this._activePointers[ 0 ].deltaY : lastDragPosition.y - _v2.y; - this.dispatchEvent( { type: 'controlend' } ); + lastDragPosition.copy( _v2 ); - } + if ( + ( this._state & ACTION.ROTATE ) === ACTION.ROTATE || + ( this._state & ACTION.TOUCH_ROTATE ) === ACTION.TOUCH_ROTATE || + ( this._state & ACTION.TOUCH_DOLLY_ROTATE ) === ACTION.TOUCH_DOLLY_ROTATE || + ( this._state & ACTION.TOUCH_ZOOM_ROTATE ) === ACTION.TOUCH_ZOOM_ROTATE + ) { - }; + this._rotateInternal( deltaX, deltaY ); + + } + + if ( + ( this._state & ACTION.DOLLY ) === ACTION.DOLLY || + ( this._state & ACTION.ZOOM ) === ACTION.ZOOM + ) { + + const dollyX = this.dollyToCursor ? ( dragStartPosition.x - this._elementRect.x ) / this._elementRect.width * 2 - 1 : 0; + const dollyY = this.dollyToCursor ? ( dragStartPosition.y - this._elementRect.y ) / this._elementRect.height * - 2 + 1 : 0; + ( this._state & ACTION.DOLLY ) === ACTION.DOLLY ? + this._dollyInternal( deltaY * TOUCH_DOLLY_FACTOR, dollyX, dollyY ) : + this._zoomInternal( deltaY * TOUCH_DOLLY_FACTOR, dollyX, dollyY ); + + } + + if ( + ( this._state & ACTION.TOUCH_DOLLY ) === ACTION.TOUCH_DOLLY || + ( this._state & ACTION.TOUCH_ZOOM ) === ACTION.TOUCH_ZOOM || + ( this._state & ACTION.TOUCH_DOLLY_TRUCK ) === ACTION.TOUCH_DOLLY_TRUCK || + ( this._state & ACTION.TOUCH_ZOOM_TRUCK ) === ACTION.TOUCH_ZOOM_TRUCK || + ( this._state & ACTION.TOUCH_DOLLY_OFFSET ) === ACTION.TOUCH_DOLLY_OFFSET || + ( this._state & ACTION.TOUCH_ZOOM_OFFSET ) === ACTION.TOUCH_ZOOM_OFFSET || + ( this._state & ACTION.TOUCH_DOLLY_ROTATE ) === ACTION.TOUCH_DOLLY_ROTATE || + ( this._state & ACTION.TOUCH_ZOOM_ROTATE ) === ACTION.TOUCH_ZOOM_ROTATE + ) { + + const dx = _v2.x - this._activePointers[ 1 ].clientX; + const dy = _v2.y - this._activePointers[ 1 ].clientY; + const distance = Math.sqrt( dx * dx + dy * dy ); + const dollyDelta = dollyStart.y - distance; + dollyStart.set( 0, distance ); + + const dollyX = this.dollyToCursor ? ( lastDragPosition.x - this._elementRect.x ) / this._elementRect.width * 2 - 1 : 0; + const dollyY = this.dollyToCursor ? ( lastDragPosition.y - this._elementRect.y ) / this._elementRect.height * - 2 + 1 : 0; + + ( this._state & ACTION.TOUCH_DOLLY ) === ACTION.TOUCH_DOLLY || + ( this._state & ACTION.TOUCH_DOLLY_ROTATE ) === ACTION.TOUCH_DOLLY_ROTATE || + ( this._state & ACTION.TOUCH_DOLLY_TRUCK ) === ACTION.TOUCH_DOLLY_TRUCK || + ( this._state & ACTION.TOUCH_DOLLY_OFFSET ) === ACTION.TOUCH_DOLLY_OFFSET ? + this._dollyInternal( dollyDelta * TOUCH_DOLLY_FACTOR, dollyX, dollyY ) : + this._zoomInternal( dollyDelta * TOUCH_DOLLY_FACTOR, dollyX, dollyY ); + + } + + if ( + ( this._state & ACTION.TRUCK ) === ACTION.TRUCK || + ( this._state & ACTION.TOUCH_TRUCK ) === ACTION.TOUCH_TRUCK || + ( this._state & ACTION.TOUCH_DOLLY_TRUCK ) === ACTION.TOUCH_DOLLY_TRUCK || + ( this._state & ACTION.TOUCH_ZOOM_TRUCK ) === ACTION.TOUCH_ZOOM_TRUCK + ) { + + this._truckInternal( deltaX, deltaY, false ); + + } + + if ( + ( this._state & ACTION.OFFSET ) === ACTION.OFFSET || + ( this._state & ACTION.TOUCH_OFFSET ) === ACTION.TOUCH_OFFSET || + ( this._state & ACTION.TOUCH_DOLLY_OFFSET ) === ACTION.TOUCH_DOLLY_OFFSET || + ( this._state & ACTION.TOUCH_ZOOM_OFFSET ) === ACTION.TOUCH_ZOOM_OFFSET + ) { + + this._truckInternal( deltaX, deltaY, true ); + + } + + this.dispatchEvent( { type: 'control' } ); + + }; + + const endDragging = (): void => { + + extractClientCoordFromEvent( this._activePointers, _v2 ); + lastDragPosition.copy( _v2 ); + + if ( this._activePointers.length === 0 && this._domElement ) { + + // eslint-disable-next-line no-undef + this._domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove, { passive: false } as AddEventListenerOptions ); + this._domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); + + // eslint-disable-next-line no-undef + this._domElement.ownerDocument.removeEventListener( 'touchmove', onTouchMove, { passive: false } as AddEventListenerOptions ); + this._domElement.ownerDocument.removeEventListener( 'touchend', onTouchEnd ); + + this.dispatchEvent( { type: 'controlend' } ); + + } + + }; + + this._addAllEventListeners = ( domElement: HTMLElement ): void => { + + this._domElement = domElement; + + this._domElement.style.touchAction = 'none'; + this._domElement.style.userSelect = 'none'; + this._domElement.style.webkitUserSelect = 'none'; + if ( 'setAttribute' in this._domElement ) this._domElement.setAttribute( 'data-camera-controls-version', VERSION ); this._domElement.addEventListener( 'pointerdown', onPointerDown ); isPointerEventsNotSupported && this._domElement.addEventListener( 'mousedown', onMouseDown ); @@ -1061,38 +1062,48 @@ export class CameraControls extends EventDispatcher { this._domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); this._domElement.addEventListener( 'contextmenu', onContextMenu ); - this._removeAllEventListeners = (): void => { + }; - this._domElement.removeEventListener( 'pointerdown', onPointerDown ); - this._domElement.removeEventListener( 'mousedown', onMouseDown ); - this._domElement.removeEventListener( 'touchstart', onTouchStart ); - this._domElement.removeEventListener( 'pointercancel', onPointerUp ); - // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener#matching_event_listeners_for_removal - // > it's probably wise to use the same values used for the call to `addEventListener()` when calling `removeEventListener()` - // see https://github.com/microsoft/TypeScript/issues/32912#issuecomment-522142969 - // eslint-disable-next-line no-undef - this._domElement.removeEventListener( 'wheel', onMouseWheel, { passive: false } as AddEventListenerOptions ); - this._domElement.removeEventListener( 'contextmenu', onContextMenu ); - // eslint-disable-next-line no-undef - this._domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove, { passive: false } as AddEventListenerOptions ); - this._domElement.ownerDocument.removeEventListener( 'mousemove', onMouseMove ); - // eslint-disable-next-line no-undef - this._domElement.ownerDocument.removeEventListener( 'touchmove', onTouchMove, { passive: false } as AddEventListenerOptions ); - this._domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); - this._domElement.ownerDocument.removeEventListener( 'mouseup', onMouseUp ); - this._domElement.ownerDocument.removeEventListener( 'touchend', onTouchEnd ); + this._removeAllEventListeners = (): void => { + + if ( ! this._domElement ) return; + + this._domElement.removeEventListener( 'pointerdown', onPointerDown ); + this._domElement.removeEventListener( 'mousedown', onMouseDown ); + this._domElement.removeEventListener( 'touchstart', onTouchStart ); + this._domElement.removeEventListener( 'pointercancel', onPointerUp ); + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener#matching_event_listeners_for_removal + // > it's probably wise to use the same values used for the call to `addEventListener()` when calling `removeEventListener()` + // see https://github.com/microsoft/TypeScript/issues/32912#issuecomment-522142969 + // eslint-disable-next-line no-undef + this._domElement.removeEventListener( 'wheel', onMouseWheel, { passive: false } as AddEventListenerOptions ); + this._domElement.removeEventListener( 'contextmenu', onContextMenu ); + // eslint-disable-next-line no-undef + this._domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove, { passive: false } as AddEventListenerOptions ); + this._domElement.ownerDocument.removeEventListener( 'mousemove', onMouseMove ); + // eslint-disable-next-line no-undef + this._domElement.ownerDocument.removeEventListener( 'touchmove', onTouchMove, { passive: false } as AddEventListenerOptions ); + this._domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); + this._domElement.ownerDocument.removeEventListener( 'mouseup', onMouseUp ); + this._domElement.ownerDocument.removeEventListener( 'touchend', onTouchEnd ); + + if ( 'setAttribute' in this._domElement ) this._domElement.removeAttribute( 'data-camera-controls-version' ); - }; + }; - this.cancel = (): void => { + this.cancel = (): void => { - if ( this._state === ACTION.NONE ) return; + if ( this._state === ACTION.NONE ) return; - this._state = ACTION.NONE; - this._activePointers.length = 0; - endDragging(); + this._state = ACTION.NONE; + this._activePointers.length = 0; + endDragging(); - }; + }; + + if ( domElement ) { + + this._addAllEventListeners( domElement ); } @@ -1133,6 +1144,8 @@ export class CameraControls extends EventDispatcher { set enabled( enabled: boolean ) { + if ( ! this._domElement ) return; + this._enabled = enabled; if ( enabled ) { @@ -1928,6 +1941,7 @@ export class CameraControls extends EventDispatcher { this._sphericalEnd.phi = THREE.MathUtils.clamp( this.polarAngle, this.minPolarAngle, this.maxPolarAngle ); return promise; + } /** @@ -2467,6 +2481,16 @@ export class CameraControls extends EventDispatcher { } + /** + * Connect the cameraControls instance itself, add all eventListeners. + * @category Methods + */ + connect( domElement: HTMLElement ): void { + + this._addAllEventListeners( domElement ); + + } + /** * Dispose the cameraControls instance itself, remove all eventListeners. * @category Methods @@ -2474,7 +2498,6 @@ export class CameraControls extends EventDispatcher { dispose(): void { this._removeAllEventListeners(); - if ( 'setAttribute' in this._domElement ) this._domElement.removeAttribute( 'data-camera-controls-version' ); } @@ -2723,7 +2746,9 @@ export class CameraControls extends EventDispatcher { /** * Get its client rect and package into given `DOMRect` . */ - protected _getClientRect( target: DOMRect ): DOMRect { + protected _getClientRect( target: DOMRect ): DOMRect | undefined { + + if ( ! this._domElement ) return; const rect = this._domElement.getBoundingClientRect(); @@ -2770,6 +2795,9 @@ export class CameraControls extends EventDispatcher { } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected _addAllEventListeners( _domElement: HTMLElement ): void {} + protected _removeAllEventListeners(): void {} }