From b88f478dc4e744e440acadaca417b076e6bef141 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Sat, 23 Apr 2016 18:01:04 -0700 Subject: [PATCH 01/33] Checkpoint --- examples/controllers/handControllerPointer.js | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 examples/controllers/handControllerPointer.js diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js new file mode 100644 index 0000000000..2d8c9b4755 --- /dev/null +++ b/examples/controllers/handControllerPointer.js @@ -0,0 +1,104 @@ +// +// handControllerPointer.js +// examples/controllers +// +// Created by Howard Stearns on 2016/04/22 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// For now: +// Right hand only. +// HMD only. (Desktop isn't turned off, but right now it's using +// HMD.overlayFromWorldPoint(HMD.calculateRayUICollisionPoint ...) without compensation.) +// Cursor all the time when uncradled. (E.g., not just when blue ray is on, or five seconds after movement, etc.) +// Button 3 is left-mouse, button 4 is right-mouse. + +function debug() { // Display the arguments not just [Object object]. + print.apply(null, [].map.call(arguments, JSON.stringify)); +} + +function calculateRayUICollisionPoint(position, direction) { + // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection. + if (HMD.active) { + return HMD.calculateRayUICollisionPoint(position, direction); + } + // interect HUD plane, 1m in front of camera, using formula: + // scale = hudNormal dot (hudPoint - position) / hudNormal dot direction + // intersection = postion + scale*direction + var hudNormal = Quat.getFront(Camera.getOrientation()); + var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // 1m out + var denominator = Vec3.dot(hudNormal, direction); + if (denominator === 0) { return null; } // parallel to plane + var numerator = Vec3.dot(hudNormal, Vec3.subtract(hudPoint, position)); + var scale = numerator / denominator; + return Vec3.sum(position, Vec3.multiply(scale, direction)); +} +var DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees +var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds +var verticalHalfFieldOfView = (Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW) / 2; +var settingsChecker = Script.setInterval(function () { + verticalHalfFieldOfView = (Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW) / 2; +}, SETTINGS_CHANGE_RECHECK_INTERVAL); +function overlayFromWorldPoint(point) { + // Answer the 2d pixel-space location in the HUD that covers the given 3D point. + if (HMD.active) { + return HMD.overlayFromWorldPoint(point); + } + // Find the yaw and pitch from camera to position, as a fraction of view frustrum, and multiply by current screen size. + var hudNormal = Quat.getFront(Camera.getOrientation()); + var cameraToPoint = Vec3.subtract(point, Camera.getPosition()); + var eulerDegrees = Quat.safeEulerAngles(Quat.rotationBetween(hudNormal, cameraToPoint)); + var size = Reticle.maximumPosition; + var horizontalHalfFieldOfView = size.x * verticalHalfFieldOfView / size.y; + return { + x: size.x * (eulerDegrees.x / horizontalHalfFieldOfView + 0.5), + y: size.y * (eulerDegrees.y / verticalHalfFieldOfView + 0.5) + }; +} + +var MAPPING_NAME = Script.resolvePath(''); +var mapping = Controller.newMapping(MAPPING_NAME); +function mapToAction(controller, button, action) { + if (!Controller.Hardware[controller]) { return; } + mapping.from(Controller.Hardware[controller][button]).peek().to(Controller.Actions[action]); +} +mapToAction('Hydra', 'R3', 'ReticleClick'); +mapToAction('Hydra', 'R4', 'ContextMenu'); +mapping.enable(); + +var terminatingBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere + size: 0.011, + color: {red: 10, green: 10, blue: 255}, + alpha: 0.5, + solid: true, + visible: true +}); +var counter = 0; +function update() { + if (Controller.getValue(Controller.Standard.RT)) { return; } // Interferes with other scripts. + var hand = Controller.Standard.RightHand, + controllerPose = Controller.getPoseValue(hand); + if (!controllerPose.valid) { return; } // Controller is cradled. + var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), + MyAvatar.position); + // This gets point direction right, but if you want general quaternion it would be more complicated: + var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); + var hudPoint3d = HMD.calculateRayUICollisionPoint(controllerPosition, controllerDirection); + //Overlays.editOverlay(terminatingBall, {position: hudPoint3d}); // FIXME remove + if (!hudPoint3d) { return; } // E.g., parallel to the screen. + var hudPoint2d = HMD.overlayFromWorldPoint(hudPoint3d); + if (!(counter++ % 50)) debug(hudPoint3d, hudPoint2d); + Reticle.setPosition(hudPoint2d); +} + +var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. +var updater = Script.setInterval(update, UPDATE_INTERVAL); +Script.scriptEnding.connect(function () { + Overlays.deleteOverlay(terminatingBall); + Script.clearInterval(updater); + Script.clearInterval(settingsChecker); + mapping.disable(); +}); From 11e2e209e27bea2dd1577d657c201a77f418e56a Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 24 Apr 2016 20:32:21 -0700 Subject: [PATCH 02/33] Can use mouse for debugging/development when you don't have a working hydra. --- examples/controllers/handControllerPointer.js | 122 ++++++++++++++---- 1 file changed, 94 insertions(+), 28 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 2d8c9b4755..94cef25fab 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -1,3 +1,7 @@ +"use strict"; +/*jslint vars: true, plusplus: true*/ +/*globals Script, Overlays, Controller, Reticle, HMD, Camera, MyAvatar, Settings, Vec3, Quat, print */ + // // handControllerPointer.js // examples/controllers @@ -16,10 +20,31 @@ // Cursor all the time when uncradled. (E.g., not just when blue ray is on, or five seconds after movement, etc.) // Button 3 is left-mouse, button 4 is right-mouse. +var counter = 0, skip = 50; function debug() { // Display the arguments not just [Object object]. + if (skip && (counter++ % skip)) { return; } print.apply(null, [].map.call(arguments, JSON.stringify)); } +// Keep track of the vertical fieldOfView setting: +var DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees +var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds +var verticalFieldOfView = Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW; +var settingsChecker = Script.setInterval(function () { + verticalFieldOfView = Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW; +}, SETTINGS_CHANGE_RECHECK_INTERVAL); +Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); }); + +function getViewportDimensions() { + return Controller.getViewportDimensions(); +} + +// Define shimable functions for getting hand controller and setting cursor, for the normal +// case of having a hand controller. Alternative are at the bottom of the file. +var getControllerPose = Controller.getPoseValue; +var setCursor = Reticle.setPosition; + +// Generalized HUD utilities, with or without HDM: function calculateRayUICollisionPoint(position, direction) { // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection. if (HMD.active) { @@ -36,29 +61,24 @@ function calculateRayUICollisionPoint(position, direction) { var scale = numerator / denominator; return Vec3.sum(position, Vec3.multiply(scale, direction)); } -var DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees -var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds -var verticalHalfFieldOfView = (Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW) / 2; -var settingsChecker = Script.setInterval(function () { - verticalHalfFieldOfView = (Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW) / 2; -}, SETTINGS_CHANGE_RECHECK_INTERVAL); +var DEGREES_TO_HALF_RADIANS = Math.PI / 360; function overlayFromWorldPoint(point) { // Answer the 2d pixel-space location in the HUD that covers the given 3D point. if (HMD.active) { return HMD.overlayFromWorldPoint(point); } - // Find the yaw and pitch from camera to position, as a fraction of view frustrum, and multiply by current screen size. - var hudNormal = Quat.getFront(Camera.getOrientation()); var cameraToPoint = Vec3.subtract(point, Camera.getPosition()); - var eulerDegrees = Quat.safeEulerAngles(Quat.rotationBetween(hudNormal, cameraToPoint)); - var size = Reticle.maximumPosition; - var horizontalHalfFieldOfView = size.x * verticalHalfFieldOfView / size.y; - return { - x: size.x * (eulerDegrees.x / horizontalHalfFieldOfView + 0.5), - y: size.y * (eulerDegrees.y / verticalHalfFieldOfView + 0.5) - }; + var cameraX = Vec3.dot(cameraToPoint, Quat.getRight(Camera.getOrientation())); + var cameraY = Vec3.dot(cameraToPoint, Quat.getUp(Camera.getOrientation())); + var size = getViewportDimensions(); + var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); + var hudWidth = hudHeight * size.x / size.y; + var horizontalPixels = size.x * (cameraX / hudWidth + 0.5); + var verticalPixels = size.y * (1 - (cameraY / hudHeight + 0.5)); + return { x: horizontalPixels, y: verticalPixels }; } +// Synthesize left and right mouse click from controller: var MAPPING_NAME = Script.resolvePath(''); var mapping = Controller.newMapping(MAPPING_NAME); function mapToAction(controller, button, action) { @@ -68,30 +88,30 @@ function mapToAction(controller, button, action) { mapToAction('Hydra', 'R3', 'ReticleClick'); mapToAction('Hydra', 'R4', 'ContextMenu'); mapping.enable(); +Script.scriptEnding.connect(mapping.disable); var terminatingBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere size: 0.011, color: {red: 10, green: 10, blue: 255}, - alpha: 0.5, + alpha: 0.8, solid: true, visible: true }); -var counter = 0; + function update() { if (Controller.getValue(Controller.Standard.RT)) { return; } // Interferes with other scripts. - var hand = Controller.Standard.RightHand, - controllerPose = Controller.getPoseValue(hand); + var hand = Controller.Standard.RightHand; + var controllerPose = getControllerPose(hand); if (!controllerPose.valid) { return; } // Controller is cradled. var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); - // This gets point direction right, but if you want general quaternion it would be more complicated: - var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); - var hudPoint3d = HMD.calculateRayUICollisionPoint(controllerPosition, controllerDirection); - //Overlays.editOverlay(terminatingBall, {position: hudPoint3d}); // FIXME remove + // This gets point direction right, but if you want general quaternion it would be more complicated: + var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); + + var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); if (!hudPoint3d) { return; } // E.g., parallel to the screen. - var hudPoint2d = HMD.overlayFromWorldPoint(hudPoint3d); - if (!(counter++ % 50)) debug(hudPoint3d, hudPoint2d); - Reticle.setPosition(hudPoint2d); + Overlays.editOverlay(terminatingBall, {position: hudPoint3d}); + setCursor(overlayFromWorldPoint(hudPoint3d)); } var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. @@ -99,6 +119,52 @@ var updater = Script.setInterval(update, UPDATE_INTERVAL); Script.scriptEnding.connect(function () { Overlays.deleteOverlay(terminatingBall); Script.clearInterval(updater); - Script.clearInterval(settingsChecker); - mapping.disable(); }); + + +// The rest of this is for debugging without working hand controllers, using a line from camera to mouse, and an image for cursor. +var CONTROLLER_ROTATION = Quat.fromPitchYawRollDegrees(90, 180, -90); +if (!Controller.Hardware.Hydra) { + var mouseKeeper = {x: 0, y: 0}; + var onMouseMove = function (event) { mouseKeeper.x = event.x; mouseKeeper.y = event.y; }; + Controller.mouseMoveEvent.connect(onMouseMove); + Script.scriptEnding.connect(function () { Controller.mouseMoveEvent.disconnect(onMouseMove); }); + getControllerPose = function () { + var size = getViewportDimensions(); + var handPoint = Vec3.subtract(Camera.getPosition(), MyAvatar.position); // Pretend controller is at camera + + // In world-space 3D meters: + var rotation = Camera.getOrientation(); + var normal = Quat.getFront(rotation); + var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); + var hudWidth = hudHeight * size.x / size.y; + var rightFraction = mouseKeeper.x / size.x - 0.5; + var rightMeters = rightFraction * hudWidth; + var upFraction = mouseKeeper.y / size.y - 0.5; + var upMeters = upFraction * hudHeight * -1; + var right = Vec3.multiply(Quat.getRight(rotation), rightMeters); + var up = Vec3.multiply(Quat.getUp(rotation), upMeters); + var direction = Vec3.sum(normal, Vec3.sum(right, up)); + var mouseRotation = Quat.rotationBetween(normal, direction); + + var controllerRotation = Quat.multiply(Quat.multiply(mouseRotation, rotation), CONTROLLER_ROTATION); + var inverseAvatar = Quat.inverse(MyAvatar.orientation); + return { + valid: true, + translation: Vec3.multiplyQbyV(inverseAvatar, handPoint), + rotation: Quat.multiply(inverseAvatar, controllerRotation) + }; + }; + // We can't set the mouse if we're using the mouse as a fake controller. So stick an image where we would be putting the mouse. + var reticleHalfSize = 16; + var fakeReticle = Overlays.addOverlay("image", { + imageURL: "http://s3.amazonaws.com/hifi-public/images/delete.png", + width: 2 * reticleHalfSize, + height: 2 * reticleHalfSize, + alpha: 0.7 + }); + Script.scriptEnding.connect(function () { Overlays.deleteOverlay(fakeReticle); }); + setCursor = function (hudPoint2d) { + Overlays.editOverlay(fakeReticle, {x: hudPoint2d.x - reticleHalfSize, y: hudPoint2d.y - reticleHalfSize}); + }; +} From 24f78eafa405a384635a0aebaf834ff8a09cc018 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 25 Apr 2016 09:15:53 -0700 Subject: [PATCH 03/33] checkpoint --- examples/controllers/handControllerPointer.js | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 94cef25fab..b66ac5d7d8 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -35,12 +35,9 @@ var settingsChecker = Script.setInterval(function () { }, SETTINGS_CHANGE_RECHECK_INTERVAL); Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); }); -function getViewportDimensions() { - return Controller.getViewportDimensions(); -} -// Define shimable functions for getting hand controller and setting cursor, for the normal -// case of having a hand controller. Alternative are at the bottom of the file. +// Define shimmable functions for getting hand controller and setting cursor, accomodating +// the normal case of having a hand controller and the alternative at the bottom of this file. var getControllerPose = Controller.getPoseValue; var setCursor = Reticle.setPosition; @@ -70,7 +67,7 @@ function overlayFromWorldPoint(point) { var cameraToPoint = Vec3.subtract(point, Camera.getPosition()); var cameraX = Vec3.dot(cameraToPoint, Quat.getRight(Camera.getOrientation())); var cameraY = Vec3.dot(cameraToPoint, Quat.getUp(Camera.getOrientation())); - var size = getViewportDimensions(); + var size = Controller.getViewportDimensions(); var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); var hudWidth = hudHeight * size.x / size.y; var horizontalPixels = size.x * (cameraX / hudWidth + 0.5); @@ -90,12 +87,24 @@ mapToAction('Hydra', 'R4', 'ContextMenu'); mapping.enable(); Script.scriptEnding.connect(mapping.disable); + +// Here's the meat: + +var LASER_COLOR = {red: 10, green: 10, blue: 255}; var terminatingBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere size: 0.011, - color: {red: 10, green: 10, blue: 255}, - alpha: 0.8, - solid: true, - visible: true + color: LASER_COLOR, + ignoreRayIntersection: true, + alpha: 0.8, // handControllerGrab has this as 0.5, but I have trouble seeing that. + visible: true, + solid: true +}); +var laserLine = Overlays.addOverlay("line3d", { // same properties as handControllerGrab search line + lineWidth: 5, + color: LASER_COLOR, + ignoreRayIntersection: true, + visible: true, + alpha: 1 }); function update() { @@ -111,6 +120,7 @@ function update() { var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); if (!hudPoint3d) { return; } // E.g., parallel to the screen. Overlays.editOverlay(terminatingBall, {position: hudPoint3d}); + Overlays.editOverlay(laserLine, {start: controllerPosition, end: hudPoint3d}); setCursor(overlayFromWorldPoint(hudPoint3d)); } @@ -118,19 +128,20 @@ var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. var updater = Script.setInterval(update, UPDATE_INTERVAL); Script.scriptEnding.connect(function () { Overlays.deleteOverlay(terminatingBall); + Overlays.deleteOverlay(laserLine); Script.clearInterval(updater); }); - +// ------------------------------------------------------------------------------------------------------------------------------- // The rest of this is for debugging without working hand controllers, using a line from camera to mouse, and an image for cursor. var CONTROLLER_ROTATION = Quat.fromPitchYawRollDegrees(90, 180, -90); -if (!Controller.Hardware.Hydra) { +if (!Controller.Hardware.Hydra) { // Check is made at script load time, not continuously while running. var mouseKeeper = {x: 0, y: 0}; var onMouseMove = function (event) { mouseKeeper.x = event.x; mouseKeeper.y = event.y; }; Controller.mouseMoveEvent.connect(onMouseMove); Script.scriptEnding.connect(function () { Controller.mouseMoveEvent.disconnect(onMouseMove); }); getControllerPose = function () { - var size = getViewportDimensions(); + var size = Controller.getViewportDimensions(); var handPoint = Vec3.subtract(Camera.getPosition(), MyAvatar.position); // Pretend controller is at camera // In world-space 3D meters: @@ -155,7 +166,13 @@ if (!Controller.Hardware.Hydra) { rotation: Quat.multiply(inverseAvatar, controllerRotation) }; }; + // We can't set the mouse if we're using the mouse as a fake controller. So stick an image where we would be putting the mouse. + // WARNING: This fake cursor is an overlay that will be the target of clicks and drags rather than other overlays underneath it! + if (true) { // Don't do the overlay, but do turn off cursor warping, which would be circular. + setCursor = function () { }; + return; + } var reticleHalfSize = 16; var fakeReticle = Overlays.addOverlay("image", { imageURL: "http://s3.amazonaws.com/hifi-public/images/delete.png", From 570d4a4a163429b83b14581ff937de0c7b04010c Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 25 Apr 2016 15:13:23 -0700 Subject: [PATCH 04/33] checkpoint "demo" --- examples/controllers/handControllerPointer.js | 131 ++++++++++++++---- 1 file changed, 107 insertions(+), 24 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index b66ac5d7d8..1fbe5cbed0 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -20,13 +20,23 @@ // Cursor all the time when uncradled. (E.g., not just when blue ray is on, or five seconds after movement, etc.) // Button 3 is left-mouse, button 4 is right-mouse. +// UTILITIES ------------- +// var counter = 0, skip = 50; function debug() { // Display the arguments not just [Object object]. if (skip && (counter++ % skip)) { return; } print.apply(null, [].map.call(arguments, JSON.stringify)); } -// Keep track of the vertical fieldOfView setting: +// Utility to make it easier to setup and disconnect cleanly. +function setupHandler(event, handler) { + event.connect(handler); + Script.scriptEnding.connect(function () { event.disconnect(handler); }); +} + +// VERTICAL FIELD OF VIEW --------- +// +// Cache the verticalFieldOfView setting and update it every so often. var DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds var verticalFieldOfView = Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW; @@ -35,13 +45,20 @@ var settingsChecker = Script.setInterval(function () { }, SETTINGS_CHANGE_RECHECK_INTERVAL); Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); }); - -// Define shimmable functions for getting hand controller and setting cursor, accomodating -// the normal case of having a hand controller and the alternative at the bottom of this file. +// SHIMS ---------- +// +// Define shimable functions for getting hand controller and setting cursor, for the normal +// case of having a hand controller. Alternative are at the bottom of the file. var getControllerPose = Controller.getPoseValue; -var setCursor = Reticle.setPosition; +var setCursor = function (point2d) { + if (!HMD.active) { + // FIX BUG: The width of the window title bar (on Windows, anyway). + point2d = {x: point2d.x, y: point2d.y + 50}; + } + Reticle.setPosition(point2d); +} -// Generalized HUD utilities, with or without HDM: +// Generalized HUD utilities, with or without HMD: function calculateRayUICollisionPoint(position, direction) { // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection. if (HMD.active) { @@ -61,6 +78,10 @@ function calculateRayUICollisionPoint(position, direction) { var DEGREES_TO_HALF_RADIANS = Math.PI / 360; function overlayFromWorldPoint(point) { // Answer the 2d pixel-space location in the HUD that covers the given 3D point. + // REQUIRES: that the 3d point be on the hud surface! + // Note that this is based on the Camera, and doesn't know anything about any + // ray that may or may not have been used to compute the point. E.g., the + // overlay point is NOT the intersection of some non-camera ray with the HUD. if (HMD.active) { return HMD.overlayFromWorldPoint(point); } @@ -70,11 +91,15 @@ function overlayFromWorldPoint(point) { var size = Controller.getViewportDimensions(); var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); var hudWidth = hudHeight * size.x / size.y; - var horizontalPixels = size.x * (cameraX / hudWidth + 0.5); - var verticalPixels = size.y * (1 - (cameraY / hudHeight + 0.5)); + var horizontalFraction = (cameraX / hudWidth + 0.5); + var verticalFraction = 1 - (cameraY / hudHeight + 0.5); + var horizontalPixels = size.x * horizontalFraction; + var verticalPixels = size.y * verticalFraction; return { x: horizontalPixels, y: verticalPixels }; } +// CONTROLLER MAPPING --------- +// // Synthesize left and right mouse click from controller: var MAPPING_NAME = Script.resolvePath(''); var mapping = Controller.newMapping(MAPPING_NAME); @@ -88,27 +113,54 @@ mapping.enable(); Script.scriptEnding.connect(mapping.disable); -// Here's the meat: +// MOUSE LOCKOUT -------- +// +var MOUSE_MOVE_LOCKOUT_TIME = 1000; //fixme 5000; // milliseconds after mouse movement before hand controller can move pointer. +var mouseMoved = 0, weMovedReticle = false; +if (Controller.Hardware.Hydra) { + function onMouseMove(event) { + if (weMovedReticle) { weMovedReticle = false; return; } + Reticle.visible = true; + mouseMoved = Date.now(); + } + setupHandler(Controller.mouseMoveEvent, onMouseMove); +} +// MAIN OPERATIONS ----------- +// var LASER_COLOR = {red: 10, green: 10, blue: 255}; var terminatingBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere - size: 0.011, + size: 0.1, //FIXME 0.011, color: LASER_COLOR, ignoreRayIntersection: true, alpha: 0.8, // handControllerGrab has this as 0.5, but I have trouble seeing that. - visible: true, + visible: false, solid: true }); var laserLine = Overlays.addOverlay("line3d", { // same properties as handControllerGrab search line lineWidth: 5, + // BUG: If you don't supply a start and end at creation, it will never show up, even after editing. + start: MyAvatar.position, + end: Vec3.ZERO, color: LASER_COLOR, ignoreRayIntersection: true, - visible: true, + visible: false, alpha: 1 }); +var overlays = [terminatingBall, laserLine]; +var isOn = true; +function turnOffLaser() { + if (!isOn) { return; } + isOn = true; + overlays.forEach(function (overlay) { + Overlays.editOverlay(overlay, {visible: false}); + }); +} +var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. function update() { - if (Controller.getValue(Controller.Standard.RT)) { return; } // Interferes with other scripts. + if ((Date.now() - mouseMoved) < MOUSE_MOVE_LOCKOUT_TIME) { return turnOffLaser(); } // Let them use it in peace. + if (Controller.getValue(Controller.Standard.RT)) { return turnOffLaser(); } // Interferes with other scripts. var hand = Controller.Standard.RightHand; var controllerPose = getControllerPose(hand); if (!controllerPose.valid) { return; } // Controller is cradled. @@ -118,28 +170,60 @@ function update() { var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); - if (!hudPoint3d) { return; } // E.g., parallel to the screen. - Overlays.editOverlay(terminatingBall, {position: hudPoint3d}); - Overlays.editOverlay(laserLine, {start: controllerPosition, end: hudPoint3d}); - setCursor(overlayFromWorldPoint(hudPoint3d)); + if (!hudPoint3d) { print('Controller is parallel to HUD'); return turnLaserOff(); } + var hudPoint2d = overlayFromWorldPoint(hudPoint3d); + setCursor(hudPoint2d); + weMovedReticle = true; + + // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. + if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(hudPoint2d)) { + Reticle.visible = true; + return turnOffLaser(); + } + // Otherwise, show the laser and intersect it with 3d overlays and entities. + var pickRay = {origin: controllerPosition, direction: controllerDirection}; + var result = Overlays.findRayIntersection(pickRay) + if (!result.intersects) { + result = Entities.findRayIntersection(pickRay, true); + } + var termination = result.intersects ? + result.intersection : + Vec3.sum(controllerPosition, Vec3.multiply(MAX_RAY_SCALE, controllerDirection)); + isOn = true; + Overlays.editOverlay(terminatingBall, {visible: true, position: termination}); + Overlays.editOverlay(laserLine, {visible: true, start: controllerPosition, end: termination}); + /* + // Hack: Move the pointer again, this time to the intersection. This allows "clicking" on + // 2D and 3D entities without rewriting other parts of the system, but it isn't right, + // because the line from camera to the new mouse position might intersect different things + // than the line from controllerPosition to termination. + var eye = Camera.getPosition(); + var apparentHudTermination3d = calculateRayUICollisionPoint(eye, Vec3.subtract(termination, eye)); + var apparentHudTermination2d = overlayFromWorldPoint(apparentHudTermination3d); + Overlays.editOverlay(fakeReticle, {x: apparentHudTermination2d.x - reticleHalfSize, y: apparentHudTermination2d.y - reticleHalfSize}); + //Reticle.visible = false; + weMovedReticle = true; + setCursor(apparentHudTermination2d); +*/ } var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. var updater = Script.setInterval(update, UPDATE_INTERVAL); Script.scriptEnding.connect(function () { Overlays.deleteOverlay(terminatingBall); - Overlays.deleteOverlay(laserLine); Script.clearInterval(updater); }); -// ------------------------------------------------------------------------------------------------------------------------------- + +// DEBUGGING WITHOUT HYDRA ----------------------- +// // The rest of this is for debugging without working hand controllers, using a line from camera to mouse, and an image for cursor. var CONTROLLER_ROTATION = Quat.fromPitchYawRollDegrees(90, 180, -90); -if (!Controller.Hardware.Hydra) { // Check is made at script load time, not continuously while running. +if (!Controller.Hardware.Hydra) { + print('WARNING: no hand controller detected. Using mouse!'); var mouseKeeper = {x: 0, y: 0}; - var onMouseMove = function (event) { mouseKeeper.x = event.x; mouseKeeper.y = event.y; }; - Controller.mouseMoveEvent.connect(onMouseMove); - Script.scriptEnding.connect(function () { Controller.mouseMoveEvent.disconnect(onMouseMove); }); + var onMouseMoveCapture = function (event) { mouseKeeper.x = event.x; mouseKeeper.y = event.y; }; + setupHandler(Controller.mouseMoveEvent, onMouseMoveCapture); getControllerPose = function () { var size = Controller.getViewportDimensions(); var handPoint = Vec3.subtract(Camera.getPosition(), MyAvatar.position); // Pretend controller is at camera @@ -166,7 +250,6 @@ if (!Controller.Hardware.Hydra) { // Check is made at script load time, not con rotation: Quat.multiply(inverseAvatar, controllerRotation) }; }; - // We can't set the mouse if we're using the mouse as a fake controller. So stick an image where we would be putting the mouse. // WARNING: This fake cursor is an overlay that will be the target of clicks and drags rather than other overlays underneath it! if (true) { // Don't do the overlay, but do turn off cursor warping, which would be circular. From 33c491bc5d4261dffcf6d92f407d0d5999a06949 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 25 Apr 2016 17:05:03 -0700 Subject: [PATCH 05/33] checkpoint --- examples/controllers/handControllerPointer.js | 82 +++++++++++++------ 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 1fbe5cbed0..6fe4d701f5 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -109,6 +109,10 @@ function mapToAction(controller, button, action) { } mapToAction('Hydra', 'R3', 'ReticleClick'); mapToAction('Hydra', 'R4', 'ContextMenu'); +mapToAction('Hydra', 'L3', 'ReticleClick'); +mapToAction('Hydra', 'L4', 'ContextMenu'); +mapToAction('Vive', 'LeftPrimaryThumb', 'ReticleClick'); +mapToAction('Vive', 'RightPrimaryThumb', 'ReticleClick'); mapping.enable(); Script.scriptEnding.connect(mapping.disable); @@ -126,11 +130,10 @@ if (Controller.Hardware.Hydra) { setupHandler(Controller.mouseMoveEvent, onMouseMove); } -// MAIN OPERATIONS ----------- -// +// VISUAL AID ----------- var LASER_COLOR = {red: 10, green: 10, blue: 255}; var terminatingBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere - size: 0.1, //FIXME 0.011, + size: 0.011, color: LASER_COLOR, ignoreRayIntersection: true, alpha: 0.8, // handControllerGrab has this as 0.5, but I have trouble seeing that. @@ -147,20 +150,55 @@ var laserLine = Overlays.addOverlay("line3d", { // same properties as handContro visible: false, alpha: 1 }); -var overlays = [terminatingBall, laserLine]; -var isOn = true; +var overlays = [terminatingBall, laserLine] +Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); +var visualizationOn = true; // Not whether it desired, but simply whether it is. Just an optimization. +var VISUALIZATION_TOGGLE_LOCKOUT_TIME = 1000; // milliseconds +var wasToggled = 0, wantsVisualization = false; function turnOffLaser() { - if (!isOn) { return; } - isOn = true; + if (!wasToggled) { wasToggled = Date.now(); } + if (!visualizationOn) { return; } + visualizationOn = false; overlays.forEach(function (overlay) { Overlays.editOverlay(overlay, {visible: false}); }); } - var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. +function updateLaser(controllerPosition, controllerDirection) { + // Show the laser and intersect it with 3d overlays and entities. + var pickRay = {origin: controllerPosition, direction: controllerDirection}; + var result = Overlays.findRayIntersection(pickRay) + if (!result.intersects) { + result = Entities.findRayIntersection(pickRay, true); + } + var termination = result.intersects ? + result.intersection : + Vec3.sum(controllerPosition, Vec3.multiply(MAX_RAY_SCALE, controllerDirection)); + visualizationOn = true; + Overlays.editOverlay(terminatingBall, {visible: true, position: termination}); + Overlays.editOverlay(laserLine, {visible: true, start: controllerPosition, end: termination}); +} +function maybeToggleVisualization(trigger, now) { + if (trigger > 0) { + print('TRIGGER', now - wasToggled); + if ((now - wasToggled) > VISUALIZATION_TOGGLE_LOCKOUT_TIME) { + wantsVisualization = !wantsVisualization; + wasToggled = Date.now(); + } + } else { + wasToggled = 0; + } +} + + +// MAIN OPERATIONS ----------- +// function update() { - if ((Date.now() - mouseMoved) < MOUSE_MOVE_LOCKOUT_TIME) { return turnOffLaser(); } // Let them use it in peace. - if (Controller.getValue(Controller.Standard.RT)) { return turnOffLaser(); } // Interferes with other scripts. + var now = Date.now(); + if ((now - mouseMoved) < MOUSE_MOVE_LOCKOUT_TIME) { return turnOffLaser(); } // Let them use it in peace. + var trigger = Controller.getValue(Controller.Standard.RT); + if (trigger > 0.5) { print('FULL TRIGGER');return turnOffLaser(); } // Interferes with other scripts. + maybeToggleVisualization(trigger, now); var hand = Controller.Standard.RightHand; var controllerPose = getControllerPose(hand); if (!controllerPose.valid) { return; } // Controller is cradled. @@ -172,26 +210,23 @@ function update() { var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); if (!hudPoint3d) { print('Controller is parallel to HUD'); return turnLaserOff(); } var hudPoint2d = overlayFromWorldPoint(hudPoint3d); + // We don't know yet if we'll want to make the cursor visble, but we need to move it to see if + // it's pointing at a QML tool (aka system overlay). setCursor(hudPoint2d); weMovedReticle = true; // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(hudPoint2d)) { Reticle.visible = true; + //mapping.enable(); return turnOffLaser(); } - // Otherwise, show the laser and intersect it with 3d overlays and entities. - var pickRay = {origin: controllerPosition, direction: controllerDirection}; - var result = Overlays.findRayIntersection(pickRay) - if (!result.intersects) { - result = Entities.findRayIntersection(pickRay, true); + //mapping.disable(); + if (wantsVisualization) { + updateLaser(controllerPosition, controllerDirection); + } else { + Reticle.visible = false; } - var termination = result.intersects ? - result.intersection : - Vec3.sum(controllerPosition, Vec3.multiply(MAX_RAY_SCALE, controllerDirection)); - isOn = true; - Overlays.editOverlay(terminatingBall, {visible: true, position: termination}); - Overlays.editOverlay(laserLine, {visible: true, start: controllerPosition, end: termination}); /* // Hack: Move the pointer again, this time to the intersection. This allows "clicking" on // 2D and 3D entities without rewriting other parts of the system, but it isn't right, @@ -209,10 +244,7 @@ function update() { var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. var updater = Script.setInterval(update, UPDATE_INTERVAL); -Script.scriptEnding.connect(function () { - Overlays.deleteOverlay(terminatingBall); - Script.clearInterval(updater); -}); +Script.scriptEnding.connect(function () { Script.clearInterval(updater); }); // DEBUGGING WITHOUT HYDRA ----------------------- From 58b4187f227d11b1316170f3903e07e2e4fdde79 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 26 Apr 2016 09:21:40 -0700 Subject: [PATCH 06/33] Cleanup of non-hydra code. --- examples/controllers/handControllerPointer.js | 141 +++++++++++------- 1 file changed, 88 insertions(+), 53 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 6fe4d701f5..7cbf92282b 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -1,6 +1,6 @@ "use strict"; /*jslint vars: true, plusplus: true*/ -/*globals Script, Overlays, Controller, Reticle, HMD, Camera, MyAvatar, Settings, Vec3, Quat, print */ +/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Vec3, Quat, print */ // // handControllerPointer.js @@ -29,10 +29,21 @@ function debug() { // Display the arguments not just [Object object]. } // Utility to make it easier to setup and disconnect cleanly. -function setupHandler(event, handler) { +function setupHandler(event, handler) { event.connect(handler); Script.scriptEnding.connect(function () { event.disconnect(handler); }); } +// If some capability is not available until expiration milliseconds after the last update. +function TimeLock(expiration) { + var last = 0; + this.update = function (optionalNow) { + last = optionalNow || Date.now(); + }; + this.available = function (optionalNow) { + return ((optionalNow || Date.now()) - last) > expiration; + }; +} +var lockOut = new TimeLock(1000); // VERTICAL FIELD OF VIEW --------- // @@ -47,16 +58,18 @@ Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); // SHIMS ---------- // -// Define shimable functions for getting hand controller and setting cursor, for the normal -// case of having a hand controller. Alternative are at the bottom of the file. +// Define customizable versions of some standard operators. Alternative are at the bottom of the file. var getControllerPose = Controller.getPoseValue; var setCursor = function (point2d) { if (!HMD.active) { - // FIX BUG: The width of the window title bar (on Windows, anyway). - point2d = {x: point2d.x, y: point2d.y + 50}; + // FIX BUG: The width of the window title bar (on Windows, anyway). + point2d = {x: point2d.x, y: point2d.y + 50}; } Reticle.setPosition(point2d); -} +}; +var setReticleVisible = function (on) { Reticle.visible = on; }; +var getOverlayAtPoint = Overlays.getOverlayAtPoint; +var getValue = Controller.getValue; // Generalized HUD utilities, with or without HMD: function calculateRayUICollisionPoint(position, direction) { @@ -116,23 +129,20 @@ mapToAction('Vive', 'RightPrimaryThumb', 'ReticleClick'); mapping.enable(); Script.scriptEnding.connect(mapping.disable); - // MOUSE LOCKOUT -------- // -var MOUSE_MOVE_LOCKOUT_TIME = 1000; //fixme 5000; // milliseconds after mouse movement before hand controller can move pointer. -var mouseMoved = 0, weMovedReticle = false; +var weMovedReticle = false; if (Controller.Hardware.Hydra) { - function onMouseMove(event) { - if (weMovedReticle) { weMovedReticle = false; return; } - Reticle.visible = true; - mouseMoved = Date.now(); - } - setupHandler(Controller.mouseMoveEvent, onMouseMove); + setupHandler(Controller.mouseMoveEvent, function () { + if (weMovedReticle) { weMovedReticle = false; return; } + setReticleVisible(true); + lockOut.update(); + }); } // VISUAL AID ----------- var LASER_COLOR = {red: 10, green: 10, blue: 255}; -var terminatingBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere +var laserBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere size: 0.011, color: LASER_COLOR, ignoreRayIntersection: true, @@ -150,54 +160,59 @@ var laserLine = Overlays.addOverlay("line3d", { // same properties as handContro visible: false, alpha: 1 }); -var overlays = [terminatingBall, laserLine] +var overlays = [laserBall, laserLine]; Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); var visualizationOn = true; // Not whether it desired, but simply whether it is. Just an optimization. -var VISUALIZATION_TOGGLE_LOCKOUT_TIME = 1000; // milliseconds -var wasToggled = 0, wantsVisualization = false; function turnOffLaser() { - if (!wasToggled) { wasToggled = Date.now(); } if (!visualizationOn) { return; } visualizationOn = false; overlays.forEach(function (overlay) { - Overlays.editOverlay(overlay, {visible: false}); + Overlays.editOverlay(overlay, {visible: false}); }); } var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. -function updateLaser(controllerPosition, controllerDirection) { +var wantsVisualization = false; +function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { + if (!wantsVisualization) { return false; } // Show the laser and intersect it with 3d overlays and entities. var pickRay = {origin: controllerPosition, direction: controllerDirection}; - var result = Overlays.findRayIntersection(pickRay) + var result = Overlays.findRayIntersection(pickRay); if (!result.intersects) { - result = Entities.findRayIntersection(pickRay, true); + result = Entities.findRayIntersection(pickRay, true); } - var termination = result.intersects ? - result.intersection : - Vec3.sum(controllerPosition, Vec3.multiply(MAX_RAY_SCALE, controllerDirection)); + if (!visualizationOn) { setReticleVisible(true); } visualizationOn = true; - Overlays.editOverlay(terminatingBall, {visible: true, position: termination}); + var termination = result.intersects ? + result.intersection : + Vec3.sum(controllerPosition, Vec3.multiply(MAX_RAY_SCALE, controllerDirection)); Overlays.editOverlay(laserLine, {visible: true, start: controllerPosition, end: termination}); + // We show the ball at the hud intersection rather than at the termination because: + // 1) As you swing the laser in space, it's hard to judge where it will intersect with a HUD element, + // unless the intersection of the laser with the HUD is marked. But it's confusing to do that + // with the pointer, so we use the ball. + // 2) On some objects, the intersection is just enough inside the object that we're not going to see + // the ball anyway. + Overlays.editOverlay(laserBall, {visible: true, position: hudPosition3d}); + return true; } +var toggleLockout = new TimeLock(500); function maybeToggleVisualization(trigger, now) { - if (trigger > 0) { - print('TRIGGER', now - wasToggled); - if ((now - wasToggled) > VISUALIZATION_TOGGLE_LOCKOUT_TIME) { - wantsVisualization = !wantsVisualization; - wasToggled = Date.now(); - } + if (!trigger) { return; } + if (toggleLockout.available(now)) { + wantsVisualization = !wantsVisualization; + print('Toggled visualization', wantsVisualization ? 'on' : 'off'); } else { - wasToggled = 0; + toggleLockout.update(now); } } - // MAIN OPERATIONS ----------- // function update() { var now = Date.now(); - if ((now - mouseMoved) < MOUSE_MOVE_LOCKOUT_TIME) { return turnOffLaser(); } // Let them use it in peace. - var trigger = Controller.getValue(Controller.Standard.RT); - if (trigger > 0.5) { print('FULL TRIGGER');return turnOffLaser(); } // Interferes with other scripts. + if (!lockOut.available(now)) { return turnOffLaser(); } // Let them use it in peace. + var trigger = getValue(Controller.Standard.RT); + if (trigger > 0.5) { lockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. maybeToggleVisualization(trigger, now); var hand = Controller.Standard.RightHand; var controllerPose = getControllerPose(hand); @@ -208,7 +223,7 @@ function update() { var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); - if (!hudPoint3d) { print('Controller is parallel to HUD'); return turnLaserOff(); } + if (!hudPoint3d) { print('Controller is parallel to HUD'); return turnOffLaser(); } var hudPoint2d = overlayFromWorldPoint(hudPoint3d); // We don't know yet if we'll want to make the cursor visble, but we need to move it to see if // it's pointing at a QML tool (aka system overlay). @@ -216,16 +231,16 @@ function update() { weMovedReticle = true; // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. - if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(hudPoint2d)) { - Reticle.visible = true; - //mapping.enable(); - return turnOffLaser(); + if (Reticle.pointingAtSystemOverlay || getOverlayAtPoint(hudPoint2d)) { + setReticleVisible(true); + //mapping.enable(); + return turnOffLaser(); } + // We are not pointing at a HUD element (but it could be a 3d overlay). //mapping.disable(); - if (wantsVisualization) { - updateLaser(controllerPosition, controllerDirection); - } else { - Reticle.visible = false; + if (!updateLaser(controllerPosition, controllerDirection, hudPoint3d)) { + setReticleVisible(false); + turnOffLaser(); } /* // Hack: Move the pointer again, this time to the intersection. This allows "clicking" on @@ -284,10 +299,6 @@ if (!Controller.Hardware.Hydra) { }; // We can't set the mouse if we're using the mouse as a fake controller. So stick an image where we would be putting the mouse. // WARNING: This fake cursor is an overlay that will be the target of clicks and drags rather than other overlays underneath it! - if (true) { // Don't do the overlay, but do turn off cursor warping, which would be circular. - setCursor = function () { }; - return; - } var reticleHalfSize = 16; var fakeReticle = Overlays.addOverlay("image", { imageURL: "http://s3.amazonaws.com/hifi-public/images/delete.png", @@ -299,4 +310,28 @@ if (!Controller.Hardware.Hydra) { setCursor = function (hudPoint2d) { Overlays.editOverlay(fakeReticle, {x: hudPoint2d.x - reticleHalfSize, y: hudPoint2d.y - reticleHalfSize}); }; + setReticleVisible = function (on) { + Reticle.visible = on; // BUG: doesn't work on mac. + Overlays.editOverlay(fakeReticle, {visible: on}); + }; + // The idea here is that we not return a truthy result constantly when we display the fake reticle. + // But this is done wrong when we're over another overlay as well: if we hit the fakeReticle, we incorrectly answer null here. + // FIXME: display fake reticle slightly off to the side instead. + getOverlayAtPoint = function (point2d) { + var overlay = Overlays.getOverlayAtPoint(point2d); + if (overlay === fakeReticle) { return null; } + return overlay; + }; + var fakeTrigger = 0; + getValue = function () { var trigger = fakeTrigger; fakeTrigger = 0; return trigger; }; + setupHandler(Controller.keyPressEvent, function (event) { + switch (event.text) { + case '`': + fakeTrigger = 0.4; + break; + case '~': + fakeTrigger = 0.9; + break; + } + }); } From 53d7b57d49672c93441818d8ea0cbe202c02464b Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 26 Apr 2016 14:30:20 -0700 Subject: [PATCH 07/33] checkpoint --- examples/controllers/handControllerPointer.js | 128 +++++++++++++----- 1 file changed, 92 insertions(+), 36 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 7cbf92282b..0a06d2b7a3 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -13,12 +13,29 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +// Control the "mouse" using hand controller. (HMD and desktop.) // For now: -// Right hand only. -// HMD only. (Desktop isn't turned off, but right now it's using -// HMD.overlayFromWorldPoint(HMD.calculateRayUICollisionPoint ...) without compensation.) -// Cursor all the time when uncradled. (E.g., not just when blue ray is on, or five seconds after movement, etc.) // Button 3 is left-mouse, button 4 is right-mouse. +// First-person only. +// Partial trigger squeeze toggles a laser visualization. When on, you can also click on objects in-world, not just HUD. +// On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) +// +// Bugs: +// Turn in-world click off when visualization is off. +// May also interfere with other scripts? + +// Right hand only. +// Do not use with depthReticle.js. +// Trigger toggle is flakey. +// When clicking on in-world objects, the click acts on the red ball, not the termination of the blue line. +/* +ScriptDiscoveryService.getRunning().forEach(function (script) { + if (script.name === 'depthReticle.js') { + print("Stopping script", script.name); + script.stop(); + } +}); +*/ // UTILITIES ------------- // @@ -43,7 +60,20 @@ function TimeLock(expiration) { return ((optionalNow || Date.now()) - last) > expiration; }; } -var lockOut = new TimeLock(1000); +var lockOut = new TimeLock(2000); + +// Calls onFunction() or offFunction() when swtich(on), but only if it is to a new value. +function LatchedToggle(onFunction, offFunction, state) { + this.setState = function (on) { + if (state === on) { return; } + state = on; + if (on) { + onFunction(); + } else { + offFunction(); + } + }; +} // VERTICAL FIELD OF VIEW --------- // @@ -62,7 +92,7 @@ Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); var getControllerPose = Controller.getPoseValue; var setCursor = function (point2d) { if (!HMD.active) { - // FIX BUG: The width of the window title bar (on Windows, anyway). + // FIX SYSEM BUG: On Windows, setPosition is setting relative to screen origin, not the content area of the window. point2d = {x: point2d.x, y: point2d.y + 50}; } Reticle.setPosition(point2d); @@ -126,8 +156,8 @@ mapToAction('Hydra', 'L3', 'ReticleClick'); mapToAction('Hydra', 'L4', 'ContextMenu'); mapToAction('Vive', 'LeftPrimaryThumb', 'ReticleClick'); mapToAction('Vive', 'RightPrimaryThumb', 'ReticleClick'); -mapping.enable(); Script.scriptEnding.connect(mapping.disable); +toggleMap = new LatchedToggle(mapping.enable, mapping.disable); // MOUSE LOCKOUT -------- // @@ -135,6 +165,7 @@ var weMovedReticle = false; if (Controller.Hardware.Hydra) { setupHandler(Controller.mouseMoveEvent, function () { if (weMovedReticle) { weMovedReticle = false; return; } + // Should we implement dephReticle's inactivity timeout here? setReticleVisible(true); lockOut.update(); }); @@ -142,17 +173,9 @@ if (Controller.Hardware.Hydra) { // VISUAL AID ----------- var LASER_COLOR = {red: 10, green: 10, blue: 255}; -var laserBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere - size: 0.011, - color: LASER_COLOR, - ignoreRayIntersection: true, - alpha: 0.8, // handControllerGrab has this as 0.5, but I have trouble seeing that. - visible: false, - solid: true -}); var laserLine = Overlays.addOverlay("line3d", { // same properties as handControllerGrab search line lineWidth: 5, - // BUG: If you don't supply a start and end at creation, it will never show up, even after editing. + // FIX SYSTEM BUG: If you don't supply a start and end at creation, it will never show up, even after editing. start: MyAvatar.position, end: Vec3.ZERO, color: LASER_COLOR, @@ -160,12 +183,33 @@ var laserLine = Overlays.addOverlay("line3d", { // same properties as handContro visible: false, alpha: 1 }); -var overlays = [laserBall, laserLine]; +var BALL_SIZE = 0.011; +var BALL_ALPHA = 0.5; +var laserBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere + size: BALL_SIZE, + color: LASER_COLOR, + ignoreRayIntersection: true, + alpha: BALL_ALPHA, + visible: false, + solid: true, + drawInFront: true // Even when burried inside of something, show it. +}); +var fakeProjectionBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere + size: 5 * BALL_SIZE, + color: {red: 255, green: 10, blue: 10}, + ignoreRayIntersection: true, + alpha: BALL_ALPHA, + visible: false, + solid: true, + drawInFront: true // Even when burried inside of something, show it. +}); +var overlays = [laserBall, laserLine, fakeProjectionBall]; Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); -var visualizationOn = true; // Not whether it desired, but simply whether it is. Just an optimization. -function turnOffLaser() { - if (!visualizationOn) { return; } - visualizationOn = false; +var visualizationIsShowing = true; // Not whether it desired, but simply whether it is. Just an optimization. +function turnOffLaser(optionalEnableClicks) { + toggleMap.setState(optionalEnableClicks); + if (!visualizationIsShowing) { return; } + visualizationIsShowing = false; overlays.forEach(function (overlay) { Overlays.editOverlay(overlay, {visible: false}); }); @@ -173,18 +217,22 @@ function turnOffLaser() { var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. var wantsVisualization = false; function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { + toggleMap.setState(true); if (!wantsVisualization) { return false; } // Show the laser and intersect it with 3d overlays and entities. - var pickRay = {origin: controllerPosition, direction: controllerDirection}; - var result = Overlays.findRayIntersection(pickRay); - if (!result.intersects) { - result = Entities.findRayIntersection(pickRay, true); - } - if (!visualizationOn) { setReticleVisible(true); } - visualizationOn = true; - var termination = result.intersects ? + function intersection3d(position, direction) { + var pickRay = {origin: position, direction: direction}; + var result = Overlays.findRayIntersection(pickRay); + if (!result.intersects) { + result = Entities.findRayIntersection(pickRay, true); + } + return result.intersects ? result.intersection : - Vec3.sum(controllerPosition, Vec3.multiply(MAX_RAY_SCALE, controllerDirection)); + Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction)); + } + termination = intersection3d(controllerPosition, controllerDirection); + visualizationIsShowing = true; + setReticleVisible(false); Overlays.editOverlay(laserLine, {visible: true, start: controllerPosition, end: termination}); // We show the ball at the hud intersection rather than at the termination because: // 1) As you swing the laser in space, it's hard to judge where it will intersect with a HUD element, @@ -193,6 +241,15 @@ function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { // 2) On some objects, the intersection is just enough inside the object that we're not going to see // the ball anyway. Overlays.editOverlay(laserBall, {visible: true, position: hudPosition3d}); + + // We really want in-world interactions to take place at termination: + // - We could do some of that with callEntityMethod (e.g., light switch entity script) + // - But we would have to alter edit.js to accept synthetic mouse data. + // So for now, we present a false projection of the cursor onto whatever is below it. This is different from + // the laser termination because the false projection is from the camera, while the laser termination is from the hand. + var eye = Camera.getPosition(); + var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); + Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); return true; } var toggleLockout = new TimeLock(500); @@ -211,12 +268,13 @@ function maybeToggleVisualization(trigger, now) { function update() { var now = Date.now(); if (!lockOut.available(now)) { return turnOffLaser(); } // Let them use it in peace. + if (!Menu.isOptionChecked("First Person")) { debug('not 1st person'); return turnOffLaser(); } // What to do? menus can be behind hand! var trigger = getValue(Controller.Standard.RT); - if (trigger > 0.5) { lockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. + if (trigger > 0.9) { lockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. maybeToggleVisualization(trigger, now); var hand = Controller.Standard.RightHand; var controllerPose = getControllerPose(hand); - if (!controllerPose.valid) { return; } // Controller is cradled. + if (!controllerPose.valid) { wantsVisualization = false; return turnOffLaser(); } // Controller is cradled. var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: @@ -233,11 +291,9 @@ function update() { // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. if (Reticle.pointingAtSystemOverlay || getOverlayAtPoint(hudPoint2d)) { setReticleVisible(true); - //mapping.enable(); - return turnOffLaser(); + return turnOffLaser(true); } // We are not pointing at a HUD element (but it could be a 3d overlay). - //mapping.disable(); if (!updateLaser(controllerPosition, controllerDirection, hudPoint3d)) { setReticleVisible(false); turnOffLaser(); @@ -311,7 +367,7 @@ if (!Controller.Hardware.Hydra) { Overlays.editOverlay(fakeReticle, {x: hudPoint2d.x - reticleHalfSize, y: hudPoint2d.y - reticleHalfSize}); }; setReticleVisible = function (on) { - Reticle.visible = on; // BUG: doesn't work on mac. + Reticle.visible = on; // FIX SYSTEM BUG: doesn't work on mac. Overlays.editOverlay(fakeReticle, {visible: on}); }; // The idea here is that we not return a truthy result constantly when we display the fake reticle. From e2101a8996d4ba05d1b46a23f1b5db00fea443f4 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 27 Apr 2016 10:32:18 -0700 Subject: [PATCH 08/33] checkpoint --- examples/controllers/handControllerPointer.js | 182 ++++++++++++------ 1 file changed, 120 insertions(+), 62 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 0a06d2b7a3..ad22d940af 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -1,6 +1,6 @@ "use strict"; /*jslint vars: true, plusplus: true*/ -/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Vec3, Quat, print */ +/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print */ // // handControllerPointer.js @@ -25,17 +25,20 @@ // May also interfere with other scripts? // Right hand only. -// Do not use with depthReticle.js. // Trigger toggle is flakey. // When clicking on in-world objects, the click acts on the red ball, not the termination of the blue line. -/* -ScriptDiscoveryService.getRunning().forEach(function (script) { - if (script.name === 'depthReticle.js') { - print("Stopping script", script.name); - script.stop(); - } -}); -*/ + +function checkForDepthReticleScript() { + ScriptDiscoveryService.getRunning().forEach(function (script) { + if (script.name === 'depthReticle.js') { + Window.alert('Please shut down depthReticle script.\n' + script.path + + '\nMost of the behavior is included here in\n' + + Script.resolvePath('')); + // Some current deviations are listed below as fixmes. + } + }); +} + // UTILITIES ------------- // @@ -56,52 +59,75 @@ function TimeLock(expiration) { this.update = function (optionalNow) { last = optionalNow || Date.now(); }; - this.available = function (optionalNow) { + this.expired = function (optionalNow) { return ((optionalNow || Date.now()) - last) > expiration; }; } -var lockOut = new TimeLock(2000); +var handControllerLockOut = new TimeLock(2000); // Calls onFunction() or offFunction() when swtich(on), but only if it is to a new value. function LatchedToggle(onFunction, offFunction, state) { this.setState = function (on) { - if (state === on) { return; } - state = on; - if (on) { - onFunction(); - } else { - offFunction(); - } + if (state === on) { return; } + state = on; + if (on) { + onFunction(); + } else { + offFunction(); + } }; } + // VERTICAL FIELD OF VIEW --------- // // Cache the verticalFieldOfView setting and update it every so often. -var DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees -var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds -var verticalFieldOfView = Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW; -var settingsChecker = Script.setInterval(function () { +var verticalFieldOfView, DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees +function updateFieldOfView() { verticalFieldOfView = Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW; -}, SETTINGS_CHANGE_RECHECK_INTERVAL); -Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); }); +} // SHIMS ---------- // // Define customizable versions of some standard operators. Alternative are at the bottom of the file. var getControllerPose = Controller.getPoseValue; -var setCursor = function (point2d) { +var getValue = Controller.getValue; +var getOverlayAtPoint = Overlays.getOverlayAtPoint; +var setReticleVisible = function (on) { Reticle.visible = on; }; + +var weMovedReticle = false; +function handControllerMovedReticle() { // I.e., change in cursor position is from us, not the mouse. + // Only we know if we moved it, which is why this script has to replace depthReticle.js + if (!weMovedReticle) { return false; } + weMovedReticle = false; + return true; +} +var setReticlePosition = function (point2d) { if (!HMD.active) { // FIX SYSEM BUG: On Windows, setPosition is setting relative to screen origin, not the content area of the window. point2d = {x: point2d.x, y: point2d.y + 50}; } + weMovedReticle = true; Reticle.setPosition(point2d); }; -var setReticleVisible = function (on) { Reticle.visible = on; }; -var getOverlayAtPoint = Overlays.getOverlayAtPoint; -var getValue = Controller.getValue; + +// Generalizations of utilities that work with system and overlay elements. +function findRayIntersection(pickRay) { + // Check 3D overlays and entities. Argument is an object with origin and direction. + var result = Overlays.findRayIntersection(pickRay); + if (!result.intersects) { + result = Entities.findRayIntersection(pickRay, true); + } + return result; +} +function isPointingAtOverlay(optionalHudPosition2d) { + return Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(optionalHudPosition2d || Reticle.position); +} // Generalized HUD utilities, with or without HMD: +// These two "vars" are for documentation. Do not change their values! +var SPHERICAL_HUD_DISTANCE = 1; // meters. +var PLANAR_PERPENDICULAR_HUD_DISTANCE = SPHERICAL_HUD_DISTANCE; function calculateRayUICollisionPoint(position, direction) { // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection. if (HMD.active) { @@ -111,7 +137,7 @@ function calculateRayUICollisionPoint(position, direction) { // scale = hudNormal dot (hudPoint - position) / hudNormal dot direction // intersection = postion + scale*direction var hudNormal = Quat.getFront(Camera.getOrientation()); - var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // 1m out + var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // must also scale if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 var denominator = Vec3.dot(hudNormal, direction); if (denominator === 0) { return null; } // parallel to plane var numerator = Vec3.dot(hudNormal, Vec3.subtract(hudPoint, position)); @@ -132,7 +158,7 @@ function overlayFromWorldPoint(point) { var cameraX = Vec3.dot(cameraToPoint, Quat.getRight(Camera.getOrientation())); var cameraY = Vec3.dot(cameraToPoint, Quat.getUp(Camera.getOrientation())); var size = Controller.getViewportDimensions(); - var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); + var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); // must adjust if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 var hudWidth = hudHeight * size.x / size.y; var horizontalFraction = (cameraX / hudWidth + 0.5); var verticalFraction = 1 - (cameraY / hudHeight + 0.5); @@ -147,7 +173,7 @@ function overlayFromWorldPoint(point) { var MAPPING_NAME = Script.resolvePath(''); var mapping = Controller.newMapping(MAPPING_NAME); function mapToAction(controller, button, action) { - if (!Controller.Hardware[controller]) { return; } + if (!Controller.Hardware[controller]) { return; } // FIXME: recheck periodically! mapping.from(Controller.Hardware[controller][button]).peek().to(Controller.Actions[action]); } mapToAction('Hydra', 'R3', 'ReticleClick'); @@ -157,19 +183,45 @@ mapToAction('Hydra', 'L4', 'ContextMenu'); mapToAction('Vive', 'LeftPrimaryThumb', 'ReticleClick'); mapToAction('Vive', 'RightPrimaryThumb', 'ReticleClick'); Script.scriptEnding.connect(mapping.disable); -toggleMap = new LatchedToggle(mapping.enable, mapping.disable); +var toggleMap = new LatchedToggle(mapping.enable, mapping.disable); -// MOUSE LOCKOUT -------- +// MOUSE ACTIVITY -------- // -var weMovedReticle = false; -if (Controller.Hardware.Hydra) { - setupHandler(Controller.mouseMoveEvent, function () { - if (weMovedReticle) { weMovedReticle = false; return; } - // Should we implement dephReticle's inactivity timeout here? - setReticleVisible(true); - lockOut.update(); - }); +var mouseCursorActivity = new TimeLock(5000); +var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant +function updateMouseActivity() { + if (handControllerMovedReticle()) { return; } + // Turn off mouse cursor after inactivity (as in depthReticle.js), and turn off hand controller mouse for a while. + var now = Date.now(); + handControllerLockOut.update(now); + mouseCursorActivity.update(now); + // FIXME: Does not yet seek to lookAt upon waking. + setReticleVisible(true); } +function expireMouseCursor(now) { + if (mouseCursorActivity.expired(now)) { + setReticleVisible(false); + } +} +function onMouseMove() { + // Display cursor at correct depth (as in depthReticle.js), and updateMouseActivity. + if (handControllerMovedReticle()) { return; } + + if (HMD.active) { // set depth + // FIXME: does not yet adjust slowly. + if (isPointingAtOverlay()) { + Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT FOR OFFSET SPHERE! + } else { + var result = findRayIntersection(Camera.computePickRay(Reticle.position.x, Reticle.position.y)); + Reticle.depth = result.intersects ? result.depth : APPARENT_MAXIMUM_DEPTH; + } + } + updateMouseActivity(); +} +setupHandler(Controller.mouseMoveEvent, onMouseMove); +setupHandler(Controller.mousePressEvent, updateMouseActivity); +setupHandler(Controller.mouseDoublePressEvent, updateMouseActivity); + // VISUAL AID ----------- var LASER_COLOR = {red: 10, green: 10, blue: 255}; @@ -208,8 +260,9 @@ Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverla var visualizationIsShowing = true; // Not whether it desired, but simply whether it is. Just an optimization. function turnOffLaser(optionalEnableClicks) { toggleMap.setState(optionalEnableClicks); + if (!optionalEnableClicks) { expireMouseCursor(); } if (!visualizationIsShowing) { return; } - visualizationIsShowing = false; + visualizationIsShowing = false; overlays.forEach(function (overlay) { Overlays.editOverlay(overlay, {visible: false}); }); @@ -221,16 +274,11 @@ function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { if (!wantsVisualization) { return false; } // Show the laser and intersect it with 3d overlays and entities. function intersection3d(position, direction) { - var pickRay = {origin: position, direction: direction}; - var result = Overlays.findRayIntersection(pickRay); - if (!result.intersects) { - result = Entities.findRayIntersection(pickRay, true); - } - return result.intersects ? - result.intersection : - Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction)); + var pickRay = {origin: position, direction: direction}; + var result = findRayIntersection(pickRay); + return result.intersects ? result.intersection : Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction)); } - termination = intersection3d(controllerPosition, controllerDirection); + var termination = intersection3d(controllerPosition, controllerDirection); visualizationIsShowing = true; setReticleVisible(false); Overlays.editOverlay(laserLine, {visible: true, start: controllerPosition, end: termination}); @@ -255,7 +303,7 @@ function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { var toggleLockout = new TimeLock(500); function maybeToggleVisualization(trigger, now) { if (!trigger) { return; } - if (toggleLockout.available(now)) { + if (toggleLockout.expired(now)) { wantsVisualization = !wantsVisualization; print('Toggled visualization', wantsVisualization ? 'on' : 'off'); } else { @@ -265,12 +313,13 @@ function maybeToggleVisualization(trigger, now) { // MAIN OPERATIONS ----------- // +var FULL_TRIGGER_THRESHOLD = 0.9; // 0 to 1. Non-linear. function update() { var now = Date.now(); - if (!lockOut.available(now)) { return turnOffLaser(); } // Let them use it in peace. + if (!handControllerLockOut.expired(now)) { return turnOffLaser(); } // Let them use mouse it in peace. if (!Menu.isOptionChecked("First Person")) { debug('not 1st person'); return turnOffLaser(); } // What to do? menus can be behind hand! var trigger = getValue(Controller.Standard.RT); - if (trigger > 0.9) { lockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. + if (trigger > FULL_TRIGGER_THRESHOLD) { handControllerLockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. maybeToggleVisualization(trigger, now); var hand = Controller.Standard.RightHand; var controllerPose = getControllerPose(hand); @@ -285,11 +334,10 @@ function update() { var hudPoint2d = overlayFromWorldPoint(hudPoint3d); // We don't know yet if we'll want to make the cursor visble, but we need to move it to see if // it's pointing at a QML tool (aka system overlay). - setCursor(hudPoint2d); - weMovedReticle = true; + setReticlePosition(hudPoint2d); // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. - if (Reticle.pointingAtSystemOverlay || getOverlayAtPoint(hudPoint2d)) { + if (isPointingAtOverlay(hudPoint2d)) { setReticleVisible(true); return turnOffLaser(true); } @@ -308,8 +356,7 @@ function update() { var apparentHudTermination2d = overlayFromWorldPoint(apparentHudTermination3d); Overlays.editOverlay(fakeReticle, {x: apparentHudTermination2d.x - reticleHalfSize, y: apparentHudTermination2d.y - reticleHalfSize}); //Reticle.visible = false; - weMovedReticle = true; - setCursor(apparentHudTermination2d); + setReticlePosition(apparentHudTermination2d); */ } @@ -317,12 +364,22 @@ var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. var updater = Script.setInterval(update, UPDATE_INTERVAL); Script.scriptEnding.connect(function () { Script.clearInterval(updater); }); +// Check periodically for changes to setup. +var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds +function checkSettings() { + updateFieldOfView(); + updateMouseHandlers(); +} +checkSettings(); +var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); +Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); }); + // DEBUGGING WITHOUT HYDRA ----------------------- // // The rest of this is for debugging without working hand controllers, using a line from camera to mouse, and an image for cursor. var CONTROLLER_ROTATION = Quat.fromPitchYawRollDegrees(90, 180, -90); -if (!Controller.Hardware.Hydra) { +if (false && !Controller.Hardware.Hydra) { print('WARNING: no hand controller detected. Using mouse!'); var mouseKeeper = {x: 0, y: 0}; var onMouseMoveCapture = function (event) { mouseKeeper.x = event.x; mouseKeeper.y = event.y; }; @@ -363,7 +420,8 @@ if (!Controller.Hardware.Hydra) { alpha: 0.7 }); Script.scriptEnding.connect(function () { Overlays.deleteOverlay(fakeReticle); }); - setCursor = function (hudPoint2d) { + setReticlePosition = function (hudPoint2d) { + weMovedReticle = true; Overlays.editOverlay(fakeReticle, {x: hudPoint2d.x - reticleHalfSize, y: hudPoint2d.y - reticleHalfSize}); }; setReticleVisible = function (on) { From d2a92acc73832c6cee72221bb885666fb1f374c2 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 27 Apr 2016 15:38:25 -0700 Subject: [PATCH 09/33] checkpoint demo2 --- examples/controllers/handControllerPointer.js | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index ad22d940af..b8bda0c6d0 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -13,18 +13,19 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +print('handControllerPointer version', 10); + // Control the "mouse" using hand controller. (HMD and desktop.) // For now: // Button 3 is left-mouse, button 4 is right-mouse. // First-person only. +// Right hand only. // Partial trigger squeeze toggles a laser visualization. When on, you can also click on objects in-world, not just HUD. // On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) // // Bugs: -// Turn in-world click off when visualization is off. -// May also interfere with other scripts? - -// Right hand only. +// Don't turn off hand controllers on simulated click (only on real mouse click). +// Turn in-world click off when moving by hand controller. // Trigger toggle is flakey. // When clicking on in-world objects, the click acts on the red ball, not the termination of the blue line. @@ -42,7 +43,7 @@ function checkForDepthReticleScript() { // UTILITIES ------------- // -var counter = 0, skip = 50; +var counter = 0, skip = 0; //fixme 50; function debug() { // Display the arguments not just [Object object]. if (skip && (counter++ % skip)) { return; } print.apply(null, [].map.call(arguments, JSON.stringify)); @@ -96,7 +97,7 @@ var getOverlayAtPoint = Overlays.getOverlayAtPoint; var setReticleVisible = function (on) { Reticle.visible = on; }; var weMovedReticle = false; -function handControllerMovedReticle() { // I.e., change in cursor position is from us, not the mouse. +function handControllerMovedReticle() { // I.e., change in cursor position is from this script, not the mouse. // Only we know if we moved it, which is why this script has to replace depthReticle.js if (!weMovedReticle) { return false; } weMovedReticle = false; @@ -174,32 +175,40 @@ var MAPPING_NAME = Script.resolvePath(''); var mapping = Controller.newMapping(MAPPING_NAME); function mapToAction(controller, button, action) { if (!Controller.Hardware[controller]) { return; } // FIXME: recheck periodically! - mapping.from(Controller.Hardware[controller][button]).peek().to(Controller.Actions[action]); + mapping.from(Controller.Hardware[controller][button]).peek().to(action); } -mapToAction('Hydra', 'R3', 'ReticleClick'); -mapToAction('Hydra', 'R4', 'ContextMenu'); -mapToAction('Hydra', 'L3', 'ReticleClick'); -mapToAction('Hydra', 'L4', 'ContextMenu'); -mapToAction('Vive', 'LeftPrimaryThumb', 'ReticleClick'); -mapToAction('Vive', 'RightPrimaryThumb', 'ReticleClick'); +function handControllerClick(input) { + if (!input) { return; } // We get both a down (with input 1) and up (with input 0) + if (isPointingAtOverlay()) { print('OVERLAY CLICK'); return; } + print('FIXME controller click'); +} +mapToAction('Hydra', 'R3', Controller.Actions.ReticleClick); // handControllerClick); +mapToAction('Hydra', 'L3', handControllerClick); +mapToAction('Vive', 'LeftPrimaryThumb', handControllerClick); +mapToAction('Vive', 'RightPrimaryThumb', handControllerClick); +mapToAction('Hydra', 'R4', Controller.Actions.ContextMenu); +mapToAction('Hydra', 'L4', Controller.Actions.ContextMenu); Script.scriptEnding.connect(mapping.disable); -var toggleMap = new LatchedToggle(mapping.enable, mapping.disable); +mapping.enable(); +//var toggleMap = new LatchedToggle(mapping.enable, mapping.disable); // MOUSE ACTIVITY -------- // var mouseCursorActivity = new TimeLock(5000); var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant -function updateMouseActivity() { +function updateMouseActivity(isClick) { if (handControllerMovedReticle()) { return; } - // Turn off mouse cursor after inactivity (as in depthReticle.js), and turn off hand controller mouse for a while. var now = Date.now(); - handControllerLockOut.update(now); mouseCursorActivity.update(now); + if (isClick) { return; } // FIXME: mouse clicks should keep going. Just not hand controller clicks + handControllerLockOut.update(now); + // Turn off mouse cursor after inactivity (as in depthReticle.js), and turn off hand controller mouse for a while. // FIXME: Does not yet seek to lookAt upon waking. + // FIXME not unless Reticle.allowMouseCapture setReticleVisible(true); } function expireMouseCursor(now) { - if (mouseCursorActivity.expired(now)) { + if (!isPointingAtOverlay() && mouseCursorActivity.expired(now)) { setReticleVisible(false); } } @@ -210,17 +219,20 @@ function onMouseMove() { if (HMD.active) { // set depth // FIXME: does not yet adjust slowly. if (isPointingAtOverlay()) { - Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT FOR OFFSET SPHERE! + Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! } else { var result = findRayIntersection(Camera.computePickRay(Reticle.position.x, Reticle.position.y)); - Reticle.depth = result.intersects ? result.depth : APPARENT_MAXIMUM_DEPTH; + Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; } } - updateMouseActivity(); + updateMouseActivity(); // After the above, just in case the depth movement is awkward when becoming visible. +} +function onMouseClick() { + updateMouseActivity(true); } setupHandler(Controller.mouseMoveEvent, onMouseMove); -setupHandler(Controller.mousePressEvent, updateMouseActivity); -setupHandler(Controller.mouseDoublePressEvent, updateMouseActivity); +setupHandler(Controller.mousePressEvent, onMouseClick); +setupHandler(Controller.mouseDoublePressEvent, onMouseClick); // VISUAL AID ----------- @@ -257,9 +269,9 @@ var fakeProjectionBall = Overlays.addOverlay("sphere", { // Same properties as h }); var overlays = [laserBall, laserLine, fakeProjectionBall]; Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); -var visualizationIsShowing = true; // Not whether it desired, but simply whether it is. Just an optimization. +var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. function turnOffLaser(optionalEnableClicks) { - toggleMap.setState(optionalEnableClicks); + //toggleMap.setState(optionalEnableClicks); if (!optionalEnableClicks) { expireMouseCursor(); } if (!visualizationIsShowing) { return; } visualizationIsShowing = false; @@ -270,7 +282,7 @@ function turnOffLaser(optionalEnableClicks) { var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. var wantsVisualization = false; function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { - toggleMap.setState(true); + //toggleMap.setState(true); if (!wantsVisualization) { return false; } // Show the laser and intersect it with 3d overlays and entities. function intersection3d(position, direction) { @@ -317,7 +329,7 @@ var FULL_TRIGGER_THRESHOLD = 0.9; // 0 to 1. Non-linear. function update() { var now = Date.now(); if (!handControllerLockOut.expired(now)) { return turnOffLaser(); } // Let them use mouse it in peace. - if (!Menu.isOptionChecked("First Person")) { debug('not 1st person'); return turnOffLaser(); } // What to do? menus can be behind hand! + if (!Menu.isOptionChecked("First Person")) { return turnOffLaser(); } // What to do? menus can be behind hand! var trigger = getValue(Controller.Standard.RT); if (trigger > FULL_TRIGGER_THRESHOLD) { handControllerLockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. maybeToggleVisualization(trigger, now); @@ -339,6 +351,7 @@ function update() { // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. if (isPointingAtOverlay(hudPoint2d)) { setReticleVisible(true); + Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! return turnOffLaser(true); } // We are not pointing at a HUD element (but it could be a 3d overlay). @@ -368,7 +381,7 @@ Script.scriptEnding.connect(function () { Script.clearInterval(updater); }); var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds function checkSettings() { updateFieldOfView(); - updateMouseHandlers(); + checkForDepthReticleScript() } checkSettings(); var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); From 5ac67119a1cdcd9a0c4b7b205acfe567d94bb917 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 28 Apr 2016 10:28:04 -0700 Subject: [PATCH 10/33] Pretty solid except for in-world clicking via red ball instead of laser end, and not turning off clicking when no visualization. --- examples/controllers/handControllerPointer.js | 134 +++++++++++------- 1 file changed, 84 insertions(+), 50 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index b8bda0c6d0..c2a12a59ac 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -24,21 +24,28 @@ print('handControllerPointer version', 10); // On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) // // Bugs: -// Don't turn off hand controllers on simulated click (only on real mouse click). -// Turn in-world click off when moving by hand controller. -// Trigger toggle is flakey. // When clicking on in-world objects, the click acts on the red ball, not the termination of the blue line. +// While hardware mouse move switches to mouse move, hardware mouse click (without amove) does not. +// Turn in-world click off when moving by hand controller. +var wasRunningDepthReticle = false; function checkForDepthReticleScript() { ScriptDiscoveryService.getRunning().forEach(function (script) { if (script.name === 'depthReticle.js') { - Window.alert('Please shut down depthReticle script.\n' + script.path + + wasRunningDepthReticle = script.path; + Window.alert('Shuting down depthReticle script.\n' + script.path + '\nMost of the behavior is included here in\n' + Script.resolvePath('')); - // Some current deviations are listed below as fixmes. + ScriptDiscoveryService.stopScript(script.path); // BUG: getRunning gets path and url backwards. stopScript wants a url. + // Some current deviations are listed below as 'FIXME'. } }); } +Script.scriptEnding.connect(function () { + if (wasRunningDepthReticle) { + Script.load(wasRunningDepthReticle); + } +}); // UTILITIES ------------- @@ -79,6 +86,35 @@ function LatchedToggle(onFunction, offFunction, state) { }; } +// Code copied and adapted from handControllerGrab.js. We should refactor this. +function Trigger() { + var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing + var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab + var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab + var TRIGGER_OFF_VALUE = 0.15; + var that = this; + that.triggerValue = 0; // rolling average of trigger value + that.rawTriggerValue = 0; + that.triggerPress = function(value) { + print('fixme trigger', value); + that.rawTriggerValue = value; + }; + that.updateSmoothedTrigger = function() { + var triggerValue = that.rawTriggerValue; + // smooth out trigger value + that.triggerValue = (that.triggerValue * TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); + }; + that.triggerSmoothedGrab = function() { + return that.triggerValue > TRIGGER_GRAB_VALUE; + }; + that.triggerSmoothedSqueezed = function() { + return that.triggerValue > TRIGGER_ON_VALUE; + }; + that.triggerSmoothedReleased = function() { + return that.triggerValue < TRIGGER_OFF_VALUE; + }; +} // VERTICAL FIELD OF VIEW --------- // @@ -168,30 +204,6 @@ function overlayFromWorldPoint(point) { return { x: horizontalPixels, y: verticalPixels }; } -// CONTROLLER MAPPING --------- -// -// Synthesize left and right mouse click from controller: -var MAPPING_NAME = Script.resolvePath(''); -var mapping = Controller.newMapping(MAPPING_NAME); -function mapToAction(controller, button, action) { - if (!Controller.Hardware[controller]) { return; } // FIXME: recheck periodically! - mapping.from(Controller.Hardware[controller][button]).peek().to(action); -} -function handControllerClick(input) { - if (!input) { return; } // We get both a down (with input 1) and up (with input 0) - if (isPointingAtOverlay()) { print('OVERLAY CLICK'); return; } - print('FIXME controller click'); -} -mapToAction('Hydra', 'R3', Controller.Actions.ReticleClick); // handControllerClick); -mapToAction('Hydra', 'L3', handControllerClick); -mapToAction('Vive', 'LeftPrimaryThumb', handControllerClick); -mapToAction('Vive', 'RightPrimaryThumb', handControllerClick); -mapToAction('Hydra', 'R4', Controller.Actions.ContextMenu); -mapToAction('Hydra', 'L4', Controller.Actions.ContextMenu); -Script.scriptEnding.connect(mapping.disable); -mapping.enable(); -//var toggleMap = new LatchedToggle(mapping.enable, mapping.disable); - // MOUSE ACTIVITY -------- // var mouseCursorActivity = new TimeLock(5000); @@ -200,11 +212,10 @@ function updateMouseActivity(isClick) { if (handControllerMovedReticle()) { return; } var now = Date.now(); mouseCursorActivity.update(now); - if (isClick) { return; } // FIXME: mouse clicks should keep going. Just not hand controller clicks - handControllerLockOut.update(now); - // Turn off mouse cursor after inactivity (as in depthReticle.js), and turn off hand controller mouse for a while. + if (isClick) { return; } // Bug: mouse clicks should keep going. Just not hand controller clicks // FIXME: Does not yet seek to lookAt upon waking. // FIXME not unless Reticle.allowMouseCapture + handControllerLockOut.update(now); setReticleVisible(true); } function expireMouseCursor(now) { @@ -234,6 +245,37 @@ setupHandler(Controller.mouseMoveEvent, onMouseMove); setupHandler(Controller.mousePressEvent, onMouseClick); setupHandler(Controller.mouseDoublePressEvent, onMouseClick); +// CONTROLLER MAPPING --------- +// +// Synthesize left and right mouse click from controller, and get trigger values matching handControllerGrab. +var MAPPING_NAME = Script.resolvePath(''); +var mapping = Controller.newMapping(MAPPING_NAME); + +var leftTrigger = new Trigger(); +var rightTrigger = new Trigger(); +mapping.from([Controller.Standard.RT]).peek().to(rightTrigger.triggerPress); +mapping.from([Controller.Standard.LT]).peek().to(leftTrigger.triggerPress); + +function mapToAction(controller, button, action) { + if (!Controller.Hardware[controller]) { return; } // FIXME: recheck periodically! + mapping.from(Controller.Hardware[controller][button]).peek().to(action); +} +function handControllerClick(input) { // FIXME + if (!input) { return; } // We get both a down (with input 1) and up (with input 0) + if (isPointingAtOverlay()) { return; } +} +mapToAction('Hydra', 'R3', Controller.Actions.ReticleClick); // handControllerClick); +mapToAction('Hydra', 'L3', handControllerClick); +mapToAction('Vive', 'LeftPrimaryThumb', handControllerClick); +mapToAction('Vive', 'RightPrimaryThumb', handControllerClick); +mapToAction('Hydra', 'R4', Controller.Actions.ContextMenu); +mapToAction('Hydra', 'L4', Controller.Actions.ContextMenu); +Script.scriptEnding.connect(mapping.disable); +//mapping.enable(); +var toggleMap = new LatchedToggle(mapping.enable, mapping.disable); +toggleMap.setState(true); +Script.scriptEnding.connect(mapping.disable); + // VISUAL AID ----------- var LASER_COLOR = {red: 10, green: 10, blue: 255}; @@ -270,17 +312,20 @@ var fakeProjectionBall = Overlays.addOverlay("sphere", { // Same properties as h var overlays = [laserBall, laserLine, fakeProjectionBall]; Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. +var wantsVisualization = false; function turnOffLaser(optionalEnableClicks) { - //toggleMap.setState(optionalEnableClicks); - if (!optionalEnableClicks) { expireMouseCursor(); } + if (!optionalEnableClicks) { + expireMouseCursor(); + wantsVisualization = false; + } if (!visualizationIsShowing) { return; } visualizationIsShowing = false; + //toggleMap.setState(optionalEnableClicks); overlays.forEach(function (overlay) { Overlays.editOverlay(overlay, {visible: false}); }); } var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. -var wantsVisualization = false; function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { //toggleMap.setState(true); if (!wantsVisualization) { return false; } @@ -312,30 +357,19 @@ function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); return true; } -var toggleLockout = new TimeLock(500); -function maybeToggleVisualization(trigger, now) { - if (!trigger) { return; } - if (toggleLockout.expired(now)) { - wantsVisualization = !wantsVisualization; - print('Toggled visualization', wantsVisualization ? 'on' : 'off'); - } else { - toggleLockout.update(now); - } -} // MAIN OPERATIONS ----------- // -var FULL_TRIGGER_THRESHOLD = 0.9; // 0 to 1. Non-linear. function update() { var now = Date.now(); + rightTrigger.updateSmoothedTrigger(); if (!handControllerLockOut.expired(now)) { return turnOffLaser(); } // Let them use mouse it in peace. if (!Menu.isOptionChecked("First Person")) { return turnOffLaser(); } // What to do? menus can be behind hand! - var trigger = getValue(Controller.Standard.RT); - if (trigger > FULL_TRIGGER_THRESHOLD) { handControllerLockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. - maybeToggleVisualization(trigger, now); + if (rightTrigger.triggerSmoothedGrab()) { handControllerLockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. + if (rightTrigger.triggerSmoothedSqueezed()) { print('FIXME on'); wantsVisualization = true; } var hand = Controller.Standard.RightHand; var controllerPose = getControllerPose(hand); - if (!controllerPose.valid) { wantsVisualization = false; return turnOffLaser(); } // Controller is cradled. + if (!controllerPose.valid) { return turnOffLaser(); } // Controller is cradled. var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: From f3adfb7c50358205a0bedcaf30fdaa091af5137b Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 28 Apr 2016 10:49:45 -0700 Subject: [PATCH 11/33] disable controller-button to mouse click mapping when off-hud and no visualization. --- examples/controllers/handControllerPointer.js | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index c2a12a59ac..7ae0f34067 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -35,7 +35,8 @@ function checkForDepthReticleScript() { wasRunningDepthReticle = script.path; Window.alert('Shuting down depthReticle script.\n' + script.path + '\nMost of the behavior is included here in\n' + - Script.resolvePath('')); + Script.resolvePath('') + + '\ndepthReticle.js will be silently restarted when this script ends.'); ScriptDiscoveryService.stopScript(script.path); // BUG: getRunning gets path and url backwards. stopScript wants a url. // Some current deviations are listed below as 'FIXME'. } @@ -96,7 +97,6 @@ function Trigger() { that.triggerValue = 0; // rolling average of trigger value that.rawTriggerValue = 0; that.triggerPress = function(value) { - print('fixme trigger', value); that.rawTriggerValue = value; }; that.updateSmoothedTrigger = function() { @@ -248,17 +248,20 @@ setupHandler(Controller.mouseDoublePressEvent, onMouseClick); // CONTROLLER MAPPING --------- // // Synthesize left and right mouse click from controller, and get trigger values matching handControllerGrab. -var MAPPING_NAME = Script.resolvePath(''); -var mapping = Controller.newMapping(MAPPING_NAME); +var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-trigger'); +Script.scriptEnding.connect(triggerMapping.disable); var leftTrigger = new Trigger(); var rightTrigger = new Trigger(); -mapping.from([Controller.Standard.RT]).peek().to(rightTrigger.triggerPress); -mapping.from([Controller.Standard.LT]).peek().to(leftTrigger.triggerPress); +triggerMapping.from([Controller.Standard.RT]).peek().to(rightTrigger.triggerPress); +triggerMapping.from([Controller.Standard.LT]).peek().to(leftTrigger.triggerPress); +triggerMapping.enable(); +var clickMapping = Controller.newMapping(Script.resolvePath('') + '-click'); +Script.scriptEnding.connect(clickMapping.disable); function mapToAction(controller, button, action) { if (!Controller.Hardware[controller]) { return; } // FIXME: recheck periodically! - mapping.from(Controller.Hardware[controller][button]).peek().to(action); + clickMapping.from(Controller.Hardware[controller][button]).peek().to(action); } function handControllerClick(input) { // FIXME if (!input) { return; } // We get both a down (with input 1) and up (with input 0) @@ -270,11 +273,8 @@ mapToAction('Vive', 'LeftPrimaryThumb', handControllerClick); mapToAction('Vive', 'RightPrimaryThumb', handControllerClick); mapToAction('Hydra', 'R4', Controller.Actions.ContextMenu); mapToAction('Hydra', 'L4', Controller.Actions.ContextMenu); -Script.scriptEnding.connect(mapping.disable); -//mapping.enable(); -var toggleMap = new LatchedToggle(mapping.enable, mapping.disable); -toggleMap.setState(true); -Script.scriptEnding.connect(mapping.disable); +var clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); +clickMapToggle.setState(true); // VISUAL AID ----------- @@ -366,7 +366,7 @@ function update() { if (!handControllerLockOut.expired(now)) { return turnOffLaser(); } // Let them use mouse it in peace. if (!Menu.isOptionChecked("First Person")) { return turnOffLaser(); } // What to do? menus can be behind hand! if (rightTrigger.triggerSmoothedGrab()) { handControllerLockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. - if (rightTrigger.triggerSmoothedSqueezed()) { print('FIXME on'); wantsVisualization = true; } + if (rightTrigger.triggerSmoothedSqueezed()) { wantsVisualization = true; } var hand = Controller.Standard.RightHand; var controllerPose = getControllerPose(hand); if (!controllerPose.valid) { return turnOffLaser(); } // Controller is cradled. @@ -386,13 +386,16 @@ function update() { if (isPointingAtOverlay(hudPoint2d)) { setReticleVisible(true); Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + clickMapToggle.setState(true); return turnOffLaser(true); } // We are not pointing at a HUD element (but it could be a 3d overlay). - if (!updateLaser(controllerPosition, controllerDirection, hudPoint3d)) { + var visualization3d = updateLaser(controllerPosition, controllerDirection, hudPoint3d); + if (!visualization3d) { setReticleVisible(false); turnOffLaser(); } + clickMapToggle.setState(visualization3d); /* // Hack: Move the pointer again, this time to the intersection. This allows "clicking" on // 2D and 3D entities without rewriting other parts of the system, but it isn't right, From 89e4670361ef0f946b5ca3ed5a3c1ea200c62cce Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 28 Apr 2016 14:51:17 -0700 Subject: [PATCH 12/33] checkpoint --- examples/controllers/handControllerPointer.js | 127 ++++++++++-------- 1 file changed, 72 insertions(+), 55 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 7ae0f34067..3d11d0bdda 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -20,13 +20,13 @@ print('handControllerPointer version', 10); // Button 3 is left-mouse, button 4 is right-mouse. // First-person only. // Right hand only. -// Partial trigger squeeze toggles a laser visualization. When on, you can also click on objects in-world, not just HUD. // On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) +// There's an additional experimental feature that isn't quite what we want: +// A partial trigger squeeze (like "hand controller search") turns on a laser. The red ball (mostly) works as a click on in-world +// entities and 3d overlays. We'd like that red ball to be at the end of the termination of the laser line, but right now its on the HUD. // // Bugs: -// When clicking on in-world objects, the click acts on the red ball, not the termination of the blue line. // While hardware mouse move switches to mouse move, hardware mouse click (without amove) does not. -// Turn in-world click off when moving by hand controller. var wasRunningDepthReticle = false; function checkForDepthReticleScript() { @@ -37,7 +37,9 @@ function checkForDepthReticleScript() { '\nMost of the behavior is included here in\n' + Script.resolvePath('') + '\ndepthReticle.js will be silently restarted when this script ends.'); - ScriptDiscoveryService.stopScript(script.path); // BUG: getRunning gets path and url backwards. stopScript wants a url. + ScriptDiscoveryService.stopScript(script.path); + // FIX SYSTEM BUG: getRunning gets path and url backwards. stopScript wants a url. + // https://app.asana.com/0/26225263936266/118428633439650 // Some current deviations are listed below as 'FIXME'. } }); @@ -130,6 +132,8 @@ function updateFieldOfView() { var getControllerPose = Controller.getPoseValue; var getValue = Controller.getValue; var getOverlayAtPoint = Overlays.getOverlayAtPoint; +// FIX SYSTEM BUG: doesn't work on mac. +// https://app.asana.com/0/26225263936266/118428633439654 var setReticleVisible = function (on) { Reticle.visible = on; }; var weMovedReticle = false; @@ -141,7 +145,8 @@ function handControllerMovedReticle() { // I.e., change in cursor position is fr } var setReticlePosition = function (point2d) { if (!HMD.active) { - // FIX SYSEM BUG: On Windows, setPosition is setting relative to screen origin, not the content area of the window. + // FIX SYSTEM BUG: setPosition is setting relative to screen origin, not the content area of the window. + // https://app.asana.com/0/26225263936266/118427643788550 point2d = {x: point2d.x, y: point2d.y + 50}; } weMovedReticle = true; @@ -241,38 +246,37 @@ function onMouseMove() { function onMouseClick() { updateMouseActivity(true); } -setupHandler(Controller.mouseMoveEvent, onMouseMove); -setupHandler(Controller.mousePressEvent, onMouseClick); -setupHandler(Controller.mouseDoublePressEvent, onMouseClick); +var fixmehack = false; +if (!fixmehack) { + setupHandler(Controller.mouseMoveEvent, onMouseMove); + setupHandler(Controller.mousePressEvent, onMouseClick); + setupHandler(Controller.mouseDoublePressEvent, onMouseClick); +} // CONTROLLER MAPPING --------- // // Synthesize left and right mouse click from controller, and get trigger values matching handControllerGrab. +function mapToAction(mapping, controller, button, action) { + if (!Controller.Hardware[controller]) { return; } // FIXME: recheck periodically! + mapping.from(Controller.Hardware[controller][button]).peek().to(Controller.Actions[action]); +} -var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-trigger'); -Script.scriptEnding.connect(triggerMapping.disable); +var triggerMenuMapping = Controller.newMapping(Script.resolvePath('') + '-trigger'); +Script.scriptEnding.connect(triggerMenuMapping.disable); var leftTrigger = new Trigger(); var rightTrigger = new Trigger(); -triggerMapping.from([Controller.Standard.RT]).peek().to(rightTrigger.triggerPress); -triggerMapping.from([Controller.Standard.LT]).peek().to(leftTrigger.triggerPress); -triggerMapping.enable(); +triggerMenuMapping.from([Controller.Standard.RT]).peek().to(rightTrigger.triggerPress); +triggerMenuMapping.from([Controller.Standard.LT]).peek().to(leftTrigger.triggerPress); +mapToAction(triggerMenuMapping, 'Hydra', 'R4', 'ContextMenu'); +mapToAction(triggerMenuMapping, 'Hydra', 'L4', 'ContextMenu'); +triggerMenuMapping.enable(); var clickMapping = Controller.newMapping(Script.resolvePath('') + '-click'); Script.scriptEnding.connect(clickMapping.disable); -function mapToAction(controller, button, action) { - if (!Controller.Hardware[controller]) { return; } // FIXME: recheck periodically! - clickMapping.from(Controller.Hardware[controller][button]).peek().to(action); -} -function handControllerClick(input) { // FIXME - if (!input) { return; } // We get both a down (with input 1) and up (with input 0) - if (isPointingAtOverlay()) { return; } -} -mapToAction('Hydra', 'R3', Controller.Actions.ReticleClick); // handControllerClick); -mapToAction('Hydra', 'L3', handControllerClick); -mapToAction('Vive', 'LeftPrimaryThumb', handControllerClick); -mapToAction('Vive', 'RightPrimaryThumb', handControllerClick); -mapToAction('Hydra', 'R4', Controller.Actions.ContextMenu); -mapToAction('Hydra', 'L4', Controller.Actions.ContextMenu); +mapToAction(clickMapping, 'Hydra', 'R3', 'ReticleClick'); +mapToAction(clickMapping, 'Hydra', 'L3', 'ReticleClick'); +mapToAction(clickMapping, 'Vive', 'LeftPrimaryThumb', 'ReticleClick'); +mapToAction(clickMapping, 'Vive', 'RightPrimaryThumb', 'ReticleClick'); var clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); clickMapToggle.setState(true); @@ -281,7 +285,7 @@ clickMapToggle.setState(true); var LASER_COLOR = {red: 10, green: 10, blue: 255}; var laserLine = Overlays.addOverlay("line3d", { // same properties as handControllerGrab search line lineWidth: 5, - // FIX SYSTEM BUG: If you don't supply a start and end at creation, it will never show up, even after editing. + // FIX SYSTEM BUG?: If you don't supply a start and end at creation, it will never show up, even after editing. start: MyAvatar.position, end: Vec3.ZERO, color: LASER_COLOR, @@ -326,8 +330,7 @@ function turnOffLaser(optionalEnableClicks) { }); } var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. -function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { - //toggleMap.setState(true); +function updateLaser(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { if (!wantsVisualization) { return false; } // Show the laser and intersect it with 3d overlays and entities. function intersection3d(position, direction) { @@ -336,8 +339,8 @@ function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { return result.intersects ? result.intersection : Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction)); } var termination = intersection3d(controllerPosition, controllerDirection); + visualizationIsShowing = true; - setReticleVisible(false); Overlays.editOverlay(laserLine, {visible: true, start: controllerPosition, end: termination}); // We show the ball at the hud intersection rather than at the termination because: // 1) As you swing the laser in space, it's hard to judge where it will intersect with a HUD element, @@ -347,14 +350,40 @@ function updateLaser(controllerPosition, controllerDirection, hudPosition3d) { // the ball anyway. Overlays.editOverlay(laserBall, {visible: true, position: hudPosition3d}); - // We really want in-world interactions to take place at termination: - // - We could do some of that with callEntityMethod (e.g., light switch entity script) - // - But we would have to alter edit.js to accept synthetic mouse data. - // So for now, we present a false projection of the cursor onto whatever is below it. This is different from - // the laser termination because the false projection is from the camera, while the laser termination is from the hand. - var eye = Camera.getPosition(); - var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); - Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); + if (!fixmehack) { + // We really want in-world interactions to take place at termination: + // - We could do some of that with callEntityMethod (e.g., light switch entity script) + // - But we would have to alter edit.js to accept synthetic mouse data. + // So for now, we present a false projection of the cursor onto whatever is below it. This is different from + // the laser termination because the false projection is from the camera, while the laser termination is from the hand. + var eye = Camera.getPosition(); + var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); + Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); + setReticleVisible(false); + } else { + // Hack: Move the pointer again, this time to the intersection. This allows "clicking" on + // 3D entities without rewriting other parts of the system, but it isn't right, + // because the line from camera to the new mouse position might intersect different things + // than the line from controllerPosition to termination. + var eye = Camera.getPosition(); + var apparentHudTermination3d = calculateRayUICollisionPoint(eye, Vec3.subtract(termination, eye)); + var apparentHudTermination2d = overlayFromWorldPoint(apparentHudTermination3d); + Overlays.editOverlay(fakeProjectionBall, {visible: true, position: termination}); + setReticleVisible(false); + + setReticlePosition(apparentHudTermination2d); + /*if (isPointingAtOverlay(apparentHudTermination2d)) { + // The intersection could be at a point underneath a HUD element! (I said this was a hack.) + // If so, set things back so we don't oscillate. + setReticleVisible(false); + setReticlePosition(hudPosition2d) + return true; + } else { + setReticleVisible(true); + }*/ + if (HMD.active) { Reticle.depth = Vec3.distance(eye, apparentHudTermination3d); } + } + return true; } @@ -378,10 +407,10 @@ function update() { var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); if (!hudPoint3d) { print('Controller is parallel to HUD'); return turnOffLaser(); } var hudPoint2d = overlayFromWorldPoint(hudPoint3d); + // We don't know yet if we'll want to make the cursor visble, but we need to move it to see if // it's pointing at a QML tool (aka system overlay). setReticlePosition(hudPoint2d); - // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. if (isPointingAtOverlay(hudPoint2d)) { setReticleVisible(true); @@ -390,24 +419,12 @@ function update() { return turnOffLaser(true); } // We are not pointing at a HUD element (but it could be a 3d overlay). - var visualization3d = updateLaser(controllerPosition, controllerDirection, hudPoint3d); + var visualization3d = updateLaser(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); + clickMapToggle.setState(visualization3d); if (!visualization3d) { setReticleVisible(false); turnOffLaser(); } - clickMapToggle.setState(visualization3d); - /* - // Hack: Move the pointer again, this time to the intersection. This allows "clicking" on - // 2D and 3D entities without rewriting other parts of the system, but it isn't right, - // because the line from camera to the new mouse position might intersect different things - // than the line from controllerPosition to termination. - var eye = Camera.getPosition(); - var apparentHudTermination3d = calculateRayUICollisionPoint(eye, Vec3.subtract(termination, eye)); - var apparentHudTermination2d = overlayFromWorldPoint(apparentHudTermination3d); - Overlays.editOverlay(fakeReticle, {x: apparentHudTermination2d.x - reticleHalfSize, y: apparentHudTermination2d.y - reticleHalfSize}); - //Reticle.visible = false; - setReticlePosition(apparentHudTermination2d); -*/ } var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. @@ -475,7 +492,7 @@ if (false && !Controller.Hardware.Hydra) { Overlays.editOverlay(fakeReticle, {x: hudPoint2d.x - reticleHalfSize, y: hudPoint2d.y - reticleHalfSize}); }; setReticleVisible = function (on) { - Reticle.visible = on; // FIX SYSTEM BUG: doesn't work on mac. + Reticle.visible = on; Overlays.editOverlay(fakeReticle, {visible: on}); }; // The idea here is that we not return a truthy result constantly when we display the fake reticle. From c68cae5dc085ab6e68ccdf44a33e48e70b8b593d Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 28 Apr 2016 15:28:50 -0700 Subject: [PATCH 13/33] stash some commented-out code before update --- examples/controllers/handControllerPointer.js | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 3d11d0bdda..79a558ea3f 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -17,7 +17,7 @@ print('handControllerPointer version', 10); // Control the "mouse" using hand controller. (HMD and desktop.) // For now: -// Button 3 is left-mouse, button 4 is right-mouse. +// Button 3 is left-mouse, button 4 is right-mouse. What to do on Vive? // First-person only. // Right hand only. // On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) @@ -279,6 +279,33 @@ mapToAction(clickMapping, 'Vive', 'LeftPrimaryThumb', 'ReticleClick'); mapToAction(clickMapping, 'Vive', 'RightPrimaryThumb', 'ReticleClick'); var clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); clickMapToggle.setState(true); +/* +var hardware; // undefined +var leftTrigger = new Trigger(); +var rightTrigger = new Trigger(); +function checkHardware() { + var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive': null); // not undefined + if (hardware === newHardware) { return; } + if (hardware) { + triggerMenuMapping.disable(); + clickMapping.disable(); + } + hardware = newHardware; + triggerMenuMapping.from([Controller.Standard.RT]).peek().to(rightTrigger.triggerPress); + triggerMenuMapping.from([Controller.Standard.LT]).peek().to(leftTrigger.triggerPress); + mapToAction(triggerMenuMapping, 'Hydra', 'R4', 'ContextMenu'); + mapToAction(triggerMenuMapping, 'Hydra', 'L4', 'ContextMenu'); + triggerMenuMapping.enable(); + + mapToAction(clickMapping, 'Hydra', 'R3', 'ReticleClick'); + mapToAction(clickMapping, 'Hydra', 'L3', 'ReticleClick'); + mapToAction(clickMapping, 'Vive', 'LeftPrimaryThumb', 'ReticleClick'); + mapToAction(clickMapping, 'Vive', 'RightPrimaryThumb', 'ReticleClick'); + var clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); + clickMapToggle.setState(true); +} +checkHardware(); +*/ // VISUAL AID ----------- From 0362dd970ef6ff9c3ffe0120d6cca0499cad4717 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 28 Apr 2016 17:04:04 -0700 Subject: [PATCH 14/33] checkpoint. --- examples/controllers/handControllerPointer.js | 85 ++++++++++--------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 79a558ea3f..14acc62e7a 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -53,7 +53,7 @@ Script.scriptEnding.connect(function () { // UTILITIES ------------- // -var counter = 0, skip = 0; //fixme 50; +var counter = 0, skip = 50; function debug() { // Display the arguments not just [Object object]. if (skip && (counter++ % skip)) { return; } print.apply(null, [].map.call(arguments, JSON.stringify)); @@ -78,6 +78,9 @@ var handControllerLockOut = new TimeLock(2000); // Calls onFunction() or offFunction() when swtich(on), but only if it is to a new value. function LatchedToggle(onFunction, offFunction, state) { + this.getState = function () { + return state; + }; this.setState = function (on) { if (state === on) { return; } state = on; @@ -106,6 +109,7 @@ function Trigger() { // smooth out trigger value that.triggerValue = (that.triggerValue * TRIGGER_SMOOTH_RATIO) + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); + debug(that.triggerValue); }; that.triggerSmoothedGrab = function() { return that.triggerValue > TRIGGER_GRAB_VALUE; @@ -256,56 +260,58 @@ if (!fixmehack) { // CONTROLLER MAPPING --------- // // Synthesize left and right mouse click from controller, and get trigger values matching handControllerGrab. -function mapToAction(mapping, controller, button, action) { - if (!Controller.Hardware[controller]) { return; } // FIXME: recheck periodically! - mapping.from(Controller.Hardware[controller][button]).peek().to(Controller.Actions[action]); -} - -var triggerMenuMapping = Controller.newMapping(Script.resolvePath('') + '-trigger'); -Script.scriptEnding.connect(triggerMenuMapping.disable); +var triggerMapping; var leftTrigger = new Trigger(); var rightTrigger = new Trigger(); -triggerMenuMapping.from([Controller.Standard.RT]).peek().to(rightTrigger.triggerPress); -triggerMenuMapping.from([Controller.Standard.LT]).peek().to(leftTrigger.triggerPress); -mapToAction(triggerMenuMapping, 'Hydra', 'R4', 'ContextMenu'); -mapToAction(triggerMenuMapping, 'Hydra', 'L4', 'ContextMenu'); -triggerMenuMapping.enable(); -var clickMapping = Controller.newMapping(Script.resolvePath('') + '-click'); -Script.scriptEnding.connect(clickMapping.disable); -mapToAction(clickMapping, 'Hydra', 'R3', 'ReticleClick'); -mapToAction(clickMapping, 'Hydra', 'L3', 'ReticleClick'); -mapToAction(clickMapping, 'Vive', 'LeftPrimaryThumb', 'ReticleClick'); -mapToAction(clickMapping, 'Vive', 'RightPrimaryThumb', 'ReticleClick'); -var clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); -clickMapToggle.setState(true); -/* +// Create clickMappings as needed, on demand. +var clickMappings = {}, clickMapping, clickMapToggle; var hardware; // undefined -var leftTrigger = new Trigger(); -var rightTrigger = new Trigger(); function checkHardware() { + // FIX SYSTEM BUG: This does not work when hardware changes. + // https://app.asana.com/0/26225263936266/118428633439654 var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive': null); // not undefined if (hardware === newHardware) { return; } - if (hardware) { - triggerMenuMapping.disable(); - clickMapping.disable(); + print('Setting mapping for new controller hardware:', newHardware); + if (clickMapToggle) { + clickMapToggle.setState(false); + triggerMapping.disable(); } hardware = newHardware; - triggerMenuMapping.from([Controller.Standard.RT]).peek().to(rightTrigger.triggerPress); - triggerMenuMapping.from([Controller.Standard.LT]).peek().to(leftTrigger.triggerPress); - mapToAction(triggerMenuMapping, 'Hydra', 'R4', 'ContextMenu'); - mapToAction(triggerMenuMapping, 'Hydra', 'L4', 'ContextMenu'); - triggerMenuMapping.enable(); + if (clickMappings[hardware]) { + clickMapping = clickMappings[hardware].click; + triggerMapping = clickMappings[hardware].trigger; + } else { + clickMapping = Controller.newMapping(Script.resolvePath('') + '-click-' + hardware); + Script.scriptEnding.connect(clickMapping.disable); + function mapToAction(button, action) { + clickMapping.from(Controller.Hardware[hardware][button]).peek().to(Controller.Actions[action]); + } + switch (hardware) { + case 'Hydra': + mapToAction('R3', 'ReticleClick'); + mapToAction('L3', 'ReticleClick'); + mapToAction('R4', 'ContextMenu'); + mapToAction('L4', 'ContextMenu'); + break; + case 'Vive': + mapToAction('LeftPrimaryThumb', 'ReticleClick'); + mapToAction('RightPrimaryThumb', 'ReticleClick'); + break; + } - mapToAction(clickMapping, 'Hydra', 'R3', 'ReticleClick'); - mapToAction(clickMapping, 'Hydra', 'L3', 'ReticleClick'); - mapToAction(clickMapping, 'Vive', 'LeftPrimaryThumb', 'ReticleClick'); - mapToAction(clickMapping, 'Vive', 'RightPrimaryThumb', 'ReticleClick'); - var clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); + triggerMapping = Controller.newMapping(Script.resolvePath('') + '-trigger-' + hardware); + Script.scriptEnding.connect(triggerMapping.disable); + triggerMapping.from([Controller.Standard.RT]).peek().to(rightTrigger.triggerPress); + triggerMapping.from([Controller.Standard.LT]).peek().to(leftTrigger.triggerPress); + + clickMappings[hardware] = {click: clickMapping, trigger: triggerMapping}; + } + clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); clickMapToggle.setState(true); + triggerMapping.enable(); } checkHardware(); -*/ // VISUAL AID ----------- @@ -462,7 +468,8 @@ Script.scriptEnding.connect(function () { Script.clearInterval(updater); }); var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds function checkSettings() { updateFieldOfView(); - checkForDepthReticleScript() + checkForDepthReticleScript(); + checkHardware();; } checkSettings(); var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); From 9f5ee103749f8e8b089c63241f17e55e4f3f1807 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 29 Apr 2016 10:40:08 -0700 Subject: [PATCH 15/33] Simplification, with just a fake projection ball shown where click will occur in 3d. --- examples/controllers/handControllerPointer.js | 200 +++--------------- 1 file changed, 35 insertions(+), 165 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 14acc62e7a..794af64898 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -19,14 +19,14 @@ print('handControllerPointer version', 10); // For now: // Button 3 is left-mouse, button 4 is right-mouse. What to do on Vive? // First-person only. -// Right hand only. -// On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) -// There's an additional experimental feature that isn't quite what we want: -// A partial trigger squeeze (like "hand controller search") turns on a laser. The red ball (mostly) works as a click on in-world -// entities and 3d overlays. We'd like that red ball to be at the end of the termination of the laser line, but right now its on the HUD. +// Right hand only. FIXME +// When over a HUD element, the reticle is shown where the active hand controller beam intersects the HUD. +// Otherwise, the active hand controller shows a red ball where a click will act. // // Bugs: +// On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) // While hardware mouse move switches to mouse move, hardware mouse click (without amove) does not. +// lockout after click on 2d overlay? var wasRunningDepthReticle = false; function checkForDepthReticleScript() { @@ -268,14 +268,16 @@ var rightTrigger = new Trigger(); var clickMappings = {}, clickMapping, clickMapToggle; var hardware; // undefined function checkHardware() { - // FIX SYSTEM BUG: This does not work when hardware changes. - // https://app.asana.com/0/26225263936266/118428633439654 var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive': null); // not undefined if (hardware === newHardware) { return; } print('Setting mapping for new controller hardware:', newHardware); if (clickMapToggle) { clickMapToggle.setState(false); triggerMapping.disable(); + // FIX SYSTEM BUG: This does not work when hardware changes. + Window.alert("This isn't likely to work because of " + + 'https://app.asana.com/0/26225263936266/118428633439654\n' + + "You'll probably need to restart interface."); } hardware = newHardware; if (clickMappings[hardware]) { @@ -290,12 +292,9 @@ function checkHardware() { switch (hardware) { case 'Hydra': mapToAction('R3', 'ReticleClick'); - mapToAction('L3', 'ReticleClick'); mapToAction('R4', 'ContextMenu'); - mapToAction('L4', 'ContextMenu'); break; case 'Vive': - mapToAction('LeftPrimaryThumb', 'ReticleClick'); mapToAction('RightPrimaryThumb', 'ReticleClick'); break; } @@ -315,29 +314,10 @@ checkHardware(); // VISUAL AID ----------- -var LASER_COLOR = {red: 10, green: 10, blue: 255}; -var laserLine = Overlays.addOverlay("line3d", { // same properties as handControllerGrab search line - lineWidth: 5, - // FIX SYSTEM BUG?: If you don't supply a start and end at creation, it will never show up, even after editing. - start: MyAvatar.position, - end: Vec3.ZERO, - color: LASER_COLOR, - ignoreRayIntersection: true, - visible: false, - alpha: 1 -}); +// Same properties as handControllerGrab search sphere var BALL_SIZE = 0.011; var BALL_ALPHA = 0.5; -var laserBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere - size: BALL_SIZE, - color: LASER_COLOR, - ignoreRayIntersection: true, - alpha: BALL_ALPHA, - visible: false, - solid: true, - drawInFront: true // Even when burried inside of something, show it. -}); -var fakeProjectionBall = Overlays.addOverlay("sphere", { // Same properties as handControllerGrab search sphere +var fakeProjectionBall = Overlays.addOverlay("sphere", { size: 5 * BALL_SIZE, color: {red: 255, green: 10, blue: 10}, ignoreRayIntersection: true, @@ -346,14 +326,12 @@ var fakeProjectionBall = Overlays.addOverlay("sphere", { // Same properties as h solid: true, drawInFront: true // Even when burried inside of something, show it. }); -var overlays = [laserBall, laserLine, fakeProjectionBall]; +var overlays = [fakeProjectionBall]; // If we want to try showing multiple balls and lasers. Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. -var wantsVisualization = false; function turnOffLaser(optionalEnableClicks) { if (!optionalEnableClicks) { expireMouseCursor(); - wantsVisualization = false; } if (!visualizationIsShowing) { return; } visualizationIsShowing = false; @@ -364,60 +342,33 @@ function turnOffLaser(optionalEnableClicks) { } var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. function updateLaser(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { - if (!wantsVisualization) { return false; } - // Show the laser and intersect it with 3d overlays and entities. + // Show an indication of where the cursor will appear when crossing a HUD element, + // and where in-world clicking will occur. + // + // There are a number of ways we could do this, but for now, it's a blue sphere that rolls along + // the HUD surface, and a red sphere that rolls along the 3d objects that will receive the click. + // We'll leave it to other scripts (like handControllerGrab) to show a search beam when desired. + function intersection3d(position, direction) { + // Answer in-world intersection (entity or 3d overlay), or way-out point var pickRay = {origin: position, direction: direction}; var result = findRayIntersection(pickRay); return result.intersects ? result.intersection : Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction)); } - var termination = intersection3d(controllerPosition, controllerDirection); visualizationIsShowing = true; - Overlays.editOverlay(laserLine, {visible: true, start: controllerPosition, end: termination}); - // We show the ball at the hud intersection rather than at the termination because: - // 1) As you swing the laser in space, it's hard to judge where it will intersect with a HUD element, - // unless the intersection of the laser with the HUD is marked. But it's confusing to do that - // with the pointer, so we use the ball. - // 2) On some objects, the intersection is just enough inside the object that we're not going to see - // the ball anyway. - Overlays.editOverlay(laserBall, {visible: true, position: hudPosition3d}); + // We'd rather in-world interactions be done at the termination of the hand beam + // -- intersection3d(controllerPosition, controllerDirection). Maybe have handControllerGrab + // direclty manipulate both entity and 3d overlay objects. + // For now, though, we present a false projection of the cursor onto whatever is below it. This is + // different from the hand beam termination because the false projection is from the camera, while + // the hand beam termination is from the hand. + var eye = Camera.getPosition(); + var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); + Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); + setReticleVisible(false); - if (!fixmehack) { - // We really want in-world interactions to take place at termination: - // - We could do some of that with callEntityMethod (e.g., light switch entity script) - // - But we would have to alter edit.js to accept synthetic mouse data. - // So for now, we present a false projection of the cursor onto whatever is below it. This is different from - // the laser termination because the false projection is from the camera, while the laser termination is from the hand. - var eye = Camera.getPosition(); - var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); - Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); - setReticleVisible(false); - } else { - // Hack: Move the pointer again, this time to the intersection. This allows "clicking" on - // 3D entities without rewriting other parts of the system, but it isn't right, - // because the line from camera to the new mouse position might intersect different things - // than the line from controllerPosition to termination. - var eye = Camera.getPosition(); - var apparentHudTermination3d = calculateRayUICollisionPoint(eye, Vec3.subtract(termination, eye)); - var apparentHudTermination2d = overlayFromWorldPoint(apparentHudTermination3d); - Overlays.editOverlay(fakeProjectionBall, {visible: true, position: termination}); - setReticleVisible(false); - - setReticlePosition(apparentHudTermination2d); - /*if (isPointingAtOverlay(apparentHudTermination2d)) { - // The intersection could be at a point underneath a HUD element! (I said this was a hack.) - // If so, set things back so we don't oscillate. - setReticleVisible(false); - setReticlePosition(hudPosition2d) - return true; - } else { - setReticleVisible(true); - }*/ - if (HMD.active) { Reticle.depth = Vec3.distance(eye, apparentHudTermination3d); } - } - - return true; + return visualizationIsShowing; // In case we change caller to act conditionally. } // MAIN OPERATIONS ----------- @@ -428,7 +379,6 @@ function update() { if (!handControllerLockOut.expired(now)) { return turnOffLaser(); } // Let them use mouse it in peace. if (!Menu.isOptionChecked("First Person")) { return turnOffLaser(); } // What to do? menus can be behind hand! if (rightTrigger.triggerSmoothedGrab()) { handControllerLockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. - if (rightTrigger.triggerSmoothedSqueezed()) { wantsVisualization = true; } var hand = Controller.Standard.RightHand; var controllerPose = getControllerPose(hand); if (!controllerPose.valid) { return turnOffLaser(); } // Controller is cradled. @@ -446,18 +396,14 @@ function update() { setReticlePosition(hudPoint2d); // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. if (isPointingAtOverlay(hudPoint2d)) { + if (HMD.active) { // Doesn't hurt anything without the guard, but consider it documentation. + Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + } setReticleVisible(true); - Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! - clickMapToggle.setState(true); return turnOffLaser(true); } // We are not pointing at a HUD element (but it could be a 3d overlay). - var visualization3d = updateLaser(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); - clickMapToggle.setState(visualization3d); - if (!visualization3d) { - setReticleVisible(false); - turnOffLaser(); - } + updateLaser(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); } var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. @@ -474,79 +420,3 @@ function checkSettings() { checkSettings(); var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); }); - - -// DEBUGGING WITHOUT HYDRA ----------------------- -// -// The rest of this is for debugging without working hand controllers, using a line from camera to mouse, and an image for cursor. -var CONTROLLER_ROTATION = Quat.fromPitchYawRollDegrees(90, 180, -90); -if (false && !Controller.Hardware.Hydra) { - print('WARNING: no hand controller detected. Using mouse!'); - var mouseKeeper = {x: 0, y: 0}; - var onMouseMoveCapture = function (event) { mouseKeeper.x = event.x; mouseKeeper.y = event.y; }; - setupHandler(Controller.mouseMoveEvent, onMouseMoveCapture); - getControllerPose = function () { - var size = Controller.getViewportDimensions(); - var handPoint = Vec3.subtract(Camera.getPosition(), MyAvatar.position); // Pretend controller is at camera - - // In world-space 3D meters: - var rotation = Camera.getOrientation(); - var normal = Quat.getFront(rotation); - var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); - var hudWidth = hudHeight * size.x / size.y; - var rightFraction = mouseKeeper.x / size.x - 0.5; - var rightMeters = rightFraction * hudWidth; - var upFraction = mouseKeeper.y / size.y - 0.5; - var upMeters = upFraction * hudHeight * -1; - var right = Vec3.multiply(Quat.getRight(rotation), rightMeters); - var up = Vec3.multiply(Quat.getUp(rotation), upMeters); - var direction = Vec3.sum(normal, Vec3.sum(right, up)); - var mouseRotation = Quat.rotationBetween(normal, direction); - - var controllerRotation = Quat.multiply(Quat.multiply(mouseRotation, rotation), CONTROLLER_ROTATION); - var inverseAvatar = Quat.inverse(MyAvatar.orientation); - return { - valid: true, - translation: Vec3.multiplyQbyV(inverseAvatar, handPoint), - rotation: Quat.multiply(inverseAvatar, controllerRotation) - }; - }; - // We can't set the mouse if we're using the mouse as a fake controller. So stick an image where we would be putting the mouse. - // WARNING: This fake cursor is an overlay that will be the target of clicks and drags rather than other overlays underneath it! - var reticleHalfSize = 16; - var fakeReticle = Overlays.addOverlay("image", { - imageURL: "http://s3.amazonaws.com/hifi-public/images/delete.png", - width: 2 * reticleHalfSize, - height: 2 * reticleHalfSize, - alpha: 0.7 - }); - Script.scriptEnding.connect(function () { Overlays.deleteOverlay(fakeReticle); }); - setReticlePosition = function (hudPoint2d) { - weMovedReticle = true; - Overlays.editOverlay(fakeReticle, {x: hudPoint2d.x - reticleHalfSize, y: hudPoint2d.y - reticleHalfSize}); - }; - setReticleVisible = function (on) { - Reticle.visible = on; - Overlays.editOverlay(fakeReticle, {visible: on}); - }; - // The idea here is that we not return a truthy result constantly when we display the fake reticle. - // But this is done wrong when we're over another overlay as well: if we hit the fakeReticle, we incorrectly answer null here. - // FIXME: display fake reticle slightly off to the side instead. - getOverlayAtPoint = function (point2d) { - var overlay = Overlays.getOverlayAtPoint(point2d); - if (overlay === fakeReticle) { return null; } - return overlay; - }; - var fakeTrigger = 0; - getValue = function () { var trigger = fakeTrigger; fakeTrigger = 0; return trigger; }; - setupHandler(Controller.keyPressEvent, function (event) { - switch (event.text) { - case '`': - fakeTrigger = 0.4; - break; - case '~': - fakeTrigger = 0.9; - break; - } - }); -} From e847948f9b82caaa1776a14458874300098dcd84 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 29 Apr 2016 11:01:07 -0700 Subject: [PATCH 16/33] Handle away mode. --- examples/controllers/handControllerPointer.js | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 794af64898..87a52a6a10 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -141,7 +141,9 @@ var getOverlayAtPoint = Overlays.getOverlayAtPoint; var setReticleVisible = function (on) { Reticle.visible = on; }; var weMovedReticle = false; -function handControllerMovedReticle() { // I.e., change in cursor position is from this script, not the mouse. +function ignoreMouseActivity() { + // If we're paused, or if change in cursor position is from this script, not the hardware mouse. + if (!Reticle.allowMouseCapture) { return true; } // Only we know if we moved it, which is why this script has to replace depthReticle.js if (!weMovedReticle) { return false; } weMovedReticle = false; @@ -218,12 +220,11 @@ function overlayFromWorldPoint(point) { var mouseCursorActivity = new TimeLock(5000); var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant function updateMouseActivity(isClick) { - if (handControllerMovedReticle()) { return; } + if (ignoreMouseActivity()) { return; } var now = Date.now(); mouseCursorActivity.update(now); if (isClick) { return; } // Bug: mouse clicks should keep going. Just not hand controller clicks // FIXME: Does not yet seek to lookAt upon waking. - // FIXME not unless Reticle.allowMouseCapture handControllerLockOut.update(now); setReticleVisible(true); } @@ -234,7 +235,7 @@ function expireMouseCursor(now) { } function onMouseMove() { // Display cursor at correct depth (as in depthReticle.js), and updateMouseActivity. - if (handControllerMovedReticle()) { return; } + if (ignoreMouseActivity()) { return; } if (HMD.active) { // set depth // FIXME: does not yet adjust slowly. @@ -250,12 +251,9 @@ function onMouseMove() { function onMouseClick() { updateMouseActivity(true); } -var fixmehack = false; -if (!fixmehack) { - setupHandler(Controller.mouseMoveEvent, onMouseMove); - setupHandler(Controller.mousePressEvent, onMouseClick); - setupHandler(Controller.mouseDoublePressEvent, onMouseClick); -} +setupHandler(Controller.mouseMoveEvent, onMouseMove); +setupHandler(Controller.mousePressEvent, onMouseClick); +setupHandler(Controller.mouseDoublePressEvent, onMouseClick); // CONTROLLER MAPPING --------- // @@ -329,19 +327,18 @@ var fakeProjectionBall = Overlays.addOverlay("sphere", { var overlays = [fakeProjectionBall]; // If we want to try showing multiple balls and lasers. Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. -function turnOffLaser(optionalEnableClicks) { +function turnOffVisualization(optionalEnableClicks) { // because we're showing cursor on HUD if (!optionalEnableClicks) { expireMouseCursor(); } if (!visualizationIsShowing) { return; } visualizationIsShowing = false; - //toggleMap.setState(optionalEnableClicks); overlays.forEach(function (overlay) { Overlays.editOverlay(overlay, {visible: false}); }); } var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. -function updateLaser(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { +function updateVisualization(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { // Show an indication of where the cursor will appear when crossing a HUD element, // and where in-world clicking will occur. // @@ -376,19 +373,18 @@ function updateLaser(controllerPosition, controllerDirection, hudPosition3d, hud function update() { var now = Date.now(); rightTrigger.updateSmoothedTrigger(); - if (!handControllerLockOut.expired(now)) { return turnOffLaser(); } // Let them use mouse it in peace. - if (!Menu.isOptionChecked("First Person")) { return turnOffLaser(); } // What to do? menus can be behind hand! - if (rightTrigger.triggerSmoothedGrab()) { handControllerLockOut.update(now); return turnOffLaser(); } // Interferes with other scripts. + if (!handControllerLockOut.expired(now)) { return turnOffVisualization(); } // Let them use mouse it in peace. + if (!Menu.isOptionChecked("First Person")) { return turnOffVisualization(); } // What to do? menus can be behind hand! var hand = Controller.Standard.RightHand; var controllerPose = getControllerPose(hand); - if (!controllerPose.valid) { return turnOffLaser(); } // Controller is cradled. + if (!controllerPose.valid) { return turnOffVisualization(); } // Controller is cradled. var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); - if (!hudPoint3d) { print('Controller is parallel to HUD'); return turnOffLaser(); } + if (!hudPoint3d) { print('Controller is parallel to HUD'); return turnOffVisualization(); } var hudPoint2d = overlayFromWorldPoint(hudPoint3d); // We don't know yet if we'll want to make the cursor visble, but we need to move it to see if @@ -400,10 +396,10 @@ function update() { Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! } setReticleVisible(true); - return turnOffLaser(true); + return turnOffVisualization(true); } // We are not pointing at a HUD element (but it could be a 3d overlay). - updateLaser(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); + updateVisualization(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); } var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. From 37380556f67419a0c1a0442d72b1a8885cc6d026 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 29 Apr 2016 11:32:29 -0700 Subject: [PATCH 17/33] checkpoint basic handedness (before the remapping part) --- examples/controllers/handControllerPointer.js | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 87a52a6a10..6d0973622a 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -109,7 +109,7 @@ function Trigger() { // smooth out trigger value that.triggerValue = (that.triggerValue * TRIGGER_SMOOTH_RATIO) + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); - debug(that.triggerValue); + debug(that.triggerValue); // FIXME }; that.triggerSmoothedGrab = function() { return that.triggerValue > TRIGGER_GRAB_VALUE; @@ -310,6 +310,17 @@ function checkHardware() { } checkHardware(); +var activeHand = Controller.Standard.RightHand; +var activeTrigger = rightTrigger; +function toggleHand() { + if (activeHand === Controller.Standard.RightHand) { + activeHand = Controller.Standard.LeftHand; + activeTrigger = leftTrigger; + } else { + activeHand = Controller.Standard.RightHand; + activeTrigger = rightTrigger; + } +} // VISUAL AID ----------- // Same properties as handControllerGrab search sphere @@ -372,11 +383,14 @@ function updateVisualization(controllerPosition, controllerDirection, hudPositio // function update() { var now = Date.now(); + leftTrigger.updateSmoothedTrigger(); rightTrigger.updateSmoothedTrigger(); if (!handControllerLockOut.expired(now)) { return turnOffVisualization(); } // Let them use mouse it in peace. + + if (activeTrigger.triggerSmoothedSqueezed()) { toggleHand(); } + if (!Menu.isOptionChecked("First Person")) { return turnOffVisualization(); } // What to do? menus can be behind hand! - var hand = Controller.Standard.RightHand; - var controllerPose = getControllerPose(hand); + var controllerPose = getControllerPose(activeHand); if (!controllerPose.valid) { return turnOffVisualization(); } // Controller is cradled. var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); From b3fda55ea9b612c6324bb3530aad032e28e61cca Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 29 Apr 2016 11:39:36 -0700 Subject: [PATCH 18/33] handedness --- examples/controllers/handControllerPointer.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 6d0973622a..223a34658f 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -17,16 +17,16 @@ print('handControllerPointer version', 10); // Control the "mouse" using hand controller. (HMD and desktop.) // For now: -// Button 3 is left-mouse, button 4 is right-mouse. What to do on Vive? +// Thumb utton 3 is left-mouse, button 4 is right-mouse. What to do on Vive? (Currently primary thumb is left click.) // First-person only. -// Right hand only. FIXME +// Starts right handed, but switches to whichever is free: Whichever hand was NOT most recently squeezed. +// (For now, the thumb buttons on both controllers are always on.) // When over a HUD element, the reticle is shown where the active hand controller beam intersects the HUD. // Otherwise, the active hand controller shows a red ball where a click will act. // // Bugs: // On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) // While hardware mouse move switches to mouse move, hardware mouse click (without amove) does not. -// lockout after click on 2d overlay? var wasRunningDepthReticle = false; function checkForDepthReticleScript() { @@ -109,7 +109,6 @@ function Trigger() { // smooth out trigger value that.triggerValue = (that.triggerValue * TRIGGER_SMOOTH_RATIO) + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); - debug(that.triggerValue); // FIXME }; that.triggerSmoothedGrab = function() { return that.triggerValue > TRIGGER_GRAB_VALUE; @@ -291,9 +290,12 @@ function checkHardware() { case 'Hydra': mapToAction('R3', 'ReticleClick'); mapToAction('R4', 'ContextMenu'); + mapToAction('L3', 'ReticleClick'); + mapToAction('L4', 'ContextMenu'); break; case 'Vive': mapToAction('RightPrimaryThumb', 'ReticleClick'); + mapToAction('LeftPrimaryThumb', 'ReticleClick'); break; } From b05bbdd041ba14d6fab03b76243219fd31ebfa9c Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 29 Apr 2016 13:54:02 -0700 Subject: [PATCH 19/33] don't oscillate hands when both triggers are pulled. --- examples/controllers/handControllerPointer.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 223a34658f..e7eba78a45 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -13,8 +13,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -print('handControllerPointer version', 10); - // Control the "mouse" using hand controller. (HMD and desktop.) // For now: // Thumb utton 3 is left-mouse, button 4 is right-mouse. What to do on Vive? (Currently primary thumb is left click.) @@ -314,13 +312,16 @@ checkHardware(); var activeHand = Controller.Standard.RightHand; var activeTrigger = rightTrigger; +var inactiveTrigger = leftTrigger; function toggleHand() { if (activeHand === Controller.Standard.RightHand) { activeHand = Controller.Standard.LeftHand; activeTrigger = leftTrigger; + inactiveTrigger = rightTrigger; } else { activeHand = Controller.Standard.RightHand; activeTrigger = rightTrigger; + inactiveTrigger = leftTrigger; } } @@ -389,7 +390,7 @@ function update() { rightTrigger.updateSmoothedTrigger(); if (!handControllerLockOut.expired(now)) { return turnOffVisualization(); } // Let them use mouse it in peace. - if (activeTrigger.triggerSmoothedSqueezed()) { toggleHand(); } + if (activeTrigger.triggerSmoothedSqueezed() && !inactiveTrigger.triggerSmoothedSqueezed()) { toggleHand(); } if (!Menu.isOptionChecked("First Person")) { return turnOffVisualization(); } // What to do? menus can be behind hand! var controllerPose = getControllerPose(activeHand); From cd0737446bb371113a8f000561a4cc5f12e08523 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 29 Apr 2016 16:56:30 -0700 Subject: [PATCH 20/33] cleanup --- examples/controllers/handControllerPointer.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index e7eba78a45..4652c9258c 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -99,22 +99,22 @@ function Trigger() { var that = this; that.triggerValue = 0; // rolling average of trigger value that.rawTriggerValue = 0; - that.triggerPress = function(value) { + that.triggerPress = function (value) { that.rawTriggerValue = value; }; - that.updateSmoothedTrigger = function() { + that.updateSmoothedTrigger = function () { var triggerValue = that.rawTriggerValue; // smooth out trigger value that.triggerValue = (that.triggerValue * TRIGGER_SMOOTH_RATIO) + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); }; - that.triggerSmoothedGrab = function() { + that.triggerSmoothedGrab = function () { return that.triggerValue > TRIGGER_GRAB_VALUE; }; - that.triggerSmoothedSqueezed = function() { + that.triggerSmoothedSqueezed = function () { return that.triggerValue > TRIGGER_ON_VALUE; }; - that.triggerSmoothedReleased = function() { + that.triggerSmoothedReleased = function () { return that.triggerValue < TRIGGER_OFF_VALUE; }; } @@ -263,7 +263,7 @@ var rightTrigger = new Trigger(); var clickMappings = {}, clickMapping, clickMapToggle; var hardware; // undefined function checkHardware() { - var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive': null); // not undefined + var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive' : null); // not undefined if (hardware === newHardware) { return; } print('Setting mapping for new controller hardware:', newHardware); if (clickMapToggle) { @@ -292,15 +292,15 @@ function checkHardware() { mapToAction('L4', 'ContextMenu'); break; case 'Vive': - mapToAction('RightPrimaryThumb', 'ReticleClick'); - mapToAction('LeftPrimaryThumb', 'ReticleClick'); + mapToAction('RS', 'ReticleClick'); + mapToAction('LS', 'ReticleClick'); break; } triggerMapping = Controller.newMapping(Script.resolvePath('') + '-trigger-' + hardware); Script.scriptEnding.connect(triggerMapping.disable); - triggerMapping.from([Controller.Standard.RT]).peek().to(rightTrigger.triggerPress); - triggerMapping.from([Controller.Standard.LT]).peek().to(leftTrigger.triggerPress); + triggerMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); + triggerMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); clickMappings[hardware] = {click: clickMapping, trigger: triggerMapping}; } @@ -329,7 +329,7 @@ function toggleHand() { // Same properties as handControllerGrab search sphere var BALL_SIZE = 0.011; var BALL_ALPHA = 0.5; -var fakeProjectionBall = Overlays.addOverlay("sphere", { +var fakeProjectionBall = Overlays.addOverlay("sphere", { size: 5 * BALL_SIZE, color: {red: 255, green: 10, blue: 10}, ignoreRayIntersection: true, @@ -391,7 +391,7 @@ function update() { if (!handControllerLockOut.expired(now)) { return turnOffVisualization(); } // Let them use mouse it in peace. if (activeTrigger.triggerSmoothedSqueezed() && !inactiveTrigger.triggerSmoothedSqueezed()) { toggleHand(); } - + if (!Menu.isOptionChecked("First Person")) { return turnOffVisualization(); } // What to do? menus can be behind hand! var controllerPose = getControllerPose(activeHand); if (!controllerPose.valid) { return turnOffVisualization(); } // Controller is cradled. @@ -428,7 +428,7 @@ var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds function checkSettings() { updateFieldOfView(); checkForDepthReticleScript(); - checkHardware();; + checkHardware(); } checkSettings(); var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); From 6dd21d8c6fcd9e14a68757ef1002639dbcd99a1b Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 29 Apr 2016 16:59:16 -0700 Subject: [PATCH 21/33] cleanup --- examples/controllers/handControllerPointer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js index 4652c9258c..bf9accb9f0 100644 --- a/examples/controllers/handControllerPointer.js +++ b/examples/controllers/handControllerPointer.js @@ -15,7 +15,8 @@ // Control the "mouse" using hand controller. (HMD and desktop.) // For now: -// Thumb utton 3 is left-mouse, button 4 is right-mouse. What to do on Vive? (Currently primary thumb is left click.) +// Hydra thumb button 3 is left-mouse, button 4 is right-mouse. +// Vive thumb pad is left mouse (but that interferes with driveing!). Vive menu button is context menu (right mouse). // First-person only. // Starts right handed, but switches to whichever is free: Whichever hand was NOT most recently squeezed. // (For now, the thumb buttons on both controllers are always on.) From 773770a7fc4ed18da74364ac69ab3feb1e2e0d02 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 3 May 2016 07:01:23 -0700 Subject: [PATCH 22/33] All but origin-correction. --- interface/resources/controllers/vive.json | 8 +- .../impl/filters/DeadZoneFilter.cpp | 7 +- .../src/display-plugins/CompositorHelper.cpp | 10 - scripts/defaultScripts.js | 2 +- .../controllers/handControllerPointer.js | 450 ++++++++++++++++++ scripts/system/depthReticle.js | 185 ------- 6 files changed, 459 insertions(+), 203 deletions(-) create mode 100644 scripts/system/controllers/handControllerPointer.js delete mode 100644 scripts/system/depthReticle.js diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 1f71658946..d2d296aeea 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -1,15 +1,15 @@ { "name": "Vive to Standard", "channels": [ - { "from": "Vive.LY", "when": "Vive.LS", "filters": "invert", "to": "Standard.LY" }, - { "from": "Vive.LX", "when": "Vive.LS", "to": "Standard.LX" }, + { "from": "Vive.LY", "when": "Vive.LS", "filters": ["invert" ,{ "type": "deadZone", "min": 0.7 }], "to": "Standard.LY" }, + { "from": "Vive.LX", "when": "Vive.LS", "filters": [{ "type": "deadZone", "min": 0.7 }], "to": "Standard.LX" }, { "from": "Vive.LT", "to": "Standard.LT" }, { "from": "Vive.LB", "to": "Standard.LB" }, { "from": "Vive.LS", "to": "Standard.LS" }, - { "from": "Vive.RY", "when": "Vive.RS", "filters": "invert", "to": "Standard.RY" }, - { "from": "Vive.RX", "when": "Vive.RS", "to": "Standard.RX" }, + { "from": "Vive.RY", "when": "Vive.RS", "filters": ["invert", { "type": "deadZone", "min": 0.7 }], "to": "Standard.RY" }, + { "from": "Vive.RX", "when": "Vive.RS", "filters": [{ "type": "deadZone", "min": 0.7 }], "to": "Standard.RX" }, { "from": "Vive.RT", "to": "Standard.RT" }, { "from": "Vive.RB", "to": "Standard.RB" }, diff --git a/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp b/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp index 809308eeab..f07ef25976 100644 --- a/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp @@ -13,11 +13,12 @@ using namespace controller; float DeadZoneFilter::apply(float value) const { - float scale = 1.0f / (1.0f - _min); - if (std::abs(value) < _min) { + float scale = ((value < 0.0f) ? -1.0f : 1.0f) / (1.0f - _min); + float magnitude = std::abs(value); + if (magnitude < _min) { return 0.0f; } - return (value - _min) * scale; + return (magnitude - _min) * scale; } bool DeadZoneFilter::parseParameters(const QJsonValue& parameters) { diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index 4648fc8957..5bbf183141 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -322,16 +322,6 @@ void CompositorHelper::setReticlePosition(const glm::vec2& position, bool sendFa sendFakeMouseEvent(); } } else { - // NOTE: This is some debugging code we will leave in while debugging various reticle movement strategies, - // remove it after we're done - const float REASONABLE_CHANGE = 50.0f; - glm::vec2 oldPos = toGlm(QCursor::pos()); - auto distance = glm::distance(oldPos, position); - if (distance > REASONABLE_CHANGE) { - qDebug() << "Contrller::ScriptingInterface ---- UNREASONABLE CHANGE! distance:" << - distance << " oldPos:" << oldPos.x << "," << oldPos.y << " newPos:" << position.x << "," << position.y; - } - QCursor::setPos(position.x, position.y); } } diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index ceccf20647..d5001c7f53 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -15,7 +15,7 @@ Script.load("system/examples.js"); Script.load("system/selectAudioDevice.js"); Script.load("system/notifications.js"); Script.load("system/controllers/handControllerGrab.js"); +Script.load("system/controllers/handControllerPointer.js"); Script.load("system/controllers/squeezeHands.js"); Script.load("system/controllers/grab.js"); Script.load("system/dialTone.js"); -Script.load("system/depthReticle.js"); \ No newline at end of file diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js new file mode 100644 index 0000000000..229aa79fed --- /dev/null +++ b/scripts/system/controllers/handControllerPointer.js @@ -0,0 +1,450 @@ +"use strict"; +/*jslint vars: true, plusplus: true*/ +/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print */ + +// +// handControllerPointer.js +// examples/controllers +// +// Created by Howard Stearns on 2016/04/22 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Control the "mouse" using hand controller. (HMD and desktop.) +// For now: +// Hydra thumb button 3 is left-mouse, button 4 is right-mouse. +// A click in the center of the vive thumb pad is left mouse. Vive menu button is context menu (right mouse). +// First-person only. +// Starts right handed, but switches to whichever is free: Whichever hand was NOT most recently squeezed. +// (For now, the thumb buttons on both controllers are always on.) +// When over a HUD element, the reticle is shown where the active hand controller beam intersects the HUD. +// Otherwise, the active hand controller shows a red ball where a click will act. +// +// Bugs: +// On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) +// While hardware mouse move switches to mouse move, hardware mouse click (without amove) does not. + +// UTILITIES ------------- +// + +// Utility to make it easier to setup and disconnect cleanly. +function setupHandler(event, handler) { + event.connect(handler); + Script.scriptEnding.connect(function () { event.disconnect(handler); }); +} +// If some capability is not available until expiration milliseconds after the last update. +function TimeLock(expiration) { + var last = 0; + this.update = function (optionalNow) { + last = optionalNow || Date.now(); + }; + this.expired = function (optionalNow) { + return ((optionalNow || Date.now()) - last) > expiration; + }; +} +var handControllerLockOut = new TimeLock(2000); + +// Calls onFunction() or offFunction() when swtich(on), but only if it is to a new value. +function LatchedToggle(onFunction, offFunction, state) { + this.getState = function () { + return state; + }; + this.setState = function (on) { + if (state === on) { return; } + state = on; + if (on) { + onFunction(); + } else { + offFunction(); + } + }; +} + +// Code copied and adapted from handControllerGrab.js. We should refactor this. +function Trigger() { + var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing + var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab + var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab + var TRIGGER_OFF_VALUE = 0.15; + var that = this; + that.triggerValue = 0; // rolling average of trigger value + that.rawTriggerValue = 0; + that.triggerPress = function (value) { + that.rawTriggerValue = value; + }; + that.updateSmoothedTrigger = function () { + var triggerValue = that.rawTriggerValue; + // smooth out trigger value + that.triggerValue = (that.triggerValue * TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); + }; + that.triggerSmoothedGrab = function () { + return that.triggerValue > TRIGGER_GRAB_VALUE; + }; + that.triggerSmoothedSqueezed = function () { + return that.triggerValue > TRIGGER_ON_VALUE; + }; + that.triggerSmoothedReleased = function () { + return that.triggerValue < TRIGGER_OFF_VALUE; + }; +} + +// VERTICAL FIELD OF VIEW --------- +// +// Cache the verticalFieldOfView setting and update it every so often. +var verticalFieldOfView, DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees +function updateFieldOfView() { + verticalFieldOfView = Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW; +} + +// SHIMS ---------- +// +var weMovedReticle = false; +function ignoreMouseActivity() { + // If we're paused, or if change in cursor position is from this script, not the hardware mouse. + if (!Reticle.allowMouseCapture) { return true; } + // Only we know if we moved it, which is why this script has to replace depthReticle.js + if (!weMovedReticle) { return false; } + weMovedReticle = false; + return true; +} +var setReticlePosition = function (point2d) { + if (!HMD.active) { + // FIX SYSTEM BUG: setPosition is setting relative to screen origin, not the content area of the window. + // https://app.asana.com/0/26225263936266/118427643788550 + point2d = {x: point2d.x, y: point2d.y + 50}; + } + weMovedReticle = true; + Reticle.setPosition(point2d); +}; + +// Generalizations of utilities that work with system and overlay elements. +function findRayIntersection(pickRay) { + // Check 3D overlays and entities. Argument is an object with origin and direction. + var result = Overlays.findRayIntersection(pickRay); + if (!result.intersects) { + result = Entities.findRayIntersection(pickRay, true); + } + return result; +} +function isPointingAtOverlay(optionalHudPosition2d) { + return Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(optionalHudPosition2d || Reticle.position); +} + +// Generalized HUD utilities, with or without HMD: +// These two "vars" are for documentation. Do not change their values! +var SPHERICAL_HUD_DISTANCE = 1; // meters. +var PLANAR_PERPENDICULAR_HUD_DISTANCE = SPHERICAL_HUD_DISTANCE; +function calculateRayUICollisionPoint(position, direction) { + // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection. + if (HMD.active) { + return HMD.calculateRayUICollisionPoint(position, direction); + } + // interect HUD plane, 1m in front of camera, using formula: + // scale = hudNormal dot (hudPoint - position) / hudNormal dot direction + // intersection = postion + scale*direction + var hudNormal = Quat.getFront(Camera.getOrientation()); + var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // must also scale if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 + var denominator = Vec3.dot(hudNormal, direction); + if (denominator === 0) { return null; } // parallel to plane + var numerator = Vec3.dot(hudNormal, Vec3.subtract(hudPoint, position)); + var scale = numerator / denominator; + return Vec3.sum(position, Vec3.multiply(scale, direction)); +} +var DEGREES_TO_HALF_RADIANS = Math.PI / 360; +function overlayFromWorldPoint(point) { + // Answer the 2d pixel-space location in the HUD that covers the given 3D point. + // REQUIRES: that the 3d point be on the hud surface! + // Note that this is based on the Camera, and doesn't know anything about any + // ray that may or may not have been used to compute the point. E.g., the + // overlay point is NOT the intersection of some non-camera ray with the HUD. + if (HMD.active) { + return HMD.overlayFromWorldPoint(point); + } + var cameraToPoint = Vec3.subtract(point, Camera.getPosition()); + var cameraX = Vec3.dot(cameraToPoint, Quat.getRight(Camera.getOrientation())); + var cameraY = Vec3.dot(cameraToPoint, Quat.getUp(Camera.getOrientation())); + var size = Controller.getViewportDimensions(); + var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); // must adjust if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 + var hudWidth = hudHeight * size.x / size.y; + var horizontalFraction = (cameraX / hudWidth + 0.5); + var verticalFraction = 1 - (cameraY / hudHeight + 0.5); + var horizontalPixels = size.x * horizontalFraction; + var verticalPixels = size.y * verticalFraction; + return { x: horizontalPixels, y: verticalPixels }; +} + +// MOUSE ACTIVITY -------- +// +var isSeeking = false; +var averageMouseVelocity = 0, lastIntegration = 0, lastMouse; +var WEIGHTING = 1 / 20; // simple moving average over last 20 samples +var ONE_MINUS_WEIGHTING = 1 - WEIGHTING; +var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 2.5; +function isShakingMouse() { // True if the person is waving the mouse around trying to find it. + var now = Date.now(), mouse = Reticle.position, isShaking = false; + if (lastIntegration && (lastIntegration !== now)) { + var velocity = Vec3.length(Vec3.subtract(mouse, lastMouse)) / (now - lastIntegration); + averageMouseVelocity = (ONE_MINUS_WEIGHTING * averageMouseVelocity) + (WEIGHTING * velocity); + if (averageMouseVelocity > AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO) { + isShaking = true; + } + } + lastIntegration = now; + lastMouse = mouse; + return isShaking; +} +var NON_LINEAR_DIVISOR = 2; +var MINIMUM_SEEK_DISTANCE = 0.01; +function updateSeeking() { + if (!Reticle.visible || isShakingMouse()) { isSeeking = true; } // e.g., if we're about to turn it on with first movement. + if (!isSeeking) { return; } + averageMouseVelocity = lastIntegration = 0; + var lookAt2D = HMD.getHUDLookAtPosition2D(); + if (!lookAt2D) { return; } // E.g., if parallel to location in HUD + var copy = Reticle.position; // Idiomatic javascript would let us side effect the reticle, but this is a copy. + function updateDimension(axis) { + var distanceBetween = lookAt2D[axis] - Reticle.position[axis]; + var move = distanceBetween / NON_LINEAR_DIVISOR; + if (move >= MINIMUM_SEEK_DISTANCE) { return false; } + copy[axis] += move; + return true; + } + if (!updateDimension('x') && !updateDimension('y')) { + isSeeking = false; + } else { + Reticle.position = copy; + } +} + +var mouseCursorActivity = new TimeLock(5000); +var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant +function updateMouseActivity(isClick) { + if (ignoreMouseActivity()) { return; } + var now = Date.now(); + mouseCursorActivity.update(now); + if (isClick) { return; } // Bug: mouse clicks should keep going. Just not hand controller clicks + handControllerLockOut.update(now); + Reticle.visible = true; +} +function expireMouseCursor(now) { + if (!isPointingAtOverlay() && mouseCursorActivity.expired(now)) { + Reticle.visible = false; + } +} +function onMouseMove() { + // Display cursor at correct depth (as in depthReticle.js), and updateMouseActivity. + if (ignoreMouseActivity()) { return; } + + if (HMD.active) { // set depth + updateSeeking(); + if (isPointingAtOverlay()) { + Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + } else { + var result = findRayIntersection(Camera.computePickRay(Reticle.position.x, Reticle.position.y)); + Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; + } + } + updateMouseActivity(); // After the above, just in case the depth movement is awkward when becoming visible. +} +function onMouseClick() { + updateMouseActivity(true); +} +setupHandler(Controller.mouseMoveEvent, onMouseMove); +setupHandler(Controller.mousePressEvent, onMouseClick); +setupHandler(Controller.mouseDoublePressEvent, onMouseClick); + +// CONTROLLER MAPPING --------- +// +// Synthesize left and right mouse click from controller, and get trigger values matching handControllerGrab. +var triggerMapping; +var leftTrigger = new Trigger(); +var rightTrigger = new Trigger(); + +// Create clickMappings as needed, on demand. +var clickMappings = {}, clickMapping, clickMapToggle; +var hardware; // undefined +function checkHardware() { + var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive' : null); // not undefined + if (hardware === newHardware) { return; } + print('Setting mapping for new controller hardware:', newHardware); + if (clickMapToggle) { + clickMapToggle.setState(false); + triggerMapping.disable(); + // FIX SYSTEM BUG: This does not work when hardware changes. + Window.alert("This isn't likely to work because of " + + 'https://app.asana.com/0/26225263936266/118428633439654\n' + + "You'll probably need to restart interface."); + } + hardware = newHardware; + if (clickMappings[hardware]) { + clickMapping = clickMappings[hardware].click; + triggerMapping = clickMappings[hardware].trigger; + } else { + clickMapping = Controller.newMapping(Script.resolvePath('') + '-click-' + hardware); + Script.scriptEnding.connect(clickMapping.disable); + function mapToAction(button, action) { + clickMapping.from(Controller.Hardware[hardware][button]).peek().to(Controller.Actions[action]); + } + function makeViveWhen(click, x, y) { + var viveClick = Controller.Hardware.Vive[click], + viveX = Controller.Hardware.Vive[x], + viveY = Controller.Hardware.Vive[y]; + return function () { + return Controller.getValue(viveClick) && !Controller.getValue(viveX) && !Controller.getValue(viveY); + }; + } + switch (hardware) { + case 'Hydra': + mapToAction('R3', 'ReticleClick'); + mapToAction('L3', 'ReticleClick'); + mapToAction('R4', 'ContextMenu'); + mapToAction('L4', 'ContextMenu'); + break; + case 'Vive': + // When touchpad click is NOT treated as movement, treat as left click + clickMapping.from(Controller.Hardware.Vive.RS).when(makeViveWhen('RS', 'RX', 'RY')).to(Controller.Actions.ReticleClick); + clickMapping.from(Controller.Hardware.Vive.LS).when(makeViveWhen('LS', 'LX', 'LY')).to(Controller.Actions.ReticleClick); + mapToAction('RightApplicationMenu', 'ContextMenu'); + mapToAction('LeftApplicationMenu', 'ContextMenu'); + break; + } + + triggerMapping = Controller.newMapping(Script.resolvePath('') + '-trigger-' + hardware); + Script.scriptEnding.connect(triggerMapping.disable); + triggerMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); + triggerMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); + + clickMappings[hardware] = {click: clickMapping, trigger: triggerMapping}; + } + clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); + clickMapToggle.setState(true); + triggerMapping.enable(); +} +checkHardware(); + +var activeHand = Controller.Standard.RightHand; +var activeTrigger = rightTrigger; +var inactiveTrigger = leftTrigger; +function toggleHand() { + if (activeHand === Controller.Standard.RightHand) { + activeHand = Controller.Standard.LeftHand; + activeTrigger = leftTrigger; + inactiveTrigger = rightTrigger; + } else { + activeHand = Controller.Standard.RightHand; + activeTrigger = rightTrigger; + inactiveTrigger = leftTrigger; + } +} + +// VISUAL AID ----------- +// Same properties as handControllerGrab search sphere +var BALL_SIZE = 0.011; +var BALL_ALPHA = 0.5; +var fakeProjectionBall = Overlays.addOverlay("sphere", { + size: 5 * BALL_SIZE, + color: {red: 255, green: 10, blue: 10}, + ignoreRayIntersection: true, + alpha: BALL_ALPHA, + visible: false, + solid: true, + drawInFront: true // Even when burried inside of something, show it. +}); +var overlays = [fakeProjectionBall]; // If we want to try showing multiple balls and lasers. +Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); +var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. +function turnOffVisualization(optionalEnableClicks) { // because we're showing cursor on HUD + if (!optionalEnableClicks) { + expireMouseCursor(); + } + if (!visualizationIsShowing) { return; } + visualizationIsShowing = false; + overlays.forEach(function (overlay) { + Overlays.editOverlay(overlay, {visible: false}); + }); +} +var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. +function updateVisualization(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { + // Show an indication of where the cursor will appear when crossing a HUD element, + // and where in-world clicking will occur. + // + // There are a number of ways we could do this, but for now, it's a blue sphere that rolls along + // the HUD surface, and a red sphere that rolls along the 3d objects that will receive the click. + // We'll leave it to other scripts (like handControllerGrab) to show a search beam when desired. + + function intersection3d(position, direction) { + // Answer in-world intersection (entity or 3d overlay), or way-out point + var pickRay = {origin: position, direction: direction}; + var result = findRayIntersection(pickRay); + return result.intersects ? result.intersection : Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction)); + } + + visualizationIsShowing = true; + // We'd rather in-world interactions be done at the termination of the hand beam + // -- intersection3d(controllerPosition, controllerDirection). Maybe have handControllerGrab + // direclty manipulate both entity and 3d overlay objects. + // For now, though, we present a false projection of the cursor onto whatever is below it. This is + // different from the hand beam termination because the false projection is from the camera, while + // the hand beam termination is from the hand. + var eye = Camera.getPosition(); + var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); + Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); + Reticle.visible = false; + + return visualizationIsShowing; // In case we change caller to act conditionally. +} + +// MAIN OPERATIONS ----------- +// +function update() { + var now = Date.now(); + leftTrigger.updateSmoothedTrigger(); + rightTrigger.updateSmoothedTrigger(); + if (!handControllerLockOut.expired(now)) { return turnOffVisualization(); } // Let them use mouse it in peace. + + if (activeTrigger.triggerSmoothedSqueezed() && !inactiveTrigger.triggerSmoothedSqueezed()) { toggleHand(); } + + if (!Menu.isOptionChecked("First Person")) { return turnOffVisualization(); } // What to do? menus can be behind hand! + var controllerPose = Controller.getPoseValue(activeHand); + if (!controllerPose.valid) { return turnOffVisualization(); } // Controller is cradled. + var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), + MyAvatar.position); + // This gets point direction right, but if you want general quaternion it would be more complicated: + var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); + + var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); + if (!hudPoint3d) { print('Controller is parallel to HUD'); return turnOffVisualization(); } + var hudPoint2d = overlayFromWorldPoint(hudPoint3d); + + // We don't know yet if we'll want to make the cursor visble, but we need to move it to see if + // it's pointing at a QML tool (aka system overlay). + setReticlePosition(hudPoint2d); + // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. + if (isPointingAtOverlay(hudPoint2d)) { + if (HMD.active) { // Doesn't hurt anything without the guard, but consider it documentation. + Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + } + Reticle.visible = true; + return turnOffVisualization(true); + } + // We are not pointing at a HUD element (but it could be a 3d overlay). + updateVisualization(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); +} + +var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. +var updater = Script.setInterval(update, UPDATE_INTERVAL); +Script.scriptEnding.connect(function () { Script.clearInterval(updater); }); + +// Check periodically for changes to setup. +var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds +function checkSettings() { + updateFieldOfView(); + checkHardware(); +} +checkSettings(); +var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); +Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); }); diff --git a/scripts/system/depthReticle.js b/scripts/system/depthReticle.js deleted file mode 100644 index 10d604f707..0000000000 --- a/scripts/system/depthReticle.js +++ /dev/null @@ -1,185 +0,0 @@ -// depthReticle.js -// examples -// -// Created by Brad Hefta-Gaub on 2/23/16. -// Copyright 2016 High Fidelity, Inc. -// -// When used in HMD, this script will make the reticle depth track to any clickable item in view. -// This script also handles auto-hiding the reticle after inactivity, as well as having the reticle -// seek the look at position upon waking up. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -var APPARENT_2D_OVERLAY_DEPTH = 1.0; -var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant -var lastDepthCheckTime = Date.now(); -var desiredDepth = APPARENT_2D_OVERLAY_DEPTH; -var TIME_BETWEEN_DEPTH_CHECKS = 100; -var MINIMUM_DEPTH_ADJUST = 0.01; -var NON_LINEAR_DIVISOR = 2; -var MINIMUM_SEEK_DISTANCE = 0.01; - -var lastMouseMoveOrClick = Date.now(); -var lastMouseX = Reticle.position.x; -var lastMouseY = Reticle.position.y; -var HIDE_STATIC_MOUSE_AFTER = 3000; // 3 seconds -var shouldSeekToLookAt = false; -var fastMouseMoves = 0; -var averageMouseVelocity = 0; -var WEIGHTING = 1/20; // simple moving average over last 20 samples -var ONE_MINUS_WEIGHTING = 1 - WEIGHTING; -var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 50; - -function showReticleOnMouseClick() { - Reticle.visible = true; - lastMouseMoveOrClick = Date.now(); // move or click -} - -Controller.mousePressEvent.connect(showReticleOnMouseClick); -Controller.mouseDoublePressEvent.connect(showReticleOnMouseClick); - -Controller.mouseMoveEvent.connect(function(mouseEvent) { - var now = Date.now(); - - // if the reticle is hidden, and we're not in away mode... - if (!Reticle.visible && Reticle.allowMouseCapture) { - Reticle.visible = true; - if (HMD.active) { - shouldSeekToLookAt = true; - } - } else { - // even if the reticle is visible, if we're in HMD mode, and the person is moving their mouse quickly (shaking it) - // then they are probably looking for it, and we should move into seekToLookAt mode - if (HMD.active && !shouldSeekToLookAt && Reticle.allowMouseCapture) { - var dx = Reticle.position.x - lastMouseX; - var dy = Reticle.position.y - lastMouseY; - var dt = Math.max(1, (now - lastMouseMoveOrClick)); // mSecs since last mouse move - var mouseMoveDistance = Math.sqrt((dx*dx) + (dy*dy)); - var mouseVelocity = mouseMoveDistance / dt; - averageMouseVelocity = (ONE_MINUS_WEIGHTING * averageMouseVelocity) + (WEIGHTING * mouseVelocity); - if (averageMouseVelocity > AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO) { - shouldSeekToLookAt = true; - } - } - } - lastMouseMoveOrClick = now; - lastMouseX = mouseEvent.x; - lastMouseY = mouseEvent.y; -}); - -function seekToLookAt() { - // if we're currently seeking the lookAt move the mouse toward the lookat - if (shouldSeekToLookAt) { - averageMouseVelocity = 0; // reset this, these never count for movement... - var lookAt2D = HMD.getHUDLookAtPosition2D(); - var currentReticlePosition = Reticle.position; - var distanceBetweenX = lookAt2D.x - Reticle.position.x; - var distanceBetweenY = lookAt2D.y - Reticle.position.y; - var moveX = distanceBetweenX / NON_LINEAR_DIVISOR; - var moveY = distanceBetweenY / NON_LINEAR_DIVISOR; - var newPosition = { x: Reticle.position.x + moveX, y: Reticle.position.y + moveY }; - var closeEnoughX = false; - var closeEnoughY = false; - if (moveX < MINIMUM_SEEK_DISTANCE) { - newPosition.x = lookAt2D.x; - closeEnoughX = true; - } - if (moveY < MINIMUM_SEEK_DISTANCE) { - newPosition.y = lookAt2D.y; - closeEnoughY = true; - } - Reticle.position = newPosition; - if (closeEnoughX && closeEnoughY) { - shouldSeekToLookAt = false; - } - } -} - -function autoHideReticle() { - var now = Date.now(); - - // sometimes we don't actually get mouse move messages (for example, if the focus has been set - // to an overlay or web page 'overlay') in but the mouse can still be moving, and we don't want - // to autohide in these cases, so we will take this opportunity to also check if the reticle - // position has changed. - if (lastMouseX != Reticle.position.x || lastMouseY != Reticle.position.y) { - lastMouseMoveOrClick = now; - lastMouseX = Reticle.position.x; - lastMouseY = Reticle.position.y; - } - - // if we haven't moved in a long period of time, and we're not pointing at some - // system overlay (like a window), then hide the reticle - if (Reticle.visible && !Reticle.pointingAtSystemOverlay) { - var timeSinceLastMouseMove = now - lastMouseMoveOrClick; - if (timeSinceLastMouseMove > HIDE_STATIC_MOUSE_AFTER) { - Reticle.visible = false; - } - } -} - -function checkReticleDepth() { - var now = Date.now(); - var timeSinceLastDepthCheck = now - lastDepthCheckTime; - if (timeSinceLastDepthCheck > TIME_BETWEEN_DEPTH_CHECKS && Reticle.visible) { - var newDesiredDepth = desiredDepth; - lastDepthCheckTime = now; - var reticlePosition = Reticle.position; - - // first check the 2D Overlays - if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(reticlePosition)) { - newDesiredDepth = APPARENT_2D_OVERLAY_DEPTH; - } else { - var pickRay = Camera.computePickRay(reticlePosition.x, reticlePosition.y); - - // Then check the 3D overlays - var result = Overlays.findRayIntersection(pickRay); - - if (!result.intersects) { - // finally check the entities - result = Entities.findRayIntersection(pickRay, true); - } - - // If either the overlays or entities intersect, then set the reticle depth to - // the distance of intersection - if (result.intersects) { - newDesiredDepth = result.distance; - } else { - // if nothing intersects... set the depth to some sufficiently large depth - newDesiredDepth = APPARENT_MAXIMUM_DEPTH; - } - } - - // If the desired depth has changed, reset our fade start time - if (desiredDepth != newDesiredDepth) { - desiredDepth = newDesiredDepth; - } - } - -} - -function moveToDesiredDepth() { - // move the reticle toward the desired depth - if (desiredDepth != Reticle.depth) { - - // cut distance between desiredDepth and current depth in half until we're close enough - var distanceToAdjustThisCycle = (desiredDepth - Reticle.depth) / NON_LINEAR_DIVISOR; - if (Math.abs(distanceToAdjustThisCycle) < MINIMUM_DEPTH_ADJUST) { - newDepth = desiredDepth; - } else { - newDepth = Reticle.depth + distanceToAdjustThisCycle; - } - Reticle.setDepth(newDepth); - } -} - -Script.update.connect(function(deltaTime) { - autoHideReticle(); // auto hide reticle for desktop or HMD mode - if (HMD.active) { - seekToLookAt(); // handle moving the reticle toward the look at - checkReticleDepth(); // make sure reticle is at correct depth - moveToDesiredDepth(); // move the fade the reticle to the desired depth - } -}); From 5bd77bf3434cc8ee6246d4a4d003f87a31cb78f6 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 3 May 2016 13:23:04 -0700 Subject: [PATCH 23/33] Matching "final" version of test script. --- .../controllers/handControllerPointer.js | 112 +++++++----------- 1 file changed, 40 insertions(+), 72 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 229aa79fed..ed062018fe 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -27,6 +27,7 @@ // On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) // While hardware mouse move switches to mouse move, hardware mouse click (without amove) does not. + // UTILITIES ------------- // @@ -63,35 +64,6 @@ function LatchedToggle(onFunction, offFunction, state) { }; } -// Code copied and adapted from handControllerGrab.js. We should refactor this. -function Trigger() { - var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing - var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab - var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab - var TRIGGER_OFF_VALUE = 0.15; - var that = this; - that.triggerValue = 0; // rolling average of trigger value - that.rawTriggerValue = 0; - that.triggerPress = function (value) { - that.rawTriggerValue = value; - }; - that.updateSmoothedTrigger = function () { - var triggerValue = that.rawTriggerValue; - // smooth out trigger value - that.triggerValue = (that.triggerValue * TRIGGER_SMOOTH_RATIO) + - (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); - }; - that.triggerSmoothedGrab = function () { - return that.triggerValue > TRIGGER_GRAB_VALUE; - }; - that.triggerSmoothedSqueezed = function () { - return that.triggerValue > TRIGGER_ON_VALUE; - }; - that.triggerSmoothedReleased = function () { - return that.triggerValue < TRIGGER_OFF_VALUE; - }; -} - // VERTICAL FIELD OF VIEW --------- // // Cache the verticalFieldOfView setting and update it every so often. @@ -205,7 +177,7 @@ function updateSeeking() { averageMouseVelocity = lastIntegration = 0; var lookAt2D = HMD.getHUDLookAtPosition2D(); if (!lookAt2D) { return; } // E.g., if parallel to location in HUD - var copy = Reticle.position; // Idiomatic javascript would let us side effect the reticle, but this is a copy. + var copy = Reticle.position; function updateDimension(axis) { var distanceBetween = lookAt2D[axis] - Reticle.position[axis]; var move = distanceBetween / NON_LINEAR_DIVISOR; @@ -216,7 +188,7 @@ function updateSeeking() { if (!updateDimension('x') && !updateDimension('y')) { isSeeking = false; } else { - Reticle.position = copy; + Reticle.position(copy); } } @@ -242,10 +214,11 @@ function onMouseMove() { if (HMD.active) { // set depth updateSeeking(); if (isPointingAtOverlay()) { - Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + Reticle.setDepth(SPHERICAL_HUD_DISTANCE); // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! } else { var result = findRayIntersection(Camera.computePickRay(Reticle.position.x, Reticle.position.y)); - Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; + var depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; + Reticle.setDepth(depth); } } updateMouseActivity(); // After the above, just in case the depth movement is awkward when becoming visible. @@ -259,10 +232,15 @@ setupHandler(Controller.mouseDoublePressEvent, onMouseClick); // CONTROLLER MAPPING --------- // -// Synthesize left and right mouse click from controller, and get trigger values matching handControllerGrab. -var triggerMapping; -var leftTrigger = new Trigger(); -var rightTrigger = new Trigger(); + +var activeHand = Controller.Standard.RightHand; +function toggleHand() { + if (activeHand === Controller.Standard.RightHand) { + activeHand = Controller.Standard.LeftHand; + } else { + activeHand = Controller.Standard.RightHand; + } +} // Create clickMappings as needed, on demand. var clickMappings = {}, clickMapping, clickMapToggle; @@ -273,7 +251,6 @@ function checkHardware() { print('Setting mapping for new controller hardware:', newHardware); if (clickMapToggle) { clickMapToggle.setState(false); - triggerMapping.disable(); // FIX SYSTEM BUG: This does not work when hardware changes. Window.alert("This isn't likely to work because of " + 'https://app.asana.com/0/26225263936266/118428633439654\n' + @@ -281,24 +258,39 @@ function checkHardware() { } hardware = newHardware; if (clickMappings[hardware]) { - clickMapping = clickMappings[hardware].click; - triggerMapping = clickMappings[hardware].trigger; + clickMapping = clickMappings[hardware]; } else { clickMapping = Controller.newMapping(Script.resolvePath('') + '-click-' + hardware); Script.scriptEnding.connect(clickMapping.disable); function mapToAction(button, action) { clickMapping.from(Controller.Hardware[hardware][button]).peek().to(Controller.Actions[action]); } + function makeHandToggle(button, hand, optionalWhen) { + var whenThunk = optionalWhen || function () { return true; }; + function maybeToggle() { + if (activeHand !== Controller.Standard[hand]) { + toggleHand(); + } + + } + clickMapping.from(Controller.Hardware[hardware][button]).peek().when(whenThunk).to(maybeToggle); + } function makeViveWhen(click, x, y) { var viveClick = Controller.Hardware.Vive[click], - viveX = Controller.Hardware.Vive[x], - viveY = Controller.Hardware.Vive[y]; + viveX = Controller.Standard[x], // Standard after filtering by mapping + viveY = Controller.Standard[y]; return function () { - return Controller.getValue(viveClick) && !Controller.getValue(viveX) && !Controller.getValue(viveY); + var clickValue = Controller.getValue(viveClick); + var xValue = Controller.getValue(viveX); + var yValue = Controller.getValue(viveY); + return clickValue && !xValue && !yValue; }; } switch (hardware) { case 'Hydra': + makeHandToggle('R3', 'RightHand'); + makeHandToggle('L3', 'LeftHand'); + mapToAction('R3', 'ReticleClick'); mapToAction('L3', 'ReticleClick'); mapToAction('R4', 'ContextMenu'); @@ -306,41 +298,21 @@ function checkHardware() { break; case 'Vive': // When touchpad click is NOT treated as movement, treat as left click + makeHandToggle('RS', 'RightHand', makeViveWhen('RS', 'RX', 'RY')); + makeHandToggle('LS', 'LeftHand', makeViveWhen('LS', 'LX', 'LY')); clickMapping.from(Controller.Hardware.Vive.RS).when(makeViveWhen('RS', 'RX', 'RY')).to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Hardware.Vive.LS).when(makeViveWhen('LS', 'LX', 'LY')).to(Controller.Actions.ReticleClick); mapToAction('RightApplicationMenu', 'ContextMenu'); mapToAction('LeftApplicationMenu', 'ContextMenu'); break; } - - triggerMapping = Controller.newMapping(Script.resolvePath('') + '-trigger-' + hardware); - Script.scriptEnding.connect(triggerMapping.disable); - triggerMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); - triggerMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); - - clickMappings[hardware] = {click: clickMapping, trigger: triggerMapping}; + clickMappings[hardware] = clickMapping; } clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); clickMapToggle.setState(true); - triggerMapping.enable(); } checkHardware(); -var activeHand = Controller.Standard.RightHand; -var activeTrigger = rightTrigger; -var inactiveTrigger = leftTrigger; -function toggleHand() { - if (activeHand === Controller.Standard.RightHand) { - activeHand = Controller.Standard.LeftHand; - activeTrigger = leftTrigger; - inactiveTrigger = rightTrigger; - } else { - activeHand = Controller.Standard.RightHand; - activeTrigger = rightTrigger; - inactiveTrigger = leftTrigger; - } -} - // VISUAL AID ----------- // Same properties as handControllerGrab search sphere var BALL_SIZE = 0.011; @@ -402,15 +374,11 @@ function updateVisualization(controllerPosition, controllerDirection, hudPositio // function update() { var now = Date.now(); - leftTrigger.updateSmoothedTrigger(); - rightTrigger.updateSmoothedTrigger(); if (!handControllerLockOut.expired(now)) { return turnOffVisualization(); } // Let them use mouse it in peace. - - if (activeTrigger.triggerSmoothedSqueezed() && !inactiveTrigger.triggerSmoothedSqueezed()) { toggleHand(); } - if (!Menu.isOptionChecked("First Person")) { return turnOffVisualization(); } // What to do? menus can be behind hand! var controllerPose = Controller.getPoseValue(activeHand); - if (!controllerPose.valid) { return turnOffVisualization(); } // Controller is cradled. + // Vive is effectively invalid when not in HMD + if (!controllerPose.valid || ((hardware === 'Vive') && !HMD.active)) { return turnOffVisualization(); } // Controller is cradled. var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: From eb0517f3cd98f3d1a0c8d8ea99065b90c165208a Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 3 May 2016 14:36:52 -0700 Subject: [PATCH 24/33] fix updateSeeking --- scripts/system/controllers/handControllerPointer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index ed062018fe..6a1509253f 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -155,7 +155,7 @@ var isSeeking = false; var averageMouseVelocity = 0, lastIntegration = 0, lastMouse; var WEIGHTING = 1 / 20; // simple moving average over last 20 samples var ONE_MINUS_WEIGHTING = 1 - WEIGHTING; -var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 2.5; +var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 5; function isShakingMouse() { // True if the person is waving the mouse around trying to find it. var now = Date.now(), mouse = Reticle.position, isShaking = false; if (lastIntegration && (lastIntegration !== now)) { @@ -176,7 +176,7 @@ function updateSeeking() { if (!isSeeking) { return; } averageMouseVelocity = lastIntegration = 0; var lookAt2D = HMD.getHUDLookAtPosition2D(); - if (!lookAt2D) { return; } // E.g., if parallel to location in HUD + if (!lookAt2D) { print('Cannot seek without lookAt position'); return; } // E.g., if parallel to location in HUD var copy = Reticle.position; function updateDimension(axis) { var distanceBetween = lookAt2D[axis] - Reticle.position[axis]; @@ -188,7 +188,7 @@ function updateSeeking() { if (!updateDimension('x') && !updateDimension('y')) { isSeeking = false; } else { - Reticle.position(copy); + Reticle.setPosition(copy); // Not setReticlePosition } } From 71a885998b980208c6f4aec7f76cb8cbbe05feb7 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 3 May 2016 15:17:53 -0700 Subject: [PATCH 25/33] Don't duplicate script. --- examples/controllers/handControllerPointer.js | 436 ------------------ 1 file changed, 436 deletions(-) delete mode 100644 examples/controllers/handControllerPointer.js diff --git a/examples/controllers/handControllerPointer.js b/examples/controllers/handControllerPointer.js deleted file mode 100644 index bf9accb9f0..0000000000 --- a/examples/controllers/handControllerPointer.js +++ /dev/null @@ -1,436 +0,0 @@ -"use strict"; -/*jslint vars: true, plusplus: true*/ -/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print */ - -// -// handControllerPointer.js -// examples/controllers -// -// Created by Howard Stearns on 2016/04/22 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -// Control the "mouse" using hand controller. (HMD and desktop.) -// For now: -// Hydra thumb button 3 is left-mouse, button 4 is right-mouse. -// Vive thumb pad is left mouse (but that interferes with driveing!). Vive menu button is context menu (right mouse). -// First-person only. -// Starts right handed, but switches to whichever is free: Whichever hand was NOT most recently squeezed. -// (For now, the thumb buttons on both controllers are always on.) -// When over a HUD element, the reticle is shown where the active hand controller beam intersects the HUD. -// Otherwise, the active hand controller shows a red ball where a click will act. -// -// Bugs: -// On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) -// While hardware mouse move switches to mouse move, hardware mouse click (without amove) does not. - -var wasRunningDepthReticle = false; -function checkForDepthReticleScript() { - ScriptDiscoveryService.getRunning().forEach(function (script) { - if (script.name === 'depthReticle.js') { - wasRunningDepthReticle = script.path; - Window.alert('Shuting down depthReticle script.\n' + script.path + - '\nMost of the behavior is included here in\n' + - Script.resolvePath('') + - '\ndepthReticle.js will be silently restarted when this script ends.'); - ScriptDiscoveryService.stopScript(script.path); - // FIX SYSTEM BUG: getRunning gets path and url backwards. stopScript wants a url. - // https://app.asana.com/0/26225263936266/118428633439650 - // Some current deviations are listed below as 'FIXME'. - } - }); -} -Script.scriptEnding.connect(function () { - if (wasRunningDepthReticle) { - Script.load(wasRunningDepthReticle); - } -}); - - -// UTILITIES ------------- -// -var counter = 0, skip = 50; -function debug() { // Display the arguments not just [Object object]. - if (skip && (counter++ % skip)) { return; } - print.apply(null, [].map.call(arguments, JSON.stringify)); -} - -// Utility to make it easier to setup and disconnect cleanly. -function setupHandler(event, handler) { - event.connect(handler); - Script.scriptEnding.connect(function () { event.disconnect(handler); }); -} -// If some capability is not available until expiration milliseconds after the last update. -function TimeLock(expiration) { - var last = 0; - this.update = function (optionalNow) { - last = optionalNow || Date.now(); - }; - this.expired = function (optionalNow) { - return ((optionalNow || Date.now()) - last) > expiration; - }; -} -var handControllerLockOut = new TimeLock(2000); - -// Calls onFunction() or offFunction() when swtich(on), but only if it is to a new value. -function LatchedToggle(onFunction, offFunction, state) { - this.getState = function () { - return state; - }; - this.setState = function (on) { - if (state === on) { return; } - state = on; - if (on) { - onFunction(); - } else { - offFunction(); - } - }; -} - -// Code copied and adapted from handControllerGrab.js. We should refactor this. -function Trigger() { - var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing - var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab - var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab - var TRIGGER_OFF_VALUE = 0.15; - var that = this; - that.triggerValue = 0; // rolling average of trigger value - that.rawTriggerValue = 0; - that.triggerPress = function (value) { - that.rawTriggerValue = value; - }; - that.updateSmoothedTrigger = function () { - var triggerValue = that.rawTriggerValue; - // smooth out trigger value - that.triggerValue = (that.triggerValue * TRIGGER_SMOOTH_RATIO) + - (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); - }; - that.triggerSmoothedGrab = function () { - return that.triggerValue > TRIGGER_GRAB_VALUE; - }; - that.triggerSmoothedSqueezed = function () { - return that.triggerValue > TRIGGER_ON_VALUE; - }; - that.triggerSmoothedReleased = function () { - return that.triggerValue < TRIGGER_OFF_VALUE; - }; -} - -// VERTICAL FIELD OF VIEW --------- -// -// Cache the verticalFieldOfView setting and update it every so often. -var verticalFieldOfView, DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees -function updateFieldOfView() { - verticalFieldOfView = Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW; -} - -// SHIMS ---------- -// -// Define customizable versions of some standard operators. Alternative are at the bottom of the file. -var getControllerPose = Controller.getPoseValue; -var getValue = Controller.getValue; -var getOverlayAtPoint = Overlays.getOverlayAtPoint; -// FIX SYSTEM BUG: doesn't work on mac. -// https://app.asana.com/0/26225263936266/118428633439654 -var setReticleVisible = function (on) { Reticle.visible = on; }; - -var weMovedReticle = false; -function ignoreMouseActivity() { - // If we're paused, or if change in cursor position is from this script, not the hardware mouse. - if (!Reticle.allowMouseCapture) { return true; } - // Only we know if we moved it, which is why this script has to replace depthReticle.js - if (!weMovedReticle) { return false; } - weMovedReticle = false; - return true; -} -var setReticlePosition = function (point2d) { - if (!HMD.active) { - // FIX SYSTEM BUG: setPosition is setting relative to screen origin, not the content area of the window. - // https://app.asana.com/0/26225263936266/118427643788550 - point2d = {x: point2d.x, y: point2d.y + 50}; - } - weMovedReticle = true; - Reticle.setPosition(point2d); -}; - -// Generalizations of utilities that work with system and overlay elements. -function findRayIntersection(pickRay) { - // Check 3D overlays and entities. Argument is an object with origin and direction. - var result = Overlays.findRayIntersection(pickRay); - if (!result.intersects) { - result = Entities.findRayIntersection(pickRay, true); - } - return result; -} -function isPointingAtOverlay(optionalHudPosition2d) { - return Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(optionalHudPosition2d || Reticle.position); -} - -// Generalized HUD utilities, with or without HMD: -// These two "vars" are for documentation. Do not change their values! -var SPHERICAL_HUD_DISTANCE = 1; // meters. -var PLANAR_PERPENDICULAR_HUD_DISTANCE = SPHERICAL_HUD_DISTANCE; -function calculateRayUICollisionPoint(position, direction) { - // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection. - if (HMD.active) { - return HMD.calculateRayUICollisionPoint(position, direction); - } - // interect HUD plane, 1m in front of camera, using formula: - // scale = hudNormal dot (hudPoint - position) / hudNormal dot direction - // intersection = postion + scale*direction - var hudNormal = Quat.getFront(Camera.getOrientation()); - var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // must also scale if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 - var denominator = Vec3.dot(hudNormal, direction); - if (denominator === 0) { return null; } // parallel to plane - var numerator = Vec3.dot(hudNormal, Vec3.subtract(hudPoint, position)); - var scale = numerator / denominator; - return Vec3.sum(position, Vec3.multiply(scale, direction)); -} -var DEGREES_TO_HALF_RADIANS = Math.PI / 360; -function overlayFromWorldPoint(point) { - // Answer the 2d pixel-space location in the HUD that covers the given 3D point. - // REQUIRES: that the 3d point be on the hud surface! - // Note that this is based on the Camera, and doesn't know anything about any - // ray that may or may not have been used to compute the point. E.g., the - // overlay point is NOT the intersection of some non-camera ray with the HUD. - if (HMD.active) { - return HMD.overlayFromWorldPoint(point); - } - var cameraToPoint = Vec3.subtract(point, Camera.getPosition()); - var cameraX = Vec3.dot(cameraToPoint, Quat.getRight(Camera.getOrientation())); - var cameraY = Vec3.dot(cameraToPoint, Quat.getUp(Camera.getOrientation())); - var size = Controller.getViewportDimensions(); - var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); // must adjust if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 - var hudWidth = hudHeight * size.x / size.y; - var horizontalFraction = (cameraX / hudWidth + 0.5); - var verticalFraction = 1 - (cameraY / hudHeight + 0.5); - var horizontalPixels = size.x * horizontalFraction; - var verticalPixels = size.y * verticalFraction; - return { x: horizontalPixels, y: verticalPixels }; -} - -// MOUSE ACTIVITY -------- -// -var mouseCursorActivity = new TimeLock(5000); -var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant -function updateMouseActivity(isClick) { - if (ignoreMouseActivity()) { return; } - var now = Date.now(); - mouseCursorActivity.update(now); - if (isClick) { return; } // Bug: mouse clicks should keep going. Just not hand controller clicks - // FIXME: Does not yet seek to lookAt upon waking. - handControllerLockOut.update(now); - setReticleVisible(true); -} -function expireMouseCursor(now) { - if (!isPointingAtOverlay() && mouseCursorActivity.expired(now)) { - setReticleVisible(false); - } -} -function onMouseMove() { - // Display cursor at correct depth (as in depthReticle.js), and updateMouseActivity. - if (ignoreMouseActivity()) { return; } - - if (HMD.active) { // set depth - // FIXME: does not yet adjust slowly. - if (isPointingAtOverlay()) { - Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! - } else { - var result = findRayIntersection(Camera.computePickRay(Reticle.position.x, Reticle.position.y)); - Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; - } - } - updateMouseActivity(); // After the above, just in case the depth movement is awkward when becoming visible. -} -function onMouseClick() { - updateMouseActivity(true); -} -setupHandler(Controller.mouseMoveEvent, onMouseMove); -setupHandler(Controller.mousePressEvent, onMouseClick); -setupHandler(Controller.mouseDoublePressEvent, onMouseClick); - -// CONTROLLER MAPPING --------- -// -// Synthesize left and right mouse click from controller, and get trigger values matching handControllerGrab. -var triggerMapping; -var leftTrigger = new Trigger(); -var rightTrigger = new Trigger(); - -// Create clickMappings as needed, on demand. -var clickMappings = {}, clickMapping, clickMapToggle; -var hardware; // undefined -function checkHardware() { - var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive' : null); // not undefined - if (hardware === newHardware) { return; } - print('Setting mapping for new controller hardware:', newHardware); - if (clickMapToggle) { - clickMapToggle.setState(false); - triggerMapping.disable(); - // FIX SYSTEM BUG: This does not work when hardware changes. - Window.alert("This isn't likely to work because of " + - 'https://app.asana.com/0/26225263936266/118428633439654\n' + - "You'll probably need to restart interface."); - } - hardware = newHardware; - if (clickMappings[hardware]) { - clickMapping = clickMappings[hardware].click; - triggerMapping = clickMappings[hardware].trigger; - } else { - clickMapping = Controller.newMapping(Script.resolvePath('') + '-click-' + hardware); - Script.scriptEnding.connect(clickMapping.disable); - function mapToAction(button, action) { - clickMapping.from(Controller.Hardware[hardware][button]).peek().to(Controller.Actions[action]); - } - switch (hardware) { - case 'Hydra': - mapToAction('R3', 'ReticleClick'); - mapToAction('R4', 'ContextMenu'); - mapToAction('L3', 'ReticleClick'); - mapToAction('L4', 'ContextMenu'); - break; - case 'Vive': - mapToAction('RS', 'ReticleClick'); - mapToAction('LS', 'ReticleClick'); - break; - } - - triggerMapping = Controller.newMapping(Script.resolvePath('') + '-trigger-' + hardware); - Script.scriptEnding.connect(triggerMapping.disable); - triggerMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); - triggerMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); - - clickMappings[hardware] = {click: clickMapping, trigger: triggerMapping}; - } - clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); - clickMapToggle.setState(true); - triggerMapping.enable(); -} -checkHardware(); - -var activeHand = Controller.Standard.RightHand; -var activeTrigger = rightTrigger; -var inactiveTrigger = leftTrigger; -function toggleHand() { - if (activeHand === Controller.Standard.RightHand) { - activeHand = Controller.Standard.LeftHand; - activeTrigger = leftTrigger; - inactiveTrigger = rightTrigger; - } else { - activeHand = Controller.Standard.RightHand; - activeTrigger = rightTrigger; - inactiveTrigger = leftTrigger; - } -} - -// VISUAL AID ----------- -// Same properties as handControllerGrab search sphere -var BALL_SIZE = 0.011; -var BALL_ALPHA = 0.5; -var fakeProjectionBall = Overlays.addOverlay("sphere", { - size: 5 * BALL_SIZE, - color: {red: 255, green: 10, blue: 10}, - ignoreRayIntersection: true, - alpha: BALL_ALPHA, - visible: false, - solid: true, - drawInFront: true // Even when burried inside of something, show it. -}); -var overlays = [fakeProjectionBall]; // If we want to try showing multiple balls and lasers. -Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); -var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. -function turnOffVisualization(optionalEnableClicks) { // because we're showing cursor on HUD - if (!optionalEnableClicks) { - expireMouseCursor(); - } - if (!visualizationIsShowing) { return; } - visualizationIsShowing = false; - overlays.forEach(function (overlay) { - Overlays.editOverlay(overlay, {visible: false}); - }); -} -var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. -function updateVisualization(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { - // Show an indication of where the cursor will appear when crossing a HUD element, - // and where in-world clicking will occur. - // - // There are a number of ways we could do this, but for now, it's a blue sphere that rolls along - // the HUD surface, and a red sphere that rolls along the 3d objects that will receive the click. - // We'll leave it to other scripts (like handControllerGrab) to show a search beam when desired. - - function intersection3d(position, direction) { - // Answer in-world intersection (entity or 3d overlay), or way-out point - var pickRay = {origin: position, direction: direction}; - var result = findRayIntersection(pickRay); - return result.intersects ? result.intersection : Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction)); - } - - visualizationIsShowing = true; - // We'd rather in-world interactions be done at the termination of the hand beam - // -- intersection3d(controllerPosition, controllerDirection). Maybe have handControllerGrab - // direclty manipulate both entity and 3d overlay objects. - // For now, though, we present a false projection of the cursor onto whatever is below it. This is - // different from the hand beam termination because the false projection is from the camera, while - // the hand beam termination is from the hand. - var eye = Camera.getPosition(); - var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); - Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); - setReticleVisible(false); - - return visualizationIsShowing; // In case we change caller to act conditionally. -} - -// MAIN OPERATIONS ----------- -// -function update() { - var now = Date.now(); - leftTrigger.updateSmoothedTrigger(); - rightTrigger.updateSmoothedTrigger(); - if (!handControllerLockOut.expired(now)) { return turnOffVisualization(); } // Let them use mouse it in peace. - - if (activeTrigger.triggerSmoothedSqueezed() && !inactiveTrigger.triggerSmoothedSqueezed()) { toggleHand(); } - - if (!Menu.isOptionChecked("First Person")) { return turnOffVisualization(); } // What to do? menus can be behind hand! - var controllerPose = getControllerPose(activeHand); - if (!controllerPose.valid) { return turnOffVisualization(); } // Controller is cradled. - var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), - MyAvatar.position); - // This gets point direction right, but if you want general quaternion it would be more complicated: - var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); - - var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); - if (!hudPoint3d) { print('Controller is parallel to HUD'); return turnOffVisualization(); } - var hudPoint2d = overlayFromWorldPoint(hudPoint3d); - - // We don't know yet if we'll want to make the cursor visble, but we need to move it to see if - // it's pointing at a QML tool (aka system overlay). - setReticlePosition(hudPoint2d); - // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. - if (isPointingAtOverlay(hudPoint2d)) { - if (HMD.active) { // Doesn't hurt anything without the guard, but consider it documentation. - Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! - } - setReticleVisible(true); - return turnOffVisualization(true); - } - // We are not pointing at a HUD element (but it could be a 3d overlay). - updateVisualization(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); -} - -var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. -var updater = Script.setInterval(update, UPDATE_INTERVAL); -Script.scriptEnding.connect(function () { Script.clearInterval(updater); }); - -// Check periodically for changes to setup. -var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds -function checkSettings() { - updateFieldOfView(); - checkForDepthReticleScript(); - checkHardware(); -} -checkSettings(); -var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); -Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); }); From fdf9c0a2177e7a1eb5f44f5fea2505260d809caa Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 3 May 2016 16:26:45 -0700 Subject: [PATCH 26/33] Fix offset so that non-full-size window can be located anywhere on screen. --- .../src/display-plugins/CompositorHelper.cpp | 19 ++++++++++++++++++- .../src/display-plugins/CompositorHelper.h | 2 ++ .../controllers/handControllerPointer.js | 5 ----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index 5bbf183141..1d725c140b 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -305,6 +305,10 @@ void CompositorHelper::sendFakeMouseEvent() { } } +//FIXME remove static Setting::Handle windowGeometry("WindowGeometry"); + +#include "QWindow.h" +#include "QQuickWindow.h" void CompositorHelper::setReticlePosition(const glm::vec2& position, bool sendFakeEvent) { if (isHMD()) { glm::vec2 maxOverlayPosition = _currentDisplayPlugin->getRecommendedUiSize(); @@ -322,7 +326,20 @@ void CompositorHelper::setReticlePosition(const glm::vec2& position, bool sendFa sendFakeMouseEvent(); } } else { - QCursor::setPos(position.x, position.y); + if (!_mainWindow) { + auto windows = qApp->topLevelWindows(); + QWindow* result = nullptr; + for (auto window : windows) { + QVariant isMainWindow = window->property("MainWindow"); + if (!qobject_cast(window)) { + result = window; + break; + } + } + _mainWindow = result;; + } + const int MENU_BAR_HEIGHT = 20; + QCursor::setPos(position.x + _mainWindow->x(), position.y + _mainWindow->y() + MENU_BAR_HEIGHT); } } diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index c0b53b329e..83a1adde17 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -182,6 +183,7 @@ private: bool _fakeMouseEvent { false }; ReticleInterface* _reticleInterface { nullptr }; + QWindow* _mainWindow { nullptr }; }; // Scripting interface available to control the Reticle diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 6a1509253f..0ddbfa5b2d 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -84,11 +84,6 @@ function ignoreMouseActivity() { return true; } var setReticlePosition = function (point2d) { - if (!HMD.active) { - // FIX SYSTEM BUG: setPosition is setting relative to screen origin, not the content area of the window. - // https://app.asana.com/0/26225263936266/118427643788550 - point2d = {x: point2d.x, y: point2d.y + 50}; - } weMovedReticle = true; Reticle.setPosition(point2d); }; From acc216726a9d19d3745876bace2678e4b7094a19 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 3 May 2016 16:32:53 -0700 Subject: [PATCH 27/33] Cleanup --- .../src/display-plugins/CompositorHelper.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index 1d725c140b..b8da7bd4f7 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include @@ -305,10 +307,6 @@ void CompositorHelper::sendFakeMouseEvent() { } } -//FIXME remove static Setting::Handle windowGeometry("WindowGeometry"); - -#include "QWindow.h" -#include "QQuickWindow.h" void CompositorHelper::setReticlePosition(const glm::vec2& position, bool sendFakeEvent) { if (isHMD()) { glm::vec2 maxOverlayPosition = _currentDisplayPlugin->getRecommendedUiSize(); From d3c6662198fc35c2448ffcd7315c5cd191d5b3a2 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 3 May 2016 16:35:20 -0700 Subject: [PATCH 28/33] Take the deadband down a notch, following feedback from P. --- interface/resources/controllers/vive.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index d2d296aeea..37f5b45b1e 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -1,15 +1,15 @@ { "name": "Vive to Standard", "channels": [ - { "from": "Vive.LY", "when": "Vive.LS", "filters": ["invert" ,{ "type": "deadZone", "min": 0.7 }], "to": "Standard.LY" }, - { "from": "Vive.LX", "when": "Vive.LS", "filters": [{ "type": "deadZone", "min": 0.7 }], "to": "Standard.LX" }, + { "from": "Vive.LY", "when": "Vive.LS", "filters": ["invert" ,{ "type": "deadZone", "min": 0.6 }], "to": "Standard.LY" }, + { "from": "Vive.LX", "when": "Vive.LS", "filters": [{ "type": "deadZone", "min": 0.6 }], "to": "Standard.LX" }, { "from": "Vive.LT", "to": "Standard.LT" }, { "from": "Vive.LB", "to": "Standard.LB" }, { "from": "Vive.LS", "to": "Standard.LS" }, - { "from": "Vive.RY", "when": "Vive.RS", "filters": ["invert", { "type": "deadZone", "min": 0.7 }], "to": "Standard.RY" }, - { "from": "Vive.RX", "when": "Vive.RS", "filters": [{ "type": "deadZone", "min": 0.7 }], "to": "Standard.RX" }, + { "from": "Vive.RY", "when": "Vive.RS", "filters": ["invert", { "type": "deadZone", "min": 0.6 }], "to": "Standard.RY" }, + { "from": "Vive.RX", "when": "Vive.RS", "filters": [{ "type": "deadZone", "min": 0.6 }], "to": "Standard.RX" }, { "from": "Vive.RT", "to": "Standard.RT" }, { "from": "Vive.RB", "to": "Standard.RB" }, From af8d0dc2801b5d2d10e35008335ac7ea9cea03a1 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 9 May 2016 11:34:52 -0700 Subject: [PATCH 29/33] Fix setReticlePosition and getReticlePosition to match. --- .../src/display-plugins/CompositorHelper.cpp | 18 +++--------------- .../src/display-plugins/CompositorHelper.h | 2 -- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index b8da7bd4f7..f9d527de8f 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -258,7 +258,7 @@ glm::vec2 CompositorHelper::getReticlePosition() const { QMutexLocker locker(&_reticleLock); return _reticlePositionInHMD; } - return toGlm(QCursor::pos()); + return toGlm(_renderingWidget->mapFromGlobal(QCursor::pos())); } bool CompositorHelper::getReticleOverDesktop() const { @@ -324,20 +324,8 @@ void CompositorHelper::setReticlePosition(const glm::vec2& position, bool sendFa sendFakeMouseEvent(); } } else { - if (!_mainWindow) { - auto windows = qApp->topLevelWindows(); - QWindow* result = nullptr; - for (auto window : windows) { - QVariant isMainWindow = window->property("MainWindow"); - if (!qobject_cast(window)) { - result = window; - break; - } - } - _mainWindow = result;; - } - const int MENU_BAR_HEIGHT = 20; - QCursor::setPos(position.x + _mainWindow->x(), position.y + _mainWindow->y() + MENU_BAR_HEIGHT); + const QPoint point(position.x, position.y); + QCursor::setPos(_renderingWidget->mapToGlobal(point)); } } diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index 83a1adde17..c0b53b329e 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -17,7 +17,6 @@ #include #include #include -#include #include #include @@ -183,7 +182,6 @@ private: bool _fakeMouseEvent { false }; ReticleInterface* _reticleInterface { nullptr }; - QWindow* _mainWindow { nullptr }; }; // Scripting interface available to control the Reticle From 6f4204ca5bdb1fcc505c3e48f919012278c6c832 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 9 May 2016 11:41:00 -0700 Subject: [PATCH 30/33] Remove alert for route remapping on hardware change. --- scripts/system/controllers/handControllerPointer.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 0ddbfa5b2d..060af4b9d8 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -246,10 +246,6 @@ function checkHardware() { print('Setting mapping for new controller hardware:', newHardware); if (clickMapToggle) { clickMapToggle.setState(false); - // FIX SYSTEM BUG: This does not work when hardware changes. - Window.alert("This isn't likely to work because of " + - 'https://app.asana.com/0/26225263936266/118428633439654\n' + - "You'll probably need to restart interface."); } hardware = newHardware; if (clickMappings[hardware]) { From 9edd18c0178835371b2732035d514f2610130bb7 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 9 May 2016 14:05:59 -0700 Subject: [PATCH 31/33] Fix mouse seek bugs. --- scripts/system/controllers/handControllerPointer.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 060af4b9d8..b5ffaf148d 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -150,7 +150,7 @@ var isSeeking = false; var averageMouseVelocity = 0, lastIntegration = 0, lastMouse; var WEIGHTING = 1 / 20; // simple moving average over last 20 samples var ONE_MINUS_WEIGHTING = 1 - WEIGHTING; -var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 5; +var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 20; function isShakingMouse() { // True if the person is waving the mouse around trying to find it. var now = Date.now(), mouse = Reticle.position, isShaking = false; if (lastIntegration && (lastIntegration !== now)) { @@ -176,11 +176,12 @@ function updateSeeking() { function updateDimension(axis) { var distanceBetween = lookAt2D[axis] - Reticle.position[axis]; var move = distanceBetween / NON_LINEAR_DIVISOR; - if (move >= MINIMUM_SEEK_DISTANCE) { return false; } + if (Math.abs(move) < MINIMUM_SEEK_DISTANCE) { return false; } copy[axis] += move; return true; } - if (!updateDimension('x') && !updateDimension('y')) { + var okX = !updateDimension('x'), okY = !updateDimension('y'); // Evaluate both. Don't short-circuit. + if (okX && okY) { isSeeking = false; } else { Reticle.setPosition(copy); // Not setReticlePosition From 2b3f6506b7b7f7f3ace66440ae108d20c9cf0e77 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 10 May 2016 09:38:39 -0700 Subject: [PATCH 32/33] more whitespace --- .../controllers/handControllerPointer.js | 90 ++++++++++++++----- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index b5ffaf148d..e18488cfa3 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -34,7 +34,9 @@ // Utility to make it easier to setup and disconnect cleanly. function setupHandler(event, handler) { event.connect(handler); - Script.scriptEnding.connect(function () { event.disconnect(handler); }); + Script.scriptEnding.connect(function () { + event.disconnect(handler); + }); } // If some capability is not available until expiration milliseconds after the last update. function TimeLock(expiration) { @@ -54,7 +56,9 @@ function LatchedToggle(onFunction, offFunction, state) { return state; }; this.setState = function (on) { - if (state === on) { return; } + if (state === on) { + return; + } state = on; if (on) { onFunction(); @@ -77,9 +81,13 @@ function updateFieldOfView() { var weMovedReticle = false; function ignoreMouseActivity() { // If we're paused, or if change in cursor position is from this script, not the hardware mouse. - if (!Reticle.allowMouseCapture) { return true; } + if (!Reticle.allowMouseCapture) { + return true; + } // Only we know if we moved it, which is why this script has to replace depthReticle.js - if (!weMovedReticle) { return false; } + if (!weMovedReticle) { + return false; + } weMovedReticle = false; return true; } @@ -116,7 +124,9 @@ function calculateRayUICollisionPoint(position, direction) { var hudNormal = Quat.getFront(Camera.getOrientation()); var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // must also scale if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 var denominator = Vec3.dot(hudNormal, direction); - if (denominator === 0) { return null; } // parallel to plane + if (denominator === 0) { + return null; + } // parallel to plane var numerator = Vec3.dot(hudNormal, Vec3.subtract(hudPoint, position)); var scale = numerator / denominator; return Vec3.sum(position, Vec3.multiply(scale, direction)); @@ -167,16 +177,25 @@ function isShakingMouse() { // True if the person is waving the mouse around try var NON_LINEAR_DIVISOR = 2; var MINIMUM_SEEK_DISTANCE = 0.01; function updateSeeking() { - if (!Reticle.visible || isShakingMouse()) { isSeeking = true; } // e.g., if we're about to turn it on with first movement. - if (!isSeeking) { return; } + if (!Reticle.visible || isShakingMouse()) { + isSeeking = true; + } // e.g., if we're about to turn it on with first movement. + if (!isSeeking) { + return; + } averageMouseVelocity = lastIntegration = 0; var lookAt2D = HMD.getHUDLookAtPosition2D(); - if (!lookAt2D) { print('Cannot seek without lookAt position'); return; } // E.g., if parallel to location in HUD + if (!lookAt2D) { + print('Cannot seek without lookAt position'); + return; + } // E.g., if parallel to location in HUD var copy = Reticle.position; function updateDimension(axis) { var distanceBetween = lookAt2D[axis] - Reticle.position[axis]; var move = distanceBetween / NON_LINEAR_DIVISOR; - if (Math.abs(move) < MINIMUM_SEEK_DISTANCE) { return false; } + if (Math.abs(move) < MINIMUM_SEEK_DISTANCE) { + return false; + } copy[axis] += move; return true; } @@ -191,10 +210,14 @@ function updateSeeking() { var mouseCursorActivity = new TimeLock(5000); var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant function updateMouseActivity(isClick) { - if (ignoreMouseActivity()) { return; } + if (ignoreMouseActivity()) { + return; + } var now = Date.now(); mouseCursorActivity.update(now); - if (isClick) { return; } // Bug: mouse clicks should keep going. Just not hand controller clicks + if (isClick) { + return; + } // Bug: mouse clicks should keep going. Just not hand controller clicks handControllerLockOut.update(now); Reticle.visible = true; } @@ -205,7 +228,9 @@ function expireMouseCursor(now) { } function onMouseMove() { // Display cursor at correct depth (as in depthReticle.js), and updateMouseActivity. - if (ignoreMouseActivity()) { return; } + if (ignoreMouseActivity()) { + return; + } if (HMD.active) { // set depth updateSeeking(); @@ -243,7 +268,9 @@ var clickMappings = {}, clickMapping, clickMapToggle; var hardware; // undefined function checkHardware() { var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive' : null); // not undefined - if (hardware === newHardware) { return; } + if (hardware === newHardware) { + return; + } print('Setting mapping for new controller hardware:', newHardware); if (clickMapToggle) { clickMapToggle.setState(false); @@ -258,7 +285,9 @@ function checkHardware() { clickMapping.from(Controller.Hardware[hardware][button]).peek().to(Controller.Actions[action]); } function makeHandToggle(button, hand, optionalWhen) { - var whenThunk = optionalWhen || function () { return true; }; + var whenThunk = optionalWhen || function () { + return true; + }; function maybeToggle() { if (activeHand !== Controller.Standard[hand]) { toggleHand(); @@ -319,13 +348,17 @@ var fakeProjectionBall = Overlays.addOverlay("sphere", { drawInFront: true // Even when burried inside of something, show it. }); var overlays = [fakeProjectionBall]; // If we want to try showing multiple balls and lasers. -Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); +Script.scriptEnding.connect(function () { + overlays.forEach(Overlays.deleteOverlay); +}); var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. function turnOffVisualization(optionalEnableClicks) { // because we're showing cursor on HUD if (!optionalEnableClicks) { expireMouseCursor(); } - if (!visualizationIsShowing) { return; } + if (!visualizationIsShowing) { + return; + } visualizationIsShowing = false; overlays.forEach(function (overlay) { Overlays.editOverlay(overlay, {visible: false}); @@ -366,18 +399,27 @@ function updateVisualization(controllerPosition, controllerDirection, hudPositio // function update() { var now = Date.now(); - if (!handControllerLockOut.expired(now)) { return turnOffVisualization(); } // Let them use mouse it in peace. - if (!Menu.isOptionChecked("First Person")) { return turnOffVisualization(); } // What to do? menus can be behind hand! + if (!handControllerLockOut.expired(now)) { + return turnOffVisualization(); + } // Let them use mouse it in peace. + if (!Menu.isOptionChecked("First Person")) { + return turnOffVisualization(); + } // What to do? menus can be behind hand! var controllerPose = Controller.getPoseValue(activeHand); // Vive is effectively invalid when not in HMD - if (!controllerPose.valid || ((hardware === 'Vive') && !HMD.active)) { return turnOffVisualization(); } // Controller is cradled. + if (!controllerPose.valid || ((hardware === 'Vive') && !HMD.active)) { + return turnOffVisualization(); + } // Controller is cradled. var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); - if (!hudPoint3d) { print('Controller is parallel to HUD'); return turnOffVisualization(); } + if (!hudPoint3d) { + print('Controller is parallel to HUD'); + return turnOffVisualization(); + } var hudPoint2d = overlayFromWorldPoint(hudPoint3d); // We don't know yet if we'll want to make the cursor visble, but we need to move it to see if @@ -397,7 +439,9 @@ function update() { var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. var updater = Script.setInterval(update, UPDATE_INTERVAL); -Script.scriptEnding.connect(function () { Script.clearInterval(updater); }); +Script.scriptEnding.connect(function () { + Script.clearInterval(updater); +}); // Check periodically for changes to setup. var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds @@ -407,4 +451,6 @@ function checkSettings() { } checkSettings(); var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); -Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); }); +Script.scriptEnding.connect(function () { + Script.clearInterval(settingsChecker); +}); From 64dc31cd61fb7606acab5f3d5caf18c2c6170f23 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 10 May 2016 09:43:00 -0700 Subject: [PATCH 33/33] switch statement whitespace --- .../controllers/handControllerPointer.js | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index e18488cfa3..6e9fe17077 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -308,24 +308,24 @@ function checkHardware() { }; } switch (hardware) { - case 'Hydra': - makeHandToggle('R3', 'RightHand'); - makeHandToggle('L3', 'LeftHand'); + case 'Hydra': + makeHandToggle('R3', 'RightHand'); + makeHandToggle('L3', 'LeftHand'); - mapToAction('R3', 'ReticleClick'); - mapToAction('L3', 'ReticleClick'); - mapToAction('R4', 'ContextMenu'); - mapToAction('L4', 'ContextMenu'); - break; - case 'Vive': - // When touchpad click is NOT treated as movement, treat as left click - makeHandToggle('RS', 'RightHand', makeViveWhen('RS', 'RX', 'RY')); - makeHandToggle('LS', 'LeftHand', makeViveWhen('LS', 'LX', 'LY')); - clickMapping.from(Controller.Hardware.Vive.RS).when(makeViveWhen('RS', 'RX', 'RY')).to(Controller.Actions.ReticleClick); - clickMapping.from(Controller.Hardware.Vive.LS).when(makeViveWhen('LS', 'LX', 'LY')).to(Controller.Actions.ReticleClick); - mapToAction('RightApplicationMenu', 'ContextMenu'); - mapToAction('LeftApplicationMenu', 'ContextMenu'); - break; + mapToAction('R3', 'ReticleClick'); + mapToAction('L3', 'ReticleClick'); + mapToAction('R4', 'ContextMenu'); + mapToAction('L4', 'ContextMenu'); + break; + case 'Vive': + // When touchpad click is NOT treated as movement, treat as left click + makeHandToggle('RS', 'RightHand', makeViveWhen('RS', 'RX', 'RY')); + makeHandToggle('LS', 'LeftHand', makeViveWhen('LS', 'LX', 'LY')); + clickMapping.from(Controller.Hardware.Vive.RS).when(makeViveWhen('RS', 'RX', 'RY')).to(Controller.Actions.ReticleClick); + clickMapping.from(Controller.Hardware.Vive.LS).when(makeViveWhen('LS', 'LX', 'LY')).to(Controller.Actions.ReticleClick); + mapToAction('RightApplicationMenu', 'ContextMenu'); + mapToAction('LeftApplicationMenu', 'ContextMenu'); + break; } clickMappings[hardware] = clickMapping; }