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)
This commit is contained in:
Anthony J. Thibault 2016-06-14 18:01:10 -07:00
parent 4aa5e87a26
commit 1759132377

View file

@ -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) {