mirror of
https://thingvellir.net/git/overte
synced 2025-03-27 23:52:03 +01:00
Merge branch 'controller-dispatcher-1' of github.com:sethalves/hifi into controller-dispatcher-1
This commit is contained in:
commit
58a63a14fb
9 changed files with 1074 additions and 114 deletions
|
@ -9,7 +9,7 @@
|
|||
|
||||
/* global Script, Entities, Overlays, Controller, Vec3, Quat, getControllerWorldLocation, RayPick,
|
||||
controllerDispatcherPlugins, controllerDispatcherPluginsNeedSort, entityIsGrabbable,
|
||||
LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE
|
||||
LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES
|
||||
*/
|
||||
|
||||
controllerDispatcherPlugins = {};
|
||||
|
@ -25,25 +25,6 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
var NEAR_MIN_RADIUS = 0.1;
|
||||
var NEAR_MAX_RADIUS = 1.0;
|
||||
|
||||
var DISPATCHER_PROPERTIES = [
|
||||
"position",
|
||||
"registrationPoint",
|
||||
"rotation",
|
||||
"gravity",
|
||||
"collidesWith",
|
||||
"dynamic",
|
||||
"collisionless",
|
||||
"locked",
|
||||
"name",
|
||||
"shapeType",
|
||||
"parentID",
|
||||
"parentJointIndex",
|
||||
"density",
|
||||
"dimensions",
|
||||
"userData"
|
||||
];
|
||||
|
||||
|
||||
var TARGET_UPDATE_HZ = 60; // 50hz good enough, but we're using update
|
||||
var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ;
|
||||
var lastInterval = Date.now();
|
||||
|
@ -90,23 +71,28 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
this.leftTriggerClicked = 0;
|
||||
this.rightTriggerValue = 0;
|
||||
this.rightTriggerClicked = 0;
|
||||
|
||||
this.leftSecondaryValue = 0;
|
||||
this.rightSecondaryValue = 0;
|
||||
|
||||
this.leftTriggerPress = function (value) {
|
||||
_this.leftTriggerValue = value;
|
||||
};
|
||||
|
||||
this.leftTriggerClick = function (value) {
|
||||
_this.leftTriggerClicked = value;
|
||||
};
|
||||
|
||||
this.rightTriggerPress = function (value) {
|
||||
_this.rightTriggerValue = value;
|
||||
};
|
||||
|
||||
this.rightTriggerClick = function (value) {
|
||||
_this.rightTriggerClicked = value;
|
||||
};
|
||||
this.leftSecondaryPress = function (value) {
|
||||
_this.leftSecondaryValue = value;
|
||||
};
|
||||
this.rightSecondaryPress = function (value) {
|
||||
_this.rightSecondaryValue = value;
|
||||
};
|
||||
|
||||
|
||||
this.dataGatherers = {};
|
||||
this.dataGatherers.leftControllerLocation = function () {
|
||||
|
@ -150,10 +136,21 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
}
|
||||
}
|
||||
this.orderedPluginNames.sort(function (a, b) {
|
||||
return controllerDispatcherPlugins[a].priority < controllerDispatcherPlugins[b].priority;
|
||||
return controllerDispatcherPlugins[a].parameters.priority -
|
||||
controllerDispatcherPlugins[b].parameters.priority;
|
||||
});
|
||||
|
||||
print("controllerDispatcher: new plugin order: " + JSON.stringify(this.orderedPluginNames));
|
||||
// print("controllerDispatcher -- new plugin order: " + JSON.stringify(this.orderedPluginNames));
|
||||
var output = "controllerDispatcher -- new plugin order: ";
|
||||
for (var k = 0; k < this.orderedPluginNames.length; k++) {
|
||||
var dbgPluginName = this.orderedPluginNames[k];
|
||||
var priority = controllerDispatcherPlugins[dbgPluginName].parameters.priority;
|
||||
output += dbgPluginName + ":" + priority;
|
||||
if (k + 1 < this.orderedPluginNames.length) {
|
||||
output += ", ";
|
||||
}
|
||||
}
|
||||
print(output);
|
||||
|
||||
controllerDispatcherPluginsNeedSort = false;
|
||||
}
|
||||
|
@ -166,44 +163,31 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
var h;
|
||||
for (h = LEFT_HAND; h <= RIGHT_HAND; h++) {
|
||||
// todo: check controllerLocations[h].valid
|
||||
var nearbyOverlays = Overlays.findOverlays(controllerLocations[h].position, NEAR_MIN_RADIUS);
|
||||
var makeOverlaySorter = function (handIndex) {
|
||||
return function (a, b) {
|
||||
var aPosition = Overlays.getProperty(a, "position");
|
||||
var aDistance = Vec3.distance(aPosition, controllerLocations[handIndex]);
|
||||
var bPosition = Overlays.getProperty(b, "position");
|
||||
var bDistance = Vec3.distance(bPosition, controllerLocations[handIndex]);
|
||||
return aDistance - bDistance;
|
||||
};
|
||||
};
|
||||
nearbyOverlays.sort(makeOverlaySorter(h));
|
||||
var nearbyOverlays = Overlays.findOverlays(controllerLocations[h].position, NEAR_MAX_RADIUS);
|
||||
nearbyOverlays.sort(function (a, b) {
|
||||
var aPosition = Overlays.getProperty(a, "position");
|
||||
var aDistance = Vec3.distance(aPosition, controllerLocations[h].position);
|
||||
var bPosition = Overlays.getProperty(b, "position");
|
||||
var bDistance = Vec3.distance(bPosition, controllerLocations[h].position);
|
||||
return aDistance - bDistance;
|
||||
});
|
||||
nearbyOverlayIDs.push(nearbyOverlays);
|
||||
}
|
||||
|
||||
// find entities near each hand
|
||||
var nearbyEntityProperties = [[], []];
|
||||
var nearbyEntityPropertiesByID = {};
|
||||
for (h = LEFT_HAND; h <= RIGHT_HAND; h++) {
|
||||
// todo: check controllerLocations[h].valid
|
||||
var controllerPosition = controllerLocations[h].position;
|
||||
var nearbyEntityIDs = Entities.findEntities(controllerPosition, NEAR_MIN_RADIUS);
|
||||
var nearbyEntityIDs = Entities.findEntities(controllerPosition, NEAR_MAX_RADIUS);
|
||||
for (var j = 0; j < nearbyEntityIDs.length; j++) {
|
||||
var entityID = nearbyEntityIDs[j];
|
||||
var props = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
|
||||
props.id = entityID;
|
||||
props.distanceFromController = Vec3.length(Vec3.subtract(controllerPosition, props.position));
|
||||
if (props.distanceFromController < NEAR_MAX_RADIUS) {
|
||||
nearbyEntityProperties[h].push(props);
|
||||
}
|
||||
nearbyEntityPropertiesByID[entityID] = props;
|
||||
nearbyEntityProperties[h].push(props);
|
||||
}
|
||||
// sort by distance from each hand
|
||||
var makeSorter = function (handIndex) {
|
||||
return function (a, b) {
|
||||
var aDistance = Vec3.distance(a.position, controllerLocations[handIndex]);
|
||||
var bDistance = Vec3.distance(b.position, controllerLocations[handIndex]);
|
||||
return aDistance - bDistance;
|
||||
};
|
||||
};
|
||||
nearbyEntityProperties[h].sort(makeSorter(h));
|
||||
}
|
||||
|
||||
// raypick for each controller
|
||||
|
@ -221,25 +205,33 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
length: 1000
|
||||
};
|
||||
|
||||
var nearEntityID = rayPicks[h].entityID;
|
||||
if (nearEntityID) {
|
||||
if (rayPicks[h].type == RayPick.INTERSECTED_ENTITY) {
|
||||
// XXX check to make sure this one isn't already in nearbyEntityProperties?
|
||||
if (rayPicks[h].distance < NEAR_GRAB_PICK_RADIUS) {
|
||||
var nearEntityID = rayPicks[h].objectID;
|
||||
var nearbyProps = Entities.getEntityProperties(nearEntityID, DISPATCHER_PROPERTIES);
|
||||
nearbyProps.id = nearEntityID;
|
||||
if (entityIsGrabbable(nearbyProps)) {
|
||||
nearbyEntityProperties[h].push(nearbyProps);
|
||||
}
|
||||
nearbyEntityPropertiesByID[nearEntityID] = nearbyProps;
|
||||
nearbyEntityProperties[h].push(nearbyProps);
|
||||
}
|
||||
}
|
||||
|
||||
// sort by distance from each hand
|
||||
nearbyEntityProperties[h].sort(function (a, b) {
|
||||
var aDistance = Vec3.distance(a.position, controllerLocations[h].position);
|
||||
var bDistance = Vec3.distance(b.position, controllerLocations[h].position);
|
||||
return aDistance - bDistance;
|
||||
});
|
||||
}
|
||||
|
||||
// bundle up all the data about the current situation
|
||||
var controllerData = {
|
||||
triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue],
|
||||
triggerClicks: [_this.leftTriggerClicked, _this.rightTriggerClicked],
|
||||
secondaryValues: [_this.leftSecondaryValue, _this.rightSecondaryValue],
|
||||
controllerLocations: controllerLocations,
|
||||
nearbyEntityProperties: nearbyEntityProperties,
|
||||
nearbyEntityPropertiesByID: nearbyEntityPropertiesByID,
|
||||
nearbyOverlayIDs: nearbyOverlayIDs,
|
||||
rayPicks: rayPicks
|
||||
};
|
||||
|
@ -260,6 +252,8 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
}
|
||||
}
|
||||
|
||||
// print("QQQ running plugins: " + JSON.stringify(_this.runningPluginNames));
|
||||
|
||||
// give time to running plugins
|
||||
for (var runningPluginName in _this.runningPluginNames) {
|
||||
if (_this.runningPluginNames.hasOwnProperty(runningPluginName)) {
|
||||
|
@ -288,6 +282,12 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
mapping.from([Controller.Standard.RTClick]).peek().to(_this.rightTriggerClick);
|
||||
mapping.from([Controller.Standard.LT]).peek().to(_this.leftTriggerPress);
|
||||
mapping.from([Controller.Standard.LTClick]).peek().to(_this.leftTriggerClick);
|
||||
|
||||
mapping.from([Controller.Standard.RB]).peek().to(_this.rightSecondaryPress);
|
||||
mapping.from([Controller.Standard.LB]).peek().to(_this.leftSecondaryPress);
|
||||
mapping.from([Controller.Standard.LeftGrip]).peek().to(_this.leftSecondaryPress);
|
||||
mapping.from([Controller.Standard.RightGrip]).peek().to(_this.rightSecondaryPress);
|
||||
|
||||
Controller.enableMapping(MAPPING_NAME);
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
|
||||
COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
|
||||
COLORS_GRAB_DISTANCE_HOLD,
|
||||
NEAR_GRAB_RADIUS,
|
||||
DISPATCHER_PROPERTIES,
|
||||
Entities,
|
||||
makeDispatcherModuleParameters,
|
||||
makeRunningValues,
|
||||
enableDispatcherModule,
|
||||
|
@ -30,7 +33,10 @@
|
|||
controllerDispatcherPluginsNeedSort,
|
||||
projectOntoXYPlane,
|
||||
projectOntoEntityXYPlane,
|
||||
projectOntoOverlayXYPlane
|
||||
projectOntoOverlayXYPlane,
|
||||
entityHasActions,
|
||||
ensureDynamic,
|
||||
findGroupParent
|
||||
*/
|
||||
|
||||
MSECS_PER_SEC = 1000.0;
|
||||
|
@ -64,6 +70,30 @@ COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 };
|
|||
COLORS_GRAB_DISTANCE_HOLD = { red: 238, green: 75, blue: 214 };
|
||||
|
||||
|
||||
NEAR_GRAB_RADIUS = 0.1;
|
||||
|
||||
|
||||
|
||||
DISPATCHER_PROPERTIES = [
|
||||
"position",
|
||||
"registrationPoint",
|
||||
"rotation",
|
||||
"gravity",
|
||||
"collidesWith",
|
||||
"dynamic",
|
||||
"collisionless",
|
||||
"locked",
|
||||
"name",
|
||||
"shapeType",
|
||||
"parentID",
|
||||
"parentJointIndex",
|
||||
"density",
|
||||
"dimensions",
|
||||
"userData"
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
// priority -- a lower priority means the module will be asked sooner than one with a higher priority in a given update step
|
||||
// activitySlots -- indicates which "slots" must not yet be in use for this module to start
|
||||
|
@ -111,11 +141,15 @@ getGrabbableData = function (props) {
|
|||
var grabbableData = {};
|
||||
var userDataParsed = null;
|
||||
try {
|
||||
userDataParsed = JSON.parse(props.userData);
|
||||
if (!props.userDataParsed) {
|
||||
props.userDataParsed = JSON.parse(props.userData);
|
||||
}
|
||||
userDataParsed = props.userDataParsed;
|
||||
} catch (err) {
|
||||
userDataParsed = {};
|
||||
}
|
||||
if (userDataParsed && userDataParsed.grabbable) {
|
||||
grabbableData = userDataParsed.grabbable;
|
||||
if (userDataParsed.grabbableKey) {
|
||||
grabbableData = userDataParsed.grabbableKey;
|
||||
}
|
||||
if (!grabbableData.hasOwnProperty("grabbable")) {
|
||||
grabbableData.grabbable = true;
|
||||
|
@ -126,6 +160,12 @@ getGrabbableData = function (props) {
|
|||
if (!grabbableData.hasOwnProperty("kinematicGrab")) {
|
||||
grabbableData.kinematicGrab = true;
|
||||
}
|
||||
if (!grabbableData.hasOwnProperty("wantsTrigger")) {
|
||||
grabbableData.wantsTrigger = false;
|
||||
}
|
||||
if (!grabbableData.hasOwnProperty("triggerable")) {
|
||||
grabbableData.triggerable = false;
|
||||
}
|
||||
|
||||
return grabbableData;
|
||||
};
|
||||
|
@ -230,3 +270,35 @@ projectOntoOverlayXYPlane = function projectOntoOverlayXYPlane(overlayID, worldP
|
|||
|
||||
return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT);
|
||||
};
|
||||
|
||||
entityHasActions = function (entityID) {
|
||||
return Entities.getActionIDs(entityID).length > 0;
|
||||
};
|
||||
|
||||
ensureDynamic = function (entityID) {
|
||||
// if we distance hold something and keep it very still before releasing it, it ends up
|
||||
// non-dynamic in bullet. If it's too still, give it a little bounce so it will fall.
|
||||
var props = Entities.getEntityProperties(entityID, ["velocity", "dynamic", "parentID"]);
|
||||
if (props.dynamic && props.parentID == NULL_UUID) {
|
||||
var velocity = props.velocity;
|
||||
if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD
|
||||
velocity = { x: 0.0, y: 0.2, z: 0.0 };
|
||||
Entities.editEntity(entityID, { velocity: velocity });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
findGroupParent = function (controllerData, targetProps) {
|
||||
while (targetProps.parentID && targetProps.parentID != NULL_UUID) {
|
||||
// XXX use controllerData.nearbyEntityPropertiesByID ?
|
||||
var parentProps = Entities.getEntityProperties(targetProps.parentID, DISPATCHER_PROPERTIES);
|
||||
if (!parentProps) {
|
||||
break;
|
||||
}
|
||||
parentProps.id = targetProps.parentID;
|
||||
targetProps = parentProps;
|
||||
controllerData.nearbyEntityPropertiesByID[targetProps.id] = targetProps;
|
||||
}
|
||||
|
||||
return targetProps;
|
||||
};
|
||||
|
|
160
scripts/system/controllers/controllerModules/cloneEntity.js
Normal file
160
scripts/system/controllers/controllerModules/cloneEntity.js
Normal file
|
@ -0,0 +1,160 @@
|
|||
"use strict";
|
||||
|
||||
// cloneEntity.js
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
|
||||
/* global Script, Entities, RIGHT_HAND, LEFT_HAND,
|
||||
enableDispatcherModule, disableDispatcherModule, getGrabbableData, Vec3,
|
||||
TRIGGER_ON_VALUE, TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, makeRunningValues, NEAR_GRAB_RADIUS
|
||||
*/
|
||||
|
||||
Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
||||
|
||||
// Object assign polyfill
|
||||
if (typeof Object.assign != 'function') {
|
||||
Object.assign = function(target, varArgs) {
|
||||
if (target === null) {
|
||||
throw new TypeError('Cannot convert undefined or null to object');
|
||||
}
|
||||
var to = Object(target);
|
||||
for (var index = 1; index < arguments.length; index++) {
|
||||
var nextSource = arguments[index];
|
||||
if (nextSource !== null) {
|
||||
for (var nextKey in nextSource) {
|
||||
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
|
||||
to[nextKey] = nextSource[nextKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return to;
|
||||
};
|
||||
}
|
||||
|
||||
(function() {
|
||||
|
||||
function entityIsCloneable(props) {
|
||||
var grabbableData = getGrabbableData(props);
|
||||
return grabbableData.cloneable;
|
||||
}
|
||||
|
||||
function CloneEntity(hand) {
|
||||
this.hand = hand;
|
||||
this.grabbing = false;
|
||||
this.previousParentID = {};
|
||||
this.previousParentJointIndex = {};
|
||||
this.previouslyUnhooked = {};
|
||||
|
||||
this.parameters = makeDispatcherModuleParameters(
|
||||
150,
|
||||
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
|
||||
[],
|
||||
100);
|
||||
|
||||
this.getTargetProps = function (controllerData) {
|
||||
// nearbyEntityProperties is already sorted by length from controller
|
||||
var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
|
||||
for (var i = 0; i < nearbyEntityProperties.length; i++) {
|
||||
var props = nearbyEntityProperties[i];
|
||||
var handPosition = controllerData.controllerLocations[this.hand].position;
|
||||
var distance = Vec3.distance(props.position, handPosition);
|
||||
if (distance > NEAR_GRAB_RADIUS) {
|
||||
break;
|
||||
}
|
||||
if (entityIsCloneable(props)) {
|
||||
return props;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
this.isReady = function (controllerData) {
|
||||
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) {
|
||||
this.waiting = false;
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
|
||||
if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
|
||||
if (this.waiting) {
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
this.waiting = true;
|
||||
}
|
||||
|
||||
var cloneableProps = this.getTargetProps(controllerData);
|
||||
if (!cloneableProps) {
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
|
||||
// we need all the properties, for this
|
||||
cloneableProps = Entities.getEntityProperties(cloneableProps.id);
|
||||
|
||||
var worldEntityProps = controllerData.nearbyEntityProperties[this.hand];
|
||||
var count = 0;
|
||||
worldEntityProps.forEach(function(itemWE) {
|
||||
if (itemWE.name.indexOf('-clone-' + cloneableProps.id) !== -1) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
var grabInfo = getGrabbableData(cloneableProps);
|
||||
|
||||
var limit = grabInfo.cloneLimit ? grabInfo.cloneLimit : 0;
|
||||
if (count >= limit && limit !== 0) {
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
|
||||
cloneableProps.name = cloneableProps.name + '-clone-' + cloneableProps.id;
|
||||
var lifetime = grabInfo.cloneLifetime ? grabInfo.cloneLifetime : 300;
|
||||
var dynamic = grabInfo.cloneDynamic ? grabInfo.cloneDynamic : false;
|
||||
var cUserData = Object.assign({}, cloneableProps.userData);
|
||||
var cProperties = Object.assign({}, cloneableProps);
|
||||
|
||||
try {
|
||||
delete cUserData.grabbableKey.cloneLifetime;
|
||||
delete cUserData.grabbableKey.cloneable;
|
||||
delete cUserData.grabbableKey.cloneDynamic;
|
||||
delete cUserData.grabbableKey.cloneLimit;
|
||||
delete cProperties.id;
|
||||
} catch(e) {
|
||||
}
|
||||
|
||||
cProperties.dynamic = dynamic;
|
||||
cProperties.locked = false;
|
||||
if (!cUserData.grabbableKey) {
|
||||
cUserData.grabbableKey = {};
|
||||
}
|
||||
cUserData.grabbableKey.triggerable = true;
|
||||
cUserData.grabbableKey.grabbable = true;
|
||||
cProperties.lifetime = lifetime;
|
||||
cProperties.userData = JSON.stringify(cUserData);
|
||||
// var cloneID =
|
||||
Entities.addEntity(cProperties);
|
||||
|
||||
return makeRunningValues(false, [], []);
|
||||
};
|
||||
|
||||
this.run = function (controllerData) {
|
||||
};
|
||||
|
||||
this.cleanup = function () {
|
||||
};
|
||||
}
|
||||
|
||||
var leftNearParentingGrabEntity = new CloneEntity(LEFT_HAND);
|
||||
var rightNearParentingGrabEntity = new CloneEntity(RIGHT_HAND);
|
||||
|
||||
enableDispatcherModule("LeftNearParentingGrabEntity", leftNearParentingGrabEntity);
|
||||
enableDispatcherModule("RightNearParentingGrabEntity", rightNearParentingGrabEntity);
|
||||
|
||||
this.cleanup = function () {
|
||||
leftNearParentingGrabEntity.cleanup();
|
||||
rightNearParentingGrabEntity.cleanup();
|
||||
disableDispatcherModule("LeftNearParentingGrabEntity");
|
||||
disableDispatcherModule("RightNearParentingGrabEntity");
|
||||
};
|
||||
Script.scriptEnding.connect(this.cleanup);
|
||||
}());
|
626
scripts/system/controllers/controllerModules/equipEntity.js
Normal file
626
scripts/system/controllers/controllerModules/equipEntity.js
Normal file
|
@ -0,0 +1,626 @@
|
|||
"use strict";
|
||||
|
||||
// equipEntity.js
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
|
||||
/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID,
|
||||
getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule,
|
||||
Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions,
|
||||
Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic
|
||||
*/
|
||||
|
||||
Script.include("/~/system/libraries/Xform.js");
|
||||
Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
||||
Script.include("/~/system/libraries/controllers.js");
|
||||
|
||||
|
||||
var DEFAULT_SPHERE_MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx";
|
||||
var EQUIP_SPHERE_SCALE_FACTOR = 0.65;
|
||||
|
||||
|
||||
// Each overlayInfoSet describes a single equip hotspot.
|
||||
// It is an object with the following keys:
|
||||
// timestamp - last time this object was updated, used to delete stale hotspot overlays.
|
||||
// entityID - entity assosicated with this hotspot
|
||||
// localPosition - position relative to the entity
|
||||
// hotspot - hotspot object
|
||||
// overlays - array of overlay objects created by Overlay.addOverlay()
|
||||
// currentSize - current animated scale value
|
||||
// targetSize - the target of our scale animations
|
||||
// type - "sphere" or "model".
|
||||
function EquipHotspotBuddy() {
|
||||
// holds map from {string} hotspot.key to {object} overlayInfoSet.
|
||||
this.map = {};
|
||||
|
||||
// array of all hotspots that are highlighed.
|
||||
this.highlightedHotspots = [];
|
||||
}
|
||||
EquipHotspotBuddy.prototype.clear = function() {
|
||||
var keys = Object.keys(this.map);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var overlayInfoSet = this.map[keys[i]];
|
||||
this.deleteOverlayInfoSet(overlayInfoSet);
|
||||
}
|
||||
this.map = {};
|
||||
this.highlightedHotspots = [];
|
||||
};
|
||||
EquipHotspotBuddy.prototype.highlightHotspot = function(hotspot) {
|
||||
this.highlightedHotspots.push(hotspot.key);
|
||||
};
|
||||
EquipHotspotBuddy.prototype.updateHotspot = function(hotspot, timestamp) {
|
||||
var overlayInfoSet = this.map[hotspot.key];
|
||||
if (!overlayInfoSet) {
|
||||
// create a new overlayInfoSet
|
||||
overlayInfoSet = {
|
||||
timestamp: timestamp,
|
||||
entityID: hotspot.entityID,
|
||||
localPosition: hotspot.localPosition,
|
||||
hotspot: hotspot,
|
||||
currentSize: 0,
|
||||
targetSize: 1,
|
||||
overlays: []
|
||||
};
|
||||
|
||||
var diameter = hotspot.radius * 2;
|
||||
|
||||
// override default sphere with a user specified model, if it exists.
|
||||
overlayInfoSet.overlays.push(Overlays.addOverlay("model", {
|
||||
name: "hotspot overlay",
|
||||
url: hotspot.modelURL ? hotspot.modelURL : DEFAULT_SPHERE_MODEL_URL,
|
||||
position: hotspot.worldPosition,
|
||||
rotation: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
w: 1
|
||||
},
|
||||
dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR,
|
||||
scale: hotspot.modelScale,
|
||||
ignoreRayIntersection: true
|
||||
}));
|
||||
overlayInfoSet.type = "model";
|
||||
this.map[hotspot.key] = overlayInfoSet;
|
||||
} else {
|
||||
overlayInfoSet.timestamp = timestamp;
|
||||
}
|
||||
};
|
||||
EquipHotspotBuddy.prototype.updateHotspots = function(hotspots, timestamp) {
|
||||
var _this = this;
|
||||
hotspots.forEach(function(hotspot) {
|
||||
_this.updateHotspot(hotspot, timestamp);
|
||||
});
|
||||
this.highlightedHotspots = [];
|
||||
};
|
||||
EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerData) {
|
||||
|
||||
var HIGHLIGHT_SIZE = 1.1;
|
||||
var NORMAL_SIZE = 1.0;
|
||||
|
||||
var keys = Object.keys(this.map);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var overlayInfoSet = this.map[keys[i]];
|
||||
|
||||
// this overlayInfo is highlighted.
|
||||
if (this.highlightedHotspots.indexOf(keys[i]) != -1) {
|
||||
overlayInfoSet.targetSize = HIGHLIGHT_SIZE;
|
||||
} else {
|
||||
overlayInfoSet.targetSize = NORMAL_SIZE;
|
||||
}
|
||||
|
||||
// start to fade out this hotspot.
|
||||
if (overlayInfoSet.timestamp != timestamp) {
|
||||
overlayInfoSet.targetSize = 0;
|
||||
}
|
||||
|
||||
// animate the size.
|
||||
var SIZE_TIMESCALE = 0.1;
|
||||
var tau = deltaTime / SIZE_TIMESCALE;
|
||||
if (tau > 1.0) {
|
||||
tau = 1.0;
|
||||
}
|
||||
overlayInfoSet.currentSize += (overlayInfoSet.targetSize - overlayInfoSet.currentSize) * tau;
|
||||
|
||||
if (overlayInfoSet.timestamp != timestamp && overlayInfoSet.currentSize <= 0.05) {
|
||||
// this is an old overlay, that has finished fading out, delete it!
|
||||
overlayInfoSet.overlays.forEach(Overlays.deleteOverlay);
|
||||
delete this.map[keys[i]];
|
||||
} else {
|
||||
// update overlay position, rotation to follow the object it's attached to.
|
||||
var props = controllerData.nearbyEntityPropertiesByID[overlayInfoSet.entityID];
|
||||
if (props) {
|
||||
var entityXform = new Xform(props.rotation, props.position);
|
||||
var position = entityXform.xformPoint(overlayInfoSet.localPosition);
|
||||
|
||||
var dimensions;
|
||||
if (overlayInfoSet.type == "sphere") {
|
||||
dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize * EQUIP_SPHERE_SCALE_FACTOR;
|
||||
} else {
|
||||
dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize;
|
||||
}
|
||||
|
||||
overlayInfoSet.overlays.forEach(function(overlay) {
|
||||
Overlays.editOverlay(overlay, {
|
||||
position: position,
|
||||
rotation: props.rotation,
|
||||
dimensions: dimensions
|
||||
});
|
||||
});
|
||||
} else {
|
||||
overlayInfoSet.overlays.forEach(Overlays.deleteOverlay);
|
||||
delete this.map[keys[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints";
|
||||
|
||||
var EQUIP_RADIUS = 0.2; // radius used for palm vs equip-hotspot for equipping.
|
||||
|
||||
var HAPTIC_PULSE_STRENGTH = 1.0;
|
||||
var HAPTIC_PULSE_DURATION = 13.0;
|
||||
var HAPTIC_TEXTURE_STRENGTH = 0.1;
|
||||
var HAPTIC_TEXTURE_DURATION = 3.0;
|
||||
var HAPTIC_TEXTURE_DISTANCE = 0.002;
|
||||
var HAPTIC_DEQUIP_STRENGTH = 0.75;
|
||||
var HAPTIC_DEQUIP_DURATION = 50.0;
|
||||
|
||||
var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing
|
||||
var TRIGGER_OFF_VALUE = 0.1;
|
||||
var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab
|
||||
var BUMPER_ON_VALUE = 0.5;
|
||||
|
||||
|
||||
function getWearableData(props) {
|
||||
var wearable = {};
|
||||
try {
|
||||
if (!props.userDataParsed) {
|
||||
props.userDataParsed = JSON.parse(props.userData);
|
||||
}
|
||||
wearable = props.userDataParsed.wearable ? props.userDataParsed.wearable : {};
|
||||
} catch (err) {
|
||||
}
|
||||
return wearable;
|
||||
}
|
||||
function getEquipHotspotsData(props) {
|
||||
var equipHotspots = [];
|
||||
try {
|
||||
if (!props.userDataParsed) {
|
||||
props.userDataParsed = JSON.parse(props.userData);
|
||||
}
|
||||
equipHotspots = props.userDataParsed.equipHotspots ? props.userDataParsed.equipHotspots : [];
|
||||
} catch (err) {
|
||||
}
|
||||
return equipHotspots;
|
||||
}
|
||||
|
||||
function getAttachPointSettings() {
|
||||
try {
|
||||
var str = Settings.getValue(ATTACH_POINT_SETTINGS);
|
||||
if (str === "false" || str === "") {
|
||||
return {};
|
||||
} else {
|
||||
return JSON.parse(str);
|
||||
}
|
||||
} catch (err) {
|
||||
print("Error parsing attachPointSettings: " + err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function setAttachPointSettings(attachPointSettings) {
|
||||
var str = JSON.stringify(attachPointSettings);
|
||||
Settings.setValue(ATTACH_POINT_SETTINGS, str);
|
||||
}
|
||||
|
||||
function getAttachPointForHotspotFromSettings(hotspot, hand) {
|
||||
var attachPointSettings = getAttachPointSettings();
|
||||
var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand";
|
||||
var joints = attachPointSettings[hotspot.key];
|
||||
if (joints) {
|
||||
return joints[jointName];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, offsetRotation) {
|
||||
var attachPointSettings = getAttachPointSettings();
|
||||
var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand";
|
||||
var joints = attachPointSettings[hotspot.key];
|
||||
if (!joints) {
|
||||
joints = {};
|
||||
attachPointSettings[hotspot.key] = joints;
|
||||
}
|
||||
joints[jointName] = [offsetPosition, offsetRotation];
|
||||
setAttachPointSettings(attachPointSettings);
|
||||
}
|
||||
|
||||
function EquipEntity(hand) {
|
||||
this.hand = hand;
|
||||
this.targetEntityID = null;
|
||||
this.prevHandIsUpsideDown = false;
|
||||
this.triggerValue = 0;
|
||||
|
||||
this.parameters = makeDispatcherModuleParameters(
|
||||
300,
|
||||
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
|
||||
[],
|
||||
100);
|
||||
|
||||
var equipHotspotBuddy = new EquipHotspotBuddy();
|
||||
|
||||
// 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.
|
||||
// * modelURL {string} url for model to use instead of default sphere.
|
||||
// * modelScale {Vec3} scale factor for model
|
||||
this.collectEquipHotspots = function(props) {
|
||||
var result = [];
|
||||
var entityID = props.id;
|
||||
var entityXform = new Xform(props.rotation, props.position);
|
||||
|
||||
var equipHotspotsProps = getEquipHotspotsData(props);
|
||||
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,
|
||||
modelURL: hotspot.modelURL,
|
||||
modelScale: hotspot.modelScale
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var wearableProps = getWearableData(props);
|
||||
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,
|
||||
modelURL: null,
|
||||
modelScale: null
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
this.hotspotIsEquippable = function(hotspot, controllerData) {
|
||||
var props = controllerData.nearbyEntityPropertiesByID[hotspot.entityID];
|
||||
|
||||
var hasParent = true;
|
||||
if (props.parentID === NULL_UUID) {
|
||||
hasParent = false;
|
||||
}
|
||||
if (hasParent || props.locked || entityHasActions(hotspot.entityID)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
this.handToController = function() {
|
||||
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
|
||||
};
|
||||
|
||||
this.updateSmoothedTrigger = function(controllerData) {
|
||||
var triggerValue = controllerData.triggerValues[this.hand];
|
||||
// smooth out trigger value
|
||||
this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) +
|
||||
(triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO));
|
||||
};
|
||||
|
||||
this.triggerSmoothedGrab = function() {
|
||||
return this.triggerClicked;
|
||||
};
|
||||
|
||||
this.triggerSmoothedSqueezed = function() {
|
||||
return this.triggerValue > TRIGGER_ON_VALUE;
|
||||
};
|
||||
|
||||
this.triggerSmoothedReleased = function() {
|
||||
return this.triggerValue < TRIGGER_OFF_VALUE;
|
||||
};
|
||||
|
||||
this.secondaryReleased = function() {
|
||||
return this.rawSecondaryValue < BUMPER_ON_VALUE;
|
||||
};
|
||||
|
||||
this.chooseNearEquipHotspots = function(candidateEntityProps, controllerData) {
|
||||
var _this = this;
|
||||
var collectedHotspots = flatten(candidateEntityProps.map(function(props) {
|
||||
return _this.collectEquipHotspots(props);
|
||||
}));
|
||||
var controllerLocation = controllerData.controllerLocations[_this.hand];
|
||||
var worldControllerPosition = controllerLocation.position;
|
||||
var equippableHotspots = collectedHotspots.filter(function(hotspot) {
|
||||
var hotspotDistance = Vec3.distance(hotspot.worldPosition, worldControllerPosition);
|
||||
return _this.hotspotIsEquippable(hotspot, controllerData) &&
|
||||
hotspotDistance < hotspot.radius;
|
||||
});
|
||||
return equippableHotspots;
|
||||
};
|
||||
|
||||
this.chooseBestEquipHotspot = function(candidateEntityProps, controllerData) {
|
||||
var equippableHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData);
|
||||
if (equippableHotspots.length > 0) {
|
||||
// sort by distance;
|
||||
var controllerLocation = controllerData.controllerLocations[this.hand];
|
||||
var worldControllerPosition = controllerLocation.position;
|
||||
equippableHotspots.sort(function(a, b) {
|
||||
var aDistance = Vec3.distance(a.worldPosition, worldControllerPosition);
|
||||
var bDistance = Vec3.distance(b.worldPosition, worldControllerPosition);
|
||||
return aDistance - bDistance;
|
||||
});
|
||||
return equippableHotspots[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
this.dropGestureReset = function() {
|
||||
this.prevHandIsUpsideDown = false;
|
||||
};
|
||||
|
||||
this.dropGestureProcess = function (deltaTime) {
|
||||
var worldHandRotation = getControllerWorldLocation(this.handToController(), true).orientation;
|
||||
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 DROP_ANGLE = Math.PI / 3;
|
||||
var HYSTERESIS_FACTOR = 1.1;
|
||||
var ROTATION_ENTER_THRESHOLD = Math.cos(DROP_ANGLE);
|
||||
var ROTATION_EXIT_THRESHOLD = Math.cos(DROP_ANGLE * HYSTERESIS_FACTOR);
|
||||
var rotationThreshold = this.prevHandIsUpsideDown ? ROTATION_EXIT_THRESHOLD : ROTATION_ENTER_THRESHOLD;
|
||||
|
||||
var handIsUpsideDown = false;
|
||||
if (Vec3.dot(worldHandUpAxis, DOWN) > rotationThreshold) {
|
||||
handIsUpsideDown = true;
|
||||
}
|
||||
|
||||
if (handIsUpsideDown != this.prevHandIsUpsideDown) {
|
||||
this.prevHandIsUpsideDown = handIsUpsideDown;
|
||||
Controller.triggerHapticPulse(HAPTIC_DEQUIP_STRENGTH, HAPTIC_DEQUIP_DURATION, this.hand);
|
||||
}
|
||||
|
||||
return handIsUpsideDown;
|
||||
};
|
||||
|
||||
this.clearEquipHaptics = function() {
|
||||
this.prevPotentialEquipHotspot = null;
|
||||
};
|
||||
|
||||
this.updateEquipHaptics = function(potentialEquipHotspot, currentLocation) {
|
||||
if (potentialEquipHotspot && !this.prevPotentialEquipHotspot ||
|
||||
!potentialEquipHotspot && this.prevPotentialEquipHotspot) {
|
||||
Controller.triggerHapticPulse(HAPTIC_TEXTURE_STRENGTH, HAPTIC_TEXTURE_DURATION, this.hand);
|
||||
this.lastHapticPulseLocation = currentLocation;
|
||||
} else if (potentialEquipHotspot &&
|
||||
Vec3.distance(this.lastHapticPulseLocation, currentLocation) > HAPTIC_TEXTURE_DISTANCE) {
|
||||
Controller.triggerHapticPulse(HAPTIC_TEXTURE_STRENGTH, HAPTIC_TEXTURE_DURATION, this.hand);
|
||||
this.lastHapticPulseLocation = currentLocation;
|
||||
}
|
||||
this.prevPotentialEquipHotspot = potentialEquipHotspot;
|
||||
};
|
||||
|
||||
this.startEquipEntity = function (controllerData) {
|
||||
this.dropGestureReset();
|
||||
this.clearEquipHaptics();
|
||||
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
|
||||
|
||||
// if an object is "equipped" and has a predefined offset, use it.
|
||||
var offsets = getAttachPointForHotspotFromSettings(this.grabbedHotspot, this.hand);
|
||||
if (offsets) {
|
||||
this.offsetPosition = offsets[0];
|
||||
this.offsetRotation = offsets[1];
|
||||
} else {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
var handJointIndex;
|
||||
if (this.ignoreIK) {
|
||||
handJointIndex = this.controllerJointIndex;
|
||||
} else {
|
||||
handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
|
||||
}
|
||||
|
||||
var reparentProps = {
|
||||
parentID: AVATAR_SELF_ID,
|
||||
parentJointIndex: handJointIndex,
|
||||
localVelocity: {x: 0, y: 0, z: 0},
|
||||
localAngularVelocity: {x: 0, y: 0, z: 0},
|
||||
localPosition: this.offsetPosition,
|
||||
localRotation: this.offsetRotation
|
||||
};
|
||||
Entities.editEntity(this.targetEntityID, reparentProps);
|
||||
|
||||
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
||||
Entities.callEntityMethod(this.targetEntityID, "startEquip", args);
|
||||
|
||||
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
|
||||
action: 'equip',
|
||||
grabbedEntity: this.targetEntityID,
|
||||
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
|
||||
}));
|
||||
};
|
||||
|
||||
this.endEquipEntity = function () {
|
||||
Entities.editEntity(this.targetEntityID, {
|
||||
parentID: NULL_UUID,
|
||||
parentJointIndex: -1
|
||||
});
|
||||
|
||||
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
||||
Entities.callEntityMethod(this.targetEntityID, "releaseEquip", args);
|
||||
|
||||
ensureDynamic(this.targetEntityID);
|
||||
this.targetEntityID = null;
|
||||
};
|
||||
|
||||
this.updateInputs = function (controllerData) {
|
||||
this.rawTriggerValue = controllerData.triggerValues[this.hand];
|
||||
this.triggerClicked = controllerData.triggerClicks[this.hand];
|
||||
this.rawSecondaryValue = controllerData.secondaryValues[this.hand];
|
||||
this.updateSmoothedTrigger(controllerData);
|
||||
};
|
||||
|
||||
this.checkNearbyHotspots = function (controllerData, deltaTime, timestamp) {
|
||||
this.controllerJointIndex = getControllerJointIndex(this.hand);
|
||||
|
||||
if (this.triggerSmoothedReleased() && this.secondaryReleased()) {
|
||||
this.waitForTriggerRelease = false;
|
||||
}
|
||||
|
||||
var controllerLocation = getControllerWorldLocation(this.handToController(), true);
|
||||
var worldHandPosition = controllerLocation.position;
|
||||
var candidateEntityProps = controllerData.nearbyEntityProperties[this.hand];
|
||||
|
||||
var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntityProps, controllerData);
|
||||
if (!this.waitForTriggerRelease) {
|
||||
this.updateEquipHaptics(potentialEquipHotspot, worldHandPosition);
|
||||
}
|
||||
|
||||
var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData);
|
||||
equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp);
|
||||
if (potentialEquipHotspot) {
|
||||
equipHotspotBuddy.highlightHotspot(potentialEquipHotspot);
|
||||
}
|
||||
|
||||
equipHotspotBuddy.update(deltaTime, timestamp, controllerData);
|
||||
|
||||
if (potentialEquipHotspot) {
|
||||
if (this.triggerSmoothedSqueezed() && !this.waitForTriggerRelease) {
|
||||
this.grabbedHotspot = potentialEquipHotspot;
|
||||
this.targetEntityID = this.grabbedHotspot.entityID;
|
||||
this.startEquipEntity(controllerData);
|
||||
}
|
||||
return makeRunningValues(true, [potentialEquipHotspot.entityID], []);
|
||||
} else {
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
};
|
||||
|
||||
this.isReady = function (controllerData, deltaTime) {
|
||||
var timestamp = Date.now();
|
||||
this.updateInputs(controllerData);
|
||||
return this.checkNearbyHotspots(controllerData, deltaTime, timestamp);
|
||||
};
|
||||
|
||||
this.run = function (controllerData, deltaTime) {
|
||||
var timestamp = Date.now();
|
||||
this.updateInputs(controllerData);
|
||||
|
||||
if (!this.targetEntityID) {
|
||||
return this.checkNearbyHotspots(controllerData, deltaTime, timestamp);
|
||||
}
|
||||
|
||||
if (controllerData.secondaryValues[this.hand]) {
|
||||
// this.secondaryReleased() will always be true when not depressed
|
||||
// so we cannot simply rely on that for release - ensure that the
|
||||
// trigger was first "prepared" by being pushed in before the release
|
||||
this.preparingHoldRelease = true;
|
||||
}
|
||||
|
||||
if (this.preparingHoldRelease && !controllerData.secondaryValues[this.hand]) {
|
||||
// we have an equipped object and the secondary trigger was released
|
||||
// short-circuit the other checks and release it
|
||||
this.preparingHoldRelease = false;
|
||||
this.endEquipEntity();
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
|
||||
var dropDetected = this.dropGestureProcess(deltaTime);
|
||||
|
||||
if (this.triggerSmoothedReleased()) {
|
||||
this.waitForTriggerRelease = false;
|
||||
}
|
||||
|
||||
if (dropDetected && this.prevDropDetected != dropDetected) {
|
||||
this.waitForTriggerRelease = true;
|
||||
}
|
||||
|
||||
// highlight the grabbed hotspot when the dropGesture is detected.
|
||||
if (dropDetected) {
|
||||
equipHotspotBuddy.updateHotspot(this.grabbedHotspot, timestamp);
|
||||
equipHotspotBuddy.highlightHotspot(this.grabbedHotspot);
|
||||
}
|
||||
|
||||
if (dropDetected && !this.waitForTriggerRelease && this.triggerSmoothedGrab()) {
|
||||
this.waitForTriggerRelease = true;
|
||||
// store the offset attach points into preferences.
|
||||
if (this.grabbedHotspot && this.targetEntityID) {
|
||||
var prefprops = Entities.getEntityProperties(this.targetEntityID, ["localPosition", "localRotation"]);
|
||||
if (prefprops && prefprops.localPosition && prefprops.localRotation) {
|
||||
storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand,
|
||||
prefprops.localPosition, prefprops.localRotation);
|
||||
}
|
||||
}
|
||||
|
||||
this.endEquipEntity();
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
this.prevDropDetected = dropDetected;
|
||||
|
||||
equipHotspotBuddy.update(deltaTime, timestamp, controllerData);
|
||||
|
||||
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
||||
Entities.callEntityMethod(this.targetEntityID, "continueEquip", args);
|
||||
|
||||
return makeRunningValues(true, [this.targetEntityID], []);
|
||||
};
|
||||
|
||||
this.cleanup = function () {
|
||||
if (this.targetEntityID) {
|
||||
this.endEquipEntity();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var leftEquipEntity = new EquipEntity(LEFT_HAND);
|
||||
var rightEquipEntity = new EquipEntity(RIGHT_HAND);
|
||||
|
||||
enableDispatcherModule("LeftEquipEntity", leftEquipEntity);
|
||||
enableDispatcherModule("RightEquipEntity", rightEquipEntity);
|
||||
|
||||
this.cleanup = function () {
|
||||
leftEquipEntity.cleanup();
|
||||
rightEquipEntity.cleanup();
|
||||
disableDispatcherModule("LeftEquipEntity");
|
||||
disableDispatcherModule("RightEquipEntity");
|
||||
};
|
||||
Script.scriptEnding.connect(this.cleanup);
|
||||
}());
|
|
@ -308,21 +308,8 @@ Script.include("/~/system/libraries/controllers.js");
|
|||
this.previousRoomControllerPosition = roomControllerPosition;
|
||||
};
|
||||
|
||||
this.ensureDynamic = function () {
|
||||
// if we distance hold something and keep it very still before releasing it, it ends up
|
||||
// non-dynamic in bullet. If it's too still, give it a little bounce so it will fall.
|
||||
var props = Entities.getEntityProperties(this.grabbedThingID, ["velocity", "dynamic", "parentID"]);
|
||||
if (props.dynamic && props.parentID == NULL_UUID) {
|
||||
var velocity = props.velocity;
|
||||
if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD
|
||||
velocity = { x: 0.0, y: 0.2, z: 0.0 };
|
||||
Entities.editEntity(this.grabbedThingID, { velocity: velocity });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.endNearGrabAction = function () {
|
||||
this.ensureDynamic();
|
||||
ensureDynamic(this.grabbedThingID);
|
||||
this.distanceHolding = false;
|
||||
this.distanceRotating = false;
|
||||
Entities.deleteAction(this.grabbedThingID, this.actionID);
|
||||
|
@ -397,7 +384,8 @@ Script.include("/~/system/libraries/controllers.js");
|
|||
if (entityIsDistanceGrabbable(targetProps)) {
|
||||
this.grabbedThingID = entityID;
|
||||
this.grabbedDistance = rayPickInfo.distance;
|
||||
var otherModuleName = this.hand == RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity";
|
||||
var otherModuleName =
|
||||
this.hand == RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity";
|
||||
var otherFarGrabModule = getEnabledModuleByName(otherModuleName);
|
||||
if (otherFarGrabModule.grabbedThingID == this.grabbedThingID) {
|
||||
this.distanceRotating = true;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
getControllerJointIndex, getGrabbableData, NULL_UUID, enableDispatcherModule, disableDispatcherModule,
|
||||
propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, entityIsGrabbable,
|
||||
Quat, Vec3, MSECS_PER_SEC, getControllerWorldLocation, makeDispatcherModuleParameters, makeRunningValues,
|
||||
TRIGGER_OFF_VALUE
|
||||
TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, findGroupParent
|
||||
*/
|
||||
|
||||
Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
||||
|
@ -146,7 +146,17 @@ Script.include("/~/system/libraries/controllers.js");
|
|||
var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
|
||||
for (var i = 0; i < nearbyEntityProperties.length; i++) {
|
||||
var props = nearbyEntityProperties[i];
|
||||
var handPosition = controllerData.controllerLocations[this.hand].position;
|
||||
var distance = Vec3.distance(props.position, handPosition);
|
||||
if (distance > NEAR_GRAB_RADIUS) {
|
||||
break;
|
||||
}
|
||||
if (entityIsGrabbable(props)) {
|
||||
// if we've attempted to grab a child, roll up to the root of the tree
|
||||
var groupRootProps = findGroupParent(controllerData, props);
|
||||
if (entityIsGrabbable(groupRootProps)) {
|
||||
return groupRootProps;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
@ -157,23 +167,18 @@ Script.include("/~/system/libraries/controllers.js");
|
|||
this.targetEntityID = null;
|
||||
|
||||
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) {
|
||||
makeRunningValues(false, [], []);
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
|
||||
var targetProps = this.getTargetProps(controllerData);
|
||||
if (targetProps) {
|
||||
if (!propsArePhysical(targetProps)) {
|
||||
// XXX make sure no highlights are enabled from this module
|
||||
return makeRunningValues(false, [], []); // let nearParentGrabEntity handle it
|
||||
} else {
|
||||
this.targetEntityID = targetProps.id;
|
||||
ContextOverlay.entityWithContextOverlay = this.targetEntityID;
|
||||
ContextOverlay.enabled = true;
|
||||
// XXX highlight this.targetEntityID here
|
||||
return makeRunningValues(true, [this.targetEntityID], []);
|
||||
}
|
||||
} else {
|
||||
// XXX make sure no highlights are enabled from this module
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
};
|
||||
|
@ -199,26 +204,8 @@ Script.include("/~/system/libraries/controllers.js");
|
|||
|
||||
var targetProps = this.getTargetProps(controllerData);
|
||||
if (targetProps) {
|
||||
|
||||
// XXX
|
||||
var rayPickInfo = controllerData.rayPicks[this.hand];
|
||||
var pointerEvent = {
|
||||
type: "Move",
|
||||
id: this.hand + 1, // 0 is reserved for hardware mouse
|
||||
pos2D: projectOntoEntityXYPlane(rayPickInfo.entityID, rayPickInfo.intersection, targetProps),
|
||||
pos3D: rayPickInfo.intersection,
|
||||
normal: rayPickInfo.normal,
|
||||
direction: rayPickInfo.searchRay.direction,
|
||||
button: "Secondary"
|
||||
};
|
||||
if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.entityID, pointerEvent)) {
|
||||
}
|
||||
// XXX
|
||||
|
||||
|
||||
if (controllerData.triggerClicks[this.hand] == 1) {
|
||||
// stop highlighting, switch to grabbing
|
||||
// XXX stop highlight here
|
||||
// switch to grabbing
|
||||
this.startNearGrabAction(controllerData, targetProps);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID,
|
||||
getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule,
|
||||
propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE,
|
||||
makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues
|
||||
makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS,
|
||||
findGroupParent, Vec3
|
||||
*/
|
||||
|
||||
Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
||||
|
@ -29,7 +30,7 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
|
||||
this.parameters = makeDispatcherModuleParameters(
|
||||
500,
|
||||
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
|
||||
this.hand === RIGHT_HAND ? ["rightHand", "rightHandTrigger"] : ["leftHand", "leftHandTrigger"],
|
||||
[],
|
||||
100);
|
||||
|
||||
|
@ -64,6 +65,7 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
};
|
||||
|
||||
this.startNearParentingGrabEntity = function (controllerData, targetProps) {
|
||||
|
||||
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
|
||||
|
||||
var handJointIndex;
|
||||
|
@ -80,8 +82,8 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
var reparentProps = {
|
||||
parentID: AVATAR_SELF_ID,
|
||||
parentJointIndex: handJointIndex,
|
||||
velocity: {x: 0, y: 0, z: 0},
|
||||
angularVelocity: {x: 0, y: 0, z: 0}
|
||||
localVelocity: {x: 0, y: 0, z: 0},
|
||||
localAngularVelocity: {x: 0, y: 0, z: 0}
|
||||
};
|
||||
|
||||
if (this.thisHandIsParent(targetProps)) {
|
||||
|
@ -112,8 +114,8 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
Entities.editEntity(this.targetEntityID, {
|
||||
parentID: this.previousParentID[this.targetEntityID],
|
||||
parentJointIndex: this.previousParentJointIndex[this.targetEntityID],
|
||||
velocity: {x: 0, y: 0, z: 0},
|
||||
angularVelocity: {x: 0, y: 0, z: 0}
|
||||
localVelocity: {x: 0, y: 0, z: 0},
|
||||
localAngularVelocity: {x: 0, y: 0, z: 0}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -129,7 +131,17 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
|
||||
for (var i = 0; i < nearbyEntityProperties.length; i++) {
|
||||
var props = nearbyEntityProperties[i];
|
||||
var handPosition = controllerData.controllerLocations[this.hand].position;
|
||||
var distance = Vec3.distance(props.position, handPosition);
|
||||
if (distance > NEAR_GRAB_RADIUS) {
|
||||
break;
|
||||
}
|
||||
if (entityIsGrabbable(props)) {
|
||||
// if we've attempted to grab a child, roll up to the root of the tree
|
||||
var groupRootProps = findGroupParent(controllerData, props);
|
||||
if (entityIsGrabbable(groupRootProps)) {
|
||||
return groupRootProps;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
@ -141,21 +153,18 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
this.grabbing = false;
|
||||
|
||||
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) {
|
||||
makeRunningValues(false, [], []);
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
|
||||
var targetProps = this.getTargetProps(controllerData);
|
||||
if (targetProps) {
|
||||
if (propsArePhysical(targetProps)) {
|
||||
// XXX make sure no highlights are enabled from this module
|
||||
return makeRunningValues(false, [], []); // let nearActionGrabEntity handle it
|
||||
} else {
|
||||
this.targetEntityID = targetProps.id;
|
||||
// XXX highlight this.targetEntityID here
|
||||
return makeRunningValues(true, [this.targetEntityID], []);
|
||||
}
|
||||
} else {
|
||||
// XXX make sure no highlights are enabled from this module
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
};
|
||||
|
@ -176,8 +185,7 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
|||
return readiness;
|
||||
}
|
||||
if (controllerData.triggerClicks[this.hand] == 1) {
|
||||
// stop highlighting, switch to grabbing
|
||||
// XXX stop highlight here
|
||||
// switch to grab
|
||||
var targetProps = this.getTargetProps(controllerData);
|
||||
if (targetProps) {
|
||||
this.grabbing = true;
|
||||
|
|
116
scripts/system/controllers/controllerModules/nearTrigger.js
Normal file
116
scripts/system/controllers/controllerModules/nearTrigger.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
"use strict";
|
||||
|
||||
// nearTrigger.js
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
|
||||
/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND,
|
||||
enableDispatcherModule, disableDispatcherModule, getGrabbableData, Vec3,
|
||||
TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, makeRunningValues, NEAR_GRAB_RADIUS
|
||||
*/
|
||||
|
||||
Script.include("/~/system/controllers/controllerDispatcherUtils.js");
|
||||
|
||||
(function() {
|
||||
|
||||
function entityWantsNearTrigger(props) {
|
||||
var grabbableData = getGrabbableData(props);
|
||||
return grabbableData.triggerable || grabbableData.wantsTrigger;
|
||||
}
|
||||
|
||||
function NearTriggerEntity(hand) {
|
||||
this.hand = hand;
|
||||
this.targetEntityID = null;
|
||||
this.grabbing = false;
|
||||
this.previousParentID = {};
|
||||
this.previousParentJointIndex = {};
|
||||
this.previouslyUnhooked = {};
|
||||
|
||||
this.parameters = makeDispatcherModuleParameters(
|
||||
200,
|
||||
this.hand === RIGHT_HAND ? ["rightHandTrigger"] : ["leftHandTrigger"],
|
||||
[],
|
||||
100);
|
||||
|
||||
this.getTargetProps = function (controllerData) {
|
||||
// nearbyEntityProperties is already sorted by length from controller
|
||||
var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
|
||||
for (var i = 0; i < nearbyEntityProperties.length; i++) {
|
||||
var props = nearbyEntityProperties[i];
|
||||
var handPosition = controllerData.controllerLocations[this.hand].position;
|
||||
var distance = Vec3.distance(props.position, handPosition);
|
||||
if (distance > NEAR_GRAB_RADIUS) {
|
||||
break;
|
||||
}
|
||||
if (entityWantsNearTrigger(props)) {
|
||||
return props;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
this.startNearTrigger = function (controllerData) {
|
||||
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
||||
Entities.callEntityMethod(this.targetEntityID, "startNearTrigger", args);
|
||||
};
|
||||
|
||||
this.continueNearTrigger = function (controllerData) {
|
||||
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
||||
Entities.callEntityMethod(this.targetEntityID, "continueNearTrigger", args);
|
||||
};
|
||||
|
||||
this.endNearTrigger = function (controllerData) {
|
||||
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
||||
Entities.callEntityMethod(this.targetEntityID, "endNearTrigger", args);
|
||||
};
|
||||
|
||||
this.isReady = function (controllerData) {
|
||||
this.targetEntityID = null;
|
||||
|
||||
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) {
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
|
||||
var targetProps = this.getTargetProps(controllerData);
|
||||
if (targetProps) {
|
||||
this.targetEntityID = targetProps.id;
|
||||
this.startNearTrigger(controllerData);
|
||||
return makeRunningValues(true, [this.targetEntityID], []);
|
||||
} else {
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
};
|
||||
|
||||
this.run = function (controllerData) {
|
||||
if (controllerData.triggerClicks[this.hand] == 0) {
|
||||
this.endNearTrigger(controllerData);
|
||||
return makeRunningValues(false, [], []);
|
||||
}
|
||||
|
||||
this.continueNearTrigger(controllerData);
|
||||
return makeRunningValues(true, [this.targetEntityID], []);
|
||||
};
|
||||
|
||||
this.cleanup = function () {
|
||||
if (this.targetEntityID) {
|
||||
this.endNearTrigger();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var leftNearTriggerEntity = new NearTriggerEntity(LEFT_HAND);
|
||||
var rightNearTriggerEntity = new NearTriggerEntity(RIGHT_HAND);
|
||||
|
||||
enableDispatcherModule("LeftNearTriggerEntity", leftNearTriggerEntity);
|
||||
enableDispatcherModule("RightNearTriggerEntity", rightNearTriggerEntity);
|
||||
|
||||
this.cleanup = function () {
|
||||
leftNearTriggerEntity.cleanup();
|
||||
rightNearTriggerEntity.cleanup();
|
||||
disableDispatcherModule("LeftNearTriggerEntity");
|
||||
disableDispatcherModule("RightNearTriggerEntity");
|
||||
};
|
||||
Script.scriptEnding.connect(this.cleanup);
|
||||
}());
|
|
@ -23,7 +23,10 @@ var CONTOLLER_SCRIPTS = [
|
|||
"controllerModules/nearParentGrabOverlay.js",
|
||||
"controllerModules/nearActionGrabEntity.js",
|
||||
"controllerModules/farActionGrabEntity.js",
|
||||
"controllerModules/tabletStylusInput.js"
|
||||
"controllerModules/tabletStylusInput.js",
|
||||
"controllerModules/equipEntity.js",
|
||||
"controllerModules/nearTrigger.js",
|
||||
"controllerModules/cloneEntity.js"
|
||||
];
|
||||
|
||||
var DEBUG_MENU_ITEM = "Debug defaultScripts.js";
|
||||
|
|
Loading…
Reference in a new issue