306 lines
12 KiB
JavaScript
306 lines
12 KiB
JavaScript
import {Raycaster} from "./Raycaster.js"
|
|
export const POINTER_ENTER = "enter"
|
|
export const POINTER_EXIT = "exit"
|
|
export const POINTER_CLICK = "click"
|
|
export const POINTER_MOVE = "move"
|
|
export const POINTER_PRESS = "press"
|
|
export const POINTER_RELEASE = "release"
|
|
|
|
// import * as THREE from "./node_modules/three/build/three.module.js"
|
|
|
|
const toRad = (degrees) => degrees*Math.PI/180
|
|
|
|
export class Pointer {
|
|
constructor(scene, renderer, camera, opts) {
|
|
this.listeners = {}
|
|
this.opts = opts || {}
|
|
this.opts.enableLaser = (opts.enableLaser !== undefined) ? opts.enableLaser : true
|
|
this.opts.laserLength = (opts.laserLength !== undefined) ? opts.laserLength : 3
|
|
this.opts.enableMoveEvents = (opts.enableMoveEvents !== undefined) ? opts.enableMoveEvents : true
|
|
this.scene = scene
|
|
this.renderer = renderer
|
|
this.canvas = renderer.domElement
|
|
this.camera = camera
|
|
|
|
this.raycaster = new Raycaster()
|
|
this.waitcb = null
|
|
this.hoverTarget = null
|
|
|
|
this.intersectionFilter = this.opts.intersectionFilter || (() => true)
|
|
this.raycaster.recurseFilter = this.opts.recurseFilter || (()=> true)
|
|
|
|
|
|
// setup the mouse
|
|
this.canvas.addEventListener('mousemove', this.mouseMove.bind(this))
|
|
this.canvas.addEventListener('click', this.mouseClick.bind(this))
|
|
this.canvas.addEventListener('mousedown',this.mouseDown.bind(this))
|
|
this.canvas.addEventListener('mouseup',this.mouseUp.bind(this))
|
|
|
|
//touch events
|
|
this.canvas.addEventListener('touchstart',this.touchStart.bind(this))
|
|
this.canvas.addEventListener('touchmove',this.touchMove.bind(this))
|
|
this.canvas.addEventListener('touchend',this.touchEnd.bind(this))
|
|
|
|
// setup the VR controllers
|
|
this.controller1 = this.renderer.vr.getController(0);
|
|
this.controller1.addEventListener('selectstart', this.controllerSelectStart.bind(this));
|
|
this.controller1.addEventListener('selectend', this.controllerSelectEnd.bind(this));
|
|
|
|
|
|
this.controller2 = this.renderer.vr.getController(1);
|
|
this.controller2.addEventListener('selectstart', this.controllerSelectStart.bind(this));
|
|
this.controller2.addEventListener('selectend', this.controllerSelectEnd.bind(this));
|
|
|
|
this.setMouseSimulatesController(opts.mouseSimulatesController)
|
|
|
|
this.scene.add(this.controller1);
|
|
this.scene.add(this.controller2);
|
|
|
|
|
|
|
|
if(this.opts.enableLaser) {
|
|
//create visible lines for the two controllers
|
|
const geometry = new THREE.BufferGeometry()
|
|
geometry.addAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, -this.opts.laserLength], 3));
|
|
geometry.addAttribute('color', new THREE.Float32BufferAttribute([1.0, 0.5, 0.5, 0, 0, 0], 3));
|
|
|
|
const material = new THREE.LineBasicMaterial({
|
|
vertexColors: false,
|
|
color: 0x880000,
|
|
linewidth: 5,
|
|
blending: THREE.NormalBlending
|
|
})
|
|
|
|
this.controller1.add(new THREE.Line(geometry, material));
|
|
this.controller2.add(new THREE.Line(geometry, material));
|
|
}
|
|
|
|
}
|
|
|
|
//override this to do something w/ the controllers on every tick
|
|
tick(time) {
|
|
this.controllerMove(this.controller1)
|
|
this.controllerMove(this.controller2)
|
|
}
|
|
|
|
|
|
fire(obj, type, payload) {
|
|
obj.dispatchEvent(payload)
|
|
}
|
|
fireSelf(type,payload) {
|
|
if(!this.listeners[type]) return
|
|
this.listeners[type].forEach(cb => cb(payload))
|
|
}
|
|
|
|
//make the camera follow the mouse in desktop mode. Helps w/ debugging.
|
|
cameraFollowMouse(e) {
|
|
const bounds = this.canvas.getBoundingClientRect()
|
|
const ry = ((e.clientX - bounds.left) / bounds.width) * 2 - 1
|
|
const rx = 1 - ((e.clientY - bounds.top) / bounds.height) * 2
|
|
this.camera.rotation.y = -ry*2
|
|
this.camera.rotation.x = +rx
|
|
}
|
|
|
|
mouseMove(e) {
|
|
const mouse = new THREE.Vector2()
|
|
const bounds = this.canvas.getBoundingClientRect()
|
|
mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1
|
|
mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1
|
|
this.raycaster.setFromCamera(mouse, this.camera)
|
|
if(this.opts.mouseSimulatesController) {
|
|
//create target from the mouse controls
|
|
const target = new THREE.Vector3()
|
|
target.x = mouse.x
|
|
target.y = mouse.y
|
|
target.z = -3
|
|
//convert to camera space
|
|
target.add(this.camera.position)
|
|
this.spot.position.copy(target)
|
|
this.controller1.lookAt(target)
|
|
//have to flip over because the UP is down on controllers
|
|
const flip = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0),toRad(180))
|
|
this.controller1.quaternion.multiply(flip)
|
|
}
|
|
this._processMove()
|
|
|
|
if(this.opts.cameraFollowMouse) this.cameraFollowMouse(e)
|
|
}
|
|
|
|
touchStart(e) {
|
|
e.preventDefault()
|
|
if(e.changedTouches.length <= 0) return
|
|
const tch = e.changedTouches[0]
|
|
const mouse = new THREE.Vector2()
|
|
const bounds = this.canvas.getBoundingClientRect()
|
|
mouse.x = ((tch.clientX - bounds.left) / bounds.width) * 2 - 1
|
|
mouse.y = -((tch.clientY - bounds.top) / bounds.height) * 2 + 1
|
|
this.raycaster.setFromCamera(mouse, this.camera)
|
|
const intersects = this.raycaster.intersectObjects(this.scene.children, true)
|
|
.filter(it => this.intersectionFilter(it.object))
|
|
intersects.forEach((it) => {
|
|
this.fire(it.object, POINTER_PRESS, {type: POINTER_PRESS})
|
|
})
|
|
}
|
|
touchMove(e) {
|
|
e.preventDefault()
|
|
if(e.changedTouches.length <= 0) return
|
|
const tch = e.changedTouches[0]
|
|
const mouse = new THREE.Vector2()
|
|
const bounds = this.canvas.getBoundingClientRect()
|
|
mouse.x = ((tch.clientX - bounds.left) / bounds.width) * 2 - 1
|
|
mouse.y = -((tch.clientY - bounds.top) / bounds.height) * 2 + 1
|
|
this.raycaster.setFromCamera(mouse, this.camera)
|
|
this._processMove()
|
|
}
|
|
touchEnd(e) {
|
|
e.preventDefault()
|
|
if(e.changedTouches.length <= 0) return
|
|
const tch = e.changedTouches[0]
|
|
const mouse = new THREE.Vector2()
|
|
const bounds = this.canvas.getBoundingClientRect()
|
|
mouse.x = ((tch.clientX - bounds.left) / bounds.width) * 2 - 1
|
|
mouse.y = -((tch.clientY - bounds.top) / bounds.height) * 2 + 1
|
|
this.raycaster.setFromCamera(mouse, this.camera)
|
|
const intersects = this.raycaster.intersectObjects(this.scene.children, true)
|
|
.filter(it => this.intersectionFilter(it.object))
|
|
intersects.forEach((it) => {
|
|
this.fire(it.object, POINTER_RELEASE, {type: POINTER_RELEASE, point: it.point})
|
|
})
|
|
this._processClick()
|
|
}
|
|
|
|
controllerMove(controller) {
|
|
if(!controller.visible) return
|
|
const c = controller
|
|
const dir = new THREE.Vector3(0, 0, -1)
|
|
dir.applyQuaternion(c.quaternion)
|
|
this.raycaster.set(c.position, dir)
|
|
this._processMove()
|
|
}
|
|
|
|
_processMove() {
|
|
if(!this.opts.enableMoveEvents)return
|
|
const intersects = this.raycaster.intersectObjects(this.scene.children, true)
|
|
.filter(it => this.intersectionFilter(it.object))
|
|
|
|
if(intersects.length === 0 && this.hoverTarget) {
|
|
this.fire(this.hoverTarget, POINTER_EXIT, {type: POINTER_EXIT})
|
|
this.hoverTarget = null
|
|
}
|
|
if(intersects.length >= 1) {
|
|
const it = intersects[0]
|
|
const obj = it.object
|
|
if (!obj) return
|
|
this.fire(obj, POINTER_MOVE, {type: POINTER_MOVE, point: it.point, intersection:it})
|
|
if (obj === this.hoverTarget) {
|
|
//still inside
|
|
} else {
|
|
if (this.hoverTarget)
|
|
this.fire(this.hoverTarget, POINTER_EXIT, {type: POINTER_EXIT})
|
|
this.hoverTarget = obj
|
|
this.fire(this.hoverTarget, POINTER_ENTER, {type: POINTER_ENTER})
|
|
}
|
|
}
|
|
}
|
|
|
|
_processClick() {
|
|
if (this.waitcb) {
|
|
this.waitcb()
|
|
this.waitcb = null
|
|
return
|
|
}
|
|
|
|
const intersects = this.raycaster.intersectObjects(this.scene.children, true)
|
|
.filter(it => this.intersectionFilter(it.object))
|
|
if(intersects.length > 0) {
|
|
const it = intersects[0]
|
|
this.fire(it.object, POINTER_CLICK, {type: POINTER_CLICK, point: it.point, intersection:it})
|
|
}
|
|
|
|
this.fireSelf(POINTER_CLICK, {})
|
|
}
|
|
mouseClick(e) {
|
|
const mouse = new THREE.Vector2()
|
|
const bounds = this.canvas.getBoundingClientRect()
|
|
mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1
|
|
mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1
|
|
this.raycaster.setFromCamera(mouse, this.camera)
|
|
this._processClick()
|
|
}
|
|
mouseDown(e) {
|
|
const mouse = new THREE.Vector2()
|
|
const bounds = this.canvas.getBoundingClientRect()
|
|
mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1
|
|
mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1
|
|
this.raycaster.setFromCamera(mouse, this.camera)
|
|
const intersects = this.raycaster.intersectObjects(this.scene.children, true)
|
|
.filter(it => this.intersectionFilter(it.object))
|
|
intersects.forEach((it) => {
|
|
this.fire(it.object, POINTER_PRESS, {type: POINTER_PRESS, point: it.point, intersection:it})
|
|
})
|
|
}
|
|
mouseUp(e) {
|
|
const mouse = new THREE.Vector2()
|
|
const bounds = this.canvas.getBoundingClientRect()
|
|
mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1
|
|
mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1
|
|
this.raycaster.setFromCamera(mouse, this.camera)
|
|
const intersects = this.raycaster.intersectObjects(this.scene.children, true)
|
|
.filter(it => this.intersectionFilter(it.object))
|
|
intersects.forEach((it) => {
|
|
this.fire(it.object, POINTER_RELEASE, {type: POINTER_RELEASE, point: it.point, intersection:it})
|
|
})
|
|
}
|
|
|
|
controllerSelectStart(e) {
|
|
e.target.userData.isSelecting = true;
|
|
const intersects = this.raycaster.intersectObjects(this.scene.children, true)
|
|
.filter(it => this.intersectionFilter(it.object))
|
|
intersects.forEach((it) => {
|
|
this.fire(it.object, POINTER_PRESS, {type: POINTER_PRESS, point: it.point, intersection:it})
|
|
})
|
|
}
|
|
|
|
controllerSelectEnd(e) {
|
|
e.target.userData.isSelecting = false;
|
|
const c = e.target
|
|
const dir = new THREE.Vector3(0, 0, -1)
|
|
dir.applyQuaternion(c.quaternion)
|
|
this.raycaster.set(c.position, dir)
|
|
const intersects = this.raycaster.intersectObjects(this.scene.children, true)
|
|
.filter(it => this.intersectionFilter(it.object))
|
|
intersects.forEach((it) => {
|
|
this.fire(it.object, POINTER_RELEASE, {type: POINTER_RELEASE, point: it.point})
|
|
})
|
|
this._processClick()
|
|
}
|
|
|
|
waitSceneClick(cb) {
|
|
this.waitcb = cb
|
|
}
|
|
|
|
|
|
on(type,cb) {
|
|
if(!this.listeners[type]) this.listeners[type] = []
|
|
this.listeners[type].push(cb)
|
|
}
|
|
off(type,cb) {
|
|
this.listeners[type] = this.listeners[type].filter(c => c !== cb)
|
|
}
|
|
setMouseSimulatesController(val) {
|
|
this.opts.mouseSimulatesController = val
|
|
if(this.opts.mouseSimulatesController) {
|
|
this.controller1 = new THREE.Group()
|
|
this.controller1.position.set(0,1,-2)
|
|
this.controller1.quaternion.setFromUnitVectors(THREE.Object3D.DefaultUp, new THREE.Vector3(0,0,1))
|
|
this.spot = new THREE.Mesh(
|
|
new THREE.SphereBufferGeometry(0.1),
|
|
new THREE.MeshLambertMaterial({color: 'red'})
|
|
)
|
|
this.scene.add(this.spot)
|
|
} else {
|
|
}
|
|
|
|
}
|
|
}
|