463 lines
8.6 KiB
JavaScript
463 lines
8.6 KiB
JavaScript
import { Raycaster, Vector2 } from 'three';
|
|
|
|
/* eslint-disable no-unused-vars */
|
|
const CLICK = { type: 'click' };
|
|
/* eslint-enable no-unused-vars */
|
|
const HOVER = { type: 'hover' };
|
|
|
|
export default class InteractiveRaycaster {
|
|
|
|
/**
|
|
*
|
|
* @param {THREE.Camera} camera
|
|
* @param {THREE.Scene} scene
|
|
* @param {THREE.WebGLRenderer} renderer
|
|
* @param {VRControl} vrControl
|
|
*/
|
|
constructor( camera, scene, renderer, vrControl = null ) {
|
|
|
|
this._camera = camera;
|
|
this._scene = scene;
|
|
this._renderer = renderer;
|
|
|
|
/**
|
|
*
|
|
* @type {VRControl}
|
|
* @private
|
|
*/
|
|
this._vrControl = vrControl;
|
|
|
|
/**
|
|
*
|
|
* @type {Raycaster}
|
|
* @private
|
|
*/
|
|
this._raycaster = new Raycaster();
|
|
|
|
/**
|
|
*
|
|
* @type {Array.<InteractiveListener>}
|
|
* @private
|
|
*/
|
|
this._listeners = [];
|
|
|
|
/**
|
|
*
|
|
* @type {Array.<MeshUIBaseElement>}
|
|
* @private
|
|
*/
|
|
this._objectsToTest = [];
|
|
|
|
/**
|
|
*
|
|
* @type {Window|Document|HTMLElement}
|
|
* @private
|
|
*/
|
|
this._target = null;
|
|
|
|
/**
|
|
*
|
|
* @type {Vector2|null}
|
|
* @private
|
|
*/
|
|
this._mouse = null;
|
|
|
|
/**
|
|
*
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this._selectState = false;
|
|
|
|
/**
|
|
*
|
|
* @type {null|MeshUIBaseElement}
|
|
* @private
|
|
*/
|
|
this._lastHoveredElement = null;
|
|
|
|
/**
|
|
*
|
|
* @type {null|MeshUIBaseElement}
|
|
* @private
|
|
*/
|
|
this._lastSelectedElement = null;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {InteractiveListener} interactiveListener
|
|
*/
|
|
addListener( interactiveListener ) {
|
|
|
|
if ( this._listeners.indexOf( interactiveListener ) === -1 ) {
|
|
|
|
this._listeners.push( interactiveListener );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {InteractiveListener} interactiveListener
|
|
*/
|
|
removeListener( interactiveListener ) {
|
|
|
|
const index = this._listeners.indexOf( interactiveListener );
|
|
if ( index !== -1 ) {
|
|
|
|
this._listeners.splice( index, 1 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Window|Document|HTMLElement} target
|
|
*/
|
|
start( target = null ) {
|
|
|
|
if ( this._target ) throw new Error( 'InteractiveRaycaster::start() - is already started. Aborted.' );
|
|
|
|
target = !target ? window : target;
|
|
|
|
this._target = target;
|
|
|
|
this._mouse = new Vector2();
|
|
this._mouse.x = this._mouse.y = null;
|
|
|
|
// compute mouse position in normalized device coordinates
|
|
// (-1 to +1) for both directions.
|
|
// Used to raycasting against the interactive elements
|
|
|
|
target.addEventListener( 'pointermove', this._pointerMove );
|
|
target.addEventListener( 'pointerdown', this._selectStart );
|
|
target.addEventListener( 'pointerup', this._selectEnd );
|
|
|
|
|
|
target.addEventListener( 'touchstart', this._touchMove );
|
|
|
|
// According to latest documentation, pointerEvent is now widely supported.
|
|
// and if we keep this line, on mobile the "select" operation is called twice
|
|
// as a pointerup event is also generated by mobile browsers
|
|
// target.addEventListener( 'touchend', this._touchEnd );
|
|
|
|
if ( this._vrControl ) {
|
|
|
|
this._vrControl.controllers[ 0 ].addEventListener( 'selectstart', this._selectStart );
|
|
this._vrControl.controllers[ 0 ].addEventListener( 'selectend', this._selectEnd );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_selectStart = () => {
|
|
|
|
this._selectState = true;
|
|
|
|
};
|
|
|
|
_selectEnd = () => {
|
|
|
|
this._click( );
|
|
|
|
this._selectState = false;
|
|
|
|
};
|
|
|
|
_pointerMove = ( event ) => {
|
|
|
|
this._mouse.x = ( event.clientX / this._target.innerWidth ) * 2 - 1;
|
|
this._mouse.y = -( event.clientY / this._target.innerHeight ) * 2 + 1;
|
|
|
|
};
|
|
|
|
_touchMove = ( event ) => {
|
|
|
|
this._selectStart();
|
|
|
|
this._mouse.x = ( event.touches[ 0 ].clientX / this._target.innerWidth ) * 2 - 1;
|
|
this._mouse.y = -( event.touches[ 0 ].clientY / this._target.innerHeight ) * 2 + 1;
|
|
|
|
};
|
|
|
|
// /* eslint-disable no-unused-vars */
|
|
// /**
|
|
// *
|
|
// * @param event
|
|
// * @private
|
|
// */
|
|
// _touchEnd = ( event ) => { /* eslint-enable no-unused-vars */
|
|
//
|
|
// this._selectEnd();
|
|
//
|
|
// this._mouse.x = null;
|
|
// this._mouse.y = null;
|
|
//
|
|
// };
|
|
|
|
_setHoveredElement( object ) {
|
|
|
|
if ( this._lastHoveredElement ) {
|
|
|
|
if ( this._lastHoveredElement.isUI ) {
|
|
|
|
this._lastHoveredElement.deactivatePseudoState( 'hover' );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this._lastHoveredElement = object;
|
|
|
|
if ( this._lastHoveredElement !== null ) {
|
|
|
|
if ( this._lastHoveredElement.isUI ) {
|
|
|
|
this._lastHoveredElement.activatePseudoState( 'hover' );
|
|
this._lastHoveredElement.dispatchEvent( HOVER );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// tell each listeners
|
|
for ( let i = 0; i < this._listeners.length; i++ ) {
|
|
|
|
this._listeners[ i ].hoveredElementHasChanged( this._lastHoveredElement );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_setSelectedElement( object ) {
|
|
|
|
if ( this._lastSelectedElement ) {
|
|
|
|
if ( this._lastSelectedElement.isUI ) {
|
|
|
|
this._lastSelectedElement.deactivatePseudoState( 'active' );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this._lastSelectedElement = object;
|
|
|
|
if ( this._lastSelectedElement !== null ) {
|
|
|
|
if ( this._lastSelectedElement.isUI ) {
|
|
|
|
this._lastSelectedElement.activatePseudoState( 'active' );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// tell each listeners
|
|
for ( let i = 0; i < this._listeners.length; i++ ) {
|
|
|
|
this._listeners[ i ].selectedElementHasChanged( this._lastSelectedElement );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* eslint-disable no-unused-vars */
|
|
/**
|
|
* @override
|
|
* @private
|
|
*/
|
|
_click() { /* eslint-enable no-unused-vars */
|
|
|
|
if ( this._lastSelectedElement ) {
|
|
|
|
// this._lastSelectedElement.dispatchEvent( CLICK );
|
|
this._lastSelectedElement._clicked( this._lastIntersectObject );
|
|
|
|
// tell each listeners
|
|
for ( let i = 0; i < this._listeners.length; i++ ) {
|
|
|
|
this._listeners[ i ].clicked( this._lastSelectedElement );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* eslint-disable no-unused-vars */
|
|
/**
|
|
*
|
|
* @param {Object3D|MeshUIBaseElement} object
|
|
*/
|
|
addObject( object ) { /* eslint-enable no-unused-vars */
|
|
|
|
for ( let i = 0; i < arguments.length; i++ ) {
|
|
|
|
const obj = arguments[i];
|
|
|
|
if( obj.interactiveListener ) {
|
|
|
|
this.addListener( obj.interactiveListener );
|
|
|
|
}
|
|
|
|
let objs;
|
|
if( obj.interactiveObjects ) {
|
|
objs = obj.interactiveObjects;
|
|
|
|
} else {
|
|
|
|
objs = [ obj ];
|
|
}
|
|
|
|
for ( let j = 0; j < objs.length; j++ ) {
|
|
|
|
const obj2 = objs[j]
|
|
if( !obj2._clicked ) {
|
|
obj2._clicked = this._elementClicked;
|
|
}
|
|
|
|
this._objectsToTest.push( obj2 );
|
|
}
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* eslint-disable no-unused-vars */
|
|
/**
|
|
*
|
|
* @param {Object3D|MeshUIBaseElement} object
|
|
*/
|
|
removeObject( object ) { /* eslint-enable no-unused-vars */
|
|
|
|
for ( let i = 0; i < arguments.length; i++ ) {
|
|
|
|
const obj = arguments[i];
|
|
|
|
if( obj.interactiveListener ) this.removeListener( obj.interactiveListener );
|
|
|
|
let objs = [obj];
|
|
if( obj.interactiveObjects ) objs = [...obj.interactiveObjects];
|
|
|
|
|
|
for ( let j = 0; j < objs.length; j++ ) {
|
|
|
|
const objElement = objs[ j ];
|
|
const index = this._objectsToTest.indexOf( objElement );
|
|
|
|
if( index !== -1 ) this._objectsToTest.splice( index, 1 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
update() {
|
|
|
|
// Find closest intersecting object
|
|
let intersect;
|
|
|
|
if ( this._renderer.xr.isPresenting && this._vrControl ) {
|
|
|
|
this._vrControl.setFromController( 0, this._raycaster.ray );
|
|
|
|
intersect = this._raycast();
|
|
|
|
// Position the little white dot at the end of the controller pointing ray
|
|
if ( intersect ) this._vrControl.setPointerAt( 0, intersect.point );
|
|
|
|
} else if ( this._mouse.x !== null && this._mouse.y !== null ) {
|
|
|
|
this._raycaster.setFromCamera( this._mouse, this._camera );
|
|
|
|
intersect = this._raycast();
|
|
|
|
}
|
|
|
|
if ( intersect ) {
|
|
|
|
const intersectObject = intersect.object;
|
|
this._lastIntersectObject = intersect;
|
|
|
|
if ( this._lastHoveredElement !== intersectObject ) {
|
|
this._setHoveredElement( intersectObject );
|
|
}
|
|
|
|
if ( this._selectState ) {
|
|
|
|
if ( this._lastSelectedElement !== intersectObject ) {
|
|
this._setSelectedElement( intersectObject );
|
|
}
|
|
|
|
} else if ( this._lastSelectedElement !== null ) {
|
|
|
|
this._setSelectedElement( null );
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ( this._lastSelectedElement !== null ) this._setSelectedElement( null );
|
|
if ( this._lastHoveredElement !== null ) this._setHoveredElement( null );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
set camera( camera ) {
|
|
this._camera = camera;
|
|
}
|
|
|
|
get camera() {
|
|
return this._camera;
|
|
}
|
|
|
|
|
|
_raycast() {
|
|
|
|
return this._objectsToTest.reduce( ( closestIntersection, obj ) => {
|
|
|
|
if( !this._scene.getObjectById( obj.id ) ) return closestIntersection;
|
|
|
|
const intersection = this._raycaster.intersectObject( obj, true );
|
|
|
|
if ( !intersection[ 0 ] ) return closestIntersection;
|
|
|
|
if ( !closestIntersection || intersection[ 0 ].distance < closestIntersection.distance ) {
|
|
|
|
intersection[ 0 ].object = obj;
|
|
|
|
return intersection[ 0 ];
|
|
|
|
}
|
|
|
|
return closestIntersection;
|
|
|
|
|
|
}, null );
|
|
|
|
|
|
}
|
|
|
|
_elementClicked( intersect ) {
|
|
|
|
delete intersect.object;
|
|
|
|
intersect.type = 'click';
|
|
this.dispatchEvent( intersect );
|
|
|
|
}
|
|
|
|
|
|
}
|