From 17591323777d46f350221e8d34b6bc6d831ca054 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 14 Jun 2016 18:01:10 -0700 Subject: [PATCH] Use trigger to both grab and equip objects * Removed entry into HOLD state via secondary aka grip buttons. * Changed equip logic to prioritize equip over near over far grabs. * Added drop gesture, upside down controller + slight shake + trigger press * Bug fix for near grab snapping to an incorrect offset after a far grab (this bug is present in master) --- .../system/controllers/handControllerGrab.js | 743 +++++++++++------- 1 file changed, 466 insertions(+), 277 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 4ec1675ede..8c5d48667d 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -14,12 +14,11 @@ Script.include("/~/system/libraries/utils.js"); - // // add lines where the hand ray picking is happening // var WANT_DEBUG = false; -var WANT_DEBUG_STATE = false; +var WANT_DEBUG_STATE = true; var WANT_DEBUG_SEARCH_NAME = null; // @@ -126,8 +125,6 @@ var DEFAULT_GRABBABLE_DATA = { disableReleaseVelocity: false }; - - // sometimes we want to exclude objects from being picked var USE_BLACKLIST = true; var blacklist = []; @@ -140,19 +137,20 @@ var USE_ENTITY_LINES_FOR_MOVING = false; var USE_OVERLAY_LINES_FOR_MOVING = false; var USE_PARTICLE_BEAM_FOR_MOVING = true; - var USE_SPOTLIGHT = false; var USE_POINTLIGHT = false; +var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; +var FORBIDDEN_GRAB_TYPES = ['Unknown', 'Light', 'PolyLine', 'Zone']; + // states for the state machine var STATE_OFF = 0; var STATE_SEARCHING = 1; -var STATE_HOLD_SEARCHING = 2; -var STATE_DISTANCE_HOLDING = 3; -var STATE_NEAR_GRABBING = 4; -var STATE_NEAR_TRIGGER = 5; -var STATE_FAR_TRIGGER = 6; -var STATE_HOLD = 7; +var STATE_DISTANCE_HOLDING = 2; +var STATE_NEAR_GRABBING = 3; +var STATE_NEAR_TRIGGER = 4; +var STATE_FAR_TRIGGER = 5; +var STATE_HOLD = 6; // "collidesWith" is specified by comma-separated list of group names // the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar @@ -175,10 +173,6 @@ CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { enterMethod: "searchEnter", exitMethod: "searchExit" }; -CONTROLLER_STATE_MACHINE[STATE_HOLD_SEARCHING] = { - name: "hold_searching", - updateMethod: "search" -}; CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { name: "distance_holding", enterMethod: "distanceHoldingEnter", @@ -259,6 +253,58 @@ function restore2DMode() { } } +function filter(array, predicate) { + var i, l = array.length; + var result = []; + for (i = 0; i < l; i++) { + if (predicate(array[i])) { + result.push(array[i]); + } + } + return result; +} + +// constructor +function EntityPropertiesCache() { + this.cache = {}; +} +EntityPropertiesCache.prototype.clear = function() { + this.cache = {}; +}; +EntityPropertiesCache.prototype.findEntities = function(position, radius) { + var entities = Entities.findEntities(position, radius); + var i, l = entities.length; + for (i = 0; i < l; i++) { + this.addEntity(entities[i]); + } +}; +EntityPropertiesCache.prototype.addEntity = 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 }; +}; +EntityPropertiesCache.prototype.getEntities = function() { + return Object.keys(this.cache); +} +EntityPropertiesCache.prototype.getProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.props : undefined; +}; +EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.grabbableProps : undefined; +}; +EntityPropertiesCache.prototype.getGrabProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.grabProps : undefined; +}; +EntityPropertiesCache.prototype.getWearableProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.wearableProps : undefined; +}; + function MyController(hand) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -295,6 +341,8 @@ function MyController(hand) { this.overlayLine = null; this.searchSphere = null; + this.waitForTriggerRelease = false; + // how far from camera to search intersection? var DEFAULT_SEARCH_SPHERE_DISTANCE = 1000; this.intersectionDistance = 0.0; @@ -307,16 +355,18 @@ function MyController(hand) { this.lastPickTime = 0; this.lastUnequipCheckTime = 0; + this.entityPropertyCache = new EntityPropertiesCache(); + var _this = this; - var suppressedIn2D = [STATE_OFF, STATE_SEARCHING, STATE_HOLD_SEARCHING]; + var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; this.ignoreInput = function () { // We've made the decision to use 'this' for new code, even though it is fragile, // in order to keep/ the code uniform without making any no-op line changes. return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); }; - this.update = function() { + this.update = function(deltaTime) { this.updateSmoothedTrigger(); @@ -329,7 +379,7 @@ function MyController(hand) { var updateMethodName = CONTROLLER_STATE_MACHINE[this.state].updateMethod; var updateMethod = this[updateMethodName]; if (updateMethod) { - updateMethod.call(this); + updateMethod.call(this, deltaTime); } else { print("WARNING: could not find updateMethod for state " + stateToName(this.state)); } @@ -343,12 +393,12 @@ function MyController(hand) { Entities.callEntityMethod(this.grabbedEntity, entityMethodName, args); } - this.setState = function(newState) { + this.setState = function(newState, reason) { this.grabSphereOff(); if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); var newStateName = stateToName(newState); - print("STATE (" + this.hand + "): " + newStateName + " <-- " + oldStateName); + print("STATE (" + this.hand + "): " + newStateName + " <-- " + oldStateName + ", reason = " + reason); } // exit the old state @@ -819,14 +869,16 @@ function MyController(hand) { }; this.off = function() { - if (this.triggerSmoothedSqueezed() || this.secondarySqueezed()) { + + if (this.triggerSmoothedReleased()) { + this.waitForTriggerRelease = false; + } + if (!this.waitForTriggerRelease && this.triggerSmoothedSqueezed()) { this.lastPickTime = 0; var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; if (this.triggerSmoothedSqueezed()) { - this.setState(STATE_SEARCHING); - } else if (this.secondarySqueezed()) { - this.setState(STATE_HOLD_SEARCHING); + this.setState(STATE_SEARCHING, "trigger squeeze detected"); } } }; @@ -875,265 +927,345 @@ function MyController(hand) { this.equipHotspotOverlays = []; }; + /** + * Performs ray pick test from the hand controller into the world + * @param {number} which hand to use, RIGHT_HAND or LEFT_HAND + * @returns {object} returns object with two keys entityID and distance + */ + this.calcRayPickInfo = function(hand) { + + var pose = Controller.getPoseValue((hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var worldHandPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position); + var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); + + var pickRay = { + origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, + direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), + Quat.getFront(Camera.orientation), + HAND_HEAD_MIX_RATIO), + length: PICK_MAX_DISTANCE + }; + + var result = { entityID: null, + searchRay: pickRay, + distance: PICK_MAX_DISTANCE }; + + var now = Date.now(); + if (now - this.lastPickTime < MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { + return result; + } + this.lastPickTime = now; + + var directionNormalized = Vec3.normalize(pickRay.direction); + var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE); + var pickRayBacked = { + origin: Vec3.subtract(pickRay.origin, directionBacked), + direction: pickRay.direction + }; + + var intersection; + if (USE_BLACKLIST === true && blacklist.length !== 0) { + intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); + } else { + intersection = Entities.findRayIntersection(pickRayBacked, true); + } + + var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); + if (!intersection.intersects || (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { + intersection = overlayIntersection; + } + + if (intersection.intersects) { + return { entityID: intersection.entityID, + searchRay: pickRay, + distance: Vec3.distance(pickRay.origin, intersection.intersection) } + } else { + return result; + } + }; + + this.entityWantsTrigger = function (entityID) { + var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); + return grabbableProps && grabbableProps.wantsTrigger; + }; + + this.entityIsEquippable = function (entityID, handPosition) { + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var grabProps = this.entityPropertyCache.getGrabProps(entityID); + var debug = true;//(WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; + if (refCount > 0) { + if (debug) { + print("equip is skipping '" + props.name + "': it is already grabbed"); + } + return false; + } + + if (distance > NEAR_PICK_MAX_DISTANCE) { + if (debug) { + print("equip is skipping '" + props.name + "': too far away."); + } + return false; + } + + var wearableProps = this.entityPropertyCache.getWearableProps(entityID); + if (!wearableProps || !wearableProps.joints) { + if (debug) { + print("equip is skipping '" + props.name + "': no wearable attach-point"); + } + return false; + } + + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (!wearableProps.joints[handJointName]) { + if (debug) { + print("equip is skipping '" + props.name + "': no wearable joint for " + handJointName); + } + return false; + } + + return true; + }; + + this.entityIsGrabbable = function (entityID) { + var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); + var grabProps = this.entityPropertyCache.getGrabProps(entityID); + var props = this.entityPropertyCache.getProps(entityID); + var physical = propsArePhysical(props); + var grabbable = false; + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + if (physical) { + // physical things default to grabbable + grabbable = true; + } else { + // non-physical things default to non-grabbable unless they are already grabbed + if ("refCount" in grabProps && grabProps.refCount > 0) { + grabbable = true; + } else { + grabbable = false; + } + } + + if (grabbableProps.hasOwnProperty("grabbable")) { + grabbable = grabbableProps.grabbable; + } + + if (!grabbable && !grabbableProps.wantsTrigger) { + if (debug) { + print("grab is skipping '" + props.name + "': not grabbable."); + } + return false; + } + if (FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0) { + if (debug) { + print("grab is skipping '" + props.name + "': forbidden entity type."); + } + return false; + } + if (props.locked && !grabbableProps.wantsTrigger) { + if (debug) { + print("grab is skipping '" + props.name + "': locked and not triggerable."); + } + return false; + } + if (FORBIDDEN_GRAB_NAMES.indexOf(props.name) >= 0) { + if (debug) { + print("grab is skipping '" + props.name + "': forbidden name."); + } + return false; + } + + return true; + }; + + this.entityIsDistanceGrabbable = function(entityID, handPosition) { + if (!this.entityIsGrabbable(entityID)) { + return false; + } + + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + // we can't distance-grab non-physical + var isPhysical = propsArePhysical(props); + if (!isPhysical) { + if (debug) { + print("distance grab is skipping '" + props.name + "': not physical"); + } + return false; + } + + if (distance > PICK_MAX_DISTANCE) { + // too far away, don't grab + if (debug) { + print("distance grab is skipping '" + props.name + "': too far away."); + } + return false; + } + + if (entityIsGrabbedByOther(entityID)) { + // don't distance grab something that is already grabbed. + if (debug) { + print("distance grab is skipping '" + props.name + "': already grabbed by another."); + } + return false; + } + + return true; + }; + + this.entityIsNearGrabbable = function(entityID, handPosition) { + + if (!this.entityIsGrabbable(entityID)) { + return false; + } + + var props = this.entityPropertyCache.getProps(entityID); + 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) { + // too far away, don't grab + if (debug) { + print(" grab is skipping '" + props.name + "': too far away."); + } + return false; + } + + return true; + }; + this.search = function() { + var _this = this; + var name; + this.grabbedEntity = null; this.isInitialGrab = false; this.shouldResetParentOnRelease = false; this.checkForStrayChildren(); - if (this.state == STATE_SEARCHING && this.triggerSmoothedReleased()) { - this.setState(STATE_OFF); + if (this.triggerSmoothedReleased()) { + this.setState(STATE_OFF, "trigger released"); return; } - if (this.state == STATE_HOLD_SEARCHING && this.secondaryReleased()) { - this.setState(STATE_OFF); - return; - } - - // the trigger is being pressed, so do a ray test to see what we are hitting var handPosition = this.getHandPosition(); if (SHOW_GRAB_SPHERE) { this.grabSphereOn(); } - var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; - var currentControllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, - Controller.getPoseValue(controllerHandInput).translation), - MyAvatar.position); - var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? - Controller.Standard.RightHand : Controller.Standard.LeftHand); - var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); + this.entityPropertyCache.clear(); + this.entityPropertyCache.findEntities(handPosition, GRAB_RADIUS); + var candidateEntities = this.entityPropertyCache.getEntities(); - var distantPickRay = { - origin: PICK_WITH_HAND_RAY ? currentControllerPosition : Camera.position, - direction: PICK_WITH_HAND_RAY ? Quat.getUp(controllerRotation) : Vec3.mix(Quat.getUp(controllerRotation), - Quat.getFront(Camera.orientation), - HAND_HEAD_MIX_RATIO), - length: PICK_MAX_DISTANCE - }; + var equippableEntities = filter(candidateEntities, function (entity) { + return _this.entityIsEquippable(entity, handPosition); + }); - // Pick at some maximum rate, not always - var pickRays = []; - var now = Date.now(); - if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { - pickRays = [distantPickRay]; - this.lastPickTime = now; - } - - var rayPickedCandidateEntities = []; // the list of candidates to consider grabbing - - this.intersectionDistance = 0.0; - for (var index = 0; index < pickRays.length; ++index) { - var pickRay = pickRays[index]; - var directionNormalized = Vec3.normalize(pickRay.direction); - var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE); - var pickRayBacked = { - origin: Vec3.subtract(pickRay.origin, directionBacked), - direction: pickRay.direction - }; - - var intersection; - - if (USE_BLACKLIST === true && blacklist.length !== 0) { - intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); - } else { - intersection = Entities.findRayIntersection(pickRayBacked, true); - } - var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); - if (!intersection.intersects || (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { - intersection = overlayIntersection; - } - // If we want to share results with other scripts, this is where we would do it. - - if (intersection.intersects) { - if (intersection.entityID) { - rayPickedCandidateEntities.push(intersection.entityID); - } - this.intersectionDistance = Vec3.distance(pickRay.origin, intersection.intersection); - } - } - - var nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); - var candidateEntities = rayPickedCandidateEntities.concat(nearPickedCandidateEntities); - - var forbiddenNames = ["Grab Debug Entity", "grab pointer"]; - var forbiddenTypes = ['Unknown', 'Light', 'PolyLine', 'Zone']; - - var minDistance = PICK_MAX_DISTANCE; - var i, props, distance, grabbableData; - this.grabbedEntity = null; - for (i = 0; i < candidateEntities.length; i++) { - var grabbableDataForCandidate = - getEntityCustomData(GRABBABLE_DATA_KEY, candidateEntities[i], DEFAULT_GRABBABLE_DATA); - var grabDataForCandidate = getEntityCustomData(GRAB_USER_DATA_KEY, candidateEntities[i], {}); - var propsForCandidate = Entities.getEntityProperties(candidateEntities[i], GRABBABLE_PROPERTIES); - var near = (nearPickedCandidateEntities.indexOf(candidateEntities[i]) >= 0); - - var physical = propsArePhysical(propsForCandidate); - var grabbable; - if (physical) { - // physical things default to grabbable - grabbable = true; - } else { - // non-physical things default to non-grabbable unless they are already grabbed - if ("refCount" in grabDataForCandidate && grabDataForCandidate.refCount > 0) { - grabbable = true; - } else { - grabbable = false; - } - } - - if ("grabbable" in grabbableDataForCandidate) { - // if userData indicates that this is grabbable or not, override the default. - grabbable = grabbableDataForCandidate.grabbable; - } - - if (!grabbable && !grabbableDataForCandidate.wantsTrigger) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': not grabbable."); - } - continue; - } - if (forbiddenTypes.indexOf(propsForCandidate.type) >= 0) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': forbidden entity type."); - } - continue; - } - if (propsForCandidate.locked && !grabbableDataForCandidate.wantsTrigger) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': locked and not triggerable."); - } - continue; - } - if (forbiddenNames.indexOf(propsForCandidate.name) >= 0) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': forbidden name."); - } - continue; - } - - distance = Vec3.distance(propsForCandidate.position, handPosition); - if (distance > PICK_MAX_DISTANCE) { - // too far away, don't grab - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': too far away."); - } - continue; - } - if (propsForCandidate.parentID != NULL_UUID && this.state == STATE_HOLD_SEARCHING) { - // don't allow a double-equip - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': it's a child"); - } - continue; - } - - if (this.state == STATE_SEARCHING && - !physical && distance > NEAR_PICK_MAX_DISTANCE && !near && !grabbableDataForCandidate.wantsTrigger) { - // we can't distance-grab non-physical - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': not physical and too far for near-grab"); - } - continue; - } - - if (distance < minDistance) { - this.grabbedEntity = candidateEntities[i]; - minDistance = distance; - props = propsForCandidate; - grabbableData = grabbableDataForCandidate; - } - } - if ((this.grabbedEntity !== null) && (this.triggerSmoothedGrab() || this.secondarySqueezed())) { - // We are squeezing enough to grab, and we've found an entity that we'll try to do something with. - var isNear = (nearPickedCandidateEntities.indexOf(this.grabbedEntity) >= 0) || minDistance <= NEAR_PICK_MAX_DISTANCE; - var isPhysical = propsArePhysical(props); - - // near or far trigger - if (grabbableData.wantsTrigger) { - this.setState(isNear ? STATE_NEAR_TRIGGER : STATE_FAR_TRIGGER); + var entity; + if (equippableEntities.length > 0) { + // sort by distance + equippableEntities.sort(function (a, b) { + var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); + var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + return aDistance - bDistance; + }); + entity = equippableEntities[0]; + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(entity).name + "'"); return; + } else { + // TODO: highlight the equippable object? } - // near grab with action or equip - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); - var refCount = ("refCount" in grabData) ? grabData.refCount : 0; - if (isNear && (refCount < 1 || entityHasActions(this.grabbedEntity))) { - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // (this.state == STATE_HOLD_SEARCHING) - // if there was already an action, we'll need to set the parent back to null once we release - this.shouldResetParentOnRelease = true; - this.previousParentID = props.parentID; - this.previousParentJointIndex = props.parentJointIndex; - this.setState(STATE_HOLD); - } - return; - } - // far grab - if (isPhysical && !isNear) { - if (entityIsGrabbedByOther(this.grabbedEntity)) { - // don't distance grab something that is already grabbed. - if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': already grabbed by another."); - } + } + + var rayPickInfo = this.calcRayPickInfo(this.hand); + this.intersectionDistance = rayPickInfo.distance; + if (rayPickInfo.entityID) { + candidateEntities.push(rayPickInfo.entityID); + this.entityPropertyCache.addEntity(rayPickInfo.entityID); + } + + var grabbableEntities = filter(candidateEntities, function (entity) { + return _this.entityIsNearGrabbable(entity, handPosition); + }); + + if (grabbableEntities.length > 0) { + // sort by distance + grabbableEntities.sort(function (a, b) { + var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); + var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + return aDistance - bDistance; + }); + entity = grabbableEntities[0]; + name = this.entityPropertyCache.getProps(entity).name; + this.grabbedEntity = entity; + if (this.entityWantsTrigger(entity)) { + if (this.triggerSmoothedGrab()) { + this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); return; + } else { + // TODO: highlight the near-triggerable object? } - this.temporaryPositionOffset = null; - if (!this.hasPresetOffsets()) { - // We want to give a temporary position offset to this object so it is pulled close to hand - var intersectionPointToCenterDistance = Vec3.length(Vec3.subtract(intersection.intersection, - intersection.properties.position)); - var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - var handJointPosition = MyAvatar.getJointPosition(handJointIndex); - this.temporaryPositionOffset = - Vec3.normalize(Vec3.subtract(intersection.properties.position, handJointPosition)); - this.temporaryPositionOffset = Vec3.multiply(this.temporaryPositionOffset, - intersectionPointToCenterDistance * - FAR_TO_NEAR_GRAB_PADDING_FACTOR); - } - this.setState(STATE_DISTANCE_HOLDING); - - this.searchSphereOff(); - return; - } - - // else this thing isn't physical. grab it by reparenting it (but not if we've already - // grabbed it). - if (refCount < 1) { - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // this.state == STATE_HOLD_SEARCHING) - this.setState(STATE_HOLD); - } - return; } else { - // it's not physical and it's already held via parenting. go ahead and grab it, but - // save off the current parent and joint. this wont always be right if there are more than - // two grabs and the order of release isn't opposite of the order of grabs. - this.shouldResetParentOnRelease = true; - this.previousParentID = props.parentID; - this.previousParentJointIndex = props.parentJointIndex; - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // (this.state == STATE_HOLD_SEARCHING) - this.setState(STATE_HOLD); + if (this.triggerSmoothedGrab()) { + + var props = this.entityPropertyCache.getProps(entity); + var grabProps = this.entityPropertyCache.getGrabProps(entity); + var refCount = grabProps.refCount ? grabProps.refCount : 0; + if (refCount >= 1) { + // if another person is holding the object, remember to restore the + // parent info, when we are finished grabbing it. + this.shouldResetParentOnRelease = true; + this.previousParentID = props.parentID; + this.previousParentJointIndex = props.parentJointIndex; + } + + this.setState(STATE_NEAR_GRABBING, "near grab '" + name + "'"); + return; + } else { + // TODO: highlight the grabbable object? + } + } + return; + } + + if (rayPickInfo.entityID) { + entity = rayPickInfo.entityID; + name = this.entityPropertyCache.getProps(entity).name; + if (this.entityWantsTrigger(entity)) { + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_FAR_TRIGGER, "far trigger '" + name + "'"); + return; + } else { + // TODO: highlight the far-triggerable object? + } + } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); + return; + } else { + // TODO: highlight the far-grabbable object? } - return; } } //search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { - this.lineOn(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + this.lineOn(rayPickInfo.searchRay.origin, Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } - this.searchIndicatorOn(distantPickRay); + this.searchIndicatorOn(rayPickInfo.searchRay); Reticle.setVisible(false); - }; this.distanceGrabTimescale = function(mass, distance) { @@ -1210,8 +1342,8 @@ function MyController(hand) { }; this.distanceHolding = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_OFF); + if (this.triggerSmoothedReleased()) { + this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("releaseGrab"); return; } @@ -1230,7 +1362,7 @@ function MyController(hand) { var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); var now = Date.now(); - var deltaTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds this.currentObjectTime = now; // the action was set up when this.distanceHolding was called. update the targets. @@ -1267,17 +1399,17 @@ function MyController(hand) { // Update radialVelocity var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); - lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaTime); + lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaObjectTime); var newRadialVelocity = Vec3.dot(lastVelocity, Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); var VELOCITY_AVERAGING_TIME = 0.016; - this.grabRadialVelocity = (deltaTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + - (1.0 - (deltaTime / VELOCITY_AVERAGING_TIME)) * this.grabRadialVelocity; + this.grabRadialVelocity = (deltaObjectTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + + (1.0 - (deltaObjectTime / VELOCITY_AVERAGING_TIME)) * this.grabRadialVelocity; var RADIAL_GRAB_AMPLIFIER = 10.0; if (Math.abs(this.grabRadialVelocity) > 0.0) { - this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaTime * this.grabRadius * RADIAL_GRAB_AMPLIFIER); + this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * this.grabRadius * RADIAL_GRAB_AMPLIFIER); } var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation)); @@ -1431,11 +1563,56 @@ function MyController(hand) { } } + this.dropGestureReset = function() { + this.fastHandMoveDetected = false; + this.fastHandMoveTimer = 0; + }; + + this.dropGestureProcess = function(deltaTime) { + var pose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var worldHandVelocity = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); + var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); + + if (this.fastHandMoveDetected) { + this.fastHandMoveTimer -= deltaTime; + } + if (this.fastHandMoveTimer < 0) { + this.fastHandMoveDetected = false; + } + var FAST_HAND_SPEED_REST_TIME = 1; // sec + var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec + if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) { + this.fastHandMoveDetected = true; + this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME; + } + + var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0}; + var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); + var DOWN = {x: 0, y: -1, z: 0}; + var ROTATION_THRESHOLD = Math.cos(Math.PI / 8); + + var handIsUpsideDown = false; + if (Vec3.dot(worldHandUpAxis, DOWN) > ROTATION_THRESHOLD) { + handIsUpsideDown = true; + } + + var WANT_DEBUG = false; + if (WANT_DEBUG) { + print("zAxis = " + worldHandUpAxis.x + ", " + worldHandUpAxis.y + ", " + worldHandUpAxis.z); + print("dot = " + Vec3.dot(worldHandUpAxis, DOWN) + ", ROTATION_THRESHOLD = " + ROTATION_THRESHOLD); + print("handMove = " + this.fastHandMoveDetected + ", handIsUpsideDown = " + handIsUpsideDown); + } + + return this.fastHandMoveDetected && handIsUpsideDown; + }; + this.nearGrabbingEnter = function() { this.lineOff(); this.overlayLineOff(); + this.dropGestureReset(); + if (this.entityActivated) { var saveGrabbedID = this.grabbedEntity; this.release(); @@ -1524,14 +1701,18 @@ function MyController(hand) { this.currentAngularVelocity = ZERO_VEC; }; - this.nearGrabbing = function() { + this.nearGrabbing = function(deltaTime) { + + var dropDetected = this.dropGestureProcess(deltaTime); + if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { - this.setState(STATE_OFF); + this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("releaseGrab"); return; } - if (this.state == STATE_HOLD && this.secondaryReleased()) { - this.setState(STATE_OFF); + + if (this.state == STATE_HOLD && dropDetected && this.triggerSmoothedGrab()) { + this.setState(STATE_OFF, "drop detected"); this.callEntityMethodOnGrabbed("releaseEquip"); return; } @@ -1541,7 +1722,7 @@ function MyController(hand) { var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position", "rotation"]); if (!props.position) { // server may have reset, taking our equipped entity with it. move back to "off" stte - this.setState(STATE_OFF); + this.setState(STATE_OFF, "entity has no position property"); this.callEntityMethodOnGrabbed("releaseGrab"); return; } @@ -1561,7 +1742,7 @@ function MyController(hand) { // 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." + props.parentID + " " + vec3toStr(props.position)); - this.setState(STATE_OFF); + this.setState(STATE_OFF, "held object too far away"); if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); } else { // this.state == STATE_HOLD @@ -1581,17 +1762,17 @@ function MyController(hand) { var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; - var deltaTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds - if (deltaTime > 0.0) { + if (deltaObjectTime > 0.0) { var worldDeltaPosition = Vec3.subtract(props.position, this.currentObjectPosition); var previousEulers = Quat.safeEulerAngles(this.currentObjectRotation); var newEulers = Quat.safeEulerAngles(props.rotation); var worldDeltaRotation = Vec3.subtract(newEulers, previousEulers); - this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaTime); - this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaTime * 180.0)); + this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaObjectTime); + this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaObjectTime * 180.0)); this.currentObjectPosition = props.position; this.currentObjectRotation = props.rotation; @@ -1638,8 +1819,8 @@ function MyController(hand) { }; this.nearTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_OFF); + if (this.triggerSmoothedReleased()) { + this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("stopNearTrigger"); return; } @@ -1647,8 +1828,8 @@ function MyController(hand) { }; this.farTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_OFF); + if (this.triggerSmoothedReleased()) { + this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; } @@ -1665,7 +1846,7 @@ function MyController(hand) { if (intersection.accurate) { this.lastPickTime = now; if (intersection.entityID != this.grabbedEntity) { - this.setState(STATE_OFF); + this.setState(STATE_OFF, "laser moved off of entity"); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; } @@ -1680,6 +1861,10 @@ function MyController(hand) { }; this.offEnter = function() { + this.release(); + }; + + this.release = function() { this.turnLightsOff(); this.turnOffVisualizations(); @@ -1710,6 +1895,10 @@ function MyController(hand) { })); this.grabbedEntity = null; + + if (this.triggerSmoothedGrab()) { + this.waitForTriggerRelease = true; + } }; this.cleanup = function() { @@ -1918,12 +2107,12 @@ Controller.enableMapping(MAPPING_NAME); //the section below allows the grab script to listen for messages that disable either one or both hands. useful for two handed items var handToDisable = 'none'; -function update() { +function update(deltaTime) { if (handToDisable !== LEFT_HAND && handToDisable !== 'both') { - leftController.update(); + leftController.update(deltaTime); } if (handToDisable !== RIGHT_HAND && handToDisable !== 'both') { - rightController.update(); + rightController.update(deltaTime); } } @@ -1950,7 +2139,7 @@ var handleHandMessages = function(channel, message, sender) { data = JSON.parse(message); var selectedController = (data.hand === 'left') ? leftController : rightController; selectedController.release(); - selectedController.setState(STATE_HOLD); + selectedController.setState(STATE_HOLD, "Hifi-Hand-Grab msg received"); selectedController.grabbedEntity = data.entityID; } catch (e) {