Merge pull request from sethalves/fix-parent-grab

Fix parent grab
This commit is contained in:
Seth Alves 2017-09-26 13:00:16 -07:00 committed by GitHub
commit a871a6db1d
5 changed files with 226 additions and 65 deletions

View file

@ -10,7 +10,7 @@
/* global Script, Entities, Overlays, Controller, Vec3, Quat, getControllerWorldLocation, RayPick,
controllerDispatcherPlugins:true, controllerDispatcherPluginsNeedSort:true,
LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES,
getGrabPointSphereOffset, HMD, MyAvatar, Messages
getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities
*/
controllerDispatcherPlugins = {};
@ -27,6 +27,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ;
var PROFILE = false;
var DEBUG = true;
if (typeof Test !== "undefined") {
PROFILE = true;
@ -195,7 +196,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);
@ -265,6 +267,20 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
});
}
// sometimes, during a HMD snap-turn, an equipped or held item wont be near
// the hand when the findEntities is done. Gather up any hand-children here.
for (h = LEFT_HAND; h <= RIGHT_HAND; h++) {
var handChildrenIDs = findHandChildEntities(h);
handChildrenIDs.forEach(function (handChildID) {
if (handChildID in nearbyEntityPropertiesByID) {
return;
}
var props = Entities.getEntityProperties(handChildID, DISPATCHER_PROPERTIES);
props.id = handChildID;
nearbyEntityPropertiesByID[handChildID] = props;
});
}
// bundle up all the data about the current situation
var controllerData = {
triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue],
@ -300,6 +316,9 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
// activity-slots which this plugin consumes as "in use"
_this.runningPluginNames[orderedPluginName] = true;
_this.markSlots(candidatePlugin, orderedPluginName);
if (DEBUG) {
print("controllerDispatcher running " + orderedPluginName);
}
}
if (PROFILE) {
Script.endProfileRange("dispatch.isReady." + orderedPluginName);
@ -332,6 +351,9 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
// of running plugins and mark its activity-slots as "not in use"
delete _this.runningPluginNames[runningPluginName];
_this.markSlots(plugin, false);
if (DEBUG) {
print("controllerDispatcher stopping " + runningPluginName);
}
}
if (PROFILE) {
Script.endProfileRange("dispatch.run." + runningPluginName);

View file

@ -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,
@ -498,6 +499,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa
var cloneID = this.cloneHotspot(grabbedProperties, controllerData);
this.targetEntityID = cloneID;
Entities.editEntity(this.targetEntityID, reparentProps);
controllerData.nearbyEntityPropertiesByID[this.targetEntityID] = grabbedProperties;
isClone = true;
} else if (!grabbedProperties.locked) {
Entities.editEntity(this.targetEntityID, reparentProps);
@ -507,8 +509,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 +591,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 +618,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 +645,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 +683,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], []);
};

View file

@ -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;
@ -216,7 +217,8 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
var targetProps = this.getTargetProps(controllerData);
if (targetProps) {
if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
if (controllerData.triggerClicks[this.hand] ||
controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
// switch to grabbing
var targetCloneable = entityIsCloneable(targetProps);
if (targetCloneable) {

View file

@ -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,11 +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.thisHandIsParent = function(props) {
if (!props) {
return false;
}
if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== AVATAR_SELF_ID) {
return false;
}
@ -93,8 +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;
this.previousParentID[targetProps.id] = null;
this.previousParentJointIndex[targetProps.id] = -1;
} else {
this.previousParentID[targetProps.id] = targetProps.parentID;
this.previousParentJointIndex[targetProps.id] = targetProps.parentJointIndex;
@ -111,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];
@ -133,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];
@ -168,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;
@ -188,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, [], []);
}
@ -205,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;
}
@ -218,7 +301,6 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
var worldEntityProps = controllerData.nearbyEntityProperties[this.hand];
var cloneID = cloneEntity(targetProps, worldEntityProps);
var cloneProps = Entities.getEntityProperties(cloneID);
this.grabbing = true;
this.targetEntityID = cloneID;
this.startNearParentingGrabEntity(controllerData, cloneProps);

View file

@ -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,8 +294,9 @@ ensureDynamic = function (entityID) {
};
findGroupParent = function (controllerData, targetProps) {
while (targetProps.parentID && targetProps.parentID !== NULL_UUID) {
// XXX use controllerData.nearbyEntityPropertiesByID ?
while (targetProps.parentID &&
targetProps.parentID !== NULL_UUID &&
Entities.getNestableType(targetProps.parentID) == "entity") {
var parentProps = Entities.getEntityProperties(targetProps.parentID, DISPATCHER_PROPERTIES);
if (!parentProps) {
break;
@ -310,6 +309,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,
@ -319,6 +362,7 @@ if (typeof module !== 'undefined') {
LEFT_HAND: LEFT_HAND,
RIGHT_HAND: RIGHT_HAND,
BUMPER_ON_VALUE: BUMPER_ON_VALUE,
TEAR_AWAY_DISTANCE: TEAR_AWAY_DISTANCE,
propsArePhysical: propsArePhysical,
entityIsGrabbable: entityIsGrabbable,
NEAR_GRAB_RADIUS: NEAR_GRAB_RADIUS,