mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-25 13:52:49 +02:00
2377 lines
90 KiB
JavaScript
2377 lines
90 KiB
JavaScript
"use strict";
|
|
// handControllerGrab.js
|
|
//
|
|
// Created by Eric Levin on 9/2/15
|
|
// Additions by James B. Pollack @imgntn on 9/24/2015
|
|
// Additions By Seth Alves on 10/20/2015
|
|
// Copyright 2015 High Fidelity, Inc.
|
|
//
|
|
// Grabs physically moveable entities with hydra-like controllers; it works for either near or far objects.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
/* global setEntityCustomData, getEntityCustomData, vec3toStr, flatten, Xform */
|
|
|
|
Script.include("/~/system/libraries/utils.js");
|
|
Script.include("/~/system/libraries/Xform.js");
|
|
|
|
//
|
|
// add lines where the hand ray picking is happening
|
|
//
|
|
var WANT_DEBUG = false;
|
|
var WANT_DEBUG_STATE = false;
|
|
var WANT_DEBUG_SEARCH_NAME = null;
|
|
|
|
//
|
|
// these tune time-averaging and "on" value for analog trigger
|
|
//
|
|
|
|
var SPARK_MODEL_SCALE_FACTOR = 0.75;
|
|
|
|
var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing
|
|
var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab
|
|
var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab
|
|
var TRIGGER_OFF_VALUE = 0.15;
|
|
|
|
var COLLIDE_WITH_AV_AFTER_RELEASE_DELAY = 0.25; // seconds
|
|
|
|
var BUMPER_ON_VALUE = 0.5;
|
|
|
|
var THUMB_ON_VALUE = 0.5;
|
|
|
|
var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move.
|
|
|
|
var PICK_WITH_HAND_RAY = true;
|
|
|
|
var DRAW_GRAB_BOXES = false;
|
|
var DRAW_HAND_SPHERES = false;
|
|
var DROP_WITHOUT_SHAKE = false;
|
|
|
|
var EQUIP_SPHERE_COLOR = { red: 179, green: 120, blue: 211 };
|
|
var EQUIP_SPHERE_ALPHA = 0.15;
|
|
var EQUIP_SPHERE_SCALE_FACTOR = 0.65;
|
|
|
|
//
|
|
// distant manipulation
|
|
//
|
|
|
|
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
|
|
var MOVE_WITH_HEAD = true; // experimental head-control of distantly held objects
|
|
|
|
var NO_INTERSECT_COLOR = {
|
|
red: 10,
|
|
green: 10,
|
|
blue: 255
|
|
}; // line color when pick misses
|
|
var INTERSECT_COLOR = {
|
|
red: 250,
|
|
green: 10,
|
|
blue: 10
|
|
}; // line color when pick hits
|
|
var LINE_ENTITY_DIMENSIONS = {
|
|
x: 1000,
|
|
y: 1000,
|
|
z: 1000
|
|
};
|
|
|
|
var LINE_LENGTH = 500;
|
|
var PICK_MAX_DISTANCE = 500; // max length of pick-ray
|
|
|
|
var EQUIP_RADIUS_EMBIGGEN_FACTOR = 1.1;
|
|
|
|
//
|
|
// near grabbing
|
|
//
|
|
|
|
var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping.
|
|
var EQUIP_HOTSPOT_RENDER_RADIUS = 0.3; // radius used for palm vs equip-hotspot for rendering hot-spots
|
|
|
|
var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
|
|
|
|
var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing.
|
|
var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand
|
|
|
|
var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing.
|
|
|
|
var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object
|
|
var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed
|
|
var CHECK_TOO_FAR_UNEQUIP_TIME = 1.0; // seconds
|
|
|
|
//
|
|
// other constants
|
|
//
|
|
|
|
var HOTSPOT_DRAW_DISTANCE = 10;
|
|
var RIGHT_HAND = 1;
|
|
var LEFT_HAND = 0;
|
|
|
|
var ZERO_VEC = {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
};
|
|
|
|
var NULL_UUID = "{00000000-0000-0000-0000-000000000000}";
|
|
|
|
// these control how long an abandoned pointer line or action will hang around
|
|
var LIFETIME = 10;
|
|
var ACTION_TTL = 15; // seconds
|
|
var ACTION_TTL_REFRESH = 5;
|
|
var PICKS_PER_SECOND_PER_HAND = 60;
|
|
var MSECS_PER_SEC = 1000.0;
|
|
var GRABBABLE_PROPERTIES = [
|
|
"position",
|
|
"rotation",
|
|
"gravity",
|
|
"collidesWith",
|
|
"dynamic",
|
|
"collisionless",
|
|
"locked",
|
|
"name",
|
|
"shapeType",
|
|
"parentID",
|
|
"parentJointIndex",
|
|
"density",
|
|
"dimensions",
|
|
"userData"
|
|
];
|
|
|
|
var GRABBABLE_DATA_KEY = "grabbableKey"; // shared with grab.js
|
|
var GRAB_USER_DATA_KEY = "grabKey"; // shared with grab.js
|
|
|
|
var DEFAULT_GRABBABLE_DATA = {
|
|
disableReleaseVelocity: false
|
|
};
|
|
|
|
// sometimes we want to exclude objects from being picked
|
|
var USE_BLACKLIST = true;
|
|
var blacklist = [];
|
|
|
|
// we've created various ways of visualizing looking for and moving distant objects
|
|
var USE_ENTITY_LINES_FOR_SEARCHING = false;
|
|
var USE_OVERLAY_LINES_FOR_SEARCHING = true;
|
|
|
|
var USE_ENTITY_LINES_FOR_MOVING = false;
|
|
var USE_OVERLAY_LINES_FOR_MOVING = false;
|
|
var USE_PARTICLE_BEAM_FOR_MOVING = true;
|
|
|
|
var USE_SPOTLIGHT = false;
|
|
var USE_POINTLIGHT = false;
|
|
|
|
var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"];
|
|
var FORBIDDEN_GRAB_TYPES = ['Unknown', 'Light', 'PolyLine', 'Zone'];
|
|
|
|
// states for the state machine
|
|
var STATE_OFF = 0;
|
|
var STATE_SEARCHING = 1;
|
|
var STATE_DISTANCE_HOLDING = 2;
|
|
var STATE_NEAR_GRABBING = 3;
|
|
var STATE_NEAR_TRIGGER = 4;
|
|
var STATE_FAR_TRIGGER = 5;
|
|
var STATE_HOLD = 6;
|
|
|
|
// "collidesWith" is specified by comma-separated list of group names
|
|
// the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar
|
|
var COLLIDES_WITH_WHILE_GRABBED = "dynamic,otherAvatar";
|
|
var COLLIDES_WITH_WHILE_MULTI_GRABBED = "dynamic";
|
|
|
|
var HEART_BEAT_INTERVAL = 5 * MSECS_PER_SEC;
|
|
var HEART_BEAT_TIMEOUT = 15 * MSECS_PER_SEC;
|
|
|
|
var avCollideLater;
|
|
var setCollidesLaterTimeout;
|
|
var collideLaterID;
|
|
|
|
var CONTROLLER_STATE_MACHINE = {};
|
|
|
|
CONTROLLER_STATE_MACHINE[STATE_OFF] = {
|
|
name: "off",
|
|
enterMethod: "offEnter",
|
|
updateMethod: "off"
|
|
};
|
|
CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = {
|
|
name: "searching",
|
|
updateMethod: "search"
|
|
};
|
|
CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = {
|
|
name: "distance_holding",
|
|
enterMethod: "distanceHoldingEnter",
|
|
updateMethod: "distanceHolding"
|
|
};
|
|
CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = {
|
|
name: "near_grabbing",
|
|
enterMethod: "nearGrabbingEnter",
|
|
updateMethod: "nearGrabbing"
|
|
};
|
|
CONTROLLER_STATE_MACHINE[STATE_HOLD] = {
|
|
name: "hold",
|
|
enterMethod: "nearGrabbingEnter",
|
|
updateMethod: "nearGrabbing"
|
|
};
|
|
CONTROLLER_STATE_MACHINE[STATE_NEAR_TRIGGER] = {
|
|
name: "trigger",
|
|
enterMethod: "nearTriggerEnter",
|
|
updateMethod: "nearTrigger"
|
|
};
|
|
CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = {
|
|
name: "far_trigger",
|
|
enterMethod: "farTriggerEnter",
|
|
updateMethod: "farTrigger"
|
|
};
|
|
|
|
function stateToName(state) {
|
|
return CONTROLLER_STATE_MACHINE[state] ? CONTROLLER_STATE_MACHINE[state].name : "???";
|
|
}
|
|
|
|
function getTag() {
|
|
return "grab-" + MyAvatar.sessionUUID;
|
|
}
|
|
|
|
function entityHasActions(entityID) {
|
|
return Entities.getActionIDs(entityID).length > 0;
|
|
}
|
|
|
|
function findRayIntersection(pickRay, precise, include, exclude) {
|
|
var entities = Entities.findRayIntersection(pickRay, precise, include, exclude);
|
|
var overlays = Overlays.findRayIntersection(pickRay);
|
|
if (!overlays.intersects || (entities.intersects && (entities.distance <= overlays.distance))) {
|
|
return entities;
|
|
}
|
|
return overlays;
|
|
}
|
|
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;
|
|
}
|
|
|
|
function propsArePhysical(props) {
|
|
if (!props.dynamic) {
|
|
return false;
|
|
}
|
|
var isPhysical = (props.shapeType && props.shapeType != 'none');
|
|
return isPhysical;
|
|
}
|
|
|
|
function removeMyAvatarFromCollidesWith(origCollidesWith) {
|
|
var collidesWithSplit = origCollidesWith.split(",");
|
|
// remove myAvatar from the array
|
|
for (var i = collidesWithSplit.length - 1; i >= 0; i--) {
|
|
if (collidesWithSplit[i] === "myAvatar") {
|
|
collidesWithSplit.splice(i, 1);
|
|
}
|
|
}
|
|
return collidesWithSplit.join();
|
|
}
|
|
|
|
// If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here,
|
|
// and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode.
|
|
var EXTERNALLY_MANAGED_2D_MINOR_MODE = true;
|
|
var EDIT_SETTING = "io.highfidelity.isEditting";
|
|
function isEditing() {
|
|
var actualSettingValue = Settings.getValue(EDIT_SETTING) === "false" ? false : !!Settings.getValue(EDIT_SETTING);
|
|
return EXTERNALLY_MANAGED_2D_MINOR_MODE && actualSettingValue;
|
|
}
|
|
function isIn2DMode() {
|
|
// In this version, we make our own determination of whether we're aimed a HUD element,
|
|
// because other scripts (such as handControllerPointer) might be using some other visualization
|
|
// instead of setting Reticle.visible.
|
|
return (EXTERNALLY_MANAGED_2D_MINOR_MODE &&
|
|
(Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position)));
|
|
}
|
|
function restore2DMode() {
|
|
if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) {
|
|
Reticle.setVisible(true);
|
|
}
|
|
}
|
|
|
|
// EntityPropertiesCache is a helper class that contains a cache of entity properties.
|
|
// the hope is to prevent excess calls to Entity.getEntityProperties()
|
|
function EntityPropertiesCache() {
|
|
this.cache = {};
|
|
}
|
|
EntityPropertiesCache.prototype.clear = function () {
|
|
this.cache = {};
|
|
};
|
|
EntityPropertiesCache.prototype.findEntities = function (position, radius) {
|
|
var entities = Entities.findEntities(position, radius);
|
|
var _this = this;
|
|
entities.forEach(function (x) {
|
|
_this.updateEntity(x);
|
|
});
|
|
};
|
|
EntityPropertiesCache.prototype.updateEntity = function (entityID) {
|
|
var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES);
|
|
|
|
// convert props.userData from a string to an object.
|
|
var userData = {};
|
|
if (props.userData) {
|
|
try {
|
|
userData = JSON.parse(props.userData);
|
|
} catch (err) {
|
|
print("WARNING: malformed userData on " + entityID + ", name = " + props.name + ", error = " + err);
|
|
}
|
|
}
|
|
props.userData = userData;
|
|
|
|
this.cache[entityID] = props;
|
|
};
|
|
EntityPropertiesCache.prototype.getEntities = function () {
|
|
return Object.keys(this.cache);
|
|
};
|
|
EntityPropertiesCache.prototype.getProps = function (entityID) {
|
|
var obj = this.cache[entityID];
|
|
return obj ? obj : undefined;
|
|
};
|
|
EntityPropertiesCache.prototype.getGrabbableProps = function (entityID) {
|
|
var props = this.cache[entityID];
|
|
if (props) {
|
|
return props.userData.grabbableKey ? props.userData.grabbableKey : DEFAULT_GRABBABLE_DATA;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
};
|
|
EntityPropertiesCache.prototype.getGrabProps = function (entityID) {
|
|
var props = this.cache[entityID];
|
|
if (props) {
|
|
return props.userData.grabKey ? props.userData.grabKey : {};
|
|
} else {
|
|
return undefined;
|
|
}
|
|
};
|
|
EntityPropertiesCache.prototype.getWearableProps = function (entityID) {
|
|
var props = this.cache[entityID];
|
|
if (props) {
|
|
return props.userData.wearable ? props.userData.wearable : {};
|
|
} else {
|
|
return undefined;
|
|
}
|
|
};
|
|
EntityPropertiesCache.prototype.getEquipHotspotsProps = function (entityID) {
|
|
var props = this.cache[entityID];
|
|
if (props) {
|
|
return props.userData.equipHotspots ? props.userData.equipHotspots : {};
|
|
} else {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
function MyController(hand) {
|
|
this.hand = hand;
|
|
if (this.hand === RIGHT_HAND) {
|
|
this.getHandPosition = MyAvatar.getRightPalmPosition;
|
|
// this.getHandRotation = MyAvatar.getRightPalmRotation;
|
|
} else {
|
|
this.getHandPosition = MyAvatar.getLeftPalmPosition;
|
|
// this.getHandRotation = MyAvatar.getLeftPalmRotation;
|
|
}
|
|
this.getHandRotation = function () {
|
|
var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
|
|
return Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(controllerHandInput).rotation);
|
|
};
|
|
|
|
this.actionID = null; // action this script created...
|
|
this.grabbedEntity = null; // on this entity.
|
|
this.state = STATE_OFF;
|
|
this.pointer = null; // entity-id of line object
|
|
this.entityActivated = false;
|
|
|
|
this.triggerValue = 0; // rolling average of trigger value
|
|
this.rawTriggerValue = 0;
|
|
this.rawSecondaryValue = 0;
|
|
this.rawThumbValue = 0;
|
|
|
|
// for visualizations
|
|
this.overlayLine = null;
|
|
this.particleBeamObject = null;
|
|
|
|
// for lights
|
|
this.spotlight = null;
|
|
this.pointlight = null;
|
|
this.overlayLine = null;
|
|
this.searchSphere = null;
|
|
|
|
this.waitForTriggerRelease = false;
|
|
|
|
// how far from camera to search intersection?
|
|
var DEFAULT_SEARCH_SPHERE_DISTANCE = 1000;
|
|
this.intersectionDistance = 0.0;
|
|
this.searchSphereDistance = DEFAULT_SEARCH_SPHERE_DISTANCE;
|
|
|
|
this.ignoreIK = false;
|
|
this.offsetPosition = Vec3.ZERO;
|
|
this.offsetRotation = Quat.IDENTITY;
|
|
|
|
this.lastPickTime = 0;
|
|
this.lastUnequipCheckTime = 0;
|
|
|
|
this.entityPropertyCache = new EntityPropertiesCache();
|
|
|
|
this.equipOverlayInfoSetMap = {};
|
|
|
|
var _this = this;
|
|
|
|
var suppressedIn2D = [STATE_OFF, STATE_SEARCHING];
|
|
this.ignoreInput = function () {
|
|
// We've made the decision to use 'this' for new code, even though it is fragile,
|
|
// in order to keep/ the code uniform without making any no-op line changes.
|
|
return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode();
|
|
};
|
|
|
|
this.update = function (deltaTime) {
|
|
|
|
this.updateSmoothedTrigger();
|
|
|
|
if (this.ignoreInput()) {
|
|
this.turnOffVisualizations();
|
|
return;
|
|
}
|
|
|
|
if (CONTROLLER_STATE_MACHINE[this.state]) {
|
|
var updateMethodName = CONTROLLER_STATE_MACHINE[this.state].updateMethod;
|
|
var updateMethod = this[updateMethodName];
|
|
if (updateMethod) {
|
|
updateMethod.call(this, deltaTime);
|
|
} else {
|
|
print("WARNING: could not find updateMethod for state " + stateToName(this.state));
|
|
}
|
|
} else {
|
|
print("WARNING: could not find state " + this.state + " in state machine");
|
|
}
|
|
};
|
|
|
|
this.callEntityMethodOnGrabbed = function (entityMethodName) {
|
|
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
|
Entities.callEntityMethod(this.grabbedEntity, entityMethodName, args);
|
|
};
|
|
|
|
this.setState = function (newState, reason) {
|
|
|
|
if (WANT_DEBUG || WANT_DEBUG_STATE) {
|
|
var oldStateName = stateToName(this.state);
|
|
var newStateName = stateToName(newState);
|
|
print("STATE (" + this.hand + "): " + newStateName + " <-- " + oldStateName + ", reason = " + reason);
|
|
}
|
|
|
|
// exit the old state
|
|
if (CONTROLLER_STATE_MACHINE[this.state]) {
|
|
var exitMethodName = CONTROLLER_STATE_MACHINE[this.state].exitMethod;
|
|
var exitMethod = this[exitMethodName];
|
|
if (exitMethod) {
|
|
exitMethod.call(this);
|
|
}
|
|
} else {
|
|
print("WARNING: could not find state " + this.state + " in state machine");
|
|
}
|
|
|
|
this.state = newState;
|
|
|
|
// enter the new state
|
|
if (CONTROLLER_STATE_MACHINE[newState]) {
|
|
var enterMethodName = CONTROLLER_STATE_MACHINE[newState].enterMethod;
|
|
var enterMethod = this[enterMethodName];
|
|
if (enterMethod) {
|
|
enterMethod.call(this);
|
|
}
|
|
} else {
|
|
print("WARNING: could not find newState " + newState + " in state machine");
|
|
}
|
|
};
|
|
|
|
this.debugLine = function (closePoint, farPoint, color) {
|
|
Entities.addEntity({
|
|
type: "Line",
|
|
name: "Grab Debug Entity",
|
|
dimensions: LINE_ENTITY_DIMENSIONS,
|
|
visible: true,
|
|
position: closePoint,
|
|
linePoints: [ZERO_VEC, farPoint],
|
|
color: color,
|
|
lifetime: 0.1,
|
|
dynamic: false,
|
|
ignoreForCollisions: true,
|
|
userData: JSON.stringify({
|
|
grabbableKey: {
|
|
grabbable: false
|
|
}
|
|
})
|
|
});
|
|
};
|
|
|
|
this.lineOn = function (closePoint, farPoint, color) {
|
|
// draw a line
|
|
if (this.pointer === null) {
|
|
this.pointer = Entities.addEntity({
|
|
type: "Line",
|
|
name: "grab pointer",
|
|
dimensions: LINE_ENTITY_DIMENSIONS,
|
|
visible: true,
|
|
position: closePoint,
|
|
linePoints: [ZERO_VEC, farPoint],
|
|
color: color,
|
|
lifetime: LIFETIME,
|
|
dynamic: false,
|
|
ignoreForCollisions: true,
|
|
userData: JSON.stringify({
|
|
grabbableKey: {
|
|
grabbable: false
|
|
}
|
|
})
|
|
});
|
|
} else {
|
|
var age = Entities.getEntityProperties(this.pointer, "age").age;
|
|
this.pointer = Entities.editEntity(this.pointer, {
|
|
position: closePoint,
|
|
linePoints: [ZERO_VEC, farPoint],
|
|
color: color,
|
|
lifetime: age + LIFETIME
|
|
});
|
|
}
|
|
};
|
|
|
|
var SEARCH_SPHERE_ALPHA = 0.5;
|
|
this.searchSphereOn = function (location, size, color) {
|
|
if (this.searchSphere === null) {
|
|
var sphereProperties = {
|
|
position: location,
|
|
size: size,
|
|
color: color,
|
|
alpha: SEARCH_SPHERE_ALPHA,
|
|
solid: true,
|
|
ignoreRayIntersection: true,
|
|
drawInFront: true, // Even when burried inside of something, show it.
|
|
visible: true
|
|
};
|
|
this.searchSphere = Overlays.addOverlay("sphere", sphereProperties);
|
|
} else {
|
|
Overlays.editOverlay(this.searchSphere, {
|
|
position: location,
|
|
size: size,
|
|
color: color,
|
|
visible: true
|
|
});
|
|
}
|
|
};
|
|
|
|
this.overlayLineOn = function (closePoint, farPoint, color) {
|
|
if (this.overlayLine === null) {
|
|
var lineProperties = {
|
|
glow: 1.0,
|
|
start: closePoint,
|
|
end: farPoint,
|
|
color: color,
|
|
ignoreRayIntersection: true, // always ignore this
|
|
drawInFront: true, // Even when burried inside of something, show it.
|
|
visible: true,
|
|
alpha: 1
|
|
};
|
|
this.overlayLine = Overlays.addOverlay("line3d", lineProperties);
|
|
|
|
} else {
|
|
Overlays.editOverlay(this.overlayLine, {
|
|
lineWidth: 5,
|
|
start: closePoint,
|
|
end: farPoint,
|
|
color: color,
|
|
visible: true,
|
|
ignoreRayIntersection: true, // always ignore this
|
|
drawInFront: true, // Even when burried inside of something, show it.
|
|
alpha: 1
|
|
});
|
|
}
|
|
};
|
|
|
|
this.searchIndicatorOn = function (distantPickRay) {
|
|
var handPosition = distantPickRay.origin;
|
|
var SEARCH_SPHERE_SIZE = 0.011;
|
|
var SEARCH_SPHERE_FOLLOW_RATE = 0.50;
|
|
|
|
if (this.intersectionDistance > 0) {
|
|
// If we hit something with our pick ray, move the search sphere toward that distance
|
|
this.searchSphereDistance = this.searchSphereDistance * SEARCH_SPHERE_FOLLOW_RATE +
|
|
this.intersectionDistance * (1.0 - SEARCH_SPHERE_FOLLOW_RATE);
|
|
}
|
|
|
|
var searchSphereLocation = Vec3.sum(distantPickRay.origin,
|
|
Vec3.multiply(distantPickRay.direction, this.searchSphereDistance));
|
|
this.searchSphereOn(searchSphereLocation, SEARCH_SPHERE_SIZE * this.searchSphereDistance,
|
|
(this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR);
|
|
if ((USE_OVERLAY_LINES_FOR_SEARCHING === true) && PICK_WITH_HAND_RAY) {
|
|
this.overlayLineOn(handPosition, searchSphereLocation,
|
|
(this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR);
|
|
}
|
|
};
|
|
|
|
this.handleDistantParticleBeam = function (handPosition, objectPosition, color) {
|
|
|
|
var handToObject = Vec3.subtract(objectPosition, handPosition);
|
|
var finalRotationObject = Quat.rotationBetween(Vec3.multiply(-1, Vec3.UP), handToObject);
|
|
var distance = Vec3.distance(handPosition, objectPosition);
|
|
var speed = distance * 3;
|
|
var spread = 0;
|
|
var lifespan = distance / speed;
|
|
|
|
if (this.particleBeamObject === null) {
|
|
this.createParticleBeam(objectPosition, finalRotationObject, color, speed, spread, lifespan);
|
|
} else {
|
|
this.updateParticleBeam(objectPosition, finalRotationObject, color, speed, spread, lifespan);
|
|
}
|
|
};
|
|
|
|
this.createParticleBeam = function (positionObject, orientationObject, color, speed, spread, lifespan) {
|
|
|
|
var particleBeamPropertiesObject = {
|
|
type: "ParticleEffect",
|
|
isEmitting: true,
|
|
position: positionObject,
|
|
visible: false,
|
|
lifetime: 60,
|
|
"name": "Particle Beam",
|
|
"color": color,
|
|
"maxParticles": 2000,
|
|
"lifespan": lifespan,
|
|
"emitRate": 1000,
|
|
"emitSpeed": speed,
|
|
"speedSpread": spread,
|
|
"emitOrientation": {
|
|
"x": -1,
|
|
"y": 0,
|
|
"z": 0,
|
|
"w": 1
|
|
},
|
|
"emitDimensions": {
|
|
"x": 0,
|
|
"y": 0,
|
|
"z": 0
|
|
},
|
|
"emitRadiusStart": 0.5,
|
|
"polarStart": 0,
|
|
"polarFinish": 0,
|
|
"azimuthStart": -3.1415927410125732,
|
|
"azimuthFinish": 3.1415927410125732,
|
|
"emitAcceleration": {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
"accelerationSpread": {
|
|
"x": 0,
|
|
"y": 0,
|
|
"z": 0
|
|
},
|
|
"particleRadius": 0.015,
|
|
"radiusSpread": 0.005,
|
|
"alpha": 1,
|
|
"alphaSpread": 0,
|
|
"alphaStart": 1,
|
|
"alphaFinish": 1,
|
|
"additiveBlending": 0,
|
|
"textures": "https://hifi-content.s3.amazonaws.com/alan/dev/textures/grabsprite-3.png"
|
|
};
|
|
|
|
this.particleBeamObject = Entities.addEntity(particleBeamPropertiesObject);
|
|
};
|
|
|
|
this.updateParticleBeam = function (positionObject, orientationObject, color, speed, spread, lifespan) {
|
|
Entities.editEntity(this.particleBeamObject, {
|
|
rotation: orientationObject,
|
|
position: positionObject,
|
|
visible: true,
|
|
color: color,
|
|
emitSpeed: speed,
|
|
speedSpread: spread,
|
|
lifespan: lifespan
|
|
});
|
|
};
|
|
|
|
this.evalLightWorldTransform = function (modelPos, modelRot) {
|
|
|
|
var MODEL_LIGHT_POSITION = {
|
|
x: 0,
|
|
y: -0.3,
|
|
z: 0
|
|
};
|
|
|
|
var MODEL_LIGHT_ROTATION = Quat.angleAxis(-90, {
|
|
x: 1,
|
|
y: 0,
|
|
z: 0
|
|
});
|
|
|
|
return {
|
|
p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, MODEL_LIGHT_POSITION)),
|
|
q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION)
|
|
};
|
|
};
|
|
|
|
this.handleSpotlight = function (parentID) {
|
|
var LIFETIME = 100;
|
|
|
|
var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']);
|
|
|
|
var lightTransform = this.evalLightWorldTransform(modelProperties.position, modelProperties.rotation);
|
|
var lightProperties = {
|
|
type: "Light",
|
|
isSpotlight: true,
|
|
dimensions: {
|
|
x: 2,
|
|
y: 2,
|
|
z: 20
|
|
},
|
|
parentID: parentID,
|
|
color: {
|
|
red: 255,
|
|
green: 255,
|
|
blue: 255
|
|
},
|
|
intensity: 2,
|
|
exponent: 0.3,
|
|
cutoff: 20,
|
|
lifetime: LIFETIME,
|
|
position: lightTransform.p
|
|
};
|
|
|
|
if (this.spotlight === null) {
|
|
this.spotlight = Entities.addEntity(lightProperties);
|
|
} else {
|
|
Entities.editEntity(this.spotlight, {
|
|
// without this, this light would maintain rotation with its parent
|
|
rotation: Quat.fromPitchYawRollDegrees(-90, 0, 0)
|
|
});
|
|
}
|
|
};
|
|
|
|
this.handlePointLight = function (parentID) {
|
|
var LIFETIME = 100;
|
|
|
|
var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']);
|
|
var lightTransform = this.evalLightWorldTransform(modelProperties.position, modelProperties.rotation);
|
|
|
|
var lightProperties = {
|
|
type: "Light",
|
|
isSpotlight: false,
|
|
dimensions: {
|
|
x: 2,
|
|
y: 2,
|
|
z: 20
|
|
},
|
|
parentID: parentID,
|
|
color: {
|
|
red: 255,
|
|
green: 255,
|
|
blue: 255
|
|
},
|
|
intensity: 2,
|
|
exponent: 0.3,
|
|
cutoff: 20,
|
|
lifetime: LIFETIME,
|
|
position: lightTransform.p
|
|
};
|
|
|
|
if (this.pointlight === null) {
|
|
this.pointlight = Entities.addEntity(lightProperties);
|
|
}
|
|
};
|
|
|
|
this.lineOff = function () {
|
|
if (this.pointer !== null) {
|
|
Entities.deleteEntity(this.pointer);
|
|
}
|
|
this.pointer = null;
|
|
};
|
|
|
|
this.overlayLineOff = function () {
|
|
if (this.overlayLine !== null) {
|
|
Overlays.deleteOverlay(this.overlayLine);
|
|
}
|
|
this.overlayLine = null;
|
|
};
|
|
|
|
this.searchSphereOff = function () {
|
|
if (this.searchSphere !== null) {
|
|
Overlays.deleteOverlay(this.searchSphere);
|
|
this.searchSphere = null;
|
|
this.searchSphereDistance = DEFAULT_SEARCH_SPHERE_DISTANCE;
|
|
this.intersectionDistance = 0.0;
|
|
}
|
|
};
|
|
|
|
this.particleBeamOff = function () {
|
|
if (this.particleBeamObject !== null) {
|
|
Entities.deleteEntity(this.particleBeamObject);
|
|
this.particleBeamObject = null;
|
|
}
|
|
};
|
|
|
|
this.turnLightsOff = function () {
|
|
if (this.spotlight !== null) {
|
|
Entities.deleteEntity(this.spotlight);
|
|
this.spotlight = null;
|
|
}
|
|
|
|
if (this.pointlight !== null) {
|
|
Entities.deleteEntity(this.pointlight);
|
|
this.pointlight = null;
|
|
}
|
|
};
|
|
|
|
this.turnOffVisualizations = function () {
|
|
if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) {
|
|
this.lineOff();
|
|
}
|
|
|
|
if (USE_OVERLAY_LINES_FOR_SEARCHING === true || USE_OVERLAY_LINES_FOR_MOVING === true) {
|
|
this.overlayLineOff();
|
|
}
|
|
|
|
if (USE_PARTICLE_BEAM_FOR_MOVING === true) {
|
|
this.particleBeamOff();
|
|
}
|
|
this.searchSphereOff();
|
|
restore2DMode();
|
|
|
|
};
|
|
|
|
this.triggerPress = function (value) {
|
|
_this.rawTriggerValue = value;
|
|
};
|
|
|
|
this.secondaryPress = function (value) {
|
|
_this.rawSecondaryValue = value;
|
|
};
|
|
|
|
this.updateSmoothedTrigger = function () {
|
|
var triggerValue = this.rawTriggerValue;
|
|
// smooth out trigger value
|
|
this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) +
|
|
(triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO));
|
|
};
|
|
|
|
this.triggerSmoothedGrab = function () {
|
|
return this.triggerValue > TRIGGER_GRAB_VALUE;
|
|
};
|
|
|
|
this.triggerSmoothedSqueezed = function () {
|
|
return this.triggerValue > TRIGGER_ON_VALUE;
|
|
};
|
|
|
|
this.triggerSmoothedReleased = function () {
|
|
return this.triggerValue < TRIGGER_OFF_VALUE;
|
|
};
|
|
|
|
this.secondarySqueezed = function () {
|
|
return _this.rawSecondaryValue > BUMPER_ON_VALUE;
|
|
};
|
|
|
|
this.secondaryReleased = function () {
|
|
return _this.rawSecondaryValue < BUMPER_ON_VALUE;
|
|
};
|
|
|
|
// this.triggerOrsecondarySqueezed = function () {
|
|
// return triggerSmoothedSqueezed() || secondarySqueezed();
|
|
// }
|
|
|
|
// this.triggerAndSecondaryReleased = function () {
|
|
// return triggerSmoothedReleased() && secondaryReleased();
|
|
// }
|
|
|
|
this.thumbPress = function (value) {
|
|
_this.rawThumbValue = value;
|
|
};
|
|
|
|
this.thumbPressed = function () {
|
|
return _this.rawThumbValue > THUMB_ON_VALUE;
|
|
};
|
|
|
|
this.thumbReleased = function () {
|
|
return _this.rawThumbValue < THUMB_ON_VALUE;
|
|
};
|
|
|
|
this.off = function () {
|
|
if (this.triggerSmoothedReleased()) {
|
|
this.waitForTriggerRelease = false;
|
|
}
|
|
if (!this.waitForTriggerRelease && this.triggerSmoothedSqueezed()) {
|
|
this.lastPickTime = 0;
|
|
var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
|
|
this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation;
|
|
if (this.triggerSmoothedSqueezed()) {
|
|
this.setState(STATE_SEARCHING, "trigger squeeze detected");
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.entityPropertyCache.clear();
|
|
this.entityPropertyCache.findEntities(this.getHandPosition(), EQUIP_HOTSPOT_RENDER_RADIUS);
|
|
var candidateEntities = this.entityPropertyCache.getEntities();
|
|
|
|
var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities);
|
|
if (!this.waitForTriggerRelease) {
|
|
this.updateEquipHaptics(potentialEquipHotspot);
|
|
}
|
|
|
|
var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS);
|
|
this.updateEquipHotspotRendering(nearEquipHotspots, potentialEquipHotspot);
|
|
};
|
|
|
|
this.clearEquipHaptics = function () {
|
|
this.prevPotentialEquipHotspot = null;
|
|
};
|
|
|
|
this.updateEquipHaptics = function (potentialEquipHotspot) {
|
|
if (potentialEquipHotspot && !this.prevPotentialEquipHotspot ||
|
|
!potentialEquipHotspot && this.prevPotentialEquipHotspot) {
|
|
Controller.triggerShortHapticPulse(0.5, this.hand);
|
|
}
|
|
this.prevPotentialEquipHotspot = potentialEquipHotspot;
|
|
};
|
|
|
|
this.clearEquipHotspotRendering = function () {
|
|
var keys = Object.keys(this.equipOverlayInfoSetMap);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var overlayInfoSet = this.equipOverlayInfoSetMap[keys[i]];
|
|
this.deleteOverlayInfoSet(overlayInfoSet);
|
|
}
|
|
this.equipOverlayInfoSetMap = {};
|
|
};
|
|
|
|
this.createOverlayInfoSet = function (hotspot, timestamp) {
|
|
var overlayInfoSet = {
|
|
timestamp: timestamp,
|
|
entityID: hotspot.entityID,
|
|
localPosition: hotspot.localPosition,
|
|
hotspot: hotspot,
|
|
overlays: []
|
|
};
|
|
|
|
var diameter = hotspot.radius * 2;
|
|
|
|
overlayInfoSet.overlays.push(Overlays.addOverlay("sphere", {
|
|
position: hotspot.worldPosition,
|
|
rotation: {x: 0, y: 0, z: 0, w: 1},
|
|
dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR,
|
|
color: EQUIP_SPHERE_COLOR,
|
|
alpha: EQUIP_SPHERE_ALPHA,
|
|
solid: true,
|
|
visible: true,
|
|
ignoreRayIntersection: true,
|
|
drawInFront: false
|
|
}));
|
|
|
|
return overlayInfoSet;
|
|
};
|
|
|
|
this.updateOverlayInfoSet = function (overlayInfoSet, timestamp, potentialEquipHotspot) {
|
|
overlayInfoSet.timestamp = timestamp;
|
|
|
|
var diameter = overlayInfoSet.hotspot.radius * 2;
|
|
|
|
// embiggen the overlays if it maches the potentialEquipHotspot
|
|
if (potentialEquipHotspot && overlayInfoSet.entityID == potentialEquipHotspot.entityID &&
|
|
Vec3.equal(overlayInfoSet.localPosition, potentialEquipHotspot.localPosition)) {
|
|
diameter = diameter * EQUIP_RADIUS_EMBIGGEN_FACTOR;
|
|
}
|
|
|
|
var props = _this.entityPropertyCache.getProps(overlayInfoSet.entityID);
|
|
var entityXform = new Xform(props.rotation, props.position);
|
|
var position = entityXform.xformPoint(overlayInfoSet.localPosition);
|
|
|
|
overlayInfoSet.overlays.forEach(function (overlay) {
|
|
Overlays.editOverlay(overlay, {
|
|
position: position,
|
|
rotation: props.rotation,
|
|
dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR
|
|
});
|
|
});
|
|
};
|
|
|
|
this.deleteOverlayInfoSet = function (overlayInfoSet) {
|
|
overlayInfoSet.overlays.forEach(function (overlay) {
|
|
Overlays.deleteOverlay(overlay);
|
|
});
|
|
};
|
|
|
|
this.updateEquipHotspotRendering = function (hotspots, potentialEquipHotspot) {
|
|
var now = Date.now();
|
|
var _this = this;
|
|
|
|
hotspots.forEach(function (hotspot) {
|
|
var overlayInfoSet = _this.equipOverlayInfoSetMap[hotspot.key];
|
|
if (overlayInfoSet) {
|
|
_this.updateOverlayInfoSet(overlayInfoSet, now, potentialEquipHotspot);
|
|
} else {
|
|
_this.equipOverlayInfoSetMap[hotspot.key] = _this.createOverlayInfoSet(hotspot, now);
|
|
}
|
|
});
|
|
|
|
// delete sets with old timestamps.
|
|
var keys = Object.keys(this.equipOverlayInfoSetMap);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var overlayInfoSet = this.equipOverlayInfoSetMap[keys[i]];
|
|
if (overlayInfoSet.timestamp !== now) {
|
|
this.deleteOverlayInfoSet(overlayInfoSet);
|
|
delete this.equipOverlayInfoSetMap[keys[i]];
|
|
}
|
|
}
|
|
};
|
|
|
|
// Performs ray pick test from the hand controller into the world
|
|
// @param {number} which hand to use, RIGHT_HAND or LEFT_HAND
|
|
// @returns {object} returns object with two keys entityID and distance
|
|
//
|
|
this.calcRayPickInfo = function (hand) {
|
|
|
|
var standardControllerValue = (hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
|
|
var pose = Controller.getPoseValue(standardControllerValue);
|
|
var worldHandPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position);
|
|
var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation);
|
|
|
|
var pickRay = {
|
|
origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position,
|
|
direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation),
|
|
Quat.getFront(Camera.orientation),
|
|
HAND_HEAD_MIX_RATIO),
|
|
length: PICK_MAX_DISTANCE
|
|
};
|
|
|
|
var result = {
|
|
entityID: null,
|
|
searchRay: pickRay,
|
|
distance: PICK_MAX_DISTANCE
|
|
};
|
|
|
|
var now = Date.now();
|
|
if (now - this.lastPickTime < MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) {
|
|
return result;
|
|
}
|
|
this.lastPickTime = now;
|
|
|
|
var directionNormalized = Vec3.normalize(pickRay.direction);
|
|
var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE);
|
|
var pickRayBacked = {
|
|
origin: Vec3.subtract(pickRay.origin, directionBacked),
|
|
direction: pickRay.direction
|
|
};
|
|
|
|
var intersection;
|
|
if (USE_BLACKLIST === true && blacklist.length !== 0) {
|
|
intersection = findRayIntersection(pickRayBacked, true, [], blacklist);
|
|
} else {
|
|
intersection = findRayIntersection(pickRayBacked, true);
|
|
}
|
|
|
|
if (intersection.intersects) {
|
|
return {
|
|
entityID: intersection.entityID,
|
|
overlayID: intersection.overlayID,
|
|
searchRay: pickRay,
|
|
distance: Vec3.distance(pickRay.origin, intersection.intersection)
|
|
};
|
|
} else {
|
|
return result;
|
|
}
|
|
};
|
|
|
|
this.entityWantsTrigger = function (entityID) {
|
|
var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID);
|
|
return grabbableProps && grabbableProps.wantsTrigger;
|
|
};
|
|
|
|
// returns a list of all equip-hotspots assosiated with this entity.
|
|
// @param {UUID} entityID
|
|
// @returns {Object[]} array of objects with the following fields.
|
|
// * key {string} a string that can be used to uniquely identify this hotspot
|
|
// * entityID {UUID}
|
|
// * localPosition {Vec3} position of the hotspot in object space.
|
|
// * worldPosition {vec3} position of the hotspot in world space.
|
|
// * radius {number} radius of equip hotspot
|
|
// * joints {Object} keys are joint names values are arrays of two elements:
|
|
// offset position {Vec3} and offset rotation {Quat}, both are in the coordinate system of the joint.
|
|
this.collectEquipHotspots = function (entityID) {
|
|
var result = [];
|
|
var props = this.entityPropertyCache.getProps(entityID);
|
|
var entityXform = new Xform(props.rotation, props.position);
|
|
var equipHotspotsProps = this.entityPropertyCache.getEquipHotspotsProps(entityID);
|
|
if (equipHotspotsProps && equipHotspotsProps.length > 0) {
|
|
var i, length = equipHotspotsProps.length;
|
|
for (i = 0; i < length; i++) {
|
|
var hotspot = equipHotspotsProps[i];
|
|
if (hotspot.position && hotspot.radius && hotspot.joints) {
|
|
result.push({
|
|
key: entityID.toString() + i.toString(),
|
|
entityID: entityID,
|
|
localPosition: hotspot.position,
|
|
worldPosition: entityXform.xformPoint(hotspot.position),
|
|
radius: hotspot.radius,
|
|
joints: hotspot.joints
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
var wearableProps = this.entityPropertyCache.getWearableProps(entityID);
|
|
if (wearableProps && wearableProps.joints) {
|
|
result.push({
|
|
key: entityID.toString() + "0",
|
|
entityID: entityID,
|
|
localPosition: {x: 0, y: 0, z: 0},
|
|
worldPosition: entityXform.pos,
|
|
radius: EQUIP_RADIUS,
|
|
joints: wearableProps.joints
|
|
});
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
this.hotspotIsEquippable = function (hotspot) {
|
|
var props = this.entityPropertyCache.getProps(hotspot.entityID);
|
|
var grabProps = this.entityPropertyCache.getGrabProps(hotspot.entityID);
|
|
var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME);
|
|
|
|
var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0;
|
|
var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING ||
|
|
this.getOtherHandController().state == STATE_DISTANCE_HOLDING) &&
|
|
this.getOtherHandController().grabbedEntity == hotspot.entityID);
|
|
if (refCount > 0 && !okToEquipFromOtherHand) {
|
|
if (debug) {
|
|
print("equip is skipping '" + props.name + "': grabbed by someone else");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
this.entityIsGrabbable = function (entityID) {
|
|
var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID);
|
|
var grabProps = this.entityPropertyCache.getGrabProps(entityID);
|
|
var props = this.entityPropertyCache.getProps(entityID);
|
|
var physical = propsArePhysical(props);
|
|
var grabbable = false;
|
|
var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME);
|
|
|
|
if (physical) {
|
|
// physical things default to grabbable
|
|
grabbable = true;
|
|
} else {
|
|
// non-physical things default to non-grabbable unless they are already grabbed
|
|
if ("refCount" in grabProps && grabProps.refCount > 0) {
|
|
grabbable = true;
|
|
} else {
|
|
grabbable = false;
|
|
}
|
|
}
|
|
|
|
if (grabbableProps.hasOwnProperty("grabbable")) {
|
|
grabbable = grabbableProps.grabbable;
|
|
}
|
|
|
|
if (!grabbable && !grabbableProps.wantsTrigger) {
|
|
if (debug) {
|
|
print("grab is skipping '" + props.name + "': not grabbable.");
|
|
}
|
|
return false;
|
|
}
|
|
if (FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0) {
|
|
if (debug) {
|
|
print("grab is skipping '" + props.name + "': forbidden entity type.");
|
|
}
|
|
return false;
|
|
}
|
|
if (props.locked && !grabbableProps.wantsTrigger) {
|
|
if (debug) {
|
|
print("grab is skipping '" + props.name + "': locked and not triggerable.");
|
|
}
|
|
return false;
|
|
}
|
|
if (FORBIDDEN_GRAB_NAMES.indexOf(props.name) >= 0) {
|
|
if (debug) {
|
|
print("grab is skipping '" + props.name + "': forbidden name.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
this.entityIsDistanceGrabbable = function (entityID, handPosition) {
|
|
if (!this.entityIsGrabbable(entityID)) {
|
|
return false;
|
|
}
|
|
|
|
var props = this.entityPropertyCache.getProps(entityID);
|
|
var distance = Vec3.distance(props.position, handPosition);
|
|
var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME);
|
|
|
|
// we can't distance-grab non-physical
|
|
var isPhysical = propsArePhysical(props);
|
|
if (!isPhysical) {
|
|
if (debug) {
|
|
print("distance grab is skipping '" + props.name + "': not physical");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (distance > PICK_MAX_DISTANCE) {
|
|
// too far away, don't grab
|
|
if (debug) {
|
|
print("distance grab is skipping '" + props.name + "': too far away.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (entityIsGrabbedByOther(entityID)) {
|
|
// don't distance grab something that is already grabbed.
|
|
if (debug) {
|
|
print("distance grab is skipping '" + props.name + "': already grabbed by another.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
this.entityIsNearGrabbable = function (entityID, handPosition, maxDistance) {
|
|
|
|
if (!this.entityIsGrabbable(entityID)) {
|
|
return false;
|
|
}
|
|
|
|
var props = this.entityPropertyCache.getProps(entityID);
|
|
var distance = Vec3.distance(props.position, handPosition);
|
|
var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME);
|
|
|
|
if (distance > maxDistance) {
|
|
// too far away, don't grab
|
|
if (debug) {
|
|
print(" grab is skipping '" + props.name + "': too far away.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
this.chooseNearEquipHotspots = function (candidateEntities, distance) {
|
|
var equippableHotspots = flatten(candidateEntities.map(function (entityID) {
|
|
return _this.collectEquipHotspots(entityID);
|
|
})).filter(function (hotspot) {
|
|
return (_this.hotspotIsEquippable(hotspot) &&
|
|
Vec3.distance(hotspot.worldPosition, _this.getHandPosition()) < hotspot.radius + distance);
|
|
});
|
|
return equippableHotspots;
|
|
};
|
|
|
|
this.chooseBestEquipHotspot = function (candidateEntities) {
|
|
var DISTANCE = 0;
|
|
var equippableHotspots = this.chooseNearEquipHotspots(candidateEntities, DISTANCE);
|
|
if (equippableHotspots.length > 0) {
|
|
// sort by distance
|
|
equippableHotspots.sort(function (a, b) {
|
|
var aDistance = Vec3.distance(a.worldPosition, this.getHandPosition());
|
|
var bDistance = Vec3.distance(b.worldPosition, this.getHandPosition());
|
|
return aDistance - bDistance;
|
|
});
|
|
return equippableHotspots[0];
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
this.search = function () {
|
|
var _this = this;
|
|
var name;
|
|
|
|
this.grabbedEntity = null;
|
|
this.isInitialGrab = false;
|
|
this.shouldResetParentOnRelease = false;
|
|
|
|
this.checkForStrayChildren();
|
|
|
|
if (this.triggerSmoothedReleased()) {
|
|
this.setState(STATE_OFF, "trigger released");
|
|
return;
|
|
}
|
|
|
|
var handPosition = this.getHandPosition();
|
|
|
|
this.entityPropertyCache.clear();
|
|
this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS);
|
|
var candidateEntities = this.entityPropertyCache.getEntities();
|
|
|
|
var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities);
|
|
if (potentialEquipHotspot) {
|
|
if (this.triggerSmoothedGrab()) {
|
|
this.grabbedHotspot = potentialEquipHotspot;
|
|
this.grabbedEntity = potentialEquipHotspot.entityID;
|
|
this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(this.grabbedEntity).name + "'");
|
|
return;
|
|
}
|
|
}
|
|
|
|
var grabbableEntities = candidateEntities.filter(function (entity) {
|
|
return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_MAX_DISTANCE);
|
|
});
|
|
|
|
var rayPickInfo = this.calcRayPickInfo(this.hand);
|
|
if (rayPickInfo.entityID) {
|
|
this.intersectionDistance = rayPickInfo.distance;
|
|
this.entityPropertyCache.updateEntity(rayPickInfo.entityID);
|
|
if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) {
|
|
grabbableEntities.push(rayPickInfo.entityID);
|
|
}
|
|
} else if (rayPickInfo.overlayID) {
|
|
this.intersectionDistance = rayPickInfo.distance;
|
|
} else {
|
|
this.intersectionDistance = 0;
|
|
}
|
|
|
|
var entity;
|
|
if (grabbableEntities.length > 0) {
|
|
// sort by distance
|
|
grabbableEntities.sort(function (a, b) {
|
|
var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition);
|
|
var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition);
|
|
return aDistance - bDistance;
|
|
});
|
|
entity = grabbableEntities[0];
|
|
name = this.entityPropertyCache.getProps(entity).name;
|
|
this.grabbedEntity = entity;
|
|
if (this.entityWantsTrigger(entity)) {
|
|
if (this.triggerSmoothedGrab()) {
|
|
this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'");
|
|
return;
|
|
} else {
|
|
// potentialNearTriggerEntity = entity;
|
|
}
|
|
} else {
|
|
if (this.triggerSmoothedGrab()) {
|
|
var props = this.entityPropertyCache.getProps(entity);
|
|
var grabProps = this.entityPropertyCache.getGrabProps(entity);
|
|
var refCount = grabProps.refCount ? grabProps.refCount : 0;
|
|
if (refCount >= 1) {
|
|
// if another person is holding the object, remember to restore the
|
|
// parent info, when we are finished grabbing it.
|
|
this.shouldResetParentOnRelease = true;
|
|
this.previousParentID = props.parentID;
|
|
this.previousParentJointIndex = props.parentJointIndex;
|
|
}
|
|
|
|
this.setState(STATE_NEAR_GRABBING, "near grab '" + name + "'");
|
|
return;
|
|
} else {
|
|
// potentialNearGrabEntity = entity;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rayPickInfo.entityID) {
|
|
entity = rayPickInfo.entityID;
|
|
name = this.entityPropertyCache.getProps(entity).name;
|
|
if (this.entityWantsTrigger(entity)) {
|
|
if (this.triggerSmoothedGrab()) {
|
|
this.grabbedEntity = entity;
|
|
this.setState(STATE_FAR_TRIGGER, "far trigger '" + name + "'");
|
|
return;
|
|
} else {
|
|
// potentialFarTriggerEntity = entity;
|
|
}
|
|
} else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) {
|
|
if (this.triggerSmoothedGrab() && !isEditing()) {
|
|
this.grabbedEntity = entity;
|
|
this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'");
|
|
return;
|
|
} else {
|
|
// potentialFarGrabEntity = entity;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.updateEquipHaptics(potentialEquipHotspot);
|
|
|
|
var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS);
|
|
this.updateEquipHotspotRendering(nearEquipHotspots, potentialEquipHotspot);
|
|
|
|
// search line visualizations
|
|
if (USE_ENTITY_LINES_FOR_SEARCHING === true) {
|
|
this.lineOn(rayPickInfo.searchRay.origin,
|
|
Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH),
|
|
NO_INTERSECT_COLOR);
|
|
}
|
|
|
|
this.searchIndicatorOn(rayPickInfo.searchRay);
|
|
Reticle.setVisible(false);
|
|
};
|
|
|
|
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.distanceHoldingEnter = function () {
|
|
|
|
this.clearEquipHotspotRendering();
|
|
this.clearEquipHaptics();
|
|
|
|
// controller pose is in avatar frame
|
|
var avatarControllerPose =
|
|
Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand);
|
|
|
|
// transform it into world frame
|
|
var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation);
|
|
var controllerPosition = Vec3.sum(MyAvatar.position, controllerPositionVSAvatar);
|
|
var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation);
|
|
|
|
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES);
|
|
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 = Vec3.distance(this.currentObjectPosition, controllerPosition);
|
|
this.grabRadialVelocity = 0.0;
|
|
|
|
// compute a constant based on the initial conditions which we use below to exagerate 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.actionID = NULL_UUID;
|
|
this.actionID = Entities.addAction("spring", this.grabbedEntity, {
|
|
targetPosition: this.currentObjectPosition,
|
|
linearTimeScale: timeScale,
|
|
targetRotation: this.currentObjectRotation,
|
|
angularTimeScale: timeScale,
|
|
tag: getTag(),
|
|
ttl: ACTION_TTL
|
|
});
|
|
if (this.actionID === NULL_UUID) {
|
|
this.actionID = null;
|
|
}
|
|
this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
|
|
|
|
if (this.actionID !== null) {
|
|
this.activateEntity(this.grabbedEntity, grabbedProperties, false);
|
|
this.callEntityMethodOnGrabbed("startDistanceGrab");
|
|
}
|
|
|
|
this.turnOffVisualizations();
|
|
|
|
this.previousControllerPositionVSAvatar = controllerPositionVSAvatar;
|
|
this.previousControllerRotation = controllerRotation;
|
|
};
|
|
|
|
this.distanceHolding = function () {
|
|
if (this.triggerSmoothedReleased()) {
|
|
this.callEntityMethodOnGrabbed("releaseGrab");
|
|
this.setState(STATE_OFF, "trigger released");
|
|
return;
|
|
}
|
|
|
|
this.heartBeat(this.grabbedEntity);
|
|
|
|
// controller pose is in avatar frame
|
|
var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ?
|
|
Controller.Standard.RightHand : Controller.Standard.LeftHand);
|
|
|
|
// transform it into world frame
|
|
var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation);
|
|
var controllerPosition = Vec3.sum(MyAvatar.position, controllerPositionVSAvatar);
|
|
var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation);
|
|
|
|
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_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, controllerPosition) *
|
|
this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR;
|
|
if (radius < 1.0) {
|
|
radius = 1.0;
|
|
}
|
|
|
|
// scale delta controller hand movement by radius.
|
|
var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar),
|
|
radius);
|
|
|
|
/// double delta controller rotation
|
|
// var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did
|
|
// var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation,
|
|
// controllerRotation,
|
|
// DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR),
|
|
// Quat.inverse(this.previousControllerRotation));
|
|
|
|
// update the currentObject position and rotation.
|
|
this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved);
|
|
// this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation);
|
|
|
|
this.callEntityMethodOnGrabbed("continueDistantGrab");
|
|
|
|
var defaultMoveWithHeadData = {
|
|
disableMoveWithHead: false
|
|
};
|
|
|
|
var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData);
|
|
|
|
// Update radialVelocity
|
|
var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar);
|
|
lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaObjectTime);
|
|
var newRadialVelocity = Vec3.dot(lastVelocity,
|
|
Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition)));
|
|
|
|
var VELOCITY_AVERAGING_TIME = 0.016;
|
|
this.grabRadialVelocity = (deltaObjectTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity +
|
|
(1.0 - (deltaObjectTime / VELOCITY_AVERAGING_TIME)) * this.grabRadialVelocity;
|
|
|
|
var RADIAL_GRAB_AMPLIFIER = 10.0;
|
|
if (Math.abs(this.grabRadialVelocity) > 0.0) {
|
|
this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime *
|
|
this.grabRadius * RADIAL_GRAB_AMPLIFIER);
|
|
}
|
|
|
|
var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation));
|
|
newTargetPosition = Vec3.sum(newTargetPosition, controllerPosition);
|
|
|
|
|
|
var objectToAvatar = Vec3.subtract(this.currentObjectPosition, MyAvatar.position);
|
|
if (handControllerData.disableMoveWithHead !== true) {
|
|
// mix in head motion
|
|
if (MOVE_WITH_HEAD) {
|
|
var objDistance = Vec3.length(objectToAvatar);
|
|
var before = Vec3.multiplyQbyV(this.currentCameraOrientation, {
|
|
x: 0.0,
|
|
y: 0.0,
|
|
z: objDistance
|
|
});
|
|
var after = Vec3.multiplyQbyV(Camera.orientation, {
|
|
x: 0.0,
|
|
y: 0.0,
|
|
z: objDistance
|
|
});
|
|
var change = Vec3.multiply(Vec3.subtract(before, after), HAND_HEAD_MIX_RATIO);
|
|
this.currentCameraOrientation = Camera.orientation;
|
|
this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, change);
|
|
}
|
|
}
|
|
|
|
var handPosition = this.getHandPosition();
|
|
|
|
// visualizations
|
|
if (USE_ENTITY_LINES_FOR_MOVING === true) {
|
|
this.lineOn(handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR);
|
|
}
|
|
if (USE_OVERLAY_LINES_FOR_MOVING === true) {
|
|
this.overlayLineOn(handPosition, grabbedProperties.position, INTERSECT_COLOR);
|
|
}
|
|
if (USE_PARTICLE_BEAM_FOR_MOVING === true) {
|
|
this.handleDistantParticleBeam(handPosition, grabbedProperties.position, INTERSECT_COLOR);
|
|
}
|
|
if (USE_POINTLIGHT === true) {
|
|
this.handlePointLight(this.grabbedEntity);
|
|
}
|
|
if (USE_SPOTLIGHT === true) {
|
|
this.handleSpotlight(this.grabbedEntity);
|
|
}
|
|
|
|
var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition));
|
|
var success = Entities.updateAction(this.grabbedEntity, this.actionID, {
|
|
targetPosition: newTargetPosition,
|
|
linearTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
|
|
targetRotation: this.currentObjectRotation,
|
|
angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
|
|
ttl: ACTION_TTL
|
|
});
|
|
if (success) {
|
|
this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
|
|
} else {
|
|
print("continueDistanceHolding -- updateAction failed");
|
|
}
|
|
|
|
this.previousControllerPositionVSAvatar = controllerPositionVSAvatar;
|
|
this.previousControllerRotation = controllerRotation;
|
|
};
|
|
|
|
this.setupHoldAction = function () {
|
|
this.actionID = Entities.addAction("hold", this.grabbedEntity, {
|
|
hand: this.hand === RIGHT_HAND ? "right" : "left",
|
|
timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
|
|
relativePosition: this.offsetPosition,
|
|
relativeRotation: this.offsetRotation,
|
|
ttl: ACTION_TTL,
|
|
kinematic: NEAR_GRABBING_KINEMATIC,
|
|
kinematicSetVelocity: true,
|
|
ignoreIK: this.ignoreIK
|
|
});
|
|
if (this.actionID === NULL_UUID) {
|
|
this.actionID = null;
|
|
return false;
|
|
}
|
|
var now = Date.now();
|
|
this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
|
|
return true;
|
|
};
|
|
|
|
this.projectVectorAlongAxis = function (position, axisStart, axisEnd) {
|
|
var aPrime = Vec3.subtract(position, axisStart);
|
|
var bPrime = Vec3.subtract(axisEnd, axisStart);
|
|
var bPrimeMagnitude = Vec3.length(bPrime);
|
|
var dotProduct = Vec3.dot(aPrime, bPrime);
|
|
var scalar = dotProduct / bPrimeMagnitude;
|
|
if (scalar < 0) {
|
|
scalar = 0;
|
|
}
|
|
if (scalar > 1) {
|
|
scalar = 1;
|
|
}
|
|
var projection = Vec3.sum(axisStart, Vec3.multiply(scalar, Vec3.normalize(bPrime)));
|
|
return projection;
|
|
};
|
|
|
|
this.dropGestureReset = function () {
|
|
this.fastHandMoveDetected = false;
|
|
this.fastHandMoveTimer = 0;
|
|
};
|
|
|
|
this.dropGestureProcess = function (deltaTime) {
|
|
var standardControllerValue = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
|
|
var pose = Controller.getPoseValue(standardControllerValue);
|
|
var worldHandVelocity = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity);
|
|
var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation);
|
|
|
|
if (this.fastHandMoveDetected) {
|
|
this.fastHandMoveTimer -= deltaTime;
|
|
}
|
|
if (this.fastHandMoveTimer < 0) {
|
|
this.fastHandMoveDetected = false;
|
|
}
|
|
var FAST_HAND_SPEED_REST_TIME = 1; // sec
|
|
var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec
|
|
if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) {
|
|
this.fastHandMoveDetected = true;
|
|
this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME;
|
|
}
|
|
|
|
var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0};
|
|
var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis);
|
|
var DOWN = {x: 0, y: -1, z: 0};
|
|
var ROTATION_THRESHOLD = Math.cos(Math.PI / 8);
|
|
|
|
var handIsUpsideDown = false;
|
|
if (Vec3.dot(worldHandUpAxis, DOWN) > ROTATION_THRESHOLD) {
|
|
handIsUpsideDown = true;
|
|
}
|
|
|
|
var WANT_DEBUG = false;
|
|
if (WANT_DEBUG) {
|
|
print("zAxis = " + worldHandUpAxis.x + ", " + worldHandUpAxis.y + ", " + worldHandUpAxis.z);
|
|
print("dot = " + Vec3.dot(worldHandUpAxis, DOWN) + ", ROTATION_THRESHOLD = " + ROTATION_THRESHOLD);
|
|
print("handMove = " + this.fastHandMoveDetected + ", handIsUpsideDown = " + handIsUpsideDown);
|
|
}
|
|
|
|
return (DROP_WITHOUT_SHAKE || this.fastHandMoveDetected) && handIsUpsideDown;
|
|
};
|
|
|
|
this.nearGrabbingEnter = function () {
|
|
|
|
this.lineOff();
|
|
this.overlayLineOff();
|
|
|
|
this.dropGestureReset();
|
|
this.clearEquipHaptics();
|
|
this.clearEquipHotspotRendering();
|
|
|
|
Controller.triggerShortHapticPulse(1.0, this.hand);
|
|
|
|
if (this.entityActivated) {
|
|
var saveGrabbedID = this.grabbedEntity;
|
|
this.release();
|
|
this.grabbedEntity = saveGrabbedID;
|
|
}
|
|
|
|
var otherHandController = this.getOtherHandController();
|
|
if (otherHandController.grabbedEntity == this.grabbedEntity &&
|
|
(otherHandController.state == STATE_NEAR_GRABBING || otherHandController.state == STATE_DISTANCE_HOLDING)) {
|
|
otherHandController.setState(STATE_OFF, "other hand grabbed this entity");
|
|
}
|
|
|
|
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES);
|
|
this.activateEntity(this.grabbedEntity, grabbedProperties, false);
|
|
|
|
// var handRotation = this.getHandRotation();
|
|
var handRotation = (this.hand === RIGHT_HAND) ? MyAvatar.getRightPalmRotation() : MyAvatar.getLeftPalmRotation();
|
|
var handPosition = this.getHandPosition();
|
|
|
|
var hasPresetPosition = false;
|
|
if (this.state == STATE_HOLD && this.grabbedHotspot) {
|
|
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
|
|
// if an object is "equipped" and has a predefined offset, use it.
|
|
this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false;
|
|
|
|
var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
|
|
if (this.grabbedHotspot.joints[handJointName]) {
|
|
this.offsetPosition = this.grabbedHotspot.joints[handJointName][0];
|
|
this.offsetRotation = this.grabbedHotspot.joints[handJointName][1];
|
|
hasPresetPosition = true;
|
|
}
|
|
} else {
|
|
this.ignoreIK = false;
|
|
|
|
var objectRotation = grabbedProperties.rotation;
|
|
this.offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation);
|
|
|
|
var currentObjectPosition = grabbedProperties.position;
|
|
var offset = Vec3.subtract(currentObjectPosition, handPosition);
|
|
this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset);
|
|
if (this.temporaryPositionOffset) {
|
|
this.offsetPosition = this.temporaryPositionOffset;
|
|
// hasPresetPosition = true;
|
|
}
|
|
}
|
|
|
|
var isPhysical = propsArePhysical(grabbedProperties) || entityHasActions(this.grabbedEntity);
|
|
if (isPhysical && this.state == STATE_NEAR_GRABBING) {
|
|
// grab entity via action
|
|
if (!this.setupHoldAction()) {
|
|
return;
|
|
}
|
|
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
|
|
action: 'grab',
|
|
grabbedEntity: this.grabbedEntity
|
|
}));
|
|
} else {
|
|
// grab entity via parenting
|
|
this.actionID = null;
|
|
var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
|
|
var reparentProps = {
|
|
parentID: MyAvatar.sessionUUID,
|
|
parentJointIndex: handJointIndex
|
|
};
|
|
if (hasPresetPosition) {
|
|
reparentProps["localPosition"] = this.offsetPosition;
|
|
reparentProps["localRotation"] = this.offsetRotation;
|
|
}
|
|
Entities.editEntity(this.grabbedEntity, reparentProps);
|
|
|
|
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
|
|
action: 'equip',
|
|
grabbedEntity: this.grabbedEntity
|
|
}));
|
|
}
|
|
|
|
Entities.editEntity(this.grabbedEntity, {
|
|
velocity: {x: 0, y: 0, z: 0},
|
|
angularVelocity: {x: 0, y: 0, z: 0},
|
|
dynamic: false
|
|
});
|
|
|
|
if (this.state == STATE_NEAR_GRABBING) {
|
|
this.callEntityMethodOnGrabbed("startNearGrab");
|
|
} else { // this.state == STATE_HOLD
|
|
this.callEntityMethodOnGrabbed("startEquip");
|
|
}
|
|
|
|
this.currentHandControllerTipPosition =
|
|
(this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition;
|
|
this.currentObjectTime = Date.now();
|
|
|
|
this.currentObjectPosition = grabbedProperties.position;
|
|
this.currentObjectRotation = grabbedProperties.rotation;
|
|
this.currentVelocity = ZERO_VEC;
|
|
this.currentAngularVelocity = ZERO_VEC;
|
|
};
|
|
|
|
this.nearGrabbing = function (deltaTime) {
|
|
|
|
var dropDetected = this.dropGestureProcess(deltaTime);
|
|
|
|
if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) {
|
|
this.callEntityMethodOnGrabbed("releaseGrab");
|
|
this.setState(STATE_OFF, "trigger released");
|
|
return;
|
|
}
|
|
|
|
if (this.state == STATE_HOLD) {
|
|
if (dropDetected && this.triggerSmoothedGrab()) {
|
|
this.callEntityMethodOnGrabbed("releaseEquip");
|
|
this.setState(STATE_OFF, "drop gesture detected");
|
|
return;
|
|
}
|
|
|
|
if (this.thumbPressed()) {
|
|
this.callEntityMethodOnGrabbed("releaseEquip");
|
|
this.setState(STATE_OFF, "drop via thumb press");
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.heartBeat(this.grabbedEntity);
|
|
|
|
var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position", "rotation"]);
|
|
if (!props.position) {
|
|
// server may have reset, taking our equipped entity with it. move back to "off" stte
|
|
this.callEntityMethodOnGrabbed("releaseGrab");
|
|
this.setState(STATE_OFF, "entity has no position property");
|
|
return;
|
|
}
|
|
|
|
var now = Date.now();
|
|
if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * CHECK_TOO_FAR_UNEQUIP_TIME) {
|
|
this.lastUnequipCheckTime = now;
|
|
|
|
if (props.parentID == MyAvatar.sessionUUID &&
|
|
Vec3.length(props.localPosition) > NEAR_GRAB_MAX_DISTANCE) {
|
|
var handPosition = this.getHandPosition();
|
|
// the center of the equipped object being far from the hand isn't enough to autoequip -- we also
|
|
// need to fail the findEntities test.
|
|
var nearPickedCandidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS);
|
|
if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) {
|
|
// for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip.
|
|
print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." +
|
|
props.parentID + " " + vec3toStr(props.position));
|
|
|
|
if (this.state == STATE_NEAR_GRABBING) {
|
|
this.callEntityMethodOnGrabbed("releaseGrab");
|
|
} else { // this.state == STATE_HOLD
|
|
this.callEntityMethodOnGrabbed("releaseEquip");
|
|
}
|
|
this.setState(STATE_OFF, "held object too far away");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep track of the fingertip velocity to impart when we release the object.
|
|
// Note that the idea of using a constant 'tip' velocity regardless of the
|
|
// object's actual held offset is an idea intended to make it easier to throw things:
|
|
// Because we might catch something or transfer it between hands without a good idea
|
|
// of it's actual offset, let's try imparting a velocity which is at a fixed radius
|
|
// from the palm.
|
|
|
|
var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition;
|
|
|
|
var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds
|
|
|
|
if (deltaObjectTime > 0.0) {
|
|
var worldDeltaPosition = Vec3.subtract(props.position, this.currentObjectPosition);
|
|
|
|
var previousEulers = Quat.safeEulerAngles(this.currentObjectRotation);
|
|
var newEulers = Quat.safeEulerAngles(props.rotation);
|
|
var worldDeltaRotation = Vec3.subtract(newEulers, previousEulers);
|
|
|
|
this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaObjectTime);
|
|
this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaObjectTime * 180.0));
|
|
|
|
this.currentObjectPosition = props.position;
|
|
this.currentObjectRotation = props.rotation;
|
|
}
|
|
|
|
this.currentHandControllerTipPosition = handControllerPosition;
|
|
this.currentObjectTime = now;
|
|
|
|
if (this.state === STATE_HOLD) {
|
|
this.callEntityMethodOnGrabbed("continueEquip");
|
|
}
|
|
if (this.state == STATE_NEAR_GRABBING) {
|
|
this.callEntityMethodOnGrabbed("continueNearGrab");
|
|
}
|
|
|
|
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.grabbedEntity, this.actionID, {
|
|
hand: this.hand === RIGHT_HAND ? "right" : "left",
|
|
timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
|
|
relativePosition: this.offsetPosition,
|
|
relativeRotation: this.offsetRotation,
|
|
ttl: ACTION_TTL,
|
|
kinematic: NEAR_GRABBING_KINEMATIC,
|
|
kinematicSetVelocity: true,
|
|
ignoreIK: this.ignoreIK
|
|
});
|
|
if (success) {
|
|
this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
|
|
} else {
|
|
print("continueNearGrabbing -- updateAction failed");
|
|
Entities.deleteAction(this.grabbedEntity, this.actionID);
|
|
this.setupHoldAction();
|
|
}
|
|
}
|
|
};
|
|
|
|
this.nearTriggerEnter = function () {
|
|
|
|
this.clearEquipHotspotRendering();
|
|
this.clearEquipHaptics();
|
|
|
|
Controller.triggerShortHapticPulse(1.0, this.hand);
|
|
this.callEntityMethodOnGrabbed("startNearTrigger");
|
|
};
|
|
|
|
this.farTriggerEnter = function () {
|
|
this.clearEquipHotspotRendering();
|
|
this.clearEquipHaptics();
|
|
|
|
this.callEntityMethodOnGrabbed("startFarTrigger");
|
|
};
|
|
|
|
this.nearTrigger = function () {
|
|
if (this.triggerSmoothedReleased()) {
|
|
this.callEntityMethodOnGrabbed("stopNearTrigger");
|
|
this.setState(STATE_OFF, "trigger released");
|
|
return;
|
|
}
|
|
this.callEntityMethodOnGrabbed("continueNearTrigger");
|
|
};
|
|
|
|
this.farTrigger = function () {
|
|
if (this.triggerSmoothedReleased()) {
|
|
this.callEntityMethodOnGrabbed("stopFarTrigger");
|
|
this.setState(STATE_OFF, "trigger released");
|
|
return;
|
|
}
|
|
|
|
var handPosition = this.getHandPosition();
|
|
var pickRay = {
|
|
origin: handPosition,
|
|
direction: Quat.getUp(this.getHandRotation())
|
|
};
|
|
|
|
var now = Date.now();
|
|
if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) {
|
|
var intersection = findRayIntersection(pickRay, true);
|
|
if (intersection.accurate || intersection.overlayID) {
|
|
this.lastPickTime = now;
|
|
if (intersection.entityID != this.grabbedEntity) {
|
|
this.callEntityMethodOnGrabbed("stopFarTrigger");
|
|
this.setState(STATE_OFF, "laser moved off of entity");
|
|
return;
|
|
}
|
|
if (intersection.intersects) {
|
|
this.intersectionDistance = Vec3.distance(pickRay.origin, intersection.intersection);
|
|
}
|
|
this.searchIndicatorOn(pickRay);
|
|
}
|
|
}
|
|
|
|
this.callEntityMethodOnGrabbed("continueFarTrigger");
|
|
};
|
|
|
|
this.offEnter = function () {
|
|
this.release();
|
|
};
|
|
|
|
this.release = function () {
|
|
this.turnLightsOff();
|
|
this.turnOffVisualizations();
|
|
|
|
var noVelocity = false;
|
|
if (this.grabbedEntity !== null) {
|
|
if (this.actionID !== null) {
|
|
Entities.deleteAction(this.grabbedEntity, this.actionID);
|
|
// sometimes we want things to stay right where they are when we let go.
|
|
var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {});
|
|
var releaseVelocityData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
|
|
if (releaseVelocityData.disableReleaseVelocity === true ||
|
|
// this next line allowed both:
|
|
// (1) far-grab, pull to self, near grab, then throw
|
|
// (2) equip something physical and adjust it with a other-hand grab without the thing drifting
|
|
grabData.refCount > 1) {
|
|
noVelocity = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.deactivateEntity(this.grabbedEntity, noVelocity);
|
|
this.actionID = null;
|
|
|
|
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
|
|
action: 'release',
|
|
grabbedEntity: this.grabbedEntity,
|
|
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
|
|
}));
|
|
|
|
this.grabbedEntity = null;
|
|
this.grabbedHotspot = null;
|
|
|
|
if (this.triggerSmoothedGrab()) {
|
|
this.waitForTriggerRelease = true;
|
|
}
|
|
};
|
|
|
|
this.cleanup = function () {
|
|
this.release();
|
|
Entities.deleteEntity(this.particleBeamObject);
|
|
Entities.deleteEntity(this.spotLight);
|
|
Entities.deleteEntity(this.pointLight);
|
|
};
|
|
|
|
this.heartBeat = function (entityID) {
|
|
var now = Date.now();
|
|
if (now - this.lastHeartBeat > HEART_BEAT_INTERVAL) {
|
|
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
|
|
data["heartBeat"] = now;
|
|
setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data);
|
|
this.lastHeartBeat = now;
|
|
}
|
|
};
|
|
|
|
this.resetAbandonedGrab = function (entityID) {
|
|
print("cleaning up abandoned grab on " + entityID);
|
|
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
|
|
data["refCount"] = 1;
|
|
setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data);
|
|
this.deactivateEntity(entityID, false);
|
|
};
|
|
|
|
this.activateEntity = function (entityID, grabbedProperties, wasLoaded) {
|
|
if (this.entityActivated) {
|
|
return;
|
|
}
|
|
this.entityActivated = true;
|
|
|
|
if (setCollidesLaterTimeout && collideLaterID == entityID) {
|
|
// we have a timeout waiting to set collisions with myAvatar back on (so that when something
|
|
// is thrown it doesn't collide with the avatar's capsule the moment it's released). We've
|
|
// regrabbed the entity before the timeout fired, so cancel the timeout, run the function now
|
|
// and adjust the grabbedProperties. This will make the saved set of properties (the ones that
|
|
// get re-instated after all the grabs have been released) be correct.
|
|
Script.clearTimeout(setCollidesLaterTimeout);
|
|
setCollidesLaterTimeout = null;
|
|
grabbedProperties["collidesWith"] = avCollideLater();
|
|
}
|
|
|
|
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
|
|
var now = Date.now();
|
|
|
|
if (wasLoaded) {
|
|
data["refCount"] = 1;
|
|
} else {
|
|
data["refCount"] = data["refCount"] ? data["refCount"] + 1 : 1;
|
|
|
|
// zero gravity and set ignoreForCollisions in a way that lets us put them back, after all grabs are done
|
|
if (data["refCount"] == 1) {
|
|
data["heartBeat"] = now;
|
|
this.lastHeartBeat = now;
|
|
|
|
this.isInitialGrab = true;
|
|
data["gravity"] = grabbedProperties.gravity;
|
|
data["collidesWith"] = grabbedProperties.collidesWith;
|
|
data["collisionless"] = grabbedProperties.collisionless;
|
|
data["dynamic"] = grabbedProperties.dynamic;
|
|
data["parentID"] = wasLoaded ? NULL_UUID : grabbedProperties.parentID;
|
|
data["parentJointIndex"] = grabbedProperties.parentJointIndex;
|
|
|
|
var whileHeldProperties = {
|
|
gravity: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
// bummer, it isn't easy to do bitwise collisionMask operations like this:
|
|
// "collisionMask": COLLISION_MASK_WHILE_GRABBED | grabbedProperties.collisionMask
|
|
// when using string values
|
|
"collidesWith": COLLIDES_WITH_WHILE_GRABBED
|
|
};
|
|
Entities.editEntity(entityID, whileHeldProperties);
|
|
} else if (data["refCount"] > 1) {
|
|
if (data["heartBeat"] === undefined ||
|
|
now - data["heartBeat"] > HEART_BEAT_TIMEOUT) {
|
|
// this entity has userData suggesting it is grabbed, but nobody is updating the hearbeat.
|
|
// deactivate it before grabbing.
|
|
this.resetAbandonedGrab(entityID);
|
|
grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES);
|
|
return this.activateEntity(entityID, grabbedProperties, wasLoaded);
|
|
}
|
|
|
|
this.isInitialGrab = false;
|
|
// if an object is being grabbed by more than one person (or the same person twice, but nevermind), switch
|
|
// the collision groups so that it wont collide with "other" avatars. This avoids a situation where two
|
|
// people are holding something and one of them will be able (if the other releases at the right time) to
|
|
// bootstrap themselves with the held object. This happens because the meaning of "otherAvatar" in
|
|
// the collision mask hinges on who the physics simulation owner is.
|
|
Entities.editEntity(entityID, {"collidesWith": COLLIDES_WITH_WHILE_MULTI_GRABBED});
|
|
}
|
|
}
|
|
setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data);
|
|
return data;
|
|
};
|
|
|
|
this.checkForStrayChildren = function () {
|
|
// sometimes things can get parented to a hand and this script is unaware. Search for such entities and
|
|
// unhook them.
|
|
var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
|
|
var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex);
|
|
children.forEach(function (childID) {
|
|
print("disconnecting stray child of hand: (" + _this.hand + ") " + childID);
|
|
Entities.editEntity(childID, {parentID: NULL_UUID});
|
|
});
|
|
};
|
|
|
|
this.deactivateEntity = function (entityID, noVelocity) {
|
|
var deactiveProps;
|
|
|
|
if (!this.entityActivated) {
|
|
return;
|
|
}
|
|
this.entityActivated = false;
|
|
|
|
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
|
|
if (data && data["refCount"]) {
|
|
data["refCount"] = data["refCount"] - 1;
|
|
if (data["refCount"] < 1) {
|
|
deactiveProps = {
|
|
gravity: data["gravity"],
|
|
// don't set collidesWith back right away, because thrown things tend to bounce off the
|
|
// avatar's capsule.
|
|
collidesWith: removeMyAvatarFromCollidesWith(data["collidesWith"]),
|
|
collisionless: data["collisionless"],
|
|
dynamic: data["dynamic"],
|
|
parentID: data["parentID"],
|
|
parentJointIndex: data["parentJointIndex"]
|
|
};
|
|
|
|
if (data["collidesWith"].indexOf("myAvatar") >= 0) {
|
|
var javaScriptScopingIsBrokenData = data;
|
|
avCollideLater = function () {
|
|
// set collidesWith back to original value a bit later than the rest
|
|
_this.setCollidesLaterTimeout = null;
|
|
Entities.editEntity(entityID, { collidesWith: javaScriptScopingIsBrokenData["collidesWith"] });
|
|
return javaScriptScopingIsBrokenData["collidesWith"];
|
|
}
|
|
setCollidesLaterTimeout =
|
|
Script.setTimeout(avCollideLater, COLLIDE_WITH_AV_AFTER_RELEASE_DELAY * MSECS_PER_SEC);
|
|
collideLaterID = entityID;
|
|
}
|
|
|
|
// things that are held by parenting and dropped with no velocity will end up as "static" in bullet. If
|
|
// it looks like the dropped thing should fall, give it a little velocity.
|
|
var props = Entities.getEntityProperties(entityID, ["parentID", "velocity", "dynamic", "shapeType"]);
|
|
var parentID = props.parentID;
|
|
|
|
var doSetVelocity = false;
|
|
if (parentID != NULL_UUID && deactiveProps.parentID == NULL_UUID && propsArePhysical(props)) {
|
|
// TODO: EntityScriptingInterface::convertLocationToScriptSemantics should be setting up
|
|
// props.velocity to be a world-frame velocity and localVelocity to be vs parent. Until that
|
|
// is done, we use a measured velocity here so that things held via a bumper-grab / parenting-grab
|
|
// can be thrown.
|
|
doSetVelocity = true;
|
|
}
|
|
|
|
if (!noVelocity &&
|
|
!doSetVelocity &&
|
|
parentID == MyAvatar.sessionUUID &&
|
|
Vec3.length(data["gravity"]) > 0.0 &&
|
|
data["dynamic"] &&
|
|
data["parentID"] == NULL_UUID &&
|
|
!data["collisionless"]) {
|
|
deactiveProps["velocity"] = {x: 0.0, y: 0.1, z: 0.0};
|
|
doSetVelocity = false;
|
|
}
|
|
if (noVelocity) {
|
|
deactiveProps["velocity"] = {x: 0.0, y: 0.0, z: 0.0};
|
|
deactiveProps["angularVelocity"] = {x: 0.0, y: 0.0, z: 0.0};
|
|
doSetVelocity = false;
|
|
}
|
|
|
|
Entities.editEntity(entityID, deactiveProps);
|
|
|
|
if (doSetVelocity) {
|
|
// this is a continuation of the TODO above -- we shouldn't need to set this here.
|
|
// do this after the parent has been reset. setting this at the same time as
|
|
// the parent causes it to go off in the wrong direction. This is a bug that should
|
|
// be fixed.
|
|
Entities.editEntity(entityID, {
|
|
velocity: this.currentVelocity
|
|
// angularVelocity: this.currentAngularVelocity
|
|
});
|
|
}
|
|
|
|
data = null;
|
|
} else if (this.shouldResetParentOnRelease) {
|
|
// we parent-grabbed this from another parent grab. try to put it back where we found it.
|
|
deactiveProps = {
|
|
parentID: this.previousParentID,
|
|
parentJointIndex: this.previousParentJointIndex,
|
|
velocity: {x: 0.0, y: 0.0, z: 0.0},
|
|
angularVelocity: {x: 0.0, y: 0.0, z: 0.0}
|
|
};
|
|
Entities.editEntity(entityID, deactiveProps);
|
|
} else if (noVelocity) {
|
|
Entities.editEntity(entityID, {velocity: {x: 0.0, y: 0.0, z: 0.0},
|
|
angularVelocity: {x: 0.0, y: 0.0, z: 0.0},
|
|
dynamic: data["dynamic"]});
|
|
}
|
|
} else {
|
|
data = null;
|
|
}
|
|
setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data);
|
|
};
|
|
|
|
this.getOtherHandController = function () {
|
|
return (this.hand === RIGHT_HAND) ? leftController : rightController;
|
|
};
|
|
}
|
|
|
|
var rightController = new MyController(RIGHT_HAND);
|
|
var leftController = new MyController(LEFT_HAND);
|
|
|
|
var MAPPING_NAME = "com.highfidelity.handControllerGrab";
|
|
|
|
var mapping = Controller.newMapping(MAPPING_NAME);
|
|
mapping.from([Controller.Standard.RT]).peek().to(rightController.triggerPress);
|
|
mapping.from([Controller.Standard.LT]).peek().to(leftController.triggerPress);
|
|
|
|
mapping.from([Controller.Standard.RB]).peek().to(rightController.secondaryPress);
|
|
mapping.from([Controller.Standard.LB]).peek().to(leftController.secondaryPress);
|
|
mapping.from([Controller.Standard.LeftGrip]).peek().to(leftController.secondaryPress);
|
|
mapping.from([Controller.Standard.RightGrip]).peek().to(rightController.secondaryPress);
|
|
|
|
mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress);
|
|
mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress);
|
|
|
|
Controller.enableMapping(MAPPING_NAME);
|
|
|
|
// the section below allows the grab script to listen for messages
|
|
// that disable either one or both hands. useful for two handed items
|
|
var handToDisable = 'none';
|
|
|
|
function update(deltaTime) {
|
|
if (handToDisable !== LEFT_HAND && handToDisable !== 'both') {
|
|
leftController.update(deltaTime);
|
|
}
|
|
if (handToDisable !== RIGHT_HAND && handToDisable !== 'both') {
|
|
rightController.update(deltaTime);
|
|
}
|
|
}
|
|
|
|
Messages.subscribe('Hifi-Hand-Disabler');
|
|
Messages.subscribe('Hifi-Hand-Grab');
|
|
Messages.subscribe('Hifi-Hand-RayPick-Blacklist');
|
|
Messages.subscribe('Hifi-Object-Manipulation');
|
|
|
|
var handleHandMessages = function (channel, message, sender) {
|
|
var data;
|
|
if (sender === MyAvatar.sessionUUID) {
|
|
if (channel === 'Hifi-Hand-Disabler') {
|
|
if (message === 'left') {
|
|
handToDisable = LEFT_HAND;
|
|
}
|
|
if (message === 'right') {
|
|
handToDisable = RIGHT_HAND;
|
|
}
|
|
if (message === 'both' || message === 'none') {
|
|
handToDisable = message;
|
|
}
|
|
} else if (channel === 'Hifi-Hand-Grab') {
|
|
try {
|
|
data = JSON.parse(message);
|
|
var selectedController = (data.hand === 'left') ? leftController : rightController;
|
|
selectedController.release();
|
|
selectedController.setState(STATE_HOLD, "Hifi-Hand-Grab msg received");
|
|
selectedController.grabbedEntity = data.entityID;
|
|
|
|
} catch (e) {
|
|
print("WARNING: error parsing Hifi-Hand-Grab message");
|
|
}
|
|
|
|
} else if (channel === 'Hifi-Hand-RayPick-Blacklist') {
|
|
try {
|
|
data = JSON.parse(message);
|
|
var action = data.action;
|
|
var id = data.id;
|
|
var index = blacklist.indexOf(id);
|
|
|
|
if (action === 'add' && index === -1) {
|
|
blacklist.push(id);
|
|
}
|
|
if (action === 'remove') {
|
|
if (index > -1) {
|
|
blacklist.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
} catch (e) {
|
|
print("WARNING: error parsing Hifi-Hand-RayPick-Blacklist message");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Messages.messageReceived.connect(handleHandMessages);
|
|
|
|
function cleanup() {
|
|
rightController.cleanup();
|
|
leftController.cleanup();
|
|
Controller.disableMapping(MAPPING_NAME);
|
|
Reticle.setVisible(true);
|
|
Menu.removeMenuItem("Developer > Hands", "Drop Without Shake");
|
|
}
|
|
|
|
Script.scriptEnding.connect(cleanup);
|
|
Script.update.connect(update);
|
|
|
|
if (!Menu.menuExists("Developer > Grab Script")) {
|
|
Menu.addMenu("Developer > Grab Script");
|
|
}
|
|
|
|
Menu.addMenuItem({
|
|
menuName: "Developer > Grab Script",
|
|
menuItemName: "Drop Without Shake",
|
|
isCheckable: true,
|
|
isChecked: DROP_WITHOUT_SHAKE
|
|
});
|
|
|
|
Menu.addMenuItem({
|
|
menuName: "Developer > Grab Script",
|
|
menuItemName: "Draw Grab Boxes",
|
|
isCheckable: true,
|
|
isChecked: DRAW_GRAB_BOXES
|
|
});
|
|
|
|
function handleMenuItemEvent(menuItem) {
|
|
if (menuItem === "Drop Without Shake") {
|
|
DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake");
|
|
}
|
|
if (menuItem === "Draw Grab Boxes") {
|
|
DRAW_GRAB_BOXES = Menu.isOptionChecked("Draw Grab Boxes");
|
|
DRAW_HAND_SPHERES = DRAW_GRAB_BOXES;
|
|
}
|
|
}
|
|
|
|
Menu.menuItemEvent.connect(handleMenuItemEvent);
|
|
|