From 473db92a8edb255549d79428fda3311616157257 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Sep 2017 11:07:47 -0700 Subject: [PATCH] fix some parent-grab bugs, re-enable adjusting equipped entities, don't trigger guns right when they are equipped --- .../controllers/controllerDispatcher.js | 3 +- .../controllerModules/equipEntity.js | 40 +++-- .../controllerModules/nearActionGrabEntity.js | 3 +- .../controllerModules/nearParentGrabEntity.js | 155 +++++++++++++----- .../libraries/controllerDispatcherUtils.js | 73 +++++++-- 5 files changed, 201 insertions(+), 73 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 63657e9b6f..a71d1d8a2a 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -197,7 +197,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); var h; for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { if (controllerLocations[h].valid) { - var nearbyOverlays = Overlays.findOverlays(controllerLocations[h].position, NEAR_MAX_RADIUS * sensorScaleFactor); + var nearbyOverlays = + Overlays.findOverlays(controllerLocations[h].position, NEAR_MAX_RADIUS * sensorScaleFactor); nearbyOverlays.sort(function (a, b) { var aPosition = Overlays.getProperty(a, "position"); var aDistance = Vec3.distance(aPosition, controllerLocations[h].position); diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index fe868493f4..3431f8d3c3 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -254,6 +254,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.triggerValue = 0; this.messageGrabEntity = false; this.grabEntityProps = null; + this.shouldSendStart = false; this.parameters = makeDispatcherModuleParameters( 300, @@ -507,8 +508,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa return; } - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "startEquip", args); + // we don't want to send startEquip message until the trigger is released. otherwise, + // guns etc will fire right as they are equipped. + this.shouldSendStart = true; Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'equip', @@ -588,22 +590,21 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa // if the potentialHotspot is cloneable, clone it and return it // if the potentialHotspot os not cloneable and locked return null - if (potentialEquipHotspot) { - if ((this.triggerSmoothedSqueezed() && !this.waitForTriggerRelease) || this.messageGrabEntity) { - this.grabbedHotspot = potentialEquipHotspot; - this.targetEntityID = this.grabbedHotspot.entityID; - this.startEquipEntity(controllerData); - this.messageGrabEnity = false; - } + if (potentialEquipHotspot && + ((this.triggerSmoothedSqueezed() && !this.waitForTriggerRelease) || this.messageGrabEntity)) { + this.grabbedHotspot = potentialEquipHotspot; + this.targetEntityID = this.grabbedHotspot.entityID; + this.startEquipEntity(controllerData); + this.messageGrabEnity = false; return makeRunningValues(true, [potentialEquipHotspot.entityID], []); } else { return makeRunningValues(false, [], []); } }; - this.isTargetIDValid = function() { - var entityProperties = Entities.getEntityProperties(this.targetEntityID, ["type"]); - return "type" in entityProperties; + this.isTargetIDValid = function(controllerData) { + var entityProperties = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; + return entityProperties && "type" in entityProperties; }; this.isReady = function (controllerData, deltaTime) { @@ -616,7 +617,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var timestamp = Date.now(); this.updateInputs(controllerData); - if (!this.isTargetIDValid()) { + if (!this.isTargetIDValid(controllerData)) { this.endEquipEntity(); return makeRunningValues(false, [], []); } @@ -643,6 +644,13 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var dropDetected = this.dropGestureProcess(deltaTime); if (this.triggerSmoothedReleased()) { + if (this.shouldSendStart) { + // we don't want to send startEquip message until the trigger is released. otherwise, + // guns etc will fire right as they are equipped. + var startArgs = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "startEquip", startArgs); + this.shouldSendStart = false; + } this.waitForTriggerRelease = false; } @@ -674,8 +682,10 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa equipHotspotBuddy.update(deltaTime, timestamp, controllerData); - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "continueEquip", args); + if (!this.shouldSendStart) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueEquip", args); + } return makeRunningValues(true, [this.targetEntityID], []); }; diff --git a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js index bd7a64572a..41a5202887 100644 --- a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js @@ -182,7 +182,8 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); } if (targetProps) { - if (!propsArePhysical(targetProps) && !propsAreCloneDynamic(targetProps)) { + if ((!propsArePhysical(targetProps) && !propsAreCloneDynamic(targetProps)) || + targetProps.parentID != NULL_UUID) { return makeRunningValues(false, [], []); // let nearParentGrabEntity handle it } else { this.targetEntityID = targetProps.id; diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index e08b61dbd5..51790f0bfd 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -8,8 +8,10 @@ /* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID, getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule, propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, - TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, findGroupParent, - Vec3, cloneEntity, entityIsCloneable, propsAreCloneDynamic, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE + TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, + findGroupParent, Vec3, cloneEntity, entityIsCloneable, propsAreCloneDynamic, HAPTIC_PULSE_STRENGTH, + HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, findHandChildEntities, TEAR_AWAY_DISTANCE, MSECS_PER_SEC, TEAR_AWAY_CHECK_TIME, + TEAR_AWAY_COUNT, distanceBetweenPointAndEntityBoundingBox */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -28,6 +30,9 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); this.previousParentJointIndex = {}; this.previouslyUnhooked = {}; this.hapticTargetID = null; + this.lastUnequipCheckTime = 0; + this.autoUnequipCounter = 0; + this.lastUnexpectedChildrenCheckTime = 0; this.parameters = makeDispatcherModuleParameters( 500, @@ -40,15 +45,11 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); this.controllerJointIndex = getControllerJointIndex(this.hand); - this.getOtherModule = function() { - return (this.hand === RIGHT_HAND) ? leftNearParentingGrabEntity : rightNearParentingGrabEntity; - }; - - this.otherHandIsParent = function(props) { - return this.getOtherModule().thisHandIsParent(props); - }; - this.thisHandIsParent = function(props) { + if (!props) { + return false; + } + if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== AVATAR_SELF_ID) { return false; } @@ -97,14 +98,8 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); if (this.thisHandIsParent(targetProps)) { // this should never happen, but if it does, don't set previous parent to be this hand. - // this.previousParentID[targetProps.id] = NULL; - // this.previousParentJointIndex[targetProps.id] = -1; - } else if (this.otherHandIsParent(targetProps)) { - // the other hand is parent. Steal the object and information - var otherModule = this.getOtherModule(); - this.previousParentID[targetProps.id] = otherModule.previousParentID[targetProps.id]; - this.previousParentJointIndex[targetProps.id] = otherModule.previousParentJointIndex[targetProps.id]; - otherModule.endNearParentingGrabEntity(); + this.previousParentID[targetProps.id] = null; + this.previousParentJointIndex[targetProps.id] = -1; } else { this.previousParentID[targetProps.id] = targetProps.parentID; this.previousParentJointIndex[targetProps.id] = targetProps.parentJointIndex; @@ -121,20 +116,24 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); this.grabbing = true; }; - this.endNearParentingGrabEntity = function () { - if (this.previousParentID[this.targetEntityID] === NULL_UUID || this.previousParentID === undefined) { - Entities.editEntity(this.targetEntityID, { - parentID: this.previousParentID[this.targetEntityID], - parentJointIndex: this.previousParentJointIndex[this.targetEntityID] - }); - } else { - // we're putting this back as a child of some other parent, so zero its velocity - Entities.editEntity(this.targetEntityID, { - parentID: this.previousParentID[this.targetEntityID], - parentJointIndex: this.previousParentJointIndex[this.targetEntityID], - localVelocity: {x: 0, y: 0, z: 0}, - localAngularVelocity: {x: 0, y: 0, z: 0} - }); + this.endNearParentingGrabEntity = function (controllerData) { + this.hapticTargetID = null; + var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; + if (this.thisHandIsParent(props)) { + if (this.previousParentID[this.targetEntityID] === NULL_UUID || this.previousParentID === undefined) { + Entities.editEntity(this.targetEntityID, { + parentID: this.previousParentID[this.targetEntityID], + parentJointIndex: this.previousParentJointIndex[this.targetEntityID] + }); + } else { + // we're putting this back as a child of some other parent, so zero its velocity + Entities.editEntity(this.targetEntityID, { + parentID: this.previousParentID[this.targetEntityID], + parentJointIndex: this.previousParentJointIndex[this.targetEntityID], + localVelocity: {x: 0, y: 0, z: 0}, + localAngularVelocity: {x: 0, y: 0, z: 0} + }); + } } var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; @@ -143,6 +142,71 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); this.targetEntityID = null; }; + this.checkForChildTooFarAway = function (controllerData) { + var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; + var now = Date.now(); + if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * TEAR_AWAY_CHECK_TIME) { + this.lastUnequipCheckTime = now; + if (props.parentID == AVATAR_SELF_ID) { + var handPosition = controllerData.controllerLocations[this.hand].position; + var dist = distanceBetweenPointAndEntityBoundingBox(handPosition, props); + if (dist > TEAR_AWAY_DISTANCE) { + this.autoUnequipCounter++; + } else { + this.autoUnequipCounter = 0; + } + if (this.autoUnequipCounter >= TEAR_AWAY_COUNT) { + return true; + } + } + } + return false; + }; + + + this.checkForUnexpectedChildren = function (controllerData) { + // sometimes things can get parented to a hand and this script is unaware. Search for such entities and + // unhook them. + + var now = Date.now(); + var UNEXPECTED_CHILDREN_CHECK_TIME = 0.1; // seconds + if (now - this.lastUnexpectedChildrenCheckTime > MSECS_PER_SEC * UNEXPECTED_CHILDREN_CHECK_TIME) { + this.lastUnexpectedChildrenCheckTime = now; + + var children = findHandChildEntities(this.hand); + var _this = this; + + children.forEach(function(childID) { + // we appear to be holding something and this script isn't in a state that would be holding something. + // unhook it. if we previously took note of this entity's parent, put it back where it was. This + // works around some problems that happen when more than one hand or avatar is passing something around. + if (_this.previousParentID[childID]) { + var previousParentID = _this.previousParentID[childID]; + var previousParentJointIndex = _this.previousParentJointIndex[childID]; + + // The main flaw with keeping track of previous parantage in individual scripts is: + // (1) A grabs something (2) B takes it from A (3) A takes it from B (4) A releases it + // now A and B will take turns passing it back to the other. Detect this and stop the loop here... + var UNHOOK_LOOP_DETECT_MS = 200; + if (_this.previouslyUnhooked[childID]) { + if (now - _this.previouslyUnhooked[childID] < UNHOOK_LOOP_DETECT_MS) { + previousParentID = NULL_UUID; + previousParentJointIndex = -1; + } + } + _this.previouslyUnhooked[childID] = now; + + Entities.editEntity(childID, { + parentID: previousParentID, + parentJointIndex: previousParentJointIndex + }); + } else { + Entities.editEntity(childID, { parentID: NULL_UUID }); + } + }); + } + }; + this.getTargetProps = function (controllerData) { // nearbyEntityProperties is already sorted by length from controller var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; @@ -178,11 +242,13 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); var targetProps = this.getTargetProps(controllerData); if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { + this.checkForUnexpectedChildren(controllerData); return makeRunningValues(false, [], []); } if (targetProps) { - if (propsArePhysical(targetProps) || propsAreCloneDynamic(targetProps)) { + if ((propsArePhysical(targetProps) || propsAreCloneDynamic(targetProps)) && + targetProps.parentID == NULL_UUID) { return makeRunningValues(false, [], []); // let nearActionGrabEntity handle it } else { this.targetEntityID = targetProps.id; @@ -198,16 +264,23 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); if (this.grabbing) { if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { - this.endNearParentingGrabEntity(); + this.endNearParentingGrabEntity(controllerData); + return makeRunningValues(false, [], []); + } + + var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; + if (!props) { + // entity was deleted + this.grabbing = false; + this.targetEntityID = null; this.hapticTargetID = null; return makeRunningValues(false, [], []); } - var props = Entities.getEntityProperties(this.targetEntityID); - if (!this.thisHandIsParent(props)) { - this.grabbing = false; - this.targetEntityID = null; - this.hapticTargetID = null; + if (this.checkForChildTooFarAway(controllerData)) { + // if the held entity moves too far from the hand, release it + print("nearParentGrabEntity -- autoreleasing held item because it is far from hand"); + this.endNearParentingGrabEntity(controllerData); return makeRunningValues(false, [], []); } @@ -215,7 +288,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args); } else { // still searching / highlighting - var readiness = this.isReady (controllerData); + var readiness = this.isReady(controllerData); if (!readiness.active) { return readiness; } @@ -227,7 +300,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); if (targetCloneable) { var worldEntityProps = controllerData.nearbyEntityProperties[this.hand]; var cloneID = cloneEntity(targetProps, worldEntityProps); - var cloneProps = Entities.getEntityProperties(cloneID); + var cloneProps = controllerData.nearbyEntityPropertiesByID[cloneID]; this.grabbing = true; this.targetEntityID = cloneID; diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 33eec74111..ab3bea3fdd 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -6,7 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, +/* global module, Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, Xform, MSECS_PER_SEC:true , LEFT_HAND:true, RIGHT_HAND:true, NULL_UUID:true, AVATAR_SELF_ID:true, FORBIDDEN_GRAB_TYPES:true, HAPTIC_PULSE_STRENGTH:true, HAPTIC_PULSE_DURATION:true, ZERO_VEC:true, ONE_VEC:true, DEFAULT_REGISTRATION_POINT:true, INCHES_TO_METERS:true, @@ -40,7 +40,12 @@ entityHasActions:true, ensureDynamic:true, findGroupParent:true, - BUMPER_ON_VALUE:true + BUMPER_ON_VALUE:true, + findHandChildEntities:true, + TEAR_AWAY_DISTANCE:true, + TEAR_AWAY_COUNT:true, + TEAR_AWAY_CHECK_TIME:true, + distanceBetweenPointAndEntityBoundingBox:true */ MSECS_PER_SEC = 1000.0; @@ -79,6 +84,10 @@ COLORS_GRAB_DISTANCE_HOLD = { red: 238, green: 75, blue: 214 }; NEAR_GRAB_RADIUS = 1.0; +TEAR_AWAY_DISTANCE = 0.1; // ungrab an entity if its bounding-box moves this far from the hand +TEAR_AWAY_COUNT = 2; // multiply by TEAR_AWAY_CHECK_TIME to know how long the item must be away +TEAR_AWAY_CHECK_TIME = 0.15; // seconds, duration between checks + DISPATCHER_PROPERTIES = [ "position", "registrationPoint", @@ -193,17 +202,6 @@ entityIsDistanceGrabbable = function(props) { return false; } - // XXX - // var distance = Vec3.distance(props.position, handPosition); - // this.otherGrabbingUUID = entityIsGrabbedByOther(entityID); - // if (this.otherGrabbingUUID !== null) { - // // 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; }; @@ -296,7 +294,7 @@ ensureDynamic = function (entityID) { }; findGroupParent = function (controllerData, targetProps) { - while (targetProps.parentID && targetProps.parentID !== NULL_UUID) { + while (targetProps.parentID && targetProps.parentID !== NULL_UUID && targetProps.parentID !== AVATAR_SELF_ID) { // XXX use controllerData.nearbyEntityPropertiesByID ? var parentProps = Entities.getEntityProperties(targetProps.parentID, DISPATCHER_PROPERTIES); if (!parentProps) { @@ -310,6 +308,50 @@ findGroupParent = function (controllerData, targetProps) { return targetProps; }; + +findHandChildEntities = function(hand) { + // find children of avatar's hand joint + var handJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); + children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, handJointIndex)); + + // find children of faux controller joint + var controllerJointIndex = getControllerJointIndex(hand); + children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerJointIndex)); + children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerJointIndex)); + + // find children of faux camera-relative controller joint + var controllerCRJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerCRJointIndex)); + children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerCRJointIndex)); + + return children.filter(function (childID) { + var childType = Entities.getNestableType(childID); + return childType == "entity"; + }); +}; + +distanceBetweenPointAndEntityBoundingBox = function(point, entityProps) { + var entityXform = new Xform(entityProps.rotation, entityProps.position); + var localPoint = entityXform.inv().xformPoint(point); + var minOffset = Vec3.multiplyVbyV(entityProps.registrationPoint, entityProps.dimensions); + var maxOffset = Vec3.multiplyVbyV(Vec3.subtract(ONE_VEC, entityProps.registrationPoint), entityProps.dimensions); + var localMin = Vec3.subtract(entityXform.trans, minOffset); + var localMax = Vec3.sum(entityXform.trans, maxOffset); + + var v = {x: localPoint.x, y: localPoint.y, z: localPoint.z}; + v.x = Math.max(v.x, localMin.x); + v.x = Math.min(v.x, localMax.x); + v.y = Math.max(v.y, localMin.y); + v.y = Math.min(v.y, localMax.y); + v.z = Math.max(v.z, localMin.z); + v.z = Math.min(v.z, localMax.z); + + return Vec3.distance(v, localPoint); +}; + if (typeof module !== 'undefined') { module.exports = { makeDispatcherModuleParameters: makeDispatcherModuleParameters, @@ -318,6 +360,7 @@ if (typeof module !== 'undefined') { makeRunningValues: makeRunningValues, LEFT_HAND: LEFT_HAND, RIGHT_HAND: RIGHT_HAND, - BUMPER_ON_VALUE: BUMPER_ON_VALUE + BUMPER_ON_VALUE: BUMPER_ON_VALUE, + TEAR_AWAY_DISTANCE: TEAR_AWAY_DISTANCE }; }