mirror of
https://github.com/overte-org/overte.git
synced 2025-04-12 18:42:12 +02:00
Merge branch 'master' of https://github.com/highfidelity/hifi into 21542
This commit is contained in:
commit
0526d32802
33 changed files with 6371 additions and 4872 deletions
|
@ -55,6 +55,9 @@ module.exports = {
|
|||
"XMLHttpRequest": false,
|
||||
"location": false,
|
||||
"print": false,
|
||||
"RayPick": false,
|
||||
"LaserPointers": false,
|
||||
"ContextOverlay": false,
|
||||
"module": false
|
||||
},
|
||||
"rules": {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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; }
|
||||
|
|
278
scripts/developer/tests/controllerTableTest.js
Normal file
278
scripts/developer/tests/controllerTableTest.js
Normal 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
|
439
scripts/system/controllers/controllerDispatcher.js
Normal file
439
scripts/system/controllers/controllerDispatcher.js
Normal 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);
|
||||
}());
|
|
@ -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);
|
||||
}());
|
737
scripts/system/controllers/controllerModules/equipEntity.js
Normal file
737
scripts/system/controllers/controllerModules/equipEntity.js
Normal 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);
|
||||
}());
|
|
@ -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);
|
||||
}());
|
234
scripts/system/controllers/controllerModules/farTrigger.js
Normal file
234
scripts/system/controllers/controllerModules/farTrigger.js
Normal 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);
|
||||
}());
|
265
scripts/system/controllers/controllerModules/inEditMode.js
Normal file
265
scripts/system/controllers/controllerModules/inEditMode.js
Normal 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);
|
||||
}());
|
|
@ -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);
|
||||
}());
|
|
@ -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);
|
||||
}());
|
|
@ -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);
|
||||
}());
|
116
scripts/system/controllers/controllerModules/nearTrigger.js
Normal file
116
scripts/system/controllers/controllerModules/nearTrigger.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
"use strict";
|
||||
|
||||
// nearTrigger.js
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
|
||||
/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND,
|
||||
enableDispatcherModule, disableDispatcherModule, getGrabbableData, Vec3,
|
||||
TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, makeRunningValues, NEAR_GRAB_RADIUS
|
||||
*/
|
||||
|
||||
Script.include("/~/system/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);
|
||||
}());
|
|
@ -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);
|
||||
}());
|
83
scripts/system/controllers/controllerModules/scaleAvatar.js
Normal file
83
scripts/system/controllers/controllerModules/scaleAvatar.js
Normal 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");
|
||||
};
|
||||
})();
|
|
@ -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);
|
||||
}());
|
522
scripts/system/controllers/controllerModules/teleport.js
Normal file
522
scripts/system/controllers/controllerModules/teleport.js
Normal 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
|
|
@ -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);
|
||||
|
||||
}());
|
|
@ -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
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
|
|
97
scripts/system/libraries/cloneEntityUtils.js
Normal file
97
scripts/system/libraries/cloneEntityUtils.js
Normal 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;
|
||||
};
|
322
scripts/system/libraries/controllerDispatcherUtils.js
Normal file
322
scripts/system/libraries/controllerDispatcherUtils.js
Normal 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
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue