Merge pull request #14710 from sethalves/mouse-grab-with-traits

case 15962: case 20481: Mouse grab with traits
This commit is contained in:
Shannon Romano 2019-01-18 11:56:41 -08:00 committed by GitHub
commit 11b8ae6d44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 77 additions and 2046 deletions

View file

@ -18,6 +18,8 @@ void GrabManager::simulateGrabs() {
// Update grabbed objects
auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>();
auto entityTree = entityTreeRenderer->getTree();
auto sessionID = DependencyManager::get<NodeList>()->getSessionUUID();
EntityEditPacketSender* packetSender = entityTreeRenderer ? entityTreeRenderer->getPacketSender() : nullptr;
entityTree->withReadLock([&] {
PROFILE_RANGE(simulation, "Grabs");
@ -33,6 +35,8 @@ void GrabManager::simulateGrabs() {
glm::vec3 finalPosition = acc.finalizePosition();
glm::quat finalOrientation = acc.finalizeOrientation();
grabbedThing->setTransform(createMatFromQuatAndPos(finalOrientation, finalPosition));
bool iShouldTellServer = grabbedThing->getEditSenderID() == sessionID;
entityTree->updateEntityQueryAACube(grabbedThing, packetSender, false, iShouldTellServer);
}
}
});

View file

@ -770,7 +770,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
auto lastEdited = lastEditedFromBufferAdjusted;
bool otherOverwrites = overwriteLocalData && !weOwnSimulation;
auto shouldUpdate = [lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) {
auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) {
if (stillHasGrabActions()) {
return false;
}
bool simulationChanged = lastEdited > updatedTimestamp;
return otherOverwrites && simulationChanged && (valueChanged || filterRejection);
};
@ -3349,7 +3352,8 @@ void EntityItem::prepareForSimulationOwnershipBid(EntityItemProperties& properti
}
bool EntityItem::isWearable() const {
return isVisible() && (getParentID() == DependencyManager::get<NodeList>()->getSessionUUID() || getParentID() == AVATAR_SELF_ID);
return isVisible() &&
(getParentID() == DependencyManager::get<NodeList>()->getSessionUUID() || getParentID() == AVATAR_SELF_ID);
}
void EntityItem::addGrab(GrabPointer grab) {
@ -3368,7 +3372,8 @@ void EntityItem::addGrab(GrabPointer grab) {
EntityDynamicType dynamicType;
QVariantMap arguments;
int grabParentJointIndex =grab->getParentJointIndex();
if (grabParentJointIndex == FARGRAB_RIGHTHAND_INDEX || grabParentJointIndex == FARGRAB_LEFTHAND_INDEX) {
if (grabParentJointIndex == FARGRAB_RIGHTHAND_INDEX || grabParentJointIndex == FARGRAB_LEFTHAND_INDEX ||
grabParentJointIndex == FARGRAB_MOUSE_INDEX) {
// add a far-grab action
dynamicType = DYNAMIC_TYPE_FAR_GRAB;
arguments["otherID"] = grab->getOwnerID();

View file

@ -3025,6 +3025,7 @@ void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object,
properties.setLastEdited(now);
packetSender->queueEditEntityMessage(PacketType::EntityEdit, getThisPointer(), entity->getID(), properties);
entity->setLastEdited(now); // so we ignore the echo from the server
entity->setLastBroadcast(now); // for debug/physics status icons
}

View file

@ -6,11 +6,11 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print,
getControllerJointIndex, enableDispatcherModule, disableDispatcherModule, entityIsFarGrabbedByOther,
Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions,
Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, entityIsCloneable,
cloneEntity, DISPATCHER_PROPERTIES, Uuid, unhighlightTargetEntity, isInEditMode, getGrabbableData
/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print, getControllerJointIndex,
enableDispatcherModule, disableDispatcherModule, entityIsFarGrabbedByOther, Messages, makeDispatcherModuleParameters,
makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic,
entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, unhighlightTargetEntity, isInEditMode, getGrabbableData,
entityIsEquippable
*/
Script.include("/~/system/libraries/Xform.js");
@ -767,7 +767,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa
var entityProperties = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
entityProperties.id = entityID;
var hasEquipData = getWearableData(entityProperties);
if (hasEquipData && entityProperties.parentID === EMPTY_PARENT_ID && !entityIsFarGrabbedByOther(entityID)) {
if (hasEquipData && entityIsEquippable(entityProperties)) {
entityProperties.id = entityID;
var rightHandPosition = MyAvatar.getJointPosition("RightHand");
var leftHandPosition = MyAvatar.getJointPosition("LeftHand");

View file

@ -1,572 +0,0 @@
"use strict";
// farActionGrabEntity.js
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* jslint bitwise: true */
/* global Script, Controller, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat, getEnabledModuleByName,
makeRunningValues, Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable,
makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE,
TRIGGER_ON_VALUE, ZERO_VEC, getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD,
Picks, makeLaserLockInfo, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST,
Uuid, worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
var MARGIN = 25;
function TargetObject(entityID, entityProps) {
this.entityID = entityID;
this.entityProps = entityProps;
this.targetEntityID = null;
this.targetEntityProps = null;
this.getTargetEntity = function() {
var parentPropsLength = this.parentProps.length;
if (parentPropsLength !== 0) {
var targetEntity = {
id: this.parentProps[parentPropsLength - 1].id,
props: this.parentProps[parentPropsLength - 1]};
this.targetEntityID = targetEntity.id;
this.targetEntityProps = targetEntity.props;
return targetEntity;
}
this.targetEntityID = this.entityID;
this.targetEntityProps = this.entityProps;
return {
id: this.entityID,
props: this.entityProps};
};
}
function FarActionGrabEntity(hand) {
this.hand = hand;
this.grabbing = false;
this.grabbedThingID = null;
this.targetObject = null;
this.actionID = null; // action this script created...
this.entityToLockOnto = null;
this.potentialEntityWithContextOverlay = false;
this.entityWithContextOverlay = false;
this.contextOverlayTimer = false;
this.locked = false;
this.highlightedEntity = null;
this.reticleMinX = MARGIN;
this.reticleMaxX = 0;
this.reticleMinY = MARGIN;
this.reticleMaxY = 0;
var ACTION_TTL = 15; // seconds
var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object
var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position
var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified
var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified
this.parameters = makeDispatcherModuleParameters(
550,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100,
makeLaserParams(this.hand, false));
this.handToController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
};
this.distanceGrabTimescale = function(mass, distance) {
var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass /
DISTANCE_HOLDING_UNITY_MASS * distance /
DISTANCE_HOLDING_UNITY_DISTANCE;
if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) {
timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
}
return timeScale;
};
this.getMass = function(dimensions, density) {
return (dimensions.x * dimensions.y * dimensions.z) * density;
};
this.startFarGrabAction = function (controllerData, grabbedProperties) {
var controllerLocation = controllerData.controllerLocations[this.hand];
var worldControllerPosition = controllerLocation.position;
var worldControllerRotation = controllerLocation.orientation;
// transform the position into room space
var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
var now = Date.now();
// add the action and initialize some variables
this.currentObjectPosition = grabbedProperties.position;
this.currentObjectRotation = grabbedProperties.rotation;
this.currentObjectTime = now;
this.currentCameraOrientation = Camera.orientation;
this.grabRadius = this.grabbedDistance;
this.grabRadialVelocity = 0.0;
// offset between controller vector at the grab radius and the entity position
var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
targetPosition = Vec3.sum(targetPosition, worldControllerPosition);
this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition);
// compute a constant based on the initial conditions which we use below to exaggerate hand motion
// onto the held object
this.radiusScalar = Math.log(this.grabRadius + 1.0);
if (this.radiusScalar < 1.0) {
this.radiusScalar = 1.0;
}
// compute the mass for the purpose of energy and how quickly to move object
this.mass = this.getMass(grabbedProperties.dimensions, grabbedProperties.density);
var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, grabbedProperties.position));
var timeScale = this.distanceGrabTimescale(this.mass, distanceToObject);
this.linearTimeScale = timeScale;
this.actionID = Entities.addAction("far-grab", this.grabbedThingID, {
targetPosition: this.currentObjectPosition,
linearTimeScale: timeScale,
targetRotation: this.currentObjectRotation,
angularTimeScale: timeScale,
tag: "far-grab-" + MyAvatar.sessionUUID,
ttl: ACTION_TTL
});
if (this.actionID === Uuid.NULL) {
this.actionID = null;
}
if (this.actionID !== null) {
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.grabbedThingID, "startDistanceGrab", args);
}
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
this.previousRoomControllerPosition = roomControllerPosition;
this.grabbing = true;
};
this.continueDistanceHolding = function(controllerData) {
var controllerLocation = controllerData.controllerLocations[this.hand];
var worldControllerPosition = controllerLocation.position;
var worldControllerRotation = controllerLocation.orientation;
// also transform the position into room space
var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES);
var now = Date.now();
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.
var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) *
this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR;
if (radius < 1.0) {
radius = 1.0;
}
var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition);
var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta);
var handMoved = Vec3.multiply(worldHandDelta, radius);
this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.grabbedThingID, "continueDistanceGrab", args);
// Update radialVelocity
var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime);
var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition));
var newRadialVelocity = Vec3.dot(lastVelocity, delta);
var VELOCITY_AVERAGING_TIME = 0.016;
var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME;
if (blendFactor < 0.0) {
blendFactor = 0.0;
} else if (blendFactor > 1.0) {
blendFactor = 1.0;
}
this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity;
var RADIAL_GRAB_AMPLIFIER = 10.0;
if (Math.abs(this.grabRadialVelocity) > 0.0) {
this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime *
this.grabRadius * RADIAL_GRAB_AMPLIFIER);
}
// don't let grabRadius go all the way to zero, because it can't come back from that
var MINIMUM_GRAB_RADIUS = 0.1;
if (this.grabRadius < MINIMUM_GRAB_RADIUS) {
this.grabRadius = MINIMUM_GRAB_RADIUS;
}
var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition);
newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition);
// XXX
// this.maybeScale(grabbedProperties);
var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition));
this.linearTimeScale = (this.linearTimeScale / 2);
if (this.linearTimeScale <= DISTANCE_HOLDING_ACTION_TIMEFRAME) {
this.linearTimeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
}
var success = Entities.updateAction(this.grabbedThingID, this.actionID, {
targetPosition: newTargetPosition,
linearTimeScale: this.linearTimeScale,
targetRotation: this.currentObjectRotation,
angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
ttl: ACTION_TTL
});
if (!success) {
print("farActionGrabEntity continueDistanceHolding -- updateAction failed: " + this.actionID);
this.actionID = null;
}
this.previousRoomControllerPosition = roomControllerPosition;
};
this.endFarGrabAction = function () {
this.distanceHolding = false;
this.distanceRotating = false;
Entities.deleteAction(this.grabbedThingID, this.actionID);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.grabbedThingID, "releaseGrab", args);
this.actionID = null;
this.grabbedThingID = null;
this.targetObject = null;
this.potentialEntityWithContextOverlay = false;
this.grabbing = false;
};
this.updateRecommendedArea = function() {
var dims = Controller.getViewportDimensions();
this.reticleMaxX = dims.x - MARGIN;
this.reticleMaxY = dims.y - MARGIN;
};
this.calculateNewReticlePosition = function(intersection) {
this.updateRecommendedArea();
var point2d = HMD.overlayFromWorldPoint(intersection);
point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX));
point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY));
return point2d;
};
this.notPointingAtEntity = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
var entityType = entityProperty.type;
var hudRayPick = controllerData.hudRayPicks[this.hand];
var point2d = this.calculateNewReticlePosition(hudRayPick.intersection);
if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") ||
intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) {
return true;
}
return false;
};
this.distanceRotate = function(otherFarGrabModule) {
this.distanceRotating = true;
this.distanceHolding = false;
var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation;
var controllerRotationDelta =
Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation));
// Rotate entity by twice the delta rotation.
controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta);
// Perform the rotation in the translation controller's action update.
otherFarGrabModule.currentObjectRotation = Quat.multiply(controllerRotationDelta,
otherFarGrabModule.currentObjectRotation);
this.previousWorldControllerRotation = worldControllerRotation;
};
this.prepareDistanceRotatingData = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
var controllerLocation = getControllerWorldLocation(this.handToController(), true);
var worldControllerPosition = controllerLocation.position;
var worldControllerRotation = controllerLocation.orientation;
var grabbedProperties = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
this.currentObjectPosition = grabbedProperties.position;
this.grabRadius = intersection.distance;
// Offset between controller vector at the grab radius and the entity position.
var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
targetPosition = Vec3.sum(targetPosition, worldControllerPosition);
this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition);
// Initial controller rotation.
this.previousWorldControllerRotation = worldControllerRotation;
};
this.destroyContextOverlay = function(controllerData) {
if (this.entityWithContextOverlay) {
ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay);
this.entityWithContextOverlay = false;
this.potentialEntityWithContextOverlay = false;
}
};
this.targetIsNull = function() {
var properties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES);
if (Object.keys(properties).length === 0 && this.distanceHolding) {
return true;
}
return false;
};
this.getTargetProps = function (controllerData) {
var targetEntityID = controllerData.rayPicks[this.hand].objectID;
if (targetEntityID) {
return Entities.getEntityProperties(targetEntityID, DISPATCHER_PROPERTIES);
}
return null;
};
this.isReady = function (controllerData) {
if (HMD.active) {
if (this.notPointingAtEntity(controllerData)) {
return makeRunningValues(false, [], []);
}
this.distanceHolding = false;
this.distanceRotating = false;
if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
this.prepareDistanceRotatingData(controllerData);
return makeRunningValues(true, [], []);
} else {
this.destroyContextOverlay();
return makeRunningValues(false, [], []);
}
}
return makeRunningValues(false, [], []);
};
this.run = function (controllerData) {
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.targetIsNull()) {
this.endFarGrabAction();
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity",
this.highlightedEntity);
this.highlightedEntity = null;
return makeRunningValues(false, [], []);
}
this.intersectionDistance = controllerData.rayPicks[this.hand].distance;
var otherModuleName = this.hand === RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity";
var otherFarGrabModule = getEnabledModuleByName(otherModuleName);
// gather up the readiness of the near-grab modules
var nearGrabNames = [
this.hand === RIGHT_HAND ? "RightScaleAvatar" : "LeftScaleAvatar",
this.hand === RIGHT_HAND ? "RightFarTriggerEntity" : "LeftFarTriggerEntity",
this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity",
this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity"
];
if (!this.grabbing) {
nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay");
nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight");
}
var nearGrabReadiness = [];
for (var i = 0; i < nearGrabNames.length; i++) {
var nearGrabModule = getEnabledModuleByName(nearGrabNames[i]);
var ready = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []);
nearGrabReadiness.push(ready);
}
if (this.actionID) {
// if we are doing a distance grab and the object or tablet gets close enough to the controller,
// stop the far-grab so the near-grab or equip can take over.
for (var k = 0; k < nearGrabReadiness.length; k++) {
if (nearGrabReadiness[k].active && (nearGrabReadiness[k].targets[0] === this.grabbedThingID ||
HMD.tabletID && nearGrabReadiness[k].targets[0] === HMD.tabletID)) {
this.endFarGrabAction();
return makeRunningValues(false, [], []);
}
}
this.continueDistanceHolding(controllerData);
} else {
// if we are doing a distance search and this controller moves into a position
// where it could near-grab something, stop searching.
for (var j = 0; j < nearGrabReadiness.length; j++) {
if (nearGrabReadiness[j].active) {
this.endFarGrabAction();
return makeRunningValues(false, [], []);
}
}
var rayPickInfo = controllerData.rayPicks[this.hand];
if (rayPickInfo.type === Picks.INTERSECTED_ENTITY) {
if (controllerData.triggerClicks[this.hand]) {
var entityID = rayPickInfo.objectID;
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity",
this.highlightedEntity);
this.highlightedEntity = null;
var targetProps = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
if (targetProps.href !== "") {
AddressManager.handleLookupString(targetProps.href);
return makeRunningValues(false, [], []);
}
this.targetObject = new TargetObject(entityID, targetProps);
this.targetObject.parentProps = getEntityParents(targetProps);
if (this.contextOverlayTimer) {
Script.clearTimeout(this.contextOverlayTimer);
}
this.contextOverlayTimer = false;
if (entityID === this.entityWithContextOverlay) {
this.destroyContextOverlay();
} else {
Selection.removeFromSelectedItemsList("contextOverlayHighlightList", "entity", entityID);
}
var targetEntity = this.targetObject.getTargetEntity();
entityID = targetEntity.id;
targetProps = targetEntity.props;
if (!targetProps.dynamic && !this.targetObject.entityProps.dynamic) {
// let farParentGrabEntity handle it
return makeRunningValues(false, [], []);
}
if (entityIsGrabbable(targetProps) || entityIsGrabbable(this.targetObject.entityProps)) {
if (!this.distanceRotating) {
this.grabbedThingID = entityID;
this.grabbedDistance = rayPickInfo.distance;
}
if (otherFarGrabModule.grabbedThingID === this.grabbedThingID &&
otherFarGrabModule.distanceHolding) {
this.prepareDistanceRotatingData(controllerData);
this.distanceRotate(otherFarGrabModule);
} else {
this.distanceHolding = true;
this.distanceRotating = false;
this.startFarGrabAction(controllerData, targetProps);
}
}
} else {
var targetEntityID = rayPickInfo.objectID;
if (this.highlightedEntity !== targetEntityID) {
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity",
this.highlightedEntity);
var selectionTargetProps = Entities.getEntityProperties(targetEntityID, DISPATCHER_PROPERTIES);
var selectionTargetObject = new TargetObject(targetEntityID, selectionTargetProps);
selectionTargetObject.parentProps = getEntityParents(selectionTargetProps);
var selectionTargetEntity = selectionTargetObject.getTargetEntity();
if (entityIsGrabbable(selectionTargetEntity.props) ||
entityIsGrabbable(selectionTargetObject.entityProps)) {
Selection.addToSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", rayPickInfo.objectID);
}
this.highlightedEntity = rayPickInfo.objectID;
}
if (!this.entityWithContextOverlay) {
var _this = this;
if (_this.potentialEntityWithContextOverlay !== rayPickInfo.objectID) {
if (_this.contextOverlayTimer) {
Script.clearTimeout(_this.contextOverlayTimer);
}
_this.contextOverlayTimer = false;
_this.potentialEntityWithContextOverlay = rayPickInfo.objectID;
}
if (!_this.contextOverlayTimer) {
_this.contextOverlayTimer = Script.setTimeout(function () {
if (!_this.entityWithContextOverlay &&
_this.contextOverlayTimer &&
_this.potentialEntityWithContextOverlay === rayPickInfo.objectID) {
var pEvProps = Entities.getEntityProperties(rayPickInfo.objectID,
DISPATCHER_PROPERTIES);
var pointerEvent = {
type: "Move",
id: _this.hand + 1, // 0 is reserved for hardware mouse
pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID,
rayPickInfo.intersection, pEvProps),
pos3D: rayPickInfo.intersection,
normal: rayPickInfo.surfaceNormal,
direction: Vec3.subtract(ZERO_VEC, rayPickInfo.surfaceNormal),
button: "Secondary"
};
if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.objectID, pointerEvent)) {
_this.entityWithContextOverlay = rayPickInfo.objectID;
}
}
_this.contextOverlayTimer = false;
}, 500);
}
}
}
} else if (this.distanceRotating) {
this.distanceRotate(otherFarGrabModule);
} else if (this.highlightedEntity) {
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
this.highlightedEntity = null;
}
}
return this.exitIfDisabled(controllerData);
};
this.exitIfDisabled = function(controllerData) {
var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules";
var disableModule = getEnabledModuleByName(moduleName);
if (disableModule) {
if (disableModule.disableModules) {
this.endFarGrabAction();
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity",
this.highlightedEntity);
this.highlightedEntity = null;
return makeRunningValues(false, [], []);
}
}
var grabbedThing = (this.distanceHolding || this.distanceRotating) ? this.targetObject.entityID : null;
var offset = this.calculateOffset(controllerData);
var laserLockInfo = makeLaserLockInfo(grabbedThing, false, this.hand, offset);
return makeRunningValues(true, [], [], laserLockInfo);
};
this.calculateOffset = function(controllerData) {
if (this.distanceHolding || this.distanceRotating) {
var targetProps = Entities.getEntityProperties(this.targetObject.entityID,
[ "position", "rotation", "registrationPoint", "dimensions" ]);
return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection);
}
return undefined;
};
}
var leftFarActionGrabEntity = new FarActionGrabEntity(LEFT_HAND);
var rightFarActionGrabEntity = new FarActionGrabEntity(RIGHT_HAND);
enableDispatcherModule("LeftFarActionGrabEntity", leftFarActionGrabEntity);
enableDispatcherModule("RightFarActionGrabEntity", rightFarActionGrabEntity);
function cleanup() {
disableDispatcherModule("LeftFarActionGrabEntity");
disableDispatcherModule("RightFarActionGrabEntity");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -1,664 +0,0 @@
"use strict";
// farParentGrabEntity.js
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* jslint bitwise: true */
/* global Script, Controller, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Quat, getEnabledModuleByName, makeRunningValues,
Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable, makeDispatcherModuleParameters, MSECS_PER_SEC,
HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, getControllerWorldLocation,
projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager,
getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, Uuid, findGroupParent,
worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, findFarGrabJointChildEntities
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
var MARGIN = 25;
function TargetObject(entityID, entityProps) {
this.entityID = entityID;
this.entityProps = entityProps;
this.targetEntityID = null;
this.targetEntityProps = null;
this.getTargetEntity = function() {
var parentPropsLength = this.parentProps.length;
if (parentPropsLength !== 0) {
var targetEntity = {
id: this.parentProps[parentPropsLength - 1].id,
props: this.parentProps[parentPropsLength - 1]};
this.targetEntityID = targetEntity.id;
this.targetEntityProps = targetEntity.props;
return targetEntity;
}
this.targetEntityID = this.entityID;
this.targetEntityProps = this.entityProps;
return {
id: this.entityID,
props: this.entityProps};
};
}
function FarParentGrabEntity(hand) {
this.hand = hand;
this.grabbing = false;
this.targetEntityID = null;
this.targetObject = null;
this.previouslyUnhooked = {};
this.previousParentID = {};
this.previousParentJointIndex = {};
this.potentialEntityWithContextOverlay = false;
this.entityWithContextOverlay = false;
this.contextOverlayTimer = false;
this.highlightedEntity = null;
this.reticleMinX = MARGIN;
this.reticleMaxX = 0;
this.reticleMinY = MARGIN;
this.reticleMaxY = 0;
this.lastUnexpectedChildrenCheckTime = 0;
this.endedGrab = 0;
this.MIN_HAPTIC_PULSE_INTERVAL = 500; // ms
var FAR_GRAB_JOINTS = [65527, 65528]; // FARGRAB_LEFTHAND_INDEX, FARGRAB_RIGHTHAND_INDEX
var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object
var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position
var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified
var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified
this.parameters = makeDispatcherModuleParameters(
540,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100,
makeLaserParams(this.hand, false));
this.handToController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
};
this.distanceGrabTimescale = function(mass, distance) {
var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass /
DISTANCE_HOLDING_UNITY_MASS * distance /
DISTANCE_HOLDING_UNITY_DISTANCE;
if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) {
timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
}
return timeScale;
};
this.getMass = function(dimensions, density) {
return (dimensions.x * dimensions.y * dimensions.z) * density;
};
this.thisFarGrabJointIsParent = function(isParentProps) {
if (!isParentProps) {
return false;
}
if (isParentProps.parentID !== MyAvatar.sessionUUID && isParentProps.parentID !== MyAvatar.SELF_ID) {
return false;
}
if (isParentProps.parentJointIndex === FAR_GRAB_JOINTS[this.hand]) {
return true;
}
return false;
};
this.startFarParentGrab = function (controllerData, grabbedProperties) {
var controllerLocation = controllerData.controllerLocations[this.hand];
var worldControllerPosition = controllerLocation.position;
var worldControllerRotation = controllerLocation.orientation;
// transform the position into room space
var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
var now = Date.now();
// add the action and initialize some variables
this.currentObjectPosition = grabbedProperties.position;
this.currentObjectRotation = grabbedProperties.rotation;
this.currentObjectTime = now;
this.grabRadius = this.grabbedDistance;
this.grabRadialVelocity = 0.0;
// offset between controller vector at the grab radius and the entity position
var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
targetPosition = Vec3.sum(targetPosition, worldControllerPosition);
this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition);
// compute a constant based on the initial conditions which we use below to exaggerate hand motion
// onto the held object
this.radiusScalar = Math.log(this.grabRadius + 1.0);
if (this.radiusScalar < 1.0) {
this.radiusScalar = 1.0;
}
// compute the mass for the purpose of energy and how quickly to move object
this.mass = this.getMass(grabbedProperties.dimensions, grabbedProperties.density);
// Debounce haptic pules. Can occur as near grab controller module vacillates between being ready or not due to
// changing positions and floating point rounding.
if (Date.now() - this.endedGrab > this.MIN_HAPTIC_PULSE_INTERVAL) {
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
}
unhighlightTargetEntity(this.targetEntityID);
var message = {
hand: this.hand,
entityID: this.targetEntityID
};
Messages.sendLocalMessage('Hifi-unhighlight-entity', JSON.stringify(message));
var newTargetPosLocal = MyAvatar.worldToJointPoint(grabbedProperties.position);
MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(grabbedProperties.id, "startNearGrab", args);
var reparentProps = {
parentID: MyAvatar.SELF_ID,
parentJointIndex: FAR_GRAB_JOINTS[this.hand],
localVelocity: {x: 0, y: 0, z: 0},
localAngularVelocity: {x: 0, y: 0, z: 0}
};
if (this.thisFarGrabJointIsParent(grabbedProperties)) {
// this should never happen, but if it does, don't set previous parent to be this hand.
this.previousParentID[grabbedProperties.id] = null;
this.previousParentJointIndex[grabbedProperties.id] = -1;
} else {
this.previousParentID[grabbedProperties.id] = grabbedProperties.parentID;
this.previousParentJointIndex[grabbedProperties.id] = grabbedProperties.parentJointIndex;
}
this.targetEntityID = grabbedProperties.id;
Entities.editEntity(grabbedProperties.id, reparentProps);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'grab',
grabbedEntity: grabbedProperties.id,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
this.grabbing = true;
this.previousRoomControllerPosition = roomControllerPosition;
};
this.continueDistanceHolding = function(controllerData) {
var controllerLocation = controllerData.controllerLocations[this.hand];
var worldControllerPosition = controllerLocation.position;
var worldControllerRotation = controllerLocation.orientation;
// also transform the position into room space
var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
var grabbedProperties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
var now = Date.now();
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.
var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) *
this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR;
if (radius < 1.0) {
radius = 1.0;
}
var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition);
var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta);
var handMoved = Vec3.multiply(worldHandDelta, radius);
this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "continueDistanceGrab", args);
// Update radialVelocity
var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime);
var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition));
var newRadialVelocity = Vec3.dot(lastVelocity, delta);
var VELOCITY_AVERAGING_TIME = 0.016;
var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME;
if (blendFactor < 0.0) {
blendFactor = 0.0;
} else if (blendFactor > 1.0) {
blendFactor = 1.0;
}
this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity;
var RADIAL_GRAB_AMPLIFIER = 10.0;
if (Math.abs(this.grabRadialVelocity) > 0.0) {
this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime *
this.grabRadius * RADIAL_GRAB_AMPLIFIER);
}
// don't let grabRadius go all the way to zero, because it can't come back from that
var MINIMUM_GRAB_RADIUS = 0.1;
if (this.grabRadius < MINIMUM_GRAB_RADIUS) {
this.grabRadius = MINIMUM_GRAB_RADIUS;
}
var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition);
newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition);
// MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], MyAvatar.worldToJointPoint(newTargetPosition));
// var newTargetPosLocal = Mat4.transformPoint(MyAvatar.getSensorToWorldMatrix(), newTargetPosition);
var newTargetPosLocal = MyAvatar.worldToJointPoint(newTargetPosition);
MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
this.previousRoomControllerPosition = roomControllerPosition;
};
this.endFarParentGrab = function (controllerData) {
this.endedGrab = Date.now();
// var endProps = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
var endProps = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
if (this.thisFarGrabJointIsParent(endProps)) {
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];
Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'release',
grabbedEntity: this.targetEntityID,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
unhighlightTargetEntity(this.targetEntityID);
this.grabbing = false;
this.targetEntityID = null;
this.potentialEntityWithContextOverlay = false;
MyAvatar.clearJointData(FAR_GRAB_JOINTS[this.hand]);
};
this.updateRecommendedArea = function() {
var dims = Controller.getViewportDimensions();
this.reticleMaxX = dims.x - MARGIN;
this.reticleMaxY = dims.y - MARGIN;
};
this.calculateNewReticlePosition = function(intersection) {
this.updateRecommendedArea();
var point2d = HMD.overlayFromWorldPoint(intersection);
point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX));
point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY));
return point2d;
};
this.notPointingAtEntity = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
var entityType = entityProperty.type;
var hudRayPick = controllerData.hudRayPicks[this.hand];
var point2d = this.calculateNewReticlePosition(hudRayPick.intersection);
if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") ||
intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) {
return true;
}
return false;
};
this.distanceRotate = function(otherFarGrabModule) {
this.distanceRotating = true;
this.distanceHolding = false;
var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation;
var controllerRotationDelta =
Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation));
// Rotate entity by twice the delta rotation.
controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta);
// Perform the rotation in the translation controller's action update.
otherFarGrabModule.currentObjectRotation = Quat.multiply(controllerRotationDelta,
otherFarGrabModule.currentObjectRotation);
this.previousWorldControllerRotation = worldControllerRotation;
};
this.prepareDistanceRotatingData = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
var controllerLocation = getControllerWorldLocation(this.handToController(), true);
var worldControllerPosition = controllerLocation.position;
var worldControllerRotation = controllerLocation.orientation;
var grabbedProperties = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
this.currentObjectPosition = grabbedProperties.position;
this.grabRadius = intersection.distance;
// Offset between controller vector at the grab radius and the entity position.
var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
targetPosition = Vec3.sum(targetPosition, worldControllerPosition);
this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition);
// Initial controller rotation.
this.previousWorldControllerRotation = worldControllerRotation;
};
this.destroyContextOverlay = function(controllerData) {
if (this.entityWithContextOverlay) {
ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay);
this.entityWithContextOverlay = false;
this.potentialEntityWithContextOverlay = 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 = findFarGrabJointChildEntities(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 parentage 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 = Uuid.NULL;
previousParentJointIndex = -1;
}
}
_this.previouslyUnhooked[childID] = now;
Entities.editEntity(childID, {
parentID: previousParentID,
parentJointIndex: previousParentJointIndex
});
} else {
Entities.editEntity(childID, { parentID: Uuid.NULL });
}
});
}
};
this.targetIsNull = function() {
var properties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
if (Object.keys(properties).length === 0 && this.distanceHolding) {
return true;
}
return false;
};
this.getTargetProps = function (controllerData) {
var targetEntity = controllerData.rayPicks[this.hand].objectID;
if (targetEntity) {
var gtProps = Entities.getEntityProperties(targetEntity, DISPATCHER_PROPERTIES);
if (entityIsGrabbable(gtProps)) {
// if we've attempted to grab a child, roll up to the root of the tree
var groupRootProps = findGroupParent(controllerData, gtProps);
if (entityIsGrabbable(groupRootProps)) {
return groupRootProps;
}
return gtProps;
}
}
return null;
};
this.isReady = function (controllerData) {
if (HMD.active) {
if (this.notPointingAtEntity(controllerData)) {
return makeRunningValues(false, [], []);
}
this.distanceHolding = false;
this.distanceRotating = false;
if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
var targetProps = this.getTargetProps(controllerData);
if (targetProps && (targetProps.dynamic && targetProps.parentID === Uuid.NULL)) {
return makeRunningValues(false, [], []); // let farActionGrabEntity handle it
} else {
this.prepareDistanceRotatingData(controllerData);
return makeRunningValues(true, [], []);
}
} else {
this.checkForUnexpectedChildren(controllerData);
this.destroyContextOverlay();
return makeRunningValues(false, [], []);
}
}
return makeRunningValues(false, [], []);
};
this.run = function (controllerData) {
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.targetIsNull()) {
this.endFarParentGrab(controllerData);
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
this.highlightedEntity = null;
return makeRunningValues(false, [], []);
}
this.intersectionDistance = controllerData.rayPicks[this.hand].distance;
var otherModuleName = this.hand === RIGHT_HAND ? "LeftFarParentGrabEntity" : "RightFarParentGrabEntity";
var otherFarGrabModule = getEnabledModuleByName(otherModuleName);
// gather up the readiness of the near-grab modules
var nearGrabNames = [
this.hand === RIGHT_HAND ? "RightScaleAvatar" : "LeftScaleAvatar",
this.hand === RIGHT_HAND ? "RightFarTriggerEntity" : "LeftFarTriggerEntity",
this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity",
this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity"
];
if (!this.grabbing) {
nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay");
nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight");
}
var nearGrabReadiness = [];
for (var i = 0; i < nearGrabNames.length; i++) {
var nearGrabModule = getEnabledModuleByName(nearGrabNames[i]);
var ready = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []);
nearGrabReadiness.push(ready);
}
if (this.targetEntityID) {
// if we are doing a distance grab and the object gets close enough to the controller,
// stop the far-grab so the near-grab or equip can take over.
for (var k = 0; k < nearGrabReadiness.length; k++) {
if (nearGrabReadiness[k].active && (nearGrabReadiness[k].targets[0] === this.targetEntityID)) {
this.endFarParentGrab(controllerData);
return makeRunningValues(false, [], []);
}
}
this.continueDistanceHolding(controllerData);
} else {
// if we are doing a distance search and this controller moves into a position
// where it could near-grab something, stop searching.
for (var j = 0; j < nearGrabReadiness.length; j++) {
if (nearGrabReadiness[j].active) {
this.endFarParentGrab(controllerData);
return makeRunningValues(false, [], []);
}
}
var rayPickInfo = controllerData.rayPicks[this.hand];
if (rayPickInfo.type === Picks.INTERSECTED_ENTITY) {
if (controllerData.triggerClicks[this.hand]) {
var entityID = rayPickInfo.objectID;
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
this.highlightedEntity = null;
var targetProps = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
if (targetProps.href !== "") {
AddressManager.handleLookupString(targetProps.href);
return makeRunningValues(false, [], []);
}
this.targetObject = new TargetObject(entityID, targetProps);
this.targetObject.parentProps = getEntityParents(targetProps);
if (this.contextOverlayTimer) {
Script.clearTimeout(this.contextOverlayTimer);
}
this.contextOverlayTimer = false;
if (entityID === this.entityWithContextOverlay) {
this.destroyContextOverlay();
} else {
Selection.removeFromSelectedItemsList("contextOverlayHighlightList", "entity", entityID);
}
var targetEntity = this.targetObject.getTargetEntity();
entityID = targetEntity.id;
targetProps = targetEntity.props;
if (targetProps.dynamic || this.targetObject.entityProps.dynamic) {
// let farActionGrabEntity handle it
return makeRunningValues(false, [], []);
}
if (entityIsGrabbable(targetProps) || entityIsGrabbable(this.targetObject.entityProps)) {
if (!this.distanceRotating) {
this.targetEntityID = entityID;
this.grabbedDistance = rayPickInfo.distance;
}
if (otherFarGrabModule.targetEntityID === this.targetEntityID &&
otherFarGrabModule.distanceHolding) {
this.prepareDistanceRotatingData(controllerData);
this.distanceRotate(otherFarGrabModule);
} else {
this.distanceHolding = true;
this.distanceRotating = false;
this.startFarParentGrab(controllerData, targetProps);
}
}
} else {
var targetEntityID = rayPickInfo.objectID;
if (this.highlightedEntity !== targetEntityID) {
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
var selectionTargetProps = Entities.getEntityProperties(targetEntityID, DISPATCHER_PROPERTIES);
var selectionTargetObject = new TargetObject(targetEntityID, selectionTargetProps);
selectionTargetObject.parentProps = getEntityParents(selectionTargetProps);
var selectionTargetEntity = selectionTargetObject.getTargetEntity();
if (entityIsGrabbable(selectionTargetEntity.props) ||
entityIsGrabbable(selectionTargetObject.entityProps)) {
Selection.addToSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", rayPickInfo.objectID);
}
this.highlightedEntity = rayPickInfo.objectID;
}
if (!this.entityWithContextOverlay) {
var _this = this;
if (_this.potentialEntityWithContextOverlay !== rayPickInfo.objectID) {
if (_this.contextOverlayTimer) {
Script.clearTimeout(_this.contextOverlayTimer);
}
_this.contextOverlayTimer = false;
_this.potentialEntityWithContextOverlay = rayPickInfo.objectID;
}
if (!_this.contextOverlayTimer) {
_this.contextOverlayTimer = Script.setTimeout(function () {
if (!_this.entityWithContextOverlay &&
_this.contextOverlayTimer &&
_this.potentialEntityWithContextOverlay === rayPickInfo.objectID) {
var cotProps = Entities.getEntityProperties(rayPickInfo.objectID,
DISPATCHER_PROPERTIES);
var pointerEvent = {
type: "Move",
id: _this.hand + 1, // 0 is reserved for hardware mouse
pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID,
rayPickInfo.intersection, cotProps),
pos3D: rayPickInfo.intersection,
normal: rayPickInfo.surfaceNormal,
direction: Vec3.subtract(ZERO_VEC, rayPickInfo.surfaceNormal),
button: "Secondary"
};
if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.objectID, pointerEvent)) {
_this.entityWithContextOverlay = rayPickInfo.objectID;
}
}
_this.contextOverlayTimer = false;
}, 500);
}
}
}
} else if (this.distanceRotating) {
this.distanceRotate(otherFarGrabModule);
} else if (this.highlightedEntity) {
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
this.highlightedEntity = null;
}
}
return this.exitIfDisabled(controllerData);
};
this.exitIfDisabled = function(controllerData) {
var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules";
var disableModule = getEnabledModuleByName(moduleName);
if (disableModule) {
if (disableModule.disableModules) {
this.endFarParentGrab(controllerData);
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
this.highlightedEntity = null;
return makeRunningValues(false, [], []);
}
}
var grabbedThing = (this.distanceHolding || this.distanceRotating) ? this.targetObject.entityID : null;
var offset = this.calculateOffset(controllerData);
var laserLockInfo = makeLaserLockInfo(grabbedThing, false, this.hand, offset);
return makeRunningValues(true, [], [], laserLockInfo);
};
this.calculateOffset = function(controllerData) {
if (this.distanceHolding || this.distanceRotating) {
var targetProps = Entities.getEntityProperties(this.targetObject.entityID,
[ "position", "rotation", "registrationPoint", "dimensions" ]);
return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection);
}
return undefined;
};
}
var leftFarParentGrabEntity = new FarParentGrabEntity(LEFT_HAND);
var rightFarParentGrabEntity = new FarParentGrabEntity(RIGHT_HAND);
enableDispatcherModule("LeftFarParentGrabEntity", leftFarParentGrabEntity);
enableDispatcherModule("RightFarParentGrabEntity", rightFarParentGrabEntity);
function cleanup() {
disableDispatcherModule("LeftFarParentGrabEntity");
disableDispatcherModule("RightFarParentGrabEntity");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -1,250 +0,0 @@
"use strict";
// nearActionGrabEntity.js
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND,
getControllerJointIndex, getGrabbableData, enableDispatcherModule, disableDispatcherModule,
propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, entityIsGrabbable,
MSECS_PER_SEC, makeDispatcherModuleParameters, makeRunningValues,
TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, findGroupParent, entityIsCloneable, propsAreCloneDynamic, cloneEntity,
HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, unhighlightTargetEntity, Uuid,
DISPATCHER_PROPERTIES, HMD
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
Script.include("/~/system/libraries/cloneEntityUtils.js");
(function() {
function NearActionGrabEntity(hand) {
this.hand = hand;
this.targetEntityID = null;
this.actionID = null; // action this script created...
this.parameters = makeDispatcherModuleParameters(
500,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
var ACTION_TTL = 15; // seconds
var ACTION_TTL_REFRESH = 5;
// XXX does handJointIndex change if the avatar changes?
this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
this.controllerJointIndex = getControllerJointIndex(this.hand);
// handPosition is where the avatar's hand appears to be, in-world.
this.getHandPosition = function () {
if (this.hand === RIGHT_HAND) {
return MyAvatar.getRightPalmPosition();
} else {
return MyAvatar.getLeftPalmPosition();
}
};
this.getHandRotation = function () {
if (this.hand === RIGHT_HAND) {
return MyAvatar.getRightPalmRotation();
} else {
return MyAvatar.getLeftPalmRotation();
}
};
this.startNearGrabAction = function (controllerData, targetProps) {
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
var grabbableData = getGrabbableData(targetProps);
this.grabFollowsController = grabbableData.grabFollowsController;
this.kinematicGrab = grabbableData.grabKinematic;
var handJointIndex;
if (HMD.mounted && HMD.isHandControllerAvailable() && grabbableData.grabFollowsController) {
handJointIndex = getControllerJointIndex(this.hand);
} else {
handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
}
this.offsetPosition = Entities.worldToLocalPosition(targetProps.position, MyAvatar.SELF_ID, handJointIndex);
this.offsetRotation = Entities.worldToLocalRotation(targetProps.rotation, MyAvatar.SELF_ID, handJointIndex);
var now = Date.now();
this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
if (this.actionID) {
Entities.deleteAction(this.targetEntityID, this.actionID);
}
this.actionID = Entities.addAction("hold", this.targetEntityID, {
hand: this.hand === RIGHT_HAND ? "right" : "left",
timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
relativePosition: this.offsetPosition,
relativeRotation: this.offsetRotation,
ttl: ACTION_TTL,
kinematic: this.kinematicGrab,
kinematicSetVelocity: true,
ignoreIK: this.grabFollowsController
});
if (this.actionID === Uuid.NULL) {
this.actionID = null;
return;
}
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'grab',
grabbedEntity: this.targetEntityID,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "startNearGrab", args);
};
// this is for when the action is going to time-out
this.refreshNearGrabAction = function (controllerData) {
var now = Date.now();
if (this.actionID && this.actionTimeout - now < ACTION_TTL_REFRESH * MSECS_PER_SEC) {
// if less than a 5 seconds left, refresh the actions ttl
var success = Entities.updateAction(this.targetEntityID, this.actionID, {
hand: this.hand === RIGHT_HAND ? "right" : "left",
timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
relativePosition: this.offsetPosition,
relativeRotation: this.offsetRotation,
ttl: ACTION_TTL,
kinematic: this.kinematicGrab,
kinematicSetVelocity: true,
ignoreIK: this.grabFollowsController
});
if (success) {
this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
}
}
};
this.endNearGrabAction = function () {
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
Entities.deleteAction(this.targetEntityID, this.actionID);
this.actionID = null;
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'release',
grabbedEntity: this.targetEntityID,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
this.targetEntityID = null;
};
this.getTargetProps = function (controllerData) {
// nearbyEntityProperties is already sorted by distance from controller
var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
for (var i = 0; i < nearbyEntityProperties.length; i++) {
var props = nearbyEntityProperties[i];
if (props.distance > NEAR_GRAB_RADIUS * sensorScaleFactor) {
break;
}
if (entityIsGrabbable(props) || entityIsCloneable(props)) {
if (!entityIsCloneable(props)) {
// if we've attempted to grab a non-cloneable child, roll up to the root of the tree
var groupRootProps = findGroupParent(controllerData, props);
if (entityIsGrabbable(groupRootProps)) {
return groupRootProps;
}
}
return props;
}
}
return null;
};
this.isReady = function (controllerData) {
this.targetEntityID = null;
var targetProps = this.getTargetProps(controllerData);
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE &&
controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
return makeRunningValues(false, [], []);
}
if (targetProps) {
if ((!propsArePhysical(targetProps) && !propsAreCloneDynamic(targetProps)) ||
targetProps.parentID !== Uuid.NULL) {
return makeRunningValues(false, [], []); // let nearParentGrabEntity handle it
} else {
this.targetEntityID = targetProps.id;
return makeRunningValues(true, [this.targetEntityID], []);
}
} else {
return makeRunningValues(false, [], []);
}
};
this.run = function (controllerData) {
if (this.actionID) {
if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE &&
controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
this.endNearGrabAction();
return makeRunningValues(false, [], []);
}
this.refreshNearGrabAction(controllerData);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args);
} else {
// still searching / highlighting
var readiness = this.isReady (controllerData);
if (!readiness.active) {
return readiness;
}
var targetProps = this.getTargetProps(controllerData);
if (targetProps) {
if (controllerData.triggerClicks[this.hand] ||
controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
// switch to grabbing
var targetCloneable = entityIsCloneable(targetProps);
if (targetCloneable) {
var cloneID = cloneEntity(targetProps);
var cloneProps = Entities.getEntityProperties(cloneID, DISPATCHER_PROPERTIES);
this.targetEntityID = cloneID;
this.startNearGrabAction(controllerData, cloneProps);
} else {
this.startNearGrabAction(controllerData, targetProps);
}
}
}
}
return makeRunningValues(true, [this.targetEntityID], []);
};
this.cleanup = function () {
if (this.targetEntityID) {
this.endNearGrabAction();
}
};
}
var leftNearActionGrabEntity = new NearActionGrabEntity(LEFT_HAND);
var rightNearActionGrabEntity = new NearActionGrabEntity(RIGHT_HAND);
enableDispatcherModule("LeftNearActionGrabEntity", leftNearActionGrabEntity);
enableDispatcherModule("RightNearActionGrabEntity", rightNearActionGrabEntity);
function cleanup() {
leftNearActionGrabEntity.cleanup();
rightNearActionGrabEntity.cleanup();
disableDispatcherModule("LeftNearActionGrabEntity");
disableDispatcherModule("RightNearActionGrabEntity");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -1,359 +0,0 @@
"use strict";
// nearParentGrabEntity.js
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, getControllerJointIndex,
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, findHandChildEntities, TEAR_AWAY_DISTANCE, MSECS_PER_SEC, TEAR_AWAY_CHECK_TIME,
TEAR_AWAY_COUNT, distanceBetweenPointAndEntityBoundingBox, print, Uuid, NEAR_GRAB_DISTANCE,
distanceBetweenEntityLocalPositionAndBoundingBox, getGrabbableData, getGrabPointSphereOffset, DISPATCHER_PROPERTIES
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/cloneEntityUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
// XXX this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true;
// XXX this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC;
function NearParentingGrabEntity(hand) {
this.hand = hand;
this.targetEntityID = null;
this.grabbing = false;
this.previousParentID = {};
this.previousParentJointIndex = {};
this.previouslyUnhooked = {};
this.lastUnequipCheckTime = 0;
this.autoUnequipCounter = 0;
this.lastUnexpectedChildrenCheckTime = 0;
this.robbed = false;
this.cloneAllowed = true;
this.parameters = makeDispatcherModuleParameters(
500,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
this.thisHandIsParent = function(props) {
if (!props) {
return false;
}
if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== MyAvatar.SELF_ID) {
return false;
}
var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
if (props.parentJointIndex === handJointIndex) {
return true;
}
if (props.parentJointIndex === getControllerJointIndex(this.hand)) {
return true;
}
var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ?
"_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" :
"_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
if (props.parentJointIndex === controllerCRJointIndex) {
return true;
}
return false;
};
this.getOtherModule = function() {
return this.hand === RIGHT_HAND ? leftNearParentingGrabEntity : rightNearParentingGrabEntity;
};
this.otherHandIsParent = function(props) {
var otherModule = this.getOtherModule();
return (otherModule.thisHandIsParent(props) && otherModule.grabbing);
};
this.startNearParentingGrabEntity = function (controllerData, targetProps) {
var grabData = getGrabbableData(targetProps);
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
var handJointIndex;
if (grabData.grabFollowsController) {
handJointIndex = getControllerJointIndex(this.hand);
} else {
handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
}
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(targetProps.id, "startNearGrab", args);
var reparentProps = {
parentID: MyAvatar.SELF_ID,
parentJointIndex: handJointIndex,
localVelocity: {x: 0, y: 0, z: 0},
localAngularVelocity: {x: 0, y: 0, z: 0}
};
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)) {
var otherModule = this.getOtherModule();
this.previousParentID[this.grabbedThingID] = otherModule.previousParentID[this.grabbedThingID];
this.previousParentJointIndex[this.grabbedThingID] = otherModule.previousParentJointIndex[this.grabbedThingID];
otherModule.robbed = true;
} else {
this.previousParentID[targetProps.id] = targetProps.parentID;
this.previousParentJointIndex[targetProps.id] = targetProps.parentJointIndex;
}
this.targetEntityID = targetProps.id;
Entities.editEntity(targetProps.id, reparentProps);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'grab',
grabbedEntity: targetProps.id,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
this.grabbing = true;
};
this.endNearParentingGrabEntity = function (controllerData) {
var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
if (this.thisHandIsParent(props) && !this.robbed) {
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];
Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'release',
grabbedEntity: this.targetEntityID,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
this.grabbing = false;
this.targetEntityID = null;
this.robbed = false;
};
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 === MyAvatar.SELF_ID) {
var tearAwayDistance = TEAR_AWAY_DISTANCE * MyAvatar.sensorToWorldScale;
var controllerIndex =
this.hand === LEFT_HAND ? Controller.Standard.LeftHand : Controller.Standard.RightHand;
var controllerGrabOffset = getGrabPointSphereOffset(controllerIndex, true);
controllerGrabOffset = Vec3.multiply(-MyAvatar.sensorToWorldScale, controllerGrabOffset);
var distance = distanceBetweenEntityLocalPositionAndBoundingBox(props, controllerGrabOffset);
if (distance > tearAwayDistance) {
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 parentage 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 = Uuid.NULL;
previousParentJointIndex = -1;
}
}
_this.previouslyUnhooked[childID] = now;
Entities.editEntity(childID, {
parentID: previousParentID,
parentJointIndex: previousParentJointIndex
});
} else {
Entities.editEntity(childID, { parentID: Uuid.NULL });
}
});
}
};
this.getTargetProps = function (controllerData) {
// nearbyEntityProperties is already sorted by length from controller
var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
var nearGrabDistance = NEAR_GRAB_DISTANCE * sensorScaleFactor;
var nearGrabRadius = NEAR_GRAB_RADIUS * sensorScaleFactor;
for (var i = 0; i < nearbyEntityProperties.length; i++) {
var props = nearbyEntityProperties[i];
var grabPosition = controllerData.controllerLocations[this.hand].position; // Is offset from hand position.
var dist = distanceBetweenPointAndEntityBoundingBox(grabPosition, props);
var distance = Vec3.distance(grabPosition, props.position);
if ((dist > nearGrabDistance) ||
(distance > nearGrabRadius)) { // Only smallish entities can be near grabbed.
continue;
}
if (entityIsGrabbable(props) || entityIsCloneable(props)) {
if (!entityIsCloneable(props)) {
// if we've attempted to grab a non-cloneable child, roll up to the root of the tree
var groupRootProps = findGroupParent(controllerData, props);
if (entityIsGrabbable(groupRootProps)) {
return groupRootProps;
}
}
return props;
}
}
return null;
};
this.isReady = function (controllerData, deltaTime) {
this.targetEntityID = null;
this.grabbing = false;
var targetProps = this.getTargetProps(controllerData);
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE &&
controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
this.checkForUnexpectedChildren(controllerData);
this.robbed = false;
this.cloneAllowed = true;
return makeRunningValues(false, [], []);
}
if (targetProps) {
if ((propsArePhysical(targetProps) || propsAreCloneDynamic(targetProps)) &&
targetProps.parentID === Uuid.NULL) {
this.robbed = false;
return makeRunningValues(false, [], []); // let nearActionGrabEntity handle it
} else {
this.targetEntityID = targetProps.id;
return makeRunningValues(true, [this.targetEntityID], []);
}
} else {
this.robbed = false;
return makeRunningValues(false, [], []);
}
};
this.run = function (controllerData, deltaTime) {
if (this.grabbing) {
if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE &&
controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
this.endNearParentingGrabEntity(controllerData);
return makeRunningValues(false, [], []);
}
var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
if (!props) {
// entity was deleted
this.grabbing = false;
this.targetEntityID = null;
this.robbed = false;
return makeRunningValues(false, [], []);
}
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, [], []);
}
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args);
} else {
// still searching
var readiness = this.isReady(controllerData);
if (!readiness.active) {
this.robbed = false;
return readiness;
}
if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
// switch to grab
var targetProps = this.getTargetProps(controllerData);
var targetCloneable = entityIsCloneable(targetProps);
if (targetCloneable) {
if (this.cloneAllowed) {
var cloneID = cloneEntity(targetProps);
if (cloneID !== null) {
var cloneProps = Entities.getEntityProperties(cloneID, DISPATCHER_PROPERTIES);
this.grabbing = true;
this.targetEntityID = cloneID;
this.startNearParentingGrabEntity(controllerData, cloneProps);
this.cloneAllowed = false; // prevent another clone call until inputs released
}
}
} else if (targetProps) {
this.grabbing = true;
this.startNearParentingGrabEntity(controllerData, targetProps);
}
}
}
return makeRunningValues(true, [this.targetEntityID], []);
};
this.cleanup = function () {
if (this.targetEntityID) {
this.endNearParentingGrabEntity();
}
};
}
var leftNearParentingGrabEntity = new NearParentingGrabEntity(LEFT_HAND);
var rightNearParentingGrabEntity = new NearParentingGrabEntity(RIGHT_HAND);
enableDispatcherModule("LeftNearParentingGrabEntity", leftNearParentingGrabEntity);
enableDispatcherModule("RightNearParentingGrabEntity", rightNearParentingGrabEntity);
function cleanup() {
leftNearParentingGrabEntity.cleanup();
rightNearParentingGrabEntity.cleanup();
disableDispatcherModule("LeftNearParentingGrabEntity");
disableDispatcherModule("RightNearParentingGrabEntity");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -32,22 +32,13 @@ var CONTOLLER_SCRIPTS = [
"controllerModules/mouseHMD.js",
"controllerModules/scaleEntity.js",
"controllerModules/nearGrabHyperLinkEntity.js",
"controllerModules/nearTabletHighlight.js"
"controllerModules/nearTabletHighlight.js",
"controllerModules/nearGrabEntity.js",
"controllerModules/farGrabEntity.js"
];
if (Settings.getValue("useTraitsGrab", true)) {
CONTOLLER_SCRIPTS.push("controllerModules/nearGrabEntity.js");
CONTOLLER_SCRIPTS.push("controllerModules/farGrabEntity.js");
} else {
CONTOLLER_SCRIPTS.push("controllerModules/nearParentGrabEntity.js");
CONTOLLER_SCRIPTS.push("controllerModules/nearActionGrabEntity.js");
CONTOLLER_SCRIPTS.push("controllerModules/farActionGrabEntityDynOnly.js");
CONTOLLER_SCRIPTS.push("controllerModules/farParentGrabEntity.js");
}
var DEBUG_MENU_ITEM = "Debug defaultScripts.js";
function runDefaultsTogether() {
for (var j in CONTOLLER_SCRIPTS) {
if (CONTOLLER_SCRIPTS.hasOwnProperty(j)) {

View file

@ -14,79 +14,25 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* global MyAvatar, Entities, Script, HMD, Camera, Vec3, Reticle, Overlays, getEntityCustomData, Messages, Quat, Controller,
/* global MyAvatar, Entities, Script, HMD, Camera, Vec3, Reticle, Overlays, Messages, Quat, Controller,
isInEditMode, entityIsGrabbable, Picks, PickType, Pointers, unhighlightTargetEntity, DISPATCHER_PROPERTIES,
entityIsGrabbable, entityIsEquipped, getMainTabletIDs
entityIsGrabbable, getMainTabletIDs
*/
/* jslint bitwise: true */
(function() { // BEGIN LOCAL_SCOPE
Script.include("/~/system/libraries/utils.js");
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/utils.js");
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
var MOUSE_GRAB_JOINT = 65526; // FARGRAB_MOUSE_INDEX
var MAX_SOLID_ANGLE = 0.01; // objects that appear smaller than this can't be grabbed
var DELAY_FOR_30HZ = 33; // milliseconds
var ZERO_VEC3 = {
x: 0,
y: 0,
z: 0
};
var IDENTITY_QUAT = {
x: 0,
y: 0,
z: 0,
w: 0
};
var DEFAULT_GRABBABLE_DATA = {
grabbable: true,
invertSolidWhileHeld: false
};
var ACTION_TTL = 10; // seconds
function getTag() {
return "grab-" + MyAvatar.sessionUUID;
}
var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position
var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified
var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified
function distanceGrabTimescale(mass, distance) {
var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass /
DISTANCE_HOLDING_UNITY_MASS * distance /
DISTANCE_HOLDING_UNITY_DISTANCE;
if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) {
timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
}
return timeScale;
}
function getMass(dimensions, density) {
return (dimensions.x * dimensions.y * dimensions.z) * density;
}
function entityIsGrabbedByOther(entityID) {
// by convention, a distance grab sets the tag of its action to be grab-*owner-session-id*.
var actionIDs = Entities.getActionIDs(entityID);
for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) {
var actionID = actionIDs[actionIndex];
var actionArguments = Entities.getActionArguments(entityID, actionID);
var tag = actionArguments.tag;
if (tag == getTag()) {
// we see a grab-*uuid* shaped tag, but it's our tag, so that's okay.
continue;
}
if (tag.slice(0, 5) == "grab-") {
// we see a grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it.
return true;
}
}
return false;
}
var ZERO_VEC3 = { x: 0, y: 0, z: 0 };
var IDENTITY_QUAT = { x: 0, y: 0, z: 0, w: 1 };
// helper function
function mouseIntersectionWithPlane(pointOnPlane, planeNormal, event, maxDistance) {
@ -227,7 +173,6 @@ var beacon = {
function Grabber() {
this.isGrabbing = false;
this.entityID = null;
this.actionID = null;
this.startPosition = ZERO_VEC3;
this.lastRotation = IDENTITY_QUAT;
this.currentPosition = ZERO_VEC3;
@ -253,9 +198,6 @@ function Grabber() {
z: 0
};
this.targetPosition = null;
this.targetRotation = null;
this.liftKey = false; // SHIFT
this.rotateKey = false; // CONTROL
@ -305,7 +247,7 @@ Grabber.prototype.computeNewGrabPlane = function() {
}
}
this.pointOnPlane = Vec3.sum(this.currentPosition, this.offset);
this.pointOnPlane = Vec3.subtract(this.currentPosition, this.offset);
var xzOffset = Vec3.subtract(this.pointOnPlane, Camera.getPosition());
xzOffset.y = 0;
this.xzDistanceToGrab = Vec3.length(xzOffset);
@ -315,15 +257,12 @@ Grabber.prototype.pressEvent = function(event) {
if (isInEditMode() || HMD.active) {
return;
}
if (event.button !== "LEFT") {
return;
}
if (event.isAlt || event.isMeta) {
return;
}
if (Overlays.getOverlayAtPoint(Reticle.position) > 0) {
// the mouse is pointing at an overlay; don't look for entities underneath the overlay.
return;
@ -341,13 +280,12 @@ Grabber.prototype.pressEvent = function(event) {
}
var props = Entities.getEntityProperties(pickResults.objectID, DISPATCHER_PROPERTIES);
var isDynamic = props.dynamic;
if (!entityIsGrabbable(props)) {
// only grab grabbable objects
return;
}
if (!props.grab.grabbable) {
if (props.grab.equippable) {
// don't mouse-grab click-to-equip entities (let equipEntity.js handle these)
return;
}
@ -361,7 +299,6 @@ Grabber.prototype.pressEvent = function(event) {
var entityProperties = Entities.getEntityProperties(clickedEntity, DISPATCHER_PROPERTIES);
this.startPosition = entityProperties.position;
this.lastRotation = entityProperties.rotation;
this.madeDynamic = false;
var cameraPosition = Camera.getPosition();
var objectBoundingDiameter = Vec3.length(entityProperties.dimensions);
@ -373,21 +310,10 @@ Grabber.prototype.pressEvent = function(event) {
return;
}
if (entityIsGrabbable(props) && !isDynamic) {
entityProperties.dynamic = true;
Entities.editEntity(clickedEntity, entityProperties);
this.madeDynamic = true;
}
// this.activateEntity(clickedEntity, entityProperties);
this.isGrabbing = true;
this.entityID = clickedEntity;
this.currentPosition = entityProperties.position;
this.targetPosition = {
x: this.startPosition.x,
y: this.startPosition.y,
z: this.startPosition.z
};
// compute the grab point
var pickRay = Camera.computePickRay(event.x, event.y);
@ -396,14 +322,13 @@ Grabber.prototype.pressEvent = function(event) {
nearestPoint = Vec3.multiply(distanceToGrab, pickRay.direction);
this.pointOnPlane = Vec3.sum(cameraPosition, nearestPoint);
// compute the grab offset (points from object center to point of grab)
this.offset = Vec3.subtract(this.pointOnPlane, this.startPosition);
// compute the grab offset (points from point of grab to object center)
this.offset = Vec3.subtract(this.startPosition, this.pointOnPlane); // offset in world-space
MyAvatar.setJointTranslation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointPoint(this.startPosition));
MyAvatar.setJointRotation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointRotation(this.lastRotation));
this.computeNewGrabPlane();
if (!entityIsGrabbedByOther(this.entityID)) {
this.moveEvent(event);
}
this.moveEvent(event);
var args = "mouse";
Entities.callEntityMethod(this.entityID, "startDistanceGrab", args);
@ -413,6 +338,12 @@ Grabber.prototype.pressEvent = function(event) {
grabbedEntity: this.entityID
}));
if (this.grabID) {
MyAvatar.releaseGrab(this.grabID);
this.grabID = null;
}
this.grabID = MyAvatar.grab(this.entityID, MOUSE_GRAB_JOINT, ZERO_VEC3, IDENTITY_QUAT);
// TODO: play sounds again when we aren't leaking AudioInjector threads
//Audio.playSound(grabSound, { position: entityProperties.position, volume: VOLUME });
};
@ -428,20 +359,7 @@ Grabber.prototype.releaseEvent = function(event) {
}
if (this.isGrabbing) {
// this.deactivateEntity(this.entityID);
this.isGrabbing = false;
if (this.actionID) {
Entities.deleteAction(this.entityID, this.actionID);
}
if (this.madeDynamic) {
var entityProps = {};
entityProps.dynamic = false;
entityProps.localVelocity = {x: 0, y: 0, z: 0};
Entities.editEntity(this.entityID, entityProps);
}
this.actionID = null;
Pointers.setRenderState(this.mouseRayEntities, "");
Pointers.setLockEndUUID(this.mouseRayEntities, null, false);
@ -455,6 +373,13 @@ Grabber.prototype.releaseEvent = function(event) {
joint: "mouse"
}));
if (this.grabID) {
MyAvatar.releaseGrab(this.grabID);
this.grabID = null;
}
MyAvatar.clearJointData(MOUSE_GRAB_JOINT);
// TODO: play sounds again when we aren't leaking AudioInjector threads
//Audio.playSound(releaseSound, { position: entityProperties.position, volume: VOLUME });
}
@ -482,23 +407,12 @@ Grabber.prototype.moveEvent = function(event) {
Grabber.prototype.moveEventProcess = function() {
this.moveEventTimer = null;
// see if something added/restored gravity
var entityProperties = Entities.getEntityProperties(this.entityID, DISPATCHER_PROPERTIES);
if (!entityProperties || !entityProperties.gravity || HMD.active) {
if (!entityProperties || HMD.active) {
return;
}
if (Vec3.length(entityProperties.gravity) !== 0.0) {
this.originalGravity = entityProperties.gravity;
}
this.currentPosition = entityProperties.position;
this.mass = getMass(entityProperties.dimensions, entityProperties.density);
var cameraPosition = Camera.getPosition();
var actionArgs = {
tag: getTag(),
ttl: ACTION_TTL
};
if (this.mode === "rotate") {
var drag = mouse.getDrag();
@ -510,19 +424,9 @@ Grabber.prototype.moveEventProcess = function() {
var ROTATE_STRENGTH = 0.4; // magic number tuned by hand
var angle = ROTATE_STRENGTH * Math.sqrt((drag.x * drag.x) + (drag.y * drag.y));
var deltaQ = Quat.angleAxis(angle, axis);
// var qZero = entityProperties.rotation;
//var qZero = this.lastRotation;
this.lastRotation = Quat.multiply(deltaQ, this.lastRotation);
var distanceToCameraR = Vec3.length(Vec3.subtract(this.currentPosition, cameraPosition));
var angularTimeScale = distanceGrabTimescale(this.mass, distanceToCameraR);
actionArgs = {
targetRotation: this.lastRotation,
angularTimeScale: angularTimeScale,
tag: getTag(),
ttl: ACTION_TTL
};
MyAvatar.setJointRotation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointRotation(this.lastRotation));
} else {
var newPointOnPlane;
@ -534,17 +438,10 @@ Grabber.prototype.moveEventProcess = function() {
planeNormal = Vec3.normalize(planeNormal);
var pointOnCylinder = Vec3.multiply(planeNormal, this.xzDistanceToGrab);
pointOnCylinder = Vec3.sum(Camera.getPosition(), pointOnCylinder);
this.pointOnPlane = mouseIntersectionWithPlane(pointOnCylinder, planeNormal, mouse.current, this.maxDistance);
newPointOnPlane = {
x: this.pointOnPlane.x,
y: this.pointOnPlane.y,
z: this.pointOnPlane.z
};
newPointOnPlane = mouseIntersectionWithPlane(pointOnCylinder, planeNormal, mouse.current, this.maxDistance);
} else {
newPointOnPlane = mouseIntersectionWithPlane(
this.pointOnPlane, this.planeNormal, mouse.current, this.maxDistance);
var cameraPosition = Camera.getPosition();
newPointOnPlane = mouseIntersectionWithPlane(this.pointOnPlane, this.planeNormal, mouse.current, this.maxDistance);
var relativePosition = Vec3.subtract(newPointOnPlane, cameraPosition);
var distance = Vec3.length(relativePosition);
if (distance > this.maxDistance) {
@ -553,26 +450,8 @@ Grabber.prototype.moveEventProcess = function() {
newPointOnPlane = Vec3.sum(relativePosition, cameraPosition);
}
}
this.targetPosition = Vec3.subtract(newPointOnPlane, this.offset);
var distanceToCameraL = Vec3.length(Vec3.subtract(this.targetPosition, cameraPosition));
var linearTimeScale = distanceGrabTimescale(this.mass, distanceToCameraL);
actionArgs = {
targetPosition: this.targetPosition,
linearTimeScale: linearTimeScale,
tag: getTag(),
ttl: ACTION_TTL
};
}
if (!this.actionID) {
if (!entityIsGrabbedByOther(this.entityID) && !entityIsEquipped(this.entityID)) {
this.actionID = Entities.addAction("far-grab", this.entityID, actionArgs);
}
} else {
Entities.updateAction(this.entityID, this.actionID, actionArgs);
MyAvatar.setJointTranslation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointPoint(Vec3.sum(newPointOnPlane, this.offset)));
}
this.scheduleMouseMoveProcessor();
@ -601,6 +480,10 @@ Grabber.prototype.keyPressEvent = function(event) {
Grabber.prototype.cleanup = function() {
Pointers.removePointer(this.mouseRayEntities);
Picks.removePick(this.mouseRayOverlays);
if (this.grabID) {
MyAvatar.releaseGrab(this.grabID);
this.grabID = null;
}
};
var grabber = new Grabber();

View file

@ -33,6 +33,7 @@
getGrabbableData:true,
isAnothersAvatarEntity:true,
isAnothersChildEntity:true,
entityIsEquippable:true,
entityIsGrabbable:true,
entityIsDistanceGrabbable:true,
getControllerJointIndexCacheTime:true,
@ -58,7 +59,6 @@
NEAR_GRAB_DISTANCE: true,
distanceBetweenPointAndEntityBoundingBox:true,
entityIsEquipped:true,
entityIsFarGrabbedByOther:true,
highlightTargetEntity:true,
clearHighlightedEntities:true,
unhighlightTargetEntity:true,
@ -323,6 +323,18 @@ isAnothersChildEntity = function (iaceProps) {
return false;
};
entityIsEquippable = function (eieProps) {
var grabbable = getGrabbableData(eieProps).grabbable;
if (!grabbable ||
isAnothersAvatarEntity(eieProps) ||
isAnothersChildEntity(eieProps) ||
FORBIDDEN_GRAB_TYPES.indexOf(eieProps.type) >= 0) {
return false;
}
return true;
};
entityIsGrabbable = function (eigProps) {
var grabbable = getGrabbableData(eigProps).grabbable;
if (!grabbable ||
@ -561,27 +573,6 @@ entityIsEquipped = function(entityID) {
return equippedInRightHand || equippedInLeftHand;
};
entityIsFarGrabbedByOther = function(entityID) {
// by convention, a far grab sets the tag of its action to be far-grab-*owner-session-id*.
var actionIDs = Entities.getActionIDs(entityID);
var myFarGrabTag = "far-grab-" + MyAvatar.sessionUUID;
for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) {
var actionID = actionIDs[actionIndex];
var actionArguments = Entities.getActionArguments(entityID, actionID);
var tag = actionArguments.tag;
if (tag == myFarGrabTag) {
// we see a far-grab-*uuid* shaped tag, but it's our tag, so that's okay.
continue;
}
if (tag.slice(0, 9) == "far-grab-") {
// we see a far-grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it.
return true;
}
}
return false;
};
worldPositionToRegistrationFrameMatrix = function(wptrProps, pos) {
// get world matrix for intersection point
var intersectionMat = new Xform({ x: 0, y: 0, z:0, w: 1 }, pos);
@ -620,6 +611,7 @@ if (typeof module !== 'undefined') {
BUMPER_ON_VALUE: BUMPER_ON_VALUE,
TEAR_AWAY_DISTANCE: TEAR_AWAY_DISTANCE,
propsArePhysical: propsArePhysical,
entityIsEquippable: entityIsEquippable,
entityIsGrabbable: entityIsGrabbable,
NEAR_GRAB_RADIUS: NEAR_GRAB_RADIUS,
projectOntoOverlayXYPlane: projectOntoOverlayXYPlane,