Created
October 7, 2017 15:18
-
-
Save leon/e68aa0d2bcfab20ad5964dbb1c74684b to your computer and use it in GitHub Desktop.
Aframe Map Controls
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* global AFRAME THREE */ | |
if (typeof AFRAME === 'undefined') { | |
throw new Error('Component attempted to register before AFRAME was available.'); | |
} | |
var radToDeg = THREE.Math.radToDeg; | |
/** | |
* Example component for A-Frame. | |
*/ | |
AFRAME.registerComponent('map-controls', { | |
dependencies: ['position', 'rotation'], | |
schema: { | |
enabled: { | |
default: true | |
}, | |
target: { | |
default: '' | |
}, | |
ground: { | |
default: '' | |
}, | |
distance: { | |
default: 1 | |
}, | |
enableRotate: { | |
default: true | |
}, | |
rotateSpeed: { | |
default: 1.0 | |
}, | |
enableZoom: { | |
default: true | |
}, | |
zoomSpeed: { | |
default: 1.0 | |
}, | |
enablePan: { | |
default: true | |
}, | |
keyPanSpeed: { | |
default: 7.0 | |
}, | |
enableDamping: { | |
default: false | |
}, | |
dampingFactor: { | |
default: 0.25 | |
}, | |
autoRotate: { | |
default: false | |
}, | |
autoRotateSpeed: { | |
default: 2.0 | |
}, | |
enableKeys: { | |
default: true | |
}, | |
minAzimuthAngle: { | |
default: -Infinity | |
}, | |
maxAzimuthAngle: { | |
default: Infinity | |
}, | |
minPolarAngle: { | |
default: 0 | |
}, | |
maxPolarAngle: { | |
default: Math.PI | |
}, | |
minZoom: { | |
default: 0 | |
}, | |
maxZoom: { | |
default: Infinity | |
}, | |
invertZoom: { | |
default: false | |
}, | |
minDistance: { | |
default: 0 | |
}, | |
maxDistance: { | |
default: Infinity | |
}, | |
rotateTo: { | |
type: 'vec3', | |
default: {x: 0, y: 0, z: 0} | |
}, | |
rotateToSpeed: { | |
type: 'number', | |
default: 0.05 | |
}, | |
logPosition: { | |
type: 'boolean', | |
default: false | |
}, | |
autoVRLookCam: { | |
type: 'boolean', | |
default: true | |
} | |
}, | |
/** | |
* Set if component needs multiple instancing. | |
*/ | |
multiple: false, | |
/** | |
* Called once when component is attached. Generally for initial setup. | |
*/ | |
init: function () { | |
this.sceneEl = this.el.sceneEl; | |
this.object = this.el.object3D; | |
this.target = this.sceneEl.querySelector(this.data.target).object3D.position; | |
this.ground = this.data.ground === 'scene' ? this.sceneEl.object3D : this.sceneEl.querySelector(this.data.ground).object3D; | |
// Find the look-controls component on this camera, or create if it doesn't exist. | |
this.lookControls = null; | |
if (this.data.autoVRLookCam) { | |
if (this.el.components['look-controls']) { | |
this.lookControls = this.el.components['look-controls']; | |
} else { | |
this.el.setAttribute('look-controls', ''); | |
this.lookControls = this.el.components['look-controls']; | |
} | |
this.lookControls.pause(); | |
this.sceneEl.addEventListener('enter-vr', this.onEnterVR.bind(this), false); | |
this.sceneEl.addEventListener('exit-vr', this.onExitVR.bind(this), false); | |
} | |
this.dolly = new THREE.Object3D(); | |
this.dolly.position.copy(this.object.position); | |
this.savedPose = null; | |
this.STATE = { | |
NONE: -1, | |
ROTATE: 0, | |
DOLLY: 1, | |
PAN: 2, | |
TOUCH_ROTATE: 3, | |
TOUCH_DOLLY: 4, | |
TOUCH_PAN: 5, | |
ROTATE_TO: 6 | |
}; | |
this.state = this.STATE.NONE; | |
this.EPS = 0.000001; | |
this.lastPosition = new THREE.Vector3(); | |
this.lastQuaternion = new THREE.Quaternion(); | |
this.spherical = new THREE.Spherical(); | |
this.sphericalDelta = new THREE.Spherical(); | |
this.scale = 1.0; | |
this.zoomChanged = false; | |
this.rotateStart = new THREE.Vector2(); | |
this.rotateEnd = new THREE.Vector2(); | |
this.rotateDelta = new THREE.Vector2(); | |
this.panStart = new THREE.Vector3(); | |
this.panEnd = new THREE.Vector3(); | |
this.panDelta = new THREE.Vector3(); | |
this.panOffset = new THREE.Vector3(); | |
this.dollyStart = new THREE.Vector2(); | |
this.dollyEnd = new THREE.Vector2(); | |
this.dollyDelta = new THREE.Vector2(); | |
this.desiredPosition = new THREE.Vector3(); | |
this.mouse = new THREE.Vector2(); | |
this.raycaster = new THREE.Raycaster(); | |
this.mouseButtons = { | |
PAN: THREE.MOUSE.LEFT, | |
ORBIT: THREE.MOUSE.RIGHT, | |
ZOOM: THREE.MOUSE.MIDDLE, | |
}; | |
this.keys = { | |
LEFT: 37, | |
UP: 38, | |
RIGHT: 39, | |
BOTTOM: 40 | |
}; | |
this.bindMethods(); | |
}, | |
/** | |
* Called when component is attached and when component data changes. | |
* Generally modifies the entity based on the data. | |
*/ | |
update: function (oldData) { | |
console.log('component update'); | |
if (this.data.rotateTo) { | |
var rotateToVec3 = new THREE.Vector3(this.data.rotateTo.x, this.data.rotateTo.y, this.data.rotateTo.z); | |
// Check if rotateToVec3 is already desiredPosition | |
if (!this.desiredPosition.equals(rotateToVec3)) { | |
this.desiredPosition.copy(rotateToVec3); | |
this.rotateTo(this.desiredPosition); | |
} | |
} | |
this.dolly.position.copy(this.object.position); | |
this.updateView(true); | |
}, | |
/** | |
* Called when a component is removed (e.g., via removeAttribute). | |
* Generally undoes all modifications to the entity. | |
*/ | |
remove: function () { | |
// console.log("component remove"); | |
this.removeEventListeners(); | |
this.sceneEl.removeEventListener('enter-vr', this.onEnterVR, false); | |
this.sceneEl.removeEventListener('exit-vr', this.onExitVR, false); | |
}, | |
/** | |
* Called on each scene tick. | |
*/ | |
tick: function (time, delta) { | |
var render = this.data.enabled ? this.updateView(time, delta) : false; | |
if (render === true && this.data.logPosition === true) { | |
console.log(this.el.object3D.position); | |
} | |
}, | |
/* | |
* Called when entering VR mode | |
*/ | |
onEnterVR: function (event) { | |
// console.log('enter vr', this); | |
this.saveCameraPose(); | |
this.el.setAttribute('position', {x: 0, y: 2, z: 5}); | |
this.el.setAttribute('rotation', {x: 0, y: 0, z: 0}); | |
this.pause(); | |
this.lookControls.play(); | |
if (this.data.autoRotate) console.warn('map-controls: Sorry, autoRotate is not implemented in VR mode'); | |
}, | |
/* | |
* Called when exiting VR mode | |
*/ | |
onExitVR: function (event) { | |
// console.log('exit vr'); | |
this.lookControls.pause(); | |
this.play(); | |
this.restoreCameraPose(); | |
this.updateView(true); | |
}, | |
/** | |
* Called when entity pauses. | |
* Use to stop or remove any dynamic or background behavior such as events. | |
*/ | |
pause: function () { | |
console.log("component pause"); | |
this.data.enabled = false; | |
this.removeEventListeners(); | |
}, | |
/** | |
* Called when entity resumes. | |
* Use to continue or add any dynamic or background behavior such as events. | |
*/ | |
play: function () { | |
console.log("component play"); | |
this.data.enabled = true; | |
var camera, cameraType; | |
this.object.traverse(function (child) { | |
if (child instanceof THREE.PerspectiveCamera) { | |
camera = child; | |
cameraType = 'PerspectiveCamera'; | |
} else if (child instanceof THREE.OrthographicCamera) { | |
camera = child; | |
cameraType = 'OrthographicCamera'; | |
} | |
}); | |
this.camera = camera; | |
this.cameraType = cameraType; | |
this.sceneEl.addEventListener('renderstart', this.onRenderStart, false); | |
console.log('this.sceneEl', this.sceneEl); | |
if (this.lookControls) this.lookControls.pause(); | |
if (this.canvasEl) this.addEventListeners(); | |
}, | |
/* | |
* Called when Render Target is completely loaded | |
* Then set canvasEl and add event listeners | |
*/ | |
onRenderStart: function () { | |
console.log('render target loaded'); | |
this.sceneEl.removeEventListener('renderstart', this.onRenderStart, false); | |
this.canvasEl = this.sceneEl.canvas; | |
this.addEventListeners(); | |
}, | |
/* | |
* Bind this to all event handlera | |
*/ | |
bindMethods: function () { | |
this.onRenderStart = this.onRenderStart.bind(this); | |
this.onContextMenu = this.onContextMenu.bind(this); | |
this.onMouseDown = this.onMouseDown.bind(this); | |
this.onMouseWheel = this.onMouseWheel.bind(this); | |
this.onMouseMove = this.onMouseMove.bind(this); | |
this.onMouseUp = this.onMouseUp.bind(this); | |
this.onTouchStart = this.onTouchStart.bind(this); | |
this.onTouchMove = this.onTouchMove.bind(this); | |
this.onTouchEnd = this.onTouchEnd.bind(this); | |
this.onKeyDown = this.onKeyDown.bind(this); | |
}, | |
/* | |
* Add event listeners | |
*/ | |
addEventListeners: function () { | |
this.canvasEl.addEventListener('contextmenu', this.onContextMenu, false); | |
this.canvasEl.addEventListener('mousedown', this.onMouseDown, false); | |
this.canvasEl.addEventListener('mousewheel', this.onMouseWheel, false); | |
this.canvasEl.addEventListener('MozMousePixelScroll', this.onMouseWheel, false); // firefox | |
this.canvasEl.addEventListener('touchstart', this.onTouchStart, false); | |
this.canvasEl.addEventListener('touchend', this.onTouchEnd, false); | |
this.canvasEl.addEventListener('touchmove', this.onTouchMove, false); | |
window.addEventListener('keydown', this.onKeyDown, false); | |
}, | |
/* | |
* Remove event listeners | |
*/ | |
removeEventListeners: function () { | |
if (this.canvasEl) { | |
this.canvasEl.removeEventListener('contextmenu', this.onContextMenu, false); | |
this.canvasEl.removeEventListener('mousedown', this.onMouseDown, false); | |
this.canvasEl.removeEventListener('mousewheel', this.onMouseWheel, false); | |
this.canvasEl.removeEventListener('MozMousePixelScroll', this.onMouseWheel, false); // firefox | |
this.canvasEl.removeEventListener('touchstart', this.onTouchStart, false); | |
this.canvasEl.removeEventListener('touchend', this.onTouchEnd, false); | |
this.canvasEl.removeEventListener('touchmove', this.onTouchMove, false); | |
this.canvasEl.removeEventListener('mousemove', this.onMouseMove, false); | |
this.canvasEl.removeEventListener('mouseup', this.onMouseUp, false); | |
this.canvasEl.removeEventListener('mouseout', this.onMouseUp, false); | |
} | |
window.removeEventListener('keydown', this.onKeyDown, false); | |
}, | |
/* | |
* EVENT LISTENERS | |
*/ | |
/* | |
* Called when right clicking the A-Frame component | |
*/ | |
onContextMenu: function (event) { | |
event.preventDefault(); | |
}, | |
/* | |
* MOUSE CLICK EVENT LISTENERS | |
*/ | |
onMouseDown: function (event) { | |
console.log('onMouseDown'); | |
if (this.data.enabled === false) return; | |
if (event.button === this.mouseButtons.PAN && (event.shiftKey || event.ctrlKey)) { | |
if (this.data.enableRotate === false) return; | |
this.handleMouseDownRotate(event); | |
this.state = this.STATE.ROTATE; | |
} else if (event.button === this.mouseButtons.ORBIT) { | |
this.panOffset.set(0, 0, 0); | |
if (this.data.enableRotate === false) return; | |
this.handleMouseDownRotate(event); | |
this.state = this.STATE.ROTATE; | |
} else if (event.button === this.mouseButtons.ZOOM) { | |
this.panOffset.set(0, 0, 0); | |
if (this.data.enableZoom === false) return; | |
this.handleMouseDownDolly(event); | |
this.state = this.STATE.DOLLY; | |
} else if (event.button === this.mouseButtons.PAN) { | |
if (this.data.enablePan === false) return; | |
this.handleMouseDownPan(event); | |
this.state = this.STATE.PAN; | |
} | |
if (this.state !== this.STATE.NONE) { | |
this.canvasEl.addEventListener('mousemove', this.onMouseMove, false); | |
this.canvasEl.addEventListener('mouseup', this.onMouseUp, false); | |
this.canvasEl.addEventListener('mouseout', this.onMouseUp, false); | |
this.el.emit('start-drag-map-controls', null, false); | |
} | |
}, | |
onMouseMove: function (event) { | |
// console.log('onMouseMove'); | |
if (this.data.enabled === false) return; | |
event.preventDefault(); | |
if (this.state === this.STATE.ROTATE) { | |
if (this.data.enableRotate === false) return; | |
this.handleMouseMoveRotate(event); | |
} else if (this.state === this.STATE.DOLLY) { | |
if (this.data.enableZoom === false) return; | |
this.handleMouseMoveDolly(event); | |
} else if (this.state === this.STATE.PAN) { | |
if (this.data.enablePan === false) return; | |
this.handleMouseMovePan(event); | |
} | |
}, | |
onMouseUp: function (event) { | |
// console.log('onMouseUp'); | |
if (this.data.enabled === false) return; | |
if (this.state === this.STATE.ROTATE_TO) return; | |
event.preventDefault(); | |
event.stopPropagation(); | |
this.handleMouseUp(event); | |
this.canvasEl.removeEventListener('mousemove', this.onMouseMove, false); | |
this.canvasEl.removeEventListener('mouseup', this.onMouseUp, false); | |
this.canvasEl.removeEventListener('mouseout', this.onMouseUp, false); | |
this.state = this.STATE.NONE; | |
this.el.emit('end-drag-map-controls', null, false); | |
}, | |
/* | |
* MOUSE WHEEL EVENT LISTENERS | |
*/ | |
onMouseWheel: function (event) { | |
// console.log('onMouseWheel'); | |
if (this.data.enabled === false || this.data.enableZoom === false || (this.state !== this.STATE.NONE && this.state !== this.STATE.ROTATE)) return; | |
event.preventDefault(); | |
event.stopPropagation(); | |
this.handleMouseWheel(event); | |
}, | |
/* | |
* TOUCH EVENT LISTENERS | |
*/ | |
onTouchStart: function (event) { | |
// console.log('onTouchStart'); | |
if (this.data.enabled === false) return; | |
// number of fingers on screen | |
switch (event.touches.length) { | |
case 1: | |
// one-fingered touch: pan | |
if (this.data.enablePan === false) return; | |
this.handleTouchStartPan(event); | |
this.state = this.STATE.TOUCH_PAN; | |
break; | |
case 2: | |
// two-fingered touch: orbit | |
if (this.data.enableRotate === false) return; | |
this.handleTouchStartRotate(event); | |
this.state = this.STATE.TOUCH_ROTATE; | |
break; | |
case 3: | |
// three-fingered touch: zoom | |
if (this.data.enableZoom === false) return; | |
this.handleTouchStartDolly(event); | |
this.state = this.STATE.TOUCH_DOLLY; | |
break; | |
default: | |
this.state = this.STATE.NONE; | |
} | |
if (this.state !== this.STATE.NONE) { | |
this.el.emit('start-drag-map-controls', null, false); | |
} | |
}, | |
onTouchMove: function (event) { | |
// console.log('onTouchMove'); | |
if (this.data.enabled === false) return; | |
event.preventDefault(); | |
event.stopPropagation(); | |
switch (event.touches.length) { | |
case 1: // one-fingered touch: rotate | |
if (this.enableRotate === false) return; | |
if (this.state !== this.STATE.TOUCH_ROTATE) return; // is this needed?... | |
this.handleTouchMoveRotate(event); | |
break; | |
case 2: // two-fingered touch: dolly | |
if (this.data.enableZoom === false) return; | |
if (this.state !== this.STATE.TOUCH_DOLLY) return; // is this needed?... | |
this.handleTouchMoveDolly(event); | |
break; | |
case 3: // three-fingered touch: pan | |
if (this.data.enablePan === false) return; | |
if (this.state !== this.STATE.TOUCH_PAN) return; // is this needed?... | |
this.handleTouchMovePan(event); | |
break; | |
default: | |
this.state = this.STATE.NONE; | |
} | |
}, | |
onTouchEnd: function (event) { | |
// console.log('onTouchEnd'); | |
if (this.data.enabled === false) return; | |
this.handleTouchEnd(event); | |
this.el.emit('end-drag-map-controls', null, false); | |
this.state = this.STATE.NONE; | |
}, | |
/* | |
* KEYBOARD EVENT LISTENERS | |
*/ | |
onKeyDown: function (event) { | |
// console.log('onKeyDown'); | |
if (this.data.enabled === false || this.data.enableKeys === false || this.data.enablePan === false) return; | |
this.handleKeyDown(event); | |
}, | |
/* | |
* EVENT HANDLERS | |
*/ | |
/* | |
* MOUSE CLICK EVENT HANDLERS | |
*/ | |
handleMouseDownRotate: function (event) { | |
// console.log( 'handleMouseDownRotate' ); | |
this.rotateStart.set(event.clientX, event.clientY); | |
}, | |
handleMouseDownDolly: function (event) { | |
console.log('handleMouseDownDolly'); | |
this.dollyStart.set(event.clientX, event.clientY); | |
}, | |
handleMouseDownPan: function (event) { | |
if (this.ground) { | |
// Pan along mesh | |
// console.log('handleMouseDownPan'); | |
this.panStart = this.getRaycastPosition(event); | |
} else { | |
// Pan along mouse plane | |
this.panStart.set(event.clientX, event.clientY); | |
} | |
}, | |
handleMouseMoveRotate: function (event) { | |
console.log('handleMouseMoveRotate'); | |
this.rotateEnd.set(event.clientX, event.clientY); | |
this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart); | |
var canvas = this.canvasEl === document ? this.canvasEl.body : this.canvasEl; | |
// rotating across whole screen goes 360 degrees around | |
this.rotateLeft(2 * Math.PI * this.rotateDelta.x / canvas.clientWidth * this.data.rotateSpeed); | |
// rotating up and down along whole screen attempts to go 360, but limited to 180 | |
this.rotateUp(2 * Math.PI * this.rotateDelta.y / canvas.clientHeight * this.data.rotateSpeed); | |
this.rotateStart.copy(this.rotateEnd); | |
this.updateView(); | |
}, | |
handleMouseMoveDolly: function (event) { | |
console.log('handleMouseMoveDolly'); | |
this.dollyEnd.set(event.clientX, event.clientY); | |
this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart); | |
if (this.dollyDelta.y > 0) { | |
!this.data.invertZoom ? this.dollyIn(this.getZoomScale()) : this.dollyOut(this.getZoomScale()); | |
} else if (this.dollyDelta.y < 0) { | |
!this.data.invertZoom ? this.dollyOut(this.getZoomScale()) : this.dollyIn(this.getZoomScale()); | |
} | |
this.dollyStart.copy(this.dollyEnd); | |
this.updateView(); | |
}, | |
handleMouseMovePan: function (event) { | |
if (this.ground) { | |
const panEnd = this.getRaycastPosition(event); | |
if (panEnd) { | |
this.panEnd = panEnd; | |
} | |
// If there were no raytraced results we cannot calculate the movement, so we bail early | |
if (!this.panStart || !this.panEnd) { | |
return; | |
} | |
// Calculate offset based on start - end | |
this.panOffset.subVectors(this.panStart, this.panEnd); | |
} else { | |
// Pan along mouse plane | |
this.panEnd.set(event.clientX, event.clientY); | |
this.panDelta.subVectors(this.panEnd, this.panStart); | |
this.pan(this.panDelta.x, this.panDelta.y); | |
this.panStart.copy(this.panEnd); | |
} | |
this.updateView(); | |
}, | |
handleMouseUp: function (event) { | |
// console.log( 'handleMouseUp' ); | |
}, | |
/* | |
* MOUSE WHEEL EVENT HANDLERS | |
*/ | |
handleMouseWheel: function (event) { | |
// console.log( 'handleMouseWheel' ); | |
var delta = 0; | |
if (event.wheelDelta !== undefined) { | |
// WebKit / Opera / Explorer 9 | |
delta = event.wheelDelta; | |
} else if (event.detail !== undefined) { | |
// Firefox | |
delta = -event.detail; | |
} | |
if (delta > 0) { | |
!this.data.invertZoom ? this.dollyOut(this.getZoomScale()) : this.dollyIn(this.getZoomScale()); | |
} else if (delta < 0) { | |
!this.data.invertZoom ? this.dollyIn(this.getZoomScale()) : this.dollyOut(this.getZoomScale()); | |
} | |
this.updateView(); | |
}, | |
/* | |
* TOUCH EVENT HANDLERS | |
*/ | |
handleTouchStartRotate: function (event) { | |
// console.log( 'handleTouchStartRotate' ); | |
this.rotateStart.set(event.touches[0].pageX, event.touches[0].pageY); | |
}, | |
handleTouchStartDolly: function (event) { | |
// console.log( 'handleTouchStartDolly' ); | |
var dx = event.touches[0].pageX - event.touches[1].pageX; | |
var dy = event.touches[0].pageY - event.touches[1].pageY; | |
var distance = Math.sqrt(dx * dx + dy * dy); | |
this.dollyStart.set(0, distance); | |
}, | |
handleTouchStartPan: function (event) { | |
// console.log( 'handleTouchStartPan' ); | |
this.panStart.set(event.touches[0].pageX, event.touches[0].pageY); | |
}, | |
handleTouchMoveRotate: function (event) { | |
// console.log( 'handleTouchMoveRotate' ); | |
this.rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY); | |
this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart); | |
var canvas = this.canvasEl === document ? this.canvasEl.body : this.canvasEl; | |
// rotating across whole screen goes 360 degrees around | |
this.rotateLeft(2 * Math.PI * this.rotateDelta.x / canvas.clientWidth * this.data.rotateSpeed); | |
// rotating up and down along whole screen attempts to go 360, but limited to 180 | |
this.rotateUp(2 * Math.PI * this.rotateDelta.y / canvas.clientHeight * this.data.rotateSpeed); | |
this.rotateStart.copy(this.rotateEnd); | |
this.updateView(); | |
}, | |
handleTouchMoveDolly: function (event) { | |
// console.log( 'handleTouchMoveDolly' ); | |
var dx = event.touches[0].pageX - event.touches[1].pageX; | |
var dy = event.touches[0].pageY - event.touches[1].pageY; | |
var distance = Math.sqrt(dx * dx + dy * dy); | |
this.dollyEnd.set(0, distance); | |
this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart); | |
if (this.dollyDelta.y > 0) { | |
this.dollyIn(this.getZoomScale()); | |
} else if (this.dollyDelta.y < 0) { | |
this.dollyOut(this.getZoomScale()); | |
} | |
this.dollyStart.copy(this.dollyEnd); | |
this.updateView(); | |
}, | |
handleTouchMovePan: function (event) { | |
// console.log( 'handleTouchMovePan' ); | |
this.panEnd.set(event.touches[0].pageX, event.touches[0].pageY); | |
this.panDelta.subVectors(this.panEnd, this.panStart); | |
this.pan(this.panDelta.x, this.panDelta.y); | |
this.panStart.copy(this.panEnd); | |
this.updateView(); | |
}, | |
handleTouchEnd: function (event) { | |
// console.log( 'handleTouchEnd' ); | |
}, | |
/* | |
* KEYBOARD EVENT HANDLERS | |
*/ | |
handleKeyDown: function (event) { | |
// console.log( 'handleKeyDown' ); | |
switch (event.keyCode) { | |
case this.keys.UP: | |
this.pan(0, this.data.keyPanSpeed); | |
this.updateView(); | |
break; | |
case this.keys.BOTTOM: | |
this.pan(0, -this.data.keyPanSpeed); | |
this.updateView(); | |
break; | |
case this.keys.LEFT: | |
this.pan(this.data.keyPanSpeed, 0); | |
this.updateView(); | |
break; | |
case this.keys.RIGHT: | |
this.pan(-this.data.keyPanSpeed, 0); | |
this.updateView(); | |
break; | |
default: | |
} | |
}, | |
/* | |
* HELPER FUNCTIONS | |
*/ | |
// Raycast from camera towards mouse position and return the intersection point on the ground mesh | |
getRaycastPosition: function (event) { | |
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
this.raycaster.setFromCamera(this.mouse, this.camera); | |
// raycast and check if we get any matches | |
const intersects = this.raycaster.intersectObjects(this.ground.children, true); | |
if (intersects.length > 0) { | |
return intersects[0].point; | |
} | |
// No raytrace matches | |
return null; | |
}, | |
getAutoRotationAngle: function () { | |
return 2 * Math.PI / 60 / 60 * this.data.autoRotateSpeed; | |
}, | |
getZoomScale: function () { | |
return Math.pow(0.95, this.data.zoomSpeed); | |
}, | |
rotateLeft: function (angle) { | |
this.sphericalDelta.theta -= angle; | |
}, | |
rotateUp: function (angle) { | |
this.sphericalDelta.phi -= angle; | |
}, | |
rotateTo: function (vec3) { | |
this.state = this.STATE.ROTATE_TO; | |
this.desiredPosition.copy(vec3); | |
}, | |
panHorizontally: function (distance, objectMatrix) { | |
// console.log('pan horizontally', distance, objectMatrix); | |
var v = new THREE.Vector3(); | |
v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix | |
v.multiplyScalar(-distance); | |
this.panOffset.add(v); | |
}, | |
panVertically: function (distance, objectMatrix) { | |
// console.log('pan vertically', distance, objectMatrix); | |
var v = new THREE.Vector3(); | |
v.setFromMatrixColumn(objectMatrix, 1); // get Y column of objectMatrix | |
v.multiplyScalar(distance); | |
this.panOffset.add(v); | |
}, | |
pan: function (deltaX, deltaY) { // deltaX and deltaY are in pixels; right and down are positive | |
console.log('panning', deltaX, deltaY); | |
var offset = new THREE.Vector3(); | |
var canvas = this.canvasEl === document ? this.canvasEl.body : this.canvasEl; | |
if (this.cameraType === 'PerspectiveCamera') { | |
// perspective | |
var position = this.dolly.position; | |
offset.copy(position).sub(this.target); | |
var targetDistance = offset.length(); | |
targetDistance *= Math.tan((this.camera.fov / 2) * Math.PI / 180.0); // half of the fov is center to top of screen | |
this.panHorizontally(2 * deltaX * targetDistance / canvas.clientHeight, this.object.matrix); // we actually don't use screenWidth, since perspective camera is fixed to screen height | |
this.panVertically(2 * deltaY * targetDistance / canvas.clientHeight, this.object.matrix); | |
} else if (this.cameraType === 'OrthographicCamera') { | |
// orthographic | |
this.panHorizontally(deltaX * (this.dolly.right - this.dolly.left) / this.camera.zoom / canvas.clientWidth, this.object.matrix); | |
this.panVertically(deltaY * (this.dolly.top - this.dolly.bottom) / this.camera.zoom / canvas.clientHeight, this.object.matrix); | |
} else { | |
// camera neither orthographic nor perspective | |
console.warn('Trying to pan: WARNING: Orbit Controls encountered an unknown camera type - pan disabled.'); | |
this.data.enablePan = false; | |
} | |
}, | |
dollyIn: function (dollyScale) { | |
console.log("dollyIn camera"); | |
if (this.cameraType === 'PerspectiveCamera') { | |
this.scale *= dollyScale; | |
} else if (this.cameraType === 'OrthographicCamera') { | |
this.camera.zoom = Math.max(this.data.minZoom, Math.min(this.data.maxZoom, this.camera.zoom * dollyScale)); | |
this.camera.updateProjectionMatrix(); | |
this.zoomChanged = true; | |
} else { | |
console.warn('Trying to dolly in: WARNING: Orbit Controls encountered an unknown camera type - dolly/zoom disabled.'); | |
this.data.enableZoom = false; | |
} | |
}, | |
dollyOut: function (dollyScale) { | |
console.log("dollyOut camera"); | |
if (this.cameraType === 'PerspectiveCamera') { | |
this.scale /= dollyScale; | |
} else if (this.cameraType === 'OrthographicCamera') { | |
this.camera.zoom = Math.max(this.data.minZoom, Math.min(this.data.maxZoom, this.camera.zoom / dollyScale)); | |
this.camera.updateProjectionMatrix(); | |
this.zoomChanged = true; | |
} else { | |
console.warn('Trying to dolly out: WARNING: Orbit Controls encountered an unknown camera type - dolly/zoom disabled.'); | |
this.data.enableZoom = false; | |
} | |
}, | |
lookAtTarget: function (object, target) { | |
var v = new THREE.Vector3(); | |
v.subVectors(object.position, target).add(object.position); | |
object.lookAt(v); | |
}, | |
/* | |
* SAVES CAMERA POSE (WHEN ENTERING VR) | |
*/ | |
saveCameraPose: function () { | |
if (this.savedPose) { | |
return; | |
} | |
this.savedPose = { | |
position: this.dolly.position, | |
rotation: this.dolly.rotation | |
}; | |
}, | |
/* | |
* RESTORE CAMERA POSE (WHEN EXITING VR) | |
*/ | |
restoreCameraPose: function () { | |
if (!this.savedPose) { | |
return; | |
} | |
this.dolly.position.copy(this.savedPose.position); | |
this.dolly.rotation.copy(this.savedPose.rotation); | |
this.savedPose = null; | |
}, | |
/* | |
* VIEW UPDATE | |
*/ | |
updateView: function (forceUpdate) { | |
if (this.desiredPosition && this.state === this.STATE.ROTATE_TO) { | |
var desiredSpherical = new THREE.Spherical(); | |
desiredSpherical.setFromVector3(this.desiredPosition); | |
var phiDiff = desiredSpherical.phi - this.spherical.phi; | |
var thetaDiff = desiredSpherical.theta - this.spherical.theta; | |
this.sphericalDelta.set(this.spherical.radius, phiDiff * this.data.rotateToSpeed, thetaDiff * this.data.rotateToSpeed); | |
} | |
var offset = new THREE.Vector3(); | |
var quat = new THREE.Quaternion().setFromUnitVectors(this.dolly.up, new THREE.Vector3(0, 1, 0)); // so camera.up is the orbit axis | |
var quatInverse = quat.clone().inverse(); | |
offset.copy(this.dolly.position).sub(this.target); | |
offset.applyQuaternion(quat); // rotate offset to "y-axis-is-up" space | |
this.spherical.setFromVector3(offset); // angle from z-axis around y-axis | |
if (this.data.autoRotate && this.state === this.STATE.NONE) this.rotateLeft(this.getAutoRotationAngle()); | |
this.spherical.theta += this.sphericalDelta.theta; | |
this.spherical.phi += this.sphericalDelta.phi; | |
this.spherical.theta = Math.max(this.data.minAzimuthAngle, Math.min(this.data.maxAzimuthAngle, this.spherical.theta)); // restrict theta to be inside desired limits | |
this.spherical.phi = Math.max(this.data.minPolarAngle, Math.min(this.data.maxPolarAngle, this.spherical.phi)); // restrict phi to be inside desired limits | |
this.spherical.makeSafe(); | |
this.spherical.radius *= this.scale; | |
this.spherical.radius = Math.max(this.data.minDistance, Math.min(this.data.maxDistance, this.spherical.radius)); // restrict radius to be inside desired limits | |
this.target.add(this.panOffset); // move target to panned location | |
offset.setFromSpherical(this.spherical); | |
offset.applyQuaternion(quatInverse); // rotate offset back to "camera-up-vector-is-up" space | |
this.dolly.position.copy(this.target).add(offset); | |
if (this.target) { | |
this.lookAtTarget(this.dolly, this.target); | |
} | |
if (this.data.enableDamping === true) { | |
this.sphericalDelta.theta *= (1 - this.data.dampingFactor); | |
this.sphericalDelta.phi *= (1 - this.data.dampingFactor); | |
} else { | |
this.sphericalDelta.set(0, 0, 0); | |
} | |
this.scale = 1; | |
this.panOffset.set(0, 0, 0); | |
// update condition is: | |
// min(camera displacement, camera rotation in radians)^2 > EPS | |
// using small-angle approximation cos(x/2) = 1 - x^2 / 8 | |
if (forceUpdate === true || | |
this.zoomChanged || | |
this.lastPosition.distanceToSquared(this.dolly.position) > this.EPS || | |
8 * (1 - this.lastQuaternion.dot(this.dolly.quaternion)) > this.EPS) { | |
// this.el.emit('change-drag-map-controls', null, false); | |
var hmdQuaternion = this.calculateHMDQuaternion(); | |
var hmdEuler = new THREE.Euler(); | |
hmdEuler.setFromQuaternion(hmdQuaternion, 'YXZ'); | |
this.el.setAttribute('position', { | |
x: this.dolly.position.x, | |
y: this.dolly.position.y, | |
z: this.dolly.position.z | |
}); | |
this.el.setAttribute('rotation', { | |
x: radToDeg(hmdEuler.x), | |
y: radToDeg(hmdEuler.y), | |
z: radToDeg(hmdEuler.z) | |
}); | |
this.lastPosition.copy(this.dolly.position); | |
this.lastQuaternion.copy(this.dolly.quaternion); | |
this.zoomChanged = false; | |
return true; | |
} | |
return false; | |
}, | |
calculateHMDQuaternion: (function () { | |
var hmdQuaternion = new THREE.Quaternion(); | |
return function () { | |
hmdQuaternion.copy(this.dolly.quaternion); | |
return hmdQuaternion; | |
}; | |
})() | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment