From 2a82dddc2b019e53e2b6cfc1b9a7d9f2eb8932d7 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 21 Jun 2016 17:36:36 -0700 Subject: [PATCH 1/7] Draw attach points as if they were equip-hotspots --- .../system/controllers/handControllerGrab.js | 17 ++++++-- scripts/system/libraries/Xform.js | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 scripts/system/libraries/Xform.js diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 39d85f1224..31f8ebd73f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -10,9 +10,10 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr */ +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr, Xform */ Script.include("/~/system/libraries/utils.js"); +Script.include("../libraries/Xform.js"); // // add lines where the hand ray picking is happening @@ -879,21 +880,29 @@ function MyController(hand) { var entities = Entities.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); var i, l = entities.length; for (i = 0; i < l; i++) { - var grabProps = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); + var props = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); // does this entity have an attach point? var wearableData = getEntityCustomData("wearable", entities[i], undefined); if (wearableData && wearableData.joints) { var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; if (wearableData.joints[handJointName]) { + var handOffsetPos = wearableData.joints[handJointName][0]; + var handOffsetRot = wearableData.joints[handJointName][1]; + + var handOffsetXform = new Xform(handOffsetRot, handOffsetPos); + var objectXform = new Xform(props.rotation, props.position); + var overlayXform = Xform.mul(objectXform, handOffsetXform.inv()); + // draw the hotspot this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { - position: grabProps.position, + rotation: overlayXform.rot, + position: overlayXform.pos, size: 0.2, color: { red: 90, green: 255, blue: 90 }, alpha: 0.7, solid: true, visible: true, - ignoreRayIntersection: false, + ignoreRayIntersection: true, drawInFront: false })); } diff --git a/scripts/system/libraries/Xform.js b/scripts/system/libraries/Xform.js new file mode 100644 index 0000000000..75051c4983 --- /dev/null +++ b/scripts/system/libraries/Xform.js @@ -0,0 +1,40 @@ +// +// Created by Anthony J. Thibault on 2016/06/21 +// 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 +// + +// ctor +Xform = function(rot, pos) { + this.rot = rot; + this.pos = pos; +} + +Xform.ident = function() { + return new Xform({x: 0, y: 0, z: 0, w: 1}, {x: 0, y: 0, z: 0}); +}; + +Xform.mul = function(lhs, rhs) { + var rot = Quat.multiply(lhs.rot, rhs.rot); + var pos = Vec3.sum(lhs.pos, Vec3.multiplyQbyV(lhs.rot, rhs.pos)); + return new Xform(rot, pos); +}; + +Xform.prototype.inv = function() { + var invRot = Quat.inverse(this.rot); + var invPos = Vec3.multiply(-1, this.pos); + return new Xform(invRot, Vec3.multiplyQbyV(invRot, invPos)); +}; + +Xform.prototype.mirrorX = function() { + return new Xform({x: this.rot.x, y: -this.rot.y, z: -this.rot.z, w: this.rot.w}, + {x: -this.pos.x, y: this.pos.y, z: this.pos.z}); +}; + +Xform.prototype.toString = function() { + var rot = this.rot; + var pos = this.pos; + return "Xform rot = (" + rot.x + ", " + rot.y + ", " + rot.z + ", " + rot.w + "), pos = (" + pos.x + ", " + pos.y + ", " + pos.z + ")"; +}; From fc42a3aef52759e8f8cda7a665deeede5377345e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 23 Jun 2016 15:36:47 -0700 Subject: [PATCH 2/7] Grab script hotspot work * Updated grab/equip logic to use sphere vs sphere tests, instead of sphere vs entity bounding box. * Added debug flag for visualizing grab spheres. * hotspot overlays are now updated as the objects they are attached to move. * You can now use the search beam to near grab large objects, as well as the sphere sphere test. * Optimized EntityPropertyCache to make a single call to Entities.getEntityProperties instead of three. * Moved grab script options from the "Developer > Hands" menu to the "Developer > Grab Script" menu. --- .../system/controllers/handControllerGrab.js | 312 +++++++++++------- 1 file changed, 200 insertions(+), 112 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 31f8ebd73f..1772e55015 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -10,10 +10,9 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr, Xform */ +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr */ Script.include("/~/system/libraries/utils.js"); -Script.include("../libraries/Xform.js"); // // add lines where the hand ray picking is happening @@ -38,6 +37,9 @@ var THUMB_ON_VALUE = 0.5; var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move. var PICK_WITH_HAND_RAY = true; + +var DRAW_GRAB_SPHERES = false; +var DRAW_HAND_SPHERES = false; var DROP_WITHOUT_SHAKE = false; // @@ -68,16 +70,20 @@ var LINE_ENTITY_DIMENSIONS = { var LINE_LENGTH = 500; var PICK_MAX_DISTANCE = 500; // max length of pick-ray + // // near grabbing // -var GRAB_RADIUS = 0.06; // if the ray misses but an object is this close, it will still be selected +var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. + var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position -var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected + +var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing. +var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. + var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed -var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size var CHECK_TOO_FAR_UNEQUIP_TIME = 1.0; // seconds // @@ -254,7 +260,8 @@ function restore2DMode() { } } -// constructor +// EntityPropertiesCache is a helper class that contains a cache of entity properties. +// the hope is to prevent excess calls to Entity.getEntityProperties() function EntityPropertiesCache() { this.cache = {}; } @@ -265,34 +272,55 @@ EntityPropertiesCache.prototype.findEntities = function(position, radius) { var entities = Entities.findEntities(position, radius); var _this = this; entities.forEach(function (x) { - _this.addEntity(x); + _this.updateEntity(x); }); }; -EntityPropertiesCache.prototype.addEntity = function(entityID) { +EntityPropertiesCache.prototype.updateEntity = function(entityID) { var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); - var grabbableProps = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA); - var grabProps = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - var wearableProps = getEntityCustomData("wearable", entityID, {}); - this.cache[entityID] = { props: props, grabbableProps: grabbableProps, grabProps: grabProps, wearableProps: wearableProps }; + + // convert props.userData from a string to an object. + var userData = {}; + if (props.userData) { + try { + userData = JSON.parse(props.userData); + } catch(err) { + print("WARNING: malformed userData on " + entityID + ", name = " + props.name + ", error = " + err); + } + } + props.userData = userData; + + this.cache[entityID] = props; }; EntityPropertiesCache.prototype.getEntities = function() { return Object.keys(this.cache); } EntityPropertiesCache.prototype.getProps = function(entityID) { var obj = this.cache[entityID] - return obj ? obj.props : undefined; + return obj ? obj : undefined; }; EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { - var obj = this.cache[entityID] - return obj ? obj.grabbableProps : undefined; + var props = this.cache[entityID]; + if (props) { + return props.userData.grabbableKey ? props.userData.grabbableKey : DEFAULT_GRABBABLE_DATA; + } else { + return undefined; + } }; EntityPropertiesCache.prototype.getGrabProps = function(entityID) { - var obj = this.cache[entityID] - return obj ? obj.grabProps : undefined; + var props = this.cache[entityID]; + if (props) { + return props.userData.grabKey ? props.userData.grabKey : {}; + } else { + return undefined; + } }; EntityPropertiesCache.prototype.getWearableProps = function(entityID) { - var obj = this.cache[entityID] - return obj ? obj.wearableProps : undefined; + var props = this.cache[entityID]; + if (props) { + return props.userData.wearable ? props.userData.wearable : {}; + } else { + return undefined; + } }; function MyController(hand) { @@ -323,7 +351,6 @@ function MyController(hand) { //for visualizations this.overlayLine = null; this.particleBeamObject = null; - this.grabSphere = null; //for lights this.spotlight = null; @@ -384,7 +411,7 @@ function MyController(hand) { } this.setState = function(newState, reason) { - this.grabSphereOff(); + if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); var newStateName = stateToName(newState); @@ -491,39 +518,6 @@ function MyController(hand) { } } - this.grabSphereOn = function() { - var color = {red: 0, green: 255, blue: 0}; - if (this.grabSphere === null) { - var sphereProperties = { - position: this.getHandPosition(), - size: GRAB_RADIUS*2, - color: color, - alpha: 0.1, - solid: true, - ignoreRayIntersection: true, - drawInFront: true, // Even when burried inside of something, show it. - visible: true - } - this.grabSphere = Overlays.addOverlay("sphere", sphereProperties); - } else { - Overlays.editOverlay(this.grabSphere, { - position: this.getHandPosition(), - size: GRAB_RADIUS*2, - color: color, - alpha: 0.1, - solid: true, - visible: true - }); - } - } - - this.grabSphereOff = function() { - if (this.grabSphere !== null) { - Overlays.deleteOverlay(this.grabSphere); - this.grabSphere = null; - } - }; - this.overlayLineOn = function(closePoint, farPoint, color) { if (this.overlayLine === null) { var lineProperties = { @@ -873,51 +867,123 @@ function MyController(hand) { } }; - this.searchEnter = function() { - this.equipHotspotOverlays = []; + this.createHotspots = function () { + + var HAND_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var HAND_SPHERE_ALPHA = 0.7; + var HAND_SPHERE_RADIUS = 0.01; + + var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var EQUIP_SPHERE_ALPHA = 0.3; + + var NEAR_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; + var NEAR_SPHERE_ALPHA = 0.1; + + this.hotspotOverlays = []; + + if (DRAW_HAND_SPHERES) { + // add tiny spheres around the palm. + var handPosition = this.getHandPosition(); + var overlay = Overlays.addOverlay("sphere", { + position: handPosition, + size: HAND_SPHERE_RADIUS * 2, + color: HAND_SPHERE_COLOR, + alpha: HAND_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + this.hotspotOverlays.push({ + entityID: undefined, + overlay: overlay, + type: "hand" + }); + } // find entities near the avatar that might be equipable. - var entities = Entities.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); - var i, l = entities.length; - for (i = 0; i < l; i++) { - var props = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); - // does this entity have an attach point? - var wearableData = getEntityCustomData("wearable", entities[i], undefined); - if (wearableData && wearableData.joints) { - var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (wearableData.joints[handJointName]) { - var handOffsetPos = wearableData.joints[handJointName][0]; - var handOffsetRot = wearableData.joints[handJointName][1]; + this.entityPropertyCache.clear(); + this.entityPropertyCache.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); - var handOffsetXform = new Xform(handOffsetRot, handOffsetPos); - var objectXform = new Xform(props.rotation, props.position); - var overlayXform = Xform.mul(objectXform, handOffsetXform.inv()); + var _this = this; + this.entityPropertyCache.getEntities().forEach(function (entityID) { + if (_this.entityIsEquippableWithoutDistanceCheck(entityID)) { + var props = _this.entityPropertyCache.getProps(entityID); - // draw the hotspot - this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { - rotation: overlayXform.rot, - position: overlayXform.pos, - size: 0.2, - color: { red: 90, green: 255, blue: 90 }, - alpha: 0.7, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - })); - } + overlay = Overlays.addOverlay("sphere", { + rotation: props.rotation, + position: props.position, + size: EQUIP_RADIUS * 2, + color: EQUIP_SPHERE_COLOR, + alpha: EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + _this.hotspotOverlays.push({ + entityID: entityID, + overlay: overlay, + type: "equip" + }); } - } + + if (DRAW_GRAB_SPHERES && _this.entityIsGrabbable(entityID)) { + var props = _this.entityPropertyCache.getProps(entityID); + + overlay = Overlays.addOverlay("sphere", { + rotation: props.rotation, + position: props.position, + size: NEAR_GRAB_RADIUS * 2, + color: NEAR_SPHERE_COLOR, + alpha: NEAR_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + _this.hotspotOverlays.push({ + entityID: entityID, + overlay: overlay, + type: "near" + }); + } + }); + }; + + this.updateHotspots = function() { + var _this = this; + this.hotspotOverlays.forEach(function (overlayInfo) { + if (overlayInfo.type === "hand") { + Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); + } else if (overlayInfo.type === "equip") { + _this.entityPropertyCache.updateEntity(overlayInfo.entityID); + var props = _this.entityPropertyCache.getProps(overlayInfo.entityID); + Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + } else if (overlayInfo.type === "near") { + _this.entityPropertyCache.updateEntity(overlayInfo.entityID); + var props = _this.entityPropertyCache.getProps(overlayInfo.entityID); + Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + } + }); + }; + + this.destroyHotspots = function() { + this.hotspotOverlays.forEach(function (overlayInfo) { + Overlays.deleteOverlay(overlayInfo.overlay); + }); + this.hotspotOverlays = []; + }; + + this.searchEnter = function() { + this.createHotspots(); }; this.searchExit = function() { - - // delete all equip hotspots - var i, l = this.equipHotspotOverlays.length; - for (i = 0; i < l; i++) { - Overlays.deleteOverlay(this.equipHotspotOverlays[i]); - } - this.equipHotspotOverlays = []; + this.destroyHotspots(); }; /// @@ -984,7 +1050,13 @@ function MyController(hand) { return grabbableProps && grabbableProps.wantsTrigger; }; - this.entityIsEquippable = function (entityID, handPosition) { + this.entityIsEquippableWithoutDistanceCheck = function (entityID) { + var props = this.entityPropertyCache.getProps(entityID); + var handPosition = props.position; + return this.entityIsEquippableWithDistanceCheck(entityID, handPosition); + }; + + this.entityIsEquippableWithDistanceCheck = function (entityID, handPosition) { var props = this.entityPropertyCache.getProps(entityID); var distance = Vec3.distance(props.position, handPosition); var grabProps = this.entityPropertyCache.getGrabProps(entityID); @@ -998,9 +1070,9 @@ function MyController(hand) { return false; } - if (distance > NEAR_PICK_MAX_DISTANCE) { + if (distance > EQUIP_RADIUS) { if (debug) { - print("equip is skipping '" + props.name + "': too far away."); + print("equip is skipping '" + props.name + "': too far away, " + distance + " meters"); } return false; } @@ -1113,7 +1185,7 @@ function MyController(hand) { return true; }; - this.entityIsNearGrabbable = function(entityID, handPosition) { + this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { if (!this.entityIsGrabbable(entityID)) { return false; @@ -1123,7 +1195,7 @@ function MyController(hand) { var distance = Vec3.distance(props.position, handPosition); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - if (distance > NEAR_PICK_MAX_DISTANCE) { + if (distance > maxDistance) { // too far away, don't grab if (debug) { print(" grab is skipping '" + props.name + "': too far away."); @@ -1138,6 +1210,8 @@ function MyController(hand) { var _this = this; var name; + this.updateHotspots(); + this.grabbedEntity = null; this.isInitialGrab = false; this.shouldResetParentOnRelease = false; @@ -1151,16 +1225,12 @@ function MyController(hand) { var handPosition = this.getHandPosition(); - if (SHOW_GRAB_SPHERE) { - this.grabSphereOn(); - } - this.entityPropertyCache.clear(); - this.entityPropertyCache.findEntities(handPosition, GRAB_RADIUS); + this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS); var candidateEntities = this.entityPropertyCache.getEntities(); var equippableEntities = candidateEntities.filter(function (entity) { - return _this.entityIsEquippable(entity, handPosition); + return _this.entityIsEquippableWithDistanceCheck(entity, handPosition); }); var entity; @@ -1181,16 +1251,19 @@ function MyController(hand) { } } + var grabbableEntities = candidateEntities.filter(function (entity) { + return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_RADIUS); + }); + var rayPickInfo = this.calcRayPickInfo(this.hand); this.intersectionDistance = rayPickInfo.distance; - if (rayPickInfo.entityID) { - candidateEntities.push(rayPickInfo.entityID); - this.entityPropertyCache.addEntity(rayPickInfo.entityID); - } - var grabbableEntities = candidateEntities.filter(function (entity) { - return _this.entityIsNearGrabbable(entity, handPosition); - }); + if (rayPickInfo.entityID) { + this.entityPropertyCache.updateEntity(rayPickInfo.entityID); + if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) { + grabbableEntities.push(rayPickInfo.entityID); + } + } if (grabbableEntities.length > 0) { // sort by distance @@ -1732,11 +1805,11 @@ function MyController(hand) { this.lastUnequipCheckTime = now; if (props.parentID == MyAvatar.sessionUUID && - Vec3.length(props.localPosition) > NEAR_PICK_MAX_DISTANCE * 2.0) { + Vec3.length(props.localPosition) > NEAR_GRAB_PICK_RADIUS * 2.0) { var handPosition = this.getHandPosition(); // the center of the equipped object being far from the hand isn't enough to autoequip -- we also // need to fail the findEntities test. - var nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); + var nearPickedCandidateEntities = Entities.findEntities(handPosition, EQUIP_RADIUS); if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + @@ -2182,17 +2255,32 @@ function cleanup() { Script.scriptEnding.connect(cleanup); Script.update.connect(update); +if (!Menu.menuExists("Developer > Grab Script")) { + Menu.addMenu("Developer > Grab Script"); +} + Menu.addMenuItem({ - menuName: "Developer > Hands", + menuName: "Developer > Grab Script", menuItemName: "Drop Without Shake", isCheckable: true, isChecked: DROP_WITHOUT_SHAKE }); +Menu.addMenuItem({ + menuName: "Developer > Grab Script", + menuItemName: "Draw Grab Spheres", + isCheckable: true, + isChecked: DRAW_GRAB_SPHERES +}); + function handleMenuItemEvent(menuItem) { if (menuItem === "Drop Without Shake") { DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake"); } + if (menuItem === "Draw Grab Spheres") { + DRAW_GRAB_SPHERES = Menu.isOptionChecked("Draw Grab Spheres"); + DRAW_HAND_SPHERES = DRAW_GRAB_SPHERES; + } } Menu.menuItemEvent.connect(handleMenuItemEvent); From f8391f00629535b9849d928b0075c0f032dc61f2 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 23 Jun 2016 16:51:15 -0700 Subject: [PATCH 3/7] fix reticle bugs --- interface/src/Application.cpp | 26 +++++++++---------- interface/src/Application.h | 1 + libraries/ui/src/OffscreenUi.cpp | 5 ++-- .../controllers/handControllerPointer.js | 21 +++++++++++---- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 37c3b361bf..26da43fb58 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -921,17 +921,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : cycleCamera(); } else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) { if (!offscreenUi->navigationFocused()) { - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); + toggleMenuUnderReticle(); } } else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) { - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); - } else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) { - if (!offscreenUi->navigationFocused()) { - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); - } + toggleMenuUnderReticle(); } else if (action == controller::toInt(controller::Action::RETICLE_X)) { auto oldPos = getApplicationCompositor().getReticlePosition(); getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); @@ -1240,7 +1233,16 @@ QString Application::getUserAgent() { return userAgent; } - +void Application::toggleMenuUnderReticle() const { + // In HMD, if the menu is near the mouse but not under it, the reticle can be at a significantly + // different depth. When you focus on the menu, the cursor can appear to your crossed eyes as both + // on the menu and off. + // Even in 2D, it is arguable whether the user would want the menu to be to the side. + const float X_LEFT_SHIFT = 50.0; + auto offscreenUi = DependencyManager::get(); + auto reticlePosition = getApplicationCompositor().getReticlePosition(); + offscreenUi->toggleMenu(QPoint(reticlePosition.x - X_LEFT_SHIFT, reticlePosition.y)); +} void Application::checkChangeCursor() { QMutexLocker locker(&_changeCursorLock); @@ -2462,9 +2464,7 @@ void Application::keyPressEvent(QKeyEvent* event) { void Application::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Alt && _altPressed && hasFocus()) { - auto offscreenUi = DependencyManager::get(); - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); + toggleMenuUnderReticle(); } _keysPressed.remove(event->key()); diff --git a/interface/src/Application.h b/interface/src/Application.h index 5beaa5b455..6857ba2a3a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -408,6 +408,7 @@ private: static void dragEnterEvent(QDragEnterEvent* event); void maybeToggleMenuVisible(QMouseEvent* event) const; + void toggleMenuUnderReticle() const; MainWindow* _window; QElapsedTimer& _sessionRunTimer; diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index fa1a31d196..1a7d4b2328 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -492,10 +492,9 @@ void OffscreenUi::unfocusWindows() { Q_ASSERT(invokeResult); } -void OffscreenUi::toggleMenu(const QPoint& screenPosition) { +void OffscreenUi::toggleMenu(const QPoint& screenPosition) { // caller should already have mapped using getReticlePosition emit showDesktop(); // we really only want to do this if you're showing the menu, but for now this works - auto virtualPos = mapToVirtualScreen(screenPosition, nullptr); - QMetaObject::invokeMethod(_desktop, "toggleMenu", Q_ARG(QVariant, virtualPos)); + QMetaObject::invokeMethod(_desktop, "toggleMenu", Q_ARG(QVariant, screenPosition)); } diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 374be0d1a1..a0f1f47b3c 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -208,9 +208,9 @@ function isShakingMouse() { // True if the person is waving the mouse around try return isShaking; } var NON_LINEAR_DIVISOR = 2; -var MINIMUM_SEEK_DISTANCE = 0.01; -function updateSeeking() { - if (!Reticle.visible || isShakingMouse()) { +var MINIMUM_SEEK_DISTANCE = 0.1; +function updateSeeking(doNotStartSeeking) { + if (!doNotStartSeeking && (!Reticle.visible || isShakingMouse())) { if (!isSeeking) { print('Start seeking mouse.'); isSeeking = true; @@ -224,8 +224,8 @@ function updateSeeking() { if (!lookAt2D) { // If this happens, something has gone terribly wrong. print('Cannot seek without lookAt position'); isSeeking = false; - return; - } // E.g., if parallel to location in HUD + return; // E.g., if parallel to location in HUD + } var copy = Reticle.position; function updateDimension(axis) { var distanceBetween = lookAt2D[axis] - Reticle.position[axis]; @@ -353,6 +353,16 @@ clickMapping.from(rightTrigger.full).when(isPointingAtOverlayStartedNonFullTrigg clickMapping.from(leftTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(leftTrigger)).to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); +clickMapping.from(Controller.Hardware.Keyboard.RightMouseClicked).peek().to(function () { + // Allow the reticle depth to be set correctly: + // Wait a tick for the context menu to be displayed, and then simulate a (non-hand-controller) mouse move + // so that the system updates qml state (Reticle.pointingAtSystemOverlay) before it gives us a mouseMove. + // We don't want the system code to always do this for us, because, e.g., we do not want to get a mouseMove + // after the Left/RightSecondaryThumb gives us a context menu. Only from the mouse. + Script.setTimeout(function () { + Reticle.setPosition(Reticle.position); + }, 0); +}); // Partial smoothed trigger is activation. clickMapping.from(rightTrigger.partial).to(makeToggleAction(Controller.Standard.RightHand)); clickMapping.from(leftTrigger.partial).to(makeToggleAction(Controller.Standard.LeftHand)); @@ -386,6 +396,7 @@ function update() { expireMouseCursor(); clearSystemLaser(); } + updateSeeking(true); if (!handControllerLockOut.expired(now)) { return off(); // Let them use mouse it in peace. } From 7bd553c09cea7784828354085901adf788f37a0c Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 23 Jun 2016 17:03:15 -0700 Subject: [PATCH 4/7] near grab logic to uses sphere vs entity box instead of sphere vs sphere. Adjusted debug drawing accordingly. --- .../system/controllers/handControllerGrab.js | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 26f977f9b0..b1cf3927d9 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -38,7 +38,7 @@ var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only var PICK_WITH_HAND_RAY = true; -var DRAW_GRAB_SPHERES = false; +var DRAW_GRAB_BOXES = false; var DRAW_HAND_SPHERES = false; var DROP_WITHOUT_SHAKE = false; @@ -80,6 +80,8 @@ var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing. +var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand + var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object @@ -870,26 +872,48 @@ function MyController(hand) { this.createHotspots = function () { var props, overlay; - var HAND_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; - var HAND_SPHERE_ALPHA = 0.7; - var HAND_SPHERE_RADIUS = 0.01; + var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var HAND_EQUIP_SPHERE_ALPHA = 0.7; + var HAND_EQUIP_SPHERE_RADIUS = 0.01; + + var HAND_GRAB_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; + var HAND_GRAB_SPHERE_ALPHA = 0.3; + var HAND_GRAB_SPHERE_RADIUS = NEAR_GRAB_RADIUS; var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; var EQUIP_SPHERE_ALPHA = 0.3; - var NEAR_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; - var NEAR_SPHERE_ALPHA = 0.1; + var GRAB_BOX_COLOR = { red: 90, green: 90, blue: 255 }; + var GRAB_BOX_ALPHA = 0.1; this.hotspotOverlays = []; if (DRAW_HAND_SPHERES) { - // add tiny spheres around the palm. + // add tiny green sphere around the palm. var handPosition = this.getHandPosition(); overlay = Overlays.addOverlay("sphere", { position: handPosition, - size: HAND_SPHERE_RADIUS * 2, - color: HAND_SPHERE_COLOR, - alpha: HAND_SPHERE_ALPHA, + size: HAND_EQUIP_SPHERE_RADIUS * 2, + color: HAND_EQUIP_SPHERE_COLOR, + alpha: HAND_EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + this.hotspotOverlays.push({ + entityID: undefined, + overlay: overlay, + type: "hand" + }); + + // add larger blue sphere around the palm. + overlay = Overlays.addOverlay("sphere", { + position: handPosition, + size: HAND_GRAB_SPHERE_RADIUS * 2, + color: HAND_GRAB_SPHERE_COLOR, + alpha: HAND_GRAB_SPHERE_ALPHA, solid: true, visible: true, ignoreRayIntersection: true, @@ -931,15 +955,15 @@ function MyController(hand) { }); } - if (DRAW_GRAB_SPHERES && _this.entityIsGrabbable(entityID)) { + if (DRAW_GRAB_BOXES && _this.entityIsGrabbable(entityID)) { props = _this.entityPropertyCache.getProps(entityID); - overlay = Overlays.addOverlay("sphere", { + overlay = Overlays.addOverlay("cube", { rotation: props.rotation, position: props.position, - size: NEAR_GRAB_RADIUS * 2, - color: NEAR_SPHERE_COLOR, - alpha: NEAR_SPHERE_ALPHA, + size: props.dimensions, //{x: props.dimensions.x, y: props.dimensions.y, z: props.dimensions.z}, + color: GRAB_BOX_COLOR, + alpha: GRAB_BOX_ALPHA, solid: true, visible: true, ignoreRayIntersection: true, @@ -1254,7 +1278,7 @@ function MyController(hand) { } var grabbableEntities = candidateEntities.filter(function (entity) { - return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_RADIUS); + return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_MAX_DISTANCE); }); var rayPickInfo = this.calcRayPickInfo(this.hand); @@ -1807,11 +1831,11 @@ function MyController(hand) { this.lastUnequipCheckTime = now; if (props.parentID == MyAvatar.sessionUUID && - Vec3.length(props.localPosition) > NEAR_GRAB_PICK_RADIUS * 2.0) { + Vec3.length(props.localPosition) > NEAR_GRAB_MAX_DISTANCE) { var handPosition = this.getHandPosition(); // the center of the equipped object being far from the hand isn't enough to autoequip -- we also // need to fail the findEntities test. - var nearPickedCandidateEntities = Entities.findEntities(handPosition, EQUIP_RADIUS); + var nearPickedCandidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS); if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + @@ -2270,18 +2294,18 @@ Menu.addMenuItem({ Menu.addMenuItem({ menuName: "Developer > Grab Script", - menuItemName: "Draw Grab Spheres", + menuItemName: "Draw Grab Boxes", isCheckable: true, - isChecked: DRAW_GRAB_SPHERES + isChecked: DRAW_GRAB_BOXES }); function handleMenuItemEvent(menuItem) { if (menuItem === "Drop Without Shake") { DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake"); } - if (menuItem === "Draw Grab Spheres") { - DRAW_GRAB_SPHERES = Menu.isOptionChecked("Draw Grab Spheres"); - DRAW_HAND_SPHERES = DRAW_GRAB_SPHERES; + if (menuItem === "Draw Grab Boxes") { + DRAW_GRAB_BOXES = Menu.isOptionChecked("Draw Grab Boxes"); + DRAW_HAND_SPHERES = DRAW_GRAB_BOXES; } } From 7da854d98c70cf4272c4d68c84f898789e6f28e3 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 24 Jun 2016 13:35:47 -0700 Subject: [PATCH 5/7] do not reset hud from button --- interface/src/ui/OverlayConductor.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 6a99641ce4..7d0dc6c650 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -118,10 +118,12 @@ void OverlayConductor::update(float dt) { bool isDriving = updateAvatarHasDriveInput(); bool drivingChanged = prevDriving != isDriving; bool isAtRest = updateAvatarIsAtRest(); + bool shouldRecenter = false; if (_flags & SuppressedByDrive) { if (!isDriving) { _flags &= ~SuppressedByDrive; + shouldRecenter = true; } } else { if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { @@ -132,6 +134,7 @@ void OverlayConductor::update(float dt) { if (_flags & SuppressedByHead) { if (isAtRest) { _flags &= ~SuppressedByHead; + shouldRecenter = true; } } else { if (_hmdMode && headOutsideOverlay()) { @@ -143,8 +146,8 @@ void OverlayConductor::update(float dt) { bool targetVisible = Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && (0 == (_flags & SuppressMask)); if (targetVisible != currentVisible) { offscreenUi->setPinned(!targetVisible); - if (targetVisible && _hmdMode) { - centerUI(); - } + } + if (shouldRecenter) { + centerUI(); } } From 2ae0a7defc2bfbcdb4fc030d56153e1736363d37 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 24 Jun 2016 14:46:46 -0700 Subject: [PATCH 6/7] If there are TWO conditions holding things back, weight for them both to clear. --- interface/src/ui/OverlayConductor.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 7d0dc6c650..e18522cb2f 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -123,7 +123,9 @@ void OverlayConductor::update(float dt) { if (_flags & SuppressedByDrive) { if (!isDriving) { _flags &= ~SuppressedByDrive; - shouldRecenter = true; + if (_flags & SuppressMask) { + shouldRecenter = true; + } } } else { if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { @@ -134,7 +136,9 @@ void OverlayConductor::update(float dt) { if (_flags & SuppressedByHead) { if (isAtRest) { _flags &= ~SuppressedByHead; - shouldRecenter = true; + if (_flags & SuppressMask) { + shouldRecenter = true; + } } } else { if (_hmdMode && headOutsideOverlay()) { From 9ae3c386166a6834c8c571324189c65d1fe51b69 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 24 Jun 2016 15:26:24 -0700 Subject: [PATCH 7/7] doh! --- interface/src/ui/OverlayConductor.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index e18522cb2f..2ee106b6b3 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -123,9 +123,7 @@ void OverlayConductor::update(float dt) { if (_flags & SuppressedByDrive) { if (!isDriving) { _flags &= ~SuppressedByDrive; - if (_flags & SuppressMask) { - shouldRecenter = true; - } + shouldRecenter = true; } } else { if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { @@ -136,9 +134,7 @@ void OverlayConductor::update(float dt) { if (_flags & SuppressedByHead) { if (isAtRest) { _flags &= ~SuppressedByHead; - if (_flags & SuppressMask) { - shouldRecenter = true; - } + shouldRecenter = true; } } else { if (_hmdMode && headOutsideOverlay()) { @@ -151,7 +147,7 @@ void OverlayConductor::update(float dt) { if (targetVisible != currentVisible) { offscreenUi->setPinned(!targetVisible); } - if (shouldRecenter) { + if (shouldRecenter && !_flags) { centerUI(); } }