/* global THREE */ var registerComponent = require('../core/component').registerComponent; // Found at https://github.com/aframevr/assets. var MODEL_URLS = { toonLeft: 'https://cdn.aframe.io/controllers/hands/leftHand.glb', toonRight: 'https://cdn.aframe.io/controllers/hands/rightHand.glb', lowPolyLeft: 'https://cdn.aframe.io/controllers/hands/leftHandLow.glb', lowPolyRight: 'https://cdn.aframe.io/controllers/hands/rightHandLow.glb', highPolyLeft: 'https://cdn.aframe.io/controllers/hands/leftHandHigh.glb', highPolyRight: 'https://cdn.aframe.io/controllers/hands/rightHandHigh.glb' }; // Poses. var ANIMATIONS = { open: 'Open', // point: grip active, trackpad surface active, trigger inactive. point: 'Point', // pointThumb: grip active, trigger inactive, trackpad surface inactive. pointThumb: 'Point + Thumb', // fist: grip active, trigger active, trackpad surface active. fist: 'Fist', // hold: trigger active, grip inactive. hold: 'Hold', // thumbUp: grip active, trigger active, trackpad surface inactive. thumbUp: 'Thumb Up' }; // Map animation to public events for the API. var EVENTS = {}; EVENTS[ANIMATIONS.fist] = 'grip'; EVENTS[ANIMATIONS.thumbUp] = 'pistol'; EVENTS[ANIMATIONS.point] = 'pointing'; /** * Hand controls component that abstracts 6DoF controls: * oculus-touch-controls, vive-controls, windows-motion-controls. * * Originally meant to be a sample implementation of applications-specific controls that * abstracts multiple types of controllers. * * Auto-detect appropriate controller. * Handle common events coming from the detected vendor-specific controls. * Translate button events to semantic hand-related event names: * (gripclose, gripopen, thumbup, thumbdown, pointup, pointdown) * Load hand model with gestures that are applied based on the button pressed. * * @property {string} Hand mapping (`left`, `right`). */ module.exports.Component = registerComponent('hand-controls', { schema: { color: {default: 'white', type: 'color'}, hand: { default: 'left' }, handModelStyle: {default: 'lowPoly', oneOf: ['lowPoly', 'highPoly', 'toon']} }, init: function () { var self = this; var el = this.el; // Current pose. this.gesture = ANIMATIONS.open; // Active buttons populated by events provided by the attached controls. this.pressedButtons = {}; this.touchedButtons = {}; this.loader = new THREE.GLTFLoader(); this.loader.setCrossOrigin('anonymous'); this.onGripDown = function () { self.handleButton('grip', 'down'); }; this.onGripUp = function () { self.handleButton('grip', 'up'); }; this.onTrackpadDown = function () { self.handleButton('trackpad', 'down'); }; this.onTrackpadUp = function () { self.handleButton('trackpad', 'up'); }; this.onTrackpadTouchStart = function () { self.handleButton('trackpad', 'touchstart'); }; this.onTrackpadTouchEnd = function () { self.handleButton('trackpad', 'touchend'); }; this.onTriggerDown = function () { self.handleButton('trigger', 'down'); }; this.onTriggerUp = function () { self.handleButton('trigger', 'up'); }; this.onTriggerTouchStart = function () { self.handleButton('trigger', 'touchstart'); }; this.onTriggerTouchEnd = function () { self.handleButton('trigger', 'touchend'); }; this.onGripTouchStart = function () { self.handleButton('grip', 'touchstart'); }; this.onGripTouchEnd = function () { self.handleButton('grip', 'touchend'); }; this.onThumbstickDown = function () { self.handleButton('thumbstick', 'down'); }; this.onThumbstickUp = function () { self.handleButton('thumbstick', 'up'); }; this.onAorXTouchStart = function () { self.handleButton('AorX', 'touchstart'); }; this.onAorXTouchEnd = function () { self.handleButton('AorX', 'touchend'); }; this.onBorYTouchStart = function () { self.handleButton('BorY', 'touchstart'); }; this.onBorYTouchEnd = function () { self.handleButton('BorY', 'touchend'); }; this.onSurfaceTouchStart = function () { self.handleButton('surface', 'touchstart'); }; this.onSurfaceTouchEnd = function () { self.handleButton('surface', 'touchend'); }; this.onControllerConnected = this.onControllerConnected.bind(this); this.onControllerDisconnected = this.onControllerDisconnected.bind(this); el.addEventListener('controllerconnected', this.onControllerConnected); el.addEventListener('controllerdisconnected', this.onControllerDisconnected); // Hidden by default. el.object3D.visible = false; }, play: function () { this.addEventListeners(); }, pause: function () { this.removeEventListeners(); }, tick: function (time, delta) { var mesh = this.el.getObject3D('mesh'); if (!mesh || !mesh.mixer) { return; } mesh.mixer.update(delta / 1000); }, onControllerConnected: function () { this.el.object3D.visible = true; }, onControllerDisconnected: function () { this.el.object3D.visible = false; }, addEventListeners: function () { var el = this.el; el.addEventListener('gripdown', this.onGripDown); el.addEventListener('gripup', this.onGripUp); el.addEventListener('trackpaddown', this.onTrackpadDown); el.addEventListener('trackpadup', this.onTrackpadUp); el.addEventListener('trackpadtouchstart', this.onTrackpadTouchStart); el.addEventListener('trackpadtouchend', this.onTrackpadTouchEnd); el.addEventListener('triggerdown', this.onTriggerDown); el.addEventListener('triggerup', this.onTriggerUp); el.addEventListener('triggertouchstart', this.onTriggerTouchStart); el.addEventListener('triggertouchend', this.onTriggerTouchEnd); el.addEventListener('griptouchstart', this.onGripTouchStart); el.addEventListener('griptouchend', this.onGripTouchEnd); el.addEventListener('thumbstickdown', this.onThumbstickDown); el.addEventListener('thumbstickup', this.onThumbstickUp); el.addEventListener('abuttontouchstart', this.onAorXTouchStart); el.addEventListener('abuttontouchend', this.onAorXTouchEnd); el.addEventListener('bbuttontouchstart', this.onBorYTouchStart); el.addEventListener('bbuttontouchend', this.onBorYTouchEnd); el.addEventListener('xbuttontouchstart', this.onAorXTouchStart); el.addEventListener('xbuttontouchend', this.onAorXTouchEnd); el.addEventListener('ybuttontouchstart', this.onBorYTouchStart); el.addEventListener('ybuttontouchend', this.onBorYTouchEnd); el.addEventListener('surfacetouchstart', this.onSurfaceTouchStart); el.addEventListener('surfacetouchend', this.onSurfaceTouchEnd); }, removeEventListeners: function () { var el = this.el; el.removeEventListener('gripdown', this.onGripDown); el.removeEventListener('gripup', this.onGripUp); el.removeEventListener('trackpaddown', this.onTrackpadDown); el.removeEventListener('trackpadup', this.onTrackpadUp); el.removeEventListener('trackpadtouchstart', this.onTrackpadTouchStart); el.removeEventListener('trackpadtouchend', this.onTrackpadTouchEnd); el.removeEventListener('triggerdown', this.onTriggerDown); el.removeEventListener('triggerup', this.onTriggerUp); el.removeEventListener('triggertouchstart', this.onTriggerTouchStart); el.removeEventListener('triggertouchend', this.onTriggerTouchEnd); el.removeEventListener('griptouchstart', this.onGripTouchStart); el.removeEventListener('griptouchend', this.onGripTouchEnd); el.removeEventListener('thumbstickdown', this.onThumbstickDown); el.removeEventListener('thumbstickup', this.onThumbstickUp); el.removeEventListener('abuttontouchstart', this.onAorXTouchStart); el.removeEventListener('abuttontouchend', this.onAorXTouchEnd); el.removeEventListener('bbuttontouchstart', this.onBorYTouchStart); el.removeEventListener('bbuttontouchend', this.onBorYTouchEnd); el.removeEventListener('xbuttontouchstart', this.onAorXTouchStart); el.removeEventListener('xbuttontouchend', this.onAorXTouchEnd); el.removeEventListener('ybuttontouchstart', this.onBorYTouchStart); el.removeEventListener('ybuttontouchend', this.onBorYTouchEnd); el.removeEventListener('surfacetouchstart', this.onSurfaceTouchStart); el.removeEventListener('surfacetouchend', this.onSurfaceTouchEnd); }, /** * Update handler. More like the `init` handler since the only property is the hand, and * that won't be changing much. */ update: function (previousHand) { var controlConfiguration; var el = this.el; var hand = this.data.hand; var handModelStyle = this.data.handModelStyle; var handColor = this.data.color; var self = this; // Get common configuration to abstract different vendor controls. controlConfiguration = { hand: hand, model: false }; // Set model. if (hand !== previousHand) { var handmodelUrl = MODEL_URLS[handModelStyle + hand.charAt(0).toUpperCase() + hand.slice(1)]; this.loader.load(handmodelUrl, function (gltf) { var mesh = gltf.scene.children[0]; var handModelOrientation = hand === 'left' ? Math.PI / 2 : -Math.PI / 2; mesh.mixer = new THREE.AnimationMixer(mesh); self.clips = gltf.animations; el.setObject3D('mesh', mesh); var handMaterial = mesh.children[1].material; handMaterial.color = new THREE.Color(handColor); mesh.position.set(0, 0, 0); mesh.rotation.set(0, 0, handModelOrientation); el.setAttribute('magicleap-controls', controlConfiguration); el.setAttribute('vive-controls', controlConfiguration); el.setAttribute('oculus-touch-controls', controlConfiguration); el.setAttribute('windows-motion-controls', controlConfiguration); }); } }, remove: function () { this.el.removeObject3D('mesh'); }, /** * Play model animation, based on which button was pressed and which kind of event. * * 1. Process buttons. * 2. Determine gesture (this.determineGesture()). * 3. Animation gesture (this.animationGesture()). * 4. Emit gesture events (this.emitGestureEvents()). * * @param {string} button - Name of the button. * @param {string} evt - Type of event for the button (i.e., down/up/touchstart/touchend). */ handleButton: function (button, evt) { var lastGesture; var isPressed = evt === 'down'; var isTouched = evt === 'touchstart'; // Update objects. if (evt.indexOf('touch') === 0) { // Update touch object. if (isTouched === this.touchedButtons[button]) { return; } this.touchedButtons[button] = isTouched; } else { // Update button object. if (isPressed === this.pressedButtons[button]) { return; } this.pressedButtons[button] = isPressed; } // Determine the gesture. lastGesture = this.gesture; this.gesture = this.determineGesture(); // Same gesture. if (this.gesture === lastGesture) { return; } // Animate gesture. this.animateGesture(this.gesture, lastGesture); // Emit events. this.emitGestureEvents(this.gesture, lastGesture); }, /** * Determine which pose hand should be in considering active and touched buttons. */ determineGesture: function () { var gesture; var isGripActive = this.pressedButtons.grip; var isSurfaceActive = this.pressedButtons.surface || this.touchedButtons.surface; var isTrackpadActive = this.pressedButtons.trackpad || this.touchedButtons.trackpad; var isTriggerActive = this.pressedButtons.trigger || this.touchedButtons.trigger; var isABXYActive = this.touchedButtons.AorX || this.touchedButtons.BorY; var isVive = isViveController(this.el.components['tracked-controls']); // Works well with Oculus Touch and Windows Motion Controls, but Vive needs tweaks. if (isVive) { if (isGripActive || isTriggerActive) { gesture = ANIMATIONS.fist; } else if (isTrackpadActive) { gesture = ANIMATIONS.point; } } else { if (isGripActive) { if (isSurfaceActive || isABXYActive || isTrackpadActive) { gesture = isTriggerActive ? ANIMATIONS.fist : ANIMATIONS.point; } else { gesture = isTriggerActive ? ANIMATIONS.thumbUp : ANIMATIONS.pointThumb; } } else if (isTriggerActive) { gesture = ANIMATIONS.hold; } } return gesture; }, /** * Play corresponding clip to a gesture */ getClip: function (gesture) { var clip; var i; for (i = 0; i < this.clips.length; i++) { clip = this.clips[i]; if (clip.name !== gesture) { continue; } return clip; } }, /** * Play gesture animation. * * @param {string} gesture - Which pose to animate to. If absent, then animate to open. * @param {string} lastGesture - Previous gesture, to reverse back to open if needed. */ animateGesture: function (gesture, lastGesture) { if (gesture) { this.playAnimation(gesture || ANIMATIONS.open, lastGesture, false); return; } // If no gesture, then reverse the current gesture back to open pose. this.playAnimation(lastGesture, lastGesture, true); }, /** * Emit `hand-controls`-specific events. */ emitGestureEvents: function (gesture, lastGesture) { var el = this.el; var eventName; if (lastGesture === gesture) { return; } // Emit event for lastGesture not inactive. eventName = getGestureEventName(lastGesture, false); if (eventName) { el.emit(eventName); } // Emit event for current gesture now active. eventName = getGestureEventName(gesture, true); if (eventName) { el.emit(eventName); } }, /** * Play hand animation based on button state. * * @param {string} gesture - Name of the animation as specified by the model. * @param {string} lastGesture - Previous pose. * @param {boolean} reverse - Whether animation should play in reverse. */ playAnimation: function (gesture, lastGesture, reverse) { var clip; var fromAction; var mesh = this.el.getObject3D('mesh'); var toAction; if (!mesh) { return; } // Stop all current animations. mesh.mixer.stopAllAction(); // Grab clip action. clip = this.getClip(gesture); toAction = mesh.mixer.clipAction(clip); toAction.clampWhenFinished = true; toAction.loop = THREE.LoopRepeat; toAction.repetitions = 0; toAction.timeScale = reverse ? -1 : 1; toAction.time = reverse ? clip.duration : 0; toAction.weight = 1; // No gesture to gesture or gesture to no gesture. if (!lastGesture || gesture === lastGesture) { // Stop all current animations. mesh.mixer.stopAllAction(); // Play animation. toAction.play(); return; } // Animate or crossfade from gesture to gesture. clip = this.getClip(lastGesture); fromAction = mesh.mixer.clipAction(clip); fromAction.weight = 0.15; fromAction.play(); toAction.play(); fromAction.crossFadeTo(toAction, 0.15, true); } }); /** * Suffix gestures based on toggle state (e.g., open/close, up/down, start/end). * * @param {string} gesture * @param {boolean} active */ function getGestureEventName (gesture, active) { var eventName; if (!gesture) { return; } eventName = EVENTS[gesture]; if (eventName === 'grip') { return eventName + (active ? 'close' : 'open'); } if (eventName === 'point') { return eventName + (active ? 'up' : 'down'); } if (eventName === 'pointing' || eventName === 'pistol') { return eventName + (active ? 'start' : 'end'); } } function isViveController (trackedControls) { var controller = trackedControls && trackedControls.controller; var isVive = controller && (controller.id && controller.id.indexOf('OpenVR ') === 0 || (controller.profiles && controller.profiles[0] && controller.profiles[0] === 'htc-vive-controller-mv')); return isVive; }