Merge branch 'master' of https://github.com/highfidelity/hifi into 21542

This commit is contained in:
Cain Kilgore 2017-09-15 06:05:43 +01:00
commit 0526d32802
33 changed files with 6371 additions and 4872 deletions

View file

@ -55,6 +55,9 @@ module.exports = {
"XMLHttpRequest": false,
"location": false,
"print": false,
"RayPick": false,
"LaserPointers": false,
"ContextOverlay": false,
"module": false
},
"rules": {

View file

@ -137,48 +137,11 @@ Item {
}
}
}
// Left gray MouseArea
MouseArea {
anchors.left: parent.left;
anchors.right: textContainer.left;
anchors.top: textContainer.top;
anchors.bottom: textContainer.bottom;
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
letterbox.visible = false
}
}
// Right gray MouseArea
MouseArea {
anchors.left: textContainer.left;
anchors.right: parent.left;
anchors.top: textContainer.top;
anchors.bottom: textContainer.bottom;
acceptedButtons: Qt.LeftButton
onClicked: {
letterbox.visible = false
}
}
// Top gray MouseArea
MouseArea {
anchors.left: parent.left;
anchors.right: parent.right;
anchors.top: parent.top;
anchors.bottom: textContainer.top;
acceptedButtons: Qt.LeftButton
onClicked: {
letterbox.visible = false
}
}
// Bottom gray MouseArea
MouseArea {
anchors.left: parent.left;
anchors.right: parent.right;
anchors.top: textContainer.bottom;
anchors.bottom: parent.bottom;
acceptedButtons: Qt.LeftButton
onClicked: {
letterbox.visible = false
letterbox.visible = false;
}
}
}

View file

@ -387,9 +387,9 @@ ScrollingWindow {
readOnly: true
Connections {
target: treeView
target: treeView.selection
onCurrentIndexChanged: {
var path = scriptsModel.data(treeView.currentIndex, 0x100)
var path = scriptsModel.data(treeView.selection.currentIndex, 0x100)
if (path) {
selectedScript.text = path
} else {

View file

@ -416,9 +416,9 @@ Rectangle {
readOnly: true
Connections {
target: treeView
target: treeView.selection
onCurrentIndexChanged: {
var path = scriptsModel.data(treeView.currentIndex, 0x100)
var path = scriptsModel.data(treeView.selection.currentIndex, 0x100)
if (path) {
selectedScript.text = path
} else {

View file

@ -121,14 +121,19 @@ bool PolyLineEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityP
}
void PolyLineEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
static const QUrl DEFAULT_POLYLINE_TEXTURE = QUrl(PathUtils::resourcesPath() + "images/paintStroke.png");
QUrl entityTextures = DEFAULT_POLYLINE_TEXTURE;
if (entity->texturesChanged()) {
entity->resetTexturesChanged();
auto textures = entity->getTextures();
QString path = textures.isEmpty() ? PathUtils::resourcesPath() + "images/paintStroke.png" : textures;
if (!_texture || _lastTextures != path) {
_texture = DependencyManager::get<TextureCache>()->getTexture(QUrl(path));
if (!textures.isEmpty()) {
entityTextures = QUrl(textures);
}
}
if (!_texture || _texture->getURL() != entityTextures) {
_texture = DependencyManager::get<TextureCache>()->getTexture(entityTextures);
}
}
void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) {
@ -140,6 +145,10 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo
auto normalsChanged = entity->normalsChanged();
entity->resetPolyLineChanged();
_polylineTransform = Transform();
_polylineTransform.setTranslation(entity->getPosition());
_polylineTransform.setRotation(entity->getRotation());
if (pointsChanged) {
_lastPoints = entity->getLinePoints();
}
@ -217,13 +226,13 @@ void PolyLineEntityRenderer::doRender(RenderArgs* args) {
Q_ASSERT(args->_batch);
gpu::Batch& batch = *args->_batch;
batch.setModelTransform(Transform{ _modelTransform }.setScale(vec3(1)));
batch.setModelTransform(_polylineTransform);
batch.setUniformBuffer(PAINTSTROKE_UNIFORM_SLOT, _uniformBuffer);
if (_texture->isLoaded()) {
if (_texture && _texture->isLoaded()) {
batch.setResourceTexture(PAINTSTROKE_TEXTURE_SLOT, _texture->getGPUTexture());
} else {
batch.setResourceTexture(PAINTSTROKE_TEXTURE_SLOT, nullptr);
batch.setResourceTexture(PAINTSTROKE_TEXTURE_SLOT, DependencyManager::get<TextureCache>()->getWhiteTexture());
}
batch.setInputFormat(polylineFormat);

View file

@ -47,6 +47,7 @@ protected:
void updateGeometry(const std::vector<Vertex>& vertices);
static std::vector<Vertex> updateVertices(const QVector<glm::vec3>& points, const QVector<glm::vec3>& normals, const QVector<float>& strokeWidths);
Transform _polylineTransform;
QVector<glm::vec3> _lastPoints;
QVector<glm::vec3> _lastNormals;
QVector<float> _lastStrokeWidths;
@ -54,7 +55,6 @@ protected:
gpu::BufferView _uniformBuffer;
uint32_t _numVertices { 0 };
bool _empty{ true };
QString _lastTextures;
NetworkTexturePointer _texture;
};

View file

@ -176,6 +176,10 @@ void ZoneEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scen
_lastRotation = entity->getRotation();
_lastDimensions = entity->getDimensions();
_keyLightProperties = entity->getKeyLightProperties();
_stageProperties = entity->getStageProperties();
_skyboxProperties = entity->getSkyboxProperties();
#if 0
if (_lastShapeURL != _typedEntity->getCompoundShapeURL()) {
@ -196,14 +200,14 @@ void ZoneEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scen
}
#endif
updateKeyZoneItemFromEntity(entity);
updateKeyZoneItemFromEntity();
if (sunChanged) {
updateKeySunFromEntity(entity);
updateKeySunFromEntity();
}
if (sunChanged || skyboxChanged) {
updateKeyAmbientFromEntity(entity);
updateKeyAmbientFromEntity();
}
if (backgroundChanged || skyboxChanged) {
updateKeyBackgroundFromEntity(entity);
@ -265,19 +269,19 @@ bool ZoneEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoint
return false;
}
void ZoneEntityRenderer::updateKeySunFromEntity(const TypedEntityPointer& entity) {
void ZoneEntityRenderer::updateKeySunFromEntity() {
const auto& sunLight = editSunLight();
sunLight->setType(model::Light::SUN);
sunLight->setPosition(_lastPosition);
sunLight->setOrientation(_lastRotation);
// Set the keylight
sunLight->setColor(ColorUtils::toVec3(entity->getKeyLightProperties().getColor()));
sunLight->setIntensity(entity->getKeyLightProperties().getIntensity());
sunLight->setDirection(entity->getKeyLightProperties().getDirection());
sunLight->setColor(ColorUtils::toVec3(_keyLightProperties.getColor()));
sunLight->setIntensity(_keyLightProperties.getIntensity());
sunLight->setDirection(_keyLightProperties.getDirection());
}
void ZoneEntityRenderer::updateKeyAmbientFromEntity(const TypedEntityPointer& entity) {
void ZoneEntityRenderer::updateKeyAmbientFromEntity() {
const auto& ambientLight = editAmbientLight();
ambientLight->setType(model::Light::AMBIENT);
ambientLight->setPosition(_lastPosition);
@ -285,24 +289,24 @@ void ZoneEntityRenderer::updateKeyAmbientFromEntity(const TypedEntityPointer& en
// Set the keylight
ambientLight->setAmbientIntensity(entity->getKeyLightProperties().getAmbientIntensity());
ambientLight->setAmbientIntensity(_keyLightProperties.getAmbientIntensity());
if (entity->getKeyLightProperties().getAmbientURL().isEmpty()) {
setAmbientURL(entity->getSkyboxProperties().getURL());
if (_keyLightProperties.getAmbientURL().isEmpty()) {
setAmbientURL(_skyboxProperties.getURL());
} else {
setAmbientURL(entity->getKeyLightProperties().getAmbientURL());
setAmbientURL(_keyLightProperties.getAmbientURL());
}
}
void ZoneEntityRenderer::updateKeyBackgroundFromEntity(const TypedEntityPointer& entity) {
editBackground();
setBackgroundMode(entity->getBackgroundMode());
setSkyboxColor(entity->getSkyboxProperties().getColorVec3());
setSkyboxColor(_skyboxProperties.getColorVec3());
setProceduralUserData(entity->getUserData());
setSkyboxURL(entity->getSkyboxProperties().getURL());
setSkyboxURL(_skyboxProperties.getURL());
}
void ZoneEntityRenderer::updateKeyZoneItemFromEntity(const TypedEntityPointer& entity) {
void ZoneEntityRenderer::updateKeyZoneItemFromEntity() {
/* TODO: Implement the sun model behavior / Keep this code here for reference, this is how we
{
// Set the stage

View file

@ -41,9 +41,9 @@ protected:
virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override;
private:
void updateKeyZoneItemFromEntity(const TypedEntityPointer& entity);
void updateKeySunFromEntity(const TypedEntityPointer& entity);
void updateKeyAmbientFromEntity(const TypedEntityPointer& entity);
void updateKeyZoneItemFromEntity();
void updateKeySunFromEntity();
void updateKeyAmbientFromEntity();
void updateKeyBackgroundFromEntity(const TypedEntityPointer& entity);
void updateAmbientMap();
void updateSkyboxMap();
@ -89,6 +89,10 @@ private:
bool _needAmbientUpdate{ true };
bool _needBackgroundUpdate{ true };
KeyLightPropertyGroup _keyLightProperties;
StagePropertyGroup _stageProperties;
SkyboxPropertyGroup _skyboxProperties;
// More attributes used for rendering:
QString _ambientTextureURL;
NetworkTexturePointer _ambientTexture;

View file

@ -49,8 +49,10 @@ ZoneEntityItem::ZoneEntityItem(const EntityItemID& entityItemID) : EntityItem(en
EntityItemProperties ZoneEntityItem::getProperties(EntityPropertyFlags desiredProperties) const {
EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class
_keyLightProperties.getProperties(properties);
// Contains a QString property, must be synchronized
withReadLock([&] {
_keyLightProperties.getProperties(properties);
});
_stageProperties.getProperties(properties);
@ -58,7 +60,10 @@ EntityItemProperties ZoneEntityItem::getProperties(EntityPropertyFlags desiredPr
COPY_ENTITY_PROPERTY_TO_PROPERTIES(compoundShapeURL, getCompoundShapeURL);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(backgroundMode, getBackgroundMode);
_skyboxProperties.getProperties(properties);
// Contains a QString property, must be synchronized
withReadLock([&] {
_skyboxProperties.getProperties(properties);
});
COPY_ENTITY_PROPERTY_TO_PROPERTIES(flyingAllowed, getFlyingAllowed);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(ghostingAllowed, getGhostingAllowed);
@ -88,8 +93,10 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) {
bool ZoneEntityItem::setSubClassProperties(const EntityItemProperties& properties) {
bool somethingChanged = EntityItem::setSubClassProperties(properties); // set the properties in our base class
_keyLightPropertiesChanged = _keyLightProperties.setProperties(properties);
// Contains a QString property, must be synchronized
withWriteLock([&] {
_keyLightPropertiesChanged = _keyLightProperties.setProperties(properties);
});
_stagePropertiesChanged = _stageProperties.setProperties(properties);
@ -101,11 +108,13 @@ bool ZoneEntityItem::setSubClassProperties(const EntityItemProperties& propertie
SET_ENTITY_PROPERTY_FROM_PROPERTIES(ghostingAllowed, setGhostingAllowed);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(filterURL, setFilterURL);
_skyboxPropertiesChanged = _skyboxProperties.setProperties(properties);
// Contains a QString property, must be synchronized
withWriteLock([&] {
_skyboxPropertiesChanged = _skyboxProperties.setProperties(properties);
});
somethingChanged = somethingChanged || _keyLightPropertiesChanged || _stagePropertiesChanged || _skyboxPropertiesChanged;
return somethingChanged;
}
@ -116,8 +125,12 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data,
int bytesRead = 0;
const unsigned char* dataAt = data;
int bytesFromKeylight = _keyLightProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args,
propertyFlags, overwriteLocalData, _keyLightPropertiesChanged);
int bytesFromKeylight;
withWriteLock([&] {
bytesFromKeylight = _keyLightProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args,
propertyFlags, overwriteLocalData, _keyLightPropertiesChanged);
});
somethingChanged = somethingChanged || _keyLightPropertiesChanged;
bytesRead += bytesFromKeylight;
dataAt += bytesFromKeylight;
@ -132,8 +145,11 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data,
READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL);
READ_ENTITY_PROPERTY(PROP_BACKGROUND_MODE, BackgroundMode, setBackgroundMode);
int bytesFromSkybox = _skyboxProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args,
propertyFlags, overwriteLocalData, _skyboxPropertiesChanged);
int bytesFromSkybox;
withWriteLock([&] {
bytesFromSkybox = _skyboxProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args,
propertyFlags, overwriteLocalData, _skyboxPropertiesChanged);
});
somethingChanged = somethingChanged || _skyboxPropertiesChanged;
bytesRead += bytesFromSkybox;
dataAt += bytesFromSkybox;
@ -150,13 +166,18 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data,
EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& params) const {
EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params);
requestedProperties += _keyLightProperties.getEntityProperties(params);
withReadLock([&] {
requestedProperties += _keyLightProperties.getEntityProperties(params);
});
requestedProperties += PROP_SHAPE_TYPE;
requestedProperties += PROP_COMPOUND_SHAPE_URL;
requestedProperties += PROP_BACKGROUND_MODE;
requestedProperties += _stageProperties.getEntityProperties(params);
requestedProperties += _skyboxProperties.getEntityProperties(params);
withReadLock([&] {
requestedProperties += _skyboxProperties.getEntityProperties(params);
});
requestedProperties += PROP_FLYING_ALLOWED;
requestedProperties += PROP_GHOSTING_ALLOWED;

View file

@ -63,12 +63,12 @@ public:
QString getCompoundShapeURL() const;
virtual void setCompoundShapeURL(const QString& url);
const KeyLightPropertyGroup& getKeyLightProperties() const { return _keyLightProperties; }
KeyLightPropertyGroup getKeyLightProperties() const { return resultWithReadLock<KeyLightPropertyGroup>([&] { return _keyLightProperties; }); }
void setBackgroundMode(BackgroundMode value) { _backgroundMode = value; _backgroundPropertiesChanged = true; }
BackgroundMode getBackgroundMode() const { return _backgroundMode; }
const SkyboxPropertyGroup& getSkyboxProperties() const { return _skyboxProperties; }
SkyboxPropertyGroup getSkyboxProperties() const { return resultWithReadLock<SkyboxPropertyGroup>([&] { return _skyboxProperties; }); }
const StagePropertyGroup& getStageProperties() const { return _stageProperties; }
bool getFlyingAllowed() const { return _flyingAllowed; }

View file

@ -0,0 +1,278 @@
"use strict";
/* jslint bitwise: true */
/* global Script, Entities, MyAvatar, Vec3, Quat, Mat4 */
(function() { // BEGIN LOCAL_SCOPE
// var lifetime = -1;
var lifetime = 600;
var tableSections = 32;
var tableRadius = 9;
var sectionRelativeRotation = 0;
var sectionRotation = 0;
var sectionRelativeCenterA = 0;
var sectionRelativeCenterB = 0;
var sectionRelativeCenterSign = 0;
var sectionCenterA = 0;
var sectionCenterB = 0;
var sectionCenterSign = 0;
var yFlip = 0;
var objects = [];
var overlays = [];
var testNames = [
"FarActionGrab",
"NearParentGrabEntity",
"NearParentGrabOverlay",
"Clone Entity (dynamic)",
"Clone Entity (non-dynamic"
];
function createCloneDynamicEntity(index) {
createPropsCube(index, false, false, true, true);
createPropsModel(index, false, false, true, true);
createSign(index, "Clone Dynamic Entity");
};
function createCloneEntity(index) {
createPropsCube(index, false, false, true, false);
createPropsModel(index, false, false, true, false);
createSign(index, "Clone Non-Dynamic Entity");
};
function createNearGrabOverlay(index) {
createPropsCubeOverlay(index, false, false, true, true);
createPropsModelOverlay(index, false, false, true, true);
createSign(index, "Near Grab Overlay");
};
function createNearGrabEntity(index) {
createPropsCube(index, false, false, false, false);
createPropsModel(index, false, false, false, false);
createSign(index, "Near Grab Entity");
};
function createFarGrabEntity(index) {
createPropsCube(index, true, false, false, false);
createPropsModel(index, true, false, false, false);
createSign(index, "Far Grab Entity");
};
function createPropsModel(i, dynamic, collisionless, clone, cloneDynamic) {
var propsModel = {
name: "controller-tests model object " + i,
type: "Model",
modelURL: "http://headache.hungry.com/~seth/hifi/controller-tests/color-cube.obj",
position: sectionCenterA,
rotation: sectionRotation,
gravity: (dynamic && !collisionless) ? { x: 0, y: -1, z: 0 } : { x: 0, y: 0, z: 0 },
dimensions: { x: 0.2, y: 0.2, z: 0.2 },
userData: JSON.stringify({
grabbableKey: {
grabbable: true,
cloneLimit: 10,
cloneable: clone,
cloneDynamic: cloneDynamic
},
controllerTestEntity: true
}),
lifetime: lifetime,
shapeType: "box",
dynamic: dynamic,
collisionless: collisionless
};
objects.push(Entities.addEntity(propsModel));
}
function createPropsModelOverlay(i, dynamic, collisionless, clone, cloneDynamic) {
var propsModel = {
name: "controller-tests model object " + i,
type: "Model",
modelURL: "http://headache.hungry.com/~seth/hifi/controller-tests/color-cube.obj",
url: "http://headache.hungry.com/~seth/hifi/controller-tests/color-cube.obj",
grabbable: true,
position: sectionCenterA,
rotation: sectionRotation,
dimensions: { x: 0.2, y: 0.2, z: 0.2 },
userData: JSON.stringify({
grabbableKey: {
grabbable: true,
},
controllerTestEntity: true
}),
lifetime: lifetime,
visible: true,
};
overlays.push(Overlays.addOverlay("model", propsModel));
}
function createPropsCubeOverlay(i, dynamic, collisionless, clone, cloneDynamic) {
var propsCube = {
name: "controller-tests cube object " + i,
type: "Box",
color: { "blue": 200, "green": 10, "red": 20 },
position: sectionCenterB,
rotation: sectionRotation,
grabbable: true,
dimensions: { x: 0.2, y: 0.2, z: 0.2 },
userData: JSON.stringify({
grabbableKey: {
grabbable: true,
},
controllerTestEntity: true
}),
lifetime: lifetime,
solid: true,
visible: true,
};
overlays.push(Overlays.addOverlay("cube", propsCube));
}
function createPropsCube(i, dynamic, collisionless, clone, cloneDynamic) {
var propsCube = {
name: "controller-tests cube object " + i,
type: "Box",
shape: "Cube",
color: { "blue": 200, "green": 10, "red": 20 },
position: sectionCenterB,
rotation: sectionRotation,
gravity: dynamic ? { x: 0, y: -1, z: 0 } : { x: 0, y: 0, z: 0 },
dimensions: { x: 0.2, y: 0.2, z: 0.2 },
userData: JSON.stringify({
grabbableKey: {
grabbable: true,
cloneLimit: 10,
cloneable: clone,
cloneDynamic: cloneDynamic
},
controllerTestEntity: true
}),
lifetime: lifetime,
shapeType: "box",
dynamic: dynamic,
collisionless: collisionless
};
objects.push(Entities.addEntity(propsCube));
}
function createSign(i, signText) {
var propsLabel = {
name: "controller-tests sign " + i,
type: "Text",
lineHeight: 0.125,
position: sectionCenterSign,
rotation: Quat.multiply(sectionRotation, yFlip),
text: signText,
dimensions: { x: 1, y: 1, z: 0.01 },
lifetime: lifetime,
userData: JSON.stringify({
grabbableKey: {
grabbable: false,
},
controllerTestEntity: true
})
};
objects.push(Entities.addEntity(propsLabel));
}
function chooseType(index) {
switch (index) {
case 0:
createFarGrabEntity(index);
break;
case 1:
createNearGrabEntity(index);
break;
case 2:
createNearGrabOverlay(index);
break;
case 3:
createCloneDynamicEntity();
break;
case 4:
createCloneEntity(index);
break;
}
}
function setupControllerTests(testBaseTransform) {
// var tableID =
objects.push(Entities.addEntity({
name: "controller-tests table",
type: "Model",
modelURL: "http://headache.hungry.com/~seth/hifi/controller-tests/controller-tests-table.obj.gz",
position: Mat4.transformPoint(testBaseTransform, { x: 0, y: 1, z: 0 }),
rotation: Mat4.extractRotation(testBaseTransform),
userData: JSON.stringify({
grabbableKey: { grabbable: false },
soundKey: {
url: "http://headache.hungry.com/~seth/hifi/sound/clock-ticking-3.wav",
volume: 0.4,
loop: true,
playbackGap: 0,
playbackGapRange: 0
},
controllerTestEntity: true
}),
shapeType: "static-mesh",
lifetime: lifetime
}));
var Xdynamic = 1;
var Xcollisionless = 2;
var Xkinematic = 4;
var XignoreIK = 8;
yFlip = Quat.fromPitchYawRollDegrees(0, 180, 0);
for (var i = 0; i < 16; i++) {
sectionRelativeRotation = Quat.fromPitchYawRollDegrees(0, -360 * i / tableSections, 0);
sectionRotation = Quat.multiply(Mat4.extractRotation(testBaseTransform), sectionRelativeRotation);
sectionRelativeCenterA = Vec3.multiplyQbyV(sectionRotation, { x: -0.2, y: 1.25, z: tableRadius - 0.8 });
sectionRelativeCenterB = Vec3.multiplyQbyV(sectionRotation, { x: 0.2, y: 1.25, z: tableRadius - 0.8 });
sectionRelativeCenterSign = Vec3.multiplyQbyV(sectionRotation, { x: 0, y: 1.5, z: tableRadius + 1.0 });
sectionCenterA = Mat4.transformPoint(testBaseTransform, sectionRelativeCenterA);
sectionCenterB = Mat4.transformPoint(testBaseTransform, sectionRelativeCenterB);
sectionCenterSign = Mat4.transformPoint(testBaseTransform, sectionRelativeCenterSign);
var dynamic = (i & Xdynamic) ? true : false;
var collisionless = (i & Xcollisionless) ? true : false;
var kinematic = (i & Xkinematic) ? true : false;
var ignoreIK = (i & XignoreIK) ? true : false;
chooseType(i);
}
}
// This assumes the avatar is standing on a flat floor with plenty of space.
// Find the floor:
var pickRay = {
origin: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 2, z: -1 })),
direction: { x: 0, y: -1, z: 0 },
length: 20
};
var intersection = Entities.findRayIntersection(pickRay, true, [], [], true);
if (intersection.intersects) {
var testBaseTransform = Mat4.createFromRotAndTrans(MyAvatar.rotation, intersection.intersection);
setupControllerTests(testBaseTransform);
}
Script.scriptEnding.connect(function () {
for (var i = 0; i < objects.length; i++) {
var nearbyID = objects[i];
Entities.deleteEntity(nearbyID);
}
for (var i = 0; i < overlays.length; i++) {
var overlayID = overlays[i];
Overlays.deleteOverlay(overlayID);
}
});
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,439 @@
"use strict";
// controllerDispatcher.js
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* jslint bitwise: true */
/* global Script, Entities, Overlays, Controller, Vec3, Quat, getControllerWorldLocation, RayPick,
controllerDispatcherPlugins:true, controllerDispatcherPluginsNeedSort:true,
LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES,
getGrabPointSphereOffset, HMD, MyAvatar, Messages
*/
controllerDispatcherPlugins = {};
controllerDispatcherPluginsNeedSort = false;
Script.include("/~/system/libraries/utils.js");
Script.include("/~/system/libraries/controllers.js");
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
(function() {
var NEAR_MAX_RADIUS = 0.1;
var TARGET_UPDATE_HZ = 60; // 50hz good enough, but we're using update
var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ;
var PROFILE = false;
if (typeof Test !== "undefined") {
PROFILE = true;
}
function ControllerDispatcher() {
var _this = this;
this.lastInterval = Date.now();
this.intervalCount = 0;
this.totalDelta = 0;
this.totalVariance = 0;
this.highVarianceCount = 0;
this.veryhighVarianceCount = 0;
this.tabletID = null;
this.blacklist = [];
// a module can occupy one or more "activity" slots while it's running. If all the required slots for a module are
// not set to false (not in use), a module cannot start. When a module is using a slot, that module's name
// is stored as the value, rather than false.
this.activitySlots = {
leftHand: false,
rightHand: false,
rightHandTrigger: false,
leftHandTrigger: false,
rightHandEquip: false,
leftHandEquip: false,
mouse: false
};
this.slotsAreAvailableForPlugin = function (plugin) {
for (var i = 0; i < plugin.parameters.activitySlots.length; i++) {
if (_this.activitySlots[plugin.parameters.activitySlots[i]]) {
return false; // something is already using a slot which _this plugin requires
}
}
return true;
};
this.markSlots = function (plugin, pluginName) {
for (var i = 0; i < plugin.parameters.activitySlots.length; i++) {
_this.activitySlots[plugin.parameters.activitySlots[i]] = pluginName;
}
};
this.unmarkSlotsForPluginName = function (runningPluginName) {
// this is used to free activity-slots when a plugin is deactivated while it's running.
for (var activitySlot in _this.activitySlots) {
if (activitySlot.hasOwnProperty(activitySlot) && _this.activitySlots[activitySlot] === runningPluginName) {
_this.activitySlots[activitySlot] = false;
}
}
};
this.runningPluginNames = {};
this.leftTriggerValue = 0;
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 () {
return getControllerWorldLocation(Controller.Standard.LeftHand, true);
};
this.dataGatherers.rightControllerLocation = function () {
return getControllerWorldLocation(Controller.Standard.RightHand, true);
};
this.updateTimings = function () {
_this.intervalCount++;
var thisInterval = Date.now();
var deltaTimeMsec = thisInterval - _this.lastInterval;
var deltaTime = deltaTimeMsec / 1000;
_this.lastInterval = thisInterval;
_this.totalDelta += deltaTimeMsec;
var variance = Math.abs(deltaTimeMsec - BASIC_TIMER_INTERVAL_MS);
_this.totalVariance += variance;
if (variance > 1) {
_this.highVarianceCount++;
}
if (variance > 5) {
_this.veryhighVarianceCount++;
}
return deltaTime;
};
this.setIgnoreTablet = function() {
if (HMD.tabletID !== _this.tabletID) {
RayPick.setIgnoreOverlays(_this.leftControllerRayPick, [HMD.tabletID]);
RayPick.setIgnoreOverlays(_this.rightControllerRayPick, [HMD.tabletID]);
}
};
this.update = function () {
if (PROFILE) {
Script.beginProfileRange("dispatch.pre");
}
var deltaTime = _this.updateTimings();
_this.setIgnoreTablet();
if (controllerDispatcherPluginsNeedSort) {
_this.orderedPluginNames = [];
for (var pluginName in controllerDispatcherPlugins) {
if (controllerDispatcherPlugins.hasOwnProperty(pluginName)) {
_this.orderedPluginNames.push(pluginName);
}
}
_this.orderedPluginNames.sort(function (a, b) {
return controllerDispatcherPlugins[a].parameters.priority -
controllerDispatcherPlugins[b].parameters.priority;
});
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 += ", ";
}
}
controllerDispatcherPluginsNeedSort = false;
}
if (PROFILE) {
Script.endProfileRange("dispatch.pre");
}
if (PROFILE) {
Script.beginProfileRange("dispatch.gather");
}
var controllerLocations = [
_this.dataGatherers.leftControllerLocation(),
_this.dataGatherers.rightControllerLocation()
];
// find 3d overlays near each hand
var nearbyOverlayIDs = [];
var h;
for (h = LEFT_HAND; h <= RIGHT_HAND; h++) {
if (controllerLocations[h].valid) {
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);
} else {
nearbyOverlayIDs.push([]);
}
}
// find entities near each hand
var nearbyEntityProperties = [[], []];
var nearbyEntityPropertiesByID = {};
for (h = LEFT_HAND; h <= RIGHT_HAND; h++) {
if (controllerLocations[h].valid) {
var controllerPosition = controllerLocations[h].position;
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.distance = Vec3.distance(props.position, controllerLocations[h].position);
nearbyEntityPropertiesByID[entityID] = props;
nearbyEntityProperties[h].push(props);
}
}
}
// raypick for each controller
var rayPicks = [
RayPick.getPrevRayPickResult(_this.leftControllerRayPick),
RayPick.getPrevRayPickResult(_this.rightControllerRayPick)
];
var hudRayPicks = [
RayPick.getPrevRayPickResult(_this.leftControllerHudRayPick),
RayPick.getPrevRayPickResult(_this.rightControllerHudRayPick)
];
// if the pickray hit something very nearby, put it into the nearby entities list
for (h = LEFT_HAND; h <= RIGHT_HAND; h++) {
// XXX find a way to extract searchRay from samuel's stuff
rayPicks[h].searchRay = {
origin: controllerLocations[h].position,
direction: Quat.getUp(controllerLocations[h].orientation),
length: 1000
};
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;
nearbyProps.distance = rayPicks[h].distance;
nearbyEntityPropertiesByID[nearEntityID] = nearbyProps;
nearbyEntityProperties[h].push(nearbyProps);
}
}
// sort by distance from each hand
nearbyEntityProperties[h].sort(function (a, b) {
return a.distance - b.distance;
});
}
// 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,
hudRayPicks: hudRayPicks
};
if (PROFILE) {
Script.endProfileRange("dispatch.gather");
}
if (PROFILE) {
Script.beginProfileRange("dispatch.isReady");
}
// check for plugins that would like to start. ask in order of increasing priority value
for (var pluginIndex = 0; pluginIndex < _this.orderedPluginNames.length; pluginIndex++) {
var orderedPluginName = _this.orderedPluginNames[pluginIndex];
var candidatePlugin = controllerDispatcherPlugins[orderedPluginName];
if (_this.slotsAreAvailableForPlugin(candidatePlugin)) {
if (PROFILE) {
Script.beginProfileRange("dispatch.isReady." + orderedPluginName);
}
var readiness = candidatePlugin.isReady(controllerData, deltaTime);
if (readiness.active) {
// this plugin will start. add it to the list of running plugins and mark the
// activity-slots which this plugin consumes as "in use"
_this.runningPluginNames[orderedPluginName] = true;
_this.markSlots(candidatePlugin, orderedPluginName);
}
if (PROFILE) {
Script.endProfileRange("dispatch.isReady." + orderedPluginName);
}
}
}
if (PROFILE) {
Script.endProfileRange("dispatch.isReady");
}
if (PROFILE) {
Script.beginProfileRange("dispatch.run");
}
// give time to running plugins
for (var runningPluginName in _this.runningPluginNames) {
if (_this.runningPluginNames.hasOwnProperty(runningPluginName)) {
var plugin = controllerDispatcherPlugins[runningPluginName];
if (!plugin) {
// plugin was deactivated while running. find the activity-slots it was using and make
// them available.
delete _this.runningPluginNames[runningPluginName];
_this.unmarkSlotsForPluginName(runningPluginName);
} else {
if (PROFILE) {
Script.beginProfileRange("dispatch.run." + runningPluginName);
}
var runningness = plugin.run(controllerData, deltaTime);
if (!runningness.active) {
// plugin is finished running, for now. remove it from the list
// of running plugins and mark its activity-slots as "not in use"
delete _this.runningPluginNames[runningPluginName];
_this.markSlots(plugin, false);
}
if (PROFILE) {
Script.endProfileRange("dispatch.run." + runningPluginName);
}
}
}
}
if (PROFILE) {
Script.endProfileRange("dispatch.run");
}
};
this.setBlacklist = function() {
RayPick.setIgnoreEntities(_this.leftControllerRayPick, this.blacklist);
RayPick.setIgnoreEntities(_this.rightControllerRayPick, this.blacklist);
};
var MAPPING_NAME = "com.highfidelity.controllerDispatcher";
var mapping = Controller.newMapping(MAPPING_NAME);
mapping.from([Controller.Standard.RT]).peek().to(_this.rightTriggerPress);
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);
this.leftControllerRayPick = RayPick.createRayPick({
joint: "_CONTROLLER_LEFTHAND",
filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS,
enabled: true,
maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE,
posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand)
});
this.leftControllerHudRayPick = RayPick.createRayPick({
joint: "_CONTROLLER_LEFTHAND",
filter: RayPick.PICK_HUD,
enabled: true,
maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE,
posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand)
});
this.rightControllerRayPick = RayPick.createRayPick({
joint: "_CONTROLLER_RIGHTHAND",
filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS,
enabled: true,
maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE,
posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand)
});
this.rightControllerHudRayPick = RayPick.createRayPick({
joint: "_CONTROLLER_RIGHTHAND",
filter: RayPick.PICK_HUD,
enabled: true,
maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE,
posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand)
});
this.handleHandMessage = function(channel, message, sender) {
var data;
if (sender === MyAvatar.sessionUUID) {
try {
if (channel === 'Hifi-Hand-RayPick-Blacklist') {
data = JSON.parse(message);
var action = data.action;
var id = data.id;
var index = _this.blacklis.indexOf(id);
if (action === 'add' && index === -1) {
_this.blacklist.push(id);
_this.setBlacklist();
}
if (action === 'remove') {
if (index > -1) {
_this.blacklist.splice(index, 1);
_this.setBlacklist();
}
}
}
} catch (e) {
print("WARNING: handControllerGrab.js -- error parsing Hifi-Hand-RayPick-Blacklist message: " + message);
}
}
};
this.cleanup = function () {
Script.update.disconnect(_this.update);
Controller.disableMapping(MAPPING_NAME);
RayPick.removeRayPick(_this.leftControllerRayPick);
RayPick.removeRayPick(_this.rightControllerRayPick);
RayPick.removeRayPick(_this.rightControllerHudRayPick);
RayPick.removeRayPick(_this.leftControllerHudRayPick);
};
}
var controllerDispatcher = new ControllerDispatcher();
Messages.subscribe('Hifi-Hand-RayPick-Blacklist');
Messages.messageReceived.connect(controllerDispatcher.handleHandMessage);
Script.scriptEnding.connect(controllerDispatcher.cleanup);
Script.update.connect(controllerDispatcher.update);
}());

View file

@ -0,0 +1,86 @@
"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,
getEnabledModuleByName
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
(function() {
function DisableModules(hand) {
this.hand = hand;
this.disableModules = false;
this.parameters = makeDispatcherModuleParameters(
90,
this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"],
[],
100);
this.isReady = function(controllerData) {
if (this.disableModules) {
return makeRunningValues(true, [], []);
}
return false;
};
this.run = function(controllerData) {
var teleportModuleName = this.hand === RIGHT_HAND ? "RightTeleporter" : "LeftTeleporter";
var teleportModule = getEnabledModuleByName(teleportModuleName);
if (teleportModule) {
var ready = teleportModule.isReady(controllerData);
if (ready) {
return makeRunningValues(false, [], []);
}
}
if (!this.disablemodules) {
return makeRunningValues(false, [], []);
}
return makeRunningValues(true, [], []);
};
}
var leftDisableModules = new DisableModules(LEFT_HAND);
var rightDisableModules = new DisableModules(RIGHT_HAND);
enableDispatcherModule("LeftDisableModules", leftDisableModules);
enableDispatcherModule("RightDisableModules", rightDisableModules);
this.handleMessage = function(channel, message, sender) {
if (sender === MyAvatar.sessionUUID) {
if (channel === 'Hifi-Hand-Disabler') {
if (message === 'left') {
leftDisableModules.disableModules = true;
}
if (message === 'right') {
rightDisableModules.disableModules = true;
}
if (message === 'both' || message === 'none') {
if (message === 'both') {
leftDisableModules.disableModules = true;
rightDisableModules.disableModules = true;
} else if (message === 'none') {
leftDisableModules.disableModules = false;
rightDisableModules.disableModules = false;
}
}
}
}
};
Messages.subscribe('Hifi-Hand-Disabler');
this.cleanup = function() {
disableDispatcherModule("LeftDisableModules");
disableDispatcherModule("RightDisableModules");
};
Messages.messageReceived.connect(this.handleMessage);
Script.scriptEnding.connect(this.cleanup);
}());

View file

@ -0,0 +1,737 @@
"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, entityIsCloneable,
cloneEntity, DISPATCHER_PROPERTIES
*/
Script.include("/~/system/libraries/Xform.js");
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
Script.include("/~/system/libraries/cloneEntityUtils.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) {
// don't want to spam the logs
}
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) {
// don't want to spam the logs
}
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.messageGrabEntity = false;
this.grabEntityProps = null;
this.parameters = makeDispatcherModuleParameters(
300,
this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip"] : ["leftHand", "leftHandEquip"],
[],
100);
var equipHotspotBuddy = new EquipHotspotBuddy();
this.setMessageGrabData = function(entityProperties) {
if (entityProperties) {
this.messageGrabEntity = true;
this.grabEntityProps = entityProperties;
}
};
// 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 || 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.cloneHotspot = function(props, controllerData) {
if (entityIsCloneable(props)) {
var worldEntityProps = controllerData.nearbyEntityProperties[this.hand];
var cloneID = cloneEntity(props, worldEntityProps);
return cloneID;
}
return null;
};
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);
var grabbedProperties = Entities.getEntityProperties(this.targetEntityID);
// 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
};
var isClone = false;
if (entityIsCloneable(grabbedProperties)) {
var cloneID = this.cloneHotspot(grabbedProperties, controllerData);
this.targetEntityID = cloneID;
Entities.editEntity(this.targetEntityID, reparentProps);
isClone = true;
} else if (!grabbedProperties.locked) {
Entities.editEntity(this.targetEntityID, reparentProps);
} else {
this.grabbedHotspot = null;
this.targetEntityID = null;
return;
}
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"
}));
var _this = this;
var grabEquipCheck = function() {
var args = [_this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(_this.targetEntityID, "startEquip", args);
};
if (isClone) {
// 100 ms seems to be sufficient time to force the check even occur after the object has been initialized.
Script.setTimeout(grabEquipCheck, 100);
}
};
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.messageGrabEntity = false;
this.grabEntityProps = 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 = null;
if (this.messageGrabEntity) {
var hotspots = this.collectEquipHotspots(this.grabEntityProps);
if (hotspots.length > -1) {
potentialEquipHotspot = hotspots[0];
}
} else {
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 the potentialHotspot is cloneable, clone it and return it
// if the potentialHotspot os not cloneable and locked return null
if (potentialEquipHotspot) {
if ((this.triggerSmoothedSqueezed() && !this.waitForTriggerRelease) || this.messageGrabEntity) {
this.grabbedHotspot = potentialEquipHotspot;
this.targetEntityID = this.grabbedHotspot.entityID;
this.startEquipEntity(controllerData);
this.messageGrabEnity = false;
}
return makeRunningValues(true, [potentialEquipHotspot.entityID], []);
} else {
return makeRunningValues(false, [], []);
}
};
this.isTargetIDValid = function() {
var entityProperties = Entities.getEntityProperties(this.targetEntityID);
for (var propertry in entityProperties) {
return true;
}
return 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.isTargetIDValid()) {
this.endEquipEntity();
return makeRunningValues(false, [], []);
}
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 handleMessage = function(channel, message, sender) {
var data;
if (sender === MyAvatar.sessionUUID) {
if (channel === 'Hifi-Hand-Grab') {
try {
data = JSON.parse(message);
var equipModule = (data.hand === "left") ? leftEquipEntity : rightEquipEntity;
var entityProperties = Entities.getEntityProperties(data.entityID, DISPATCHER_PROPERTIES);
entityProperties.id = data.entityID;
equipModule.setMessageGrabData(entityProperties);
} catch (e) {
print("WARNING: equipEntity.js -- error parsing Hifi-Hand-Grab message: " + message);
}
} else if (channel === 'Hifi-Hand-Drop') {
if (message === "left") {
leftEquipEntity.endEquipEntity();
} else if (message === "right") {
rightEquipEntity.endEquipEntity();
} else if (message === "both") {
leftEquipEntity.endEquipEntity();
rightEquipEntity.endEquipEntity();
}
}
}
};
Messages.subscribe('Hifi-Hand-Grab');
Messages.subscribe('Hifi-Hand-Drop');
Messages.messageReceived.connect(handleMessage);
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

@ -0,0 +1,578 @@
"use strict";
// farActionGrabEntity.js
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* jslint bitwise: true */
/* global Script, Controller, LaserPointers, RayPick, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat,
getGrabPointSphereOffset, getEnabledModuleByName, makeRunningValues, Entities, NULL_UUID,
enableDispatcherModule, disableDispatcherModule, entityIsDistanceGrabbable,
makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION,
PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD,
AVATAR_SELF_ID, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic,
getControllerWorldLocation, projectOntoEntityXYPlane
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
var PICK_WITH_HAND_RAY = true;
var halfPath = {
type: "line3d",
color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var halfEnd = {
type: "sphere",
solid: true,
color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
alpha: 0.9,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
};
var fullPath = {
type: "line3d",
color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var fullEnd = {
type: "sphere",
solid: true,
color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
alpha: 0.9,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
};
var holdPath = {
type: "line3d",
color: COLORS_GRAB_DISTANCE_HOLD,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var renderStates = [
{name: "half", path: halfPath, end: halfEnd},
{name: "full", path: fullPath, end: fullEnd},
{name: "hold", path: holdPath}
];
var defaultRenderStates = [
{name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath},
{name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath},
{name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath}
];
var GRABBABLE_PROPERTIES = [
"position",
"registrationPoint",
"rotation",
"gravity",
"collidesWith",
"dynamic",
"collisionless",
"locked",
"name",
"shapeType",
"parentID",
"parentJointIndex",
"density",
"dimensions",
"userData"
];
function FarActionGrabEntity(hand) {
this.hand = hand;
this.grabbedThingID = null;
this.actionID = null; // action this script created...
this.entityWithContextOverlay = false;
this.contextOverlayTimer = false;
var ACTION_TTL = 15; // seconds
var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object
var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position
var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified
var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified
this.parameters = makeDispatcherModuleParameters(
550,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
this.updateLaserPointer = function(controllerData) {
var SEARCH_SPHERE_SIZE = 0.011;
var MIN_SPHERE_SIZE = 0.0005;
var radius = Math.max(1.2 * SEARCH_SPHERE_SIZE * this.intersectionDistance, MIN_SPHERE_SIZE);
var dim = {x: radius, y: radius, z: radius};
var mode = "hold";
if (!this.distanceHolding && !this.distanceRotating) {
if (controllerData.triggerClicks[this.hand]) {
mode = "full";
} else {
mode = "half";
}
}
var laserPointerID = PICK_WITH_HAND_RAY ? this.laserPointer : this.headLaserPointer;
if (mode === "full") {
var fullEndToEdit = PICK_WITH_HAND_RAY ? this.fullEnd : fullEnd;
fullEndToEdit.dimensions = dim;
LaserPointers.editRenderState(laserPointerID, mode, {path: fullPath, end: fullEndToEdit});
} else if (mode === "half") {
var halfEndToEdit = PICK_WITH_HAND_RAY ? this.halfEnd : halfEnd;
halfEndToEdit.dimensions = dim;
LaserPointers.editRenderState(laserPointerID, mode, {path: halfPath, end: halfEndToEdit});
}
LaserPointers.enableLaserPointer(laserPointerID);
LaserPointers.setRenderState(laserPointerID, mode);
if (this.distanceHolding || this.distanceRotating) {
LaserPointers.setLockEndUUID(laserPointerID, this.grabbedThingID, this.grabbedIsOverlay);
} else {
LaserPointers.setLockEndUUID(laserPointerID, null, false);
}
};
this.laserPointerOff = function() {
LaserPointers.disableLaserPointer(this.laserPointer);
LaserPointers.disableLaserPointer(this.headLaserPointer);
};
this.handToController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
};
this.distanceGrabTimescale = function(mass, distance) {
var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass /
DISTANCE_HOLDING_UNITY_MASS * distance /
DISTANCE_HOLDING_UNITY_DISTANCE;
if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) {
timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
}
return timeScale;
};
this.getMass = function(dimensions, density) {
return (dimensions.x * dimensions.y * dimensions.z) * density;
};
this.startFarGrabAction = function (controllerData, grabbedProperties) {
var controllerLocation = controllerData.controllerLocations[this.hand];
var worldControllerPosition = controllerLocation.position;
var worldControllerRotation = controllerLocation.orientation;
// transform the position into room space
var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
var now = Date.now();
// add the action and initialize some variables
this.currentObjectPosition = grabbedProperties.position;
this.currentObjectRotation = grabbedProperties.rotation;
this.currentObjectTime = now;
this.currentCameraOrientation = Camera.orientation;
this.grabRadius = this.grabbedDistance;
this.grabRadialVelocity = 0.0;
// offset between controller vector at the grab radius and the entity position
var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
targetPosition = Vec3.sum(targetPosition, worldControllerPosition);
this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition);
// compute a constant based on the initial conditions which we use below to exaggerate hand motion
// onto the held object
this.radiusScalar = Math.log(this.grabRadius + 1.0);
if (this.radiusScalar < 1.0) {
this.radiusScalar = 1.0;
}
// compute the mass for the purpose of energy and how quickly to move object
this.mass = this.getMass(grabbedProperties.dimensions, grabbedProperties.density);
var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, grabbedProperties.position));
var timeScale = this.distanceGrabTimescale(this.mass, distanceToObject);
this.linearTimeScale = timeScale;
this.actionID = Entities.addAction("far-grab", this.grabbedThingID, {
targetPosition: this.currentObjectPosition,
linearTimeScale: timeScale,
targetRotation: this.currentObjectRotation,
angularTimeScale: timeScale,
tag: "far-grab-" + MyAvatar.sessionUUID,
ttl: ACTION_TTL
});
if (this.actionID === NULL_UUID) {
this.actionID = null;
}
// XXX
// if (this.actionID !== null) {
// this.callEntityMethodOnGrabbed("startDistanceGrab");
// }
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
this.previousRoomControllerPosition = roomControllerPosition;
};
this.continueDistanceHolding = function(controllerData) {
var controllerLocation = controllerData.controllerLocations[this.hand];
var worldControllerPosition = controllerLocation.position;
var worldControllerRotation = controllerLocation.orientation;
// also transform the position into room space
var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, ["position"]);
var now = Date.now();
var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds
this.currentObjectTime = now;
// the action was set up when this.distanceHolding was called. update the targets.
var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) *
this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR;
if (radius < 1.0) {
radius = 1.0;
}
var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition);
var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta);
var handMoved = Vec3.multiply(worldHandDelta, radius);
this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved);
// XXX
// this.callEntityMethodOnGrabbed("continueDistantGrab");
// Update radialVelocity
var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime);
var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition));
var newRadialVelocity = Vec3.dot(lastVelocity, delta);
var VELOCITY_AVERAGING_TIME = 0.016;
var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME;
if (blendFactor < 0.0) {
blendFactor = 0.0;
} else if (blendFactor > 1.0) {
blendFactor = 1.0;
}
this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity;
var RADIAL_GRAB_AMPLIFIER = 10.0;
if (Math.abs(this.grabRadialVelocity) > 0.0) {
this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime *
this.grabRadius * RADIAL_GRAB_AMPLIFIER);
}
// don't let grabRadius go all the way to zero, because it can't come back from that
var MINIMUM_GRAB_RADIUS = 0.1;
if (this.grabRadius < MINIMUM_GRAB_RADIUS) {
this.grabRadius = MINIMUM_GRAB_RADIUS;
}
var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition);
newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition);
// XXX
// this.maybeScale(grabbedProperties);
// visualizations
this.updateLaserPointer(controllerData);
var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition));
this.linearTimeScale = (this.linearTimeScale / 2);
if (this.linearTimeScale <= DISTANCE_HOLDING_ACTION_TIMEFRAME) {
this.linearTimeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
}
var success = Entities.updateAction(this.grabbedThingID, this.actionID, {
targetPosition: newTargetPosition,
linearTimeScale: this.linearTimeScale,
targetRotation: this.currentObjectRotation,
angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
ttl: ACTION_TTL
});
if (!success) {
print("continueDistanceHolding -- updateAction failed: " + this.actionID);
this.actionID = null;
}
this.previousRoomControllerPosition = roomControllerPosition;
};
this.endNearGrabAction = function () {
ensureDynamic(this.grabbedThingID);
this.distanceHolding = false;
this.distanceRotating = false;
Entities.deleteAction(this.grabbedThingID, this.actionID);
this.actionID = null;
this.grabbedThingID = null;
};
this.notPointingAtEntity = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
var entityProperty = Entities.getEntityProperties(intersection.objectID);
var entityType = entityProperty.type;
if ((intersection.type === RayPick.INTERSECTED_ENTITY && entityType === "Web") || intersection.type === RayPick.INTERSECTED_OVERLAY) {
return true;
}
return false;
};
this.distanceRotate = function(otherFarGrabModule) {
this.distanceRotating = true;
this.distanceHolding = false;
var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation;
var controllerRotationDelta = Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation));
// Rotate entity by twice the delta rotation.
controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta);
// Perform the rotation in the translation controller's action update.
otherFarGrabModule.currentObjectRotation = Quat.multiply(controllerRotationDelta,
otherFarGrabModule.currentObjectRotation);
// Rotate about the translation controller's target position.
this.offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, this.offsetPosition);
otherFarGrabModule.offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta,
otherFarGrabModule.offsetPosition);
this.previousWorldControllerRotation = worldControllerRotation;
};
this.prepareDistanceRotatingData = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
var controllerLocation = getControllerWorldLocation(this.handToController(), true);
var worldControllerPosition = controllerLocation.position;
var worldControllerRotation = controllerLocation.orientation;
var grabbedProperties = Entities.getEntityProperties(intersection.objectID, GRABBABLE_PROPERTIES);
this.currentObjectPosition = grabbedProperties.position;
this.grabRadius = intersection.distance;
// Offset between controller vector at the grab radius and the entity position.
var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
targetPosition = Vec3.sum(targetPosition, worldControllerPosition);
this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition);
// Initial controller rotation.
this.previousWorldControllerRotation = worldControllerRotation;
};
this.destroyContextOverlay = function(controllerData) {
if (this.entityWithContextOverlay) {
ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay);
this.entityWithContextOverlay = false;
}
};
this.isReady = function (controllerData) {
if (this.notPointingAtEntity(controllerData)) {
return makeRunningValues(false, [], []);
}
this.distanceHolding = false;
this.distanceRotating = false;
if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
this.updateLaserPointer(controllerData);
this.prepareDistanceRotatingData(controllerData);
return makeRunningValues(true, [], []);
} else {
this.destroyContextOverlay();
return makeRunningValues(false, [], []);
}
};
this.isPointingAtUI = function(controllerData) {
var hudRayPickInfo = controllerData.hudRayPicks[this.hand];
var hudPoint2d = HMD.overlayFromWorldPoint(hudRayPickInfo.intersection);
if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(hudPoint2d || Reticle.position)) {
return true;
}
return false;
};
this.run = function (controllerData) {
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.notPointingAtEntity(controllerData) || this.isPointingAtUI(controllerData)) {
this.endNearGrabAction();
this.laserPointerOff();
return makeRunningValues(false, [], []);
}
this.updateLaserPointer(controllerData);
var otherModuleName =this.hand === RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity";
var otherFarGrabModule = getEnabledModuleByName(otherModuleName);
// gather up the readiness of the near-grab modules
var nearGrabNames = [
this.hand === RIGHT_HAND ? "RightScaleAvatar" : "LeftScaleAvatar",
this.hand === RIGHT_HAND ? "RightFarTriggerEntity" : "LeftFarTriggerEntity",
this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity",
this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity",
this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"
];
var nearGrabReadiness = [];
for (var i = 0; i < nearGrabNames.length; i++) {
var nearGrabModule = getEnabledModuleByName(nearGrabNames[i]);
var ready = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []);
nearGrabReadiness.push(ready);
}
if (this.actionID) {
// if we are doing a distance grab and the object gets close enough to the controller,
// stop the far-grab so the near-grab or equip can take over.
for (var k = 0; k < nearGrabReadiness.length; k++) {
if (nearGrabReadiness[k].active && nearGrabReadiness[k].targets[0] === this.grabbedThingID) {
this.laserPointerOff();
this.endNearGrabAction();
return makeRunningValues(false, [], []);
}
}
this.continueDistanceHolding(controllerData);
} else {
// if we are doing a distance search and this controller moves into a position
// where it could near-grab something, stop searching.
for (var j = 0; j < nearGrabReadiness.length; j++) {
if (nearGrabReadiness[j].active) {
this.laserPointerOff();
return makeRunningValues(false, [], []);
}
}
var rayPickInfo = controllerData.rayPicks[this.hand];
if (rayPickInfo.type === RayPick.INTERSECTED_ENTITY) {
if (controllerData.triggerClicks[this.hand]) {
var entityID = rayPickInfo.objectID;
var targetProps = Entities.getEntityProperties(entityID, [
"dynamic", "shapeType", "position",
"rotation", "dimensions", "density",
"userData", "locked", "type"
]);
if (entityID !== this.entityWithContextOverlay) {
this.destroyContextOverlay();
}
if (entityIsDistanceGrabbable(targetProps)) {
if (!this.distanceRotating) {
this.grabbedThingID = entityID;
this.grabbedDistance = rayPickInfo.distance;
}
if (otherFarGrabModule.grabbedThingID === this.grabbedThingID && otherFarGrabModule.distanceHolding) {
this.distanceRotate(otherFarGrabModule);
} else {
this.distanceHolding = true;
this.distanceRotating = false;
this.startFarGrabAction(controllerData, targetProps);
}
}
} else if (!this.entityWithContextOverlay && !this.contextOverlayTimer) {
var _this = this;
_this.contextOverlayTimer = Script.setTimeout(function () {
if (!_this.entityWithContextOverlay && _this.contextOverlayTimer) {
var props = Entities.getEntityProperties(rayPickInfo.objectID);
var pointerEvent = {
type: "Move",
id: this.hand + 1, // 0 is reserved for hardware mouse
pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID, rayPickInfo.intersection, props),
pos3D: rayPickInfo.intersection,
normal: rayPickInfo.surfaceNormal,
direction: Vec3.subtract(ZERO_VEC, rayPickInfo.surfaceNormal),
button: "Secondary"
};
if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.objectID, pointerEvent)) {
_this.entityWithContextOverlay = rayPickInfo.objectID;
}
}
_this.contextOverlayTimer = false;
}, 500);
}
} else if (this.distanceRotating) {
this.distanceRotate(otherFarGrabModule);
}
}
return this.exitIfDisabled();
};
this.exitIfDisabled = function() {
var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules";
var disableModule = getEnabledModuleByName(moduleName);
if (disableModule) {
if (disableModule.disableModules) {
this.laserPointerOff();
this.endNearGrabAction();
return makeRunningValues(false, [], []);
}
}
return makeRunningValues(true, [], []);
};
this.cleanup = function () {
LaserPointers.disableLaserPointer(this.laserPointer);
LaserPointers.removeLaserPointer(this.laserPointer);
};
this.halfEnd = halfEnd;
this.fullEnd = fullEnd;
this.laserPointer = LaserPointers.createLaserPointer({
joint: (this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS,
maxDistance: PICK_MAX_DISTANCE,
posOffset: getGrabPointSphereOffset(this.handToController()),
renderStates: renderStates,
faceAvatar: true,
defaultRenderStates: defaultRenderStates
});
}
var leftFarActionGrabEntity = new FarActionGrabEntity(LEFT_HAND);
var rightFarActionGrabEntity = new FarActionGrabEntity(RIGHT_HAND);
enableDispatcherModule("LeftFarActionGrabEntity", leftFarActionGrabEntity);
enableDispatcherModule("RightFarActionGrabEntity", rightFarActionGrabEntity);
this.cleanup = function () {
leftFarActionGrabEntity.cleanup();
rightFarActionGrabEntity.cleanup();
disableDispatcherModule("LeftFarActionGrabEntity");
disableDispatcherModule("RightFarActionGrabEntity");
};
Script.scriptEnding.connect(this.cleanup);
}());

View file

@ -0,0 +1,234 @@
"use strict";
// farTrigger.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, Controller, LaserPointers, RayPick, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat,
getGrabPointSphereOffset, getEnabledModuleByName, makeRunningValues, Entities, NULL_UUID,
enableDispatcherModule, disableDispatcherModule, entityIsDistanceGrabbable,
makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION,
PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD,
AVATAR_SELF_ID, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic,
getControllerWorldLocation, projectOntoEntityXYPlane, getGrabbableData
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
var halfPath = {
type: "line3d",
color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var halfEnd = {
type: "sphere",
solid: true,
color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
alpha: 0.9,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
};
var fullPath = {
type: "line3d",
color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var fullEnd = {
type: "sphere",
solid: true,
color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
alpha: 0.9,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
};
var holdPath = {
type: "line3d",
color: COLORS_GRAB_DISTANCE_HOLD,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var renderStates = [
{name: "half", path: halfPath, end: halfEnd},
{name: "full", path: fullPath, end: fullEnd},
{name: "hold", path: holdPath}
];
var defaultRenderStates = [
{name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath},
{name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath},
{name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath}
];
function entityWantsNearTrigger(props) {
var grabbableData = getGrabbableData(props);
return grabbableData.triggerable || grabbableData.wantsTrigger;
}
function FarTriggerEntity(hand) {
this.hand = hand;
this.targetEntityID = null;
this.grabbing = false;
this.previousParentID = {};
this.previousParentJointIndex = {};
this.previouslyUnhooked = {};
this.parameters = makeDispatcherModuleParameters(
520,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
this.handToController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
};
this.updateLaserPointer = function(controllerData) {
var SEARCH_SPHERE_SIZE = 0.011;
var MIN_SPHERE_SIZE = 0.0005;
var radius = Math.max(1.2 * SEARCH_SPHERE_SIZE * this.intersectionDistance, MIN_SPHERE_SIZE);
var dim = {x: radius, y: radius, z: radius};
var mode = "none";
if (controllerData.triggerClicks[this.hand]) {
mode = "full";
} else {
mode = "half";
}
var laserPointerID = this.laserPointer;
if (mode === "full") {
var fullEndToEdit = this.fullEnd;
fullEndToEdit.dimensions = dim;
LaserPointers.editRenderState(laserPointerID, mode, {path: fullPath, end: fullEndToEdit});
} else if (mode === "half") {
var halfEndToEdit = this.halfEnd;
halfEndToEdit.dimensions = dim;
LaserPointers.editRenderState(laserPointerID, mode, {path: halfPath, end: halfEndToEdit});
}
LaserPointers.enableLaserPointer(laserPointerID);
LaserPointers.setRenderState(laserPointerID, mode);
};
this.laserPointerOff = function() {
LaserPointers.disableLaserPointer(this.laserPointer);
};
this.getTargetProps = function (controllerData) {
// nearbyEntityProperties is already sorted by length from controller
var targetEntity = controllerData.rayPicks[this.hand].objectID;
if (targetEntity) {
var targetProperties = Entities.getEntityProperties(targetEntity);
if (entityWantsNearTrigger(targetProperties)) {
return targetProperties;
}
}
return null;
};
this.startFarTrigger = function (controllerData) {
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "startFarTrigger", args);
};
this.continueFarTrigger = function (controllerData) {
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "continueFarTrigger", args);
};
this.endFarTrigger = function (controllerData) {
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "stopFarTrigger", args);
this.laserPointerOff();
};
this.isReady = function (controllerData) {
this.targetEntityID = null;
if (controllerData.triggerClicks[this.hand] === 0) {
return makeRunningValues(false, [], []);
}
var targetProps = this.getTargetProps(controllerData);
if (targetProps) {
this.targetEntityID = targetProps.id;
this.startFarTrigger(controllerData);
return makeRunningValues(true, [this.targetEntityID], []);
} else {
return makeRunningValues(false, [], []);
}
};
this.run = function (controllerData) {
var targetEntity = controllerData.rayPicks[this.hand].objectID;
if (controllerData.triggerClicks[this.hand] === 0 || this.targetEntityID !== targetEntity) {
this.endFarTrigger(controllerData);
return makeRunningValues(false, [], []);
}
this.updateLaserPointer(controllerData);
this.continueFarTrigger(controllerData);
return makeRunningValues(true, [this.targetEntityID], []);
};
this.halfEnd = halfEnd;
this.fullEnd = fullEnd;
this.laserPointer = LaserPointers.createLaserPointer({
joint: (this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS,
maxDistance: PICK_MAX_DISTANCE,
posOffset: getGrabPointSphereOffset(this.handToController()),
renderStates: renderStates,
faceAvatar: true,
defaultRenderStates: defaultRenderStates
});
this.cleanup = function () {
if (this.targetEntityID) {
this.endFarTrigger();
}
LaserPointers.disableLaserPointer(this.laserPointer);
LaserPointers.removeLaserPointer(this.laserPointer);
};
}
var leftFarTriggerEntity = new FarTriggerEntity(LEFT_HAND);
var rightFarTriggerEntity = new FarTriggerEntity(RIGHT_HAND);
enableDispatcherModule("LeftFarTriggerEntity", leftFarTriggerEntity);
enableDispatcherModule("RightFarTriggerEntity", rightFarTriggerEntity);
this.cleanup = function () {
leftFarTriggerEntity.cleanup();
rightFarTriggerEntity.cleanup();
disableDispatcherModule("LeftFarTriggerEntity");
disableDispatcherModule("RightFarTriggerEntity");
};
Script.scriptEnding.connect(this.cleanup);
}());

View file

@ -0,0 +1,265 @@
"use strict";
// inEditMode.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,
NULL_UUID, enableDispatcherModule, disableDispatcherModule, makeRunningValues,
Messages, Quat, Vec3, getControllerWorldLocation, makeDispatcherModuleParameters, Overlays, ZERO_VEC,
AVATAR_SELF_ID, HMD, INCHES_TO_METERS, DEFAULT_REGISTRATION_POINT, Settings, getGrabPointSphereOffset,
COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD,
DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_ON_VALUE, TRIGGER_OFF_VALUE, getEnabledModuleByName, PICK_MAX_DISTANCE,
isInEditMode
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
Script.include("/~/system/libraries/utils.js");
(function () {
var halfPath = {
type: "line3d",
color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var halfEnd = {
type: "sphere",
solid: true,
color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
alpha: 0.9,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
};
var fullPath = {
type: "line3d",
color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var fullEnd = {
type: "sphere",
solid: true,
color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
alpha: 0.9,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
};
var holdPath = {
type: "line3d",
color: COLORS_GRAB_DISTANCE_HOLD,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var renderStates = [
{name: "half", path: halfPath, end: halfEnd},
{name: "full", path: fullPath, end: fullEnd},
{name: "hold", path: holdPath}
];
var defaultRenderStates = [
{name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath},
{name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath},
{name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath}
];
function InEditMode(hand) {
this.hand = hand;
this.triggerClicked = false;
this.mode = "none";
this.parameters = makeDispatcherModuleParameters(
160,
this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"],
[],
100);
this.nearTablet = function(overlays) {
for (var i = 0; i < overlays.length; i++) {
if (overlays[i] === HMD.tabletID) {
return true;
}
}
return false;
};
this.handToController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
};
this.processControllerTriggers = function(controllerData) {
if (controllerData.triggerClicks[this.hand]) {
this.mode = "full";
} else if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
this.mode = "half";
} else {
this.mode = "none";
}
};
this.updateLaserPointer = function(controllerData) {
var RADIUS = 0.005;
var dim = { x: RADIUS, y: RADIUS, z: RADIUS };
if (this.mode === "full") {
this.fullEnd.dimensions = dim;
LaserPointers.editRenderState(this.laserPointer, this.mode, {path: fullPath, end: this.fullEnd});
} else if (this.mode === "half") {
this.halfEnd.dimensions = dim;
LaserPointers.editRenderState(this.laserPointer, this.mode, {path: halfPath, end: this.halfEnd});
}
LaserPointers.enableLaserPointer(this.laserPointer);
LaserPointers.setRenderState(this.laserPointer, this.mode);
};
this.pointingAtTablet = function(objectID) {
if (objectID === HMD.tabletScreenID || objectID === HMD.tabletButtonID) {
return true;
}
return false;
};
this.sendPickData = function(controllerData) {
if (controllerData.triggerClicks[this.hand] && !this.triggerClicked) {
var intersection = controllerData.rayPicks[this.hand];
if (intersection.type === RayPick.INTERSECTED_ENTITY) {
Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({
method: "selectEntity",
entityID: intersection.objectID
}));
} else if (intersection.type === RayPick.INTERSECTED_OVERLAY) {
Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({
method: "selectOverlay",
overlayID: intersection.objectID
}));
}
this.triggerClicked = true;
} else {
this.triggerClicked = false;
}
};
this.exitModule = function() {
this.disableLasers();
return makeRunningValues(false, [], []);
};
this.disableLasers = function() {
LaserPointers.disableLaserPointer(this.laserPointer);
};
this.isReady = function(controllerData) {
if (isInEditMode()) {
this.triggerClicked = false;
return makeRunningValues(true, [], []);
}
return this.exitModule();
};
this.run = function(controllerData) {
var tabletStylusInput = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightTabletStylusInput" : "LeftTabletStylusInput");
if (tabletStylusInput) {
var tabletReady = tabletStylusInput.isReady(controllerData);
if (tabletReady.active) {
return this.exitModule();
}
}
var overlayLaser = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightOverlayLaserInput" : "LeftOverlayLaserInput");
if (overlayLaser) {
var overlayLaserReady = overlayLaser.isReady(controllerData);
if (overlayLaserReady.active && this.pointingAtTablet(overlayLaser.target)) {
return this.exitModule();
}
}
var nearOverlay = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay");
if (nearOverlay) {
var nearOverlayReady = nearOverlay.isReady(controllerData);
if (nearOverlayReady.active && nearOverlay.grabbedThingID === HMD.tabletID) {
return this.exitModule();
}
}
var teleport = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightTeleporter" : "LeftTeleporter");
if (teleport) {
var teleportReady = teleport.isReady(controllerData);
if (teleportReady.active) {
return this.exitModule();
}
}
this.processControllerTriggers(controllerData);
this.updateLaserPointer(controllerData);
this.sendPickData(controllerData);
return this.isReady(controllerData);
};
this.cleanup = function() {
LaserPointers.disableLaserPointer(this.laserPointer);
LaserPointers.removeLaserPointer(this.laserPointer);
};
this.halfEnd = halfEnd;
this.fullEnd = fullEnd;
this.laserPointer = LaserPointers.createLaserPointer({
joint: (this.hand === RIGHT_HAND) ? "_CONTROLLER_RIGHTHAND" : "_CONTROLLER_LEFTHAND",
filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS,
maxDistance: PICK_MAX_DISTANCE,
posOffset: getGrabPointSphereOffset(this.handToController()),
renderStates: renderStates,
faceAvatar: true,
defaultRenderStates: defaultRenderStates
});
LaserPointers.setIgnoreOverlays(this.laserPointer, [HMD.tabletID, HMD.tabletButtonID, HMD.tabletScreenID]);
}
var leftHandInEditMode = new InEditMode(LEFT_HAND);
var rightHandInEditMode = new InEditMode(RIGHT_HAND);
enableDispatcherModule("LeftHandInEditMode", leftHandInEditMode);
enableDispatcherModule("RightHandInEditMode", rightHandInEditMode);
this.cleanup = function() {
leftHandInEditMode.cleanup();
rightHandInEditMode.cleanup();
disableDispatcherModule("LeftHandInEditMode");
disableDispatcherModule("RightHandInEditMode");
};
Script.scriptEnding.connect(this.cleanup);
}());

View file

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

View file

@ -0,0 +1,264 @@
"use strict";
// nearParentGrabEntity.js
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID,
getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule,
propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE,
makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS,
findGroupParent, Vec3, cloneEntity, entityIsCloneable, propsAreCloneDynamic, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/cloneEntityUtils.js");
(function() {
// XXX this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true;
// XXX this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC;
function NearParentingGrabEntity(hand) {
this.hand = hand;
this.targetEntityID = null;
this.grabbing = false;
this.previousParentID = {};
this.previousParentJointIndex = {};
this.previouslyUnhooked = {};
this.hapticTargetID = null;
this.parameters = makeDispatcherModuleParameters(
500,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
// XXX does handJointIndex change if the avatar changes?
this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
this.controllerJointIndex = getControllerJointIndex(this.hand);
this.getOtherModule = function() {
return (this.hand === RIGHT_HAND) ? leftNearParentingGrabEntity : rightNearParentingGrabEntity;
};
this.otherHandIsParent = function(props) {
return this.getOtherModule().thisHandIsParent(props);
};
this.thisHandIsParent = function(props) {
if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== AVATAR_SELF_ID) {
return false;
}
var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
if (props.parentJointIndex === handJointIndex) {
return true;
}
var controllerJointIndex = this.controllerJointIndex;
if (props.parentJointIndex === controllerJointIndex) {
return true;
}
var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ?
"_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" :
"_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
if (props.parentJointIndex === controllerCRJointIndex) {
return true;
}
return false;
};
this.startNearParentingGrabEntity = function (controllerData, targetProps) {
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
var handJointIndex;
// if (this.ignoreIK) {
// handJointIndex = this.controllerJointIndex;
// } else {
// handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
// }
handJointIndex = this.controllerJointIndex;
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(targetProps.id, "startNearGrab", args);
var reparentProps = {
parentID: AVATAR_SELF_ID,
parentJointIndex: handJointIndex,
localVelocity: {x: 0, y: 0, z: 0},
localAngularVelocity: {x: 0, y: 0, z: 0}
};
if (this.thisHandIsParent(targetProps)) {
// this should never happen, but if it does, don't set previous parent to be this hand.
// this.previousParentID[targetProps.id] = NULL;
// this.previousParentJointIndex[targetProps.id] = -1;
} else if (this.otherHandIsParent(targetProps)) {
// the other hand is parent. Steal the object and information
var otherModule = this.getOtherModule();
this.previousParentID[targetProps.id] = otherModule.previousParentID[targetProps.id];
this.previousParentJointIndex[targetProps.id] = otherModule.previousParentJointIndex[targetProps.id];
otherModule.endNearParentingGrabEntity();
} else {
this.previousParentID[targetProps.id] = targetProps.parentID;
this.previousParentJointIndex[targetProps.id] = targetProps.parentJointIndex;
}
this.targetEntityID = targetProps.id;
Entities.editEntity(targetProps.id, reparentProps);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'grab',
grabbedEntity: targetProps.id,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
this.grabbing = true;
};
this.endNearParentingGrabEntity = function () {
if (this.previousParentID[this.targetEntityID] === NULL_UUID || this.previousParentID === undefined) {
Entities.editEntity(this.targetEntityID, {
parentID: this.previousParentID[this.targetEntityID],
parentJointIndex: this.previousParentJointIndex[this.targetEntityID]
});
} else {
// we're putting this back as a child of some other parent, so zero its velocity
Entities.editEntity(this.targetEntityID, {
parentID: this.previousParentID[this.targetEntityID],
parentJointIndex: this.previousParentJointIndex[this.targetEntityID],
localVelocity: {x: 0, y: 0, z: 0},
localAngularVelocity: {x: 0, y: 0, z: 0}
});
}
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
this.grabbing = false;
this.targetEntityID = null;
};
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) {
continue;
}
if (entityIsGrabbable(props)) {
// give haptic feedback
if (props.id !== this.hapticTargetID) {
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
this.hapticTargetID = props.id;
}
// 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;
}
}
return null;
};
this.isReady = function (controllerData, deltaTime) {
this.targetEntityID = null;
this.grabbing = false;
var targetProps = this.getTargetProps(controllerData);
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
return makeRunningValues(false, [], []);
}
if (targetProps) {
if (propsArePhysical(targetProps) || propsAreCloneDynamic(targetProps)) {
return makeRunningValues(false, [], []); // let nearActionGrabEntity handle it
} else {
this.targetEntityID = targetProps.id;
return makeRunningValues(true, [this.targetEntityID], []);
}
} else {
this.hapticTargetID = null;
return makeRunningValues(false, [], []);
}
};
this.run = function (controllerData, deltaTime) {
if (this.grabbing) {
if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
this.endNearParentingGrabEntity();
this.hapticTargetID = null;
return makeRunningValues(false, [], []);
}
var props = Entities.getEntityProperties(this.targetEntityID);
if (!this.thisHandIsParent(props)) {
this.grabbing = false;
this.targetEntityID = null;
this.hapticTargetID = null;
return makeRunningValues(false, [], []);
}
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args);
} else {
// still searching / highlighting
var readiness = this.isReady (controllerData);
if (!readiness.active) {
return readiness;
}
if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
// switch to grab
var targetProps = this.getTargetProps(controllerData);
var targetCloneable = entityIsCloneable(targetProps);
if (targetCloneable) {
var worldEntityProps = controllerData.nearbyEntityProperties[this.hand];
var cloneID = cloneEntity(targetProps, worldEntityProps);
var cloneProps = Entities.getEntityProperties(cloneID);
this.grabbing = true;
this.targetEntityID = cloneID;
this.startNearParentingGrabEntity(controllerData, cloneProps);
} else if (targetProps) {
this.grabbing = true;
this.startNearParentingGrabEntity(controllerData, targetProps);
}
}
}
return makeRunningValues(true, [this.targetEntityID], []);
};
this.cleanup = function () {
if (this.targetEntityID) {
this.endNearParentingGrabEntity();
}
};
}
var leftNearParentingGrabEntity = new NearParentingGrabEntity(LEFT_HAND);
var rightNearParentingGrabEntity = new NearParentingGrabEntity(RIGHT_HAND);
enableDispatcherModule("LeftNearParentingGrabEntity", leftNearParentingGrabEntity);
enableDispatcherModule("RightNearParentingGrabEntity", rightNearParentingGrabEntity);
this.cleanup = function () {
leftNearParentingGrabEntity.cleanup();
rightNearParentingGrabEntity.cleanup();
disableDispatcherModule("LeftNearParentingGrabEntity");
disableDispatcherModule("RightNearParentingGrabEntity");
};
Script.scriptEnding.connect(this.cleanup);
}());

View file

@ -0,0 +1,218 @@
"use strict";
// nearParentGrabOverlay.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, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID,
getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule,
Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION,
makeDispatcherModuleParameters, Overlays, makeRunningValues
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
var GRAB_RADIUS = 0.35;
(function() {
// XXX this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true;
// XXX this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC;
function NearParentingGrabOverlay(hand) {
this.hand = hand;
this.grabbedThingID = null;
this.previousParentID = {};
this.previousParentJointIndex = {};
this.previouslyUnhooked = {};
this.parameters = makeDispatcherModuleParameters(
90,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
// XXX does handJointIndex change if the avatar changes?
this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
this.controllerJointIndex = getControllerJointIndex(this.hand);
this.getOtherModule = function() {
return (this.hand === RIGHT_HAND) ? leftNearParentingGrabOverlay : rightNearParentingGrabOverlay;
};
this.otherHandIsParent = function(props) {
return this.getOtherModule().thisHandIsParent(props);
};
this.thisHandIsParent = function(props) {
if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== AVATAR_SELF_ID) {
return false;
}
var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
if (props.parentJointIndex === handJointIndex) {
return true;
}
var controllerJointIndex = this.controllerJointIndex;
if (props.parentJointIndex === controllerJointIndex) {
return true;
}
var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ?
"_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" :
"_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
if (props.parentJointIndex === controllerCRJointIndex) {
return true;
}
return false;
};
this.getGrabbedProperties = function() {
return {
position: Overlays.getProperty(this.grabbedThingID, "position"),
rotation: Overlays.getProperty(this.grabbedThingID, "rotation"),
parentID: Overlays.getProperty(this.grabbedThingID, "parentID"),
parentJointIndex: Overlays.getProperty(this.grabbedThingID, "parentJointIndex"),
dynamic: false,
shapeType: "none"
};
};
this.startNearParentingGrabOverlay = function (controllerData) {
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
var handJointIndex;
// if (this.ignoreIK) {
// handJointIndex = this.controllerJointIndex;
// } else {
// handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
// }
handJointIndex = this.controllerJointIndex;
var grabbedProperties = this.getGrabbedProperties();
var reparentProps = {
parentID: AVATAR_SELF_ID,
parentJointIndex: handJointIndex,
velocity: {x: 0, y: 0, z: 0},
angularVelocity: {x: 0, y: 0, z: 0}
};
if (this.thisHandIsParent(grabbedProperties)) {
// this should never happen, but if it does, don't set previous parent to be this hand.
// this.previousParentID[this.grabbedThingID] = NULL;
// this.previousParentJointIndex[this.grabbedThingID] = -1;
} else if (this.otherHandIsParent(grabbedProperties)) {
// the other hand is parent. Steal the object and information
var otherModule = this.getOtherModule();
this.previousParentID[this.grabbedThingID] = otherModule.previousParentID[this.garbbedThingID];
this.previousParentJointIndex[this.grabbedThingID] = otherModule.previousParentJointIndex[this.grabbedThingID];
} else {
this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID;
this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex;
}
Overlays.editOverlay(this.grabbedThingID, reparentProps);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'grab',
grabbedEntity: this.grabbedThingID,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
};
this.endNearParentingGrabOverlay = function () {
var previousParentID = this.previousParentID[this.grabbedThingID];
if (previousParentID === NULL_UUID || previousParentID === null || previousParentID === undefined) {
Overlays.editOverlay(this.grabbedThingID, {
parentID: NULL_UUID,
parentJointIndex: -1
});
} else {
// before we grabbed it, overlay was a child of something; put it back.
Overlays.editOverlay(this.grabbedThingID, {
parentID: this.previousParentID[this.grabbedThingID],
parentJointIndex: this.previousParentJointIndex[this.grabbedThingID]
});
}
this.grabbedThingID = null;
};
this.getTargetID = function(overlays, controllerData) {
for (var i = 0; i < overlays.length; i++) {
var overlayPosition = Overlays.getProperty(overlays[i], "position");
var handPosition = controllerData.controllerLocations[this.hand].position;
var distance = Vec3.distance(overlayPosition, handPosition);
if (distance <= GRAB_RADIUS) {
return overlays[i];
}
}
return null;
};
this.isReady = function (controllerData) {
if (controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0) {
return makeRunningValues(false, [], []);
}
this.grabbedThingID = null;
var candidateOverlays = controllerData.nearbyOverlayIDs[this.hand];
var grabbableOverlays = candidateOverlays.filter(function(overlayID) {
return Overlays.getProperty(overlayID, "grabbable");
});
var targetID = this.getTargetID(grabbableOverlays, controllerData);
if (targetID) {
this.grabbedThingID = targetID;
this.startNearParentingGrabOverlay(controllerData);
return makeRunningValues(true, [this.grabbedThingID], []);
} else {
return makeRunningValues(false, [], []);
}
};
this.run = function (controllerData) {
if (controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0) {
this.endNearParentingGrabOverlay();
return makeRunningValues(false, [], []);
} else {
// check if someone stole the target from us
var grabbedProperties = this.getGrabbedProperties();
if (!this.thisHandIsParent(grabbedProperties)) {
return makeRunningValues(false, [], []);
}
return makeRunningValues(true, [this.grabbedThingID], []);
}
};
this.cleanup = function () {
if (this.grabbedThingID) {
this.endNearParentingGrabOverlay();
}
};
}
var leftNearParentingGrabOverlay = new NearParentingGrabOverlay(LEFT_HAND);
var rightNearParentingGrabOverlay = new NearParentingGrabOverlay(RIGHT_HAND);
enableDispatcherModule("LeftNearParentingGrabOverlay", leftNearParentingGrabOverlay);
enableDispatcherModule("RightNearParentingGrabOverlay", rightNearParentingGrabOverlay);
this.cleanup = function () {
leftNearParentingGrabOverlay.cleanup();
rightNearParentingGrabOverlay.cleanup();
disableDispatcherModule("LeftNearParentingGrabOverlay");
disableDispatcherModule("RightNearParentingGrabOverlay");
};
Script.scriptEnding.connect(this.cleanup);
}());

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/libraries/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(
520,
this.hand === RIGHT_HAND ? ["rightHandTrigger", "rightHand"] : ["leftHandTrigger", "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) {
continue;
}
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

@ -0,0 +1,553 @@
"use strict";
// overlayLaserInput.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,
NULL_UUID, enableDispatcherModule, disableDispatcherModule, makeRunningValues,
Messages, Quat, Vec3, getControllerWorldLocation, makeDispatcherModuleParameters, Overlays, ZERO_VEC,
AVATAR_SELF_ID, HMD, INCHES_TO_METERS, DEFAULT_REGISTRATION_POINT, Settings, getGrabPointSphereOffset,
COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD,
DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_ON_VALUE, TRIGGER_OFF_VALUE, getEnabledModuleByName, PICK_MAX_DISTANCE,
DISPATCHER_PROPERTIES
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
var halfPath = {
type: "line3d",
color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var halfEnd = {
type: "sphere",
solid: true,
color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
alpha: 0.9,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
};
var fullPath = {
type: "line3d",
color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var fullEnd = {
type: "sphere",
solid: true,
color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
alpha: 0.9,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
};
var holdPath = {
type: "line3d",
color: COLORS_GRAB_DISTANCE_HOLD,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var renderStates = [
{name: "half", path: halfPath, end: halfEnd},
{name: "full", path: fullPath, end: fullEnd},
{name: "hold", path: holdPath}
];
var defaultRenderStates = [
{name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath},
{name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath},
{name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath}
];
// triggered when stylus presses a web overlay/entity
var HAPTIC_STYLUS_STRENGTH = 1.0;
var HAPTIC_STYLUS_DURATION = 20.0;
function laserTargetHasKeyboardFocus(laserTarget) {
if (laserTarget && laserTarget !== NULL_UUID) {
return Overlays.keyboardFocusOverlay === laserTarget;
}
}
function setKeyboardFocusOnLaserTarget(laserTarget) {
if (laserTarget && laserTarget !== NULL_UUID) {
Overlays.keyboardFocusOverlay = laserTarget;
Entities.keyboardFocusEntity = NULL_UUID;
}
}
function sendHoverEnterEventToLaserTarget(hand, laserTarget) {
if (!laserTarget) {
return;
}
var pointerEvent = {
type: "Move",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: laserTarget.position2D,
pos3D: laserTarget.position,
normal: laserTarget.normal,
direction: Vec3.subtract(ZERO_VEC, laserTarget.normal),
button: "None"
};
if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) {
Overlays.sendHoverEnterOverlay(laserTarget.overlayID, pointerEvent);
}
}
function sendHoverOverEventToLaserTarget(hand, laserTarget) {
if (!laserTarget) {
return;
}
var pointerEvent = {
type: "Move",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: laserTarget.position2D,
pos3D: laserTarget.position,
normal: laserTarget.normal,
direction: Vec3.subtract(ZERO_VEC, laserTarget.normal),
button: "None"
};
if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) {
Overlays.sendMouseMoveOnOverlay(laserTarget.overlayID, pointerEvent);
Overlays.sendHoverOverOverlay(laserTarget.overlayID, pointerEvent);
}
}
function sendTouchStartEventToLaserTarget(hand, laserTarget) {
if (!laserTarget) {
return;
}
var pointerEvent = {
type: "Press",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: laserTarget.position2D,
pos3D: laserTarget.position,
normal: laserTarget.normal,
direction: Vec3.subtract(ZERO_VEC, laserTarget.normal),
button: "Primary",
isPrimaryHeld: true
};
if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) {
Overlays.sendMousePressOnOverlay(laserTarget.overlayID, pointerEvent);
}
}
function sendTouchEndEventToLaserTarget(hand, laserTarget) {
if (!laserTarget) {
return;
}
var pointerEvent = {
type: "Release",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: laserTarget.position2D,
pos3D: laserTarget.position,
normal: laserTarget.normal,
direction: Vec3.subtract(ZERO_VEC, laserTarget.normal),
button: "Primary"
};
if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) {
Overlays.sendMouseReleaseOnOverlay(laserTarget.overlayID, pointerEvent);
Overlays.sendMouseReleaseOnOverlay(laserTarget.overlayID, pointerEvent);
}
}
function sendTouchMoveEventToLaserTarget(hand, laserTarget) {
if (!laserTarget) {
return;
}
var pointerEvent = {
type: "Move",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: laserTarget.position2D,
pos3D: laserTarget.position,
normal: laserTarget.normal,
direction: Vec3.subtract(ZERO_VEC, laserTarget.normal),
button: "Primary",
isPrimaryHeld: true
};
if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) {
Overlays.sendMouseReleaseOnOverlay(laserTarget.overlayID, pointerEvent);
}
}
// will return undefined if overlayID does not exist.
function calculateLaserTargetFromOverlay(worldPos, overlayID) {
var overlayPosition = Overlays.getProperty(overlayID, "position");
if (overlayPosition === undefined) {
return null;
}
// project stylusTip onto overlay plane.
var overlayRotation = Overlays.getProperty(overlayID, "rotation");
if (overlayRotation === undefined) {
return null;
}
var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1});
var distance = Vec3.dot(Vec3.subtract(worldPos, overlayPosition), normal);
// calclulate normalized position
var invRot = Quat.inverse(overlayRotation);
var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, overlayPosition));
var dpi = Overlays.getProperty(overlayID, "dpi");
var dimensions;
if (dpi) {
// Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property
// is used as a scale.
var resolution = Overlays.getProperty(overlayID, "resolution");
if (resolution === undefined) {
return null;
}
resolution.z = 1;// Circumvent divide-by-zero.
var scale = Overlays.getProperty(overlayID, "dimensions");
if (scale === undefined) {
return null;
}
scale.z = 0.01;// overlay dimensions are 2D, not 3D.
dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale);
} else {
dimensions = Overlays.getProperty(overlayID, "dimensions");
if (dimensions === undefined) {
return null;
}
if (!dimensions.z) {
dimensions.z = 0.01;// sometimes overlay dimensions are 2D, not 3D.
}
}
var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z };
var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT);
// 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner.
var position2D = {
x: normalizedPosition.x * dimensions.x,
y: (1 - normalizedPosition.y) * dimensions.y // flip y-axis
};
return {
entityID: null,
overlayID: overlayID,
distance: distance,
position: worldPos,
position2D: position2D,
normal: normal,
normalizedPosition: normalizedPosition,
dimensions: dimensions,
valid: true
};
}
function distance2D(a, b) {
var dx = (a.x - b.x);
var dy = (a.y - b.y);
return Math.sqrt(dx * dx + dy * dy);
}
function OverlayLaserInput(hand) {
this.hand = hand;
this.active = false;
this.previousLaserClikcedTarget = false;
this.laserPressingTarget = false;
this.tabletScreenID = null;
this.mode = "none";
this.laserTargetID = null;
this.laserTarget = null;
this.pressEnterLaserTarget = null;
this.hover = false;
this.target = null;
this.lastValidTargetID = this.tabletTargetID;
this.parameters = makeDispatcherModuleParameters(
120,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
this.getOtherHandController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand;
};
this.getOtherModule = function() {
return (this.hand === RIGHT_HAND) ? leftOverlayLaserInput : rightOverlayLaserInput;
};
this.handToController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
};
this.stealTouchFocus = function(laserTarget) {
this.requestTouchFocus(laserTarget);
};
this.requestTouchFocus = function(laserTarget) {
if (laserTarget !== null || laserTarget !== undefined) {
sendHoverEnterEventToLaserTarget(this.hand, this.laserTarget);
this.lastValidTargetID = laserTarget;
}
};
this.relinquishTouchFocus = function() {
// send hover leave event.
var pointerEvent = { type: "Move", id: this.hand + 1 };
Overlays.sendMouseMoveOnOverlay(this.lastValidTargetID, pointerEvent);
Overlays.sendHoverOverOverlay(this.lastValidTargetID, pointerEvent);
Overlays.sendHoverLeaveOverlay(this.lastValidID, pointerEvent);
};
this.updateLaserPointer = function(controllerData) {
var RADIUS = 0.005;
var dim = { x: RADIUS, y: RADIUS, z: RADIUS };
if (this.mode === "full") {
this.fullEnd.dimensions = dim;
LaserPointers.editRenderState(this.laserPointer, this.mode, {path: fullPath, end: this.fullEnd});
} else if (this.mode === "half") {
this.halfEnd.dimensions = dim;
LaserPointers.editRenderState(this.laserPointer, this.mode, {path: halfPath, end: this.halfEnd});
}
LaserPointers.enableLaserPointer(this.laserPointer);
LaserPointers.setRenderState(this.laserPointer, this.mode);
};
this.processControllerTriggers = function(controllerData) {
if (controllerData.triggerClicks[this.hand]) {
this.mode = "full";
this.laserPressingTarget = true;
this.hover = false;
} else if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
this.mode = "half";
this.laserPressingTarget = false;
this.hover = true;
this.requestTouchFocus(this.laserTargetID);
} else {
this.mode = "none";
this.laserPressingTarget = false;
this.hover = false;
this.relinquishTouchFocus();
}
};
this.hovering = function() {
if (!laserTargetHasKeyboardFocus(this.laserTagetID)) {
setKeyboardFocusOnLaserTarget(this.laserTargetID);
}
sendHoverOverEventToLaserTarget(this.hand, this.laserTarget);
};
this.laserPressEnter = function () {
sendTouchStartEventToLaserTarget(this.hand, this.laserTarget);
Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand);
this.touchingEnterTimer = 0;
this.pressEnterLaserTarget = this.laserTarget;
this.deadspotExpired = false;
var LASER_PRESS_TO_MOVE_DEADSPOT = 0.026;
this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT) * this.laserTarget.distance;
};
this.laserPressExit = function () {
if (this.laserTarget === null) {
return;
}
// special case to handle home button.
if (this.laserTargetID === HMD.homeButtonID) {
Messages.sendLocalMessage("home", this.laserTargetID);
}
// send press event
if (this.deadspotExpired) {
sendTouchEndEventToLaserTarget(this.hand, this.laserTarget);
} else {
sendTouchEndEventToLaserTarget(this.hand, this.pressEnterLaserTarget);
}
};
this.laserPressing = function (controllerData, dt) {
this.touchingEnterTimer += dt;
if (this.laserTarget) {
var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds
if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY ||
distance2D( this.laserTarget.position2D,
this.pressEnterLaserTarget.position2D) > this.deadspotRadius) {
sendTouchMoveEventToLaserTarget(this.hand, this.laserTarget);
this.deadspotExpired = true;
}
} else {
this.laserPressingTarget = false;
}
};
this.releaseTouchEvent = function() {
sendTouchEndEventToLaserTarget(this.hand, this.pressEnterLaserTarget);
};
this.updateLaserTargets = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
this.laserTargetID = intersection.objectID;
this.laserTarget = calculateLaserTargetFromOverlay(intersection.intersection, intersection.objectID);
};
this.shouldExit = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
var nearGrabName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay";
var nearGrabModule = getEnabledModuleByName(nearGrabName);
var status = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []);
var offOverlay = (intersection.type !== RayPick.INTERSECTED_OVERLAY);
var triggerOff = (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE);
return offOverlay || status.active || triggerOff;
};
this.exitModule = function() {
this.releaseTouchEvent();
this.relinquishTouchFocus();
this.reset();
this.updateLaserPointer();
LaserPointers.disableLaserPointer(this.laserPointer);
};
this.reset = function() {
this.hover = false;
this.pressEnterLaserTarget = null;
this.laserTarget = null;
this.laserTargetID = null;
this.laserPressingTarget = false;
this.previousLaserClickedTarget = null;
this.mode = "none";
this.active = false;
};
this.deleteContextOverlay = function() {
var farGrabModule = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightFarActionGrabEntity" : "LeftFarActionGrabEntity");
if (farGrabModule) {
var entityWithContextOverlay = farGrabModule.entityWithContextOverlay;
if (entityWithContextOverlay) {
ContextOverlay.destroyContextOverlay(entityWithContextOverlay);
farGrabModule.entityWithContextOverlay = false;
}
}
};
this.isReady = function (controllerData) {
this.target = null;
var intersection = controllerData.rayPicks[this.hand];
if (intersection.type === RayPick.INTERSECTED_OVERLAY) {
if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE && !this.getOtherModule().active) {
this.target = intersection.objectID;
this.active = true;
return makeRunningValues(true, [], []);
} else {
this.deleteContextOverlay();
}
}
this.reset();
return makeRunningValues(false, [], []);
};
this.run = function (controllerData, deltaTime) {
if (this.shouldExit(controllerData)) {
this.exitModule();
return makeRunningValues(false, [], []);
}
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) {
this.deleteContextOverlay();
}
this.updateLaserTargets(controllerData);
this.processControllerTriggers(controllerData);
this.updateLaserPointer(controllerData);
if (!this.previousLaserClickedTarget && this.laserPressingTarget) {
this.laserPressEnter();
}
if (this.previousLaserClickedTarget && !this.laserPressingTarget) {
this.laserPressExit();
}
this.previousLaserClickedTarget = this.laserPressingTarget;
if (this.laserPressingTarget) {
this.laserPressing(controllerData, deltaTime);
}
if (this.hover) {
this.hovering();
}
return makeRunningValues(true, [], []);
};
this.cleanup = function () {
LaserPointers.disableLaserPointer(this.laserPointer);
LaserPointers.removeLaserPointer(this.laserPointer);
};
this.halfEnd = halfEnd;
this.fullEnd = fullEnd;
this.laserPointer = LaserPointers.createLaserPointer({
joint: (this.hand === RIGHT_HAND) ? "_CONTROLLER_RIGHTHAND" : "_CONTROLLER_LEFTHAND",
filter: RayPick.PICK_OVERLAYS,
maxDistance: PICK_MAX_DISTANCE,
posOffset: getGrabPointSphereOffset(this.handToController()),
renderStates: renderStates,
faceAvatar: true,
defaultRenderStates: defaultRenderStates
});
LaserPointers.setIgnoreOverlays(this.laserPointer, [HMD.tabletID]);
}
var leftOverlayLaserInput = new OverlayLaserInput(LEFT_HAND);
var rightOverlayLaserInput = new OverlayLaserInput(RIGHT_HAND);
enableDispatcherModule("LeftOverlayLaserInput", leftOverlayLaserInput);
enableDispatcherModule("RightOverlayLaserInput", rightOverlayLaserInput);
this.cleanup = function () {
leftOverlayLaserInput.cleanup();
rightOverlayLaserInput.cleanup();
disableDispatcherModule("LeftOverlayLaserInput");
disableDispatcherModule("RightOverlayLaserInput");
};
Script.scriptEnding.connect(this.cleanup);
}());

View file

@ -0,0 +1,83 @@
// handControllerGrab.js
//
// Created by Dante Ruiz on 9/11/17
//
// Grabs physically moveable entities with hydra-like controllers; it works for either near or far objects.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* global getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings,
Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset,
setGrabCommunications, Menu, HMD, isInEditMode, AvatarList */
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
(function () {
var dispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js");
function ScaleAvatar(hand) {
this.hand = hand;
this.scalingStartAvatarScale = 0;
this.scalingStartDistance = 0;
this.parameters = dispatcherUtils.makeDispatcherModuleParameters(
120,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100
);
this.otherHand = function() {
return this.hand === dispatcherUtils.RIGHT_HAND ? dispatcherUtils.LEFT_HAND : dispatcherUtils.RIGHT_HAND;
};
this.getOtherModule = function() {
var otherModule = this.hand === dispatcherUtils.RIGHT_HAND ? leftScaleAvatar : rightScaleAvatar;
return otherModule;
};
this.triggersPressed = function(controllerData) {
if (controllerData.triggerClicks[this.hand] && controllerData.secondaryValues[this.hand] > dispatcherUtils.BUMPER_ON_VALUE) {
return true;
}
return false;
};
this.isReady = function(controllerData) {
var otherModule = this.getOtherModule();
if (this.triggersPressed(controllerData) && otherModule.triggersPressed(controllerData)) {
this.scalingStartAvatarScale = MyAvatar.scale;
this.scalingStartDistance = Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position,
controllerData.controllerLocations[this.otherHand()].position));
return dispatcherUtils.makeRunningValues(true, [], []);
}
return dispatcherUtils.makeRunningValues(false, [], []);
};
this.run = function(controllerData) {
var otherModule = this.getOtherModule();
if (this.triggersPressed(controllerData) && otherModule.triggersPressed(controllerData)) {
if (this.hand === dispatcherUtils.RIGHT_HAND) {
var scalingCurrentDistance = Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position,
controllerData.controllerLocations[this.otherHand()].position));
var newAvatarScale = (scalingCurrentDistance / this.scalingStartDistance) * this.scalingStartAvatarScale;
MyAvatar.scale = newAvatarScale;
}
return dispatcherUtils.makeRunningValues(true, [], []);
}
return dispatcherUtils.makeRunningValues(false, [], []);
};
}
var leftScaleAvatar = new ScaleAvatar(dispatcherUtils.LEFT_HAND);
var rightScaleAvatar = new ScaleAvatar(dispatcherUtils.RIGHT_HAND);
dispatcherUtils.enableDispatcherModule("LeftScaleAvatar", leftScaleAvatar);
dispatcherUtils.enableDispatcherModule("RightScaleAvatar", rightScaleAvatar);
this.cleanup = function() {
dispatcherUtils.disableDispatcherModule("LeftScaleAvatar");
dispatcherUtils.disableDispatcherModule("RightScaleAvatar");
};
})();

View file

@ -0,0 +1,704 @@
"use strict";
// tabletStylusInput.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,
NULL_UUID, enableDispatcherModule, disableDispatcherModule, makeRunningValues,
Messages, Quat, Vec3, getControllerWorldLocation, makeDispatcherModuleParameters, Overlays, ZERO_VEC,
AVATAR_SELF_ID, HMD, INCHES_TO_METERS, DEFAULT_REGISTRATION_POINT, Settings, getGrabPointSphereOffset
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
// triggered when stylus presses a web overlay/entity
var HAPTIC_STYLUS_STRENGTH = 1.0;
var HAPTIC_STYLUS_DURATION = 20.0;
var WEB_DISPLAY_STYLUS_DISTANCE = 0.5;
var WEB_STYLUS_LENGTH = 0.2;
var WEB_TOUCH_Y_OFFSET = 0.05; // how far forward (or back with a negative number) to slide stylus in hand
function stylusTargetHasKeyboardFocus(stylusTarget) {
if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) {
return Entities.keyboardFocusEntity === stylusTarget.entityID;
} else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) {
return Overlays.keyboardFocusOverlay === stylusTarget.overlayID;
}
}
function setKeyboardFocusOnStylusTarget(stylusTarget) {
if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID &&
Entities.wantsHandControllerPointerEvents(stylusTarget.entityID)) {
Overlays.keyboardFocusOverlay = NULL_UUID;
Entities.keyboardFocusEntity = stylusTarget.entityID;
} else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) {
Overlays.keyboardFocusOverlay = stylusTarget.overlayID;
Entities.keyboardFocusEntity = NULL_UUID;
}
}
function sendHoverEnterEventToStylusTarget(hand, stylusTarget) {
var pointerEvent = {
type: "Move",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: stylusTarget.position2D,
pos3D: stylusTarget.position,
normal: stylusTarget.normal,
direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal),
button: "None"
};
if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) {
Entities.sendHoverEnterEntity(stylusTarget.entityID, pointerEvent);
} else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) {
Overlays.sendHoverEnterOverlay(stylusTarget.overlayID, pointerEvent);
}
}
function sendHoverOverEventToStylusTarget(hand, stylusTarget) {
var pointerEvent = {
type: "Move",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: stylusTarget.position2D,
pos3D: stylusTarget.position,
normal: stylusTarget.normal,
direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal),
button: "None"
};
if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) {
Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent);
Entities.sendHoverOverEntity(stylusTarget.entityID, pointerEvent);
} else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) {
Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent);
Overlays.sendHoverOverOverlay(stylusTarget.overlayID, pointerEvent);
}
}
function sendTouchStartEventToStylusTarget(hand, stylusTarget) {
var pointerEvent = {
type: "Press",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: stylusTarget.position2D,
pos3D: stylusTarget.position,
normal: stylusTarget.normal,
direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal),
button: "Primary",
isPrimaryHeld: true
};
if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) {
Entities.sendMousePressOnEntity(stylusTarget.entityID, pointerEvent);
Entities.sendClickDownOnEntity(stylusTarget.entityID, pointerEvent);
} else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) {
Overlays.sendMousePressOnOverlay(stylusTarget.overlayID, pointerEvent);
}
}
function sendTouchEndEventToStylusTarget(hand, stylusTarget) {
var pointerEvent = {
type: "Release",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: stylusTarget.position2D,
pos3D: stylusTarget.position,
normal: stylusTarget.normal,
direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal),
button: "Primary"
};
if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) {
Entities.sendMouseReleaseOnEntity(stylusTarget.entityID, pointerEvent);
Entities.sendClickReleaseOnEntity(stylusTarget.entityID, pointerEvent);
Entities.sendHoverLeaveEntity(stylusTarget.entityID, pointerEvent);
} else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) {
Overlays.sendMouseReleaseOnOverlay(stylusTarget.overlayID, pointerEvent);
}
}
function sendTouchMoveEventToStylusTarget(hand, stylusTarget) {
var pointerEvent = {
type: "Move",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: stylusTarget.position2D,
pos3D: stylusTarget.position,
normal: stylusTarget.normal,
direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal),
button: "Primary",
isPrimaryHeld: true
};
if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) {
Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent);
Entities.sendHoldingClickOnEntity(stylusTarget.entityID, pointerEvent);
} else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) {
Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent);
}
}
// will return undefined if overlayID does not exist.
function calculateStylusTargetFromOverlay(stylusTip, overlayID) {
var overlayPosition = Overlays.getProperty(overlayID, "position");
if (overlayPosition === undefined) {
return;
}
// project stylusTip onto overlay plane.
var overlayRotation = Overlays.getProperty(overlayID, "rotation");
if (overlayRotation === undefined) {
return;
}
var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1});
var distance = Vec3.dot(Vec3.subtract(stylusTip.position, overlayPosition), normal);
var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance));
// calclulate normalized position
var invRot = Quat.inverse(overlayRotation);
var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, overlayPosition));
var dpi = Overlays.getProperty(overlayID, "dpi");
var dimensions;
if (dpi) {
// Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property
// is used as a scale.
var resolution = Overlays.getProperty(overlayID, "resolution");
if (resolution === undefined) {
return;
}
resolution.z = 1; // Circumvent divide-by-zero.
var scale = Overlays.getProperty(overlayID, "dimensions");
if (scale === undefined) {
return;
}
scale.z = 0.01; // overlay dimensions are 2D, not 3D.
dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale);
} else {
dimensions = Overlays.getProperty(overlayID, "dimensions");
if (dimensions === undefined) {
return;
}
if (!dimensions.z) {
dimensions.z = 0.01; // sometimes overlay dimensions are 2D, not 3D.
}
}
var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z };
var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT);
// 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner.
var position2D = {
x: normalizedPosition.x * dimensions.x,
y: (1 - normalizedPosition.y) * dimensions.y // flip y-axis
};
return {
entityID: null,
overlayID: overlayID,
distance: distance,
position: position,
position2D: position2D,
normal: normal,
normalizedPosition: normalizedPosition,
dimensions: dimensions,
valid: true
};
}
// will return undefined if entity does not exist.
function calculateStylusTargetFromEntity(stylusTip, props) {
if (props.rotation === undefined) {
// if rotation is missing from props object, then this entity has probably been deleted.
return;
}
// project stylus tip onto entity plane.
var normal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1});
Vec3.multiplyQbyV(props.rotation, {x: 0, y: 1, z: 0});
var distance = Vec3.dot(Vec3.subtract(stylusTip.position, props.position), normal);
var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance));
// generate normalized coordinates
var invRot = Quat.inverse(props.rotation);
var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, props.position));
var invDimensions = { x: 1 / props.dimensions.x, y: 1 / props.dimensions.y, z: 1 / props.dimensions.z };
var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint);
// 2D position on entity plane in meters, relative to the bounding box upper-left hand corner.
var position2D = {
x: normalizedPosition.x * props.dimensions.x,
y: (1 - normalizedPosition.y) * props.dimensions.y // flip y-axis
};
return {
entityID: props.id,
entityProps: props,
overlayID: null,
distance: distance,
position: position,
position2D: position2D,
normal: normal,
normalizedPosition: normalizedPosition,
dimensions: props.dimensions,
valid: true
};
}
function isNearStylusTarget(stylusTargets, edgeBorder, minNormalDistance, maxNormalDistance) {
for (var i = 0; i < stylusTargets.length; i++) {
var stylusTarget = stylusTargets[i];
// check to see if the projected stylusTip is within within the 2d border
var borderMin = {x: -edgeBorder, y: -edgeBorder};
var borderMax = {x: stylusTarget.dimensions.x + edgeBorder, y: stylusTarget.dimensions.y + edgeBorder};
if (stylusTarget.distance >= minNormalDistance && stylusTarget.distance <= maxNormalDistance &&
stylusTarget.position2D.x >= borderMin.x && stylusTarget.position2D.y >= borderMin.y &&
stylusTarget.position2D.x <= borderMax.x && stylusTarget.position2D.y <= borderMax.y) {
return true;
}
}
return false;
}
function calculateNearestStylusTarget(stylusTargets) {
var nearestStylusTarget;
for (var i = 0; i < stylusTargets.length; i++) {
var stylusTarget = stylusTargets[i];
if ((!nearestStylusTarget || stylusTarget.distance < nearestStylusTarget.distance) &&
stylusTarget.normalizedPosition.x >= 0 && stylusTarget.normalizedPosition.y >= 0 &&
stylusTarget.normalizedPosition.x <= 1 && stylusTarget.normalizedPosition.y <= 1) {
nearestStylusTarget = stylusTarget;
}
}
return nearestStylusTarget;
}
function getFingerWorldLocation(hand) {
var fingerJointName = (hand === RIGHT_HAND) ? "RightHandIndex4" : "LeftHandIndex4";
var fingerJointIndex = MyAvatar.getJointIndex(fingerJointName);
var fingerPosition = MyAvatar.getAbsoluteJointTranslationInObjectFrame(fingerJointIndex);
var fingerRotation = MyAvatar.getAbsoluteJointRotationInObjectFrame(fingerJointIndex);
var worldFingerRotation = Quat.multiply(MyAvatar.orientation, fingerRotation);
var worldFingerPosition = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, fingerPosition));
return {
position: worldFingerPosition,
orientation: worldFingerRotation,
rotation: worldFingerRotation,
valid: true
};
}
function distance2D(a, b) {
var dx = (a.x - b.x);
var dy = (a.y - b.y);
return Math.sqrt(dx * dx + dy * dy);
}
function TabletStylusInput(hand) {
this.hand = hand;
this.previousStylusTouchingTarget = false;
this.stylusTouchingTarget = false;
this.useFingerInsteadOfStylus = false;
this.fingerPointing = false;
// initialize stylus tip
var DEFAULT_STYLUS_TIP = {
position: {x: 0, y: 0, z: 0},
orientation: {x: 0, y: 0, z: 0, w: 0},
rotation: {x: 0, y: 0, z: 0, w: 0},
velocity: {x: 0, y: 0, z: 0},
valid: false
};
this.stylusTip = DEFAULT_STYLUS_TIP;
this.parameters = makeDispatcherModuleParameters(
100,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
this.getOtherHandController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand;
};
this.handToController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
};
this.updateFingerAsStylusSetting = function () {
var DEFAULT_USE_FINGER_AS_STYLUS = false;
var USE_FINGER_AS_STYLUS = Settings.getValue("preferAvatarFingerOverStylus");
if (USE_FINGER_AS_STYLUS === "") {
USE_FINGER_AS_STYLUS = DEFAULT_USE_FINGER_AS_STYLUS;
}
if (USE_FINGER_AS_STYLUS && MyAvatar.getJointIndex("LeftHandIndex4") !== -1) {
this.useFingerInsteadOfStylus = true;
} else {
this.useFingerInsteadOfStylus = false;
}
};
this.updateStylusTip = function() {
if (this.useFingerInsteadOfStylus) {
this.stylusTip = getFingerWorldLocation(this.hand);
} else {
this.stylusTip = getControllerWorldLocation(this.handToController(), true);
// translate tip forward according to constant.
var TIP_OFFSET = {x: 0, y: WEB_STYLUS_LENGTH - WEB_TOUCH_Y_OFFSET, z: 0};
this.stylusTip.position = Vec3.sum(this.stylusTip.position,
Vec3.multiplyQbyV(this.stylusTip.orientation, TIP_OFFSET));
}
// compute tip velocity from hand controller motion, it is more accurate than computing it from previous positions.
var pose = Controller.getPoseValue(this.handToController());
if (pose.valid) {
var worldControllerPos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation));
var worldControllerLinearVel = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity);
var worldControllerAngularVel = Vec3.multiplyQbyV(MyAvatar.orientation, pose.angularVelocity);
var tipVelocity = Vec3.sum(worldControllerLinearVel, Vec3.cross(worldControllerAngularVel,
Vec3.subtract(this.stylusTip.position, worldControllerPos)));
this.stylusTip.velocity = tipVelocity;
} else {
this.stylusTip.velocity = {x: 0, y: 0, z: 0};
}
};
this.showStylus = function() {
if (this.stylus) {
return;
}
var stylusProperties = {
name: "stylus",
url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx",
loadPriority: 10.0,
localPosition: Vec3.sum({
x: 0.0,
y: WEB_TOUCH_Y_OFFSET,
z: 0.0
}, getGrabPointSphereOffset(this.handToController())),
localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }),
dimensions: { x: 0.01, y: 0.01, z: WEB_STYLUS_LENGTH },
solid: true,
visible: true,
ignoreRayIntersection: true,
drawInFront: false,
parentID: AVATAR_SELF_ID,
parentJointIndex: MyAvatar.getJointIndex(this.hand === RIGHT_HAND ?
"_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" :
"_CAMERA_RELATIVE_CONTROLLER_LEFTHAND")
};
this.stylus = Overlays.addOverlay("model", stylusProperties);
};
this.hideStylus = function() {
if (!this.stylus) {
return;
}
Overlays.deleteOverlay(this.stylus);
this.stylus = null;
};
this.stealTouchFocus = function(stylusTarget) {
// send hover events to target
// record the entity or overlay we are hovering over.
if ((stylusTarget.entityID === this.getOtherHandController().hoverEntity) ||
(stylusTarget.overlayID === this.getOtherHandController().hoverOverlay)) {
this.getOtherHandController().relinquishTouchFocus();
}
this.requestTouchFocus(stylusTarget);
};
this.requestTouchFocus = function(stylusTarget) {
// send hover events to target if we can.
// record the entity or overlay we are hovering over.
if (stylusTarget.entityID &&
stylusTarget.entityID !== this.hoverEntity &&
stylusTarget.entityID !== this.getOtherHandController().hoverEntity) {
this.hoverEntity = stylusTarget.entityID;
sendHoverEnterEventToStylusTarget(this.hand, stylusTarget);
} else if (stylusTarget.overlayID &&
stylusTarget.overlayID !== this.hoverOverlay &&
stylusTarget.overlayID !== this.getOtherHandController().hoverOverlay) {
this.hoverOverlay = stylusTarget.overlayID;
sendHoverEnterEventToStylusTarget(this.hand, stylusTarget);
}
};
this.hasTouchFocus = function(stylusTarget) {
return ((stylusTarget.entityID && stylusTarget.entityID === this.hoverEntity) ||
(stylusTarget.overlayID && stylusTarget.overlayID === this.hoverOverlay));
};
this.relinquishTouchFocus = function() {
// send hover leave event.
var pointerEvent = { type: "Move", id: this.hand + 1 };
if (this.hoverEntity) {
Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent);
this.hoverEntity = null;
} else if (this.hoverOverlay) {
Overlays.sendMouseMoveOnOverlay(this.hoverOverlay, pointerEvent);
Overlays.sendHoverOverOverlay(this.hoverOverlay, pointerEvent);
Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent);
this.hoverOverlay = null;
}
};
this.pointFinger = function(value) {
var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index";
if (this.fingerPointing !== value) {
var message;
if (this.hand === RIGHT_HAND) {
message = { pointRightIndex: value };
} else {
message = { pointLeftIndex: value };
}
Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify(message), true);
this.fingerPointing = value;
}
};
this.processStylus = function(controllerData) {
this.updateStylusTip();
if (!this.stylusTip.valid || this.overlayLaserActive(controllerData)) {
this.pointFinger(false);
this.hideStylus();
return false;
}
if (this.useFingerInsteadOfStylus) {
this.hideStylus();
}
// build list of stylus targets, near the stylusTip
var stylusTargets = [];
var candidateEntities = controllerData.nearbyEntityProperties;
var i, props, stylusTarget;
for (i = 0; i < candidateEntities.length; i++) {
props = candidateEntities[i];
if (props && props.type === "Web") {
stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, candidateEntities[i]);
if (stylusTarget) {
stylusTargets.push(stylusTarget);
}
}
}
// add the tabletScreen, if it is valid
if (HMD.tabletScreenID && HMD.tabletScreenID !== NULL_UUID &&
Overlays.getProperty(HMD.tabletScreenID, "visible")) {
stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.tabletScreenID);
if (stylusTarget) {
stylusTargets.push(stylusTarget);
}
}
// add the tablet home button.
if (HMD.homeButtonID && HMD.homeButtonID !== NULL_UUID &&
Overlays.getProperty(HMD.homeButtonID, "visible")) {
stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.homeButtonID);
if (stylusTarget) {
stylusTargets.push(stylusTarget);
}
}
var TABLET_MIN_HOVER_DISTANCE = 0.01;
var TABLET_MAX_HOVER_DISTANCE = 0.1;
var TABLET_MIN_TOUCH_DISTANCE = -0.05;
var TABLET_MAX_TOUCH_DISTANCE = TABLET_MIN_HOVER_DISTANCE;
var EDGE_BORDER = 0.075;
var hysteresisOffset = 0.0;
if (this.isNearStylusTarget) {
hysteresisOffset = 0.05;
}
this.isNearStylusTarget = isNearStylusTarget(stylusTargets, EDGE_BORDER + hysteresisOffset,
TABLET_MIN_TOUCH_DISTANCE - hysteresisOffset,
WEB_DISPLAY_STYLUS_DISTANCE + hysteresisOffset);
if (this.isNearStylusTarget) {
if (!this.useFingerInsteadOfStylus) {
this.showStylus();
} else {
this.pointFinger(true);
}
} else {
this.hideStylus();
this.pointFinger(false);
}
var nearestStylusTarget = calculateNearestStylusTarget(stylusTargets);
if (nearestStylusTarget && nearestStylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE &&
nearestStylusTarget.distance < TABLET_MAX_HOVER_DISTANCE) {
this.requestTouchFocus(nearestStylusTarget);
if (!stylusTargetHasKeyboardFocus(nearestStylusTarget)) {
setKeyboardFocusOnStylusTarget(nearestStylusTarget);
}
if (this.hasTouchFocus(nearestStylusTarget)) {
sendHoverOverEventToStylusTarget(this.hand, nearestStylusTarget);
}
// filter out presses when tip is moving away from tablet.
// ensure that stylus is within bounding box by checking normalizedPosition
if (nearestStylusTarget.valid && nearestStylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE &&
nearestStylusTarget.distance < TABLET_MAX_TOUCH_DISTANCE &&
Vec3.dot(this.stylusTip.velocity, nearestStylusTarget.normal) < 0 &&
nearestStylusTarget.normalizedPosition.x >= 0 && nearestStylusTarget.normalizedPosition.x <= 1 &&
nearestStylusTarget.normalizedPosition.y >= 0 && nearestStylusTarget.normalizedPosition.y <= 1) {
this.stylusTarget = nearestStylusTarget;
this.stylusTouchingTarget = true;
}
} else {
this.relinquishTouchFocus();
}
this.homeButtonTouched = false;
if (this.isNearStylusTarget) {
return true;
} else {
this.pointFinger(false);
this.hideStylus();
return false;
}
};
this.stylusTouchingEnter = function () {
this.stealTouchFocus(this.stylusTarget);
sendTouchStartEventToStylusTarget(this.hand, this.stylusTarget);
Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand);
this.touchingEnterTimer = 0;
this.touchingEnterStylusTarget = this.stylusTarget;
this.deadspotExpired = false;
var TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0381;
this.deadspotRadius = TOUCH_PRESS_TO_MOVE_DEADSPOT;
};
this.stylusTouchingExit = function () {
if (this.stylusTarget === undefined) {
return;
}
// special case to handle home button.
if (this.stylusTarget.overlayID === HMD.homeButtonID) {
Messages.sendLocalMessage("home", this.stylusTarget.overlayID);
}
// send press event
if (this.deadspotExpired) {
sendTouchEndEventToStylusTarget(this.hand, this.stylusTarget);
} else {
sendTouchEndEventToStylusTarget(this.hand, this.touchingEnterStylusTarget);
}
};
this.stylusTouching = function (controllerData, dt) {
this.touchingEnterTimer += dt;
if (this.stylusTarget.entityID) {
this.stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, this.stylusTarget.entityProps);
} else if (this.stylusTarget.overlayID) {
this.stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, this.stylusTarget.overlayID);
}
var TABLET_MIN_TOUCH_DISTANCE = -0.1;
var TABLET_MAX_TOUCH_DISTANCE = 0.01;
if (this.stylusTarget) {
if (this.stylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE &&
this.stylusTarget.distance < TABLET_MAX_TOUCH_DISTANCE) {
var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds
if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY ||
distance2D(this.stylusTarget.position2D,
this.touchingEnterStylusTarget.position2D) > this.deadspotRadius) {
sendTouchMoveEventToStylusTarget(this.hand, this.stylusTarget);
this.deadspotExpired = true;
}
} else {
this.stylusTouchingTarget = false;
}
} else {
this.stylusTouchingTarget = false;
}
};
this.overlayLaserActive = function(controllerData) {
var overlayLaserModule = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightOverlayLaserInput" : "LeftOverlayLaserInput");
if (overlayLaserModule) {
return overlayLaserModule.isReady(controllerData).active;
}
return false;
};
this.isReady = function (controllerData) {
if (this.processStylus(controllerData)) {
return makeRunningValues(true, [], []);
} else {
return makeRunningValues(false, [], []);
}
};
this.run = function (controllerData, deltaTime) {
this.updateFingerAsStylusSetting();
if (!this.previousStylusTouchingTarget && this.stylusTouchingTarget) {
this.stylusTouchingEnter();
}
if (this.previousStylusTouchingTarget && !this.stylusTouchingTarget) {
this.stylusTouchingExit();
}
this.previousStylusTouchingTarget = this.stylusTouchingTarget;
if (this.stylusTouchingTarget) {
this.stylusTouching(controllerData, deltaTime);
}
if (this.processStylus(controllerData)) {
return makeRunningValues(true, [], []);
} else {
return makeRunningValues(false, [], []);
}
};
this.cleanup = function () {
this.hideStylus();
};
}
var leftTabletStylusInput = new TabletStylusInput(LEFT_HAND);
var rightTabletStylusInput = new TabletStylusInput(RIGHT_HAND);
enableDispatcherModule("LeftTabletStylusInput", leftTabletStylusInput);
enableDispatcherModule("RightTabletStylusInput", rightTabletStylusInput);
this.cleanup = function () {
leftTabletStylusInput.cleanup();
rightTabletStylusInput.cleanup();
disableDispatcherModule("LeftTabletStylusInput");
disableDispatcherModule("RightTabletStylusInput");
};
Script.scriptEnding.connect(this.cleanup);
}());

View file

@ -0,0 +1,522 @@
"use strict";
// Created by james b. pollack @imgntn on 7/2/2016
// Copyright 2016 High Fidelity, Inc.
//
// Creates a beam and target and then teleports you there. Release when its close to you to cancel.
//
// 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
*/
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
Script.include("/~/system/libraries/Xform.js");
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() { // BEGIN LOCAL_SCOPE
var inTeleportMode = false;
var SMOOTH_ARRIVAL_SPACING = 33;
var NUMBER_OF_STEPS = 6;
var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleport-destination.fbx");
var TOO_CLOSE_MODEL_URL = Script.resolvePath("../../assets/models/teleport-cancel.fbx");
var SEAT_MODEL_URL = Script.resolvePath("../../assets/models/teleport-seat.fbx");
var TARGET_MODEL_DIMENSIONS = {
x: 1.15,
y: 0.5,
z: 1.15
};
var COLORS_TELEPORT_SEAT = {
red: 255,
green: 0,
blue: 170
};
var COLORS_TELEPORT_CAN_TELEPORT = {
red: 97,
green: 247,
blue: 255
};
var COLORS_TELEPORT_CANCEL = {
red: 255,
green: 184,
blue: 73
};
var TELEPORT_CANCEL_RANGE = 1;
var COOL_IN_DURATION = 500;
var handInfo = {
right: {
controllerInput: Controller.Standard.RightHand
},
left: {
controllerInput: Controller.Standard.LeftHand
}
};
var cancelPath = {
type: "line3d",
color: COLORS_TELEPORT_CANCEL,
ignoreRayIntersection: true,
alpha: 1,
solid: true,
drawInFront: true,
glow: 1.0
};
var teleportPath = {
type: "line3d",
color: COLORS_TELEPORT_CAN_TELEPORT,
ignoreRayIntersection: true,
alpha: 1,
solid: true,
drawInFront: true,
glow: 1.0
};
var seatPath = {
type: "line3d",
color: COLORS_TELEPORT_SEAT,
ignoreRayIntersection: true,
alpha: 1,
solid: true,
drawInFront: true,
glow: 1.0
};
var cancelEnd = {
type: "model",
url: TOO_CLOSE_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
ignoreRayIntersection: true
};
var teleportEnd = {
type: "model",
url: TARGET_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
ignoreRayIntersection: true
};
var seatEnd = {
type: "model",
url: SEAT_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
ignoreRayIntersection: true
}
var teleportRenderStates = [{name: "cancel", path: cancelPath, end: cancelEnd},
{name: "teleport", path: teleportPath, end: teleportEnd},
{name: "seat", path: seatPath, end: seatEnd}];
var DEFAULT_DISTANCE = 50;
var teleportDefaultRenderStates = [{name: "cancel", distance: DEFAULT_DISTANCE, path: cancelPath}];
function ThumbPad(hand) {
this.hand = hand;
var _thisPad = this;
this.buttonPress = function(value) {
_thisPad.buttonValue = value;
};
}
function Trigger(hand) {
this.hand = hand;
var _this = this;
this.buttonPress = function(value) {
_this.buttonValue = value;
};
this.down = function() {
var down = _this.buttonValue === 1 ? 1.0 : 0.0;
return down;
};
}
var coolInTimeout = null;
var ignoredEntities = [];
var TELEPORTER_STATES = {
IDLE: 'idle',
COOL_IN: 'cool_in',
TARGETTING: 'targetting',
TARGETTING_INVALID: 'targetting_invalid',
};
var TARGET = {
NONE: 'none', // Not currently targetting anything
INVISIBLE: 'invisible', // The current target is an invvsible surface
INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.)
SURFACE: 'surface', // The current target is a valid surface
SEAT: 'seat', // The current target is a seat
};
function Teleporter(hand) {
var _this = this;
this.hand = hand;
this.buttonValue = 0;
this.active = false;
this.state = TELEPORTER_STATES.IDLE;
this.currentTarget = TARGET.INVALID;
this.currentResult = null;
this.getOtherModule = function() {
var otherModule = this.hand === RIGHT_HAND ? leftTeleporter : rightTeleporter;
return otherModule;
};
this.teleportRayHandVisible = LaserPointers.createLaserPointer({
joint: (_this.hand === RIGHT_HAND) ? "RightHand" : "LeftHand",
filter: RayPick.PICK_ENTITIES,
faceAvatar: true,
centerEndY: false,
renderStates: teleportRenderStates,
defaultRenderStates: teleportDefaultRenderStates
});
this.teleportRayHandInvisible = LaserPointers.createLaserPointer({
joint: (_this.hand === RIGHT_HAND) ? "RightHand" : "LeftHand",
filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE,
faceAvatar: true,
centerEndY: false,
renderStates: teleportRenderStates
});
this.teleportRayHeadVisible = LaserPointers.createLaserPointer({
joint: "Avatar",
filter: RayPick.PICK_ENTITIES,
faceAvatar: true,
centerEndY: false,
renderStates: teleportRenderStates,
defaultRenderStates: teleportDefaultRenderStates
});
this.teleportRayHeadInvisible = LaserPointers.createLaserPointer({
joint: "Avatar",
filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE,
faceAvatar: true,
centerEndY: false,
renderStates: teleportRenderStates
});
this.teleporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random();
this.teleportMappingInternal = Controller.newMapping(this.teleporterMappingInternalName);
this.enableMappings = function() {
Controller.enableMapping(this.teleporterMappingInternalName);
};
this.disableMappings = function() {
Controller.disableMapping(teleporter.teleporterMappingInternalName);
};
this.cleanup = function() {
this.disableMappings();
LaserPointers.removeLaserPointer(this.teleportRayHandVisible);
LaserPointers.removeLaserPointer(this.teleportRayHandInvisible);
LaserPointers.removeLaserPointer(this.teleportRayHeadVisible);
LaserPointers.removeLaserPointer(this.teleportRayHeadInvisible);
};
this.buttonPress = function(value) {
_this.buttonValue = value;
}
this.parameters = makeDispatcherModuleParameters(
80,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
this.enterTeleport = function() {
if (coolInTimeout !== null) {
Script.clearTimeout(coolInTimeout);
}
this.state = TELEPORTER_STATES.COOL_IN;
coolInTimeout = Script.setTimeout(function() {
if (_this.state === TELEPORTER_STATES.COOL_IN) {
_this.state = TELEPORTER_STATES.TARGETTING;
}
}, COOL_IN_DURATION);
};
this.isReady = function(controllerData, deltaTime) {
var otherModule = this.getOtherModule();
if (_this.buttonValue !== 0 && !otherModule.active) {
this.active = true;
this.enterTeleport();
return makeRunningValues(true, [], []);
}
return makeRunningValues(false, [], []);
};
this.run = function(controllerData, deltaTime) {
//_this.state = TELEPORTER_STATES.TARGETTING;
// Get current hand pose information to see if the pose is valid
var pose = Controller.getPoseValue(handInfo[(_this.hand === RIGHT_HAND) ? 'right' : 'left'].controllerInput);
var mode = pose.valid ? _this.hand : 'head';
if (!pose.valid) {
LaserPointers.disableLaserPointer(_this.teleportRayHandVisible);
LaserPointers.disableLaserPointer(_this.teleportRayHandInvisible);
LaserPointers.enableLaserPointer(_this.teleportRayHeadVisible);
LaserPointers.enableLaserPointer(_this.teleportRayHeadInvisible);
} else {
LaserPointers.enableLaserPointer(_this.teleportRayHandVisible);
LaserPointers.enableLaserPointer(_this.teleportRayHandInvisible);
LaserPointers.disableLaserPointer(_this.teleportRayHeadVisible);
LaserPointers.disableLaserPointer(_this.teleportRayHeadInvisible);
}
// We do up to 2 ray picks to find a teleport location.
// There are 2 types of teleport locations we are interested in:
// 1. A visible floor. This can be any entity surface that points within some degree of "up"
// 2. A seat. The seat can be visible or invisible.
//
// * In the first pass we pick against visible and invisible entities so that we can find invisible seats.
// We might hit an invisible entity that is not a seat, so we need to do a second pass.
// * In the second pass we pick against visible entities only.
//
var result;
if (mode === 'head') {
result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadInvisible);
} else {
result = LaserPointers.getPrevRayPickResult(_this.teleportRayHandInvisible);
}
var teleportLocationType = getTeleportTargetType(result);
if (teleportLocationType === TARGET.INVISIBLE) {
if (mode === 'head') {
result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadVisible);
} else {
result = LaserPointers.getPrevRayPickResult(_this.teleportRayHandVisible);
}
teleportLocationType = getTeleportTargetType(result);
}
if (teleportLocationType === TARGET.NONE) {
// Use the cancel default state
this.setTeleportState(mode, "cancel", "");
} else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) {
this.setTeleportState(mode, "", "cancel");
} else if (teleportLocationType === TARGET.SURFACE) {
if (this.state === TELEPORTER_STATES.COOL_IN) {
this.setTeleportState(mode, "cancel", "");
} else {
this.setTeleportState(mode, "teleport", "");
}
} else if (teleportLocationType === TARGET.SEAT) {
this.setTeleportState(mode, "", "seat");
}
return this.teleport(result, teleportLocationType);
};
this.teleport = function(newResult, target) {
var result = newResult;
if (_this.buttonValue !== 0) {
return makeRunningValues(true, [], []);
}
if (target === TARGET.NONE || target === TARGET.INVALID || this.state === TELEPORTER_STATES.COOL_IN) {
// Do nothing
} else if (target === TARGET.SEAT) {
Entities.callEntityMethod(result.objectID, 'sit');
} else if (target === TARGET.SURFACE) {
var offset = getAvatarFootOffset();
result.intersection.y += offset;
MyAvatar.goToLocation(result.intersection, false, {x: 0, y: 0, z: 0, w: 1}, false);
HMD.centerUI();
MyAvatar.centerBody();
}
this.disableLasers();
this.active = false;
return makeRunningValues(false, [], []);
};
this.disableLasers = function() {
LaserPointers.disableLaserPointer(_this.teleportRayHandVisible);
LaserPointers.disableLaserPointer(_this.teleportRayHandInvisible);
LaserPointers.disableLaserPointer(_this.teleportRayHeadVisible);
LaserPointers.disableLaserPointer(_this.teleportRayHeadInvisible);
};
this.setTeleportState = function(mode, visibleState, invisibleState) {
if (mode === 'head') {
LaserPointers.setRenderState(_this.teleportRayHeadVisible, visibleState);
LaserPointers.setRenderState(_this.teleportRayHeadInvisible, invisibleState);
} else {
LaserPointers.setRenderState(_this.teleportRayHandVisible, visibleState);
LaserPointers.setRenderState(_this.teleportRayHandInvisible, invisibleState);
}
};
}
// related to repositioning the avatar after you teleport
var FOOT_JOINT_NAMES = ["RightToe_End", "RightToeBase", "RightFoot"];
var DEFAULT_ROOT_TO_FOOT_OFFSET = 0.5;
function getAvatarFootOffset() {
// find a valid foot jointIndex
var footJointIndex = -1;
var i, l = FOOT_JOINT_NAMES.length;
for (i = 0; i < l; i++) {
footJointIndex = MyAvatar.getJointIndex(FOOT_JOINT_NAMES[i]);
if (footJointIndex != -1) {
break;
}
}
if (footJointIndex != -1) {
// default vertical offset from foot to avatar root.
var footPos = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(footJointIndex);
if (footPos.x === 0 && footPos.y === 0 && footPos.z === 0.0) {
// if footPos is exactly zero, it's probably wrong because avatar is currently loading, fall back to default.
return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale;
} else {
return -footPos.y;
}
} else {
return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale;
}
}
var leftPad = new ThumbPad('left');
var rightPad = new ThumbPad('right');
var mappingName, teleportMapping;
var TELEPORT_DELAY = 0;
function isMoving() {
var LY = Controller.getValue(Controller.Standard.LY);
var LX = Controller.getValue(Controller.Standard.LX);
if (LY !== 0 || LX !== 0) {
return true;
} else {
return false;
}
}
function parseJSON(json) {
try {
return JSON.parse(json);
} catch (e) {
return undefined;
}
}
// When determininig whether you can teleport to a location, the normal of the
// point that is being intersected with is looked at. If this normal is more
// than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from <0, 1, 0> (straight up), then
// you can't teleport there.
var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70;
function getTeleportTargetType(result) {
if (result.type == RayPick.INTERSECTED_NONE) {
return TARGET.NONE;
}
var props = Entities.getEntityProperties(result.objectID, ['userData', 'visible']);
var data = parseJSON(props.userData);
if (data !== undefined && data.seat !== undefined) {
var avatarUuid = Uuid.fromString(data.seat.user);
if (Uuid.isNull(avatarUuid) || !AvatarList.getAvatar(avatarUuid)) {
return TARGET.SEAT;
} else {
return TARGET.INVALID;
}
}
if (!props.visible) {
return TARGET.INVISIBLE;
}
var surfaceNormal = result.surfaceNormal;
var adj = Math.sqrt(surfaceNormal.x * surfaceNormal.x + surfaceNormal.z * surfaceNormal.z);
var angleUp = Math.atan2(surfaceNormal.y, adj) * (180 / Math.PI);
if (angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
Vec3.distance(MyAvatar.position, result.intersection) <= TELEPORT_CANCEL_RANGE) {
return TARGET.INVALID;
} else {
return TARGET.SURFACE;
}
}
function registerMappings() {
mappingName = 'Hifi-Teleporter-Dev-' + Math.random();
teleportMapping = Controller.newMapping(mappingName);
teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightTeleporter.buttonPress);
teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftTeleporter.buttonPress);
}
var leftTeleporter = new Teleporter(LEFT_HAND);
var rightTeleporter = new Teleporter(RIGHT_HAND);
enableDispatcherModule("LeftTeleporter", leftTeleporter);
enableDispatcherModule("RightTeleporter", rightTeleporter);
registerMappings();
Controller.enableMapping(mappingName);
function cleanup() {
teleportMapping.disable();
disableDispatcherModule("LeftTeleporter");
disableDispatcherModule("RightTeleporter");
}
Script.scriptEnding.connect(cleanup);
var setIgnoreEntities = function() {
LaserPointers.setIgnoreEntities(teleporter.teleportRayRightVisible, ignoredEntities);
LaserPointers.setIgnoreEntities(teleporter.teleportRayRightInvisible, ignoredEntities);
LaserPointers.setIgnoreEntities(teleporter.teleportRayLeftVisible, ignoredEntities);
LaserPointers.setIgnoreEntities(teleporter.teleportRayLeftInvisible, ignoredEntities);
LaserPointers.setIgnoreEntities(teleporter.teleportRayHeadVisible, ignoredEntities);
LaserPointers.setIgnoreEntities(teleporter.teleportRayHeadInvisible, ignoredEntities);
}
var isDisabled = false;
var handleTeleportMessages = function(channel, message, sender) {
if (sender === MyAvatar.sessionUUID) {
if (channel === 'Hifi-Teleport-Disabler') {
if (message === 'both') {
isDisabled = 'both';
}
if (message === 'left') {
isDisabled = 'left';
}
if (message === 'right') {
isDisabled = 'right';
}
if (message === 'none') {
isDisabled = false;
}
} else if (channel === 'Hifi-Teleport-Ignore-Add' && !Uuid.isNull(message) && ignoredEntities.indexOf(message) === -1) {
ignoredEntities.push(message);
setIgnoreEntities();
} else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) {
var removeIndex = ignoredEntities.indexOf(message);
if (removeIndex > -1) {
ignoredEntities.splice(removeIndex, 1);
setIgnoreEntities();
}
}
}
};
Messages.subscribe('Hifi-Teleport-Disabler');
Messages.subscribe('Hifi-Teleport-Ignore-Add');
Messages.subscribe('Hifi-Teleport-Ignore-Remove');
Messages.messageReceived.connect(handleTeleportMessages);
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,484 @@
"use strict";
// webEntityLaserInput.js
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* jslint bitwise: true */
/* global Script, Controller, LaserPointers, RayPick, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat,
getGrabPointSphereOffset, getEnabledModuleByName, makeRunningValues, Entities, NULL_UUID,
enableDispatcherModule, disableDispatcherModule, entityIsDistanceGrabbable,
makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION,
PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD,
AVATAR_SELF_ID, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
var halfPath = {
type: "line3d",
color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var halfEnd = {
type: "sphere",
solid: true,
color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
alpha: 0.9,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
};
var fullPath = {
type: "line3d",
color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var fullEnd = {
type: "sphere",
solid: true,
color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
alpha: 0.9,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
};
var holdPath = {
type: "line3d",
color: COLORS_GRAB_DISTANCE_HOLD,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
lineWidth: 5,
ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it.
parentID: AVATAR_SELF_ID
};
var renderStates = [
{name: "half", path: halfPath, end: halfEnd},
{name: "full", path: fullPath, end: fullEnd},
{name: "hold", path: holdPath}
];
var defaultRenderStates = [
{name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath},
{name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath},
{name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath}
];
// triggered when stylus presses a web overlay/entity
var HAPTIC_STYLUS_STRENGTH = 1.0;
var HAPTIC_STYLUS_DURATION = 20.0;
function laserTargetHasKeyboardFocus(laserTarget) {
if (laserTarget && laserTarget !== NULL_UUID) {
return Entities.keyboardFocusOverlay === laserTarget;
}
}
function setKeyboardFocusOnLaserTarget(laserTarget) {
if (laserTarget && laserTarget !== NULL_UUID) {
Entities.wantsHandControllerPointerEvents(laserTarget);
Overlays.keyboardFocusOverlay = NULL_UUID;
Entities.keyboardFocusEntity = laserTarget;
}
}
function sendHoverEnterEventToLaserTarget(hand, laserTarget) {
if (!laserTarget) {
return;
}
var pointerEvent = {
type: "Move",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: laserTarget.position2D,
pos3D: laserTarget.position,
normal: laserTarget.normal,
direction: Vec3.subtract(ZERO_VEC, laserTarget.normal),
button: "None"
};
if (laserTarget.entityID && laserTarget.entityID !== NULL_UUID) {
Entities.sendHoverEnterEntity(laserTarget.entityID, pointerEvent);
}
}
function sendHoverOverEventToLaserTarget(hand, laserTarget) {
if (!laserTarget) {
return;
}
var pointerEvent = {
type: "Move",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: laserTarget.position2D,
pos3D: laserTarget.position,
normal: laserTarget.normal,
direction: Vec3.subtract(ZERO_VEC, laserTarget.normal),
button: "None"
};
if (laserTarget.entityID && laserTarget.entityID !== NULL_UUID) {
Entities.sendMouseMoveOnEntity(laserTarget.entityID, pointerEvent);
Entities.sendHoverOverEntity(laserTarget.entityID, pointerEvent);
}
}
function sendTouchStartEventToLaserTarget(hand, laserTarget) {
var pointerEvent = {
type: "Press",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: laserTarget.position2D,
pos3D: laserTarget.position,
normal: laserTarget.normal,
direction: Vec3.subtract(ZERO_VEC, laserTarget.normal),
button: "Primary",
isPrimaryHeld: true
};
if (laserTarget.entityID && laserTarget.entityID !== NULL_UUID) {
Entities.sendMousePressOnEntity(laserTarget.entityID, pointerEvent);
Entities.sendClickDownOnEntity(laserTarget.entityID, pointerEvent);
}
}
function sendTouchEndEventToLaserTarget(hand, laserTarget) {
var pointerEvent = {
type: "Release",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: laserTarget.position2D,
pos3D: laserTarget.position,
normal: laserTarget.normal,
direction: Vec3.subtract(ZERO_VEC, laserTarget.normal),
button: "Primary"
};
if (laserTarget.entityID && laserTarget.entityID !== NULL_UUID) {
Entities.sendMouseReleaseOnEntity(laserTarget.entityID, pointerEvent);
Entities.sendClickReleaseOnEntity(laserTarget.entityID, pointerEvent);
Entities.sendHoverLeaveEntity(laserTarget.entityID, pointerEvent);
}
}
function sendTouchMoveEventToLaserTarget(hand, laserTarget) {
var pointerEvent = {
type: "Move",
id: hand + 1, // 0 is reserved for hardware mouse
pos2D: laserTarget.position2D,
pos3D: laserTarget.position,
normal: laserTarget.normal,
direction: Vec3.subtract(ZERO_VEC, laserTarget.normal),
button: "Primary",
isPrimaryHeld: true
};
if (laserTarget.entityID && laserTarget.entityID !== NULL_UUID) {
Entities.sendMouseMoveOnEntity(laserTarget.entityID, pointerEvent);
Entities.sendHoldingClickOnEntity(laserTarget.entityID, pointerEvent);
}
}
function calculateTargetFromEntity(intersection, props) {
if (props.rotation === undefined) {
// if rotation is missing from props object, then this entity has probably been deleted.
return null;
}
// project stylus tip onto entity plane.
var normal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1});
Vec3.multiplyQbyV(props.rotation, {x: 0, y: 1, z: 0});
var distance = Vec3.dot(Vec3.subtract(intersection, props.position), normal);
var position = Vec3.subtract(intersection, Vec3.multiply(normal, distance));
// generate normalized coordinates
var invRot = Quat.inverse(props.rotation);
var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, props.position));
var invDimensions = { x: 1 / props.dimensions.x, y: 1 / props.dimensions.y, z: 1 / props.dimensions.z };
var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint);
// 2D position on entity plane in meters, relative to the bounding box upper-left hand corner.
var position2D = {
x: normalizedPosition.x * props.dimensions.x,
y: (1 - normalizedPosition.y) * props.dimensions.y // flip y-axis
};
return {
entityID: props.id,
entityProps: props,
overlayID: null,
distance: distance,
position: position,
position2D: position2D,
normal: normal,
normalizedPosition: normalizedPosition,
dimensions: props.dimensions,
valid: true
};
}
function distance2D(a, b) {
var dx = (a.x - b.x);
var dy = (a.y - b.y);
return Math.sqrt(dx * dx + dy * dy);
}
function WebEntityLaserInput(hand) {
this.hand = hand;
this.active = false;
this.previousLaserClickedTarget = false;
this.laserPressingTarget = false;
this.hover = false;
this.mode = "none";
this.pressEnterLaserTarget = null;
this.laserTarget = null;
this.laserTargetID = null;
this.lastValidTargetID = null;
this.handToController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
};
this.getOtherModule = function() {
return (this.hand === RIGHT_HAND) ? leftWebEntityLaserInput : rightWebEntityLaserInput;
};
this.parameters = makeDispatcherModuleParameters(
550,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
this.requestTouchFocus = function(laserTarget) {
if (laserTarget !== null || laserTarget !== undefined) {
sendHoverEnterEventToLaserTarget(this.hand, this.laserTarget);
this.lastValidTargetID = laserTarget;
}
};
this.relinquishTouchFocus = function() {
// send hover leave event.
var pointerEvent = { type: "Move", id: this.hand + 1 };
Entities.sendMouseMoveOnEntity(this.lastValidTargetID, pointerEvent);
Entities.sendHoverOverEntity(this.lastValidTargetID, pointerEvent);
Entities.sendHoverLeaveEntity(this.lastValidID, pointerEvent);
};
this.updateLaserTargets = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
this.laserTargetID = intersection.objectID;
var props = Entities.getEntityProperties(intersection.objectID);
this.laserTarget = calculateTargetFromEntity(intersection.intersection, props);
};
this.processControllerTriggers = function(controllerData) {
if (controllerData.triggerClicks[this.hand]) {
this.mode = "full";
this.laserPressingTarget = true;
this.hover = false;
} else if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
this.mode = "half";
this.laserPressingTarget = false;
this.hover = true;
this.requestTouchFocus(this.laserTargetID);
} else {
this.mode = "none";
this.laserPressingTarget = false;
this.hover = false;
this.relinquishTouchFocus();
}
};
this.hovering = function() {
if (!laserTargetHasKeyboardFocus(this.laserTagetID)) {
setKeyboardFocusOnLaserTarget(this.laserTargetID);
}
sendHoverOverEventToLaserTarget(this.hand, this.laserTarget);
};
this.laserPressEnter = function () {
sendTouchStartEventToLaserTarget(this.hand, this.laserTarget);
Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand);
this.touchingEnterTimer = 0;
this.pressEnterLaserTarget = this.laserTarget;
this.deadspotExpired = false;
var LASER_PRESS_TO_MOVE_DEADSPOT = 0.026;
this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT) * this.laserTarget.distance;
};
this.laserPressExit = function () {
if (this.laserTarget === null) {
return;
}
// send press event
if (this.deadspotExpired) {
sendTouchEndEventToLaserTarget(this.hand, this.laserTarget);
} else {
sendTouchEndEventToLaserTarget(this.hand, this.pressEnterLaserTarget);
}
};
this.laserPressing = function (controllerData, dt) {
this.touchingEnterTimer += dt;
if (this.laserTarget) {
var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds
if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY ||
distance2D(this.laserTarget.position2D,
this.pressEnterLaserTarget.position2D) > this.deadspotRadius) {
sendTouchMoveEventToLaserTarget(this.hand, this.laserTarget);
this.deadspotExpired = true;
}
} else {
this.laserPressingTarget = false;
}
};
this.releaseTouchEvent = function() {
if (this.pressEnterLaserTarget === null) {
return;
}
sendTouchEndEventToLaserTarget(this.hand, this.pressEnterLaserTarget);
};
this.updateLaserPointer = function(controllerData) {
var RADIUS = 0.005;
var dim = { x: RADIUS, y: RADIUS, z: RADIUS };
if (this.mode === "full") {
fullEnd.dimensions = dim;
LaserPointers.editRenderState(this.laserPointer, this.mode, {path: fullPath, end: fullEnd});
} else if (this.mode === "half") {
halfEnd.dimensions = dim;
LaserPointers.editRenderState(this.laserPointer, this.mode, {path: halfPath, end: halfEnd});
}
LaserPointers.enableLaserPointer(this.laserPointer);
LaserPointers.setRenderState(this.laserPointer, this.mode);
};
this.isPointingAtWebEntity = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
var entityProperty = Entities.getEntityProperties(intersection.objectID);
var entityType = entityProperty.type;
if ((intersection.type === RayPick.INTERSECTED_ENTITY && entityType === "Web")) {
return true;
}
return false;
};
this.exitModule = function() {
this.releaseTouchEvent();
this.relinquishTouchFocus();
this.reset();
this.updateLaserPointer();
LaserPointers.disableLaserPointer(this.laserPointer);
};
this.reset = function() {
this.pressEnterLaserTarget = null;
this.laserTarget = null;
this.laserTargetID = null;
this.laserPressingTarget = false;
this.previousLaserClickedTarget = null;
this.mode = "none";
this.active = false;
};
this.isReady = function(controllerData) {
var otherModule = this.getOtherModule();
if (this.isPointingAtWebEntity(controllerData) && !otherModule.active) {
return makeRunningValues(true, [], []);
}
return makeRunningValues(false, [], []);
};
this.run = function(controllerData, deltaTime) {
if (!this.isPointingAtWebEntity(controllerData)) {
this.exitModule();
return makeRunningValues(false, [], []);
}
this.updateLaserTargets(controllerData);
this.processControllerTriggers(controllerData);
this.updateLaserPointer(controllerData);
if (!this.previousLaserClickedTarget && this.laserPressingTarget) {
this.laserPressEnter();
}
if (this.previousLaserClickedTarget && !this.laserPressingTarget) {
this.laserPressExit();
}
this.previousLaserClickedTarget = this.laserPressingTarget;
if (this.laserPressingTarget) {
this.laserPressing(controllerData, deltaTime);
}
if (this.hover) {
this.hovering();
}
return makeRunningValues(true, [], []);
};
this.cleanup = function() {
LaserPointers.disableLaserPointer(this.laserPointer);
LaserPointers.removeLaserPointer(this.laserPointer);
};
this.laserPointer = LaserPointers.createLaserPointer({
joint: (this.hand === RIGHT_HAND) ? "_CONTROLLER_RIGHTHAND" : "_CONTROLLER_LEFTHAND",
filter: RayPick.PICK_ENTITIES,
maxDistance: PICK_MAX_DISTANCE,
posOffset: getGrabPointSphereOffset(this.handToController()),
renderStates: renderStates,
faceAvatar: true,
defaultRenderStates: defaultRenderStates
});
}
var leftWebEntityLaserInput = new WebEntityLaserInput(LEFT_HAND);
var rightWebEntityLaserInput = new WebEntityLaserInput(RIGHT_HAND);
enableDispatcherModule("LeftWebEntityLaserInput", leftWebEntityLaserInput);
enableDispatcherModule("RightWebEntityLaserInput", rightWebEntityLaserInput);
this.cleanup = function() {
leftWebEntityLaserInput.cleanup();
rightWebEntityLaserInput.cleanup();
disableDispatcherModule("LeftWebEntityLaserInput");
disableDispatcherModule("RightWebEntityLaserInput");
};
Script.scriptEnding.connect(this.cleanup);
}());

View file

@ -12,11 +12,24 @@
var CONTOLLER_SCRIPTS = [
"squeezeHands.js",
"controllerDisplayManager.js",
"handControllerGrab.js",
"handControllerPointer.js",
"grab.js",
"teleport.js",
"toggleAdvancedMovementForHandControllers.js",
"controllerDispatcher.js",
"controllerModules/nearParentGrabEntity.js",
"controllerModules/nearParentGrabOverlay.js",
"controllerModules/nearActionGrabEntity.js",
"controllerModules/farActionGrabEntity.js",
"controllerModules/tabletStylusInput.js",
"controllerModules/equipEntity.js",
"controllerModules/nearTrigger.js",
"controllerModules/overlayLaserInput.js",
"controllerModules/webEntityLaserInput.js",
"controllerModules/inEditMode.js",
"controllerModules/disableOtherModule.js",
"controllerModules/farTrigger.js",
"controllerModules/teleport.js",
"controllerModules/scaleAvatar.js"
];
var DEBUG_MENU_ITEM = "Debug defaultScripts.js";

File diff suppressed because it is too large Load diff

View file

@ -1,575 +0,0 @@
"use strict";
// Created by james b. pollack @imgntn on 7/2/2016
// Copyright 2016 High Fidelity, Inc.
//
// Creates a beam and target and then teleports you there. Release when its close to you to cancel.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
(function() { // BEGIN LOCAL_SCOPE
var inTeleportMode = false;
var SMOOTH_ARRIVAL_SPACING = 33;
var NUMBER_OF_STEPS = 6;
var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport-destination.fbx");
var TOO_CLOSE_MODEL_URL = Script.resolvePath("../assets/models/teleport-cancel.fbx");
var SEAT_MODEL_URL = Script.resolvePath("../assets/models/teleport-seat.fbx");
var TARGET_MODEL_DIMENSIONS = {
x: 1.15,
y: 0.5,
z: 1.15
};
var COLORS_TELEPORT_SEAT = {
red: 255,
green: 0,
blue: 170
};
var COLORS_TELEPORT_CAN_TELEPORT = {
red: 97,
green: 247,
blue: 255
};
var COLORS_TELEPORT_CANCEL = {
red: 255,
green: 184,
blue: 73
};
var TELEPORT_CANCEL_RANGE = 1;
var COOL_IN_DURATION = 500;
var handInfo = {
right: {
controllerInput: Controller.Standard.RightHand
},
left: {
controllerInput: Controller.Standard.LeftHand
}
};
var cancelPath = {
type: "line3d",
color: COLORS_TELEPORT_CANCEL,
ignoreRayIntersection: true,
alpha: 1,
solid: true,
drawInFront: true,
glow: 1.0
};
var teleportPath = {
type: "line3d",
color: COLORS_TELEPORT_CAN_TELEPORT,
ignoreRayIntersection: true,
alpha: 1,
solid: true,
drawInFront: true,
glow: 1.0
};
var seatPath = {
type: "line3d",
color: COLORS_TELEPORT_SEAT,
ignoreRayIntersection: true,
alpha: 1,
solid: true,
drawInFront: true,
glow: 1.0
};
var cancelEnd = {
type: "model",
url: TOO_CLOSE_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
ignoreRayIntersection: true
};
var teleportEnd = {
type: "model",
url: TARGET_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
ignoreRayIntersection: true
};
var seatEnd = {
type: "model",
url: SEAT_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
ignoreRayIntersection: true
}
var teleportRenderStates = [{name: "cancel", path: cancelPath, end: cancelEnd},
{name: "teleport", path: teleportPath, end: teleportEnd},
{name: "seat", path: seatPath, end: seatEnd}];
var DEFAULT_DISTANCE = 50;
var teleportDefaultRenderStates = [{name: "cancel", distance: DEFAULT_DISTANCE, path: cancelPath}];
function ThumbPad(hand) {
this.hand = hand;
var _thisPad = this;
this.buttonPress = function(value) {
_thisPad.buttonValue = value;
};
}
function Trigger(hand) {
this.hand = hand;
var _this = this;
this.buttonPress = function(value) {
_this.buttonValue = value;
};
this.down = function() {
var down = _this.buttonValue === 1 ? 1.0 : 0.0;
return down;
};
}
var coolInTimeout = null;
var ignoredEntities = [];
var TELEPORTER_STATES = {
IDLE: 'idle',
COOL_IN: 'cool_in',
TARGETTING: 'targetting',
TARGETTING_INVALID: 'targetting_invalid',
};
var TARGET = {
NONE: 'none', // Not currently targetting anything
INVISIBLE: 'invisible', // The current target is an invvsible surface
INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.)
SURFACE: 'surface', // The current target is a valid surface
SEAT: 'seat', // The current target is a seat
};
function Teleporter() {
var _this = this;
this.active = false;
this.state = TELEPORTER_STATES.IDLE;
this.currentTarget = TARGET.INVALID;
this.teleportRayLeftVisible = LaserPointers.createLaserPointer({
joint: "LeftHand",
filter: RayPick.PICK_ENTITIES,
faceAvatar: true,
centerEndY: false,
renderStates: teleportRenderStates,
defaultRenderStates: teleportDefaultRenderStates
});
this.teleportRayLeftInvisible = LaserPointers.createLaserPointer({
joint: "LeftHand",
filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE,
faceAvatar: true,
centerEndY: false,
renderStates: teleportRenderStates
});
this.teleportRayRightVisible = LaserPointers.createLaserPointer({
joint: "RightHand",
filter: RayPick.PICK_ENTITIES,
faceAvatar: true,
centerEndY: false,
renderStates: teleportRenderStates,
defaultRenderStates: teleportDefaultRenderStates
});
this.teleportRayRightInvisible = LaserPointers.createLaserPointer({
joint: "RightHand",
filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE,
faceAvatar: true,
centerEndY: false,
renderStates: teleportRenderStates
});
this.teleportRayHeadVisible = LaserPointers.createLaserPointer({
joint: "Avatar",
filter: RayPick.PICK_ENTITIES,
faceAvatar: true,
centerEndY: false,
renderStates: teleportRenderStates,
defaultRenderStates: teleportDefaultRenderStates
});
this.teleportRayHeadInvisible = LaserPointers.createLaserPointer({
joint: "Avatar",
filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE,
faceAvatar: true,
centerEndY: false,
renderStates: teleportRenderStates
});
this.updateConnected = null;
this.activeHand = null;
this.teleporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random();
this.teleportMappingInternal = Controller.newMapping(this.teleporterMappingInternalName);
this.enableMappings = function() {
Controller.enableMapping(this.teleporterMappingInternalName);
};
this.disableMappings = function() {
Controller.disableMapping(teleporter.teleporterMappingInternalName);
};
this.cleanup = function() {
this.disableMappings();
LaserPointers.removeLaserPointer(this.teleportRayLeftVisible);
LaserPointers.removeLaserPointer(this.teleportRayLeftInvisible);
LaserPointers.removeLaserPointer(this.teleportRayRightVisible);
LaserPointers.removeLaserPointer(this.teleportRayRightInvisible);
LaserPointers.removeLaserPointer(this.teleportRayHeadVisible);
LaserPointers.removeLaserPointer(this.teleportRayHeadInvisible);
if (this.updateConnected === true) {
Script.update.disconnect(this, this.update);
}
};
this.enterTeleportMode = function(hand) {
if (inTeleportMode === true) {
return;
}
if (isDisabled === 'both' || isDisabled === hand) {
return;
}
inTeleportMode = true;
if (coolInTimeout !== null) {
Script.clearTimeout(coolInTimeout);
}
this.state = TELEPORTER_STATES.COOL_IN;
coolInTimeout = Script.setTimeout(function() {
if (_this.state === TELEPORTER_STATES.COOL_IN) {
_this.state = TELEPORTER_STATES.TARGETTING;
}
}, COOL_IN_DURATION);
this.activeHand = hand;
this.enableMappings();
Script.update.connect(this, this.update);
this.updateConnected = true;
};
this.exitTeleportMode = function(value) {
if (this.updateConnected === true) {
Script.update.disconnect(this, this.update);
}
this.disableMappings();
LaserPointers.disableLaserPointer(this.teleportRayLeftVisible);
LaserPointers.disableLaserPointer(this.teleportRayLeftInvisible);
LaserPointers.disableLaserPointer(this.teleportRayRightVisible);
LaserPointers.disableLaserPointer(this.teleportRayRightInvisible);
LaserPointers.disableLaserPointer(this.teleportRayHeadVisible);
LaserPointers.disableLaserPointer(this.teleportRayHeadInvisible);
this.updateConnected = null;
this.state = TELEPORTER_STATES.IDLE;
inTeleportMode = false;
};
this.update = function() {
if (_this.state === TELEPORTER_STATES.IDLE) {
return;
}
// Get current hand pose information to see if the pose is valid
var pose = Controller.getPoseValue(handInfo[_this.activeHand].controllerInput);
var mode = pose.valid ? _this.activeHand : 'head';
if (!pose.valid) {
if (mode === 'right') {
LaserPointers.disableLaserPointer(_this.teleportRayRightVisible);
LaserPointers.disableLaserPointer(_this.teleportRayRightInvisible);
} else {
LaserPointers.disableLaserPointer(_this.teleportRayLeftVisible);
LaserPointers.disableLaserPointer(_this.teleportRayLeftInvisible);
}
LaserPointers.enableLaserPointer(_this.teleportRayHeadVisible);
LaserPointers.enableLaserPointer(_this.teleportRayHeadInvisible);
} else {
if (mode === 'right') {
LaserPointers.enableLaserPointer(_this.teleportRayRightVisible);
LaserPointers.enableLaserPointer(_this.teleportRayRightInvisible);
} else {
LaserPointers.enableLaserPointer(_this.teleportRayLeftVisible);
LaserPointers.enableLaserPointer(_this.teleportRayLeftInvisible);
}
LaserPointers.disableLaserPointer(_this.teleportRayHeadVisible);
LaserPointers.disableLaserPointer(_this.teleportRayHeadInvisible);
}
// We do up to 2 ray picks to find a teleport location.
// There are 2 types of teleport locations we are interested in:
// 1. A visible floor. This can be any entity surface that points within some degree of "up"
// 2. A seat. The seat can be visible or invisible.
//
// * In the first pass we pick against visible and invisible entities so that we can find invisible seats.
// We might hit an invisible entity that is not a seat, so we need to do a second pass.
// * In the second pass we pick against visible entities only.
//
var result;
if (mode === 'right') {
result = LaserPointers.getPrevRayPickResult(_this.teleportRayRightInvisible);
} else if (mode === 'left') {
result = LaserPointers.getPrevRayPickResult(_this.teleportRayLeftInvisible);
} else {
result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadInvisible);
}
var teleportLocationType = getTeleportTargetType(result);
if (teleportLocationType === TARGET.INVISIBLE) {
if (mode === 'right') {
result = LaserPointers.getPrevRayPickResult(_this.teleportRayRightVisible);
} else if (mode === 'left') {
result = LaserPointers.getPrevRayPickResult(_this.teleportRayLeftVisible);
} else {
result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadVisible);
}
teleportLocationType = getTeleportTargetType(result);
}
if (teleportLocationType === TARGET.NONE) {
// Use the cancel default state
this.setTeleportState(mode, "cancel", "");
} else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) {
this.setTeleportState(mode, "", "cancel");
} else if (teleportLocationType === TARGET.SURFACE) {
if (this.state === TELEPORTER_STATES.COOL_IN) {
this.setTeleportState(mode, "cancel", "");
} else {
this.setTeleportState(mode, "teleport", "");
}
} else if (teleportLocationType === TARGET.SEAT) {
this.setTeleportState(mode, "", "seat");
}
if (((_this.activeHand === 'left' ? leftPad : rightPad).buttonValue === 0) && inTeleportMode === true) {
// remember the state before we exit teleport mode and set it back to IDLE
var previousState = this.state;
this.exitTeleportMode();
if (teleportLocationType === TARGET.NONE || teleportLocationType === TARGET.INVALID || previousState === TELEPORTER_STATES.COOL_IN) {
// Do nothing
} else if (teleportLocationType === TARGET.SEAT) {
Entities.callEntityMethod(result.objectID, 'sit');
} else if (teleportLocationType === TARGET.SURFACE) {
var offset = getAvatarFootOffset();
result.intersection.y += offset;
MyAvatar.goToLocation(result.intersection, false, {x: 0, y: 0, z: 0, w: 1}, false);
HMD.centerUI();
MyAvatar.centerBody();
}
}
};
this.setTeleportState = function(mode, visibleState, invisibleState) {
if (mode === 'right') {
LaserPointers.setRenderState(_this.teleportRayRightVisible, visibleState);
LaserPointers.setRenderState(_this.teleportRayRightInvisible, invisibleState);
} else if (mode === 'left') {
LaserPointers.setRenderState(_this.teleportRayLeftVisible, visibleState);
LaserPointers.setRenderState(_this.teleportRayLeftInvisible, invisibleState);
} else {
LaserPointers.setRenderState(_this.teleportRayHeadVisible, visibleState);
LaserPointers.setRenderState(_this.teleportRayHeadInvisible, invisibleState);
}
};
}
// related to repositioning the avatar after you teleport
var FOOT_JOINT_NAMES = ["RightToe_End", "RightToeBase", "RightFoot"];
var DEFAULT_ROOT_TO_FOOT_OFFSET = 0.5;
function getAvatarFootOffset() {
// find a valid foot jointIndex
var footJointIndex = -1;
var i, l = FOOT_JOINT_NAMES.length;
for (i = 0; i < l; i++) {
footJointIndex = MyAvatar.getJointIndex(FOOT_JOINT_NAMES[i]);
if (footJointIndex != -1) {
break;
}
}
if (footJointIndex != -1) {
// default vertical offset from foot to avatar root.
var footPos = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(footJointIndex);
if (footPos.x === 0 && footPos.y === 0 && footPos.z === 0.0) {
// if footPos is exactly zero, it's probably wrong because avatar is currently loading, fall back to default.
return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale;
} else {
return -footPos.y;
}
} else {
return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale;
}
}
var leftPad = new ThumbPad('left');
var rightPad = new ThumbPad('right');
var leftTrigger = new Trigger('left');
var rightTrigger = new Trigger('right');
var mappingName, teleportMapping;
var TELEPORT_DELAY = 0;
function isMoving() {
var LY = Controller.getValue(Controller.Standard.LY);
var LX = Controller.getValue(Controller.Standard.LX);
if (LY !== 0 || LX !== 0) {
return true;
} else {
return false;
}
}
function parseJSON(json) {
try {
return JSON.parse(json);
} catch (e) {
return undefined;
}
}
// When determininig whether you can teleport to a location, the normal of the
// point that is being intersected with is looked at. If this normal is more
// than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from <0, 1, 0> (straight up), then
// you can't teleport there.
var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70;
function getTeleportTargetType(result) {
if (result.type == RayPick.INTERSECTED_NONE) {
return TARGET.NONE;
}
var props = Entities.getEntityProperties(result.objectID, ['userData', 'visible']);
var data = parseJSON(props.userData);
if (data !== undefined && data.seat !== undefined) {
var avatarUuid = Uuid.fromString(data.seat.user);
if (Uuid.isNull(avatarUuid) || !AvatarList.getAvatar(avatarUuid)) {
return TARGET.SEAT;
} else {
return TARGET.INVALID;
}
}
if (!props.visible) {
return TARGET.INVISIBLE;
}
var surfaceNormal = result.surfaceNormal;
var adj = Math.sqrt(surfaceNormal.x * surfaceNormal.x + surfaceNormal.z * surfaceNormal.z);
var angleUp = Math.atan2(surfaceNormal.y, adj) * (180 / Math.PI);
if (angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
Vec3.distance(MyAvatar.position, result.intersection) <= TELEPORT_CANCEL_RANGE) {
return TARGET.INVALID;
} else {
return TARGET.SURFACE;
}
}
function registerMappings() {
mappingName = 'Hifi-Teleporter-Dev-' + Math.random();
teleportMapping = Controller.newMapping(mappingName);
teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress);
teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress);
teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress);
teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress);
teleportMapping.from(Controller.Standard.LeftPrimaryThumb)
.to(function(value) {
if (isDisabled === 'left' || isDisabled === 'both') {
return;
}
if (leftTrigger.down()) {
return;
}
if (isMoving() === true) {
return;
}
teleporter.enterTeleportMode('left');
return;
});
teleportMapping.from(Controller.Standard.RightPrimaryThumb)
.to(function(value) {
if (isDisabled === 'right' || isDisabled === 'both') {
return;
}
if (rightTrigger.down()) {
return;
}
if (isMoving() === true) {
return;
}
teleporter.enterTeleportMode('right');
return;
});
}
registerMappings();
var teleporter = new Teleporter();
Controller.enableMapping(mappingName);
function cleanup() {
teleportMapping.disable();
teleporter.cleanup();
}
Script.scriptEnding.connect(cleanup);
var setIgnoreEntities = function() {
LaserPointers.setIgnoreEntities(teleporter.teleportRayRightVisible, ignoredEntities);
LaserPointers.setIgnoreEntities(teleporter.teleportRayRightInvisible, ignoredEntities);
LaserPointers.setIgnoreEntities(teleporter.teleportRayLeftVisible, ignoredEntities);
LaserPointers.setIgnoreEntities(teleporter.teleportRayLeftInvisible, ignoredEntities);
LaserPointers.setIgnoreEntities(teleporter.teleportRayHeadVisible, ignoredEntities);
LaserPointers.setIgnoreEntities(teleporter.teleportRayHeadInvisible, ignoredEntities);
}
var isDisabled = false;
var handleTeleportMessages = function(channel, message, sender) {
if (sender === MyAvatar.sessionUUID) {
if (channel === 'Hifi-Teleport-Disabler') {
if (message === 'both') {
isDisabled = 'both';
}
if (message === 'left') {
isDisabled = 'left';
}
if (message === 'right') {
isDisabled = 'right';
}
if (message === 'none') {
isDisabled = false;
}
} else if (channel === 'Hifi-Teleport-Ignore-Add' && !Uuid.isNull(message) && ignoredEntities.indexOf(message) === -1) {
ignoredEntities.push(message);
setIgnoreEntities();
} else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) {
var removeIndex = ignoredEntities.indexOf(message);
if (removeIndex > -1) {
ignoredEntities.splice(removeIndex, 1);
setIgnoreEntities();
}
}
}
};
Messages.subscribe('Hifi-Teleport-Disabler');
Messages.subscribe('Hifi-Teleport-Ignore-Add');
Messages.subscribe('Hifi-Teleport-Ignore-Remove');
Messages.messageReceived.connect(handleTeleportMessages);
}()); // END LOCAL_SCOPE

View file

@ -1,4 +1,4 @@
<!--
<!--
// entityProperties.html
//
// Created by Ryan Huffman on 13 Nov 2014
@ -136,17 +136,21 @@
<fieldset class="column" id="group-cloneable-group" style="display:none;">
<legend class="sub-section-header">
<span>Cloneable Settings</span>
<span>Cloneable Settings</span>
</legend>
<fieldset class="minor">
<div><label>Clone Lifetime</label><input type="number" data-user-data-type="cloneLifetime" id="property-cloneable-lifetime"></div>
<div><label>Clone Lifetime</label><input type="number" data-user-data-type="cloneLifetime" id="property-cloneable-lifetime"></div>
</fieldset>
<fieldset class="minor">
<div><label>Clone Limit </label><input type="number" data-user-data-type="cloneLimit" id="property-cloneable-limit"></div>
<div><label>Clone Limit</label><input type="number" data-user-data-type="cloneLimit" id="property-cloneable-limit"></div>
</fieldset>
<div class="property checkbox">
<input type="checkbox" id="property-cloneable-dynamic">
<label for="property-cloneable-dynamic">Clone Dynamic</label>
<input type="checkbox" id="property-cloneable-dynamic">
<label for="property-cloneable-dynamic">Clone Dynamic</label>
</div>
<div class="property checkbox">
<input type="checkbox" id="property-cloneable-avatarEntity">
<label for="property-cloneable-avatarEntity">Clone Avatar Entity</label>
</div>
</fieldset>
</div>

View file

@ -554,6 +554,7 @@ function loaded() {
var elCloneable = document.getElementById("property-cloneable");
var elCloneableDynamic = document.getElementById("property-cloneable-dynamic");
var elCloneableAvatarEntity = document.getElementById("property-cloneable-avatarEntity");
var elCloneableGroup = document.getElementById("group-cloneable-group");
var elCloneableLifetime = document.getElementById("property-cloneable-lifetime");
var elCloneableLimit = document.getElementById("property-cloneable-limit");
@ -844,25 +845,29 @@ function loaded() {
parsedUserData = JSON.parse(properties.userData);
if ("grabbableKey" in parsedUserData) {
if ("grabbable" in parsedUserData["grabbableKey"]) {
elGrabbable.checked = parsedUserData["grabbableKey"].grabbable;
var grabbableData = parsedUserData["grabbableKey"];
if ("grabbable" in grabbableData) {
elGrabbable.checked = grabbableData.grabbable;
}
if ("wantsTrigger" in parsedUserData["grabbableKey"]) {
elWantsTrigger.checked = parsedUserData["grabbableKey"].wantsTrigger;
if ("wantsTrigger" in grabbableData) {
elWantsTrigger.checked = grabbableData.wantsTrigger;
}
if ("ignoreIK" in parsedUserData["grabbableKey"]) {
elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK;
if ("ignoreIK" in grabbableData) {
elIgnoreIK.checked = grabbableData.ignoreIK;
}
if ("cloneable" in parsedUserData["grabbableKey"]) {
elCloneable.checked = parsedUserData["grabbableKey"].cloneable;
if ("cloneable" in grabbableData) {
elCloneable.checked = grabbableData.cloneable;
elCloneableGroup.style.display = elCloneable.checked ? "block": "none";
elCloneableDynamic.checked = parsedUserData["grabbableKey"].cloneDynamic ? parsedUserData["grabbableKey"].cloneDynamic : properties.dynamic;
elCloneableDynamic.checked = grabbableData.cloneDynamic ? grabbableData.cloneDynamic : properties.dynamic;
if (elCloneable.checked) {
if ("cloneLifetime" in parsedUserData["grabbableKey"]) {
elCloneableLifetime.value = parsedUserData["grabbableKey"].cloneLifetime ? parsedUserData["grabbableKey"].cloneLifetime : 300;
if ("cloneLifetime" in grabbableData) {
elCloneableLifetime.value = grabbableData.cloneLifetime ? grabbableData.cloneLifetime : 300;
}
if ("cloneLimit" in parsedUserData["grabbableKey"]) {
elCloneableLimit.value = parsedUserData["grabbableKey"].cloneLimit ? parsedUserData["grabbableKey"].cloneLimit : 0;
if ("cloneLimit" in grabbableData) {
elCloneableLimit.value = grabbableData.cloneLimit ? grabbableData.cloneLimit : 0;
}
if ("cloneAvatarEntity" in grabbableData) {
elCloneableAvatarEntity.checked = grabbableData.cloneAvatarEntity ? grabbableData.cloneAvatarEntity : false;
}
}
}
@ -1114,9 +1119,14 @@ function loaded() {
}
userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic);
});
elCloneableDynamic.addEventListener('change', function (event){
elCloneableDynamic.addEventListener('change', function(event) {
userDataChanger("grabbableKey", "cloneDynamic", event.target, elUserData, -1);
});
elCloneableAvatarEntity.addEventListener('change', function(event) {
userDataChanger("grabbableKey", "cloneAvatarEntity", event.target, elUserData, -1);
});
elCloneable.addEventListener('change', function (event) {
var checked = event.target.checked;
if (checked) {
@ -1124,6 +1134,7 @@ function loaded() {
cloneLifetime: elCloneableLifetime,
cloneLimit: elCloneableLimit,
cloneDynamic: elCloneableDynamic,
cloneAvatarEntity: elCloneableAvatarEntity,
cloneable: event.target,
grabbable: null
}, elUserData, {});
@ -1134,6 +1145,7 @@ function loaded() {
cloneLifetime: null,
cloneLimit: null,
cloneDynamic: null,
cloneAvatarEntity: null,
cloneable: false
}, elUserData, {});
elCloneableGroup.style.display = "none";

View file

@ -0,0 +1,97 @@
"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 entityIsCloneable:true, getGrabbableData:true, cloneEntity:true propsAreCloneDynamic:true */
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;
};
}
entityIsCloneable = function(props) {
if (props) {
var grabbableData = getGrabbableData(props);
return grabbableData.cloneable;
}
return false;
};
propsAreCloneDynamic = function(props) {
var cloneable = entityIsCloneable(props);
if (cloneable) {
var grabInfo = getGrabbableData(props);
if (grabInfo.cloneDynamic) {
return true;
}
}
return false;
};
cloneEntity = function(props, worldEntityProps) {
// we need all the properties, for this
var cloneableProps = Entities.getEntityProperties(props.id);
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 null;
}
cloneableProps.name = cloneableProps.name + '-clone-' + cloneableProps.id;
var lifetime = grabInfo.cloneLifetime ? grabInfo.cloneLifetime : 300;
var dynamic = grabInfo.cloneDynamic ? grabInfo.cloneDynamic : false;
var triggerable = grabInfo.triggerable ? grabInfo.triggerable : false;
var avatarEntity = grabInfo.cloneAvatarEntity ? grabInfo.cloneAvatarEntity : false;
var cUserData = Object.assign({}, JSON.parse(cloneableProps.userData));
var cProperties = Object.assign({}, cloneableProps);
delete cUserData.grabbableKey.cloneLifetime;
delete cUserData.grabbableKey.cloneable;
delete cUserData.grabbableKey.cloneDynamic;
delete cUserData.grabbableKey.cloneLimit;
delete cUserData.grabbableKey.cloneAvatarEntity;
delete cProperties.id;
cProperties.dynamic = dynamic;
cProperties.locked = false;
cUserData.grabbableKey.triggerable = triggerable;
cUserData.grabbableKey.grabbable = true;
cProperties.lifetime = lifetime;
cProperties.userData = JSON.stringify(cUserData);
var cloneID = Entities.addEntity(cProperties, avatarEntity);
return cloneID;
};

View file

@ -0,0 +1,322 @@
"use strict";
// controllerDispatcherUtils.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 Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays,
MSECS_PER_SEC:true , LEFT_HAND:true, RIGHT_HAND:true, NULL_UUID:true, AVATAR_SELF_ID:true, FORBIDDEN_GRAB_TYPES:true,
HAPTIC_PULSE_STRENGTH:true, HAPTIC_PULSE_DURATION:true, ZERO_VEC:true, ONE_VEC:true,
DEFAULT_REGISTRATION_POINT:true, INCHES_TO_METERS:true,
TRIGGER_OFF_VALUE:true,
TRIGGER_ON_VALUE:true,
PICK_MAX_DISTANCE:true,
DEFAULT_SEARCH_SPHERE_DISTANCE:true,
NEAR_GRAB_PICK_RADIUS:true,
COLORS_GRAB_SEARCHING_HALF_SQUEEZE:true,
COLORS_GRAB_SEARCHING_FULL_SQUEEZE:true,
COLORS_GRAB_DISTANCE_HOLD:true,
NEAR_GRAB_RADIUS:true,
DISPATCHER_PROPERTIES:true,
HAPTIC_PULSE_STRENGTH:true,
HAPTIC_PULSE_DURATION:true,
Entities,
makeDispatcherModuleParameters:true,
makeRunningValues:true,
enableDispatcherModule:true,
disableDispatcherModule:true,
getEnabledModuleByName:true,
getGrabbableData:true,
entityIsGrabbable:true,
entityIsDistanceGrabbable:true,
getControllerJointIndex:true,
propsArePhysical:true,
controllerDispatcherPluginsNeedSort:true,
projectOntoXYPlane:true,
projectOntoEntityXYPlane:true,
projectOntoOverlayXYPlane:true,
entityHasActions:true,
ensureDynamic:true,
findGroupParent:true
*/
MSECS_PER_SEC = 1000.0;
INCHES_TO_METERS = 1.0 / 39.3701;
HAPTIC_PULSE_STRENGTH = 1.0;
HAPTIC_PULSE_DURATION = 13.0;
ZERO_VEC = { x: 0, y: 0, z: 0 };
ONE_VEC = { x: 1, y: 1, z: 1 };
LEFT_HAND = 0;
RIGHT_HAND = 1;
NULL_UUID = "{00000000-0000-0000-0000-000000000000}";
AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}";
FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"];
HAPTIC_PULSE_STRENGTH = 1.0;
HAPTIC_PULSE_DURATION = 13.0;
DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 };
TRIGGER_OFF_VALUE = 0.1;
TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab
BUMPER_ON_VALUE = 0.5;
PICK_MAX_DISTANCE = 500; // max length of pick-ray
DEFAULT_SEARCH_SPHERE_DISTANCE = 1000; // how far from camera to search intersection?
NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing.
COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 };
COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 };
COLORS_GRAB_DISTANCE_HOLD = { red: 238, green: 75, blue: 214 };
NEAR_GRAB_RADIUS = 1.0;
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
// requiredDataForReady -- which "situation" parts this module looks at to decide if it will start
// sleepMSBetweenRuns -- how long to wait between calls to this module's "run" method
makeDispatcherModuleParameters = function (priority, activitySlots, requiredDataForReady, sleepMSBetweenRuns) {
return {
priority: priority,
activitySlots: activitySlots,
requiredDataForReady: requiredDataForReady,
sleepMSBetweenRuns: sleepMSBetweenRuns
};
};
makeRunningValues = function (active, targets, requiredDataForRun) {
return {
active: active,
targets: targets,
requiredDataForRun: requiredDataForRun
};
};
enableDispatcherModule = function (moduleName, module, priority) {
if (!controllerDispatcherPlugins) {
controllerDispatcherPlugins = {};
}
controllerDispatcherPlugins[moduleName] = module;
controllerDispatcherPluginsNeedSort = true;
};
disableDispatcherModule = function (moduleName) {
delete controllerDispatcherPlugins[moduleName];
controllerDispatcherPluginsNeedSort = true;
};
getEnabledModuleByName = function (moduleName) {
if (controllerDispatcherPlugins.hasOwnProperty(moduleName)) {
return controllerDispatcherPlugins[moduleName];
}
return null;
};
getGrabbableData = function (props) {
// look in userData for a "grabbable" key, return the value or some defaults
var grabbableData = {};
var userDataParsed = null;
try {
if (!props.userDataParsed) {
props.userDataParsed = JSON.parse(props.userData);
}
userDataParsed = props.userDataParsed;
} catch (err) {
userDataParsed = {};
}
if (userDataParsed.grabbableKey) {
grabbableData = userDataParsed.grabbableKey;
}
if (!grabbableData.hasOwnProperty("grabbable")) {
grabbableData.grabbable = true;
}
if (!grabbableData.hasOwnProperty("ignoreIK")) {
grabbableData.ignoreIK = true;
}
if (!grabbableData.hasOwnProperty("kinematic")) {
grabbableData.kinematic = true;
}
if (!grabbableData.hasOwnProperty("wantsTrigger")) {
grabbableData.wantsTrigger = false;
}
if (!grabbableData.hasOwnProperty("triggerable")) {
grabbableData.triggerable = false;
}
return grabbableData;
};
entityIsGrabbable = function (props) {
var grabbable = getGrabbableData(props).grabbable;
if (!grabbable ||
props.locked ||
FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0) {
return false;
}
return true;
};
entityIsDistanceGrabbable = function(props) {
if (!entityIsGrabbable(props)) {
return false;
}
// we can't distance-grab non-physical
var isPhysical = propsArePhysical(props);
if (!isPhysical) {
return false;
}
// XXX
// var distance = Vec3.distance(props.position, handPosition);
// this.otherGrabbingUUID = entityIsGrabbedByOther(entityID);
// if (this.otherGrabbingUUID !== null) {
// // don't distance grab something that is already grabbed.
// if (debug) {
// print("distance grab is skipping '" + props.name + "': already grabbed by another.");
// }
// return false;
// }
return true;
};
getControllerJointIndex = function (hand) {
if (HMD.isHandControllerAvailable()) {
var controllerJointIndex = -1;
if (Camera.mode === "first person") {
controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ?
"_CONTROLLER_RIGHTHAND" :
"_CONTROLLER_LEFTHAND");
} else if (Camera.mode === "third person") {
controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ?
"_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" :
"_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
}
return controllerJointIndex;
}
return MyAvatar.getJointIndex("Head");
};
propsArePhysical = function (props) {
if (!props.dynamic) {
return false;
}
var isPhysical = (props.shapeType && props.shapeType !== 'none');
return isPhysical;
};
projectOntoXYPlane = function (worldPos, position, rotation, dimensions, registrationPoint) {
var invRot = Quat.inverse(rotation);
var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, position));
var invDimensions = {
x: 1 / dimensions.x,
y: 1 / dimensions.y,
z: 1 / dimensions.z
};
var normalizedPos = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), registrationPoint);
return {
x: normalizedPos.x * dimensions.x,
y: (1 - normalizedPos.y) * dimensions.y // flip y-axis
};
};
projectOntoEntityXYPlane = function (entityID, worldPos, props) {
return projectOntoXYPlane(worldPos, props.position, props.rotation, props.dimensions, props.registrationPoint);
};
projectOntoOverlayXYPlane = function projectOntoOverlayXYPlane(overlayID, worldPos) {
var position = Overlays.getProperty(overlayID, "position");
var rotation = Overlays.getProperty(overlayID, "rotation");
var dimensions;
var dpi = Overlays.getProperty(overlayID, "dpi");
if (dpi) {
// Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale.
var resolution = Overlays.getProperty(overlayID, "resolution");
resolution.z = 1; // Circumvent divide-by-zero.
var scale = Overlays.getProperty(overlayID, "dimensions");
scale.z = 0.01; // overlay dimensions are 2D, not 3D.
dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale);
} else {
dimensions = Overlays.getProperty(overlayID, "dimensions");
if (dimensions.z) {
dimensions.z = 0.01; // overlay dimensions are 2D, not 3D.
}
}
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;
};
if (typeof module !== 'undefined') {
module.exports = {
makeDispatcherModuleParameters: makeDispatcherModuleParameters,
enableDispatcherModule: enableDispatcherModule,
disableDispatcherModule: disableDispatcherModule,
makeRunningValues: makeRunningValues,
LEFT_HAND: LEFT_HAND,
RIGHT_HAND: RIGHT_HAND,
BUMPER_ON_VALUE: BUMPER_ON_VALUE
};
}