Last active
May 7, 2019 15:09
-
-
Save arpruss/21ecdf5ebdf01e2f9e0ddb3eafd2d3b1 to your computer and use it in GitHub Desktop.
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
/** | |
The MIT License (MIT) | |
Copyright (c) 2015 Don McCurdy | |
Copyright (c) 2018 Alexander Pruss | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
*/ | |
// configs | |
var MAX_DELTA = 200, // ms | |
ROTATION_EPS = 0.000, | |
QUATERNION_EPS = 0.000, | |
DEFAULT_FOV = 60, | |
DEG_TO_RAD = 1 / 180 * Math.PI, | |
RAD_TO_DEG = 180 / Math.PI | |
// main | |
var SpaceNavigator = { | |
schema: { | |
// Enable/disable features | |
enabled: { default: true }, | |
movementEnabled: { default: true }, | |
lookEnabled: { default: true }, | |
rollEnabled: { default: true }, | |
invertPitch: { default: false }, | |
fovEnabled: { default: true }, | |
fovMin: { default: 2 }, | |
fovMax: { default: 115 }, | |
// Constants | |
rotationSensitivity: { default: 0.05 }, | |
movementEasing: { default: 3 }, | |
movementAcceleration: { default: 700 }, | |
fovSensitivity: { default: 0.01 }, | |
fovEasing: { default: 3 }, | |
fovAcceleration: { default: 5 }, | |
invertScroll: { default: false } | |
}, | |
/** | |
* Called once when component is attached. Generally for initial setup. | |
*/ | |
init: function () { | |
var this_ = this | |
// Movement | |
this.position = new THREE.Vector3(0, 0, 0) | |
this.movement = new THREE.Vector3(0, 0, 0) | |
this.movementVelocity = new THREE.Vector3(0, 0, 0) | |
this.movementDirection = new THREE.Vector3(0, 0, 0) | |
// Rotation | |
this.rotation = new THREE.Quaternion() | |
// FOV | |
this.fov = DEFAULT_FOV | |
this.fovVelocity = 0 | |
// Button state | |
this.buttons = {} | |
// scroll wheel | |
this.scroll = 0 | |
this.scrollDelta = 0 | |
// time | |
this._previousUpdate = performance.now() | |
// bind scroll events | |
// IE, Opera, Google Chrome, Safari | |
var inverScrollFactor = this.data.invertScroll ? -1 : 1 | |
document.addEventListener('mousewheel', function(event){ | |
event.preventDefault() | |
this_.scroll += event.wheelDelta / 60 * inverScrollFactor | |
}) | |
// Firefox | |
document.addEventListener('DOMMouseScroll', function(event){ | |
event.preventDefault() | |
this_.scroll -= event.detail * inverScrollFactor | |
}) | |
if (!this.getSpaceNavigator()) { | |
console.warn( 'Space Navigator not found. Connect and press any button to continue.') | |
} | |
}, | |
/** | |
* AFRAME specific: Called on each iteration of main render loop. | |
*/ | |
tick: function (t, dt) { | |
this.updateRotation(dt) | |
this.updatePosition(dt) | |
this.updateButtonState() | |
if (this.data.fovEnabled) this.updateFov(dt) | |
}, | |
/** | |
* THREE specific: Called on each iteration of main render loop. | |
*/ | |
update: function () { | |
var time = performance.now() | |
var dt = time - this._previousUpdate | |
this._previousUpdate = time | |
this.updateRotation(dt) | |
this.updatePosition(dt) | |
this.updateButtonState() | |
if (this.data.fovEnabled) this.updateFov(dt) | |
}, | |
/******************************************************************* | |
* Movement | |
*/ | |
updatePosition: function (dt) { | |
var data = this.data | |
var acceleration = data.movementAcceleration | |
var easing = data.movementEasing | |
var velocity = this.movementVelocity | |
var el = this.el | |
var spaceNavigator = this.getSpaceNavigator() | |
// If data has changed or FPS is too low | |
// we reset the velocity | |
if (dt > MAX_DELTA) { | |
velocity.x = 0 | |
velocity.y = 0 | |
velocity.z = 0 | |
return | |
} | |
velocity.z -= velocity.z * easing * dt / 1000 | |
velocity.x -= velocity.x * easing * dt / 1000 | |
velocity.y -= velocity.y * easing * dt / 1000 | |
var position = el ? el.getAttribute('position') : this.position | |
if (data.enabled && data.movementEnabled && spaceNavigator) { | |
/* | |
* 3dconnexion space navigator position axes | |
* | |
* "right handed coordinate system" | |
* 0: - left / + right (pos: X axis pointing to the right) | |
* 1: - backwards / + forward (pos: Z axis pointing forwards) | |
* 2: - up / + down (pos: Y axis pointing down) | |
*/ | |
var xDelta = spaceNavigator.axes[0], | |
yDelta = -spaceNavigator.axes[2], | |
zDelta = spaceNavigator.axes[1] | |
velocity.x += xDelta * acceleration * dt / 1000 | |
velocity.z += zDelta * acceleration * dt / 1000 | |
velocity.y -= yDelta * acceleration * dt / 1000 | |
} | |
var movementVector = this.getMovementVector(dt); | |
this.movement.copy(movementVector) | |
this.position.add(movementVector) | |
if (el) { | |
el.object3D.position.copy(this.position) | |
el.setAttribute('position', { | |
x: this.position.x, | |
y: this.position.y, | |
z: this.position.z | |
}); | |
} | |
}, | |
getMovementVector: function (dt) { | |
if (this._getMovementVector) return this._getMovementVector(dt) | |
var euler = new THREE.Euler(0, 0, 0, 'YXZ'), | |
rotation = new THREE.Quaternion(), | |
direction = this.movementDirection, | |
velocity = this.movementVelocity | |
this._getMovementVector = function (dt) { | |
if (this.el) { | |
rotation.copy( this.el.getAttribute('rotation') ) | |
} else { | |
rotation.copy( this.rotation ) | |
} | |
direction.copy(velocity) | |
direction.multiplyScalar(dt / 1000) | |
if (!rotation) return direction | |
direction.applyQuaternion(rotation) | |
return direction | |
} | |
return this._getMovementVector(dt) | |
}, | |
quaternionDistanceSq: function(a,b) { | |
return Math.pow(a.x-b.x,2)+Math.pow(a.y-b.y,2)+Math.pow(a.z-b.z,2)+Math.pow(a.w-b.w,2) | |
}, | |
/******************************************************************* | |
* Rotation | |
*/ | |
updateRotation: function () { | |
if (this._updateRotation) return this._updateRotation(); | |
var initialRotation = new THREE.Quaternion(), | |
prevInitialRotation = new THREE.Quaternion(), | |
prevFinalRotation = new THREE.Quaternion(); | |
var tCurrent, | |
tLastLocalActivity = 0, | |
tLastExternalActivity = 0; | |
var rotationEps = 0.0001, | |
debounce = 500; | |
this._updateRotation = function () { | |
var spaceNavigator = this.getSpaceNavigator() | |
if (!this.data.lookEnabled || !spaceNavigator) return; | |
tCurrent = Date.now(); | |
if (this.el) { | |
initialRotation.copy(this.el.getAttribute('rotation') || initialRotation) | |
} else { | |
initialRotation.copy(this.rotation) | |
} | |
// If initial rotation for this frame is different from last frame, and | |
// doesn't match last spaceNavigator state, assume an external component is | |
// active on this element. | |
/* TODO: FIX: | |
if (//this.quaternionDistanceSq(initialRotation,prevInitialRotation) > rotationEps || | |
this.quaternionDistanceSq(initialRotation,prevFinalRotation) > rotationEps) { | |
//prevInitialRotation.copy(initialRotation); | |
prevFinalRotation.copy(initialRotation) | |
tLastExternalActivity = tCurrent; | |
return; | |
} */ | |
prevInitialRotation.copy(initialRotation); | |
// If external controls have been active in last 500ms, wait. | |
if (tCurrent - tLastExternalActivity < debounce) return | |
/* | |
* 3dconnexion space navigator rotation axes | |
* | |
* "right handed coordinate system" | |
* 3: - pitch down / + pitch up (rot: X axis clock wise) | |
* 4: - roll right / + roll left (rot: Z axis clock wise) | |
* 5: - yaw right / + yaw left (rot: Y axis clock wise) | |
*/ | |
var delta = new THREE.Vector3(spaceNavigator.axes[3], spaceNavigator.axes[5], spaceNavigator.axes[4]) | |
//console.log(delta) | |
if (delta.x < ROTATION_EPS && delta.x > -ROTATION_EPS) delta.z = 0 | |
if (delta.y < ROTATION_EPS && delta.y > -ROTATION_EPS) delta.y = 0 | |
if (delta.z < ROTATION_EPS && delta.z > -ROTATION_EPS) delta.x = 0 | |
if (this.data.invertPitch) delta.x *= -delta.x | |
if (!this.data.rollEnabled) delta.z = 0 | |
// If external controls have been active more recently than spaceNavigator, | |
// and spaceNavigator hasn't moved, don't overwrite the existing rotation. | |
if (tLastExternalActivity > tLastLocalActivity && !delta.lengthSq()) return | |
delta.multiplyScalar(this.data.rotationSensitivity) | |
var q = new THREE.Quaternion() | |
q.setFromEuler(new THREE.Euler(delta.x,delta.y,delta.z)) | |
this.rotation.multiply(q) | |
if (this.el) { | |
this.el.setAttribute('rotation', rotation) | |
prevFinalRotation.copy(this.el.getAttribute('rotation')) | |
} else { | |
prevFinalRotation.copy(this.rotation) | |
} | |
tLastLocalActivity = tCurrent; | |
}; | |
return this._updateRotation(); | |
}, | |
updateFov: function (dt) { | |
if (this._updateFov) return this._updateFov(dt) | |
var self = this | |
var previousScroll = 0 | |
this._updateFov = function (dt) { | |
var fovFromAttribute = self.el ? self.el.getAttribute('fov') : null | |
var fov = fovFromAttribute ? parseFloat(fovFromAttribute) : self.fov | |
var lensDistance = 1 / Math.tan(fov / 2 * DEG_TO_RAD) | |
// easing | |
if (dt > 1000) return | |
self.fovVelocity = self.fovVelocity - self.fovVelocity * dt / 1000 * self.data.fovEasing | |
if (self.fovVelocity > -0.001 && self.fovVelocity < 0.001) self.fovVelocity = 0 | |
// acceleration | |
var scrollDelta = previousScroll - self.scroll | |
self.fovVelocity += scrollDelta * dt / 1000 * self.data.fovAcceleration | |
// applay | |
var newLensDistance = lensDistance + self.fovVelocity * self.data.fovSensitivity | |
//var newFov = Math.min(140, Math.max(10, Math.atan( 1 / newLensDistance ) * 2)) | |
fov = Math.atan(1 / newLensDistance) * 2 * RAD_TO_DEG | |
if (fov > self.data.fovMin && fov < self.data.fovMax) { | |
if (self.el) self.el.setAttribute('fov', fov) | |
self.fov = fov | |
} | |
previousScroll = self.scroll | |
} | |
return this._updateFov(dt) | |
}, | |
/******************************************************************* | |
* Button events | |
*/ | |
updateButtonState: function () { | |
var spaceNavigator = this.getSpaceNavigator(); | |
if (this.data.enabled && spaceNavigator) { | |
// Fire DOM events for button state changes. | |
for (var i = 0; i < spaceNavigator.buttons.length; i++) { | |
if (spaceNavigator.buttons[i].pressed && !this.buttons[i]) { | |
this.emit(new ButtonEvent('navigatorbuttondown', i, spaceNavigator.buttons[i])); | |
} else if (!spaceNavigator.buttons[i].pressed && this.buttons[i]) { | |
this.emit(new ButtonEvent('navigatorbuttonup', i, spaceNavigator.buttons[i])); | |
} | |
this.buttons[i] = spaceNavigator.buttons[i].pressed; | |
} | |
} else if (Object.keys(this.buttons)) { | |
// Reset state if controls are disabled or controller is lost. | |
this.buttons = {}; | |
} | |
}, | |
emit: function (event) { | |
/* // Emit original event. | |
this.el.emit(event.type, event); | |
// Emit convenience event, identifying button index. | |
this.el.emit( | |
event.type + ':' + event.index, | |
new ButtonEvent(event.type, event.index, event) | |
); */ | |
}, | |
/******************************************************************* | |
* SpaceNavigator state | |
*/ | |
/** | |
* Returns SpaceNavigator instance attached to the component. If connected, | |
* a proxy-controls component may provide access to spaceNavigator input from a | |
* remote device. | |
* | |
* @return {SpaceNavigator} | |
*/ | |
getSpaceNavigator: function () { | |
var this_ = this | |
var proxyControls = this.el ? this.el.sceneEl.components['proxy-controls'] : null | |
if (proxyControls) { | |
// use proxy space navigator | |
return proxyControls && proxyControls.isConnected() && proxyControls.getSpaceNavigator() | |
} else { | |
// use local space navigator | |
if (!navigator.getGamepads) { | |
console.error('Gamepad API is not supported on this browser. Please use Firefox or Chrome.') | |
return false | |
} | |
if (this.spaceNavigatorId === undefined) { | |
// find space navigator | |
var gamepadList = navigator.getGamepads() | |
Object.keys(gamepadList).forEach(function(i){ | |
var gamepadName = gamepadList[i] ? gamepadList[i].id : null | |
if (gamepadName /* && | |
( | |
gamepadName.toLowerCase().indexOf('spacenavigator') > -1 | |
|| gamepadName.toLowerCase().indexOf('space navigator') > -1 | |
|| gamepadName.toLowerCase().indexOf('spacemouse') > -1 | |
|| gamepadName.toLowerCase().indexOf('space mouse') > -1 | |
|| (gamepadName.toLowerCase().indexOf('vendor: 046d') > -1 && gamepadName.toLowerCase().indexOf('product: c6')) | |
) */ | |
) { | |
this_.spaceNavigatorId = i | |
} | |
}) | |
} | |
return navigator.getGamepads()[this.spaceNavigatorId] | |
} | |
}, | |
/** | |
* Returns true if Space Navigator is currently connected to the system. | |
* @return {boolean} | |
*/ | |
isConnected: function () { | |
var spaceNavigator = this.getSpaceNavigator(); | |
return !!(spaceNavigator && spaceNavigator.connected); | |
}, | |
/** | |
* Returns a string containing some information about the controller. Result | |
* may vary across browsers, for a given controller. | |
* @return {string} | |
*/ | |
getID: function () { | |
return this.getSpaceNavigator().id; | |
} | |
} | |
// helpers | |
function ButtonEvent (type, index, details) { | |
this.type = type; | |
this.index = index; | |
this.pressed = details.pressed; | |
this.value = details.value; | |
} | |
// performance now polyfill | |
// inspired by: https://gist.github.com/paulirish/5438650 | |
if (!window.performance) { | |
window.performance = {} | |
} | |
if (!window.performance.now){ | |
var navigationStart = performance.timing ? performance.timing.navigationStart : null | |
var nowOffset = navigationStart || Date.now() | |
window.performance.now = function now(){ | |
return Date.now() - nowOffset | |
} | |
} | |
if (window.THREE) { | |
var SpaceNavigatorControls = function SpaceNavigatorControls(args) { | |
args = args || {} | |
var this_ = this | |
this_.data = {} | |
Object.keys(this_.schema).forEach(function(argName){ | |
if (args[argName] !== undefined) { | |
// use argument | |
this_.data[argName] = args[argName] | |
} else { | |
// set default | |
this_.data[argName] = this_.schema[argName].default | |
} | |
}) | |
this_.init() | |
} | |
SpaceNavigatorControls.prototype = SpaceNavigator | |
window.THREE.SpaceNavigatorControls = SpaceNavigatorControls | |
} | |
var tinkerCADPatch = { | |
EPS_SQUARED: Math.pow(1e-3,2), | |
getCamera: function() { | |
return tcApp._editor3DContent._editor3DModel.submodel._content.Navigating.val.navigation.getCamera() | |
}, | |
syncCamera: function() { | |
tcApp._editor3DContent._editor3DModel.submodel._content.Navigating.val.syncCamera() | |
}, | |
updateFromCamera: function(cam) { | |
_this = tinkerCADPatch | |
if (_this.prev.position && cam.position.distanceToSquared( _this.prev.position ) <= _this.EPS_SQUARED && | |
_this.prev.target && cam.target.distanceToSquared( _this.prev.target ) <= _this.EPS_SQUARED && | |
_this.prev.up && cam.up.distanceToSquared( _this.prev.up ) > _this.EPS_SQUARED) | |
return false; | |
_this.prev.position = cam.position.clone() | |
_this.prev.target = cam.target.clone() | |
_this.prev.up = cam.up.clone() | |
_this.prev.look = cam.target.clone() | |
_this.prev.look.sub(_this.prev.position) | |
var upNorm = _this.prev.up.clone() | |
upNorm.normalize() | |
var lookNorm = _this.prev.look.clone() | |
lookNorm.normalize() | |
var upCrossLook = upNorm.clone() | |
upCrossLook.cross(lookNorm) | |
var m = new THREE.Matrix4() | |
m.set(upCrossLook.x, upNorm.x, lookNorm.x, 1, | |
upCrossLook.y, upNorm.y, lookNorm.y, 1, | |
upCrossLook.z, upNorm.z, lookNorm.z, 1, | |
0, 0, 0, 1) | |
_this.controls.rotation.setFromRotationMatrix(m) | |
_this.controls.position.copy(_this.prev.target) | |
return true | |
}, | |
updateToCamera: function(cam) { | |
_this = tinkerCADPatch | |
cam.target.copy(_this.controls.position) | |
var camPosition = new THREE.Vector3() | |
camPosition.copy(_this.controls.position.copy) | |
var look = new THREE.Vector3(0,0,1) | |
look.applyQuaternion(_this.controls.rotation) | |
look.multiplyScalar(_this.prev.look.length()) | |
cam.position.subVectors(cam.target, look) | |
cam.up.set(0,1,0) | |
cam.up.applyQuaternion(_this.controls.rotation) | |
return cam.position.distanceToSquared(_this.prev.position) >= _this.EPS_SQUARED || | |
cam.target.distanceToSquared(_this.prev.target) >= _this.EPS_SQUARED || | |
cam.up.distanceToSquared(_this.prev.up) >= _this.EPS_SQUARED | |
}, | |
update: function() { | |
var cam = tinkerCADPatch.getCamera() | |
tinkerCADPatch.updateFromCamera(cam) | |
tinkerCADPatch.controls.update() | |
if (tinkerCADPatch.updateToCamera(cam)) { | |
tinkerCADPatch.prev.position = cam.position.clone() | |
tinkerCADPatch.prev.target = cam.target.clone() | |
tinkerCADPatch.prev.up = cam.up.clone() | |
tinkerCADPatch.syncCamera() | |
} | |
}, | |
init: function() { | |
this.prev = {} | |
this.controls = new THREE.SpaceNavigatorControls() | |
this.controls.init() | |
this.updateFromCamera(this.getCamera()) | |
setInterval(this.update, 50) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment