mirror of
https://github.com/lubosz/overte.git
synced 2025-04-25 13:53:09 +02:00
2168 lines
82 KiB
JavaScript
2168 lines
82 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 print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr */
|
|
|
|
Script.include("/~/system/libraries/utils.js");
|
|
|
|
//
|
|
// add lines where the hand ray picking is happening
|
|
//
|
|
var WANT_DEBUG = false;
|
|
var WANT_DEBUG_STATE = false;
|
|
var WANT_DEBUG_SEARCH_NAME = null;
|
|
|
|
//
|
|
// these tune time-averaging and "on" value for analog trigger
|
|
//
|
|
|
|
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.75; // Squeezed far enough to complete distant grab
|
|
var TRIGGER_OFF_VALUE = 0.15;
|
|
|
|
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;
|
|
|
|
//
|
|
// 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 FAR_TO_NEAR_GRAB_PADDING_FACTOR = 1.2;
|
|
|
|
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
|
|
//
|
|
// near grabbing
|
|
//
|
|
|
|
var GRAB_RADIUS = 0.06; // if the ray misses but an object is this close, it will still be selected
|
|
var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
|
|
var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected
|
|
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 SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size
|
|
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 CONTROLLER_STATE_MACHINE = {};
|
|
|
|
CONTROLLER_STATE_MACHINE[STATE_OFF] = {
|
|
name: "off",
|
|
enterMethod: "offEnter",
|
|
updateMethod: "off"
|
|
};
|
|
CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = {
|
|
name: "searching",
|
|
updateMethod: "search",
|
|
enterMethod: "searchEnter",
|
|
exitMethod: "searchExit"
|
|
};
|
|
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 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;
|
|
}
|
|
|
|
// 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;
|
|
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);
|
|
}
|
|
}
|
|
|
|
// constructor
|
|
function EntityPropertiesCache() {
|
|
this.cache = {};
|
|
}
|
|
EntityPropertiesCache.prototype.clear = function() {
|
|
this.cache = {};
|
|
};
|
|
EntityPropertiesCache.prototype.findEntities = function(position, radius) {
|
|
var entities = Entities.findEntities(position, radius);
|
|
var i, l = entities.length;
|
|
for (i = 0; i < l; i++) {
|
|
this.addEntity(entities[i]);
|
|
}
|
|
};
|
|
EntityPropertiesCache.prototype.addEntity = function(entityID) {
|
|
var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES);
|
|
var grabbableProps = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA);
|
|
var grabProps = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
|
|
var wearableProps = getEntityCustomData("wearable", entityID, {});
|
|
this.cache[entityID] = { props: props, grabbableProps: grabbableProps, grabProps: grabProps, wearableProps: wearableProps };
|
|
};
|
|
EntityPropertiesCache.prototype.getEntities = function() {
|
|
return Object.keys(this.cache);
|
|
}
|
|
EntityPropertiesCache.prototype.getProps = function(entityID) {
|
|
var obj = this.cache[entityID]
|
|
return obj ? obj.props : undefined;
|
|
};
|
|
EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) {
|
|
var obj = this.cache[entityID]
|
|
return obj ? obj.grabbableProps : undefined;
|
|
};
|
|
EntityPropertiesCache.prototype.getGrabProps = function(entityID) {
|
|
var obj = this.cache[entityID]
|
|
return obj ? obj.grabProps : undefined;
|
|
};
|
|
EntityPropertiesCache.prototype.getWearableProps = function(entityID) {
|
|
var obj = this.cache[entityID]
|
|
return obj ? obj.wearableProps : undefined;
|
|
};
|
|
|
|
function MyController(hand) {
|
|
this.hand = hand;
|
|
if (this.hand === RIGHT_HAND) {
|
|
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;
|
|
this.grabSphere = 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();
|
|
|
|
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) {
|
|
this.grabSphereOff();
|
|
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.grabSphereOn = function() {
|
|
var color = {red: 0, green: 255, blue: 0};
|
|
if (this.grabSphere === null) {
|
|
var sphereProperties = {
|
|
position: this.getHandPosition(),
|
|
size: GRAB_RADIUS*2,
|
|
color: color,
|
|
alpha: 0.1,
|
|
solid: true,
|
|
ignoreRayIntersection: true,
|
|
drawInFront: true, // Even when burried inside of something, show it.
|
|
visible: true
|
|
}
|
|
this.grabSphere = Overlays.addOverlay("sphere", sphereProperties);
|
|
} else {
|
|
Overlays.editOverlay(this.grabSphere, {
|
|
position: this.getHandPosition(),
|
|
size: GRAB_RADIUS*2,
|
|
color: color,
|
|
alpha: 0.1,
|
|
solid: true,
|
|
visible: true
|
|
});
|
|
}
|
|
}
|
|
|
|
this.grabSphereOff = function() {
|
|
if (this.grabSphere !== null) {
|
|
Overlays.deleteOverlay(this.grabSphere);
|
|
this.grabSphere = null;
|
|
}
|
|
};
|
|
|
|
this.overlayLineOn = function(closePoint, farPoint, color) {
|
|
if (this.overlayLine === null) {
|
|
var lineProperties = {
|
|
lineWidth: 5,
|
|
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");
|
|
}
|
|
}
|
|
};
|
|
|
|
this.searchEnter = function() {
|
|
this.equipHotspotOverlays = [];
|
|
|
|
// find entities near the avatar that might be equipable.
|
|
var entities = Entities.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE);
|
|
var i, l = entities.length;
|
|
for (i = 0; i < l; i++) {
|
|
|
|
// is this entity equipable?
|
|
var grabData = getEntityCustomData(GRABBABLE_DATA_KEY, entities[i], undefined);
|
|
var grabProps = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES);
|
|
if (grabData) {
|
|
// does this entity have an attach point?
|
|
var wearableData = getEntityCustomData("wearable", entities[i], undefined);
|
|
if (wearableData && wearableData.joints) {
|
|
var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
|
|
if (wearableData.joints[handJointName]) {
|
|
// draw the hotspot
|
|
this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", {
|
|
position: grabProps.position,
|
|
size: 0.2,
|
|
color: { red: 90, green: 255, blue: 90 },
|
|
alpha: 0.7,
|
|
solid: true,
|
|
visible: true,
|
|
ignoreRayIntersection: false,
|
|
drawInFront: false
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this.searchExit = function() {
|
|
|
|
// delete all equip hotspots
|
|
var i, l = this.equipHotspotOverlays.length;
|
|
for (i = 0; i < l; i++) {
|
|
Overlays.deleteOverlay(this.equipHotspotOverlays[i]);
|
|
}
|
|
this.equipHotspotOverlays = [];
|
|
};
|
|
|
|
///
|
|
// Performs ray pick test from the hand controller into the world
|
|
// @param {number} which hand to use, RIGHT_HAND or LEFT_HAND
|
|
// @returns {object} returns object with two keys entityID and distance
|
|
//
|
|
this.calcRayPickInfo = function(hand) {
|
|
|
|
var pose = Controller.getPoseValue((hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand);
|
|
var worldHandPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position);
|
|
var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation);
|
|
|
|
var pickRay = {
|
|
origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position,
|
|
direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation),
|
|
Quat.getFront(Camera.orientation),
|
|
HAND_HEAD_MIX_RATIO),
|
|
length: PICK_MAX_DISTANCE
|
|
};
|
|
|
|
var result = {
|
|
entityID: null,
|
|
searchRay: pickRay,
|
|
distance: PICK_MAX_DISTANCE
|
|
};
|
|
|
|
var now = Date.now();
|
|
if (now - this.lastPickTime < MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) {
|
|
return result;
|
|
}
|
|
this.lastPickTime = now;
|
|
|
|
var directionNormalized = Vec3.normalize(pickRay.direction);
|
|
var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE);
|
|
var pickRayBacked = {
|
|
origin: Vec3.subtract(pickRay.origin, directionBacked),
|
|
direction: pickRay.direction
|
|
};
|
|
|
|
var intersection;
|
|
if (USE_BLACKLIST === true && blacklist.length !== 0) {
|
|
intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist);
|
|
} else {
|
|
intersection = Entities.findRayIntersection(pickRayBacked, true);
|
|
}
|
|
|
|
var overlayIntersection = Overlays.findRayIntersection(pickRayBacked);
|
|
if (!intersection.intersects || (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) {
|
|
intersection = overlayIntersection;
|
|
}
|
|
|
|
if (intersection.intersects) {
|
|
return { entityID: intersection.entityID,
|
|
searchRay: pickRay,
|
|
distance: Vec3.distance(pickRay.origin, intersection.intersection) }
|
|
} else {
|
|
return result;
|
|
}
|
|
};
|
|
|
|
this.entityWantsTrigger = function (entityID) {
|
|
var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID);
|
|
return grabbableProps && grabbableProps.wantsTrigger;
|
|
};
|
|
|
|
this.entityIsEquippable = function (entityID, handPosition) {
|
|
var props = this.entityPropertyCache.getProps(entityID);
|
|
var distance = Vec3.distance(props.position, handPosition);
|
|
var grabProps = this.entityPropertyCache.getGrabProps(entityID);
|
|
var debug = true;//(WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME);
|
|
|
|
var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0;
|
|
if (refCount > 0) {
|
|
if (debug) {
|
|
print("equip is skipping '" + props.name + "': it is already grabbed");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (distance > NEAR_PICK_MAX_DISTANCE) {
|
|
if (debug) {
|
|
print("equip is skipping '" + props.name + "': too far away.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
var wearableProps = this.entityPropertyCache.getWearableProps(entityID);
|
|
if (!wearableProps || !wearableProps.joints) {
|
|
if (debug) {
|
|
print("equip is skipping '" + props.name + "': no wearable attach-point");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
|
|
if (!wearableProps.joints[handJointName]) {
|
|
if (debug) {
|
|
print("equip is skipping '" + props.name + "': no wearable joint for " + handJointName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
this.entityIsGrabbable = function (entityID) {
|
|
var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID);
|
|
var grabProps = this.entityPropertyCache.getGrabProps(entityID);
|
|
var props = this.entityPropertyCache.getProps(entityID);
|
|
var physical = propsArePhysical(props);
|
|
var grabbable = false;
|
|
var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME);
|
|
|
|
if (physical) {
|
|
// physical things default to grabbable
|
|
grabbable = true;
|
|
} else {
|
|
// non-physical things default to non-grabbable unless they are already grabbed
|
|
if ("refCount" in grabProps && grabProps.refCount > 0) {
|
|
grabbable = true;
|
|
} else {
|
|
grabbable = false;
|
|
}
|
|
}
|
|
|
|
if (grabbableProps.hasOwnProperty("grabbable")) {
|
|
grabbable = grabbableProps.grabbable;
|
|
}
|
|
|
|
if (!grabbable && !grabbableProps.wantsTrigger) {
|
|
if (debug) {
|
|
print("grab is skipping '" + props.name + "': not grabbable.");
|
|
}
|
|
return false;
|
|
}
|
|
if (FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0) {
|
|
if (debug) {
|
|
print("grab is skipping '" + props.name + "': forbidden entity type.");
|
|
}
|
|
return false;
|
|
}
|
|
if (props.locked && !grabbableProps.wantsTrigger) {
|
|
if (debug) {
|
|
print("grab is skipping '" + props.name + "': locked and not triggerable.");
|
|
}
|
|
return false;
|
|
}
|
|
if (FORBIDDEN_GRAB_NAMES.indexOf(props.name) >= 0) {
|
|
if (debug) {
|
|
print("grab is skipping '" + props.name + "': forbidden name.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
this.entityIsDistanceGrabbable = function(entityID, handPosition) {
|
|
if (!this.entityIsGrabbable(entityID)) {
|
|
return false;
|
|
}
|
|
|
|
var props = this.entityPropertyCache.getProps(entityID);
|
|
var distance = Vec3.distance(props.position, handPosition);
|
|
var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME);
|
|
|
|
// we can't distance-grab non-physical
|
|
var isPhysical = propsArePhysical(props);
|
|
if (!isPhysical) {
|
|
if (debug) {
|
|
print("distance grab is skipping '" + props.name + "': not physical");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (distance > PICK_MAX_DISTANCE) {
|
|
// too far away, don't grab
|
|
if (debug) {
|
|
print("distance grab is skipping '" + props.name + "': too far away.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (entityIsGrabbedByOther(entityID)) {
|
|
// don't distance grab something that is already grabbed.
|
|
if (debug) {
|
|
print("distance grab is skipping '" + props.name + "': already grabbed by another.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
this.entityIsNearGrabbable = function(entityID, handPosition) {
|
|
|
|
if (!this.entityIsGrabbable(entityID)) {
|
|
return false;
|
|
}
|
|
|
|
var props = this.entityPropertyCache.getProps(entityID);
|
|
var distance = Vec3.distance(props.position, handPosition);
|
|
var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME);
|
|
|
|
if (distance > NEAR_PICK_MAX_DISTANCE) {
|
|
// too far away, don't grab
|
|
if (debug) {
|
|
print(" grab is skipping '" + props.name + "': too far away.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
this.search = function() {
|
|
var _this = this;
|
|
var name;
|
|
|
|
this.grabbedEntity = null;
|
|
this.isInitialGrab = false;
|
|
this.shouldResetParentOnRelease = false;
|
|
|
|
this.checkForStrayChildren();
|
|
|
|
if (this.triggerSmoothedReleased()) {
|
|
this.setState(STATE_OFF, "trigger released");
|
|
return;
|
|
}
|
|
|
|
var handPosition = this.getHandPosition();
|
|
|
|
if (SHOW_GRAB_SPHERE) {
|
|
this.grabSphereOn();
|
|
}
|
|
|
|
this.entityPropertyCache.clear();
|
|
this.entityPropertyCache.findEntities(handPosition, GRAB_RADIUS);
|
|
var candidateEntities = this.entityPropertyCache.getEntities();
|
|
|
|
var equippableEntities = candidateEntities.filter(function (entity) {
|
|
return _this.entityIsEquippable(entity, handPosition);
|
|
});
|
|
|
|
var entity;
|
|
if (equippableEntities.length > 0) {
|
|
// sort by distance
|
|
equippableEntities.sort(function (a, b) {
|
|
var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition);
|
|
var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition);
|
|
return aDistance - bDistance;
|
|
});
|
|
entity = equippableEntities[0];
|
|
if (this.triggerSmoothedGrab()) {
|
|
this.grabbedEntity = entity;
|
|
this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(entity).name + "'");
|
|
return;
|
|
} else {
|
|
// TODO: highlight the equippable object?
|
|
}
|
|
}
|
|
|
|
var rayPickInfo = this.calcRayPickInfo(this.hand);
|
|
this.intersectionDistance = rayPickInfo.distance;
|
|
if (rayPickInfo.entityID) {
|
|
candidateEntities.push(rayPickInfo.entityID);
|
|
this.entityPropertyCache.addEntity(rayPickInfo.entityID);
|
|
}
|
|
|
|
var grabbableEntities = candidateEntities.filter(function (entity) {
|
|
return _this.entityIsNearGrabbable(entity, handPosition);
|
|
});
|
|
|
|
if (grabbableEntities.length > 0) {
|
|
// sort by distance
|
|
grabbableEntities.sort(function (a, b) {
|
|
var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition);
|
|
var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition);
|
|
return aDistance - bDistance;
|
|
});
|
|
entity = grabbableEntities[0];
|
|
name = this.entityPropertyCache.getProps(entity).name;
|
|
this.grabbedEntity = entity;
|
|
if (this.entityWantsTrigger(entity)) {
|
|
if (this.triggerSmoothedGrab()) {
|
|
this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'");
|
|
return;
|
|
} else {
|
|
// TODO: highlight the near-triggerable object?
|
|
}
|
|
} 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 {
|
|
// TODO: highlight the grabbable object?
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (rayPickInfo.entityID) {
|
|
entity = rayPickInfo.entityID;
|
|
name = this.entityPropertyCache.getProps(entity).name;
|
|
if (this.entityWantsTrigger(entity)) {
|
|
if (this.triggerSmoothedGrab()) {
|
|
this.grabbedEntity = entity;
|
|
this.setState(STATE_FAR_TRIGGER, "far trigger '" + name + "'");
|
|
return;
|
|
} else {
|
|
// TODO: highlight the far-triggerable object?
|
|
}
|
|
} else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) {
|
|
if (this.triggerSmoothedGrab()) {
|
|
this.grabbedEntity = entity;
|
|
this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'");
|
|
return;
|
|
} else {
|
|
// TODO: highlight the far-grabbable object?
|
|
}
|
|
}
|
|
}
|
|
|
|
//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() {
|
|
|
|
// 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.setState(STATE_OFF, "trigger released");
|
|
this.callEntityMethodOnGrabbed("releaseGrab");
|
|
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 defaultConstraintData = {
|
|
// axisStart: false,
|
|
// axisEnd: false
|
|
// }
|
|
//
|
|
// var constraintData = getEntityCustomData('lightModifierKey', this.grabbedEntity, defaultConstraintData);
|
|
// var clampedVector;
|
|
// var targetPosition;
|
|
// if (constraintData.axisStart !== false) {
|
|
// clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition,
|
|
// constraintData.axisStart,
|
|
// constraintData.axisEnd);
|
|
// targetPosition = clampedVector;
|
|
// } else {
|
|
// targetPosition = {
|
|
// x: this.currentObjectPosition.x,
|
|
// y: this.currentObjectPosition.y,
|
|
// z: this.currentObjectPosition.z
|
|
// }
|
|
// }
|
|
|
|
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.hasPresetOffsets = function() {
|
|
var wearableData = getEntityCustomData('wearable', this.grabbedEntity, {joints: {}});
|
|
if ("joints" in wearableData) {
|
|
var allowedJoints = wearableData.joints;
|
|
var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
|
|
if (handJointName in allowedJoints) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
this.getPresetPosition = function() {
|
|
var wearableData = getEntityCustomData('wearable', this.grabbedEntity, {joints: {}});
|
|
var allowedJoints = wearableData.joints;
|
|
var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
|
|
if (handJointName in allowedJoints) {
|
|
return allowedJoints[handJointName][0];
|
|
}
|
|
}
|
|
|
|
this.getPresetRotation = function() {
|
|
var wearableData = getEntityCustomData('wearable', this.grabbedEntity, {joints: {}});
|
|
var allowedJoints = wearableData.joints;
|
|
var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
|
|
if (handJointName in allowedJoints) {
|
|
return allowedJoints[handJointName][1];
|
|
}
|
|
}
|
|
|
|
this.dropGestureReset = function() {
|
|
this.fastHandMoveDetected = false;
|
|
this.fastHandMoveTimer = 0;
|
|
};
|
|
|
|
this.dropGestureProcess = function(deltaTime) {
|
|
var pose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand);
|
|
var worldHandVelocity = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity);
|
|
var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation);
|
|
|
|
if (this.fastHandMoveDetected) {
|
|
this.fastHandMoveTimer -= deltaTime;
|
|
}
|
|
if (this.fastHandMoveTimer < 0) {
|
|
this.fastHandMoveDetected = false;
|
|
}
|
|
var FAST_HAND_SPEED_REST_TIME = 1; // sec
|
|
var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec
|
|
if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) {
|
|
this.fastHandMoveDetected = true;
|
|
this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME;
|
|
}
|
|
|
|
var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0};
|
|
var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis);
|
|
var DOWN = {x: 0, y: -1, z: 0};
|
|
var ROTATION_THRESHOLD = Math.cos(Math.PI / 8);
|
|
|
|
var handIsUpsideDown = false;
|
|
if (Vec3.dot(worldHandUpAxis, DOWN) > ROTATION_THRESHOLD) {
|
|
handIsUpsideDown = true;
|
|
}
|
|
|
|
var WANT_DEBUG = false;
|
|
if (WANT_DEBUG) {
|
|
print("zAxis = " + worldHandUpAxis.x + ", " + worldHandUpAxis.y + ", " + worldHandUpAxis.z);
|
|
print("dot = " + Vec3.dot(worldHandUpAxis, DOWN) + ", ROTATION_THRESHOLD = " + ROTATION_THRESHOLD);
|
|
print("handMove = " + this.fastHandMoveDetected + ", handIsUpsideDown = " + handIsUpsideDown);
|
|
}
|
|
|
|
return this.fastHandMoveDetected && handIsUpsideDown;
|
|
};
|
|
|
|
this.nearGrabbingEnter = function() {
|
|
|
|
this.lineOff();
|
|
this.overlayLineOff();
|
|
|
|
this.dropGestureReset();
|
|
|
|
if (this.entityActivated) {
|
|
var saveGrabbedID = this.grabbedEntity;
|
|
this.release();
|
|
this.grabbedEntity = saveGrabbedID;
|
|
}
|
|
|
|
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.hasPresetOffsets()) {
|
|
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;
|
|
this.offsetPosition = this.getPresetPosition();
|
|
this.offsetRotation = this.getPresetRotation();
|
|
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.setState(STATE_OFF, "trigger released");
|
|
this.callEntityMethodOnGrabbed("releaseGrab");
|
|
return;
|
|
}
|
|
|
|
if ((this.state == STATE_HOLD && dropDetected && this.triggerSmoothedGrab()) || this.thumbPressed()) {
|
|
this.setState(STATE_OFF, "drop detected");
|
|
this.callEntityMethodOnGrabbed("releaseEquip");
|
|
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.setState(STATE_OFF, "entity has no position property");
|
|
this.callEntityMethodOnGrabbed("releaseGrab");
|
|
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_PICK_MAX_DISTANCE * 2.0) {
|
|
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, 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));
|
|
this.setState(STATE_OFF, "held object too far away");
|
|
if (this.state == STATE_NEAR_GRABBING) {
|
|
this.callEntityMethodOnGrabbed("releaseGrab");
|
|
} else { // this.state == STATE_HOLD
|
|
this.callEntityMethodOnGrabbed("releaseEquip");
|
|
}
|
|
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.callEntityMethodOnGrabbed("startNearTrigger");
|
|
};
|
|
|
|
this.farTriggerEnter = function() {
|
|
this.callEntityMethodOnGrabbed("startFarTrigger");
|
|
};
|
|
|
|
this.nearTrigger = function() {
|
|
if (this.triggerSmoothedReleased()) {
|
|
this.setState(STATE_OFF, "trigger released");
|
|
this.callEntityMethodOnGrabbed("stopNearTrigger");
|
|
return;
|
|
}
|
|
this.callEntityMethodOnGrabbed("continueNearTrigger");
|
|
};
|
|
|
|
this.farTrigger = function() {
|
|
if (this.triggerSmoothedReleased()) {
|
|
this.setState(STATE_OFF, "trigger released");
|
|
this.callEntityMethodOnGrabbed("stopFarTrigger");
|
|
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 = Entities.findRayIntersection(pickRay, true);
|
|
if (intersection.accurate) {
|
|
this.lastPickTime = now;
|
|
if (intersection.entityID != this.grabbedEntity) {
|
|
this.setState(STATE_OFF, "laser moved off of entity");
|
|
this.callEntityMethodOnGrabbed("stopFarTrigger");
|
|
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;
|
|
|
|
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;
|
|
|
|
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"],
|
|
collidesWith: data["collidesWith"],
|
|
collisionless: data["collisionless"],
|
|
dynamic: data["dynamic"],
|
|
parentID: data["parentID"],
|
|
parentJointIndex: data["parentJointIndex"]
|
|
};
|
|
|
|
// 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);
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|
|
Script.scriptEnding.connect(cleanup);
|
|
Script.update.connect(update);
|