Merge branch 'controller-dispatcher-1' of github.com:sethalves/hifi into controller-dispatcher-1

This commit is contained in:
Seth Alves 2017-08-16 09:14:10 -07:00
commit 58a63a14fb
9 changed files with 1074 additions and 114 deletions

View file

@ -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);

View file

@ -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;
};

View 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);
}());

View 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);
}());

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;

View 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);
}());

View file

@ -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";